@rubytech/create-maxy-code 0.1.159 → 0.1.163
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/src/__tests__/fulltext-coverage.test.ts +48 -17
- package/payload/platform/neo4j/schema.cypher +59 -36
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +29 -3
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
- package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +11 -6
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +3 -2
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +5 -1
- package/payload/platform/plugins/email/mcp/dist/index.js +25 -0
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +8 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +177 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +6 -54
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +12 -300
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts +6 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js +89 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-classify.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +11 -6
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +10 -37
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +1 -0
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +10 -1
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +139 -439
- package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +15 -47
- package/payload/platform/plugins/memory/mcp/dist/index.js +168 -29
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +43 -27
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js +62 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-delete-gate.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/typed-edge-schema.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +42 -25
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +23 -17
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js +61 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/email.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js +3 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-normalisers/types.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js +9 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/conversation-pipeline/derive-keys.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts +41 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +71 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js +31 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-delete-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +162 -78
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-normalisers-source-agnosticism.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js +141 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-delete-reserved-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js +86 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-empty-trash-reserved-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +33 -34
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +94 -273
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts +43 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js +96 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-list-chunks.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js +73 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/kd-classify.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +0 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +40 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +7 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +17 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +11 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts +28 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js +46 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-node-exists.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +26 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +5 -4
- package/payload/platform/plugins/memory/references/schema-base.md +4 -6
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +100 -116
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +74 -24
- package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +1 -1
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +1 -1
- package/payload/platform/plugins/substack-import/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/substack-import/PLUGIN.md +34 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +182 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/archive-shape.md +68 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/attachments.md +72 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/engagement.md +61 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/posts.md +80 -0
- package/payload/platform/plugins/substack-import/skills/substack-import/references/subscribers.md +74 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -4
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
- package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +45 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +5 -17
- package/payload/platform/templates/specialists/agents/librarian.md +1 -1
|
@@ -1,58 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// =============================================================================
|
|
3
|
-
// conversation-archive-ingest.mjs —
|
|
4
|
-
// conversation-archive
|
|
5
|
-
// env-only identity, fsync-per-line progress sink, per-session
|
|
6
|
-
// checkpointing).
|
|
3
|
+
// conversation-archive-ingest.mjs — thin parse-and-prepare helper for the
|
|
4
|
+
// `conversation-archive` skill (Task 435).
|
|
7
5
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// Contract (post-Task 435): this script does ONLY the deterministic work —
|
|
7
|
+
// argv + env validation, source-file resolution, per-source normalisation,
|
|
8
|
+
// archiveSha256 + conversationIdentity computation, owner + participant
|
|
9
|
+
// verification, prior-archive cursor lookup, delta slicing, sessionization,
|
|
10
|
+
// stable archive-title computation. It emits ONE JSON line on stdout
|
|
11
|
+
// containing the prepared sessions array (turn-attributed text per session +
|
|
12
|
+
// per-session cursor metadata) plus every identity/scope value the
|
|
13
|
+
// `memory-ingest` MCP call needs. The script does NOT classify, does NOT
|
|
14
|
+
// call `memory-ingest`, does NOT loop.
|
|
10
15
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
+
// The dispatched specialist drives the rest in-turn: for each session in the
|
|
17
|
+
// emitted JSON, it reads the turn text, produces a typed-section JSON
|
|
18
|
+
// chunking, and invokes `memory-ingest` with that session's sections + the
|
|
19
|
+
// per-session cursor (`lastIngestedMessageHash`, `lastIngestedMessageAt`).
|
|
20
|
+
// The first session's call carries `cleanupPriorChunks: true` iff the run
|
|
21
|
+
// was invoked with `--rebuild`.
|
|
16
22
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
23
|
+
// Pre-Task-435 the script ran the Haiku classify + per-session memory-ingest
|
|
24
|
+
// itself; that path was deleted in Task 424 (server-side classifier removed)
|
|
25
|
+
// and finished off here in Task 435 (driver-side cleanup). The empty
|
|
26
|
+
// classifier stub the .mjs carried between 424 and 435 produced one raw
|
|
27
|
+
// unenriched `:Conversation` chunk per session — that path is gone too.
|
|
22
28
|
//
|
|
23
29
|
// Argv (positional): <archive-path>
|
|
24
30
|
// Argv (flags): --source <enum>
|
|
25
31
|
// --participant-person-ids <csv>
|
|
26
32
|
// --scope <admin|public>
|
|
27
|
-
// [--rebuild]
|
|
33
|
+
// [--rebuild]
|
|
28
34
|
// [--timezone <iana>]
|
|
29
35
|
// [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>]
|
|
30
|
-
// [--session-id <id>]
|
|
31
36
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
37
|
+
// Identity: `process.env.ACCOUNT_ID` and `process.env.USER_ID` are the only
|
|
38
|
+
// inputs. Both are plumbed by `spawn-env.ts` into every Bash subprocess;
|
|
39
|
+
// missing/malformed → LOUD-FAIL at phase=argv. The writer derives the owner
|
|
40
|
+
// :AdminUser elementId from (accountId, userId) one Cypher hop away.
|
|
36
41
|
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
42
|
+
// Sessions split at SESSION_GAP_HOURS=8 (fixed constant). The legacy
|
|
43
|
+
// `--session-gap-hours` flag is REMOVED; passing it FAILs at phase=argv.
|
|
44
|
+
//
|
|
45
|
+
// Stdout (success): one JSON line. See SKILL.md for the shape.
|
|
46
|
+
// Stderr: `[conversation-archive] FAIL phase=<…> reason="..."` on failure.
|
|
47
|
+
// Exit 0 on success; non-zero on failure.
|
|
43
48
|
// =============================================================================
|
|
44
49
|
|
|
45
50
|
import {
|
|
46
|
-
closeSync,
|
|
47
51
|
existsSync,
|
|
48
|
-
fsyncSync,
|
|
49
|
-
mkdirSync,
|
|
50
52
|
mkdtempSync,
|
|
51
|
-
openSync,
|
|
52
53
|
readdirSync,
|
|
53
54
|
rmSync,
|
|
54
55
|
statSync,
|
|
55
|
-
writeSync,
|
|
56
56
|
} from "node:fs";
|
|
57
57
|
import { join, resolve, dirname } from "node:path";
|
|
58
58
|
import { tmpdir } from "node:os";
|
|
@@ -61,116 +61,34 @@ import { fileURLToPath } from "node:url";
|
|
|
61
61
|
|
|
62
62
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// internally so the gap value is no longer an operational lever. 8h is a
|
|
70
|
-
// tighter default than 12h and matches the operator-decided post-incident
|
|
71
|
-
// constant.
|
|
64
|
+
// Fixed session gap — pre-902 this was a CLI flag with a 12h default; oversize
|
|
65
|
+
// sessions on dense archives drove operators to re-issue with a smaller gap,
|
|
66
|
+
// which combined with cleanup-by-archiveSha256 to silently destroy 138 chunks
|
|
67
|
+
// (Adam Mackay incident, 2026-05-04). The flag is gone; the in-turn classify
|
|
68
|
+
// path absorbs oversize sessions on the specialist side now.
|
|
72
69
|
const SESSION_GAP_HOURS = 8;
|
|
73
70
|
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// 1. Resolve dist paths.
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
71
|
const platformRoot =
|
|
78
72
|
process.env.MAXY_PLATFORM_ROOT?.trim() ||
|
|
79
73
|
resolve(__dirname, "..", "..", "..");
|
|
80
74
|
|
|
81
75
|
const NORMALISERS_PATH = resolve(
|
|
82
|
-
platformRoot,
|
|
83
|
-
"
|
|
84
|
-
"memory",
|
|
85
|
-
"mcp",
|
|
86
|
-
"dist",
|
|
87
|
-
"lib",
|
|
88
|
-
"conversation-normalisers",
|
|
89
|
-
"index.js",
|
|
76
|
+
platformRoot, "plugins", "memory", "mcp", "dist", "lib",
|
|
77
|
+
"conversation-normalisers", "index.js",
|
|
90
78
|
);
|
|
91
79
|
const PIPELINE_PATH = resolve(
|
|
92
|
-
platformRoot,
|
|
93
|
-
"plugins",
|
|
94
|
-
"memory",
|
|
95
|
-
"mcp",
|
|
96
|
-
"dist",
|
|
97
|
-
"lib",
|
|
80
|
+
platformRoot, "plugins", "memory", "mcp", "dist", "lib",
|
|
98
81
|
"conversation-pipeline",
|
|
99
82
|
);
|
|
100
83
|
const NEO4J_LIB_PATH = resolve(
|
|
101
|
-
platformRoot,
|
|
102
|
-
"plugins",
|
|
103
|
-
"memory",
|
|
104
|
-
"mcp",
|
|
105
|
-
"dist",
|
|
106
|
-
"lib",
|
|
107
|
-
"neo4j.js",
|
|
108
|
-
);
|
|
109
|
-
// LLM_CLASSIFIER_PATH removed by Task 424. Task 435 reintroduces per-chunk
|
|
110
|
-
// enrichment as in-turn specialist work.
|
|
111
|
-
const MEMORY_INGEST_PATH = resolve(
|
|
112
|
-
platformRoot,
|
|
113
|
-
"plugins",
|
|
114
|
-
"memory",
|
|
115
|
-
"mcp",
|
|
116
|
-
"dist",
|
|
117
|
-
"tools",
|
|
118
|
-
"memory-ingest.js",
|
|
84
|
+
platformRoot, "plugins", "memory", "mcp", "dist", "lib", "neo4j.js",
|
|
119
85
|
);
|
|
120
86
|
const UUID_LIB_PATH = resolve(
|
|
121
|
-
platformRoot,
|
|
122
|
-
"plugins",
|
|
123
|
-
"memory",
|
|
124
|
-
"mcp",
|
|
125
|
-
"dist",
|
|
126
|
-
"lib",
|
|
127
|
-
"uuid.js",
|
|
87
|
+
platformRoot, "plugins", "memory", "mcp", "dist", "lib", "uuid.js",
|
|
128
88
|
);
|
|
129
89
|
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// 2. Progress sink + logger fan-out (sub-scope A).
|
|
132
|
-
// `log()` and `fail()` write to stderr AND the progress file with fsync per
|
|
133
|
-
// write. Parent Bash returns nothing until child exits; the progress file
|
|
134
|
-
// is the only surface the agent's heartbeat poll can read mid-run.
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
let progressFd = -1;
|
|
137
90
|
const startedMs = Date.now();
|
|
138
91
|
|
|
139
|
-
function openProgressSink(absolutePath) {
|
|
140
|
-
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
141
|
-
progressFd = openSync(absolutePath, "a");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function writeProgressLine(line) {
|
|
145
|
-
// stderr first — preserves existing observability for parents that read
|
|
146
|
-
// stderr after the child exits, AND makes the line visible via the
|
|
147
|
-
// mcp-spawn-tee'd device server.log without a separate sink.
|
|
148
|
-
process.stderr.write(line);
|
|
149
|
-
if (progressFd >= 0) {
|
|
150
|
-
try {
|
|
151
|
-
writeSync(progressFd, line);
|
|
152
|
-
fsyncSync(progressFd);
|
|
153
|
-
} catch {
|
|
154
|
-
// Disk pressure / EBADF: don't kill the run — stderr already carries
|
|
155
|
-
// the line.
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function log(line) {
|
|
161
|
-
writeProgressLine(`[conversation-archive] ${line}\n`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function endRun(exitCode) {
|
|
165
|
-
const totalMs = Date.now() - startedMs;
|
|
166
|
-
writeProgressLine(`[conversation-archive] end exit-code=${exitCode} ms=${totalMs}\n`);
|
|
167
|
-
if (progressFd >= 0) {
|
|
168
|
-
try { closeSync(progressFd); } catch { /* fd already closed */ }
|
|
169
|
-
progressFd = -1;
|
|
170
|
-
}
|
|
171
|
-
process.exit(exitCode);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
92
|
function fail(phase, fields) {
|
|
175
93
|
const fieldStr = Object.entries(fields)
|
|
176
94
|
.map(([k, v]) =>
|
|
@@ -179,12 +97,12 @@ function fail(phase, fields) {
|
|
|
179
97
|
: `${k}=${v ?? "-"}`,
|
|
180
98
|
)
|
|
181
99
|
.join(" ");
|
|
182
|
-
|
|
183
|
-
|
|
100
|
+
process.stderr.write(`[conversation-archive] FAIL phase=${phase} ${fieldStr}\n`);
|
|
101
|
+
process.exit(1);
|
|
184
102
|
}
|
|
185
103
|
|
|
186
104
|
// ---------------------------------------------------------------------------
|
|
187
|
-
//
|
|
105
|
+
// Argv parsing
|
|
188
106
|
// ---------------------------------------------------------------------------
|
|
189
107
|
const VALID_SOURCES = new Set([
|
|
190
108
|
"whatsapp",
|
|
@@ -195,12 +113,17 @@ const VALID_SOURCES = new Set([
|
|
|
195
113
|
"meeting-minutes",
|
|
196
114
|
"imessage",
|
|
197
115
|
"slack",
|
|
116
|
+
"x-dm",
|
|
117
|
+
"email",
|
|
198
118
|
"other",
|
|
199
119
|
]);
|
|
200
120
|
|
|
201
|
-
// Boolean flags carry no value; every other flag consumes the following arg.
|
|
202
121
|
const BOOLEAN_FLAGS = new Set(["rebuild"]);
|
|
203
122
|
|
|
123
|
+
function camelCase(s) {
|
|
124
|
+
return s.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
125
|
+
}
|
|
126
|
+
|
|
204
127
|
function parseArgv(argv) {
|
|
205
128
|
const args = argv.slice(2);
|
|
206
129
|
let archive = null;
|
|
@@ -213,9 +136,6 @@ function parseArgv(argv) {
|
|
|
213
136
|
continue;
|
|
214
137
|
}
|
|
215
138
|
const key = a.slice(2);
|
|
216
|
-
// sub-scope D — the flag re-introduced the surface that enabled
|
|
217
|
-
// the silent-partial-wipe failure (Defect B). Reject loudly with the
|
|
218
|
-
// remediation the operator should use instead.
|
|
219
139
|
if (key === "session-gap-hours") {
|
|
220
140
|
fail("argv", {
|
|
221
141
|
reason: `--session-gap-hours is no longer supported; gap is fixed at 8h. Use --rebuild on the prior export bytes if you need to re-classify.`,
|
|
@@ -232,7 +152,7 @@ function parseArgv(argv) {
|
|
|
232
152
|
if (!archive) fail("argv", { reason: "missing positional <archive>" });
|
|
233
153
|
if (!flags.source) fail("argv", { reason: "missing --source" });
|
|
234
154
|
if (!VALID_SOURCES.has(flags.source)) {
|
|
235
|
-
fail("argv", { reason: `invalid --source "${flags.source}" (whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|other)` });
|
|
155
|
+
fail("argv", { reason: `invalid --source "${flags.source}" (whatsapp|telegram|signal|linkedin-messages|zoom-transcript|meeting-minutes|imessage|slack|x-dm|email|other)` });
|
|
236
156
|
}
|
|
237
157
|
if (!flags.participantPersonIds) {
|
|
238
158
|
fail("argv", {
|
|
@@ -245,13 +165,10 @@ function parseArgv(argv) {
|
|
|
245
165
|
}
|
|
246
166
|
return { archive, flags };
|
|
247
167
|
}
|
|
248
|
-
function camelCase(s) {
|
|
249
|
-
return s.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
250
|
-
}
|
|
251
168
|
|
|
252
169
|
// ---------------------------------------------------------------------------
|
|
253
|
-
//
|
|
254
|
-
//
|
|
170
|
+
// Archive resolution. WhatsApp ships zip-or-dir-or-_chat.txt; other sources
|
|
171
|
+
// pass a single file path the normaliser interprets directly.
|
|
255
172
|
// ---------------------------------------------------------------------------
|
|
256
173
|
function resolveSourceFile(archivePath, source) {
|
|
257
174
|
const abs = resolve(archivePath);
|
|
@@ -290,6 +207,7 @@ function resolveSourceFile(archivePath, source) {
|
|
|
290
207
|
fail("argv", { reason: `unsupported archive shape for source=${source}: ${abs}` });
|
|
291
208
|
return { sourceFile: abs, cleanup: () => {} };
|
|
292
209
|
}
|
|
210
|
+
|
|
293
211
|
function findChatTxt(dir) {
|
|
294
212
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
295
213
|
for (const e of entries) {
|
|
@@ -305,28 +223,17 @@ function findChatTxt(dir) {
|
|
|
305
223
|
}
|
|
306
224
|
|
|
307
225
|
// ---------------------------------------------------------------------------
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
// Format: `<source> · <owner> ↔ <other1>, <other2>, … · <YYYY-MM-DD>→<YYYY-MM-DD>`.
|
|
311
|
-
//
|
|
312
|
-
// The title is computed once per ingest from inputs that are immutable for
|
|
226
|
+
// Stable archive title — computed once per ingest from inputs immutable for
|
|
313
227
|
// the conversation (source, accountId, sortedParticipantElementIds, archive
|
|
314
228
|
// dateRange) so re-ingests produce the same string. Memory-ingest writes it
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
229
|
+
// ON CREATE and COALESCE-on-MATCH; the UI reads `:ConversationArchive.title`
|
|
230
|
+
// directly. Format:
|
|
231
|
+
// `<source> · <owner> ↔ <other1>, <other2>, … · <YYYY-MM-DD>→<YYYY-MM-DD>`
|
|
319
232
|
// Names: AdminUser uses `displayName` then `slug`; Person composes
|
|
320
|
-
// `givenName + familyName`.
|
|
321
|
-
//
|
|
322
|
-
// so a re-ingest after the migration produces the same title shape that
|
|
323
|
-
// already populated the node.
|
|
233
|
+
// `givenName + familyName`. Missing names fall back to short elementId
|
|
234
|
+
// prefixes — same degraded format the migration uses.
|
|
324
235
|
// ---------------------------------------------------------------------------
|
|
325
|
-
|
|
326
236
|
function isoToYmd(iso) {
|
|
327
|
-
// Read the YYYY-MM-DD prefix verbatim — same tz-stable contract as
|
|
328
|
-
// formatRunDate in display-helpers.ts. Parsing to Date + reformatting
|
|
329
|
-
// would flip the day between operator timezones.
|
|
330
237
|
const m = typeof iso === "string" ? iso.match(/^(\d{4}-\d{2}-\d{2})/) : null;
|
|
331
238
|
return m ? m[1] : "?";
|
|
332
239
|
}
|
|
@@ -348,16 +255,11 @@ function pickNameForRow(labels, props) {
|
|
|
348
255
|
}
|
|
349
256
|
|
|
350
257
|
async function computeArchiveTitle({
|
|
351
|
-
source,
|
|
352
|
-
|
|
353
|
-
ownerElementId,
|
|
354
|
-
participantElementIds,
|
|
355
|
-
firstMessageAt,
|
|
356
|
-
lastMessageAt,
|
|
357
|
-
getSession,
|
|
258
|
+
source, accountId, ownerElementId, participantElementIds,
|
|
259
|
+
firstMessageAt, lastMessageAt, getSession,
|
|
358
260
|
}) {
|
|
359
261
|
const session = getSession();
|
|
360
|
-
|
|
262
|
+
const nameByElementId = new Map();
|
|
361
263
|
try {
|
|
362
264
|
const ids = [ownerElementId, ...participantElementIds];
|
|
363
265
|
const r = await session.run(
|
|
@@ -380,22 +282,16 @@ async function computeArchiveTitle({
|
|
|
380
282
|
const otherNames = participantElementIds
|
|
381
283
|
.map((id) => nameByElementId.get(id) || id.slice(0, 8))
|
|
382
284
|
.join(", ");
|
|
383
|
-
|
|
384
|
-
const lastYmd = isoToYmd(lastMessageAt);
|
|
385
|
-
return `${source} · ${ownerName} ↔ ${otherNames} · ${firstYmd}→${lastYmd}`;
|
|
285
|
+
return `${source} · ${ownerName} ↔ ${otherNames} · ${isoToYmd(firstMessageAt)}→${isoToYmd(lastMessageAt)}`;
|
|
386
286
|
}
|
|
387
287
|
|
|
388
288
|
// ---------------------------------------------------------------------------
|
|
389
|
-
//
|
|
289
|
+
// Main
|
|
390
290
|
// ---------------------------------------------------------------------------
|
|
391
291
|
async function main() {
|
|
392
292
|
const { archive, flags } = parseArgv(process.argv);
|
|
393
293
|
const source = flags.source;
|
|
394
294
|
|
|
395
|
-
// Identity is env-only (sub-scope C). spawn-env.ts plumbs both
|
|
396
|
-
// ACCOUNT_ID and USER_ID into every Bash subprocess; the bin LOUD-FAILs if
|
|
397
|
-
// either is absent or malformed. The disk-scan resolver and the
|
|
398
|
-
// --account-id / --owner-element-id flags are gone.
|
|
399
295
|
let UUID_REGEX;
|
|
400
296
|
try {
|
|
401
297
|
({ UUID_REGEX } = await import(UUID_LIB_PATH));
|
|
@@ -431,36 +327,14 @@ async function main() {
|
|
|
431
327
|
const scope = flags.scope;
|
|
432
328
|
const timezone = flags.timezone || "Europe/London";
|
|
433
329
|
const dateFormat = flags.dateFormat;
|
|
434
|
-
// sub-scope B — `--rebuild` is the single opt-in for destructive
|
|
435
|
-
// re-run-with-same-bytes. Without it, re-issuing the bin on the same export
|
|
436
|
-
// is a no-op (chunk-count summary, exit 0). With it, the bin treats the run
|
|
437
|
-
// as first-ingest (skips prior-cursor lookup) and the FIRST session's
|
|
438
|
-
// memoryIngest call cleans prior chunks for the archiveSha256.
|
|
439
330
|
const rebuild = flags.rebuild === true;
|
|
440
|
-
const sessionId =
|
|
441
|
-
flags.sessionId ||
|
|
442
|
-
`conversation-archive:${source}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
|
|
443
|
-
|
|
444
|
-
// ---------------------------------------------------------------------------
|
|
445
|
-
// Open progress sink BEFORE any await. Path is deterministic from
|
|
446
|
-
// (accountId, sessionId); the wrapper sh prints the same path on stdout so
|
|
447
|
-
// the agent's heartbeat polls the right file.
|
|
448
|
-
// ---------------------------------------------------------------------------
|
|
449
|
-
const accountsDir = resolve(platformRoot, "..", "data", "accounts");
|
|
450
|
-
const logsDir = resolve(accountsDir, accountId, "logs");
|
|
451
|
-
const progressFilePath = resolve(logsDir, `conversation-archive-${sessionId}.log`);
|
|
452
|
-
openProgressSink(progressFilePath);
|
|
453
|
-
|
|
454
|
-
log(
|
|
455
|
-
`start session=${sessionId} source=${source} archive=${archive} accountId=${accountId} userId=${userId} participants=${participantElementIds.length} scope=${scope} pid=${process.pid}`,
|
|
456
|
-
);
|
|
457
331
|
|
|
458
332
|
// Imports — fail loudly if any compiled dist missing
|
|
459
333
|
let getNormaliser;
|
|
460
334
|
let sessionize, toTurnText, findDeltaCursor;
|
|
461
335
|
let deriveConversationIdentity, deriveMessageContentHash;
|
|
462
336
|
let verifyParticipantElementIds;
|
|
463
|
-
let getSession
|
|
337
|
+
let getSession;
|
|
464
338
|
try {
|
|
465
339
|
({ getNormaliser } = await import(NORMALISERS_PATH));
|
|
466
340
|
({ sessionize } = await import(join(PIPELINE_PATH, "sessionize.js")));
|
|
@@ -470,7 +344,6 @@ async function main() {
|
|
|
470
344
|
await import(join(PIPELINE_PATH, "derive-keys.js")));
|
|
471
345
|
({ verifyParticipantElementIds } = await import(join(PIPELINE_PATH, "sender-bind.js")));
|
|
472
346
|
({ getSession } = await import(NEO4J_LIB_PATH));
|
|
473
|
-
({ memoryIngest } = await import(MEMORY_INGEST_PATH));
|
|
474
347
|
} catch (err) {
|
|
475
348
|
fail("import", {
|
|
476
349
|
reason: "failed to import compiled dist",
|
|
@@ -478,20 +351,7 @@ async function main() {
|
|
|
478
351
|
});
|
|
479
352
|
}
|
|
480
353
|
|
|
481
|
-
//
|
|
482
|
-
// unenriched raw `:Conversation` chunk; topic summaries / keywords / per-chunk
|
|
483
|
-
// boundaries are reintroduced under Task 435 (specialist produces them
|
|
484
|
-
// in-turn). Until then, the driver still ingests the archive — operator gets
|
|
485
|
-
// the conversation in the graph, just without per-chunk enrichment.
|
|
486
|
-
const classifyDocument = async ({ documentText }) => ({
|
|
487
|
-
kind: "error",
|
|
488
|
-
reason: "Task 424 — server-side classifier removed; Task 435 introduces in-turn enrichment.",
|
|
489
|
-
documentText,
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
// Resolve owner AdminUser elementId from (accountId, userId). One Cypher
|
|
493
|
-
// hop replaces the deleted --owner-element-id flag — owner identity is env
|
|
494
|
-
// input, never an out-of-band flag.
|
|
354
|
+
// Resolve owner :AdminUser elementId from (accountId, userId).
|
|
495
355
|
let ownerElementId;
|
|
496
356
|
{
|
|
497
357
|
const lookupSession = getSession();
|
|
@@ -517,12 +377,11 @@ async function main() {
|
|
|
517
377
|
}
|
|
518
378
|
await lookupSession.close().catch(() => {});
|
|
519
379
|
}
|
|
520
|
-
log(`owner-resolved ownerElementId=${ownerElementId.slice(0, 12)} userId=${userId.slice(0, 8)}`);
|
|
521
380
|
|
|
522
|
-
//
|
|
381
|
+
// Resolve source file
|
|
523
382
|
const { sourceFile, cleanup } = resolveSourceFile(archive, source);
|
|
524
383
|
|
|
525
|
-
//
|
|
384
|
+
// Run the source-specific normaliser
|
|
526
385
|
let normaliserResult;
|
|
527
386
|
try {
|
|
528
387
|
const normaliser = getNormaliser(source);
|
|
@@ -539,16 +398,10 @@ async function main() {
|
|
|
539
398
|
const allLines = normaliserResult.parsedLines;
|
|
540
399
|
const archiveSha256 = normaliserResult.archiveSha256;
|
|
541
400
|
const archiveSourceFile = normaliserResult.archiveSourceFile;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
`parsed lines=${normaliserResult.counters.parsed} media-skipped=${normaliserResult.counters.mediaSkipped} system-skipped=${normaliserResult.counters.systemSkipped}`,
|
|
547
|
-
);
|
|
548
|
-
// zero matched timestamps means this isn't a chat archive — the
|
|
549
|
-
// file should route through document-ingest instead. The agent's
|
|
550
|
-
// database-operator skill picks the right entry; this LOUD-FAIL surfaces
|
|
551
|
-
// misroute as an actionable signal rather than a silent empty ingest.
|
|
401
|
+
|
|
402
|
+
// Zero matched timestamps means this isn't a chat archive — route through
|
|
403
|
+
// document-ingest instead. LOUD-FAIL surfaces misroute as an actionable
|
|
404
|
+
// signal rather than a silent empty ingest.
|
|
552
405
|
if (allLines.length === 0) {
|
|
553
406
|
cleanup();
|
|
554
407
|
fail("parse", {
|
|
@@ -556,50 +409,46 @@ async function main() {
|
|
|
556
409
|
});
|
|
557
410
|
}
|
|
558
411
|
|
|
559
|
-
//
|
|
560
|
-
// accountId and labels. Per-line senderName binding is gone;
|
|
561
|
-
// operator-confirmed participants attach
|
|
412
|
+
// Verify operator-confirmed participants resolve to graph nodes with the
|
|
413
|
+
// right accountId and labels. Per-line senderName binding is gone; only
|
|
414
|
+
// owner + operator-confirmed participants attach via :PARTICIPANT_IN.
|
|
562
415
|
let session = getSession();
|
|
563
416
|
try {
|
|
564
417
|
await verifyParticipantElementIds({
|
|
565
|
-
session,
|
|
566
|
-
accountId,
|
|
567
|
-
ownerElementId,
|
|
568
|
-
participantElementIds,
|
|
418
|
+
session, accountId, ownerElementId, participantElementIds,
|
|
569
419
|
});
|
|
570
420
|
} catch (err) {
|
|
571
421
|
await session.close().catch(() => {});
|
|
572
422
|
cleanup();
|
|
573
423
|
if (err && err.userFacing) {
|
|
574
|
-
|
|
575
|
-
|
|
424
|
+
process.stderr.write(`[conversation-archive] FAIL ${err.message}\n`);
|
|
425
|
+
process.exit(1);
|
|
576
426
|
}
|
|
577
427
|
fail("argv", { reason: err instanceof Error ? err.message : String(err) });
|
|
578
428
|
}
|
|
579
429
|
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
// delta-append OR (sub-scope B) the same-bytes no-op exit when
|
|
586
|
-
// chunks already exist for this archiveSha256.
|
|
430
|
+
// Derive conversationIdentity and look up prior :ConversationArchive (Task
|
|
431
|
+
// 397). Under `--rebuild` we skip the prior-cursor lookup entirely; the
|
|
432
|
+
// run is treated as first-ingest, and the specialist's first memory-ingest
|
|
433
|
+
// call will carry `cleanupPriorChunks: true` to drop prior chunks for this
|
|
434
|
+
// archiveSha256.
|
|
587
435
|
const conversationIdentity = deriveConversationIdentity({
|
|
588
436
|
accountId,
|
|
589
437
|
participantElementIds: [ownerElementId, ...participantElementIds],
|
|
438
|
+
source,
|
|
590
439
|
});
|
|
591
440
|
let priorArchive = null;
|
|
592
441
|
let priorChunkCountForSha = 0;
|
|
593
442
|
if (!rebuild) {
|
|
594
443
|
try {
|
|
595
444
|
const r = await session.run(
|
|
596
|
-
`MATCH (a:ConversationArchive { conversationIdentity: $cid })
|
|
445
|
+
`MATCH (a:ConversationArchive { conversationIdentity: $cid, source: $source })
|
|
597
446
|
OPTIONAL MATCH (a)-[:HAS_SECTION]->(c:Section { archiveSha256: $sha })
|
|
598
447
|
RETURN elementId(a) AS elemId,
|
|
599
448
|
a.lastIngestedMessageHash AS lastHash,
|
|
600
449
|
a.lastIngestedMessageAt AS lastAt,
|
|
601
450
|
count(c) AS chunksForSha LIMIT 1`,
|
|
602
|
-
{ cid: conversationIdentity, sha: archiveSha256 },
|
|
451
|
+
{ cid: conversationIdentity, source, sha: archiveSha256 },
|
|
603
452
|
);
|
|
604
453
|
if (r.records[0] && r.records[0].get("elemId")) {
|
|
605
454
|
priorArchive = {
|
|
@@ -619,11 +468,12 @@ async function main() {
|
|
|
619
468
|
}
|
|
620
469
|
await session.close().catch(() => {});
|
|
621
470
|
|
|
622
|
-
//
|
|
623
|
-
// (i) --rebuild: deltaStart=0, kind=rebuild,
|
|
471
|
+
// Compute deltaStart. Three branches:
|
|
472
|
+
// (i) --rebuild: deltaStart=0, kind=rebuild, prepare entire archive.
|
|
624
473
|
// (ii) no prior archive (or no cursor): first-ingest, deltaStart=0.
|
|
625
474
|
// (iii) prior archive + cursor: standard delta-append. Same-bytes (no
|
|
626
|
-
// cursor advance possible) AND chunks exist for this sha →
|
|
475
|
+
// cursor advance possible) AND chunks exist for this sha → emit
|
|
476
|
+
// empty-delta JSON; specialist surfaces a noop message.
|
|
627
477
|
let deltaStart = 0;
|
|
628
478
|
let deltaKind = rebuild ? "rebuild" : "first-ingest";
|
|
629
479
|
if (!rebuild && priorArchive && priorArchive.lastHash) {
|
|
@@ -637,243 +487,93 @@ async function main() {
|
|
|
637
487
|
});
|
|
638
488
|
}
|
|
639
489
|
if (cursor.kind === "empty") {
|
|
640
|
-
log(`noop reason="no new messages since ${priorArchive.lastAt}" prior-chunks=${priorChunkCountForSha}`);
|
|
641
490
|
cleanup();
|
|
642
|
-
const totalMs = Date.now() - startedMs;
|
|
643
491
|
process.stdout.write(JSON.stringify({
|
|
644
492
|
archiveElementId: priorArchive.elemId,
|
|
645
493
|
conversationIdentity,
|
|
646
494
|
archiveSha256,
|
|
647
495
|
archiveSourceFile,
|
|
648
496
|
source,
|
|
497
|
+
scope,
|
|
498
|
+
accountId,
|
|
499
|
+
ownerElementId,
|
|
500
|
+
participantElementIds,
|
|
501
|
+
rebuild,
|
|
649
502
|
parsed: normaliserResult.counters.parsed,
|
|
650
503
|
mediaSkipped: normaliserResult.counters.mediaSkipped,
|
|
651
504
|
systemSkipped: normaliserResult.counters.systemSkipped,
|
|
652
505
|
delta: { kind: "empty-delta", deltaStart: allLines.length, deltaMessages: 0 },
|
|
653
|
-
sessions: 0,
|
|
654
|
-
chunks: priorChunkCountForSha,
|
|
655
|
-
nextEdgesCreated: 0,
|
|
656
|
-
participantsLinked: 0,
|
|
657
506
|
dateRange: { first: allLines[0].dateSent, last: allLines[allLines.length - 1].dateSent },
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
topicKeywords: [],
|
|
661
|
-
ms: totalMs,
|
|
507
|
+
senderHistogram: normaliserResult.counters.senderHistogram ?? [],
|
|
508
|
+
priorChunkCount: priorChunkCountForSha,
|
|
662
509
|
priorLastIngestedMessageAt: priorArchive.lastAt,
|
|
510
|
+
sessions: [],
|
|
511
|
+
ms: Date.now() - startedMs,
|
|
663
512
|
}) + "\n");
|
|
664
|
-
|
|
513
|
+
process.exit(0);
|
|
665
514
|
}
|
|
666
515
|
deltaStart = cursor.deltaStart;
|
|
667
516
|
deltaKind = "delta";
|
|
668
517
|
}
|
|
669
518
|
const deltaLines = allLines.slice(deltaStart);
|
|
670
|
-
log(
|
|
671
|
-
`delta kind=${deltaKind} cursor=${priorArchive && priorArchive.lastHash && !rebuild ? priorArchive.lastHash.slice(0, 12) : "(first-ingest)"} cursor-line=${deltaStart} delta-messages=${deltaLines.length}`,
|
|
672
|
-
);
|
|
673
519
|
|
|
674
|
-
//
|
|
520
|
+
// Sessionize delta at the fixed 8h gap.
|
|
675
521
|
const sessions = sessionize(deltaLines, SESSION_GAP_HOURS);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
// sub-scope A — compute the stable archive title once, before
|
|
681
|
-
// the per-session loop. The title is `<source> · <owner> ↔ <others> ·
|
|
682
|
-
// <YYYY-MM-DD>→<YYYY-MM-DD>`. Operator-confirmed participant names come
|
|
683
|
-
// from one Cypher hop; absent canonical names fall back to elementId
|
|
684
|
-
// prefixes so the migration's degraded-title path mirrors live behaviour.
|
|
522
|
+
|
|
523
|
+
// Stable archive title — computed once from the full archive's date range
|
|
524
|
+
// (not the delta), so a delta-append re-ingest produces the same title.
|
|
685
525
|
const archiveTitle = await computeArchiveTitle({
|
|
686
|
-
source,
|
|
687
|
-
accountId,
|
|
688
|
-
ownerElementId,
|
|
689
|
-
participantElementIds,
|
|
526
|
+
source, accountId, ownerElementId, participantElementIds,
|
|
690
527
|
firstMessageAt: allLines[0].dateSent,
|
|
691
528
|
lastMessageAt: allLines[allLines.length - 1].dateSent,
|
|
692
529
|
getSession,
|
|
693
530
|
});
|
|
694
|
-
log(`archive-title=${archiveTitle}`);
|
|
695
|
-
|
|
696
|
-
// 6g. Per-session classify + immediate memoryIngest (sub-scope E).
|
|
697
|
-
// Each session commits atomically: chunks + cursor advance happen together
|
|
698
|
-
// via memoryIngest. A kill mid-loop leaves a partial archive whose cursor
|
|
699
|
-
// is at session N-1's last message; the next invocation slices from there
|
|
700
|
-
// and re-classifies only session N onward. degrade-on-error
|
|
701
|
-
// is preserved: a Haiku error on session N still emits an unenriched chunk
|
|
702
|
-
// and advances the cursor, so resumption never re-attempts the same Haiku
|
|
703
|
-
// error.
|
|
704
|
-
const allKeywords = new Set();
|
|
705
|
-
let sessionsUnenriched = 0;
|
|
706
|
-
let totalChunksWritten = 0;
|
|
707
|
-
let totalNextEdges = 0;
|
|
708
|
-
let participantsLinked = 0;
|
|
709
|
-
let archiveElementIdLatest = priorArchive ? priorArchive.elemId : "";
|
|
710
|
-
|
|
711
|
-
for (let sIdx = 0; sIdx < sessions.length; sIdx++) {
|
|
712
|
-
const s = sessions[sIdx];
|
|
713
|
-
const isFirstSessionInRun = sIdx === 0;
|
|
714
|
-
const sessionStart = Date.now();
|
|
715
|
-
const text = toTurnText(s);
|
|
716
|
-
const result = await classifyDocument({
|
|
717
|
-
accountId,
|
|
718
|
-
mode: "chat",
|
|
719
|
-
anchorDescription: `Conversation transcript (${[ownerElementId, ...participantElementIds].length} participants, session ${s.index + 1} of ${sessions.length})`,
|
|
720
|
-
ontologyLabels: new Set([]),
|
|
721
|
-
naturalEdgeMap: "",
|
|
722
|
-
documentText: text,
|
|
723
|
-
});
|
|
724
531
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
lastMessageAt: s.lastMessageAt,
|
|
744
|
-
messageCount: s.messages.length,
|
|
745
|
-
},
|
|
746
|
-
anchorEdge: null,
|
|
747
|
-
}];
|
|
748
|
-
} else {
|
|
749
|
-
const chunkCount = result.output.sections.length;
|
|
750
|
-
log(
|
|
751
|
-
`classify-session sessionIndex=${s.index + 1}/${sessions.length} messages=${s.messages.length} chars=${text.length} chunks=${chunkCount} ms=${Date.now() - sessionStart}`,
|
|
752
|
-
);
|
|
753
|
-
if (chunkCount === 0 && s.messages.length > 0) {
|
|
754
|
-
cleanup();
|
|
755
|
-
fail("classify", {
|
|
756
|
-
reason: `session ${s.index} of ${s.messages.length} messages produced zero chunks (classifier-prompt regression)`,
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
sessionChunks = result.output.sections;
|
|
760
|
-
sessionKeywords = result.output.documentKeywords ?? [];
|
|
761
|
-
for (const kw of sessionKeywords) allKeywords.add(kw);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Per-session cursor: advance to THIS session's last message. Atomic with
|
|
765
|
-
// chunk writes inside memoryIngest's MERGE/CREATE Cypher transaction.
|
|
766
|
-
const lastSessionLine = s.messages[s.messages.length - 1];
|
|
767
|
-
const sessionLastHash = deriveMessageContentHash({
|
|
768
|
-
dateSent: lastSessionLine.dateSent,
|
|
769
|
-
body: lastSessionLine.body,
|
|
770
|
-
});
|
|
771
|
-
const sessionLastAt = lastSessionLine.dateSent;
|
|
772
|
-
|
|
773
|
-
let ingestResult;
|
|
774
|
-
const ingestStart = Date.now();
|
|
775
|
-
try {
|
|
776
|
-
ingestResult = await memoryIngest({
|
|
777
|
-
accountId,
|
|
778
|
-
conversationIdentity,
|
|
779
|
-
source,
|
|
780
|
-
// sub-scope A — `archiveTitle` is the stable label written
|
|
781
|
-
// to the conversation-document parent's `title` property (UI
|
|
782
|
-
// prefers it over `summary`). `documentSummary` is also the stable
|
|
783
|
-
// title rather than the per-session counter that previously locked
|
|
784
|
-
// the parent's first checkpoint into "Session 1/N: …".
|
|
785
|
-
archiveTitle,
|
|
786
|
-
documentSummary: archiveTitle,
|
|
787
|
-
anchorNodeId: ownerElementId,
|
|
788
|
-
anchorLabel: "AdminUser",
|
|
789
|
-
sections: sessionChunks,
|
|
790
|
-
scope,
|
|
791
|
-
sessionId,
|
|
792
|
-
documentKeywords: Array.from(allKeywords),
|
|
793
|
-
archiveSha256,
|
|
794
|
-
archiveSourceFile,
|
|
795
|
-
lastIngestedMessageHash: sessionLastHash,
|
|
796
|
-
lastIngestedMessageAt: sessionLastAt,
|
|
797
|
-
participantElementIds: [ownerElementId, ...participantElementIds],
|
|
798
|
-
// sub-scope B — cleanup-by-archiveSha256 runs ONLY under
|
|
799
|
-
// `--rebuild`, AND only on the first session of the run (subsequent
|
|
800
|
-
// sessions skip cleanup or they would delete chunks the earlier
|
|
801
|
-
// sessions in the same run just wrote).
|
|
802
|
-
cleanupPriorChunks: rebuild && isFirstSessionInRun,
|
|
803
|
-
});
|
|
804
|
-
} catch (err) {
|
|
805
|
-
cleanup();
|
|
806
|
-
fail("memory-ingest", {
|
|
807
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
808
|
-
sessionIndex: s.index + 1,
|
|
809
|
-
sessionsTotal: sessions.length,
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
archiveElementIdLatest = ingestResult.documentNodeId;
|
|
814
|
-
totalChunksWritten += ingestResult.sectionCount;
|
|
815
|
-
totalNextEdges += ingestResult.edgeBreakdown.NEXT ?? 0;
|
|
816
|
-
if (isFirstSessionInRun) {
|
|
817
|
-
participantsLinked = ingestResult.edgeBreakdown.PARTICIPANT_IN ?? 0;
|
|
818
|
-
// sub-scope E — surface destructive cleanup as a WARN line in
|
|
819
|
-
// the progress sink (not just memory-ingest's stderr). The agent's
|
|
820
|
-
// heartbeat grep-matches this exact prefix and elevates to operator
|
|
821
|
-
// chat. Cleanup only ever fires under `--rebuild` per sub-scope B,
|
|
822
|
-
// so seeing the line outside of an operator-issued `--rebuild` is a
|
|
823
|
-
// doctrine violation worth surfacing loudly.
|
|
824
|
-
const cleaned = ingestResult.cleanedPriorChunks ?? 0;
|
|
825
|
-
if (cleaned > 0) {
|
|
826
|
-
log(
|
|
827
|
-
`WARN cleanup-dropped chunks=${cleaned} archiveSha256=${archiveSha256.slice(0, 12)} — prior data deleted, expected only under --rebuild`,
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
log(
|
|
832
|
-
`session-committed sessionIndex=${s.index + 1}/${sessions.length} chunks-written=${ingestResult.sectionCount} cursor-at="${sessionLastAt}" ms=${Date.now() - ingestStart}`,
|
|
833
|
-
);
|
|
834
|
-
// sub-scope F — derived progress line. The agent's heartbeat
|
|
835
|
-
// surfaces only `progress` lines as chat tokens; raw `classify-session`
|
|
836
|
-
// and `session-committed` lines remain on disk for diagnosis. The render
|
|
837
|
-
// is computed in the bin (deterministic) instead of in SKILL.md prose
|
|
838
|
-
// (LLM drift) — sessionIndex/N → P% with running totals comes from one
|
|
839
|
-
// place, not from per-tick agent arithmetic.
|
|
840
|
-
const pct = Math.round((100 * (s.index + 1)) / sessions.length);
|
|
841
|
-
log(
|
|
842
|
-
`progress sessionIndex=${s.index + 1}/${sessions.length} pct=${pct} chunks-so-far=${totalChunksWritten} elapsed-ms=${Date.now() - startedMs}`,
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (sessionsUnenriched > 0) {
|
|
847
|
-
log(`classify-summary sessions=${sessions.length} unenriched=${sessionsUnenriched} enriched=${sessions.length - sessionsUnenriched}`);
|
|
848
|
-
}
|
|
532
|
+
// Build the per-session payload the specialist iterates in-turn. Each entry
|
|
533
|
+
// carries the turn text the specialist classifies + the cursor values the
|
|
534
|
+
// specialist passes through to `memory-ingest` for atomic checkpointing.
|
|
535
|
+
const sessionPayloads = sessions.map((s, sIdx) => {
|
|
536
|
+
const lastMessage = s.messages[s.messages.length - 1];
|
|
537
|
+
return {
|
|
538
|
+
sessionIndex: sIdx + 1,
|
|
539
|
+
totalSessions: sessions.length,
|
|
540
|
+
messageCount: s.messages.length,
|
|
541
|
+
firstMessageAt: s.firstMessageAt,
|
|
542
|
+
lastMessageAt: s.lastMessageAt,
|
|
543
|
+
lastMessageHash: deriveMessageContentHash({
|
|
544
|
+
dateSent: lastMessage.dateSent,
|
|
545
|
+
body: lastMessage.body,
|
|
546
|
+
}),
|
|
547
|
+
turnText: toTurnText(s),
|
|
548
|
+
};
|
|
549
|
+
});
|
|
849
550
|
|
|
850
551
|
cleanup();
|
|
851
|
-
const totalMs = Date.now() - startedMs;
|
|
852
|
-
log(
|
|
853
|
-
`done source=${source} conversationIdentity=${conversationIdentity.slice(0, 12)} archiveElementId=${archiveElementIdLatest} chunks-written=${totalChunksWritten} sessions=${sessions.length} total-ms=${totalMs}`,
|
|
854
|
-
);
|
|
855
552
|
|
|
856
553
|
process.stdout.write(JSON.stringify({
|
|
857
|
-
archiveElementId:
|
|
554
|
+
archiveElementId: priorArchive ? priorArchive.elemId : null,
|
|
858
555
|
conversationIdentity,
|
|
859
556
|
archiveSha256,
|
|
860
557
|
archiveSourceFile,
|
|
558
|
+
archiveTitle,
|
|
861
559
|
source,
|
|
560
|
+
scope,
|
|
561
|
+
accountId,
|
|
562
|
+
ownerElementId,
|
|
563
|
+
participantElementIds,
|
|
564
|
+
rebuild,
|
|
862
565
|
parsed: normaliserResult.counters.parsed,
|
|
863
566
|
mediaSkipped: normaliserResult.counters.mediaSkipped,
|
|
864
567
|
systemSkipped: normaliserResult.counters.systemSkipped,
|
|
865
568
|
delta: { kind: deltaKind, deltaStart, deltaMessages: deltaLines.length },
|
|
866
|
-
sessions: sessions.length,
|
|
867
|
-
chunks: totalChunksWritten,
|
|
868
|
-
nextEdgesCreated: totalNextEdges,
|
|
869
|
-
participantsLinked,
|
|
870
569
|
dateRange: { first: allLines[0].dateSent, last: allLines[allLines.length - 1].dateSent },
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
570
|
+
senderHistogram: normaliserResult.counters.senderHistogram ?? [],
|
|
571
|
+
priorChunkCount: priorChunkCountForSha,
|
|
572
|
+
priorLastIngestedMessageAt: priorArchive ? priorArchive.lastAt : null,
|
|
573
|
+
sessions: sessionPayloads,
|
|
574
|
+
ms: Date.now() - startedMs,
|
|
875
575
|
}) + "\n");
|
|
876
|
-
|
|
576
|
+
process.exit(0);
|
|
877
577
|
}
|
|
878
578
|
|
|
879
579
|
main().catch((err) => {
|