[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M69BGM8PXJ7614X2HVKZPD9C":3,"skill-same:01M69BGM8PXJ7614X2HVKZPD9C":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":33,"linkOnly":34,"markdownContent":35,"markdownBody":36,"frontMatter":37,"refName":51,"commitSha":52,"skillDirectory":53,"forksCount":54,"repositoryHtmlUrl":55,"repositoryHomepageUrl":6,"ownerHtmlUrl":56,"ownerBlogUrl":57,"ownerDescription":58,"updatedAt":59,"install":60,"resources":73},"01M69BGM8PXJ7614X2HVKZPD9C","context-intelligence-graph-query",null,"Use when querying the context-intelligence property graph for session history, tool call traces, LLM iteration analysis, execution scale metrics, agent delegation trees, skill loading, and recipe orchestration. Covers all graph layers, cross-layer SOURCED_FROM joins, SST navigation, blob handling, and verified Cypher patterns.","microsoft\u002Famplifier-bundle-context-intelligence","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M69BGBVCCWXG1MNNF1E8TCWN",5,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fcontext-intelligence-graph-query\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32],"context intelligence graph query","sessions","valid","http","orchestration","run","merge","directory","safe","2026-11-10 10:12:30",false,"---\nname: context-intelligence-graph-query\ndescription: >\n  Use when querying the context-intelligence property graph for session history,\n  tool call traces, LLM iteration analysis, execution scale metrics, agent\n  delegation trees, skill loading, and recipe orchestration. Covers all graph\n  layers, cross-layer SOURCED_FROM joins, SST navigation, blob handling, and\n  verified Cypher patterns.\nlicense: MIT\nvisibility:\n  priority: 5\n  summary: Query the context-intelligence property graph for session history, tool traces, delegation trees, skills, and recipe orchestration via verified Cypher patterns.\nmetadata:\n  version: \"2.6.0\"\n  changelog:\n    - \"2.6.0: Absorbed the former standalone `blob-reading` skill as the level-3 reference\n      `blob-reading.md` beside this file (same content, verbatim), and pointed Section 6 at it.\n      The catalog now carries one always-visible entry for graph work instead of two; nothing was\n      dropped - the full blob contract (when not to resolve, the four-step resolution, blob_read's\n      success\u002Ffailure shapes and which errors carry a citable `source`, the safe jq patterns,\n      blob-vs-inline detection) moved intact into the reference file.\"\n    - \"2.5.0: Elevated the anti-multi-count guidance scattered across Trap 6 (cost) and Trap 8\n      (tokens) into a single named cross-cutting rule (Section 3: \\\"Rule \\u2014 one owner per numeric\n      fact: never multi-count cost or tokens by over-reading events\\\") \\u2014 every usage value is\n      emitted once per LLM turn, owned by :LlmResponseEvent, and echoed onto ContentBlockEndEvent \u002F\n      ProviderResponseEvent; aggregating across the wider set sums it 2\\u20133x over. Trimmed Trap 6\n      and Trap 8 to reference the rule instead of each restating the mechanism; both traps keep\n      only their field-specific specifics (cost_usd is a STRING requiring toFloat; token\u002Fcache\n      fields are INTEGER with the correct cache field names). Section 8 queries unchanged.\"\n    - \"2.4.0: Added Trap 8 (token & cache usage fields on :LlmResponseEvent are payload INTEGERs — unlike cost_usd's STRING — but still require pinning to :LlmResponseEvent; an unpinned :Event sweep replays the same usage map via ContentBlockEndEvent\u002FProviderResponseEvent and inflates every total by ~3.4x, measured: cost 3.43x, input 3.31x, output 3.63x, cache 3.39x) with a validated combined cost+token aggregation and a per-model breakdown pattern in Section 8. Documented the correct cache field names (cache_read_input_tokens, cache_creation_input_tokens — complete) versus the sparse\u002Fwrong-typed alternatives (cache_read_tokens, cache_creation), and reinforced that Iteration.usage_input\u002Fusage_output\u002Fusage_cache_write (Trap 7) must never be used for token or cost aggregation — they are last-write-wins corrupted and Iteration carries no cost field at all.\"\n    - \"2.3.0: Corrected the working_dir population figure from an inaccurate ~7% (238\u002F3,259 sessions) down to its true measured value: ~0% queryable in-graph (0 of 4,504 \u002F 0 of 9,086 sessions — not a lifted Session property); workspace remains ~100% and the only valid scoping lever. Added Trap 6 (cost_usd totals: a JSON-string payload field, not a lifted property; a bare sum() returns HTTP 500; must pin to :LlmResponseEvent to avoid triple-counting against ContentBlockEndEvent \u002F ProviderResponseEvent) with a validated APOC aggregation pattern in Section 8. Added Trap 7 (Iteration.node_id MERGEs across orchestrator runs, corrupting iteration-scoped counts\u002Faggregates by up to +66%) with caveats on every affected iteration-based query pattern. Added a validated temporal-join pattern (Section 7) for per-prompt\u002Fper-run tool-call attribution — more accurate than the structural run→Iteration→ToolCall path and unaffected by the Trap 7 MERGE bug.\"\n    - \"2.2.0: Corrected the Section 2 workspace\u002Fworking_dir claims — workspace is a lifted, reliable (~100%) scoping slug, not the literal working directory; working_dir is ~0% queryable in-graph (0 of 4,504 \u002F 0 of 9,086 sessions — not a lifted Session property) payload-only field with no canonical per-session source, and must never be used to scope a population. Consolidated one visible+correct in-graph APOC payload-parse pattern: dot-access on a JSON-string field (e.g. e.data.working_dir) raises a type error, not a silent null — parse in-graph with apoc.convert.fromJsonMap and return only the scalar needed. De-duplicated the Section 6 copy into a pointer at this pattern.\"\n    - \"2.1.0: Reporting guidance — agent-level \u002F self-delegation answers must STATE the resolved self→actor breakdown (root\u002Fmain vs named) as an explicit standalone finding, not fold it into a blended statistic. Tightened the Section 8 agent-rollup: project Session\u002FFORKED (the labels that exist) and roll up by the Session.agent property; agents are NOT a projectable Agent graph.\"\n    - \"2.0.1: Schema accuracy fix: corrected Prompt\u002FSkillLoad\u002FOrchestrator property names, RecipeStep\u002FRecipeRun property placement, RecipeStep→RecipeRun edge (SPAWNED), removed phantom L1 edges, fixed example queries — all validated against the live graph.\"\n---\n\n## When the graph server is not configured\n\nIf the context-intelligence server is unavailable or not configured for this\nsession, graph-based analysis cannot proceed here. This limits graph work, not\nall session evidence: conversation uses `session_transcript` when mounted, and\nlocal event, tool, error, or capture questions use `session-navigator` without a\ngraph health check. Delegate only when permitted and available; a specialist\nperforms its matching work itself, never delegates the same task to itself.\nUse authorized local fallback only when it can sufficiently answer the question;\notherwise report the specific gap and any partial coverage. A failed query or\ntimeout is not proof the server is down, and a graph miss is not proof of absence\nin local captures or other evidence layers. Do not attempt Cypher against a\nserver known to be unreachable.\n\n---\n\n# Context Intelligence Graph Query\n\nThis skill equips you to navigate and extract insights from the context-intelligence\nproperty graph using the `graph_query` tool. The graph holds indexed session\nevidence — what happened, when, how things connect, and at what scale — only to\nthe extent capture, forwarding, and indexing have supplied it. State the selected\nsource, requested scope, observed coverage, and freshness rather than assuming a\ncomplete record of every Amplifier session.\n\n**Read the live schema from the server; read the meaning from this skill.** The\nserver can always tell you the current shapes — labels, relationship types, property\nnames. It CANNOT tell you what those shapes mean, where they lie about their meaning,\nor how to scope, join, and bound a query safely. This skill deliberately does not\nfreeze a schema dictionary (it goes stale and misleads). It keeps the grammar: the\nscoping levers, the traps, the cross-layer joins, and the verified patterns.\n\n---\n\n## Section 1 — Introspect the Live Schema First\n\nBefore writing analytical Cypher against an unfamiliar area of the graph, ask the\nserver what it actually contains. These calls are cheap and always current.\n\n```cypher\nCALL db.labels()                          \u002F\u002F every node label present\nCALL db.relationshipTypes()               \u002F\u002F every edge type present\nCALL db.schema.nodeTypeProperties()       \u002F\u002F property names + types per label\nCALL db.schema.visualization()            \u002F\u002F label\u002Fedge connectivity overview\n```\n\nTo learn the real properties on a specific label (names AND which are populated),\nsample a few live nodes rather than trusting any documented list:\n\n```cypher\nMATCH (n:ToolCall) WITH n LIMIT 25\nUNWIND keys(n) AS k\nRETURN k AS property, count(*) AS present_on_n_nodes\nORDER BY present_on_n_nodes DESC\n```\n\n**The three-layer shape (stable orientation, not a dictionary):**\n\n- **Data layer 1 — raw events.** Every kernel event preserved as an `:Event` node\n  (`:ToolPreEvent`, `:LlmResponseEvent`, `:PromptSubmitEvent`, …). Answers *what\n  happened and when*. `node_id` uses a `__` (double-underscore) separator.\n- **Data layer 2 — semantic entities.** Events assembled into `Session`,\n  `OrchestratorRun`, `Iteration`, `ContentBlock`, `ToolCall`, `Prompt`, and more,\n  connected by typed relationships. Answers *what ran, how, at what scale*. `node_id`\n  uses a `::` (double-colon) separator (a few types — `ToolCall`, concept nodes — use\n  a bare identifier).\n- **Foundation layer — above the kernel.** Delegation trees (`Delegation`, `Agent`),\n  skill loads (`SkillLoad`), recipe orchestration (`RecipeRun`, `RecipeStep`,\n  `Recipe`).\n\nAll layers coexist in one graph, bridged by **`SOURCED_FROM`** edges: every layer-2\nentity points back to the layer-1 event(s) that produced it (Section 4). Use\n`db.schema.nodeTypeProperties()` to confirm exact property names before you rely on\nthem — the sections below cover only the meaning the server cannot self-describe.\n\n### Reading results & citing the source (required)\n\n`graph_query` returns `{\"source\": {\"name\", \"url\", \"origin\"}, \"rows\": [...]}` on\nsuccess. Read your data from `rows`. ALWAYS report `source.name` (its `origin` is\n`source`, `destination`, or `env`) in your answer — the user must know which\nendpoint the outcome came from.\n\nOn **failure**, `source` is present only when an endpoint was actually chosen:\n**endpoint-level** errors (`connection_error`, `timeout`, `http_status`,\n`decode_error`, and input-validation errors like a missing\u002Finvalid `query` that\noccur *after* selection) carry `error.source` — cite it. **Selection\u002Fconfig**\nerrors that occur *before* any endpoint is chosen (`ambiguous_source_selection`,\n`unknown_source`, `source_misconfigured`, `configuration_error`) have **no**\n`source` — there is no single endpoint to name, so report that selection failed\nrather than inventing one.\n\nTo discover what you can reach, call `graph_query` with `list_sources: true` — it\nreturns the connectable set (`{\"connectable_set\": [{name, url, origin}, ...]}`)\nwithout running a query. To target a specific endpoint (a configured read source\nOR a hook upload destination), pass `source: \"\u003Cname>\"`.\n\n---\n\n## Section 2 — Scoping (Mandatory, and Not Self-Describing)\n\nScoping is the single most error-prone part of querying this graph, and the server\ncannot tell you how its client scopes. Get this right first.\n\n### The `workspace` tool argument — NOT a Cypher param\n\n**The `graph_query` tool reads a top-level `workspace` argument.** It injects that\nvalue as `$workspace` into your Cypher. Do **not** try to set the workspace through\n`params: {\"workspace\": ...}` — the tool does not read the workspace from `params`, and\na mis-set workspace silently scopes to a tiny `\"default\"` decoy partition instead of\nthe real one. If your counts look implausibly small (a handful of sessions where you\nexpected hundreds), you are almost certainly scoped to the decoy.\n\n```cypher\n\u002F\u002F Correct: reference $workspace; set the workspace via the tool's top-level\n\u002F\u002F `workspace` argument, not via params.\nMATCH (s:Session {workspace: $workspace})\nRETURN count(s) AS session_count\n```\n\n### The two real scoping levers\n\nBoth live as first-class properties on nodes — confirm them by sampling:\n\n- **`created_by`** — *who* produced the data (present on every node; the data is\n  typically single-user).\n- **`workspace`** — *where*: a **lifted, dash-slugified label\u002Fslug** (e.g.\n  `-mnt-linuxdata-workspaces-...-dashboard-ui`), reliable at ~100% coverage. It is a **label,\n  not the literal working directory**: the true directory lives in the event `data` payload as\n  `working_dir`, and the **same `working_dir`** (`\u002Fhome\u002Fuser\u002Fproject`) can map to **different**\n  `workspace` slugs (`myproject`, `proj`, `multi`). Treat `workspace` as the reliable scoping\n  slug; treat `working_dir` as a **payload field, ~0% queryable in-graph (0 of 4,504 \u002F 0 of\n  9,086 sessions — not a lifted Session property)** with no canonical per-session source — read\n  it via the APOC parse in Section 8, but do **not** scope by it. The `\u002Fcypher` request's own\n  `workspace` scope is a second, orthogonal server-side filter layered on top.\n\n**Always anchor scope on the first `MATCH`** so the workspace index is used:\n\n```cypher\n\u002F\u002F CORRECT — workspace on the anchor node\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(run:OrchestratorRun)\nRETURN run.orchestrator_name, run.started_at\n\n\u002F\u002F INCORRECT — workspace on a downstream node misses the index\nMATCH (s:Session {node_id: $session_id})\n      -[:HAS_EXECUTION]->(run:OrchestratorRun {workspace: $workspace})\nRETURN run.orchestrator_name, run.started_at\n```\n\nTo discover which workspaces exist before scoping, aggregate across all of them\n(use sparingly — it skips the partition index):\n\n```cypher\nMATCH (s:Session)\nWHERE s.workspace \u003C> ''\nRETURN s.workspace, s.created_by, count(s) AS sessions\nORDER BY sessions DESC\nLIMIT 25\n```\n\n### Resolving \"my\" — use `whoami`, don't guess\n\n`created_by` scopes by *who produced the data*, but the graph never tells you\nwho \"you\" are. When a request is scoped to the acting user themselves —\n\"my sessions\", \"sessions I created\", \"what have I been working on\" — call the\n`whoami` tool for the relevant server before filtering on `created_by`:\n\n```python\nwhoami()                       # default endpoint\nwhoami(source=\"prod\")          # a specific configured endpoint\n```\n\nIt returns `{\"contributor_id\": \"\u003Cgithub-id-or-null>\", \"source\": {...}}`. Use\n`contributor_id` as the value to match against `created_by`:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, created_by: $contributor_id})\nRETURN count(s) AS my_sessions\n```\n\n**Null case.** A null `contributor_id` means auth is disabled (or otherwise\nunknown) on that server — there is no acting-user identity to filter by. Do\n**not** guess or fall back to some other field. Ask the user how they want to\nscope the query instead (e.g. by name, by workspace, or \"show everyone's\").\n\n---\n\n## Section 3 — Traps: Where the Shapes Lie About Their Meaning\n\nThese are the highest-value items in this skill. Each is a place where reading the\nschema literally leads you to a **wrong answer that looks rigorous**. The server will\nhappily hand you the misleading shape; only domain knowledge corrects it.\n\n### Trap 1 — Delegation lineage: use `FORKED` + `Session.parent_id`, not the edge walk\n\nThe true delegation\u002Ffork lineage is carried by the **`FORKED` edge** together with\n**`Session.parent_id`**. This is the authoritative source for delegation depth and\nancestry. Verified live: the fork lineage reaches **max depth 3**.\n\n```cypher\n\u002F\u002F Authoritative lineage depth via the FORKED edge.\n\u002F\u002F Bound the path (never bare `*` — Section 7): *1..20 is generous headroom over the\n\u002F\u002F observed max depth of 3, and the RETURN is aggregate-only (no rows materialized).\nMATCH path = (root:Session {workspace: $workspace})-[:FORKED*1..20]->(leaf:Session)\nWHERE root.parent_id IS NULL\nRETURN max(length(path)) AS max_fork_depth, count(path) AS lineages\n```\n\n`Session.parent_id` lets you reconstruct the same chain by property when an edge walk\nis inconvenient (each child names its parent):\n\n```cypher\nMATCH (child:Session {workspace: $workspace})\nWHERE child.parent_id IS NOT NULL\nRETURN child.node_id, child.parent_id\nORDER BY child.node_id LIMIT 200   \u002F\u002F bounded (Section 7); anchor on a node_id to walk one specific chain\n```\n\n**Two known undercounts — do NOT trust either for depth:**\n\n- **`HAS_SUBSESSION` is a single hop only.** It materializes only the immediate\n  parent→child link; each sub-session is ingested as its own root, so a multi-hop walk\n  `(:Session)-[:HAS_SUBSESSION*]->()` collapses to depth 1. It is a one-level lookup,\n  **not** the delegation tree.\n- **The `Delegation` property-join (`sub_session_id → parent_session_id`) undercounts\n  beyond hop 1.** It is more complete than the `HAS_SUBSESSION` walk (it finds a second\n  hop), which is exactly why it is dangerous: it *looks* like the answer and reports\n  depth 2, but it still misses the deeper lineage that `FORKED`\u002F`parent_id` reveals.\n  Use it to enumerate individual delegations, not to measure tree depth.\n\n### Trap 2 — `context_depth` \u002F `context_scope` are inheritance modes, NOT tree depth\n\nOn `Delegation` nodes, `context_depth` and `context_scope` describe **how much parent\ncontext a delegation inherited** — an inheritance mode with values like `none`,\n`recent`, `all`. The name invites reading it as nesting\u002Ftree depth. It is not.\nSelf-delegations are almost always `none`; regular delegations spread across\n`none` \u002F `recent` \u002F `all`.\n\n### Trap 3 — Tool data lives on `ToolCall`, not `ToolExecution`\n\n`:ToolCall` carries the tool data you want: `tool_name`, `tool_input`, `started_at`,\n`ended_at`, `result_success`, `result_error`, `result_output`. `:ToolExecution` is\n**property-less — a dead end**; do not route tool queries through it. `ToolCall` is the\ncorrect, simple source (verified: `tool_name` and `started_at` populated on 100% of\n`ToolCall` nodes in a real workspace).\n\n```cypher\nMATCH (t:ToolCall {workspace: $workspace})\nRETURN t.tool_name AS tool, count(*) AS n\nORDER BY n DESC LIMIT 12\n```\n\n### Trap 4 — `self` is a self-delegation marker, never a real agent\n\nSelf-delegation is recorded explicitly as **`Delegation.is_self_delegation = true`**\n(equivalently the child `Session.agent = 'self'`). Never infer it by string-matching\n`tool_input` or comparing names.\n\n**`'self'` is a marker, not an identity** — it means \"this session spawned a fresh\nsub-context of *itself*,\" so `self` is never the real actor. Before any per-agent stat\n(centrality, counts, durations), **resolve it up the fork chain** to the nearest ancestor\nwhose `agent \u003C> 'self'`. That actor is often the **root\u002Fmain session** (`agent IS NULL` —\nthe user's top-level session, not a named sub-agent); occasionally a named agent that\nself-delegated. Empirically the chain is a single hop (no `self`→`self`→…).\n\n```cypher\n\u002F\u002F Resolve every self-session to the actor that actually spawned it\nMATCH (s:Session {workspace: $workspace, agent: 'self'})\nOPTIONAL MATCH pth = (anc:Session)-[:FORKED*1..20]->(s)\nWHERE anc.agent IS NULL OR anc.agent \u003C> 'self'\nWITH s, anc, pth ORDER BY length(pth) ASC\nWITH s, head(collect(coalesce(anc.agent, 'root\u002Fmain session'))) AS actor\nRETURN coalesce(actor, 'unresolved') AS real_actor, count(*) AS self_delegations\nORDER BY self_delegations DESC\n```\n\nSelf-delegation is itself a **first-class signal** (an actor extending its own context —\ncontinuation \u002F context management): count it *as* self-delegation when that is the\nquestion, and *resolve* it when you need the real actor. A raw `self` bucket in a\nper-agent aggregate is a bug — it attributes real work to a non-existent agent.\n\n**Report the resolved breakdown as an explicit finding — do not bury it in a blended\nstat.** The resolve-self join above only pays off if its result reaches the answer as\nits *own stated fact*. For any agent-level or self-delegation question, **lead with the\nresolved `self`→actor split as a standalone sentence** — how many, and what percent, of\nself-delegations resolve to the **root\u002Fmain session continuing itself** (`agent IS NULL`)\nversus to **named agents** — *before* any roll-up or combined figure. Computing the\nnumber correctly but folding it into a mixed \"X% of all delegations are root-launched\"\nstatistic hides the very fact the question asked for. This is a general output-shape\nprinciple: surface the load-bearing breakdown first, then add supporting context — state\nit, don't just compute it.\n\n```cypher\n\u002F\u002F The plain self-delegation split (the signal itself)\nMATCH (d:Delegation {workspace: $workspace})\nRETURN d.is_self_delegation AS self_delegation, count(*) AS n\nORDER BY n DESC\n```\n\n### Trap 5 — Timestamps are ZONED DATETIME; durations are computed\n\nEvery `*_at` property (`started_at`, `ended_at`, `occurred_at`, …) and `last_updated`\n— on nodes and on the edges that carry `occurred_at` (`HAS_EVENT`, `HAS_SUBSESSION`,\n`FORKED`) — is stored as a native Neo4j **`ZONED DATETIME`**, not a string.\n\n- ❌ `WHERE s.started_at > '2026-05-01'` — comparing to a string literal silently\n  returns nothing (no error raised).\n- ✅ `WHERE s.started_at > datetime('2026-05-01')` — wrap every literal in `datetime()`.\n- ✅ Rolling windows: `WHERE s.started_at > datetime() - duration('P30D')`.\n\n**Durations are not stored — compute them** with `duration.between()`:\n\n```cypher\nMATCH (t:ToolCall {workspace: $workspace})\nWHERE t.started_at IS NOT NULL AND t.ended_at IS NOT NULL\nWITH t, duration.between(t.started_at, t.ended_at) AS d\nRETURN t.tool_name AS tool, d.seconds AS secs\nORDER BY secs DESC LIMIT 8\n```\n\n### Rule — one owner per numeric fact: never multi-count cost or tokens by over-reading events\n\nThis is a cross-cutting rule, not a single trap — Traps 6 and 8 below are both instances of it.\n\nEvery numeric usage value — dollars (`cost_usd`) and every token field (`input_tokens`,\n`output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`) — is emitted **once**\nper LLM turn and is **owned** by the `:LlmResponseEvent` node. The identical `usage` map is then\n**echoed** onto other events of the same turn: `ContentBlockEndEvent` (streaming replays it ~2.33x\nper response) and `ProviderResponseEvent` (a third, lower-layer copy).\n\n**Consequence:** any aggregation that reads a set *wider* than the owning label — an unpinned\n`:Event` \u002F `:SST_EVENT` sweep, or a cross-layer join through `ContentBlockEndEvent` \u002F\n`ProviderResponseEvent` — sums the same dollars and tokens 2–3x over. This is **over-reading the\nevents**, not a rounding error. Measured inflation ~3.4x on the production graph (a true ~$45.5K\nreads as a phantom ~$156K); reproduced on an isolated DTU at 2.1x–3.9x across fields.\n\n**The rule, stated generally:** a numeric fact has exactly **one owning event layer** — aggregate\nit only from that layer. For LLM cost and tokens, the owner is `:LlmResponseEvent`. Concretely:\nalways `MATCH (e:LlmResponseEvent)`; never `sum()` a usage field across a broader label set, and\nnever reach the same figure through a cross-layer join. If a numeric value appears on more than one\nlabel, those are **copies, not addends** — pick the owner. This applies identically to dollars and\ntokens, and to any future numeric usage field added to the payload.\n\n### Trap 6 — `cost_usd` is a JSON string nested in the payload, not a lifted property\n\nAn instance of the **Rule** above — for cost, the owner is `:LlmResponseEvent`. The trap-specific\nwrinkle here is typing, not scoping:\n\nThere is **no top-level lifted cost property** anywhere in the graph. The real value lives at\n`data.usage.cost_usd` on `:LlmResponseEvent`, serialized as a **JSON string** — not a number, and\nnot a top-level field. A bare `sum(e.cost_usd)` or `sum(e.data.usage.cost_usd)` **returns HTTP\n500** (Section 6: `data` is a string, dot-access into it type-errors; the nested numeric value also\narrives as a JSON string, not a Cypher number, so it must be parsed *and* cast with `toFloat()`).\n\n**Validated** (returns `$44,886.32` over 63,758 events):\n\n```cypher\nMATCH (e:LlmResponseEvent) WHERE e.data CONTAINS 'cost_usd'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN sum(toFloat(d.usage.cost_usd)) AS total_cost_usd\n```\n\nSee Section 8 for the full worked pattern.\n\n### Trap 7 — `Iteration.node_id` MERGEs across orchestrator runs\n\n`Iteration.node_id` is composed as `\"{session_id}::iteration::{N}\"` — but `N` (`iteration_number`)\n**restarts at 1 for every `OrchestratorRun`**, not once per session. Because the MERGE key omits\nthe run, **one `Iteration` node absorbs iterations from every run that reused the same number** —\nobserved up to **15 run parents on a single `Iteration` node**.\n\nThis corrupts anything that aggregates through `Iteration`:\n\n- **Last-write-wins property garbage** on `usage_input`, `usage_output`, `usage_cache_write`, and\n  `message_count` — whichever run's ingest landed last overwrites the others' values.\n- **`count(DISTINCT Iteration)` undercounts** — iterations from different runs that share a number\n  collapse into one node.\n- **`run → Iteration → ToolCall` traversals overcount tool calls by +1.1% to +66%** — a `ToolCall`\n  reached through the merged `Iteration` looks like it belongs to every run that touches that node.\n\n**Every query pattern below that groups or counts through `Iteration` inherits this bug** — see the\ncaveat noted at each: Section 4's `SOURCED_FROM` example, and Section 7's \"Full conversation turn\ntrace,\" \"Tool usage per iteration,\" \"Failed tool calls,\" and \"Skills active per iteration.\"\n\n**What to do instead:**\n1. For **tool-call attribution** to a prompt or run, use the **temporal join** pattern in Section 7\n   — it never touches `Iteration` and is unaffected by this bug.\n2. For **token or cost counts**, never trust `Iteration.usage_*` \u002F `message_count` — recompute\n   directly from `LlmResponseEvent` payloads using the validated Trap 8 \u002F Section 8 aggregation\n   pattern (`Iteration` carries no cost field at all, and its token counters are last-write-wins\n   corrupted).\n\n**Trust marker:** a bare `node_id` of the form `\"{session_id}::iteration::{N}\"` with no\norchestrator-run segment is **pre-fix data — suspect**. A `node_id` that additionally scopes to\nthe run (i.e. the run is part of the key, not just `N`) is **post-fix — trustworthy**. Check the\nliteral `node_id` shape before trusting an `Iteration` aggregate.\n\n### Trap 8 — Token & cache usage: payload integers, still owned by `:LlmResponseEvent`\n\nAn instance of the **Rule** above (before Trap 6) — for tokens, the owner is also\n`:LlmResponseEvent`, exactly as for cost. Token and cache-usage figures live at\n`data.usage.{input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens}`\non `:LlmResponseEvent`, parsed the same way as cost (Trap 6) via `apoc.convert.fromJsonMap(e.data)`.\nThe trap-specific wrinkle is the opposite typing quirk:\n\n- **Typing is the opposite of `cost_usd`.** The token fields deserialize as **INTEGER**, not a\n  JSON string — `toInteger()` on them is defensive\u002Foptional, not required. `cost_usd` (Trap 6) is\n  the **STRING** one; `toFloat()` there is mandatory. Don't let the cost pattern's mandatory cast\n  make you assume tokens need the same cast for the same reason — they don't need it to avoid an\n  error, only to normalize type. Correct typing does **not** protect you from wrong scoping —\n  validated inflation when the Rule is violated: cost 3.43x, input tokens 3.31x, output tokens\n  3.63x, cache tokens 3.39x.\n- **Use the complete cache fields, not the sparse\u002Fwrong-typed ones.** `cache_read_input_tokens` and\n  `cache_creation_input_tokens` are complete. Do **not** sum `cache_read_tokens` (sparse — NULL on\n  many events) or `cache_creation` (a **MAP**, not a number — summing it type-errors or silently\n  drops). `cache_write_tokens` is close to `cache_creation_input_tokens` but diverges ~1.5%; prefer\n  `cache_creation_input_tokens` as the validated figure.\n- **Never aggregate from `Iteration`.** `Iteration.usage_input` \u002F `usage_output` \u002F\n  `usage_cache_write` are the same Trap 7 last-write-wins corruption (undercounts 10–14%), and\n  `Iteration` carries **no cost field at all** — there is no `Iteration`-based cost figure to fall\n  back to even approximately. Recompute both tokens and cost from `LlmResponseEvent` payloads.\n- **NULL and zero-cost caveat.** ~4% of events have a null `cost_usd` (`sum()` ignores nulls, so the\n  total stays correct — just don't expect `count(*)` to equal the events actually priced). Some\n  models\u002Fproviders are non-priced and report `$0` cost with real, non-zero token counts. Always\n  report `llm_calls` and the token columns alongside cost so this gap is visible rather than hidden\n  inside a dollar figure.\n\nSee Section 8 for the validated combined cost+token aggregation and per-model breakdown.\n\n---\n\n## Section 4 — Cross-Layer Joins (SOURCED_FROM)\n\nData layer 1 (raw events) and data layer 2 (semantic entities) coexist in one graph.\nThe canonical bridge is the **`SOURCED_FROM`** edge: every layer-2 entity points to the\nlayer-1 event(s) that produced it. Use layer 1 for exact raw fields\u002Ftimeline; use\nlayer 2 for structure, scale, and causation; move between them with `SOURCED_FROM`.\n\n```cypher\n\u002F\u002F Semantic ToolCall → its originating raw event (canonical)\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nMATCH (tc)-[:SOURCED_FROM]->(pre:ToolPreEvent)\nRETURN iter.iteration_number AS iteration,\n       tc.tool_name          AS tool,\n       tc.result_success     AS succeeded,\n       pre.occurred_at       AS event_fired_at\nORDER BY pre.occurred_at\nLIMIT 25\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` here can be a cross-run merged node (up to 15 run\n> parents observed) — do not treat this join as an authoritative per-run tool-call count. For\n> per-prompt\u002Fper-run attribution, prefer the temporal join pattern in Section 7.\n\n**Fallback joins** (only when `SOURCED_FROM` is absent — older sessions ingested before\nthe handler existed; check with the query in Gotcha 5):\n\n- **ToolCall direct match** — `:ToolCall.node_id` is the provider's `tool_call_id`,\n  which `:ToolPreEvent` also lifts as `tool_call_id`. Join on that shared key. Works\n  for `ToolCall` only.\n\n  ```cypher\n  MATCH (e:ToolPreEvent {workspace: $workspace, tool_call_id: $tool_call_id})\n  MATCH (tc:ToolCall {node_id: e.tool_call_id})\n  RETURN e.tool_name, e.occurred_at, tc.result_success, tc.result_output\n  ```\n\n- **Session containment** — join through the shared `:Session`: layer 1 attaches raw\n  events via `HAS_EVENT`; layer 2 attaches entities via `HAS_EXECUTION`\u002F`HAS_PART`.\n  Correlate on a shared scalar (e.g. `iteration_number`).\n\n---\n\n## Section 5 — SST Navigation (Reasoning by Semantic Type)\n\nLayer-2 nodes carry an **SST type label** that classifies each node by its fundamental\ncharacter, letting you query across entity boundaries without knowing specific labels\nin advance. Confirm the current partition with `db.labels()`; the meaning is:\n\n- **`:SST_EVENT`** — a temporal, bounded occurrence (Session, OrchestratorRun,\n  Iteration, ContentBlock, ToolCall, Prompt, Delegation, SkillLoad, RecipeRun, …).\n- **`:SST_THING`** — a persistent resource\u002Fartifact (e.g. MountPlan).\n- **`:SST_CONCEPT`** — an abstract, reusable identity (Orchestrator, Agent, Recipe).\n\nLayer-2 edges carry an **`sst_semantic`** property expressing the abstract relationship\nindependent of concrete edge type: `CONTAINS` (containment), `LEADS_TO` (causal\u002F\nsequential), `EXPRESSES` (attribution), `NEAR` (concurrency). This lets you query\ncausation or containment uniformly.\n\n```cypher\n\u002F\u002F All temporal events in the most recent session, mixed types sorted correctly\nMATCH (s:Session {workspace: $workspace})\nWITH s ORDER BY s.started_at DESC LIMIT 1\nMATCH (s)-[:HAS_EXECUTION|HAS_PART*1..3]->(e:SST_EVENT)\nRETURN labels(e) AS types,\n       coalesce(e.started_at, e.occurred_at) AS at\nORDER BY at\nLIMIT 50\n```\n\n> **Two temporal keys across `:SST_EVENT`:** span nodes (Iteration, ContentBlock,\n> ToolCall, Session, OrchestratorRun) store `started_at`; occurrence nodes (Prompt,\n> Cancellation, ContextCompaction) store `occurred_at`. When you sweep a mixed set,\n> use `coalesce(e.started_at, e.occurred_at)` in both projection and `ORDER BY` so\n> occurrence nodes are not silently null and sorted out of order.\n\n**Hierarchy traversal** — reach the full Session → Run → Iteration → ContentBlock tree\nwith a bounded variable-length path. Bound it (`*1..3`) to prevent runaway fanout:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION|HAS_PART*1..3]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nRETURN tc.tool_name, tc.started_at, tc.result_success\nORDER BY tc.started_at\nLIMIT 50\n```\n\n---\n\n## Section 6 — Text Extraction: Bounded Always, Blobs Carefully\n\nLarge text fields are the fastest way to destroy your context window. **One unbounded\n`p.prompt` query returned 1.21M tokens and overflowed the context.** Treat every large\ntext field with discipline.\n\n### Firm rule — never return full text of many rows\n\nFor any large text field (`prompt`, `data`, `data.content`, `tool_input`,\n`result_output`), you MUST bound it — no exceptions:\n\n- Add a small `LIMIT`, **and**\n- Truncate with `substring(field, 0, 200)`, or return only `size(field)` to gauge\n  scale first, or aggregate (`count`, `collect` of a truncated projection).\n\n```cypher\n\u002F\u002F WRONG — full prompt text over many rows → context overflow\nMATCH (p:Prompt {workspace: $workspace}) RETURN p.prompt\n\n\u002F\u002F RIGHT — measure scale first\nMATCH (p:Prompt {workspace: $workspace})\nRETURN count(p) AS n, max(size(p.prompt)) AS longest_chars\n\n\u002F\u002F RIGHT — bounded preview when you do need text\nMATCH (p:Prompt {workspace: $workspace})\nWHERE p.session_id = $session_id\nRETURN substring(p.prompt, 0, 200) AS prompt_preview\nORDER BY p.occurred_at\nLIMIT 25\n```\n\n### The `data` field is a JSON string, not a Cypher map\n\n`:Event.data` is a serialized JSON string. Dot notation (`e.data.tool_name`) does **not**\nwork in Cypher — it raises a type-mismatch error, not a silent null. Prefer **lifted\nproperties** (`tool_name`, `tool_call_id`, `model`, `provider`, …) when they exist; when you\nneed a raw field that is **only** in the payload, parse it **in-graph with APOC** — see the\none authoritative pattern **\"Lift payload fields in-graph with APOC\" in Section 8**. Always bounded.\n\n**Inline vs blob — know which you have.** Some payloads live inline; some are offloaded.\nFor example, orchestrator steering text lives **inline** at\n`OrchestratorSteeringInjectedEvent.data.content` (a JSON string — parse it, no blob\nhop). Do not assume a `ci-blob:\u002F\u002F` indirection where the text is actually inline.\n\n### `ci-blob:\u002F\u002F` references and the `\u002Fblobs` endpoint\n\nWhen a payload exceeds the storage threshold, the server replaces `data` with a\n`ci-blob:\u002F\u002FSESSION_ID\u002FEVENT_KEY` URI. Resolve it via the blob store — the `\u002Fblobs`\nendpoint (`GET {url}\u002Fblobs\u002F{session_id}[\u002F{key}]`), or the `blob_read` tool which\nreturns a local **file path**, not content:\n\n```python\nresult = blob_read(uri=\"ci-blob:\u002F\u002FSESSION_ID\u002FEVENT_KEY\")  # -> {\"path\": \"...\", \"source\": {name, url, origin}}\n```\n\nThen extract only the fields you need with `jq` — never load a whole blob into\ncontext. `$BLOB_PATH` below is the `path` value returned by `blob_read`; use it\nverbatim rather than constructing a path, since the blob store lives under the OS\ntemp directory and differs by platform:\n\n```bash\njq 'keys' \"$BLOB_PATH\"                    # structure first\njq '.messages[-1].content' \"$BLOB_PATH\"\n```\n\n**Rules:** check for a `ci-blob:\u002F\u002F` prefix before parsing as JSON; lifted properties\nbypass blobs entirely; always bound before loading.\n\n**Going further — `blob-reading.md`, beside this file.** When you actually have to\nresolve a blob (rather than avoid one), read the level-3 reference\n`@context-intelligence:skills\u002Fcontext-intelligence-graph-query\u002Fblob-reading.md`\nwith `read_file`. It carries the full contract: when NOT to resolve at all, the\nfour-step resolution, `blob_read`'s success and failure shapes (including which\nerrors carry a `source` you must cite and which do not), the safe `jq` extraction\npatterns, and blob-vs-inline field detection.\n\n---\n\n## Section 7 — Verified Query Patterns\n\nThese are validated against the live layer-2\u002Ffoundation schema. All reference\n`$workspace` (set via the tool's `workspace` argument, Section 2) and are bounded.\n\n**Full conversation turn trace** — prompt → run → iterations → tool calls:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_PART]->(p:Prompt)\n      -[:TRIGGERS]->(run:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nRETURN p.occurred_at AS turn_start, iter.iteration_number AS iteration,\n       tc.tool_name AS tool, tc.result_success AS succeeded\nORDER BY p.occurred_at, iter.iteration_number, tc.started_at\nLIMIT 100\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` here can be a merged node spanning multiple\n> orchestrator runs — do not trust it as a pure per-run counter. For attributing tool calls to a\n> specific prompt\u002Frun, prefer the temporal join pattern below.\n\n**Tool usage per iteration** — how many tools each LLM round fired:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nRETURN iter.iteration_number AS iteration,\n       collect(tc.tool_name) AS tools, count(tc) AS tool_count\nORDER BY iter.iteration_number\nLIMIT 50\n```\n\n> **Caveat (Trap 7):** counts and groupings by `iter.iteration_number` above inherit the\n> `Iteration`-MERGE bug (one node can span up to 15 runs) — treat this as an approximate, not\n> authoritative, per-iteration tool count.\n\n**Per-prompt (or per-run) tool-call attribution — temporal join, no edges (preferred over the\n`Iteration`-based counts above; see Trap 7):**\n\nThere is no direct edge from `ToolCall` to `Prompt` or `OrchestratorRun`. The reliable way to\nattribute a tool call to the prompt (or run) that was active when it fired is a **temporal join**\non `tc.started_at` — this is *more* accurate than the structural `run → Iteration → ToolCall`\npath above, which inflates counts by +4–20% due to the Trap 7 `Iteration`-MERGE bug and silently\ndrops prompts:\n\n```cypher\n\u002F\u002F Per-prompt tool-call attribution — temporal, no new edges.\n\u002F\u002F Swap (p:Prompt)\u002Fp.occurred_at for (p:OrchestratorRun)\u002Fp.started_at for per-run.\nMATCH (tc:ToolCall {session_id:$sid}) WHERE tc.started_at IS NOT NULL\nOPTIONAL MATCH (p:Prompt {session_id:$sid}) WHERE p.occurred_at \u003C= tc.started_at\nWITH tc, p ORDER BY p.occurred_at DESC\nWITH tc, head(collect(p)) AS last_prompt\nRETURN coalesce(last_prompt.node_id,'\u003Corphan: no preceding prompt>') AS prompt_id,\n       count(tc) AS tool_calls\nORDER BY prompt_id\n```\n\n**Caveats:**\n- Use `tc.started_at` directly — it equals `ToolPreEvent.occurred_at` on 100% of rows. Do **not**\n  join via `SOURCED_FROM` for this: it fans out to *both* `tool:pre` and `tool:post` events for\n  the same call and double-counts.\n- Report an explicit **orphan bucket** (`\u003Corphan: no preceding prompt>`) rather than dropping\n  those rows — ~0.2% of tool calls occur in sub-agent sessions with no `Prompt` node, and a few\n  precede the session's first prompt.\n- Filter the ~0.007% of `ToolCall` rows with a null `started_at` (the `WHERE` above already does\n  this).\n- This is a single-writer timeline — no clock skew, no tied timestamps observed in validation.\n\n**Failed tool calls** — scope to one session; add `ORDER BY tc.started_at DESC` for\nmost-recent-first:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nWHERE tc.result_success = false\nRETURN iter.iteration_number AS iteration, tc.tool_name AS tool,\n       tc.result_error AS error, tc.started_at AS failed_at\nORDER BY tc.started_at\nLIMIT 50\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` may be a cross-run merged node — a failed call\n> attributed to \"iteration 3\" may actually belong to iteration 3 of a *different* run. Cross-check\n> against the temporal join pattern above when per-run precision matters.\n\n**Delegations in a session** — which tool call triggered which agent (enumeration, not\ndepth — see Trap 1 for lineage\u002Fdepth):\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)-[:HAS_PART]->(:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)-[:TRIGGERED]->(d:Delegation)\nRETURN d.agent, d.sub_session_id, d.is_self_delegation, d.context_depth,\n       d.started_at, tc.tool_name AS via_tool\nORDER BY d.started_at\nLIMIT 50\n```\n\n**Skills active per iteration** — note skills loaded before the first request attach to\n`Session`, not `Iteration` (Gotcha 6):\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)-[:HAS_PART]->(iter:Iteration)\n      -[:HAS_SKILL_LOAD]->(sl:SkillLoad)\nRETURN iter.iteration_number, sl.skill_name, sl.content_length, sl.started_at\nORDER BY iter.iteration_number, sl.started_at\nLIMIT 100\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` can be a cross-run merged node — a skill load shown\n> under \"iteration 2\" may actually have loaded during a different run's iteration 2.\n\n**Recipe run trace:**\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_RECIPE_RUN]->(rr:RecipeRun)-[:HAS_STEP]->(step:RecipeStep)\nOPTIONAL MATCH (step)-[:TRIGGERED]->(target)\nRETURN rr.name, rr.status, step.name,\n       labels(target) AS triggered_type, target.node_id AS triggered_id\nORDER BY step.started_at\nLIMIT 50\n```\n\n### Size discipline (the cardinal rule)\n\nThe graph can hold thousands of sessions. **Every traversal query MUST have a `LIMIT`.**\nWork as a funnel:\n\n1. **Count first.** `RETURN count(...)` costs almost nothing — run it before any wide\n   query over an unknown population.\n2. **Anchor on a session** (`node_id: $session_id` or `WHERE s.node_id IN [...]`)\n   before traversing; an un-anchored `MATCH (s:Session {workspace: $workspace})` spans\n   every session and multiplies rows.\n3. **Filter and aggregate in Cypher**, not in the client — every unneeded row is wasted\n   context.\n4. **Bound variable-length paths** (`*1..3`), never bare `*`.\n5. **Paginate** with `ORDER BY \u003Cstable key> SKIP n LIMIT m` when you genuinely need more\n   than one page.\n\n---\n\n## Section 8 — Push Work to the Database (APOC \u002F GDS)\n\n**Prefer one server-side computation over many client round-trips.** When an analysis\nneeds a deep or variable-length traversal, path-finding, centrality\u002Finfluence, community\nstructure, or a multi-step aggregation that would otherwise mean pulling rows to the\nclient and iterating, do the work **inside the graph**. It is faster and keeps the\nprotocol un-chatty — fewer tool calls, less context churn. Both plugins are installed and\n**run on the live target** (Neo4j Community here — GDS works). Rule of thumb: **if\nanswering the question the naive way means many round-trips or a large client-side pull,\npush that computation into the database.** Probe once, compute server-side, and still\n**bound the RETURNED result** (count \u002F aggregate \u002F `LIMIT`).\n\nProbe availability: `CALL apoc.help('path')`, `RETURN gds.version()` (or `CALL gds.list()`).\n\n- **APOC — in-graph helpers.** `apoc.convert.fromJsonMap(e.data)` parses a `data` JSON\n  string into a map without a client `jq` hop. `apoc.path.expandConfig(...)` runs\n  configurable in-graph traversals — subject to the **same 1-hop caveat as\n  `HAS_SUBSESSION`** (Trap 1): for delegation depth, expand over `FORKED`\u002F`parent_id`.\n  Still bound your output.\n- **GDS — real graph analytics in one pass.** For centrality\u002Finfluence, community\u002Fcluster\n  detection, or pathfinding **across many sessions**, project once and run the algorithm\n  server-side instead of reconstructing the graph client-side. Always `gds.graph.drop`\n  the projection when done:\n\n  ```cypher\n  CALL gds.graph.project('g', 'Session', 'FORKED')\n  YIELD nodeCount, relationshipCount;\n\n  CALL gds.pageRank.stream('g')          \u002F\u002F or gds.degree \u002F gds.betweenness (influence)\n  YIELD nodeId, score\n  RETURN gds.util.asNode(nodeId).node_id AS session, score\n  ORDER BY score DESC LIMIT 20;\n\n  CALL gds.wcc.stream('g')               \u002F\u002F or gds.louvain (community structure)\n  YIELD componentId\n  RETURN componentId, count(*) AS size ORDER BY size DESC LIMIT 20;\n\n  CALL gds.graph.drop('g') YIELD graphName;   \u002F\u002F always release the projection\n  ```\n\n  **Agent-level, not session-level — agents are a PROPERTY to roll up by, not a projectable graph.**\n  \"Which *agent* is a hub\" ≠ \"which *session* is a hub\": one session can be highly central\n  while its agent is not. `Agent` concept nodes *do* exist (Section 1; Gotcha 4), but they\n  carry only inbound `HAS_AGENT` edges from sessions — there is **no Agent→Agent edge\n  structure to project**, so GDS centrality has nothing to traverse at the agent level. Do NOT\n  `gds.graph.project.cypher(...)` an agent-level graph; it returns an empty projection and\n  is a dead end. Instead **project the labels that exist** — `Session` (nodes) and `FORKED`\n  (relationships) — run the algorithm over *sessions*, then **roll the per-session scores up\n  by the `Session.agent` property in the `RETURN`**. For the named-agent-hub ranking, drop\n  the `self` marker and the root\u002Fmain session (`agent IS NULL`) — a \"which named agent\"\n  question is about named agents:\n\n  ```cypher\n  \u002F\u002F 'g' is the Session \u002F FORKED projection from above — agents are NOT projected;\n  \u002F\u002F agent-level results come from aggregating session scores by the s.agent PROPERTY.\n  CALL gds.pageRank.stream('g') YIELD nodeId, score\n  WITH gds.util.asNode(nodeId) AS s, score\n  WHERE s.agent IS NOT NULL AND s.agent \u003C> 'self'   \u002F\u002F named agents only (see Trap 4 for 'self')\n  RETURN s.agent AS agent, round(sum(score), 2) AS agent_influence, count(*) AS sessions\n  ORDER BY agent_influence DESC LIMIT 20\n  ```\n\n  Excluding `self` here answers \"which *named* agent is a hub\" — it does **not** make `self`\n  vanish from the story. If the question also touches self-delegation, resolve `self` up the\n  `FORKED` chain (Trap 4) and **state that root\u002Fmain-vs-named breakdown as its own finding**;\n  don't let the exclusion above silently drop it.\n\n  Skip GDS for a simple count or grouping that one plain Cypher statement already does —\n  reach for it when the naive alternative is iterative client-side fetching.\n\n---\n\n### Lift payload fields in-graph with APOC (the one authoritative pattern)\n\n**Problem.** `Event.data` (and nested payloads) is a serialized JSON **string**, not a Cypher\nmap. Dot-access like `e.data.working_dir` **fails loudly** — Neo4j raises:\n\n```\nNeo.ClientError.Statement.TypeError: Type mismatch: expected a map but was String(...)\n```\n\nYou **must** parse the string before reading a field. (Do **not** confuse this with the\nsilent-null trap in **Trap 5** — that one is comparing a *zoned datetime to a string literal*,\nwhich returns `null` with no error. This APOC case is a *loud* error, a different failure mode.)\n\n**Pattern — parse in-graph, project only the scalar you need:**\n\n```cypher\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN d.working_dir AS working_dir      \u002F\u002F one scalar, not the whole payload\n```\n\nUse `apoc.convert.fromJsonList(...)` for arrays. **Never** pull the full `data` string to the\nclient to parse with `jq`.\n\n**Two wins:**\n- **Correctness** — dot-access on the string errors out; parsing with APOC is the *only* way to\n  read the real value.\n- **Leanness** — the parse **and** the field-selection happen server-side; the payload never\n  crosses the wire or enters context — same discipline as Section 6 (\"never return full text\").\n\n**Rule.** Prefer lifted first-class properties when they exist; when a field is **only** in the\npayload, lift it in-graph with APOC and return just the scalar. (`working_dir` is the worked\nexample here — proven, and the whole point of the pattern — but per Section 2, it is **not**\na scoping lever: it is ~0% queryable in-graph (0 of 4,504 \u002F 0 of 9,086 sessions — not a lifted\nSession property) with no canonical per-session source, so use it to read one session's\ndirectory, never to scope a population.)\n\n### Aggregate a numeric JSON-string payload field: total cost (Trap 6)\n\nThe same parse-in-graph idiom extends past reading a scalar to **aggregating** a numeric value\nburied inside a JSON string. `cost_usd` lives at `data.usage.cost_usd` on `:LlmResponseEvent`,\nitself a string inside the parsed map — cast it with `toFloat()` before summing, and pin the\nlabel to avoid the triple-count described in Trap 6:\n\n```cypher\nMATCH (e:LlmResponseEvent) WHERE e.data CONTAINS 'cost_usd'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN sum(toFloat(d.usage.cost_usd)) AS total_cost_usd\n```\n\n**Rules:**\n- Pre-filter with `WHERE e.data CONTAINS 'cost_usd'` before parsing — cheaper than parsing every\n  `LlmResponseEvent` just to find the field absent.\n- Pin the label to `:LlmResponseEvent` — do not sweep `:SST_EVENT` or join through\n  `ContentBlockEndEvent` \u002F `ProviderResponseEvent` for the same figure (Trap 6).\n- Always `toFloat()` the nested value — it deserializes from JSON as a string, and a bare\n  `sum()` over it returns HTTP 500, not a wrong number.\n\n### Aggregate token + cost usage from LlmResponseEvent payloads (Trap 8)\n\nToken and cache-usage fields sit alongside `cost_usd` in the same `data.usage` map, but they\ndeserialize as **INTEGER**, not a JSON string — `toInteger()` is defensive here, not required the\nway `toFloat()` is for `cost_usd`. The scoping danger is identical to Trap 6: an unpinned `:Event`\nsweep also matches `ContentBlockEndEvent` and `ProviderResponseEvent`, each replaying the same\nresponse-level `usage` map, and inflates every total by **~3.4x** regardless of field type.\n\n**Validated combined cost + tokens (canonical — aggregate == independent reduce == hand-summed\nsubset):**\n\n```cypher\nMATCH (e:LlmResponseEvent)\nWHERE e.data CONTAINS 'input_tokens'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN count(*)                                            AS llm_calls,\n       sum(toFloat(d.usage.cost_usd))                      AS total_cost_usd,\n       sum(toInteger(d.usage.input_tokens))                AS input_tokens,\n       sum(toInteger(d.usage.output_tokens))               AS output_tokens,\n       sum(toInteger(d.usage.cache_read_input_tokens))     AS cache_read_tokens,\n       sum(toInteger(d.usage.cache_creation_input_tokens)) AS cache_creation_tokens\n```\n\n**Per-model breakdown:**\n\n```cypher\nMATCH (e:LlmResponseEvent)\nWHERE e.data CONTAINS 'input_tokens'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN d.model AS model, d.provider AS provider, count(*) AS calls,\n       round(sum(toFloat(d.usage.cost_usd)), 2)            AS cost_usd,\n       sum(toInteger(d.usage.input_tokens))                AS input_tokens,\n       sum(toInteger(d.usage.output_tokens))                AS output_tokens,\n       sum(toInteger(d.usage.cache_read_input_tokens))     AS cache_read,\n       sum(toInteger(d.usage.cache_creation_input_tokens)) AS cache_creation\nORDER BY cost_usd DESC LIMIT 15\n```\n\nAdd `{workspace: $workspace}` to the `MATCH (e:LlmResponseEvent ...)` anchor for a per-workspace\nvariant of either query.\n\n**Validated global scale (real corpus snapshot, reader's sanity check):** ~68k LLM calls → ~$45.5K\ncost, input ~4.50B tokens, output ~80.6M tokens, cache_read ~4.62B tokens, cache_creation ~8.05B\ntokens. The same query with the `:LlmResponseEvent` pin removed reports ~$156K (~3.4x) — wrong,\nfor the exact reason given in Trap 8.\n\n**Rules:**\n- Use `cache_read_input_tokens` and `cache_creation_input_tokens` — both complete. Do **not** sum\n  `cache_read_tokens` (sparse\u002FNULL on many events) or `cache_creation` (a MAP, not a number).\n  `cache_write_tokens` is close to `cache_creation_input_tokens` but diverges ~1.5% — prefer\n  `cache_creation_input_tokens`.\n- Never source tokens or cost from `Iteration.usage_*` (Trap 7) — last-write-wins corrupted, and\n  `Iteration` has no cost field at all.\n- Report `llm_calls` alongside cost and tokens — ~4% of events have a null `cost_usd` (`sum()`\n  ignores nulls so the total is still correct), and some non-priced models report `$0` cost with\n  real token counts. Surfacing `llm_calls` and the token columns makes that gap visible instead of\n  hiding it inside a dollar figure.\n\n---\n\n## Gotchas\n\n1. **Layer-2 \u002F foundation nodes only exist if handlers ran, or the feature was used.**\n   Sessions ingested before a handler was deployed, or with no delegation\u002Fskills\u002Frecipes,\n   simply lack those nodes. Use `OPTIONAL MATCH` when joining them against arbitrary\n   sessions.\n\n2. **`result_success = false` is the error path;** `result_error` holds the message. A\n   null `result_success` means the post\u002Ferror event hasn't been processed — the call is\n   in-flight or its handler didn't run.\n\n3. **`ENABLES` edges are sparse.** The `OrchestratorRun → next Prompt` edge exists only\n   for multi-turn chains (N prompts ⇒ N−1 edges). Do not use its presence to judge clean\n   session termination.\n\n4. **Concept nodes (`Agent`, `Recipe`, `Orchestrator`) are shared across sessions,**\n   merged by name. Querying `(a:Agent)` without a session anchor spans everything — reach\n   them through the session (`HAS_AGENT` from the sub-session, `HAS_RECIPE` from a\n   `RecipeRun`).\n\n5. **`SOURCED_FROM` may be absent on older sessions.** Detect and fall back to the\n   Section 4 fallback joins:\n\n   ```cypher\n   MATCH (n:SST_EVENT) WHERE NOT (n)-[:SOURCED_FROM]->() AND NOT n:Session\n   RETURN labels(n), count(*)\n   ```\n\n6. **`SkillLoad` may attach to `Session`, not `Iteration`.** Skills loaded before the\n   first request have no active iteration. Add\n   `OPTIONAL MATCH (s)-[:HAS_SKILL_LOAD]->(sl:SkillLoad)` to catch session-level loads.\n\n7. **`IncompleteSession` is a health marker, not a terminal label.** It reached\n   `session:end` with no captured `session:start`\u002F`fork`, carries `has_terminal: false`,\n   and none of `:RootSession`\u002F`:SubSession`\u002F`:ForkedSession`. Exclude it from normal\n   terminal-session queries with `WHERE NOT s:IncompleteSession`. A spike in its count\n   signals upstream event loss.\n\n8. **The node MERGE key is `{node_id, workspace}`.** The same logical entity (e.g. an\n   Orchestrator named `loop-streaming`) exists as a separate node per workspace. A\n   cross-workspace query returns one node per workspace — account for this when\n   aggregating.\n","## When the graph server is not configured\n\nIf the context-intelligence server is unavailable or not configured for this\nsession, graph-based analysis cannot proceed here. This limits graph work, not\nall session evidence: conversation uses `session_transcript` when mounted, and\nlocal event, tool, error, or capture questions use `session-navigator` without a\ngraph health check. Delegate only when permitted and available; a specialist\nperforms its matching work itself, never delegates the same task to itself.\nUse authorized local fallback only when it can sufficiently answer the question;\notherwise report the specific gap and any partial coverage. A failed query or\ntimeout is not proof the server is down, and a graph miss is not proof of absence\nin local captures or other evidence layers. Do not attempt Cypher against a\nserver known to be unreachable.\n\n---\n\n# Context Intelligence Graph Query\n\nThis skill equips you to navigate and extract insights from the context-intelligence\nproperty graph using the `graph_query` tool. The graph holds indexed session\nevidence — what happened, when, how things connect, and at what scale — only to\nthe extent capture, forwarding, and indexing have supplied it. State the selected\nsource, requested scope, observed coverage, and freshness rather than assuming a\ncomplete record of every Amplifier session.\n\n**Read the live schema from the server; read the meaning from this skill.** The\nserver can always tell you the current shapes — labels, relationship types, property\nnames. It CANNOT tell you what those shapes mean, where they lie about their meaning,\nor how to scope, join, and bound a query safely. This skill deliberately does not\nfreeze a schema dictionary (it goes stale and misleads). It keeps the grammar: the\nscoping levers, the traps, the cross-layer joins, and the verified patterns.\n\n---\n\n## Section 1 — Introspect the Live Schema First\n\nBefore writing analytical Cypher against an unfamiliar area of the graph, ask the\nserver what it actually contains. These calls are cheap and always current.\n\n```cypher\nCALL db.labels()                          \u002F\u002F every node label present\nCALL db.relationshipTypes()               \u002F\u002F every edge type present\nCALL db.schema.nodeTypeProperties()       \u002F\u002F property names + types per label\nCALL db.schema.visualization()            \u002F\u002F label\u002Fedge connectivity overview\n```\n\nTo learn the real properties on a specific label (names AND which are populated),\nsample a few live nodes rather than trusting any documented list:\n\n```cypher\nMATCH (n:ToolCall) WITH n LIMIT 25\nUNWIND keys(n) AS k\nRETURN k AS property, count(*) AS present_on_n_nodes\nORDER BY present_on_n_nodes DESC\n```\n\n**The three-layer shape (stable orientation, not a dictionary):**\n\n- **Data layer 1 — raw events.** Every kernel event preserved as an `:Event` node\n  (`:ToolPreEvent`, `:LlmResponseEvent`, `:PromptSubmitEvent`, …). Answers *what\n  happened and when*. `node_id` uses a `__` (double-underscore) separator.\n- **Data layer 2 — semantic entities.** Events assembled into `Session`,\n  `OrchestratorRun`, `Iteration`, `ContentBlock`, `ToolCall`, `Prompt`, and more,\n  connected by typed relationships. Answers *what ran, how, at what scale*. `node_id`\n  uses a `::` (double-colon) separator (a few types — `ToolCall`, concept nodes — use\n  a bare identifier).\n- **Foundation layer — above the kernel.** Delegation trees (`Delegation`, `Agent`),\n  skill loads (`SkillLoad`), recipe orchestration (`RecipeRun`, `RecipeStep`,\n  `Recipe`).\n\nAll layers coexist in one graph, bridged by **`SOURCED_FROM`** edges: every layer-2\nentity points back to the layer-1 event(s) that produced it (Section 4). Use\n`db.schema.nodeTypeProperties()` to confirm exact property names before you rely on\nthem — the sections below cover only the meaning the server cannot self-describe.\n\n### Reading results & citing the source (required)\n\n`graph_query` returns `{\"source\": {\"name\", \"url\", \"origin\"}, \"rows\": [...]}` on\nsuccess. Read your data from `rows`. ALWAYS report `source.name` (its `origin` is\n`source`, `destination`, or `env`) in your answer — the user must know which\nendpoint the outcome came from.\n\nOn **failure**, `source` is present only when an endpoint was actually chosen:\n**endpoint-level** errors (`connection_error`, `timeout`, `http_status`,\n`decode_error`, and input-validation errors like a missing\u002Finvalid `query` that\noccur *after* selection) carry `error.source` — cite it. **Selection\u002Fconfig**\nerrors that occur *before* any endpoint is chosen (`ambiguous_source_selection`,\n`unknown_source`, `source_misconfigured`, `configuration_error`) have **no**\n`source` — there is no single endpoint to name, so report that selection failed\nrather than inventing one.\n\nTo discover what you can reach, call `graph_query` with `list_sources: true` — it\nreturns the connectable set (`{\"connectable_set\": [{name, url, origin}, ...]}`)\nwithout running a query. To target a specific endpoint (a configured read source\nOR a hook upload destination), pass `source: \"\u003Cname>\"`.\n\n---\n\n## Section 2 — Scoping (Mandatory, and Not Self-Describing)\n\nScoping is the single most error-prone part of querying this graph, and the server\ncannot tell you how its client scopes. Get this right first.\n\n### The `workspace` tool argument — NOT a Cypher param\n\n**The `graph_query` tool reads a top-level `workspace` argument.** It injects that\nvalue as `$workspace` into your Cypher. Do **not** try to set the workspace through\n`params: {\"workspace\": ...}` — the tool does not read the workspace from `params`, and\na mis-set workspace silently scopes to a tiny `\"default\"` decoy partition instead of\nthe real one. If your counts look implausibly small (a handful of sessions where you\nexpected hundreds), you are almost certainly scoped to the decoy.\n\n```cypher\n\u002F\u002F Correct: reference $workspace; set the workspace via the tool's top-level\n\u002F\u002F `workspace` argument, not via params.\nMATCH (s:Session {workspace: $workspace})\nRETURN count(s) AS session_count\n```\n\n### The two real scoping levers\n\nBoth live as first-class properties on nodes — confirm them by sampling:\n\n- **`created_by`** — *who* produced the data (present on every node; the data is\n  typically single-user).\n- **`workspace`** — *where*: a **lifted, dash-slugified label\u002Fslug** (e.g.\n  `-mnt-linuxdata-workspaces-...-dashboard-ui`), reliable at ~100% coverage. It is a **label,\n  not the literal working directory**: the true directory lives in the event `data` payload as\n  `working_dir`, and the **same `working_dir`** (`\u002Fhome\u002Fuser\u002Fproject`) can map to **different**\n  `workspace` slugs (`myproject`, `proj`, `multi`). Treat `workspace` as the reliable scoping\n  slug; treat `working_dir` as a **payload field, ~0% queryable in-graph (0 of 4,504 \u002F 0 of\n  9,086 sessions — not a lifted Session property)** with no canonical per-session source — read\n  it via the APOC parse in Section 8, but do **not** scope by it. The `\u002Fcypher` request's own\n  `workspace` scope is a second, orthogonal server-side filter layered on top.\n\n**Always anchor scope on the first `MATCH`** so the workspace index is used:\n\n```cypher\n\u002F\u002F CORRECT — workspace on the anchor node\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(run:OrchestratorRun)\nRETURN run.orchestrator_name, run.started_at\n\n\u002F\u002F INCORRECT — workspace on a downstream node misses the index\nMATCH (s:Session {node_id: $session_id})\n      -[:HAS_EXECUTION]->(run:OrchestratorRun {workspace: $workspace})\nRETURN run.orchestrator_name, run.started_at\n```\n\nTo discover which workspaces exist before scoping, aggregate across all of them\n(use sparingly — it skips the partition index):\n\n```cypher\nMATCH (s:Session)\nWHERE s.workspace \u003C> ''\nRETURN s.workspace, s.created_by, count(s) AS sessions\nORDER BY sessions DESC\nLIMIT 25\n```\n\n### Resolving \"my\" — use `whoami`, don't guess\n\n`created_by` scopes by *who produced the data*, but the graph never tells you\nwho \"you\" are. When a request is scoped to the acting user themselves —\n\"my sessions\", \"sessions I created\", \"what have I been working on\" — call the\n`whoami` tool for the relevant server before filtering on `created_by`:\n\n```python\nwhoami()                       # default endpoint\nwhoami(source=\"prod\")          # a specific configured endpoint\n```\n\nIt returns `{\"contributor_id\": \"\u003Cgithub-id-or-null>\", \"source\": {...}}`. Use\n`contributor_id` as the value to match against `created_by`:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, created_by: $contributor_id})\nRETURN count(s) AS my_sessions\n```\n\n**Null case.** A null `contributor_id` means auth is disabled (or otherwise\nunknown) on that server — there is no acting-user identity to filter by. Do\n**not** guess or fall back to some other field. Ask the user how they want to\nscope the query instead (e.g. by name, by workspace, or \"show everyone's\").\n\n---\n\n## Section 3 — Traps: Where the Shapes Lie About Their Meaning\n\nThese are the highest-value items in this skill. Each is a place where reading the\nschema literally leads you to a **wrong answer that looks rigorous**. The server will\nhappily hand you the misleading shape; only domain knowledge corrects it.\n\n### Trap 1 — Delegation lineage: use `FORKED` + `Session.parent_id`, not the edge walk\n\nThe true delegation\u002Ffork lineage is carried by the **`FORKED` edge** together with\n**`Session.parent_id`**. This is the authoritative source for delegation depth and\nancestry. Verified live: the fork lineage reaches **max depth 3**.\n\n```cypher\n\u002F\u002F Authoritative lineage depth via the FORKED edge.\n\u002F\u002F Bound the path (never bare `*` — Section 7): *1..20 is generous headroom over the\n\u002F\u002F observed max depth of 3, and the RETURN is aggregate-only (no rows materialized).\nMATCH path = (root:Session {workspace: $workspace})-[:FORKED*1..20]->(leaf:Session)\nWHERE root.parent_id IS NULL\nRETURN max(length(path)) AS max_fork_depth, count(path) AS lineages\n```\n\n`Session.parent_id` lets you reconstruct the same chain by property when an edge walk\nis inconvenient (each child names its parent):\n\n```cypher\nMATCH (child:Session {workspace: $workspace})\nWHERE child.parent_id IS NOT NULL\nRETURN child.node_id, child.parent_id\nORDER BY child.node_id LIMIT 200   \u002F\u002F bounded (Section 7); anchor on a node_id to walk one specific chain\n```\n\n**Two known undercounts — do NOT trust either for depth:**\n\n- **`HAS_SUBSESSION` is a single hop only.** It materializes only the immediate\n  parent→child link; each sub-session is ingested as its own root, so a multi-hop walk\n  `(:Session)-[:HAS_SUBSESSION*]->()` collapses to depth 1. It is a one-level lookup,\n  **not** the delegation tree.\n- **The `Delegation` property-join (`sub_session_id → parent_session_id`) undercounts\n  beyond hop 1.** It is more complete than the `HAS_SUBSESSION` walk (it finds a second\n  hop), which is exactly why it is dangerous: it *looks* like the answer and reports\n  depth 2, but it still misses the deeper lineage that `FORKED`\u002F`parent_id` reveals.\n  Use it to enumerate individual delegations, not to measure tree depth.\n\n### Trap 2 — `context_depth` \u002F `context_scope` are inheritance modes, NOT tree depth\n\nOn `Delegation` nodes, `context_depth` and `context_scope` describe **how much parent\ncontext a delegation inherited** — an inheritance mode with values like `none`,\n`recent`, `all`. The name invites reading it as nesting\u002Ftree depth. It is not.\nSelf-delegations are almost always `none`; regular delegations spread across\n`none` \u002F `recent` \u002F `all`.\n\n### Trap 3 — Tool data lives on `ToolCall`, not `ToolExecution`\n\n`:ToolCall` carries the tool data you want: `tool_name`, `tool_input`, `started_at`,\n`ended_at`, `result_success`, `result_error`, `result_output`. `:ToolExecution` is\n**property-less — a dead end**; do not route tool queries through it. `ToolCall` is the\ncorrect, simple source (verified: `tool_name` and `started_at` populated on 100% of\n`ToolCall` nodes in a real workspace).\n\n```cypher\nMATCH (t:ToolCall {workspace: $workspace})\nRETURN t.tool_name AS tool, count(*) AS n\nORDER BY n DESC LIMIT 12\n```\n\n### Trap 4 — `self` is a self-delegation marker, never a real agent\n\nSelf-delegation is recorded explicitly as **`Delegation.is_self_delegation = true`**\n(equivalently the child `Session.agent = 'self'`). Never infer it by string-matching\n`tool_input` or comparing names.\n\n**`'self'` is a marker, not an identity** — it means \"this session spawned a fresh\nsub-context of *itself*,\" so `self` is never the real actor. Before any per-agent stat\n(centrality, counts, durations), **resolve it up the fork chain** to the nearest ancestor\nwhose `agent \u003C> 'self'`. That actor is often the **root\u002Fmain session** (`agent IS NULL` —\nthe user's top-level session, not a named sub-agent); occasionally a named agent that\nself-delegated. Empirically the chain is a single hop (no `self`→`self`→…).\n\n```cypher\n\u002F\u002F Resolve every self-session to the actor that actually spawned it\nMATCH (s:Session {workspace: $workspace, agent: 'self'})\nOPTIONAL MATCH pth = (anc:Session)-[:FORKED*1..20]->(s)\nWHERE anc.agent IS NULL OR anc.agent \u003C> 'self'\nWITH s, anc, pth ORDER BY length(pth) ASC\nWITH s, head(collect(coalesce(anc.agent, 'root\u002Fmain session'))) AS actor\nRETURN coalesce(actor, 'unresolved') AS real_actor, count(*) AS self_delegations\nORDER BY self_delegations DESC\n```\n\nSelf-delegation is itself a **first-class signal** (an actor extending its own context —\ncontinuation \u002F context management): count it *as* self-delegation when that is the\nquestion, and *resolve* it when you need the real actor. A raw `self` bucket in a\nper-agent aggregate is a bug — it attributes real work to a non-existent agent.\n\n**Report the resolved breakdown as an explicit finding — do not bury it in a blended\nstat.** The resolve-self join above only pays off if its result reaches the answer as\nits *own stated fact*. For any agent-level or self-delegation question, **lead with the\nresolved `self`→actor split as a standalone sentence** — how many, and what percent, of\nself-delegations resolve to the **root\u002Fmain session continuing itself** (`agent IS NULL`)\nversus to **named agents** — *before* any roll-up or combined figure. Computing the\nnumber correctly but folding it into a mixed \"X% of all delegations are root-launched\"\nstatistic hides the very fact the question asked for. This is a general output-shape\nprinciple: surface the load-bearing breakdown first, then add supporting context — state\nit, don't just compute it.\n\n```cypher\n\u002F\u002F The plain self-delegation split (the signal itself)\nMATCH (d:Delegation {workspace: $workspace})\nRETURN d.is_self_delegation AS self_delegation, count(*) AS n\nORDER BY n DESC\n```\n\n### Trap 5 — Timestamps are ZONED DATETIME; durations are computed\n\nEvery `*_at` property (`started_at`, `ended_at`, `occurred_at`, …) and `last_updated`\n— on nodes and on the edges that carry `occurred_at` (`HAS_EVENT`, `HAS_SUBSESSION`,\n`FORKED`) — is stored as a native Neo4j **`ZONED DATETIME`**, not a string.\n\n- ❌ `WHERE s.started_at > '2026-05-01'` — comparing to a string literal silently\n  returns nothing (no error raised).\n- ✅ `WHERE s.started_at > datetime('2026-05-01')` — wrap every literal in `datetime()`.\n- ✅ Rolling windows: `WHERE s.started_at > datetime() - duration('P30D')`.\n\n**Durations are not stored — compute them** with `duration.between()`:\n\n```cypher\nMATCH (t:ToolCall {workspace: $workspace})\nWHERE t.started_at IS NOT NULL AND t.ended_at IS NOT NULL\nWITH t, duration.between(t.started_at, t.ended_at) AS d\nRETURN t.tool_name AS tool, d.seconds AS secs\nORDER BY secs DESC LIMIT 8\n```\n\n### Rule — one owner per numeric fact: never multi-count cost or tokens by over-reading events\n\nThis is a cross-cutting rule, not a single trap — Traps 6 and 8 below are both instances of it.\n\nEvery numeric usage value — dollars (`cost_usd`) and every token field (`input_tokens`,\n`output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`) — is emitted **once**\nper LLM turn and is **owned** by the `:LlmResponseEvent` node. The identical `usage` map is then\n**echoed** onto other events of the same turn: `ContentBlockEndEvent` (streaming replays it ~2.33x\nper response) and `ProviderResponseEvent` (a third, lower-layer copy).\n\n**Consequence:** any aggregation that reads a set *wider* than the owning label — an unpinned\n`:Event` \u002F `:SST_EVENT` sweep, or a cross-layer join through `ContentBlockEndEvent` \u002F\n`ProviderResponseEvent` — sums the same dollars and tokens 2–3x over. This is **over-reading the\nevents**, not a rounding error. Measured inflation ~3.4x on the production graph (a true ~$45.5K\nreads as a phantom ~$156K); reproduced on an isolated DTU at 2.1x–3.9x across fields.\n\n**The rule, stated generally:** a numeric fact has exactly **one owning event layer** — aggregate\nit only from that layer. For LLM cost and tokens, the owner is `:LlmResponseEvent`. Concretely:\nalways `MATCH (e:LlmResponseEvent)`; never `sum()` a usage field across a broader label set, and\nnever reach the same figure through a cross-layer join. If a numeric value appears on more than one\nlabel, those are **copies, not addends** — pick the owner. This applies identically to dollars and\ntokens, and to any future numeric usage field added to the payload.\n\n### Trap 6 — `cost_usd` is a JSON string nested in the payload, not a lifted property\n\nAn instance of the **Rule** above — for cost, the owner is `:LlmResponseEvent`. The trap-specific\nwrinkle here is typing, not scoping:\n\nThere is **no top-level lifted cost property** anywhere in the graph. The real value lives at\n`data.usage.cost_usd` on `:LlmResponseEvent`, serialized as a **JSON string** — not a number, and\nnot a top-level field. A bare `sum(e.cost_usd)` or `sum(e.data.usage.cost_usd)` **returns HTTP\n500** (Section 6: `data` is a string, dot-access into it type-errors; the nested numeric value also\narrives as a JSON string, not a Cypher number, so it must be parsed *and* cast with `toFloat()`).\n\n**Validated** (returns `$44,886.32` over 63,758 events):\n\n```cypher\nMATCH (e:LlmResponseEvent) WHERE e.data CONTAINS 'cost_usd'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN sum(toFloat(d.usage.cost_usd)) AS total_cost_usd\n```\n\nSee Section 8 for the full worked pattern.\n\n### Trap 7 — `Iteration.node_id` MERGEs across orchestrator runs\n\n`Iteration.node_id` is composed as `\"{session_id}::iteration::{N}\"` — but `N` (`iteration_number`)\n**restarts at 1 for every `OrchestratorRun`**, not once per session. Because the MERGE key omits\nthe run, **one `Iteration` node absorbs iterations from every run that reused the same number** —\nobserved up to **15 run parents on a single `Iteration` node**.\n\nThis corrupts anything that aggregates through `Iteration`:\n\n- **Last-write-wins property garbage** on `usage_input`, `usage_output`, `usage_cache_write`, and\n  `message_count` — whichever run's ingest landed last overwrites the others' values.\n- **`count(DISTINCT Iteration)` undercounts** — iterations from different runs that share a number\n  collapse into one node.\n- **`run → Iteration → ToolCall` traversals overcount tool calls by +1.1% to +66%** — a `ToolCall`\n  reached through the merged `Iteration` looks like it belongs to every run that touches that node.\n\n**Every query pattern below that groups or counts through `Iteration` inherits this bug** — see the\ncaveat noted at each: Section 4's `SOURCED_FROM` example, and Section 7's \"Full conversation turn\ntrace,\" \"Tool usage per iteration,\" \"Failed tool calls,\" and \"Skills active per iteration.\"\n\n**What to do instead:**\n1. For **tool-call attribution** to a prompt or run, use the **temporal join** pattern in Section 7\n   — it never touches `Iteration` and is unaffected by this bug.\n2. For **token or cost counts**, never trust `Iteration.usage_*` \u002F `message_count` — recompute\n   directly from `LlmResponseEvent` payloads using the validated Trap 8 \u002F Section 8 aggregation\n   pattern (`Iteration` carries no cost field at all, and its token counters are last-write-wins\n   corrupted).\n\n**Trust marker:** a bare `node_id` of the form `\"{session_id}::iteration::{N}\"` with no\norchestrator-run segment is **pre-fix data — suspect**. A `node_id` that additionally scopes to\nthe run (i.e. the run is part of the key, not just `N`) is **post-fix — trustworthy**. Check the\nliteral `node_id` shape before trusting an `Iteration` aggregate.\n\n### Trap 8 — Token & cache usage: payload integers, still owned by `:LlmResponseEvent`\n\nAn instance of the **Rule** above (before Trap 6) — for tokens, the owner is also\n`:LlmResponseEvent`, exactly as for cost. Token and cache-usage figures live at\n`data.usage.{input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens}`\non `:LlmResponseEvent`, parsed the same way as cost (Trap 6) via `apoc.convert.fromJsonMap(e.data)`.\nThe trap-specific wrinkle is the opposite typing quirk:\n\n- **Typing is the opposite of `cost_usd`.** The token fields deserialize as **INTEGER**, not a\n  JSON string — `toInteger()` on them is defensive\u002Foptional, not required. `cost_usd` (Trap 6) is\n  the **STRING** one; `toFloat()` there is mandatory. Don't let the cost pattern's mandatory cast\n  make you assume tokens need the same cast for the same reason — they don't need it to avoid an\n  error, only to normalize type. Correct typing does **not** protect you from wrong scoping —\n  validated inflation when the Rule is violated: cost 3.43x, input tokens 3.31x, output tokens\n  3.63x, cache tokens 3.39x.\n- **Use the complete cache fields, not the sparse\u002Fwrong-typed ones.** `cache_read_input_tokens` and\n  `cache_creation_input_tokens` are complete. Do **not** sum `cache_read_tokens` (sparse — NULL on\n  many events) or `cache_creation` (a **MAP**, not a number — summing it type-errors or silently\n  drops). `cache_write_tokens` is close to `cache_creation_input_tokens` but diverges ~1.5%; prefer\n  `cache_creation_input_tokens` as the validated figure.\n- **Never aggregate from `Iteration`.** `Iteration.usage_input` \u002F `usage_output` \u002F\n  `usage_cache_write` are the same Trap 7 last-write-wins corruption (undercounts 10–14%), and\n  `Iteration` carries **no cost field at all** — there is no `Iteration`-based cost figure to fall\n  back to even approximately. Recompute both tokens and cost from `LlmResponseEvent` payloads.\n- **NULL and zero-cost caveat.** ~4% of events have a null `cost_usd` (`sum()` ignores nulls, so the\n  total stays correct — just don't expect `count(*)` to equal the events actually priced). Some\n  models\u002Fproviders are non-priced and report `$0` cost with real, non-zero token counts. Always\n  report `llm_calls` and the token columns alongside cost so this gap is visible rather than hidden\n  inside a dollar figure.\n\nSee Section 8 for the validated combined cost+token aggregation and per-model breakdown.\n\n---\n\n## Section 4 — Cross-Layer Joins (SOURCED_FROM)\n\nData layer 1 (raw events) and data layer 2 (semantic entities) coexist in one graph.\nThe canonical bridge is the **`SOURCED_FROM`** edge: every layer-2 entity points to the\nlayer-1 event(s) that produced it. Use layer 1 for exact raw fields\u002Ftimeline; use\nlayer 2 for structure, scale, and causation; move between them with `SOURCED_FROM`.\n\n```cypher\n\u002F\u002F Semantic ToolCall → its originating raw event (canonical)\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nMATCH (tc)-[:SOURCED_FROM]->(pre:ToolPreEvent)\nRETURN iter.iteration_number AS iteration,\n       tc.tool_name          AS tool,\n       tc.result_success     AS succeeded,\n       pre.occurred_at       AS event_fired_at\nORDER BY pre.occurred_at\nLIMIT 25\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` here can be a cross-run merged node (up to 15 run\n> parents observed) — do not treat this join as an authoritative per-run tool-call count. For\n> per-prompt\u002Fper-run attribution, prefer the temporal join pattern in Section 7.\n\n**Fallback joins** (only when `SOURCED_FROM` is absent — older sessions ingested before\nthe handler existed; check with the query in Gotcha 5):\n\n- **ToolCall direct match** — `:ToolCall.node_id` is the provider's `tool_call_id`,\n  which `:ToolPreEvent` also lifts as `tool_call_id`. Join on that shared key. Works\n  for `ToolCall` only.\n\n  ```cypher\n  MATCH (e:ToolPreEvent {workspace: $workspace, tool_call_id: $tool_call_id})\n  MATCH (tc:ToolCall {node_id: e.tool_call_id})\n  RETURN e.tool_name, e.occurred_at, tc.result_success, tc.result_output\n  ```\n\n- **Session containment** — join through the shared `:Session`: layer 1 attaches raw\n  events via `HAS_EVENT`; layer 2 attaches entities via `HAS_EXECUTION`\u002F`HAS_PART`.\n  Correlate on a shared scalar (e.g. `iteration_number`).\n\n---\n\n## Section 5 — SST Navigation (Reasoning by Semantic Type)\n\nLayer-2 nodes carry an **SST type label** that classifies each node by its fundamental\ncharacter, letting you query across entity boundaries without knowing specific labels\nin advance. Confirm the current partition with `db.labels()`; the meaning is:\n\n- **`:SST_EVENT`** — a temporal, bounded occurrence (Session, OrchestratorRun,\n  Iteration, ContentBlock, ToolCall, Prompt, Delegation, SkillLoad, RecipeRun, …).\n- **`:SST_THING`** — a persistent resource\u002Fartifact (e.g. MountPlan).\n- **`:SST_CONCEPT`** — an abstract, reusable identity (Orchestrator, Agent, Recipe).\n\nLayer-2 edges carry an **`sst_semantic`** property expressing the abstract relationship\nindependent of concrete edge type: `CONTAINS` (containment), `LEADS_TO` (causal\u002F\nsequential), `EXPRESSES` (attribution), `NEAR` (concurrency). This lets you query\ncausation or containment uniformly.\n\n```cypher\n\u002F\u002F All temporal events in the most recent session, mixed types sorted correctly\nMATCH (s:Session {workspace: $workspace})\nWITH s ORDER BY s.started_at DESC LIMIT 1\nMATCH (s)-[:HAS_EXECUTION|HAS_PART*1..3]->(e:SST_EVENT)\nRETURN labels(e) AS types,\n       coalesce(e.started_at, e.occurred_at) AS at\nORDER BY at\nLIMIT 50\n```\n\n> **Two temporal keys across `:SST_EVENT`:** span nodes (Iteration, ContentBlock,\n> ToolCall, Session, OrchestratorRun) store `started_at`; occurrence nodes (Prompt,\n> Cancellation, ContextCompaction) store `occurred_at`. When you sweep a mixed set,\n> use `coalesce(e.started_at, e.occurred_at)` in both projection and `ORDER BY` so\n> occurrence nodes are not silently null and sorted out of order.\n\n**Hierarchy traversal** — reach the full Session → Run → Iteration → ContentBlock tree\nwith a bounded variable-length path. Bound it (`*1..3`) to prevent runaway fanout:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION|HAS_PART*1..3]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nRETURN tc.tool_name, tc.started_at, tc.result_success\nORDER BY tc.started_at\nLIMIT 50\n```\n\n---\n\n## Section 6 — Text Extraction: Bounded Always, Blobs Carefully\n\nLarge text fields are the fastest way to destroy your context window. **One unbounded\n`p.prompt` query returned 1.21M tokens and overflowed the context.** Treat every large\ntext field with discipline.\n\n### Firm rule — never return full text of many rows\n\nFor any large text field (`prompt`, `data`, `data.content`, `tool_input`,\n`result_output`), you MUST bound it — no exceptions:\n\n- Add a small `LIMIT`, **and**\n- Truncate with `substring(field, 0, 200)`, or return only `size(field)` to gauge\n  scale first, or aggregate (`count`, `collect` of a truncated projection).\n\n```cypher\n\u002F\u002F WRONG — full prompt text over many rows → context overflow\nMATCH (p:Prompt {workspace: $workspace}) RETURN p.prompt\n\n\u002F\u002F RIGHT — measure scale first\nMATCH (p:Prompt {workspace: $workspace})\nRETURN count(p) AS n, max(size(p.prompt)) AS longest_chars\n\n\u002F\u002F RIGHT — bounded preview when you do need text\nMATCH (p:Prompt {workspace: $workspace})\nWHERE p.session_id = $session_id\nRETURN substring(p.prompt, 0, 200) AS prompt_preview\nORDER BY p.occurred_at\nLIMIT 25\n```\n\n### The `data` field is a JSON string, not a Cypher map\n\n`:Event.data` is a serialized JSON string. Dot notation (`e.data.tool_name`) does **not**\nwork in Cypher — it raises a type-mismatch error, not a silent null. Prefer **lifted\nproperties** (`tool_name`, `tool_call_id`, `model`, `provider`, …) when they exist; when you\nneed a raw field that is **only** in the payload, parse it **in-graph with APOC** — see the\none authoritative pattern **\"Lift payload fields in-graph with APOC\" in Section 8**. Always bounded.\n\n**Inline vs blob — know which you have.** Some payloads live inline; some are offloaded.\nFor example, orchestrator steering text lives **inline** at\n`OrchestratorSteeringInjectedEvent.data.content` (a JSON string — parse it, no blob\nhop). Do not assume a `ci-blob:\u002F\u002F` indirection where the text is actually inline.\n\n### `ci-blob:\u002F\u002F` references and the `\u002Fblobs` endpoint\n\nWhen a payload exceeds the storage threshold, the server replaces `data` with a\n`ci-blob:\u002F\u002FSESSION_ID\u002FEVENT_KEY` URI. Resolve it via the blob store — the `\u002Fblobs`\nendpoint (`GET {url}\u002Fblobs\u002F{session_id}[\u002F{key}]`), or the `blob_read` tool which\nreturns a local **file path**, not content:\n\n```python\nresult = blob_read(uri=\"ci-blob:\u002F\u002FSESSION_ID\u002FEVENT_KEY\")  # -> {\"path\": \"...\", \"source\": {name, url, origin}}\n```\n\nThen extract only the fields you need with `jq` — never load a whole blob into\ncontext. `$BLOB_PATH` below is the `path` value returned by `blob_read`; use it\nverbatim rather than constructing a path, since the blob store lives under the OS\ntemp directory and differs by platform:\n\n```bash\njq 'keys' \"$BLOB_PATH\"                    # structure first\njq '.messages[-1].content' \"$BLOB_PATH\"\n```\n\n**Rules:** check for a `ci-blob:\u002F\u002F` prefix before parsing as JSON; lifted properties\nbypass blobs entirely; always bound before loading.\n\n**Going further — `blob-reading.md`, beside this file.** When you actually have to\nresolve a blob (rather than avoid one), read the level-3 reference\n`@context-intelligence:skills\u002Fcontext-intelligence-graph-query\u002Fblob-reading.md`\nwith `read_file`. It carries the full contract: when NOT to resolve at all, the\nfour-step resolution, `blob_read`'s success and failure shapes (including which\nerrors carry a `source` you must cite and which do not), the safe `jq` extraction\npatterns, and blob-vs-inline field detection.\n\n---\n\n## Section 7 — Verified Query Patterns\n\nThese are validated against the live layer-2\u002Ffoundation schema. All reference\n`$workspace` (set via the tool's `workspace` argument, Section 2) and are bounded.\n\n**Full conversation turn trace** — prompt → run → iterations → tool calls:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_PART]->(p:Prompt)\n      -[:TRIGGERS]->(run:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nRETURN p.occurred_at AS turn_start, iter.iteration_number AS iteration,\n       tc.tool_name AS tool, tc.result_success AS succeeded\nORDER BY p.occurred_at, iter.iteration_number, tc.started_at\nLIMIT 100\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` here can be a merged node spanning multiple\n> orchestrator runs — do not trust it as a pure per-run counter. For attributing tool calls to a\n> specific prompt\u002Frun, prefer the temporal join pattern below.\n\n**Tool usage per iteration** — how many tools each LLM round fired:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nRETURN iter.iteration_number AS iteration,\n       collect(tc.tool_name) AS tools, count(tc) AS tool_count\nORDER BY iter.iteration_number\nLIMIT 50\n```\n\n> **Caveat (Trap 7):** counts and groupings by `iter.iteration_number` above inherit the\n> `Iteration`-MERGE bug (one node can span up to 15 runs) — treat this as an approximate, not\n> authoritative, per-iteration tool count.\n\n**Per-prompt (or per-run) tool-call attribution — temporal join, no edges (preferred over the\n`Iteration`-based counts above; see Trap 7):**\n\nThere is no direct edge from `ToolCall` to `Prompt` or `OrchestratorRun`. The reliable way to\nattribute a tool call to the prompt (or run) that was active when it fired is a **temporal join**\non `tc.started_at` — this is *more* accurate than the structural `run → Iteration → ToolCall`\npath above, which inflates counts by +4–20% due to the Trap 7 `Iteration`-MERGE bug and silently\ndrops prompts:\n\n```cypher\n\u002F\u002F Per-prompt tool-call attribution — temporal, no new edges.\n\u002F\u002F Swap (p:Prompt)\u002Fp.occurred_at for (p:OrchestratorRun)\u002Fp.started_at for per-run.\nMATCH (tc:ToolCall {session_id:$sid}) WHERE tc.started_at IS NOT NULL\nOPTIONAL MATCH (p:Prompt {session_id:$sid}) WHERE p.occurred_at \u003C= tc.started_at\nWITH tc, p ORDER BY p.occurred_at DESC\nWITH tc, head(collect(p)) AS last_prompt\nRETURN coalesce(last_prompt.node_id,'\u003Corphan: no preceding prompt>') AS prompt_id,\n       count(tc) AS tool_calls\nORDER BY prompt_id\n```\n\n**Caveats:**\n- Use `tc.started_at` directly — it equals `ToolPreEvent.occurred_at` on 100% of rows. Do **not**\n  join via `SOURCED_FROM` for this: it fans out to *both* `tool:pre` and `tool:post` events for\n  the same call and double-counts.\n- Report an explicit **orphan bucket** (`\u003Corphan: no preceding prompt>`) rather than dropping\n  those rows — ~0.2% of tool calls occur in sub-agent sessions with no `Prompt` node, and a few\n  precede the session's first prompt.\n- Filter the ~0.007% of `ToolCall` rows with a null `started_at` (the `WHERE` above already does\n  this).\n- This is a single-writer timeline — no clock skew, no tied timestamps observed in validation.\n\n**Failed tool calls** — scope to one session; add `ORDER BY tc.started_at DESC` for\nmost-recent-first:\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)\n      -[:HAS_PART]->(iter:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)\nWHERE tc.result_success = false\nRETURN iter.iteration_number AS iteration, tc.tool_name AS tool,\n       tc.result_error AS error, tc.started_at AS failed_at\nORDER BY tc.started_at\nLIMIT 50\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` may be a cross-run merged node — a failed call\n> attributed to \"iteration 3\" may actually belong to iteration 3 of a *different* run. Cross-check\n> against the temporal join pattern above when per-run precision matters.\n\n**Delegations in a session** — which tool call triggered which agent (enumeration, not\ndepth — see Trap 1 for lineage\u002Fdepth):\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)-[:HAS_PART]->(:Iteration)\n      -[:HAS_TOOL_CALL]->(tc:ToolCall)-[:TRIGGERED]->(d:Delegation)\nRETURN d.agent, d.sub_session_id, d.is_self_delegation, d.context_depth,\n       d.started_at, tc.tool_name AS via_tool\nORDER BY d.started_at\nLIMIT 50\n```\n\n**Skills active per iteration** — note skills loaded before the first request attach to\n`Session`, not `Iteration` (Gotcha 6):\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_EXECUTION]->(:OrchestratorRun)-[:HAS_PART]->(iter:Iteration)\n      -[:HAS_SKILL_LOAD]->(sl:SkillLoad)\nRETURN iter.iteration_number, sl.skill_name, sl.content_length, sl.started_at\nORDER BY iter.iteration_number, sl.started_at\nLIMIT 100\n```\n\n> **Caveat (Trap 7):** `iter.iteration_number` can be a cross-run merged node — a skill load shown\n> under \"iteration 2\" may actually have loaded during a different run's iteration 2.\n\n**Recipe run trace:**\n\n```cypher\nMATCH (s:Session {workspace: $workspace, node_id: $session_id})\n      -[:HAS_RECIPE_RUN]->(rr:RecipeRun)-[:HAS_STEP]->(step:RecipeStep)\nOPTIONAL MATCH (step)-[:TRIGGERED]->(target)\nRETURN rr.name, rr.status, step.name,\n       labels(target) AS triggered_type, target.node_id AS triggered_id\nORDER BY step.started_at\nLIMIT 50\n```\n\n### Size discipline (the cardinal rule)\n\nThe graph can hold thousands of sessions. **Every traversal query MUST have a `LIMIT`.**\nWork as a funnel:\n\n1. **Count first.** `RETURN count(...)` costs almost nothing — run it before any wide\n   query over an unknown population.\n2. **Anchor on a session** (`node_id: $session_id` or `WHERE s.node_id IN [...]`)\n   before traversing; an un-anchored `MATCH (s:Session {workspace: $workspace})` spans\n   every session and multiplies rows.\n3. **Filter and aggregate in Cypher**, not in the client — every unneeded row is wasted\n   context.\n4. **Bound variable-length paths** (`*1..3`), never bare `*`.\n5. **Paginate** with `ORDER BY \u003Cstable key> SKIP n LIMIT m` when you genuinely need more\n   than one page.\n\n---\n\n## Section 8 — Push Work to the Database (APOC \u002F GDS)\n\n**Prefer one server-side computation over many client round-trips.** When an analysis\nneeds a deep or variable-length traversal, path-finding, centrality\u002Finfluence, community\nstructure, or a multi-step aggregation that would otherwise mean pulling rows to the\nclient and iterating, do the work **inside the graph**. It is faster and keeps the\nprotocol un-chatty — fewer tool calls, less context churn. Both plugins are installed and\n**run on the live target** (Neo4j Community here — GDS works). Rule of thumb: **if\nanswering the question the naive way means many round-trips or a large client-side pull,\npush that computation into the database.** Probe once, compute server-side, and still\n**bound the RETURNED result** (count \u002F aggregate \u002F `LIMIT`).\n\nProbe availability: `CALL apoc.help('path')`, `RETURN gds.version()` (or `CALL gds.list()`).\n\n- **APOC — in-graph helpers.** `apoc.convert.fromJsonMap(e.data)` parses a `data` JSON\n  string into a map without a client `jq` hop. `apoc.path.expandConfig(...)` runs\n  configurable in-graph traversals — subject to the **same 1-hop caveat as\n  `HAS_SUBSESSION`** (Trap 1): for delegation depth, expand over `FORKED`\u002F`parent_id`.\n  Still bound your output.\n- **GDS — real graph analytics in one pass.** For centrality\u002Finfluence, community\u002Fcluster\n  detection, or pathfinding **across many sessions**, project once and run the algorithm\n  server-side instead of reconstructing the graph client-side. Always `gds.graph.drop`\n  the projection when done:\n\n  ```cypher\n  CALL gds.graph.project('g', 'Session', 'FORKED')\n  YIELD nodeCount, relationshipCount;\n\n  CALL gds.pageRank.stream('g')          \u002F\u002F or gds.degree \u002F gds.betweenness (influence)\n  YIELD nodeId, score\n  RETURN gds.util.asNode(nodeId).node_id AS session, score\n  ORDER BY score DESC LIMIT 20;\n\n  CALL gds.wcc.stream('g')               \u002F\u002F or gds.louvain (community structure)\n  YIELD componentId\n  RETURN componentId, count(*) AS size ORDER BY size DESC LIMIT 20;\n\n  CALL gds.graph.drop('g') YIELD graphName;   \u002F\u002F always release the projection\n  ```\n\n  **Agent-level, not session-level — agents are a PROPERTY to roll up by, not a projectable graph.**\n  \"Which *agent* is a hub\" ≠ \"which *session* is a hub\": one session can be highly central\n  while its agent is not. `Agent` concept nodes *do* exist (Section 1; Gotcha 4), but they\n  carry only inbound `HAS_AGENT` edges from sessions — there is **no Agent→Agent edge\n  structure to project**, so GDS centrality has nothing to traverse at the agent level. Do NOT\n  `gds.graph.project.cypher(...)` an agent-level graph; it returns an empty projection and\n  is a dead end. Instead **project the labels that exist** — `Session` (nodes) and `FORKED`\n  (relationships) — run the algorithm over *sessions*, then **roll the per-session scores up\n  by the `Session.agent` property in the `RETURN`**. For the named-agent-hub ranking, drop\n  the `self` marker and the root\u002Fmain session (`agent IS NULL`) — a \"which named agent\"\n  question is about named agents:\n\n  ```cypher\n  \u002F\u002F 'g' is the Session \u002F FORKED projection from above — agents are NOT projected;\n  \u002F\u002F agent-level results come from aggregating session scores by the s.agent PROPERTY.\n  CALL gds.pageRank.stream('g') YIELD nodeId, score\n  WITH gds.util.asNode(nodeId) AS s, score\n  WHERE s.agent IS NOT NULL AND s.agent \u003C> 'self'   \u002F\u002F named agents only (see Trap 4 for 'self')\n  RETURN s.agent AS agent, round(sum(score), 2) AS agent_influence, count(*) AS sessions\n  ORDER BY agent_influence DESC LIMIT 20\n  ```\n\n  Excluding `self` here answers \"which *named* agent is a hub\" — it does **not** make `self`\n  vanish from the story. If the question also touches self-delegation, resolve `self` up the\n  `FORKED` chain (Trap 4) and **state that root\u002Fmain-vs-named breakdown as its own finding**;\n  don't let the exclusion above silently drop it.\n\n  Skip GDS for a simple count or grouping that one plain Cypher statement already does —\n  reach for it when the naive alternative is iterative client-side fetching.\n\n---\n\n### Lift payload fields in-graph with APOC (the one authoritative pattern)\n\n**Problem.** `Event.data` (and nested payloads) is a serialized JSON **string**, not a Cypher\nmap. Dot-access like `e.data.working_dir` **fails loudly** — Neo4j raises:\n\n```\nNeo.ClientError.Statement.TypeError: Type mismatch: expected a map but was String(...)\n```\n\nYou **must** parse the string before reading a field. (Do **not** confuse this with the\nsilent-null trap in **Trap 5** — that one is comparing a *zoned datetime to a string literal*,\nwhich returns `null` with no error. This APOC case is a *loud* error, a different failure mode.)\n\n**Pattern — parse in-graph, project only the scalar you need:**\n\n```cypher\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN d.working_dir AS working_dir      \u002F\u002F one scalar, not the whole payload\n```\n\nUse `apoc.convert.fromJsonList(...)` for arrays. **Never** pull the full `data` string to the\nclient to parse with `jq`.\n\n**Two wins:**\n- **Correctness** — dot-access on the string errors out; parsing with APOC is the *only* way to\n  read the real value.\n- **Leanness** — the parse **and** the field-selection happen server-side; the payload never\n  crosses the wire or enters context — same discipline as Section 6 (\"never return full text\").\n\n**Rule.** Prefer lifted first-class properties when they exist; when a field is **only** in the\npayload, lift it in-graph with APOC and return just the scalar. (`working_dir` is the worked\nexample here — proven, and the whole point of the pattern — but per Section 2, it is **not**\na scoping lever: it is ~0% queryable in-graph (0 of 4,504 \u002F 0 of 9,086 sessions — not a lifted\nSession property) with no canonical per-session source, so use it to read one session's\ndirectory, never to scope a population.)\n\n### Aggregate a numeric JSON-string payload field: total cost (Trap 6)\n\nThe same parse-in-graph idiom extends past reading a scalar to **aggregating** a numeric value\nburied inside a JSON string. `cost_usd` lives at `data.usage.cost_usd` on `:LlmResponseEvent`,\nitself a string inside the parsed map — cast it with `toFloat()` before summing, and pin the\nlabel to avoid the triple-count described in Trap 6:\n\n```cypher\nMATCH (e:LlmResponseEvent) WHERE e.data CONTAINS 'cost_usd'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN sum(toFloat(d.usage.cost_usd)) AS total_cost_usd\n```\n\n**Rules:**\n- Pre-filter with `WHERE e.data CONTAINS 'cost_usd'` before parsing — cheaper than parsing every\n  `LlmResponseEvent` just to find the field absent.\n- Pin the label to `:LlmResponseEvent` — do not sweep `:SST_EVENT` or join through\n  `ContentBlockEndEvent` \u002F `ProviderResponseEvent` for the same figure (Trap 6).\n- Always `toFloat()` the nested value — it deserializes from JSON as a string, and a bare\n  `sum()` over it returns HTTP 500, not a wrong number.\n\n### Aggregate token + cost usage from LlmResponseEvent payloads (Trap 8)\n\nToken and cache-usage fields sit alongside `cost_usd` in the same `data.usage` map, but they\ndeserialize as **INTEGER**, not a JSON string — `toInteger()` is defensive here, not required the\nway `toFloat()` is for `cost_usd`. The scoping danger is identical to Trap 6: an unpinned `:Event`\nsweep also matches `ContentBlockEndEvent` and `ProviderResponseEvent`, each replaying the same\nresponse-level `usage` map, and inflates every total by **~3.4x** regardless of field type.\n\n**Validated combined cost + tokens (canonical — aggregate == independent reduce == hand-summed\nsubset):**\n\n```cypher\nMATCH (e:LlmResponseEvent)\nWHERE e.data CONTAINS 'input_tokens'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN count(*)                                            AS llm_calls,\n       sum(toFloat(d.usage.cost_usd))                      AS total_cost_usd,\n       sum(toInteger(d.usage.input_tokens))                AS input_tokens,\n       sum(toInteger(d.usage.output_tokens))               AS output_tokens,\n       sum(toInteger(d.usage.cache_read_input_tokens))     AS cache_read_tokens,\n       sum(toInteger(d.usage.cache_creation_input_tokens)) AS cache_creation_tokens\n```\n\n**Per-model breakdown:**\n\n```cypher\nMATCH (e:LlmResponseEvent)\nWHERE e.data CONTAINS 'input_tokens'\nWITH apoc.convert.fromJsonMap(e.data) AS d\nRETURN d.model AS model, d.provider AS provider, count(*) AS calls,\n       round(sum(toFloat(d.usage.cost_usd)), 2)            AS cost_usd,\n       sum(toInteger(d.usage.input_tokens))                AS input_tokens,\n       sum(toInteger(d.usage.output_tokens))                AS output_tokens,\n       sum(toInteger(d.usage.cache_read_input_tokens))     AS cache_read,\n       sum(toInteger(d.usage.cache_creation_input_tokens)) AS cache_creation\nORDER BY cost_usd DESC LIMIT 15\n```\n\nAdd `{workspace: $workspace}` to the `MATCH (e:LlmResponseEvent ...)` anchor for a per-workspace\nvariant of either query.\n\n**Validated global scale (real corpus snapshot, reader's sanity check):** ~68k LLM calls → ~$45.5K\ncost, input ~4.50B tokens, output ~80.6M tokens, cache_read ~4.62B tokens, cache_creation ~8.05B\ntokens. The same query with the `:LlmResponseEvent` pin removed reports ~$156K (~3.4x) — wrong,\nfor the exact reason given in Trap 8.\n\n**Rules:**\n- Use `cache_read_input_tokens` and `cache_creation_input_tokens` — both complete. Do **not** sum\n  `cache_read_tokens` (sparse\u002FNULL on many events) or `cache_creation` (a MAP, not a number).\n  `cache_write_tokens` is close to `cache_creation_input_tokens` but diverges ~1.5% — prefer\n  `cache_creation_input_tokens`.\n- Never source tokens or cost from `Iteration.usage_*` (Trap 7) — last-write-wins corrupted, and\n  `Iteration` has no cost field at all.\n- Report `llm_calls` alongside cost and tokens — ~4% of events have a null `cost_usd` (`sum()`\n  ignores nulls so the total is still correct), and some non-priced models report `$0` cost with\n  real token counts. Surfacing `llm_calls` and the token columns makes that gap visible instead of\n  hiding it inside a dollar figure.\n\n---\n\n## Gotchas\n\n1. **Layer-2 \u002F foundation nodes only exist if handlers ran, or the feature was used.**\n   Sessions ingested before a handler was deployed, or with no delegation\u002Fskills\u002Frecipes,\n   simply lack those nodes. Use `OPTIONAL MATCH` when joining them against arbitrary\n   sessions.\n\n2. **`result_success = false` is the error path;** `result_error` holds the message. A\n   null `result_success` means the post\u002Ferror event hasn't been processed — the call is\n   in-flight or its handler didn't run.\n\n3. **`ENABLES` edges are sparse.** The `OrchestratorRun → next Prompt` edge exists only\n   for multi-turn chains (N prompts ⇒ N−1 edges). Do not use its presence to judge clean\n   session termination.\n\n4. **Concept nodes (`Agent`, `Recipe`, `Orchestrator`) are shared across sessions,**\n   merged by name. Querying `(a:Agent)` without a session anchor spans everything — reach\n   them through the session (`HAS_AGENT` from the sub-session, `HAS_RECIPE` from a\n   `RecipeRun`).\n\n5. **`SOURCED_FROM` may be absent on older sessions.** Detect and fall back to the\n   Section 4 fallback joins:\n\n   ```cypher\n   MATCH (n:SST_EVENT) WHERE NOT (n)-[:SOURCED_FROM]->() AND NOT n:Session\n   RETURN labels(n), count(*)\n   ```\n\n6. **`SkillLoad` may attach to `Session`, not `Iteration`.** Skills loaded before the\n   first request have no active iteration. Add\n   `OPTIONAL MATCH (s)-[:HAS_SKILL_LOAD]->(sl:SkillLoad)` to catch session-level loads.\n\n7. **`IncompleteSession` is a health marker, not a terminal label.** It reached\n   `session:end` with no captured `session:start`\u002F`fork`, carries `has_terminal: false`,\n   and none of `:RootSession`\u002F`:SubSession`\u002F`:ForkedSession`. Exclude it from normal\n   terminal-session queries with `WHERE NOT s:IncompleteSession`. A spike in its count\n   signals upstream event loss.\n\n8. **The node MERGE key is `{node_id, workspace}`.** The same logical entity (e.g. an\n   Orchestrator named `loop-streaming`) exists as a separate node per workspace. A\n   cross-workspace query returns one node per workspace — account for this when\n   aggregating.\n",{"name":5,"license":15,"metadata":38,"visibility":48,"description":50},{"version":39,"changelog":40},"2.6.0",[41,42,43,44,45,46,47],"2.6.0: Absorbed the former standalone `blob-reading` skill as the level-3 reference `blob-reading.md` beside this file (same content, verbatim), and pointed Section 6 at it. The catalog now carries one always-visible entry for graph work instead of two; nothing was dropped - the full blob contract (when not to resolve, the four-step resolution, blob_read's success\u002Ffailure shapes and which errors carry a citable `source`, the safe jq patterns, blob-vs-inline detection) moved intact into the reference file.","2.5.0: Elevated the anti-multi-count guidance scattered across Trap 6 (cost) and Trap 8 (tokens) into a single named cross-cutting rule (Section 3: \"Rule — one owner per numeric fact: never multi-count cost or tokens by over-reading events\") — every usage value is emitted once per LLM turn, owned by :LlmResponseEvent, and echoed onto ContentBlockEndEvent \u002F ProviderResponseEvent; aggregating across the wider set sums it 2–3x over. Trimmed Trap 6 and Trap 8 to reference the rule instead of each restating the mechanism; both traps keep only their field-specific specifics (cost_usd is a STRING requiring toFloat; token\u002Fcache fields are INTEGER with the correct cache field names). Section 8 queries unchanged.","2.4.0: Added Trap 8 (token & cache usage fields on :LlmResponseEvent are payload INTEGERs — unlike cost_usd's STRING — but still require pinning to :LlmResponseEvent; an unpinned :Event sweep replays the same usage map via ContentBlockEndEvent\u002FProviderResponseEvent and inflates every total by ~3.4x, measured: cost 3.43x, input 3.31x, output 3.63x, cache 3.39x) with a validated combined cost+token aggregation and a per-model breakdown pattern in Section 8. Documented the correct cache field names (cache_read_input_tokens, cache_creation_input_tokens — complete) versus the sparse\u002Fwrong-typed alternatives (cache_read_tokens, cache_creation), and reinforced that Iteration.usage_input\u002Fusage_output\u002Fusage_cache_write (Trap 7) must never be used for token or cost aggregation — they are last-write-wins corrupted and Iteration carries no cost field at all.","2.3.0: Corrected the working_dir population figure from an inaccurate ~7% (238\u002F3,259 sessions) down to its true measured value: ~0% queryable in-graph (0 of 4,504 \u002F 0 of 9,086 sessions — not a lifted Session property); workspace remains ~100% and the only valid scoping lever. Added Trap 6 (cost_usd totals: a JSON-string payload field, not a lifted property; a bare sum() returns HTTP 500; must pin to :LlmResponseEvent to avoid triple-counting against ContentBlockEndEvent \u002F ProviderResponseEvent) with a validated APOC aggregation pattern in Section 8. Added Trap 7 (Iteration.node_id MERGEs across orchestrator runs, corrupting iteration-scoped counts\u002Faggregates by up to +66%) with caveats on every affected iteration-based query pattern. Added a validated temporal-join pattern (Section 7) for per-prompt\u002Fper-run tool-call attribution — more accurate than the structural run→Iteration→ToolCall path and unaffected by the Trap 7 MERGE bug.","2.2.0: Corrected the Section 2 workspace\u002Fworking_dir claims — workspace is a lifted, reliable (~100%) scoping slug, not the literal working directory; working_dir is ~0% queryable in-graph (0 of 4,504 \u002F 0 of 9,086 sessions — not a lifted Session property) payload-only field with no canonical per-session source, and must never be used to scope a population. Consolidated one visible+correct in-graph APOC payload-parse pattern: dot-access on a JSON-string field (e.g. e.data.working_dir) raises a type error, not a silent null — parse in-graph with apoc.convert.fromJsonMap and return only the scalar needed. De-duplicated the Section 6 copy into a pointer at this pattern.","2.1.0: Reporting guidance — agent-level \u002F self-delegation answers must STATE the resolved self→actor breakdown (root\u002Fmain vs named) as an explicit standalone finding, not fold it into a blended statistic. Tightened the Section 8 agent-rollup: project Session\u002FFORKED (the labels that exist) and roll up by the Session.agent property; agents are NOT a projectable Agent graph.","2.0.1: Schema accuracy fix: corrected Prompt\u002FSkillLoad\u002FOrchestrator property names, RecipeStep\u002FRecipeRun property placement, RecipeStep→RecipeRun edge (SPAWNED), removed phantom L1 edges, fixed example queries — all validated against the live graph.",{"summary":49,"priority":14},"Query the context-intelligence property graph for session history, tool traces, delegation trees, skills, and recipe orchestration via verified Cypher patterns.","Use when querying the context-intelligence property graph for session history, tool call traces, LLM iteration analysis, execution scale metrics, agent delegation trees, skill loading, and recipe orchestration. Covers all graph layers, cross-layer SOURCED_FROM joins, SST navigation, blob handling, and verified Cypher patterns.\n","main","bf7cfbc82bdfd356e86f7a849622772537956b17","skills\u002Fcontext-intelligence-graph-query",7,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-14 02:56:59",{"source":8,"skillName":5,"commands":61,"prompt":71,"zipAvailable":72},[62,65,68],{"tool":63,"command":64},"npx","npx skills add microsoft\u002Famplifier-bundle-context-intelligence --skill context-intelligence-graph-query",{"tool":66,"command":67},"bunx","bunx skills add microsoft\u002Famplifier-bundle-context-intelligence --skill context-intelligence-graph-query",{"tool":69,"command":70},"pnpm","pnpm dlx skills add microsoft\u002Famplifier-bundle-context-intelligence --skill context-intelligence-graph-query","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fcontext-intelligence-graph-query\u002FSKILL.md",true,[74],{"path":75,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":78,"sourceHtmlUrl":79},"blob-reading.md","REFERENCE","参考文档",5186,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fcontext-intelligence-graph-query\u002Fblob-reading.md",[81,92,101,118,126,134],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":88,"publishedAt":33},"01M69BGZVPJBKSZV5N0WMVGR3N","workflow-pattern-analysis","Use when asked how a workflow is failing, what separates a successful run from a failing one, which steps are the most common failure points, or what patterns recur across sessions — analyses failure\u002Fsuccess patterns and signals across many runs using context-intelligence session data.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fworkflow-pattern-analysis\u002FSKILL.md",[87],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[89,90,91,29,25],"workflow pattern analysis","workflow","reasoning",{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":33},"01M69BGYAYC16BRSB0EGMCKMF5","transcript","Retrieve the prior user and assistant conversation verbatim from the current Context Intelligence capture, or named sessions. Use when a user asks to replay, quote, review, or act on a transcript.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Ftranscript\u002FSKILL.md",[],[94,99,100,25],"review","assistant",{"skillNo":102,"name":103,"nameZh":6,"description":104,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":105,"categories":106,"keywords":115,"publishedAt":33},"01M69BGWQGCEN13DHHAQGTQ609","context-intelligence-tool-design","Use when selecting a detection strategy and implementation primitive for a context-intelligence signal. Classifies signals as deterministic, probabilistic, llm-evaluated, or hybrid, and applies the cheapest-sufficient-capability principle.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fcontext-intelligence-tool-design\u002FSKILL.md",[107,110],{"categoryNo":108,"categoryName":109,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":111,"categoryName":112,"taxonomyType":113,"taxonomyTypeDesc":114},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",[116,117,91],"design","context intelligence tool design",{"skillNo":119,"name":120,"nameZh":6,"description":121,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":122,"categories":123,"keywords":124,"publishedAt":33},"01M69BGVHJJFCNZMGM21FNWMFS","context-intelligence-session-reconstruction","Use when local Amplifier session files are missing, broken, or incomplete — rebuilds events.jsonl, transcript.jsonl, and metadata.json from the context-intelligence graph server.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fcontext-intelligence-session-reconstruction\u002FSKILL.md",[],[125],"context intelligence session reconstruction",{"skillNo":127,"name":128,"nameZh":6,"description":129,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":130,"categories":131,"keywords":132,"publishedAt":33},"01M69BGT14VQD1M3P12WZGTQCS","context-intelligence-session-navigation","Use for workspace-scoped local session discovery and event, tool, error, or capture inspection in JSONL files, independent of graph availability.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fcontext-intelligence-session-navigation\u002FSKILL.md",[],[133],"context intelligence session navigation",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":146,"publishedAt":33},"01M69BGRET24BR6PTYANVE57F4","context-intelligence-server-data-ops","Exact step order, wording, and the \"session details\" block format for the delete flows the server-data-ops agent drives — delete the current session, clean up every session from the current working directory, find-then-delete a session by description, and delete a session someone else created.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Famplifier-bundle-context-intelligence\u002Fblob\u002Fmain\u002Fskills\u002Fcontext-intelligence-server-data-ops\u002FSKILL.md",[140,143],{"categoryNo":141,"categoryName":142,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":144,"categoryName":145,"taxonomyType":113,"taxonomyTypeDesc":114},"01JBSEEDCAT000000000000033","数据分析师",[147,31,148],"context intelligence server data ops","delete"]