- name
- context-intelligence-graph-query
- description
- 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.
- license
- MIT
- visibility
- {"priority":5,"summary":"Query the context-intelligence property graph for session history, tool traces, delegation trees, skills, and recipe orchestration via verified Cypher patterns."}
- metadata
- {"version":"2.6.0","changelog":["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/failure 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 / 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/cache 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/ProviderResponseEvent 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/wrong-typed alternatives (cache_read_tokens, cache_creation), and reinforced that Iteration.usage_input/usage_output/usage_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/3,259 sessions) down to its true measured value: ~0% queryable in-graph (0 of 4,504 / 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 / ProviderResponseEvent) with a validated APOC aggregation pattern in Section 8. Added Trap 7 (Iteration.node_id MERGEs across orchestrator runs, corrupting iteration-scoped counts/aggregates by up to +66%) with caveats on every affected iteration-based query pattern. Added a validated temporal-join pattern (Section 7) for per-prompt/per-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/working_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 / 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 / self-delegation answers must STATE the resolved self→actor breakdown (root/main vs named) as an explicit standalone finding, not fold it into a blended statistic. Tightened the Section 8 agent-rollup: project Session/FORKED (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/SkillLoad/Orchestrator property names, RecipeStep/RecipeRun property placement, RecipeStep→RecipeRun edge (SPAWNED), removed phantom L1 edges, fixed example queries — all validated against the live graph."]}
## When the graph server is not configured
If the context-intelligence server is unavailable or not configured for this
session, graph-based analysis cannot proceed here. Delegate immediately to
`session-navigator` for all session analysis, event lookup, and delegation
tree tracing — do not attempt Cypher queries against a server that isn't
reachable.
---
# Context Intelligence Graph Query
This skill equips you to navigate and extract insights from the context-intelligence
property graph using the `graph_query` tool. The graph holds a complete record of
every Amplifier session — what happened, when, how things connect, and at what scale.
**Read the live schema from the server; read the meaning from this skill.** The
server can always tell you the current shapes — labels, relationship types, property
names. It CANNOT tell you what those shapes mean, where they lie about their meaning,
or how to scope, join, and bound a query safely. This skill deliberately does not
freeze a schema dictionary (it goes stale and misleads). It keeps the grammar: the
scoping levers, the traps, the cross-layer joins, and the verified patterns.
---
## Section 1 — Introspect the Live Schema First
Before writing analytical Cypher against an unfamiliar area of the graph, ask the
server what it actually contains. These calls are cheap and always current.
```cypher
CALL db.labels() // every node label present
CALL db.relationshipTypes() // every edge type present
CALL db.schema.nodeTypeProperties() // property names + types per label
CALL db.schema.visualization() // label/edge connectivity overview
```
To learn the real properties on a specific label (names AND which are populated),
sample a few live nodes rather than trusting any documented list:
```cypher
MATCH (n:ToolCall) WITH n LIMIT 25
UNWIND keys(n) AS k
RETURN k AS property, count(*) AS present_on_n_nodes
ORDER BY present_on_n_nodes DESC
```
**The three-layer shape (stable orientation, not a dictionary):**
- **Data layer 1 — raw events.** Every kernel event preserved as an `:Event` node
(`:ToolPreEvent`, `:LlmResponseEvent`, `:PromptSubmitEvent`, …). Answers *what
happened and when*. `node_id` uses a `__` (double-underscore) separator.
- **Data layer 2 — semantic entities.** Events assembled into `Session`,
`OrchestratorRun`, `Iteration`, `ContentBlock`, `ToolCall`, `Prompt`, and more,
connected by typed relationships. Answers *what ran, how, at what scale*. `node_id`
uses a `::` (double-colon) separator (a few types — `ToolCall`, concept nodes — use
a bare identifier).
- **Foundation layer — above the kernel.** Delegation trees (`Delegation`, `Agent`),
skill loads (`SkillLoad`), recipe orchestration (`RecipeRun`, `RecipeStep`,
`Recipe`).
All layers coexist in one graph, bridged by **`SOURCED_FROM`** edges: every layer-2
entity points back to the layer-1 event(s) that produced it (Section 4). Use
`db.schema.nodeTypeProperties()` to confirm exact property names before you rely on
them — the sections below cover only the meaning the server cannot self-describe.
### Reading results & citing the source (required)
`graph_query` returns `{"source": {"name", "url", "origin"}, "rows": [...]}` on
success. Read your data from `rows`. ALWAYS report `source.name` (its `origin` is
`source`, `destination`, or `env`) in your answer — the user must know which
endpoint the outcome came from.
On **failure**, `source` is present only when an endpoint was actually chosen:
**endpoint-level** errors (`connection_error`, `timeout`, `http_status`,
`decode_error`, and input-validation errors like a missing/invalid `query` that
occur *after* selection) carry `error.source` — cite it. **Selection/config**
errors that occur *before* any endpoint is chosen (`ambiguous_source_selection`,
`unknown_source`, `source_misconfigured`, `configuration_error`) have **no**
`source` — there is no single endpoint to name, so report that selection failed
rather than inventing one.
To discover what you can reach, call `graph_query` with `list_sources: true` — it
returns the connectable set (`{"connectable_set": [{name, url, origin}, ...]}`)
without running a query. To target a specific endpoint (a configured read source
OR a hook upload destination), pass `source: "<name>"`.
---
## Section 2 — Scoping (Mandatory, and Not Self-Describing)
Scoping is the single most error-prone part of querying this graph, and the server
cannot tell you how its client scopes. Get this right first.
### The `workspace` tool argument — NOT a Cypher param
**The `graph_query` tool reads a top-level `workspace` argument.** It injects that
value as `$workspace` into your Cypher. Do **not** try to set the workspace through
`params: {"workspace": ...}` — the tool does not read the workspace from `params`, and
a mis-set workspace silently scopes to a tiny `"default"` decoy partition instead of
the real one. If your counts look implausibly small (a handful of sessions where you
expected hundreds), you are almost certainly scoped to the decoy.
```cypher
// Correct: reference $workspace; set the workspace via the tool's top-level
// `workspace` argument, not via params.
MATCH (s:Session {workspace: $workspace})
RETURN count(s) AS session_count
```
### The two real scoping levers
Both live as first-class properties on nodes — confirm them by sampling:
- **`created_by`** — *who* produced the data (present on every node; the data is
typically single-user).
- **`workspace`** — *where*: a **lifted, dash-slugified label/slug** (e.g.
`-mnt-linuxdata-workspaces-...-dashboard-ui`), reliable at ~100% coverage. It is a **label,
not the literal working directory**: the true directory lives in the event `data` payload as
`working_dir`, and the **same `working_dir`** (`/home/user/project`) can map to **different**
`workspace` slugs (`myproject`, `proj`, `multi`). Treat `workspace` as the reliable scoping
slug; treat `working_dir` as a **payload field, ~0% queryable in-graph (0 of 4,504 / 0 of
9,086 sessions — not a lifted Session property)** with no canonical per-session source — read
it via the APOC parse in Section 8, but do **not** scope by it. The `/cypher` request's own
`workspace` scope is a second, orthogonal server-side filter layered on top.
**Always anchor scope on the first `MATCH`** so the workspace index is used:
```cypher
// CORRECT — workspace on the anchor node
MATCH (s:Session {workspace: $workspace, node_id: $session_id})
-[:HAS_EXECUTION]->(run:OrchestratorRun)
RETURN run.orchestrator_name, run.started_at
// INCORRECT — workspace on a downstream node misses the index
MATCH (s:Session {node_id: $session_id})
-[:HAS_EXECUTION]->(run:OrchestratorRun {workspace: $workspace})
RETURN run.orchestrator_name, run.started_at
```
To discover which workspaces exist before scoping, aggregate across all of them
(use sparingly — it skips the partition index):
```cypher
MATCH (s:Session)
WHERE s.workspace <> ''
RETURN s.workspace, s.created_by, count(s) AS sessions
ORDER BY sessions DESC
LIMIT 25
```
### Resolving "my" — use `whoami`, don't guess
`created_by` scopes by *who produced the data*, but the graph never tells you
who "you" are. When a request is scoped to the acting user themselves —
"my sessions", "sessions I created", "what have I been working on" — call the
`whoami` tool for the relevant server before filtering on `created_by`:
```python
whoami() # default endpoint
whoami(source="prod") # a specific configured endpoint
```
It returns `{"contributor_id": "<github-id-or-null>", "source": {...}}`. Use
`contributor_id` as the value to match against `created_by`:
```cypher
MATCH (s:Session {workspace: $workspace, created_by: $contributor_id})
RETURN count(s) AS my_sessions
```
**Null case.** A null `contributor_id` means auth is disabled (or otherwise
unknown) on that server — there is no acting-user identity to filter by. Do
**not** guess or fall back to some other field. Ask the user how they want to
scope the query instead (e.g. by name, by workspace, or "show everyone's").
---
## Section 3 — Traps: Where the Shapes Lie About Their Meaning
These are the highest-value items in this skill. Each is a place where reading the
schema literally leads you to a **wrong answer that looks rigorous**. The server will
happily hand you the misleading shape; only domain knowledge corrects it.
### Trap 1 — Delegation lineage: use `FORKED` + `Session.parent_id`, not the edge walk
The true delegation/fork lineage is carried by the **`FORKED` edge** together with
**`Session.parent_id`**. This is the authoritative source for delegation depth and
ancestry. Verified live: the fork lineage reaches **max depth 3**.
```cypher
// Authoritative lineage depth via the FORKED edge.
// Bound the path (never bare `*` — Section 7): *1..20 is generous headroom over the
// observed max depth of 3, and the RETURN is aggregate-only (no rows materialized).
MATCH path = (root:Session {workspace: $workspace})-[:FORKED*1..20]->(leaf:Session)
WHERE root.parent_id IS NULL
RETURN max(length(path)) AS max_fork_depth, count(path) AS lineages
```
`Session.parent_id` lets you reconstruct the same chain by property when an edge walk
is inconvenient (each child names its parent):
```cypher
MATCH (child:Session {workspace: $workspace})
WHERE child.parent_id IS NOT NULL
RETURN child.node_id, child.parent_id
ORDER BY child.node_id LIMIT 200 // bounded (Section 7); anchor on a node_id to walk one specific chain
```
**Two known undercounts — do NOT trust either for depth:**
- **`HAS_SUBSESSION` is a single hop only.** It materializes only the immediate
parent→child link; each sub-session is ingested as its own root, so a multi-hop walk
`(:Session)-[:HAS_SUBSESSION*]->()` collapses to depth 1. It is a one-level lookup,
**not** the delegation tree.
- **The `Delegation` property-join (`sub_session_id → parent_session_id`) undercounts
beyond hop 1.** It is more complete than the `HAS_SUBSESSION` walk (it finds a second
hop), which is exactly why it is dangerous: it *looks* like the answer and reports
depth 2, but it still misses the deeper lineage that `FORKED`/`parent_id` reveals.
Use it to enumerate individual delegations, not to measure tree depth.
### Trap 2 — `context_depth` / `context_scope` are inheritance modes, NOT tree depth
On `Delegation` nodes, `context_depth` and `context_scope` describe **how much parent
context a delegation inherited** — an inheritance mode with values like `none`,
`recent`, `all`. The name invites reading it as nesting/tree depth. It is not.
Self-delegations are almost always `none`; regular delegations spread across
`none` / `recent` / `all`.
### Trap 3 — Tool data lives on `ToolCall`, not `ToolExecution`
`:ToolCall` carries the tool data you want: `tool_name`, `tool_input`, `started_at`,
`ended_at`, `result_success`, `result_error`, `result_output`. `:ToolExecution` is
**property-less — a dead end**; do not route tool queries through it. `ToolCall` is the
correct, simple source (verified: `tool_name` and `started_at` populated on 100% of
`ToolCall` nodes in a real workspace).
```cypher
MATCH (t:ToolCall {workspace: $workspace})
RETURN t.tool_name AS tool, count(*) AS n
ORDER BY n DESC LIMIT 12
```
### Trap 4 — `self` is a self-delegation marker, never a real agent
Self-delegation is recorded explicitly as **`Delegation.is_self_delegation = true`**
(equivalently the child `Session.agent = 'self'`). Never infer it by string-matching
`tool_input` or comparing names.
**`'self'` is a marker, not an identity** — it means "this session spawned a fresh
sub-context of *itself*," so `self` is never the real actor. Before any per-agent stat
(centrality, counts, durations), **resolve it up the fork chain** to the nearest ancestor
whose `agent <> 'self'`. That actor is often the **root/main session** (`agent IS NULL` —
the user's top-level session, not a named sub-agent); occasionally a named agent that
self-delegated. Empirically the chain is a single hop (no `self`→`self`→…).
```cypher
// Resolve every self-session to the actor that actually spawned it
MATCH (s:Session {workspace: $workspace, agent: 'self'})
OPTIONAL MATCH pth = (anc:Session)-[:FORKED*1..20]->(s)
WHERE anc.agent IS NULL OR anc.agent <> 'self'
WITH s, anc, pth ORDER BY length(pth) ASC
WITH s, head(collect(coalesce(anc.agent, 'root/main session'))) AS actor
RETURN coalesce(actor, 'unresolved') AS real_actor, count(*) AS self_delegations
ORDER BY self_delegations DESC
```
Self-delegation is itself a **first-class signal** (an actor extending its own context —
continuation / context management): count it *as* self-delegation when that is the
question, and *resolve* it when you need the real actor. A raw `self` bucket in a
per-agent aggregate is a bug — it attributes real work to a non-existent agent.
**Report the resolved breakdown as an explicit finding — do not bury it in a blended
stat.** The resolve-self join above only pays off if its result reaches the answer as
its *own stated fact*. For any agent-level or self-delegation question, **lead with the
resolved `self`→actor split as a standalone sentence** — how many, and what percent, of
self-delegations resolve to the **root/main session continuing itself** (`agent IS NULL`)
versus to **named agents** — *before* any roll-up or combined figure. Computing the
number correctly but folding it into a mixed "X% of all delegations are root-launched"
statistic hides the very fact the question asked for. This is a general output-shape
principle: surface the load-bearing breakdown first, then add supporting context — state
it, don't just compute it.
```cypher
// The plain self-delegation split (the signal itself)
MATCH (d:Delegation {workspace: $workspace})
RETURN d.is_self_delegation AS self_delegation, count(*) AS n
ORDER BY n DESC
```
### Trap 5 — Timestamps are ZONED DATETIME; durations are computed
Every `*_at` property (`started_at`, `ended_at`, `occurred_at`, …) and `last_updated`
— on nodes and on the edges that carry `occurred_at` (`HAS_EVENT`, `HAS_SUBSESSION`,
`FORKED`) — is stored as a native Neo4j **`ZONED DATETIME`**, not a string.
- ❌ `WHERE s.started_at > '2026-05-01'` — comparing to a string literal silently
returns nothing (no error raised).
- ✅ `WHERE s.started_at > datetime('2026-05-01')` — wrap every literal in `datetime()`.
- ✅ Rolling windows: `WHERE s.started_at > datetime() - duration('P30D')`.
**Durations are not stored — compute them** with `duration.between()`:
```cypher
MATCH (t:ToolCall {workspace: $workspace})
WHERE t.started_at IS NOT NULL AND t.ended_at IS NOT NULL
WITH t, duration.between(t.started_at, t.ended_at) AS d
RETURN t.tool_name AS tool, d.seconds AS secs
ORDER BY secs DESC LIMIT 8
```
### Rule — one owner per numeric fact: never multi-count cost or tokens by over-reading events
This is a cross-cutting rule, not a single trap — Traps 6 and 8 below are both instances of it.
Every numeric usage value — dollars (`cost_usd`) and every token field (`input_tokens`,
`output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`) — is emitted **once**
per LLM turn and is **owned** by the `:LlmResponseEvent` node. The identical `usage` map is then
**echoed** onto other events of the same turn: `ContentBlockEndEvent` (streaming replays it ~2.33x
Voir sur GitHub