@rubytech/create-maxy-code 0.1.127 → 0.1.131

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.js +10 -7
  2. package/package.json +1 -1
  3. package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts +5 -1
  4. package/payload/platform/lib/graph-write/dist/conversation-provenance.d.ts.map +1 -1
  5. package/payload/platform/lib/graph-write/dist/conversation-provenance.js +18 -11
  6. package/payload/platform/lib/graph-write/dist/conversation-provenance.js.map +1 -1
  7. package/payload/platform/lib/graph-write/src/conversation-provenance.ts +24 -16
  8. package/payload/platform/plugins/contacts/PLUGIN.md +1 -1
  9. package/payload/platform/plugins/docs/references/internals.md +1 -1
  10. package/payload/platform/plugins/docs/references/memory-guide.md +4 -0
  11. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +2 -6
  12. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts.map +1 -1
  14. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js +46 -6
  15. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js.map +1 -1
  16. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts +1 -1
  17. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -1
  18. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +28 -2
  19. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -1
  20. package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +6 -3
  21. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  22. package/payload/platform/plugins/memory/references/schema-estate-agent.md +3 -3
  23. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/http-server.js +16 -0
  25. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  26. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +11 -0
  27. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +11 -0
  29. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  30. package/payload/platform/templates/agents/admin/IDENTITY.md +12 -0
  31. package/payload/platform/templates/agents/public/IDENTITY.md +8 -2
  32. package/payload/platform/templates/specialists/agents/content-producer.md +4 -0
  33. package/payload/platform/templates/specialists/agents/database-operator.md +2 -0
  34. package/payload/platform/templates/specialists/agents/personal-assistant.md +2 -0
  35. package/payload/server/server.js +70 -11
package/dist/index.js CHANGED
@@ -3524,14 +3524,17 @@ else {
3524
3524
  }
3525
3525
  catch { /* non-critical */ }
3526
3526
  // Darwin has no VNC/X11 stack — `virtual` mode is Pi-only (tigervnc +
3527
- // websockify + novnc, none of which exist on macOS). Without this guard
3528
- // a fresh Mac install defaults to virtual, the OAuth step calls the
3529
- // "Open browser" path that requires a virtual display, and the user
3530
- // sees "VNC display failed to start" before they ever get to sign in.
3531
- // Auto-flip to native on darwin when the operator didn't pick a mode.
3532
- if (!preservedFromEnv && process.platform === "darwin") {
3527
+ // websockify + novnc, none of which exist on macOS). A preserved
3528
+ // DISPLAY_MODE=virtual is never operator intent on darwin it is the
3529
+ // legacy default from installers before 0.1.127, when `virtual` was the
3530
+ // only initial value written. The guard fires whenever the resolved
3531
+ // value is not already `native`, covering both fresh installs and
3532
+ // upgrades that inherited the legacy default.
3533
+ if (process.platform === "darwin" && DISPLAY_MODE !== "native") {
3534
+ DISPLAY_MODE_SOURCE = preservedFromEnv
3535
+ ? "darwin-auto (override-legacy-virtual)"
3536
+ : "darwin-auto (no VNC stack)";
3533
3537
  DISPLAY_MODE = "native";
3534
- DISPLAY_MODE_SOURCE = "darwin-auto (no VNC stack)";
3535
3538
  }
3536
3539
  }
3537
3540
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.127",
3
+ "version": "0.1.131",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -10,7 +10,11 @@ export interface InjectConversationProvenanceParams {
10
10
  writeLabels: readonly string[];
11
11
  /**
12
12
  * Value of `process.env.CONVERSATION_NODE_ID` at call time. Passed as a
13
- * parameter so tests can supply it without mutating process.env.
13
+ * parameter so tests can supply it without mutating process.env. The
14
+ * value is the `conversationId` UUID property of the active
15
+ * `:AdminConversation`, not its Neo4j elementId — the env-var name is
16
+ * historical. The lookup MATCHes `(c:Conversation {conversationId, accountId})`
17
+ * and reads `elementId(c)` itself to compose the synthetic edge.
14
18
  */
15
19
  conversationNodeId: string | undefined;
16
20
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-provenance.d.ts","sourceRoot":"","sources":["../src/conversation-provenance.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAK5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,aAAa,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC5C,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,sGAAsG;IACtG,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;OAGG;IACH,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,kCAAkC,GACzC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAmE9B"}
1
+ {"version":3,"file":"conversation-provenance.d.ts","sourceRoot":"","sources":["../src/conversation-provenance.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAK5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,aAAa,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC5C,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,sGAAsG;IACtG,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;;;OAOG;IACH,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,kCAAkC,GACzC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAmE9B"}
@@ -3,10 +3,14 @@
3
3
  //
4
4
  // MCP tool wrappers that write to ACTION_PROVENANCE_LABELS call this helper
5
5
  // before `writeNodeWithEdges`. When the admin server has stamped
6
- // `CONVERSATION_NODE_ID` in the MCP spawn env (the elementId of the active
7
- // `:AdminConversation`), the helper:
8
- // - verifies the source node exists, carries one of PROVENANCE_SOURCE_LABELS,
9
- // and shares the write's accountId
6
+ // `CONVERSATION_NODE_ID` in the MCP spawn env (the `conversationId` UUID of
7
+ // the active `:AdminConversation` — name is historical, value is the UUID
8
+ // property, not the Neo4j elementId, so it can be threaded through spawn
9
+ // without an extra Neo4j round-trip at session-start), the helper:
10
+ // - verifies a `:Conversation` exists with that conversationId AND the
11
+ // write's accountId (account isolation is enforced inside the MATCH,
12
+ // not as a separate gate)
13
+ // - verifies the node carries one of PROVENANCE_SOURCE_LABELS
10
14
  // - prepends a synthetic inbound `:PRODUCED` edge to the relationships array
11
15
  // so the gate inside `writeNodeWithEdges` accepts the write
12
16
  //
@@ -38,7 +42,14 @@ async function injectConversationProvenance(params) {
38
42
  return original;
39
43
  let lookup;
40
44
  try {
41
- lookup = await session.run(`MATCH (n) WHERE elementId(n) = $id RETURN labels(n) AS labels, n.accountId AS accountId LIMIT 1`, { id: conversationNodeId });
45
+ // MATCH on (conversationId, accountId) the account filter is part of
46
+ // the natural key, so a cross-account env-stamp produces zero rows
47
+ // (a clean `node-not-found`, not a silent leak). `elementId(n)` is
48
+ // still returned because `writeNodeWithEdges` composes the synthetic
49
+ // edge by elementId, not by conversationId — Neo4j's edge composition
50
+ // is internal-id based.
51
+ lookup = await session.run(`MATCH (c:Conversation {conversationId: $cid, accountId: $accountId})
52
+ RETURN elementId(c) AS elementId, labels(c) AS labels LIMIT 1`, { cid: conversationNodeId, accountId });
42
53
  }
43
54
  catch (err) {
44
55
  process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=driver-error message=${err instanceof Error ? err.message : String(err)} conversationNodeId=${conversationNodeId}\n`);
@@ -48,23 +59,19 @@ async function injectConversationProvenance(params) {
48
59
  process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=node-not-found conversationNodeId=${conversationNodeId}\n`);
49
60
  return original;
50
61
  }
62
+ const elementId = lookup.records[0].get("elementId");
51
63
  const labels = lookup.records[0].get("labels");
52
- const sourceAccountId = lookup.records[0].get("accountId");
53
64
  const sourceLabel = labels.find((l) => index_js_1.PROVENANCE_SOURCE_LABELS.has(l));
54
65
  if (!sourceLabel) {
55
66
  process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=wrong-source-label labels=${labels.join(",")} conversationNodeId=${conversationNodeId}\n`);
56
67
  return original;
57
68
  }
58
- if (sourceAccountId !== accountId) {
59
- process.stderr.write(`[${logNamespace}] [provenance-missing] tool=${tool} reason=account-mismatch sourceAccountId=${sourceAccountId.slice(0, 8)} writeAccountId=${accountId.slice(0, 8)} conversationNodeId=${conversationNodeId}\n`);
60
- return original;
61
- }
62
69
  process.stderr.write(`[${logNamespace}] [provenance-inject] tool=${tool} from=${sourceLabel}:${conversationNodeId}\n`);
63
70
  return [
64
71
  {
65
72
  type: "PRODUCED",
66
73
  direction: "incoming",
67
- targetNodeId: conversationNodeId,
74
+ targetNodeId: elementId,
68
75
  },
69
76
  ...original,
70
77
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-provenance.js","sourceRoot":"","sources":["../src/conversation-provenance.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"conversation-provenance.js","sourceRoot":"","sources":["../src/conversation-provenance.ts"],"names":[],"mappings":"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"}
@@ -2,10 +2,14 @@
2
2
  //
3
3
  // MCP tool wrappers that write to ACTION_PROVENANCE_LABELS call this helper
4
4
  // before `writeNodeWithEdges`. When the admin server has stamped
5
- // `CONVERSATION_NODE_ID` in the MCP spawn env (the elementId of the active
6
- // `:AdminConversation`), the helper:
7
- // - verifies the source node exists, carries one of PROVENANCE_SOURCE_LABELS,
8
- // and shares the write's accountId
5
+ // `CONVERSATION_NODE_ID` in the MCP spawn env (the `conversationId` UUID of
6
+ // the active `:AdminConversation` — name is historical, value is the UUID
7
+ // property, not the Neo4j elementId, so it can be threaded through spawn
8
+ // without an extra Neo4j round-trip at session-start), the helper:
9
+ // - verifies a `:Conversation` exists with that conversationId AND the
10
+ // write's accountId (account isolation is enforced inside the MATCH,
11
+ // not as a separate gate)
12
+ // - verifies the node carries one of PROVENANCE_SOURCE_LABELS
9
13
  // - prepends a synthetic inbound `:PRODUCED` edge to the relationships array
10
14
  // so the gate inside `writeNodeWithEdges` accepts the write
11
15
  //
@@ -41,7 +45,11 @@ export interface InjectConversationProvenanceParams {
41
45
  writeLabels: readonly string[];
42
46
  /**
43
47
  * Value of `process.env.CONVERSATION_NODE_ID` at call time. Passed as a
44
- * parameter so tests can supply it without mutating process.env.
48
+ * parameter so tests can supply it without mutating process.env. The
49
+ * value is the `conversationId` UUID property of the active
50
+ * `:AdminConversation`, not its Neo4j elementId — the env-var name is
51
+ * historical. The lookup MATCHes `(c:Conversation {conversationId, accountId})`
52
+ * and reads `elementId(c)` itself to compose the synthetic edge.
45
53
  */
46
54
  conversationNodeId: string | undefined;
47
55
  /**
@@ -75,9 +83,16 @@ export async function injectConversationProvenance(
75
83
 
76
84
  let lookup;
77
85
  try {
86
+ // MATCH on (conversationId, accountId) — the account filter is part of
87
+ // the natural key, so a cross-account env-stamp produces zero rows
88
+ // (a clean `node-not-found`, not a silent leak). `elementId(n)` is
89
+ // still returned because `writeNodeWithEdges` composes the synthetic
90
+ // edge by elementId, not by conversationId — Neo4j's edge composition
91
+ // is internal-id based.
78
92
  lookup = await session.run(
79
- `MATCH (n) WHERE elementId(n) = $id RETURN labels(n) AS labels, n.accountId AS accountId LIMIT 1`,
80
- { id: conversationNodeId },
93
+ `MATCH (c:Conversation {conversationId: $cid, accountId: $accountId})
94
+ RETURN elementId(c) AS elementId, labels(c) AS labels LIMIT 1`,
95
+ { cid: conversationNodeId, accountId },
81
96
  );
82
97
  } catch (err) {
83
98
  process.stderr.write(
@@ -93,8 +108,8 @@ export async function injectConversationProvenance(
93
108
  return original;
94
109
  }
95
110
 
111
+ const elementId = lookup.records[0].get("elementId") as string;
96
112
  const labels = lookup.records[0].get("labels") as string[];
97
- const sourceAccountId = lookup.records[0].get("accountId") as string;
98
113
  const sourceLabel = labels.find((l) => PROVENANCE_SOURCE_LABELS.has(l));
99
114
 
100
115
  if (!sourceLabel) {
@@ -104,13 +119,6 @@ export async function injectConversationProvenance(
104
119
  return original;
105
120
  }
106
121
 
107
- if (sourceAccountId !== accountId) {
108
- process.stderr.write(
109
- `[${logNamespace}] [provenance-missing] tool=${tool} reason=account-mismatch sourceAccountId=${sourceAccountId.slice(0, 8)} writeAccountId=${accountId.slice(0, 8)} conversationNodeId=${conversationNodeId}\n`,
110
- );
111
- return original;
112
- }
113
-
114
122
  process.stderr.write(
115
123
  `[${logNamespace}] [provenance-inject] tool=${tool} from=${sourceLabel}:${conversationNodeId}\n`,
116
124
  );
@@ -119,7 +127,7 @@ export async function injectConversationProvenance(
119
127
  {
120
128
  type: "PRODUCED",
121
129
  direction: "incoming",
122
- targetNodeId: conversationNodeId,
130
+ targetNodeId: elementId,
123
131
  },
124
132
  ...original,
125
133
  ];
@@ -54,7 +54,7 @@ Tools are available via the `contacts` MCP server.
54
54
 
55
55
  ## Provenance anchoring
56
56
 
57
- `contact-create` writes a `:Person` node, which is one of the action-provenance-gated labels in `writeNodeWithEdges`. The write must carry an inbound `:PRODUCED` edge from `:Task`, `:Conversation`, or `:Message`. There is no agent-visible field for this — the admin server stamps `CONVERSATION_NODE_ID` (the elementId of the active `:AdminConversation`) at MCP spawn time, and the wrapper auto-injects the `:PRODUCED` edge via `injectConversationProvenance`. Account-isolation is verified inside the helper before injection.
57
+ `contact-create` writes a `:Person` node, which is one of the action-provenance-gated labels in `writeNodeWithEdges`. The write must carry an inbound `:PRODUCED` edge from `:Task`, `:Conversation`, or `:Message`. There is no agent-visible field for this — the admin server stamps `CONVERSATION_NODE_ID` (the `conversationId` UUID of the active `:AdminConversation`) at MCP spawn time, and the wrapper auto-injects the `:PRODUCED` edge via `injectConversationProvenance`. The helper MATCHes `(c:Conversation {conversationId, accountId})` — account isolation is enforced inside the natural key, not as a separate gate, so a cross-account env-stamp returns zero rows and never injects.
58
58
 
59
59
  The agent passes whichever organic relationships are appropriate (`PARTICIPATES_IN` to a Conversation, `WORKS_FOR` to an Organization, `KNOWN_VIA` to a referral source) — the provenance edge is composed automatically and is not an agent concern. When the env-stamp resolution fails (loud `[mcp:contacts] [provenance-missing] reason=<...>`), the downstream gate emits `missing-provenance` and the write fails closed.
60
60
 
@@ -527,7 +527,7 @@ The doctrine is enforced at the storage primitive: writes to `:Person`, `:UserPr
527
527
 
528
528
  Two surfaces emit the lifecycle: agent-driven actions call `work-create`/`work-update`/`work-complete` over MCP (`work-create` accepts `kind`, the canonical `inputsProvided` call-shape record, `inputs` + `inputSchema` for the operator-meaningful form payload, and `raisedDuringConversationKey` to resolve the `RAISED_DURING` edge). Shell-driven actions wrap their script invocation in [platform/ui/app/lib/cloudflare-task-tracker.ts](../../../ui/app/lib/cloudflare-task-tracker.ts) (cloudflare is the first; installer / brand-publish / OAuth-login deferred). Both surfaces emit the same `[task] action-start|step|done` log lines so operators can grep one channel uniformly. Both also call the central `redactSecrets` primitive ([platform/lib/task-secrets/](../../../lib/task-secrets/)) to strip schema-tagged secret keys before persisting `inputs.<field>` props on the Task — see `.docs/neo4j.md § Audit Task input contract` for the contract that replaces per-kind allow-lists.
529
529
 
530
- Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation` elementId at MCP spawn time and stamps it as `CONVERSATION_NODE_ID` in the env. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which verifies `accountId` parity and prepends the synthetic `:PRODUCED` edge no agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone.
530
+ Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `conversationId` UUID and stamps it as `CONVERSATION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches (Task 382) so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {conversationId, accountId})` account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
531
531
 
532
532
  Operator audit cyphers:
533
533
  - "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {conversationId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
@@ -1,5 +1,9 @@
1
1
  # Memory Guide
2
2
 
3
+ ## Brain-first lookup
4
+
5
+ The graph is the brain, and every turn that needs to know something runs the same five-step loop in order: (1) classify the question (entity, temporal, event, general, or none — the inbound gateway emits this as `retrievalClass`), (2) read the graph with `memory-search` (and `profile-read` when the question is about the operator) as the first tool call of the turn, (3) walk one hop to hydrate a partial hit before calling it a miss, (4) call an external tool only when steps 2–3 confirmed the graph has nothing useful, and (5) write the external evidence back via `database-operator`. The loop is what makes the next turn smarter; an external call whose result is never persisted is a leak in the brain. `retrievalClass = none` (greetings, meta-instructions) is the only exception. Operator-facing doctrine lives in [`.docs/brain-first.md`](../../../.docs/brain-first.md).
6
+
3
7
  ## How Memory Works
4
8
 
5
9
  {{productName}} maintains a graph of everything you've told it. Contacts, conversations, preferences, relationships, business context — all stored as connected nodes in a local Neo4j database on your Raspberry Pi.
@@ -15,7 +15,7 @@ const server = new McpServer({
15
15
  name: "maxy-graph-viewer",
16
16
  version: "0.1.0",
17
17
  });
18
- eagerTool(server, "graph-render", "Render an inline PNG of an account-scoped Neo4j subgraph rooted at a given nodeId, walking up to hopDepth hops (0-3). Returns the image as an MCP image content block, using the same colour palette and short-label strings as the live /graph page. Node positions differ from /graph (the renderer is d3-force, not vis-network). Subgraphs above 200 nodes are capped and return a truncated={rendered,total} envelope. Trashed nodes are excluded. Use when the operator asks to see what a subgraph looks like and you want to avoid making them switch to the admin UI on a different device.", {
18
+ eagerTool(server, "graph-render", "Render a PNG of an account-scoped Neo4j subgraph rooted at a given nodeId, walking up to hopDepth hops (0-3). Writes the PNG to data/accounts/<accountId>/output/graph-render/ and returns {ok:true, path, nodeCount, edgeCount, width, height, truncated?}. Callers deliver the image to the operator by calling SendUserFile with the returned path — MCP image content blocks are not surfaced in the maxy-code chat UI. The image uses the same colour palette and short-label strings as the live /graph page; node positions differ (d3-force, not vis-network) and are auto-fit to the canvas. Subgraphs above 200 nodes are capped and the envelope carries truncated={rendered,total}. Trashed nodes are excluded. Use when the operator asks to see what a subgraph looks like and you want to avoid making them switch to the admin UI on a different device.", {
19
19
  accountId: z.string().min(1).describe("Operator account id. The root nodeId must belong to this account or the tool refuses with reason=scope."),
20
20
  nodeId: z.string().min(1).describe("elementId of the root node, as returned by memory-search or other graph tools."),
21
21
  hopDepth: z.number().int().min(0).max(3).describe("Number of hops to walk from the root (0 = just the root). Max 3."),
@@ -34,6 +34,7 @@ eagerTool(server, "graph-render", "Render an inline PNG of an account-scoped Neo
34
34
  }
35
35
  const envelope = {
36
36
  ok: true,
37
+ path: result.path,
37
38
  width: result.width,
38
39
  height: result.height,
39
40
  nodeCount: result.nodeCount,
@@ -43,11 +44,6 @@ eagerTool(server, "graph-render", "Render an inline PNG of an account-scoped Neo
43
44
  envelope.truncated = result.truncated;
44
45
  return {
45
46
  content: [
46
- {
47
- type: "image",
48
- data: result.pngBase64,
49
- mimeType: "image/png",
50
- },
51
47
  {
52
48
  type: "text",
53
49
  text: JSON.stringify(envelope, null, 2),
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAcH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AA4BD,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EAAE,EACrB,KAAK,EAAE,YAAY,EAAE,EACrB,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,YAAY,CAmDd"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAcH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AA6BD,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EAAE,EACrB,KAAK,EAAE,YAAY,EAAE,EACrB,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,YAAY,CAsFd"}
@@ -32,6 +32,7 @@ const ITERATIONS = 300;
32
32
  const LINK_DISTANCE = 90;
33
33
  const CHARGE_STRENGTH = -240;
34
34
  const CENTER_STRENGTH = 0.05;
35
+ const FIT_PADDING = 60;
35
36
  export function layoutGraph(nodes, edges, opts) {
36
37
  const seed = opts.seed ?? 1;
37
38
  const rand = seededRandom(seed);
@@ -66,12 +67,51 @@ export function layoutGraph(nodes, edges, opts) {
66
67
  .force("y", forceY(cy).strength(CENTER_STRENGTH))
67
68
  .stop();
68
69
  simulation.tick(ITERATIONS);
69
- const outNodes = simNodes.map((n) => ({
70
- id: n.id,
71
- labels: n.labels,
72
- x: n.x ?? cx,
73
- y: n.y ?? cy,
74
- }));
70
+ // Fit-to-content pass. The force simulation settles into a cluster that
71
+ // can be much smaller than the canvas (typical for sparse subgraphs of a
72
+ // few nodes), leaving the operator with two tiny dots in the middle of a
73
+ // 1200×900 image. Compute the bounding box of the settled positions and
74
+ // apply a uniform scale + translate so the cluster fills the canvas with
75
+ // a `FIT_PADDING` border. Aspect ratio is preserved. Degenerate cases
76
+ // (single node, or all nodes collapsed onto one point) skip scaling and
77
+ // are centred at (cx, cy).
78
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
79
+ for (const n of simNodes) {
80
+ const x = n.x ?? cx;
81
+ const y = n.y ?? cy;
82
+ if (x < minX)
83
+ minX = x;
84
+ if (x > maxX)
85
+ maxX = x;
86
+ if (y < minY)
87
+ minY = y;
88
+ if (y > maxY)
89
+ maxY = y;
90
+ }
91
+ const bboxW = maxX - minX;
92
+ const bboxH = maxY - minY;
93
+ const targetW = opts.width - 2 * FIT_PADDING;
94
+ const targetH = opts.height - 2 * FIT_PADDING;
95
+ // Degenerate when a single node, or all nodes collapsed within one pixel.
96
+ const degenerate = simNodes.length <= 1 || (bboxW < 1 && bboxH < 1);
97
+ const scale = degenerate
98
+ ? 1
99
+ : Math.min(targetW / Math.max(bboxW, 1), targetH / Math.max(bboxH, 1));
100
+ const outNodes = simNodes.map((n) => {
101
+ const x = n.x ?? cx;
102
+ const y = n.y ?? cy;
103
+ if (degenerate)
104
+ return { id: n.id, labels: n.labels, x: cx, y: cy };
105
+ // Centre the scaled bbox on the canvas centre.
106
+ const scaledCx = (minX + bboxW / 2) * scale;
107
+ const scaledCy = (minY + bboxH / 2) * scale;
108
+ return {
109
+ id: n.id,
110
+ labels: n.labels,
111
+ x: x * scale - scaledCx + cx,
112
+ y: y * scale - scaledCy + cy,
113
+ };
114
+ });
75
115
  return { nodes: outNodes, edges: validEdges, iterations: ITERATIONS };
76
116
  }
77
117
  //# sourceMappingURL=layout.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"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"}
@@ -30,7 +30,7 @@ export interface GraphRenderRefusal {
30
30
  }
31
31
  export interface GraphRenderSuccess {
32
32
  ok: true;
33
- pngBase64: string;
33
+ path: string;
34
34
  width: number;
35
35
  height: number;
36
36
  nodeCount: number;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-render.d.ts","sourceRoot":"","sources":["../../src/tools/graph-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAWH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,cAAc,GAAG,YAAY,GAAG,eAAe,CAAC;IAC5E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,IAAI,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD;AAED,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAMxE,wBAAsB,WAAW,CAC/B,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,iBAAiB,CAAC,CAkK5B"}
1
+ {"version":3,"file":"graph-render.d.ts","sourceRoot":"","sources":["../../src/tools/graph-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA6BH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,cAAc,GAAG,YAAY,GAAG,eAAe,CAAC;IAC5E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD;AAED,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAMxE,wBAAsB,WAAW,CAC/B,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,iBAAiB,CAAC,CA+K5B"}
@@ -18,9 +18,23 @@
18
18
  * parameterise variable-length path bounds. Anything outside that range
19
19
  * fails fast at the param-validation layer, well before string assembly.
20
20
  */
21
+ import { mkdirSync, writeFileSync } from "node:fs";
22
+ import { resolve } from "node:path";
21
23
  import { getSession } from "../lib/neo4j.js";
22
24
  import { layoutGraph } from "../render/layout.js";
23
25
  import { drawGraph } from "../render/draw.js";
26
+ function getAccountsDir() {
27
+ const root = process.env.MAXY_PLATFORM_ROOT;
28
+ if (!root) {
29
+ throw new Error("[graph-render] MAXY_PLATFORM_ROOT not set — cannot resolve account output dir.");
30
+ }
31
+ return resolve(root, "..", "data/accounts");
32
+ }
33
+ function slugifyElementId(elementId) {
34
+ // Neo4j elementIds use `:` as a separator; filesystem-safe replacement
35
+ // keeps the digest readable while avoiding path-segment confusion.
36
+ return elementId.replace(/[:/\\]/g, "_");
37
+ }
24
38
  const NODE_CAP = 200;
25
39
  const MAX_HOP_DEPTH = 3;
26
40
  const IMAGE_WIDTH = 1200;
@@ -146,16 +160,28 @@ export async function graphRender(params) {
146
160
  message: `Canvas draw failed: ${message}`,
147
161
  };
148
162
  }
163
+ // Write the PNG to a deterministic per-account output path. Operator
164
+ // delivery is via SendUserFile in the calling skill — MCP `image`
165
+ // content blocks do not surface in the maxy-code chat transport
166
+ // (see Task 384). The slug+hopDepth pair makes repeated renders of
167
+ // the same view overwrite the same file rather than accumulating.
168
+ const accountsDir = getAccountsDir();
169
+ const outputDir = resolve(accountsDir, accountId, "output", "graph-render");
170
+ mkdirSync(outputDir, { recursive: true });
171
+ const fileName = `${slugifyElementId(nodeId)}-h${hopDepth}.png`;
172
+ const outputPath = resolve(outputDir, fileName);
173
+ writeFileSync(outputPath, pngBuffer);
149
174
  const truncated = totalCount > nodeRows.length
150
175
  ? { rendered: nodeRows.length, total: totalCount }
151
176
  : undefined;
152
177
  const ms = Date.now() - t0;
153
178
  logLine(`[graph-render] ok account=${accountId} ms=${ms} bytes=${pngBuffer.length} ` +
154
179
  `nodeCount=${nodeRows.length} edgeCount=${edges.length} ` +
155
- `truncated=${truncated ? "true" : "false"} iterations=${layout.iterations}`);
180
+ `truncated=${truncated ? "true" : "false"} iterations=${layout.iterations} ` +
181
+ `path=${outputPath}`);
156
182
  return {
157
183
  ok: true,
158
- pngBase64: pngBuffer.toString("base64"),
184
+ path: outputPath,
159
185
  width: IMAGE_WIDTH,
160
186
  height: IMAGE_HEIGHT,
161
187
  nodeCount: nodeRows.length,
@@ -1 +1 @@
1
- {"version":3,"file":"graph-render.js","sourceRoot":"","sources":["../../src/tools/graph-render.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"graph-render.js","sourceRoot":"","sources":["../../src/tools/graph-render.ts"],"names":[],"mappings":"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"}
@@ -14,9 +14,12 @@ When the operator asks for a picture of a subgraph:
14
14
  asks for the wider neighbourhood. Depth 3 on a busy node will hit the
15
15
  200-node cap and return a `truncated` envelope.
16
16
  3. Call `graph-render` with `{ accountId, nodeId, hopDepth }`.
17
- 4. If the result is `ok: true`, the image attaches itself to your reply.
18
- Tell the operator how many nodes are in view and, if `truncated` is
19
- present, that there were more nodes than the cap.
17
+ 4. If the result is `ok: true`, call `SendUserFile` with the returned
18
+ `path` and a one-line caption describing the subgraph. Tell the
19
+ operator how many nodes are in view and, if `truncated` is present,
20
+ that there were more nodes than the cap. The image will not appear
21
+ in chat until you deliver it via `SendUserFile` — `graph-render` only
22
+ writes the file.
20
23
  5. If the result is `ok: false`, surface the structured `reason` and
21
24
  `message` to the operator verbatim. Do not retry with a different
22
25
  nodeId without their input. Do not fall back to Mermaid, ASCII, or any
@@ -147,7 +147,7 @@ Writes targeting `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:Loc
147
147
 
148
148
  - **Workflow path — `producedByTaskId`.** `memory-write` accepts an optional `producedByTaskId` parameter. When set, the platform composes an inbound `:PRODUCED` edge from that `:Task` node into the write's `relationships` array before the write runs. The Task and the new node must share the same `accountId`; mismatch is rejected loud. The typical pattern: call `work-create` at the start of an autonomous flow (onboarding skill, cloudflare tunnel-login) with `kind` and `raisedDuringConversationKey`; capture the returned `taskId`; pass it as `producedByTaskId` on every subsequent `memory-write` for one of the gated labels.
149
149
 
150
- - **Direct-ask path — `CONVERSATION_NODE_ID` env-stamp.** When `producedByTaskId` is not provided (typical of direct admin asks like "add Anneke as person"), `memory-write` falls back to the `CONVERSATION_NODE_ID` env var that the admin server stamps at MCP spawn time. The value is the elementId of the active `:AdminConversation`. The wrapper verifies `accountId` parity before injecting the synthetic `:PRODUCED` edge `accountId` mismatch is logged as `[mcp:memory] [provenance-missing] reason=account-mismatch` and the gate then fires `missing-provenance`. The env-stamp is invisible to the agent (no schema field changes); the LLM never decides whether to use it.
150
+ - **Direct-ask path — `CONVERSATION_NODE_ID` env-stamp.** When `producedByTaskId` is not provided (typical of direct admin asks like "add Anneke as person"), `memory-write` falls back to the `CONVERSATION_NODE_ID` env var. The admin server stamps it at PTY spawn time on both admin sessions and on the specialist subagent spawns the admin dispatches (Task 382 — listing-curator, content-producer, database-operator and friends inherit the same anchor). The value is the `conversationId` UUID of the active `:AdminConversation`; the wrapper's `injectConversationProvenance` MATCHes `(c:Conversation {conversationId, accountId})` so account isolation is enforced inside the natural keya cross-account stamp returns zero rows and never injects. Autonomous spawns with no parent conversation (cron, scheduled-task) legitimately have no env-stamp; those must thread `producedByTaskId` via `work-create` instead. The env-stamp is invisible to the agent (no schema field changes); the LLM never decides whether to use it.
151
151
 
152
152
  ## Graph Hygiene
153
153
 
@@ -96,7 +96,7 @@ If the source emits multiple distinct listing events tied to a single underlying
96
96
  ## Relationship Patterns
97
97
 
98
98
  ```
99
- (:Listing)-[:LISTED_BY]->(:LocalBusiness)
99
+ (:Listing)-[:LISTED_BY]->(:Organization {brandSlug})
100
100
  (:Listing)-[:FOR_PROPERTY]->(:Property)
101
101
  (:Listing)-[:DEPICTS]->(:ImageObject)
102
102
  (:Viewing)-[:FOR_LISTING]->(:Listing)
@@ -105,6 +105,6 @@ If the source emits multiple distinct listing events tied to a single underlying
105
105
  (:Offer)-[:FROM_APPLICANT]->(:Person)
106
106
  ```
107
107
 
108
- `(:Listing)-[:LISTED_BY]->(:LocalBusiness)` is the mandatory parent edge that every Listing carries — it satisfies the ≥1-adjacency write-gate doctrine ([[feedback_graph_hierarchy_doctrine]]) and names the estate agency that owns the listing. `[:FOR_PROPERTY]` is composed additionally when the underlying physical `:Property` node is known (full structural data captured); it is optional because Listings frequently land before Property structural data has been collected.
108
+ `(:Listing)-[:LISTED_BY]->(:Organization {brandSlug})` is the mandatory parent edge that every Listing carries — it satisfies the ≥1-adjacency write-gate doctrine ([[feedback_graph_hierarchy_doctrine]]) and names the estate agency that listed the property. The target is `:Organization` keyed by `brandSlug`, not `:LocalBusiness`. `:LocalBusiness` is the per-account installer-seeded singleton (the account's own business — Real Agent itself), held immutable by the write gate; the agencies whose listings ingest through the curator are third-party businesses and live as `:Organization`. `[:FOR_PROPERTY]` is composed additionally when the underlying physical `:Property` node is known (full structural data captured); it is optional because Listings frequently land before Property structural data has been collected.
109
109
 
110
- `(:Listing)-[:DEPICTS]->(:ImageObject)` is written by the curator once per image present on the Listing. ImageObjects satisfy the ≥1-adjacency rule via this edge (Listing is the parent); they do not carry their own `LISTED_BY` edge to LocalBusiness because the Listing already does. Hybrid search expands one hop by default, so a memory-search returning a Listing also returns its DEPICTS-linked ImageObjects in the result's `related` array — no separate search call needed to assemble a property card with the right image.
110
+ `(:Listing)-[:DEPICTS]->(:ImageObject)` is written by the curator once per image present on the Listing. ImageObjects satisfy the ≥1-adjacency rule via this edge (Listing is the parent); they do not carry their own `LISTED_BY` edge because the Listing already does. Hybrid search expands one hop by default, so a memory-search returning a Listing also returns its DEPICTS-linked ImageObjects in the result's `related` array — no separate search call needed to assemble a property card with the right image.
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,EAYL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAoB9E;4CAC4C;AAC5C,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD;AAwED,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAI1D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;CAC/B;AA+ID;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAozCpD"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,EAYL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAwB9E;4CAC4C;AAC5C,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD;AAwED,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAI1D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;CAC/B;AA+ID;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAu0CpD"}
@@ -32,6 +32,10 @@ import { basename, join } from 'node:path';
32
32
  import { validateUserTitle } from './user-title-store.js';
33
33
  const ROLES = ['admin', 'public'];
34
34
  const CHANNELS = ['browser', 'whatsapp', 'telegram', 'webchat', 'email'];
35
+ // Task 382 — body.conversationNodeId shape gate. The value is the
36
+ // conversationId UUID (v4) minted by createNewAdminConversation in
37
+ // platform/ui/app/lib/neo4j-store.ts.
38
+ const UUID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
35
39
  // Task 179 — resume idempotency. Two POSTs 9ms apart with the same
36
40
  // idempotencyKey produce one PTY and return the same ManagerSession.
37
41
  // Keyed on `${senderId}:${idempotencyKey}` so two operators' keys cannot
@@ -487,6 +491,12 @@ export function buildHttpApp(deps) {
487
491
  const agentSlug = typeof body.agentSlug === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(body.agentSlug)
488
492
  ? body.agentSlug
489
493
  : undefined;
494
+ // Task 382 — conversationId UUID of the active :AdminConversation (or
495
+ // :PublicConversation). Shape-validated as a v4 UUID; anything else is
496
+ // discarded so a body-controlled value cannot reach the env-stamp.
497
+ const conversationNodeId = typeof body.conversationNodeId === 'string' && UUID_PATTERN.test(body.conversationNodeId)
498
+ ? body.conversationNodeId
499
+ : undefined;
490
500
  const aboutOwnerShape = body.aboutOwner == null ? 'absent' : (typeof body.aboutOwner === 'object' ? 'object' : typeof body.aboutOwner);
491
501
  const dormantPluginsShape = Array.isArray(body.dormantPlugins)
492
502
  ? `array(${body.dormantPlugins.length})`
@@ -583,6 +593,7 @@ export function buildHttpApp(deps) {
583
593
  initialMessage,
584
594
  agentSlug,
585
595
  liveMemory: resolvedLiveMemory,
596
+ conversationNodeId,
586
597
  });
587
598
  if (!result.ok) {
588
599
  if ('rejected' in result) {
@@ -959,6 +970,10 @@ export function buildHttpApp(deps) {
959
970
  : undefined;
960
971
  const sourceRow = resolveRow(deps.watcher, sessionId);
961
972
  const specialist = bodySpecialist ?? sourceRow?.agent ?? undefined;
973
+ // Task 382 — conversationId UUID gate. Same shape gate as /spawn.
974
+ const conversationNodeId = typeof body.conversationNodeId === 'string' && UUID_PATTERN.test(body.conversationNodeId)
975
+ ? body.conversationNodeId
976
+ : undefined;
962
977
  // Task 258 / Task 273 — restore the archived jsonl to its canonical
963
978
  // top-level path before claude --resume runs. Claude CLI manages
964
979
  // resume natively as one file per UUID: it appends to
@@ -1035,6 +1050,7 @@ export function buildHttpApp(deps) {
1035
1050
  activePlugins,
1036
1051
  specialistDomains,
1037
1052
  specialist,
1053
+ conversationNodeId,
1038
1054
  });
1039
1055
  if (!result.ok) {
1040
1056
  if ('rejected' in result) {