How Threadline works
FalkorDB is the only database. It holds the company brain (one graph), each visitor's memory (one graph per visitor), the shared blackboard where the Investigator and Verifier agents coordinate, the stored traces that replay runs, the vector index used by both the agent and the baseline, the rate-limit counters, and even the 3D layout. Take FalkorDB out and nothing is left.
Pipeline
- Fetch: public GitHub GraphQL for six FalkorDB repositories plus the docs repo, cached as JSONL.
- Normalise: deterministic mapping to nodes and edges with stable keys; references parsed from timelines and bodies; concepts from a fixed dictionary.
- Embed: text-embedding-3-small at 256 dimensions, cached by content hash (total spend so far about four US cents).
- Load:
UNWINDbatches withMERGEonkey, then derived edges in Cypher. About 41 MB of the free tier's 100 MB. - Ask: the Investigator (Groq gpt-oss, or a capped OpenAI gpt-5.4-mini fallback when every Groq model has hit its daily limit) calls typed graph tools; every tool returns rows, node keys, traversed edges and its Cypher, streamed to the browser. The fallback allows 4 calls of at most 2,000 output tokens per run, 10 runs a day and 25 a month across all visitors. At OpenAI's Standard prices ($0.75 input, $0.075 cached input, $4.50 output per million tokens) a typical run costs about $0.0032, a worst-case run about $0.07, and a month at the cap at most about $1.84. Recorded runs never call a model.
- Verify: the Verifier checks each cited node was observed in a tool result and is reachable from the question's entry nodes within 4 hops. Rejected claims are handed back once.
- Remember: turns, tool calls and facts go to the visitor's own graph; verified tool sequences become shared playbooks.
- Stay current: a daily Vercel Cron job merges updated issues and PRs, recomputes derived edges and prunes idle memory graphs.
Graph data model
| Label (all brain nodes also carry :Entity) | Key | Properties |
|---|---|---|
| Repo | repo:FalkorDB/falkordb-browser | name, language, stars, url |
| Person | person:<login> | login, avatarUrl, isBot, firstSeenAt, lastSeenAt (no names or emails) |
| Issue / PullRequest | issue:FalkorDB/FalkorDB#123 | number, title, state, createdAt, closedAt, mergedAt, author, summary (600 chars) |
| File / Directory | file:FalkorDB/FalkorDB:src/index/x.c | path, ext, depth |
| Release | release:FalkorDB/FalkorDB@v4.8.0 | tag, publishedAt, url, notes |
| Label | label:FalkorDB/FalkorDB:bug | name, color |
| Doc | doc:docs:cypher/indexing/vector-index | title, path, url |
| Concept | concept:vector-index | name, aliases (fixed dictionary, no LLM) |
| Chunk | chunk:<owner>:<i> | text (600 chars), embedding (vecf32, 256 dims), kind |
| Relationship | Between |
|---|---|
| AUTHORED, REVIEWED {state}, MERGED, COMMENTED_ON {count} | Person to Issue or PullRequest |
| TOUCHED {additions, deletions} | PullRequest to File |
| IN_DIR | File to Directory, Directory to parent Directory |
| CLOSES {source} | PullRequest to Issue (closing references, connected events, fixes #N) |
| REFERENCES {crossRepo, source} | Thread to Thread (timeline cross-references and #N in bodies) |
| SHIPPED_IN {source} | PullRequest to Release (release notes, else first release after merge) |
| HAS_LABEL, ASSIGNED_TO, IN_REPO, HAS_REPO, PREVIOUS | structure |
| MENTIONS, DOCUMENTS | Thread, Doc or Chunk to Concept |
| PART_OF | Chunk to Thread or Doc |
| KNOWS {authored, reviewed, lastAt} | derived: Person to Directory |
| REVIEWS_FOR {count} | derived: reviewer to author |
| CONTRIBUTES_TO / HAS_CONTRIBUTOR | derived: Person and Repo, both directions for betweenness |
| CO_CHANGES {count} | derived: Directory pairs changed by the same PRs |
Memory graphs tl_mem_<visitor>: Session, Turn (episodic), ToolCall, Ref, Fact (semantic). Shared graph tl_shared: Trace, Task, Claim, Agent, Playbook (procedural, with its own vector index), QueryEmbedding, Rate.
Indexes
CREATE INDEX FOR (e:Entity) ON (e.key) CREATE INDEX FOR (c:Chunk) ON (c.key) CREATE INDEX FOR (p:Person) ON (p.login) CREATE INDEX FOR (t:Thread) ON (t.number, t.createdAt, t.state) CREATE INDEX FOR (d:Directory) ON (d.path) CREATE INDEX FOR (f:File) ON (f.path) CALL db.idx.fulltext.createNodeIndex('Entity', 'title', 'name', 'path', 'login', 'tag') CREATE VECTOR INDEX FOR (c:Chunk) ON (c.embedding) OPTIONS {dimension:256, similarityFunction:'cosine'}
Derived edges
MATCH (p:Person)-[:AUTHORED]->(pr:PullRequest)-[:TOUCHED]->(:File)-[:IN_DIR*1..6]->(d:Directory) WHERE p.isBot = false AND pr.mergedAt IS NOT NULL WITH p, d, count(DISTINCT pr) AS authored, max(pr.mergedAt) AS last MERGE (p)-[k:KNOWS]->(d) SET k.authored = authored, k.authoredLastAt = last
MATCH (p:Person)-[rv:REVIEWED]->(pr:PullRequest)-[:TOUCHED]->(:File)-[:IN_DIR*1..6]->(d:Directory) WHERE p.isBot = false WITH p, d, count(DISTINCT pr) AS reviewed, max(rv.at) AS last MERGE (p)-[k:KNOWS]->(d) SET k.reviewed = reviewed, k.reviewedLastAt = last
MATCH (:Person)-[k:KNOWS]->(:Directory) SET k.authored = coalesce(k.authored, 0), k.reviewed = coalesce(k.reviewed, 0), k.lastAt = CASE WHEN coalesce(k.authoredLastAt, 0) >= coalesce(k.reviewedLastAt, 0) THEN k.authoredLastAt ELSE k.reviewedLastAt END
MATCH (r:Person)-[rv:REVIEWED]->(pr:PullRequest)<-[:AUTHORED]-(a:Person) WHERE r.isBot = false AND a.isBot = false AND id(r) <> id(a) WITH r, a, count(rv) AS n MERGE (r)-[e:REVIEWS_FOR]->(a) SET e.count = n
MATCH (p:Person)-[:AUTHORED]->(t:Thread)-[:IN_REPO]->(r:Repo) WHERE p.isBot = false WITH p, r, sum(CASE WHEN t:PullRequest THEN 1 ELSE 0 END) AS prs, sum(CASE WHEN t:Issue THEN 1 ELSE 0 END) AS issues MERGE (p)-[c:CONTRIBUTES_TO]->(r) SET c.prs = prs, c.issues = issues
MATCH (p:Person)-[rv:REVIEWED]->(:PullRequest)-[:IN_REPO]->(r:Repo) WHERE p.isBot = false WITH p, r, count(rv) AS n MERGE (p)-[c:CONTRIBUTES_TO]->(r) SET c.reviews = n, c.prs = coalesce(c.prs, 0), c.issues = coalesce(c.issues, 0)
MATCH (p:Person)-[c:CONTRIBUTES_TO]->(r:Repo) WHERE coalesce(c.prs, 0) + coalesce(c.reviews, 0) >= 3 MERGE (r)-[h:HAS_CONTRIBUTOR]->(p) SET h.weight = coalesce(c.prs, 0) + coalesce(c.reviews, 0)
MATCH (pr:PullRequest)-[:TOUCHED]->(:File)-[:IN_DIR]->(d:Directory) WITH pr, collect(DISTINCT d) AS ds WHERE size(ds) > 1 AND size(ds) <= 25 UNWIND ds AS d1 UNWIND ds AS d2 WITH d1, d2, pr WHERE id(d1) < id(d2) WITH d1, d2, count(DISTINCT pr) AS n WHERE n >= 3 MERGE (d1)-[c:CO_CHANGES]->(d2) SET c.count = n
Cypher catalogue (the queries the tools run)
Rendered from the same constants the server executes. All values are parameters.
CALL db.idx.vector.queryNodes('Chunk', 'embedding', $k, vecf32($vec)) YIELD node, score MATCH (node)-[:PART_OF]->(n:Entity) RETURN n.key AS key, labels(n) AS labels, coalesce(n.title, n.name, n.tag, n.login, n.path, n.key) AS name, n.repo AS repo, 1 - min(score) AS similarity ORDER BY similarity DESC LIMIT $limit
CALL db.idx.fulltext.queryNodes('Entity', $text) YIELD node, score WITH node AS n, score WHERE NOT n:Label RETURN n.key AS key, labels(n) AS labels, coalesce(n.title, n.name, n.tag, n.login, n.path, n.key) AS name, n.repo AS repo, score ORDER BY score DESC LIMIT $limit
MATCH (n:Entity) WHERE n.key IN $keys RETURN n.key AS key, labels(n) AS labels, coalesce(n.title, n.name, n.tag, n.login, n.path, n.key) AS name, n.repo AS repo
MATCH (n:Thread {number: $number})-[:IN_REPO]->(r:Repo) WHERE $repo IS NULL OR toLower(r.name) = toLower($repo) RETURN n.key AS key, labels(n) AS labels, n.title AS name, r.fullName AS repo, n.state AS state
MATCH (n:Concept) WHERE toLower(n.name) CONTAINS toLower($text) OR toLower(n.aliases) CONTAINS toLower($text) RETURN n.key AS key, labels(n) AS labels, n.name AS name, null AS repo LIMIT 3
MATCH (n:Entity {key: $key}) OPTIONAL MATCH (n)-[r]-(m:Entity) WITH n, type(r) AS rel, count(m) AS c RETURN n.key AS key, labels(n) AS labels, properties(n) AS props, collect({rel: rel, count: c}) AS degree
MATCH (s:Entity {key: $key})-[r]-(n:Entity) WHERE (size($types) = 0 OR type(r) IN $types) AND coalesce(n.isBot, false) = false RETURN type(r) AS rel, id(startNode(r)) = id(s) AS outgoing, n.key AS key, labels(n) AS labels, coalesce(n.title, n.name, n.tag, n.login, n.path, n.key) AS name, coalesce(n.mergedAt, n.createdAt, n.publishedAt, n.lastSeenAt, 0) AS at ORDER BY at DESC LIMIT $limit
MATCH (d:Directory) WHERE d.depth > 0 AND NOT d.path STARTS WITH '.' AND toLower(d.path) CONTAINS toLower($term) AND ($repo IS NULL OR toLower(d.repo) ENDS WITH toLower($repo)) WITH d ORDER BY d.depth ASC LIMIT 4 MATCH (p:Person)-[k:KNOWS]->(d) WITH p, d, k, (2 * k.authored + k.reviewed) AS weight, ($now - k.lastAt) / 86400.0 AS ageDays RETURN p.key AS key, p.login AS person, d.key AS area, d.path AS path, k.authored AS authored, k.reviewed AS reviewed, toInteger(ageDays) AS daysSinceLast, weight * exp(-ageDays / 180.0) AS score ORDER BY score DESC LIMIT 10
MATCH (c:Concept {key: $concept})<-[:MENTIONS]-(pr:PullRequest) WHERE pr.mergedAt IS NOT NULL AND ($repo IS NULL OR toLower(pr.repo) ENDS WITH toLower($repo)) MATCH (p:Person)-[r:AUTHORED|REVIEWED]->(pr) WHERE p.isBot = false WITH p, c, count(DISTINCT pr) AS prs, sum(CASE WHEN type(r) = 'AUTHORED' THEN 2 ELSE 1 END) AS weight, max(pr.mergedAt) AS last, collect(DISTINCT pr.key)[0..3] AS sample RETURN p.key AS key, p.login AS person, c.key AS area, prs, toInteger(($now - last) / 86400) AS daysSinceLast, weight * exp(-(($now - last) / 86400.0) / 180.0) AS score, sample ORDER BY score DESC LIMIT 10
MATCH (p:Person)-[o:DECLARED_OWNER]->(x:Entity) WHERE x.key IN $areas RETURN p.key AS key, p.login AS person, x.key AS area, o.pattern AS pattern
MATCH (i:Issue {key: $key}) OPTIONAL MATCH (fix:PullRequest)-[c:CLOSES]->(i) OPTIONAL MATCH (fix)-[:SHIPPED_IN]->(rel:Release) OPTIONAL MATCH (author:Person)-[:AUTHORED]->(fix) OPTIONAL MATCH (opener:Person)-[:AUTHORED]->(i) RETURN i.key AS issue, i.title AS title, i.state AS state, opener.login AS openedBy, fix.key AS fix, fix.title AS fixTitle, fix.mergedAt AS mergedAt, c.source AS linkSource, author.login AS fixAuthor, rel.key AS release, rel.tag AS tag, rel.publishedAt AS releasedAt
MATCH (pr:PullRequest {key: $key}) OPTIONAL MATCH (pr)-[c:CLOSES]->(i:Issue) OPTIONAL MATCH (pr)-[s:SHIPPED_IN]->(rel:Release) OPTIONAL MATCH (author:Person)-[:AUTHORED]->(pr) OPTIONAL MATCH (rv:Person)-[r:REVIEWED]->(pr) RETURN pr.key AS pr, pr.title AS title, pr.state AS state, pr.mergedAt AS mergedAt, author.login AS author, collect(DISTINCT i.key) AS closes, collect(DISTINCT rel.key) AS releases, collect(DISTINCT rel.tag) AS tags, collect(DISTINCT s.source) AS shipSource, collect(DISTINCT {login: rv.login, state: r.state}) AS reviewers
MATCH (t:Thread {key: $key})-[r:REFERENCES|CLOSES]-(o:Thread) RETURN type(r) AS rel, id(startNode(r)) = id(t) AS outgoing, o.key AS key, o.title AS name, o.repo AS repo, o.state AS state, coalesce(r.crossRepo, o.repo <> t.repo) AS crossRepo ORDER BY crossRepo DESC LIMIT 25
MATCH (a:Entity {key: $a}), (b:Entity {key: $b}) WITH a, b MATCH p = allShortestPaths((a)-[:REFERENCES|CLOSES|AUTHORED|REVIEWED|TOUCHED|SHIPPED_IN|COMMENTED_ON|MENTIONS*..6]-(b)) RETURN [n IN nodes(p) | n.key] AS keys, [r IN relationships(p) | type(r)] AS rels LIMIT 3
MATCH (d:Directory) WHERE d.depth > 0 AND NOT d.path STARTS WITH '.' AND toLower(d.path) CONTAINS toLower($term) AND ($repo IS NULL OR toLower(d.repo) ENDS WITH toLower($repo)) WITH d ORDER BY d.depth ASC LIMIT 3 MATCH (d)<-[:IN_DIR*1..6]-(f:File)<-[t:TOUCHED]-(pr:PullRequest) WHERE pr.state = 'OPEN' RETURN pr.key AS key, pr.title AS name, d.key AS areaKey, d.path AS area, count(DISTINCT f) AS files, pr.createdAt AS createdAt ORDER BY files DESC LIMIT 15
MATCH (d:Directory) WHERE d.depth > 0 AND NOT d.path STARTS WITH '.' AND toLower(d.path) CONTAINS toLower($term) AND ($repo IS NULL OR toLower(d.repo) ENDS WITH toLower($repo)) WITH d ORDER BY d.depth ASC LIMIT 3 MATCH (d)<-[:IN_DIR*1..6]-(:File)<-[:TOUCHED]-(pr:PullRequest)-[:CLOSES|REFERENCES]-(i:Issue) WHERE i.state = 'OPEN' RETURN i.key AS key, i.title AS name, d.key AS areaKey, d.path AS area, count(DISTINCT pr) AS viaPrs ORDER BY viaPrs DESC LIMIT 10
MATCH (d:Directory) WHERE d.depth > 0 AND NOT d.path STARTS WITH '.' AND toLower(d.path) CONTAINS toLower($term) AND ($repo IS NULL OR toLower(d.repo) ENDS WITH toLower($repo)) WITH d ORDER BY d.depth ASC LIMIT 3 MATCH (d)-[c:CO_CHANGES]-(o:Directory) RETURN o.key AS key, o.path AS name, d.key AS areaKey, o.repo AS repo, c.count AS together ORDER BY together DESC LIMIT 8
MATCH (p:Person)-[e:AUTHORED]->(t:Thread)-[:IN_REPO]->(r:Repo) WHERE p.isBot = false AND ($repo IS NULL OR toLower(r.name) = toLower($repo)) AND t.createdAt >= $since AND t.createdAt < $until AND ($kind IS NULL OR ($kind = 'pr' AND t:PullRequest) OR ($kind = 'issue' AND t:Issue)) RETURN p.key AS key, p.login AS person, count(DISTINCT t) AS n, collect(t.key)[0..3] AS sample ORDER BY n DESC LIMIT $limit
MATCH (p:Person)-[e:REVIEWED]->(t:PullRequest)-[:IN_REPO]->(r:Repo) WHERE p.isBot = false AND ($repo IS NULL OR toLower(r.name) = toLower($repo)) AND t.createdAt >= $since AND t.createdAt < $until RETURN p.key AS key, p.login AS person, count(DISTINCT t) AS n, collect(t.key)[0..3] AS sample ORDER BY n DESC LIMIT $limit
MATCH (p:Person)-[e:MERGED]->(t:PullRequest)-[:IN_REPO]->(r:Repo) WHERE p.isBot = false AND ($repo IS NULL OR toLower(r.name) = toLower($repo)) AND t.mergedAt >= $since AND t.mergedAt < $until RETURN p.key AS key, p.login AS person, count(DISTINCT t) AS n, collect(t.key)[0..3] AS sample ORDER BY n DESC LIMIT $limit
MATCH (p:Person)-[e:COMMENTED_ON]->(t:Thread)-[:IN_REPO]->(r:Repo) WHERE p.isBot = false AND ($repo IS NULL OR toLower(r.name) = toLower($repo)) AND t.createdAt >= $since AND t.createdAt < $until RETURN p.key AS key, p.login AS person, sum(e.count) AS n, collect(t.key)[0..3] AS sample ORDER BY n DESC LIMIT $limit
MATCH (d:Directory) WHERE toLower(d.path) = toLower($area) AND ($repo IS NULL OR toLower(d.repo) ENDS WITH toLower($repo)) MATCH (d)<-[:IN_DIR*1..6]-(:File)<-[:TOUCHED]-(t:PullRequest)<-[e:AUTHORED|REVIEWED]-(p:Person) WHERE p.isBot = false AND t.mergedAt IS NOT NULL AND type(e) = $rel AND t.createdAt >= $since AND t.createdAt < $until RETURN p.key AS key, p.login AS person, d.key AS area, count(DISTINCT t) AS n, collect(DISTINCT t.key)[0..3] AS sample ORDER BY n DESC LIMIT $limit
CALL algo.pageRank('Person', 'REVIEWS_FOR') YIELD node, score WHERE node.isBot = false RETURN node.key AS key, node.login AS person, score ORDER BY score DESC LIMIT 10
CALL algo.betweenness({nodeLabels: ['Person', 'Repo'], relationshipTypes: ['CONTRIBUTES_TO', 'HAS_CONTRIBUTOR']}) YIELD node, score WITH node, score WHERE node:Person MATCH (node)-[c:CONTRIBUTES_TO]->(r:Repo) RETURN node.key AS key, node.login AS person, score, collect(r.name) AS repos ORDER BY score DESC LIMIT 10
CALL algo.labelPropagation({nodeLabels: ['Directory'], relationshipTypes: ['CO_CHANGES']}) YIELD node, communityId WITH communityId, collect(node) AS dirs WHERE size(dirs) >= 2 RETURN communityId, [d IN dirs | d.key][0..8] AS keys, [d IN dirs | d.repo + ':' + d.path][0..8] AS paths, size(dirs) AS size ORDER BY size DESC LIMIT 8
MATCH (a:Entity {key: $entry}), (b:Entity {key: $cited}) WITH a, b MATCH p = allShortestPaths((a)-[:AUTHORED|REVIEWED|MERGED|COMMENTED_ON|ASSIGNED_TO|TOUCHED|IN_DIR|CLOSES|REFERENCES|SHIPPED_IN|MENTIONS|DOCUMENTS|KNOWS|REVIEWS_FOR|CO_CHANGES|DECLARED_OWNER*..4]-(b)) RETURN length(p) AS hops, [n IN nodes(p) | n.key] AS keys, [r IN relationships(p) | type(r)] AS rels LIMIT 1
MATCH (s:Entity {key: $key})-[r1]-(n1:Entity) WITH s, r1, n1 LIMIT 40 OPTIONAL MATCH (n1)-[r2]-(n2:Entity) WHERE n2 <> s WITH s, r1, n1, collect({r: type(r2), k: n2.key, out: id(startNode(r2)) = id(n1)})[0..4] AS hop2 RETURN n1.key AS key, type(r1) AS rel, id(startNode(r1)) = id(s) AS outgoing, hop2
MATCH (i:Issue {key: $key}) OPTIONAL MATCH (i)-[:HAS_LABEL]->(l:Label) OPTIONAL MATCH (c:Chunk)-[:PART_OF]->(i) WITH i, collect(DISTINCT l.name) AS labels, collect(c)[0] AS c RETURN i.key AS key, i.title AS title, i.state AS state, i.summary AS summary, labels, c.embedding AS e
CALL db.idx.vector.queryNodes('Chunk', 'embedding', 24, vecf32($vec)) YIELD node, score MATCH (node)-[:PART_OF]->(t:Thread) WHERE t.key <> $key RETURN t.key AS key, t.title AS name, t.state AS state, 1 - min(score) AS similarity ORDER BY similarity DESC LIMIT 6
MATCH (t:Thread) WHERE t.key IN $similar OPTIONAL MATCH (fix:PullRequest)-[:CLOSES]->(t) WITH CASE WHEN fix IS NOT NULL THEN fix WHEN t:PullRequest THEN t ELSE null END AS p WITH p WHERE p IS NOT NULL MATCH (p)-[:TOUCHED]->(:File)-[:IN_DIR]->(d:Directory) WITH d, count(DISTINCT p) AS n ORDER BY n DESC LIMIT 3 MATCH (person:Person)-[k:KNOWS]->(d) WHERE person.isBot = false RETURN person.key AS key, person.login AS person, d.key AS area, d.path AS path, k.authored AS authored, k.reviewed AS reviewed ORDER BY k.authored * 2 + k.reviewed DESC LIMIT 6
MATCH (t:Thread)-[:HAS_LABEL]->(l:Label) WHERE t.key IN $similar RETURN l.name AS label, count(t) AS n ORDER BY n DESC LIMIT 4
CALL db.idx.vector.queryNodes('Chunk', 'embedding', $k, vecf32($vec)) YIELD node, score MATCH (node)-[:PART_OF]->(n:Entity) RETURN node.key AS chunk, node.text AS text, n.key AS key, coalesce(n.title, n.name, n.tag, n.login, n.path, n.key) AS name, n.author AS author, n.state AS state, n.createdAt AS createdAt, 1 - score AS score ORDER BY score DESC
Memory and coordination Cypher
MERGE (s:Session {id: $sid}) ON CREATE SET s.startedAt = $now CREATE (t:Turn {id: $tid, question: $q, answer: $a, at: $now, verified: $verified, total: $total, mode: $mode, traceId: $traceId}) MERGE (s)-[:HAS_TURN]->(t) WITH s, t OPTIONAL MATCH (s)-[:HAS_TURN]->(prev:Turn) WHERE prev.id <> t.id WITH t, prev ORDER BY prev.at DESC LIMIT 1 FOREACH (_ IN CASE WHEN prev IS NULL THEN [] ELSE [1] END | MERGE (prev)-[:NEXT]->(t)) WITH t UNWIND $calls AS c CREATE (tc:ToolCall {tool: c.tool, step: c.step, ms: c.ms, rows: c.rows, cypher: c.cypher}) MERGE (t)-[:RAN]->(tc) FOREACH (k IN c.refs | MERGE (r:Ref {key: k}) MERGE (tc)-[:TOUCHED_REF]->(r))
MATCH (t:Turn {id: $tid}) MERGE (f:Fact {text: $text}) ON CREATE SET f.kind = $kind, f.at = $now, f.confidence = 1.0 MERGE (f)-[:LEARNED_FROM]->(t) WITH f UNWIND $refs AS k MERGE (r:Ref {key: k}) MERGE (f)-[:ABOUT]->(r)
MATCH (f:Fact) RETURN f.text AS text, f.kind AS kind, f.at AS at ORDER BY f.at DESC LIMIT 12
MATCH (:Session {id: $sid})-[:HAS_TURN]->(t:Turn) OPTIONAL MATCH (t)-[:RAN]->(:ToolCall)-[:TOUCHED_REF]->(r:Ref) WITH t, collect(DISTINCT r.key)[0..10] AS refs RETURN t.question AS question, t.at AS at, refs ORDER BY at DESC LIMIT 1
MATCH (s:Session)-[:HAS_TURN]->(t:Turn) OPTIONAL MATCH (t)-[:RAN]->(tc:ToolCall) WITH s, t, count(tc) AS calls RETURN s.id AS session, t.id AS id, t.question AS question, t.at AS at, t.verified AS verified, t.total AS total, t.traceId AS traceId, calls ORDER BY at DESC LIMIT 20
CALL db.idx.vector.queryNodes('Playbook', 'embedding', 3, vecf32($vec)) YIELD node, score WITH node, 1 - score AS similarity WHERE similarity > 0.86 AND node.successes >= 1 RETURN node.questionType AS questionType, node.toolSequence AS toolSequence, node.successes AS successes, similarity ORDER BY similarity DESC LIMIT 1
MERGE (p:Playbook {questionType: $qt, toolSequence: $seq}) ON CREATE SET p.uses = 0, p.successes = 0, p.embedding = vecf32($vec), p.example = $q, p.createdAt = $now SET p.uses = p.uses + 1, p.successes = p.successes + $ok, p.lastAt = $now
MERGE (inv:Agent {name: 'investigator'}) MERGE (ver:Agent {name: 'verifier'}) CREATE (task:Task {id: $traceId, question: $q, status: $status, at: $now}) CREATE (inv)-[:WORKED_ON]->(task) WITH inv, ver, task UNWIND $claims AS c CREATE (cl:Claim {key: c.key, ok: c.ok, hops: c.hops, observed: c.observed}) CREATE (inv)-[:POSTED]->(cl)-[:FOR]->(task) CREATE (cl)-[:CHECKED_BY {verdict: CASE WHEN c.ok THEN 'verified' ELSE 'rejected' END, pathLength: c.hops}]->(ver)
MATCH (task:Task {id: $traceId}) CREATE (task)-[:HANDED_OFF {from: 'verifier', to: 'investigator', reason: $reason, at: $now}]->(task)
CREATE (t:Trace {id: $id, question: $q, mode: $mode, at: $now, showcase: $showcase, verified: $verified, total: $total, json: $json})
MATCH (t:Trace {id: $id}) RETURN t.json AS json
MATCH (t:Trace) WHERE t.showcase = true RETURN t.id AS id, t.question AS question, t.mode AS mode, t.verified AS verified, t.total AS total, t.at AS at ORDER BY t.id ASC
MCP
The same typed tools are served over MCP (Streamable HTTP) at /api/mcp: find_entities, details, expand, who_knows, trace, connect, impact, activity, rank, draft_triage and cypher_readonly. The client's own model chooses the tools; the server never calls an LLM.