@rubytech/create-realagent 1.0.691 → 1.0.695
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.
- package/package.json +1 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts +127 -0
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/index.js +393 -0
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-search/src/__tests__/bm25-only.test.ts +129 -0
- package/payload/platform/lib/graph-search/src/__tests__/escape-and-normalise.test.ts +53 -0
- package/payload/platform/lib/graph-search/src/__tests__/hybrid.test.ts +190 -0
- package/payload/platform/lib/graph-search/src/index.ts +498 -0
- package/payload/platform/lib/graph-search/tsconfig.json +9 -0
- package/payload/platform/lib/graph-search/vitest.config.ts +9 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +61 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.js +97 -0
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-write/src/index.ts +167 -0
- package/payload/platform/lib/graph-write/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js +69 -15
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/index.js +27 -3
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +4 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +10 -6
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +43 -36
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
- package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +44 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +3 -32
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +18 -381
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +9 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +10 -23
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/references/graph-primitives.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +8 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +24 -10
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/index.js +8 -2
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +2 -0
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -18
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js +12 -2
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js.map +1 -1
- package/payload/platform/scripts/logs-read.sh +63 -9
- package/payload/platform/scripts/logs-read.test.sh +212 -0
- package/payload/server/chunk-IAIGB5WN.js +11406 -0
- package/payload/server/chunk-Q6NDXCM6.js +11448 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{admin-Db49Ee_m.js → admin-zbb1g-mh.js} +2 -2
- package/payload/server/public/assets/{data-DGzrcwpZ.js → data-BKexd229.js} +1 -1
- package/payload/server/public/assets/{file-9OCmBpAn.js → file-DZkqmm8M.js} +1 -1
- package/payload/server/public/assets/{graph-ByzS1__T.js → graph-CPqHYozW.js} +1 -1
- package/payload/server/public/assets/{house-5FuTj56_.js → house-CPxWBrMl.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-2yRmkrVq.css +1 -0
- package/payload/server/public/assets/{public-ePQo7y74.js → public-BthX_YNC.js} +1 -1
- package/payload/server/public/assets/{share-2-DZHAp0uN.js → share-2-Bauv6ctA.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-4Cpt_4GU.js → useVoiceRecorder-BTEcf6H3.js} +1 -1
- package/payload/server/public/assets/{x-DRLvmPIm.js → x-Kc93nSru.js} +1 -1
- package/payload/server/public/data.html +6 -6
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +656 -21
- package/payload/server/public/assets/jsx-runtime-DdGj3ITV.css +0 -1
- /package/payload/server/public/assets/{jsx-runtime-CT8kMwlr.js → jsx-runtime-7o3Lvx89.js} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write doctrine (Task 673): a node without at least one adjacency is noise,
|
|
3
|
+
* not knowledge. `writeNodeWithEdges` is the single primitive every graph
|
|
4
|
+
* writer should call; it rejects zero-relationship writes, verifies every
|
|
5
|
+
* relationship target resolves before the node is created, and commits the
|
|
6
|
+
* node + all edges in one managed transaction. Provenance is stamped on the
|
|
7
|
+
* node as flattened `createdBy*` properties (Neo4j does not persist nested
|
|
8
|
+
* maps, and flat props are queryable — `MATCH (n) WHERE n.createdBySession
|
|
9
|
+
* = $id RETURN n` is the forensic entry point).
|
|
10
|
+
*
|
|
11
|
+
* Rejection paths (every one emits a stderr log the admin server pipes to
|
|
12
|
+
* server.log, so orphan pressure is visible per-write not just in the
|
|
13
|
+
* hourly [graph-health] signal):
|
|
14
|
+
* - zero relationships → `[graph-write] reject reason=zero-relationships`
|
|
15
|
+
* - unresolved target id → `[graph-write] reject reason=unresolved-target`
|
|
16
|
+
*
|
|
17
|
+
* The `createdBy.agent` field is advisory, not authoritative — it is sourced
|
|
18
|
+
* from the MCP server's AGENT_SLUG env var, which is set by the trusted
|
|
19
|
+
* spawning code (admin server / workflow runner). A misconfigured spawner
|
|
20
|
+
* will write "unknown"; that's the signal something is wrong. Do not use
|
|
21
|
+
* this field for access control — it is forensic, not a security boundary.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Session } from "neo4j-driver";
|
|
25
|
+
|
|
26
|
+
export interface GraphRelationship {
|
|
27
|
+
type: string;
|
|
28
|
+
direction: "outgoing" | "incoming";
|
|
29
|
+
targetNodeId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CreatedBy {
|
|
33
|
+
/** Agent slug (e.g. "maxy-admin", "whatsapp-public") for LLM-tool writes. */
|
|
34
|
+
agent?: string;
|
|
35
|
+
/** Session correlation id — same string across every node written in a conversation turn. */
|
|
36
|
+
session?: string;
|
|
37
|
+
/** MCP tool name ("memory-write", "contact-create", ...) for LLM-tool writes. */
|
|
38
|
+
tool?: string;
|
|
39
|
+
/** Subsystem name ("workflow-execute", "persist-tool-call", ...) for system writes. */
|
|
40
|
+
source?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WriteNodeWithEdgesParams {
|
|
44
|
+
session: Session;
|
|
45
|
+
labels: string[];
|
|
46
|
+
props: Record<string, unknown>;
|
|
47
|
+
/** At least one relationship is required — zero-rel writes are rejected. */
|
|
48
|
+
relationships: GraphRelationship[];
|
|
49
|
+
createdBy: CreatedBy;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WriteNodeResult {
|
|
53
|
+
nodeId: string;
|
|
54
|
+
labels: string[];
|
|
55
|
+
edgesCreated: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stamp flattened provenance into node properties. */
|
|
59
|
+
export function stampCreatedBy(
|
|
60
|
+
props: Record<string, unknown>,
|
|
61
|
+
createdBy: CreatedBy
|
|
62
|
+
): Record<string, unknown> {
|
|
63
|
+
return {
|
|
64
|
+
...props,
|
|
65
|
+
createdByAgent: createdBy.agent ?? "unknown",
|
|
66
|
+
createdBySession: createdBy.session ?? "unknown",
|
|
67
|
+
createdByTool: createdBy.tool ?? null,
|
|
68
|
+
createdBySource: createdBy.source ?? null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Enforce the write doctrine: node + ≥1 edge, transactional, provenance stamped.
|
|
74
|
+
* The label and relationship type strings are interpolated into Cypher — the
|
|
75
|
+
* caller must constrain them via Zod/schema before calling. Backticks are
|
|
76
|
+
* stripped to defeat the only Cypher escape char.
|
|
77
|
+
*/
|
|
78
|
+
export async function writeNodeWithEdges(
|
|
79
|
+
params: WriteNodeWithEdgesParams
|
|
80
|
+
): Promise<WriteNodeResult> {
|
|
81
|
+
const { session, labels, props, relationships, createdBy } = params;
|
|
82
|
+
|
|
83
|
+
const agentLabel = createdBy.agent ?? createdBy.source ?? "unknown";
|
|
84
|
+
const labelCsv = labels.join(",");
|
|
85
|
+
|
|
86
|
+
if (!relationships || relationships.length < 1) {
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
`[graph-write] reject reason=zero-relationships labels=${labelCsv} agent=${agentLabel}\n`
|
|
89
|
+
);
|
|
90
|
+
throw new Error(
|
|
91
|
+
"Write doctrine violated: a node must be created with at least one relationship. See .docs/neo4j.md (Write doctrine)."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const labelStr = labels.map((l) => `\`${l.replace(/`/g, "")}\``).join(":");
|
|
96
|
+
const nodeProps = stampCreatedBy(props, createdBy);
|
|
97
|
+
|
|
98
|
+
return await session.executeWrite(async (tx) => {
|
|
99
|
+
const targetIds = relationships.map((r) => r.targetNodeId);
|
|
100
|
+
const check = await tx.run(
|
|
101
|
+
`UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN count(DISTINCT t) AS found`,
|
|
102
|
+
{ ids: targetIds }
|
|
103
|
+
);
|
|
104
|
+
const found = check.records[0].get("found").toNumber();
|
|
105
|
+
const uniqueRequested = new Set(targetIds).size;
|
|
106
|
+
if (found !== uniqueRequested) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
`[graph-write] reject reason=unresolved-target labels=${labelCsv} agent=${agentLabel} requested=${uniqueRequested} found=${found}\n`
|
|
109
|
+
);
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Write doctrine violated: ${uniqueRequested - found} of ${uniqueRequested} relationship target(s) did not resolve (elementId mismatch). No node created.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const nodeRes = await tx.run(
|
|
116
|
+
`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`,
|
|
117
|
+
{ props: nodeProps }
|
|
118
|
+
);
|
|
119
|
+
const nodeId = nodeRes.records[0].get("nodeId") as string;
|
|
120
|
+
const nodeLabels = nodeRes.records[0].get("nodeLabels") as string[];
|
|
121
|
+
|
|
122
|
+
// Neo4j Community's default isolation is read-committed (not snapshot)
|
|
123
|
+
// — a target elementId that resolved during the pre-check can be
|
|
124
|
+
// deleted by a concurrent transaction before the per-edge CREATE below
|
|
125
|
+
// runs. If that happens, `MATCH (a), (b)` returns 0 rows and the CREATE
|
|
126
|
+
// quietly produces no edge. Per-edge counter inspection catches the
|
|
127
|
+
// race: any zero-result CREATE throws inside the transaction callback,
|
|
128
|
+
// so `executeWrite` rolls the node back — no silent-orphan path.
|
|
129
|
+
let edgesCreated = 0;
|
|
130
|
+
for (const rel of relationships) {
|
|
131
|
+
const type = rel.type.replace(/`/g, "");
|
|
132
|
+
const q =
|
|
133
|
+
rel.direction === "outgoing"
|
|
134
|
+
? `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (a)-[:\`${type}\`]->(b)`
|
|
135
|
+
: `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (b)-[:\`${type}\`]->(a)`;
|
|
136
|
+
const r = await tx.run(q, { from: nodeId, to: rel.targetNodeId });
|
|
137
|
+
const created = r.summary.counters.updates().relationshipsCreated;
|
|
138
|
+
if (created === 0) {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
`[graph-write] reject reason=unresolved-target-on-create labels=${labelCsv} agent=${agentLabel} relType=${rel.type} targetId=${rel.targetNodeId}\n`
|
|
141
|
+
);
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Write doctrine violated: relationship CREATE to target ${rel.targetNodeId} produced 0 edges (target likely deleted concurrently after pre-check). Transaction rolled back.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
edgesCreated += created;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (edgesCreated !== relationships.length) {
|
|
150
|
+
// Defensive: should be unreachable given the per-edge check above.
|
|
151
|
+
// The rollback throws loudly so regression shows rather than a
|
|
152
|
+
// silent commit with edgesCreated < requested.
|
|
153
|
+
process.stderr.write(
|
|
154
|
+
`[graph-write] reject reason=edge-count-mismatch labels=${labelCsv} agent=${agentLabel} requested=${relationships.length} created=${edgesCreated}\n`
|
|
155
|
+
);
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Write doctrine violated: expected ${relationships.length} edges, created ${edgesCreated}. Transaction rolled back.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
process.stderr.write(
|
|
162
|
+
`[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"}\n`
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return { nodeId, labels: nodeLabels, edgesCreated };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { readFile, writeFile } from "node:fs/promises";
|
|
7
7
|
import { resolve, join } from "node:path";
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { writeKey, validateKey, hasKey, keyFilePath, deleteKey } from "../../../../lib/anthropic-key/dist/index.js";
|
|
11
11
|
import { deviceUrlBlock } from "../../../../lib/device-url/dist/index.js";
|
|
12
12
|
import { createHash, randomInt, randomUUID } from "node:crypto";
|
|
@@ -968,6 +968,23 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=syst
|
|
|
968
968
|
// Precedes the sessionKey grep path because a conversationId is the
|
|
969
969
|
// tightest identity and answers single-conversation questions without
|
|
970
970
|
// a grep pass across every log file.
|
|
971
|
+
//
|
|
972
|
+
// Task 671: resolves two filename shapes from the same identifier —
|
|
973
|
+
// FULL: {prefix}-{conversationId}.log (post 1→2-user-turn flush)
|
|
974
|
+
// PREFLUSH: {prefix}-preflush-{id:0:12}.log (pre-flush, first turn;
|
|
975
|
+
// slice is of the sessionKey)
|
|
976
|
+
// Full is tried first; if absent, preflush is the fallback. If both
|
|
977
|
+
// exist, full wins and the preflush sibling is logged to server.log
|
|
978
|
+
// as stale (best-effort housekeeping signal).
|
|
979
|
+
//
|
|
980
|
+
// Identity note: for the abrupt-exit case this branch exists to solve,
|
|
981
|
+
// the operator passes the sessionKey (no conversationId was ever
|
|
982
|
+
// assigned). For post-flush retrievals the operator passes the
|
|
983
|
+
// conversationId and the FULL file is expected to exist.
|
|
984
|
+
//
|
|
985
|
+
// MIRROR: keep the two-shape contract in sync with:
|
|
986
|
+
// - platform/ui/app/lib/logs-read-resolve.ts (canonical TS helper + tests)
|
|
987
|
+
// - platform/scripts/logs-read.sh (per_conversation_mode)
|
|
971
988
|
if (conversationId) {
|
|
972
989
|
if (!existsSync(LOG_DIR)) {
|
|
973
990
|
return { content: [{ type: "text", text: `Log directory does not exist: ${LOG_DIR}` }] };
|
|
@@ -986,13 +1003,39 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=syst
|
|
|
986
1003
|
isError: true,
|
|
987
1004
|
};
|
|
988
1005
|
}
|
|
989
|
-
const
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
1006
|
+
const fullName = `${prefix}-${conversationId}.log`;
|
|
1007
|
+
const preflushName = `${prefix}-preflush-${conversationId.slice(0, 12)}.log`;
|
|
1008
|
+
const fullPath = resolve(LOG_DIR, fullName);
|
|
1009
|
+
const preflushPath = resolve(LOG_DIR, preflushName);
|
|
1010
|
+
// Pass 1: full first. If present, emit stale-preflush signal on sibling.
|
|
1011
|
+
if (existsSync(fullPath)) {
|
|
1012
|
+
if (existsSync(preflushPath)) {
|
|
1013
|
+
// Best-effort housekeeping emit — retrieval must not fail if
|
|
1014
|
+
// server.log is unwritable.
|
|
1015
|
+
try {
|
|
1016
|
+
const ts = new Date().toISOString();
|
|
1017
|
+
appendFileSync(resolve(CONFIG_DIR, "logs", "server.log"), `${ts} [logs-read] stale-preflush-detected path=${preflushPath}\n`);
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
// swallow
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const result = execFileSync("tail", ["-n", String(lines), fullPath], { timeout: 5000 }).toString();
|
|
1024
|
+
return { content: [{ type: "text", text: `# ${fullName} (matched_shape=full)\n\n${result}` }] };
|
|
1025
|
+
}
|
|
1026
|
+
// Pass 2: preflush fallback.
|
|
1027
|
+
if (existsSync(preflushPath)) {
|
|
1028
|
+
const result = execFileSync("tail", ["-n", String(lines), preflushPath], { timeout: 5000 }).toString();
|
|
1029
|
+
return { content: [{ type: "text", text: `# ${preflushName} (matched_shape=preflush)\n\n${result}` }] };
|
|
993
1030
|
}
|
|
994
|
-
|
|
995
|
-
|
|
1031
|
+
// Neither shape present: enumerate both filenames in the miss
|
|
1032
|
+
// message so the reader sees exactly what was tried.
|
|
1033
|
+
return {
|
|
1034
|
+
content: [{
|
|
1035
|
+
type: "text",
|
|
1036
|
+
text: `No log file found for conversationId=${conversationId} type=${resolvedType}. tried=[${fullPath}, ${preflushPath}] reason=file-not-found-in-either-shape`,
|
|
1037
|
+
}],
|
|
1038
|
+
};
|
|
996
1039
|
}
|
|
997
1040
|
if (!existsSync(LOG_DIR)) {
|
|
998
1041
|
return { content: [{ type: "text", text: `Log directory does not exist: ${LOG_DIR}` }] };
|
|
@@ -2451,17 +2494,22 @@ function cleanupPendingAction(actionId) {
|
|
|
2451
2494
|
}
|
|
2452
2495
|
}
|
|
2453
2496
|
async function persistApprovalToolCall(opts) {
|
|
2497
|
+
// Write doctrine (Task 673): a ToolCall without its owning Conversation is
|
|
2498
|
+
// noise — every admin tool call belongs to a chat. When conversationId is
|
|
2499
|
+
// missing or its Conversation cannot be matched, we log and skip the
|
|
2500
|
+
// persist rather than create an orphan ToolCall node. The legacy
|
|
2501
|
+
// OPTIONAL-MATCH + FOREACH pattern created orphans silently.
|
|
2502
|
+
if (!opts.conversationId) {
|
|
2503
|
+
console.error(`[approval] ToolCall skipped (no conversationId): tool=${opts.toolName} state=${opts.approvalState}`);
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2454
2506
|
const session = getSession();
|
|
2455
2507
|
try {
|
|
2456
2508
|
const optionalFields = [
|
|
2457
2509
|
opts.originalInput != null ? ", originalInput: $originalInput" : "",
|
|
2458
|
-
opts.conversationId != null ? ", conversationId: $conversationId" : "",
|
|
2459
2510
|
].join("");
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
? `\nWITH tc\nOPTIONAL MATCH (c:Conversation {conversationId: $conversationId})\nFOREACH (_ IN CASE WHEN c IS NOT NULL THEN [1] ELSE [] END | CREATE (c)-[:HAS_TOOL_CALL]->(tc))`
|
|
2463
|
-
: "";
|
|
2464
|
-
await session.run(`CREATE (tc:ToolCall {
|
|
2511
|
+
const res = await session.run(`MATCH (c:Conversation {conversationId: $conversationId})
|
|
2512
|
+
CREATE (c)-[:HAS_TOOL_CALL]->(tc:ToolCall {
|
|
2465
2513
|
callId: $callId,
|
|
2466
2514
|
toolName: $toolName,
|
|
2467
2515
|
pluginName: $pluginName,
|
|
@@ -2471,10 +2519,12 @@ async function persistApprovalToolCall(opts) {
|
|
|
2471
2519
|
agentType: 'admin',
|
|
2472
2520
|
accountId: $accountId,
|
|
2473
2521
|
approvalState: $approvalState,
|
|
2522
|
+
conversationId: $conversationId,
|
|
2523
|
+
createdBySource: 'admin-approval',
|
|
2474
2524
|
startedAt: datetime($startedAt),
|
|
2475
2525
|
completedAt: datetime($completedAt)
|
|
2476
2526
|
${optionalFields}
|
|
2477
|
-
})
|
|
2527
|
+
})`, {
|
|
2478
2528
|
callId: randomUUID(),
|
|
2479
2529
|
toolName: opts.toolName,
|
|
2480
2530
|
pluginName: opts.pluginName,
|
|
@@ -2483,11 +2533,15 @@ async function persistApprovalToolCall(opts) {
|
|
|
2483
2533
|
isError: opts.isError,
|
|
2484
2534
|
accountId: ACCOUNT_ID,
|
|
2485
2535
|
approvalState: opts.approvalState,
|
|
2536
|
+
conversationId: opts.conversationId,
|
|
2486
2537
|
startedAt: new Date().toISOString(),
|
|
2487
2538
|
completedAt: new Date().toISOString(),
|
|
2488
2539
|
...(opts.originalInput != null ? { originalInput: opts.originalInput.slice(0, 300) } : {}),
|
|
2489
|
-
...(opts.conversationId != null ? { conversationId: opts.conversationId } : {}),
|
|
2490
2540
|
});
|
|
2541
|
+
if (res.summary.counters.updates().nodesCreated === 0) {
|
|
2542
|
+
console.error(`[approval] ToolCall skipped (conversation ${opts.conversationId} not found): tool=${opts.toolName}`);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2491
2545
|
console.error(`[approval] ToolCall persisted: ${opts.toolName} state=${opts.approvalState}`);
|
|
2492
2546
|
}
|
|
2493
2547
|
catch (err) {
|