@rubytech/create-maxy-code 0.1.321 → 0.1.323
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/dist/index.js +134 -1
- package/package.json +1 -1
- package/payload/platform/lib/embed-client/dist/index.d.ts +2 -0
- package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/embed-client/dist/index.js +50 -10
- package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
- package/payload/platform/lib/embed-client/src/index.ts +57 -9
- package/payload/platform/plugins/admin/skills/insight/SKILL.md +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +39 -5
- package/payload/platform/plugins/docs/references/admin-ui.md +12 -0
- package/payload/platform/plugins/docs/references/deployment.md +20 -0
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +4 -4
- package/payload/platform/plugins/memory/PLUGIN.md +0 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +0 -45
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +6 -6
- package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js +41 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +34 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/notion-import/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/notion-import/PLUGIN.md +6 -6
- package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +1 -1
- package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +1 -1
- package/payload/platform/plugins/obsidian-import/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/obsidian-import/PLUGIN.md +5 -5
- package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +1 -1
- package/payload/platform/plugins/substack-import/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/substack-import/PLUGIN.md +8 -8
- package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +2 -2
- package/payload/platform/plugins/substack-import/skills/substack-import/references/posts.md +1 -1
- package/payload/platform/plugins/x-import/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/x-import/PLUGIN.md +5 -5
- package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
- package/payload/platform/scripts/check-no-esm-require.mjs +3 -0
- package/payload/platform/scripts/identity-forbidden-token-check.mjs +0 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +75 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +3 -3
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +8 -4
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +16 -3
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +29 -12
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -4
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.js +6 -6
- package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +0 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/specialists/agents/coding-assistant.md +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +2 -6
- package/payload/platform/templates/specialists/agents/librarian.md +20 -7
- package/payload/server/power-health.cjs +129 -0
- package/payload/server/public/assets/{AdminLoginScreens-DKwWudsP.js → AdminLoginScreens-BBtA0mi8.js} +1 -1
- package/payload/server/public/assets/AdminShell-CZW9x0-A.js +1 -0
- package/payload/server/public/assets/{Checkbox-V5ubVZG1.js → Checkbox-CezYRnKL.js} +1 -1
- package/payload/server/public/assets/{OperatorConversations-DOGCHZhv.css → OperatorConversations-Bhyf22Ct.css} +1 -1
- package/payload/server/public/assets/OperatorConversations-D6BawEz3.js +9 -0
- package/payload/server/public/assets/{admin-Cfqj5N4P.js → admin-BVpCUS4j.js} +1 -1
- package/payload/server/public/assets/{browser-CtMwlpyW.js → browser-ByLzuh0X.js} +1 -1
- package/payload/server/public/assets/chat-DTvzNb92.js +1 -0
- package/payload/server/public/assets/data-Da1oTowD.js +1 -0
- package/payload/server/public/assets/{graph-Bez5QF37.js → graph-iYrodF_0.js} +3 -3
- package/payload/server/public/assets/{graph-labels-BkbaBPIP.js → graph-labels-DM9u5VTf.js} +1 -1
- package/payload/server/public/assets/operator-BPplyxq-.js +1 -0
- package/payload/server/public/assets/page-oogapuCz.js +30 -0
- package/payload/server/public/assets/{public-wy93nfmL.js → public-wBFC1mQO.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server-init.cjs +40 -0
- package/payload/server/server.js +212 -12
- package/payload/platform/plugins/memory/mcp/scripts/backfill-typed-edges.ts +0 -72
- package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +0 -45
- package/payload/server/public/assets/AdminShell-BsPsXvJv.js +0 -1
- package/payload/server/public/assets/OperatorConversations-DPRRlS_z.js +0 -9
- package/payload/server/public/assets/chat-Bm11OGsU.js +0 -1
- package/payload/server/public/assets/data-7_dN5vN5.js +0 -1
- package/payload/server/public/assets/operator-F5KHg5WG.js +0 -1
- package/payload/server/public/assets/page-CExyEcvh.js +0 -30
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Chat — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/chat-
|
|
8
|
+
<script type="module" crossorigin src="/assets/chat-DTvzNb92.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-D6BawEz3.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CZW9x0-A.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-CJrGd46U.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-Bf_JiD2A.js">
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/page-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/page-oogapuCz.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-Bhyf22Ct.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Data — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/data-
|
|
8
|
+
<script type="module" crossorigin src="/assets/data-Da1oTowD.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/graph-labels-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-D6BawEz3.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/graph-labels-DM9u5VTf.js">
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-Bhyf22Ct.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Graph — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/graph-
|
|
8
|
+
<script type="module" crossorigin src="/assets/graph-iYrodF_0.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/graph-labels-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/Checkbox-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-D6BawEz3.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/graph-labels-DM9u5VTf.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CZW9x0-A.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/Checkbox-CezYRnKL.js">
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-Bhyf22Ct.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/admin-
|
|
8
|
+
<script type="module" crossorigin src="/assets/admin-BVpCUS4j.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-D6BawEz3.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CZW9x0-A.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-CJrGd46U.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/Checkbox-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/Checkbox-CezYRnKL.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-BBtA0mi8.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-Bhyf22Ct.css">
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/assets/AdminLoginScreens-CWMpccrR.css">
|
|
17
17
|
<link rel="stylesheet" href="/brand-defaults.css">
|
|
18
18
|
</head>
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Operator — Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/operator-
|
|
8
|
+
<script type="module" crossorigin src="/assets/operator-BPplyxq-.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-D6BawEz3.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CZW9x0-A.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-CJrGd46U.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/Checkbox-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/Checkbox-CezYRnKL.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-BBtA0mi8.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-Bf_JiD2A.js">
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/page-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/page-oogapuCz.js">
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-Bhyf22Ct.css">
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/AdminLoginScreens-CWMpccrR.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Maxy</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico">
|
|
8
|
-
<script type="module" crossorigin src="/assets/public-
|
|
8
|
+
<script type="module" crossorigin src="/assets/public-wBFC1mQO.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/chunk-Pqm5yXtL.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/AdminShell-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/OperatorConversations-D6BawEz3.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/AdminShell-CZW9x0-A.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/admin-types-CJrGd46U.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-Bf_JiD2A.js">
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/page-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/page-oogapuCz.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/OperatorConversations-Bhyf22Ct.css">
|
|
16
16
|
<link rel="stylesheet" href="/brand-defaults.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body class="public-surface">
|
|
@@ -101,6 +101,46 @@ function getLanIp() {
|
|
|
101
101
|
return fallback
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Power health — surface Pi brownout/throttle + DHCP churn to server.log
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
//
|
|
108
|
+
// A marginal PSU browns out under ollama embed current and the kernel logs
|
|
109
|
+
// `Undervoltage detected!` to the journal, but the admin server never saw it.
|
|
110
|
+
// Emit one greppable `[power-health]` line at boot and every 15 min reading the
|
|
111
|
+
// real vcgencmd sticky word + unclean-reboot count + EXT5V input voltage. The
|
|
112
|
+
// boot line also records the resolved LAN IP so DHCP churn across reboots is
|
|
113
|
+
// visible. Pi-only: off-Pi (no vcgencmd) it logs one skipped line and starts no
|
|
114
|
+
// interval.
|
|
115
|
+
|
|
116
|
+
const powerHealth = require('./power-health.cjs')
|
|
117
|
+
|
|
118
|
+
function phRun(cmd, args) {
|
|
119
|
+
try {
|
|
120
|
+
const r = spawnSync(cmd, args, { stdio: 'pipe', timeout: 5000, encoding: 'utf8' })
|
|
121
|
+
if (r.status !== 0 || typeof r.stdout !== 'string') return null
|
|
122
|
+
return r.stdout
|
|
123
|
+
} catch { return null }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Compute the status line once; the boot lines reuse it and the interval is
|
|
128
|
+
// armed off the same result, so vcgencmd/last are not probed twice at boot.
|
|
129
|
+
const bootStatus = powerHealth.buildStatusLine({ runner: phRun, now: new Date() })
|
|
130
|
+
for (const line of powerHealth.buildBootLines({ runner: phRun, now: new Date(), lanIp: getLanIp(), status: bootStatus })) {
|
|
131
|
+
console.error(line)
|
|
132
|
+
}
|
|
133
|
+
if (bootStatus !== null) {
|
|
134
|
+
const phInterval = setInterval(() => {
|
|
135
|
+
const line = powerHealth.buildStatusLine({ runner: phRun, now: new Date() })
|
|
136
|
+
if (line) console.error(line)
|
|
137
|
+
}, 15 * 60 * 1000)
|
|
138
|
+
phInterval.unref()
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(`[power-health] init-failed err=${err && err.message ? err.message : String(err)}`)
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
// Paths that must NOT be redirected (API calls, static assets, VNC)
|
|
105
145
|
const SKIP_REDIRECT_RE = /^\/(api|assets|brand|novnc|vnc-viewer|vnc-popout|favicon)/
|
|
106
146
|
|
package/payload/server/server.js
CHANGED
|
@@ -8637,7 +8637,7 @@ ${note}` : note;
|
|
|
8637
8637
|
}
|
|
8638
8638
|
const known = resolvedProjectDir !== null || sessionSidecarExists(target);
|
|
8639
8639
|
const indicators2 = await fetchComposerIndicators(target);
|
|
8640
|
-
return c.json({ sessionId: target, projectDir: resolvedProjectDir, transcriptSessionId, channelBinding: conflictingBinding(channel), known, sizeBytes: jsonlSizeBytes(projectDir2, target), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${target}`) ?? null, ...indicators2, ...readLevers() });
|
|
8640
|
+
return c.json({ sessionId: target, projectDir: resolvedProjectDir, transcriptSessionId, channelBinding: conflictingBinding(channel), known, sizeBytes: jsonlSizeBytes(projectDir2, target), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${target}`) ?? null, deliveryFailure: deps.deliveryFailureFor?.(`session:${target}`) ?? null, ...indicators2, ...readLevers() });
|
|
8641
8641
|
}
|
|
8642
8642
|
let account;
|
|
8643
8643
|
try {
|
|
@@ -8670,7 +8670,7 @@ ${note}` : note;
|
|
|
8670
8670
|
}
|
|
8671
8671
|
}
|
|
8672
8672
|
const indicators = await fetchComposerIndicators(sessionId);
|
|
8673
|
-
return c.json({ sessionId, projectDir, sizeBytes: jsonlSizeBytes(projectDir, sessionId), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${sessionId}`) ?? null, ...indicators, ...readLevers(account) });
|
|
8673
|
+
return c.json({ sessionId, projectDir, sizeBytes: jsonlSizeBytes(projectDir, sessionId), pendingPermissionPrompt: deps.pendingPromptFor?.(`session:${sessionId}`) ?? null, deliveryFailure: deps.deliveryFailureFor?.(`session:${sessionId}`) ?? null, ...indicators, ...readLevers(account) });
|
|
8674
8674
|
});
|
|
8675
8675
|
app53.post("/permission-verdict", requireAdminSession, async (c) => {
|
|
8676
8676
|
const body = await c.req.json().catch(() => null);
|
|
@@ -8914,6 +8914,33 @@ async function managerRcSpawn(opts) {
|
|
|
8914
8914
|
const out = await r.json();
|
|
8915
8915
|
return { sessionId: out.sessionId ?? opts.sessionId, pid: out.spawnedPid ?? null };
|
|
8916
8916
|
}
|
|
8917
|
+
async function managerRcRecover(opts) {
|
|
8918
|
+
const auth = await ensureAuth();
|
|
8919
|
+
if (auth.status === "dead" || auth.status === "missing") {
|
|
8920
|
+
console.log(`[channel-pty-bridge] rc-recover-refused reason=auth-${auth.status} ${opts.logContext}`);
|
|
8921
|
+
return { error: "claude-auth-dead", status: 503 };
|
|
8922
|
+
}
|
|
8923
|
+
const res = await fetch(`${managerBase2()}/rc-spawn`, {
|
|
8924
|
+
method: "POST",
|
|
8925
|
+
headers: { "content-type": "application/json" },
|
|
8926
|
+
body: JSON.stringify({
|
|
8927
|
+
sessionId: opts.sessionId,
|
|
8928
|
+
recover: true,
|
|
8929
|
+
role: "admin",
|
|
8930
|
+
channel: "webchat",
|
|
8931
|
+
senderId: opts.webchatChannel.key,
|
|
8932
|
+
adminUserId: opts.adminUserId,
|
|
8933
|
+
webchatChannel: opts.webchatChannel
|
|
8934
|
+
})
|
|
8935
|
+
}).catch((err) => ({ __throw: err instanceof Error ? err.message : String(err) }));
|
|
8936
|
+
if ("__throw" in res) {
|
|
8937
|
+
return { error: res.__throw, status: 0 };
|
|
8938
|
+
}
|
|
8939
|
+
const r = res;
|
|
8940
|
+
if (!r.ok) return { error: `manager-status-${r.status}`, status: r.status };
|
|
8941
|
+
const out = await r.json();
|
|
8942
|
+
return { live: out.live === true, classification: out.classification, recovered: out.recovered === true };
|
|
8943
|
+
}
|
|
8917
8944
|
async function managerWriteInput(sessionId, text) {
|
|
8918
8945
|
const res = await fetch(`${managerBase2()}/${sessionId}/input`, {
|
|
8919
8946
|
method: "POST",
|
|
@@ -19439,12 +19466,12 @@ var InboundHub2 = class {
|
|
|
19439
19466
|
* that never reached the agent:
|
|
19440
19467
|
* - a still-queued message older than queuedThresholdMs WHILE a subscriber
|
|
19441
19468
|
* is attached -> inbound-undelivered (the readiness handoff broke);
|
|
19442
|
-
* - a still-queued message older than queuedThresholdMs with NO subscriber
|
|
19443
|
-
* attached -> spawn-undelivered (the channel server never attached: spawn
|
|
19444
|
-
* failed, or the session ended and never resumed);
|
|
19445
19469
|
* - a drained message unacked past ackThresholdMs -> drain-undelivered (the
|
|
19446
19470
|
* channel server never confirmed receipt — the silent cold-start drop).
|
|
19447
|
-
*
|
|
19471
|
+
* A still-queued message with NO subscriber is NOT handled here: the gateway
|
|
19472
|
+
* owns that policy via staleQueued/dropQueued (Task-962) — a live session is
|
|
19473
|
+
* given a recovery window + respawn before any drop; a not-live one keeps the
|
|
19474
|
+
* cold-start drop. Each message is emitted at most once (removed when emitted). */
|
|
19448
19475
|
sweep(now, t) {
|
|
19449
19476
|
const events = [];
|
|
19450
19477
|
for (const [key, s] of this.keys) {
|
|
@@ -19453,10 +19480,6 @@ var InboundHub2 = class {
|
|
|
19453
19480
|
events.push({ kind: "inbound-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
|
|
19454
19481
|
return false;
|
|
19455
19482
|
}
|
|
19456
|
-
if (e.state === "queued" && !s.subscriber && now - e.enqueuedAt > t.queuedThresholdMs) {
|
|
19457
|
-
events.push({ kind: "spawn-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
|
|
19458
|
-
return false;
|
|
19459
|
-
}
|
|
19460
19483
|
if (e.state === "drained" && e.drainedAt != null && now - e.drainedAt > t.ackThresholdMs) {
|
|
19461
19484
|
events.push({ kind: "drain-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.drainedAt });
|
|
19462
19485
|
return false;
|
|
@@ -19466,6 +19489,32 @@ var InboundHub2 = class {
|
|
|
19466
19489
|
}
|
|
19467
19490
|
return events;
|
|
19468
19491
|
}
|
|
19492
|
+
/** Task-962 — queued messages with NO subscriber attached, older than
|
|
19493
|
+
* thresholdMs. Read-only: it does NOT remove anything. The gateway drives
|
|
19494
|
+
* recovery/drop for these (a live session gets a redelivery window + a wedged
|
|
19495
|
+
* respawn; a not-live one keeps the immediate cold-start drop). */
|
|
19496
|
+
staleQueued(now, thresholdMs) {
|
|
19497
|
+
const out = [];
|
|
19498
|
+
for (const [key, s] of this.keys) {
|
|
19499
|
+
if (s.subscriber) continue;
|
|
19500
|
+
for (const e of s.inFlight) {
|
|
19501
|
+
if (e.state === "queued" && now - e.enqueuedAt > thresholdMs) {
|
|
19502
|
+
out.push({ key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
|
|
19503
|
+
}
|
|
19504
|
+
}
|
|
19505
|
+
}
|
|
19506
|
+
return out;
|
|
19507
|
+
}
|
|
19508
|
+
/** Task-962 — terminal-drop one still-queued in-flight message by id. Returns
|
|
19509
|
+
* whether a matching queued entry was found and removed. */
|
|
19510
|
+
dropQueued(key, messageId) {
|
|
19511
|
+
const s = this.keys.get(key);
|
|
19512
|
+
if (!s) return false;
|
|
19513
|
+
const i = s.inFlight.findIndex((e) => e.payload.messageId === messageId && e.state === "queued");
|
|
19514
|
+
if (i < 0) return false;
|
|
19515
|
+
s.inFlight.splice(i, 1);
|
|
19516
|
+
return true;
|
|
19517
|
+
}
|
|
19469
19518
|
/** Whether a key currently has a live channel server attached. The gateway
|
|
19470
19519
|
* uses this to decide whether an inbound needs a cold-start spawn/resume. */
|
|
19471
19520
|
hasSubscriber(key) {
|
|
@@ -19477,6 +19526,24 @@ var InboundHub2 = class {
|
|
|
19477
19526
|
const s = this.keys.get(key);
|
|
19478
19527
|
return s?.subscriber != null && s.ready;
|
|
19479
19528
|
}
|
|
19529
|
+
/** Task-963 — whether a key has any in-flight message (queued or drained-
|
|
19530
|
+
* awaiting-ack). The gateway GCs per-key recover descriptors for keys with
|
|
19531
|
+
* none, so the descriptor map stays bounded to the active-key set. */
|
|
19532
|
+
hasInFlight(key) {
|
|
19533
|
+
const s = this.keys.get(key);
|
|
19534
|
+
return s != null && s.inFlight.length > 0;
|
|
19535
|
+
}
|
|
19536
|
+
/** Task-963 — every in-flight message as `${key}::${messageId}`. The gateway
|
|
19537
|
+
* intersects its recoveryAttempted set against this to drop attempt keys whose
|
|
19538
|
+
* message has left the queue by ANY terminal path — ack, drop, or the drain-
|
|
19539
|
+
* without-ack sweep — not only ack. */
|
|
19540
|
+
inFlightIds() {
|
|
19541
|
+
const out = /* @__PURE__ */ new Set();
|
|
19542
|
+
for (const [key, s] of this.keys) {
|
|
19543
|
+
for (const e of s.inFlight) out.add(`${key}::${e.payload.messageId}`);
|
|
19544
|
+
}
|
|
19545
|
+
return out;
|
|
19546
|
+
}
|
|
19480
19547
|
};
|
|
19481
19548
|
|
|
19482
19549
|
// app/lib/webchat/gateway/routes.ts
|
|
@@ -19550,6 +19617,8 @@ function createWebchatChannelRoutes(deps) {
|
|
|
19550
19617
|
var QUEUED_THRESHOLD_MS2 = 3e4;
|
|
19551
19618
|
var ACK_THRESHOLD_MS2 = 3e4;
|
|
19552
19619
|
var SWEEP_INTERVAL_MS2 = 15e3;
|
|
19620
|
+
var RECOVER_TRIGGER_MS = Number(process.env.WEBCHAT_RECOVER_TRIGGER_MS ?? String(3e4));
|
|
19621
|
+
var DELIVER_FINAL_MS = Number(process.env.WEBCHAT_DELIVER_FINAL_MS ?? String(9e4));
|
|
19553
19622
|
function channelIdleMs() {
|
|
19554
19623
|
return Number(process.env.CHANNEL_PTY_IDLE_MS ?? String(5 * 6e4));
|
|
19555
19624
|
}
|
|
@@ -19581,6 +19650,18 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
19581
19650
|
/** Task 756 — live public sessions, for the idle reaper + session-end review.
|
|
19582
19651
|
* Admin sessions are never recorded here (they never reap). */
|
|
19583
19652
|
publicSessions = /* @__PURE__ */ new Map();
|
|
19653
|
+
/** Task-962 — per-key recover context, set on each admin inbound so the
|
|
19654
|
+
* recovery pass can ask the manager to respawn a wedged live child. */
|
|
19655
|
+
recoverable = /* @__PURE__ */ new Map();
|
|
19656
|
+
/** Task-962 — per-session delivery-failure markers (set when redelivery is
|
|
19657
|
+
* exhausted; cleared on the next successful drain/ack). Surfaced on the
|
|
19658
|
+
* /session pointer the operator /chat polls. */
|
|
19659
|
+
deliveryFailures = /* @__PURE__ */ new Map();
|
|
19660
|
+
/** Task-962 — `${key}::${messageId}` for messages a recovery call has already
|
|
19661
|
+
* been issued for, so recovery is attempted at most once per message. */
|
|
19662
|
+
recoveryAttempted = /* @__PURE__ */ new Set();
|
|
19663
|
+
/** Task-962 — guards against overlapping async recovery passes. */
|
|
19664
|
+
recovering = false;
|
|
19584
19665
|
seq = 0;
|
|
19585
19666
|
/** Register a one-shot awaiter for the next reply on `key`. Resolves with the
|
|
19586
19667
|
* reply text, or `{ timeout: true }` if no reply arrives within `timeoutMs`.
|
|
@@ -19702,6 +19783,7 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
19702
19783
|
if (count > 0) {
|
|
19703
19784
|
console.error(`[webchat:inbound] op=drained key=${key} count=${count}`);
|
|
19704
19785
|
this.lastDeliveredAt.set(key, now);
|
|
19786
|
+
this.deliveryFailures.delete(key);
|
|
19705
19787
|
}
|
|
19706
19788
|
},
|
|
19707
19789
|
onReceived: (key, messageId) => {
|
|
@@ -19709,6 +19791,10 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
19709
19791
|
console.error(
|
|
19710
19792
|
`[webchat:inbound] op=channel-received key=${key} messageId=${messageId}${found ? "" : " note=unknown"}`
|
|
19711
19793
|
);
|
|
19794
|
+
if (found) {
|
|
19795
|
+
this.recoveryAttempted.delete(`${key}::${messageId}`);
|
|
19796
|
+
this.deliveryFailures.delete(key);
|
|
19797
|
+
}
|
|
19712
19798
|
},
|
|
19713
19799
|
onReply: (key, text) => this.handleReply(key, text),
|
|
19714
19800
|
awaitPermissionVerdict: (p) => this.awaitPermissionVerdict(p, permissionVerdictTtlMs())
|
|
@@ -19727,9 +19813,94 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
19727
19813
|
}
|
|
19728
19814
|
return events;
|
|
19729
19815
|
}
|
|
19730
|
-
/** Start the periodic reconciliation sweep in the long-lived UI process.
|
|
19816
|
+
/** Start the periodic reconciliation sweep in the long-lived UI process. Each
|
|
19817
|
+
* tick runs the sync sweep (drain-/inbound-undelivered) AND the async
|
|
19818
|
+
* recovery pass for queued-no-subscriber messages (Task-962). */
|
|
19731
19819
|
startSweeper(intervalMs = SWEEP_INTERVAL_MS2) {
|
|
19732
|
-
return setInterval(() =>
|
|
19820
|
+
return setInterval(() => {
|
|
19821
|
+
const now = Date.now();
|
|
19822
|
+
this.reconcile(now);
|
|
19823
|
+
void this.recoverStalled(now);
|
|
19824
|
+
}, intervalMs);
|
|
19825
|
+
}
|
|
19826
|
+
/** Task-962 — the per-session delivery-failure marker for the /session pointer.
|
|
19827
|
+
* Non-null only when redelivery was exhausted for `key`; cleared on the next
|
|
19828
|
+
* successful drain/ack. */
|
|
19829
|
+
deliveryFailureFor(key) {
|
|
19830
|
+
return this.deliveryFailures.get(key) ?? null;
|
|
19831
|
+
}
|
|
19832
|
+
/** Task-962 — clear a key's delivery-failure marker (a later message drained,
|
|
19833
|
+
* or the operator dismissed/retried). */
|
|
19834
|
+
clearDeliveryFailure(key) {
|
|
19835
|
+
this.deliveryFailures.delete(key);
|
|
19836
|
+
}
|
|
19837
|
+
/** Task-962 — recovery-aware pass over queued-no-subscriber messages.
|
|
19838
|
+
* - age in [RECOVER_TRIGGER_MS, DELIVER_FINAL_MS): ask the manager to recover
|
|
19839
|
+
* the live session ONCE. busy → leave queued for the channel server's
|
|
19840
|
+
* auto-reconnect to drain; wedged → the manager respawns and the resumed
|
|
19841
|
+
* child re-binds the channel + drains. A not-live target (or no recover
|
|
19842
|
+
* dep/descriptor) is dropped now — the cold-start class, out of scope.
|
|
19843
|
+
* - age ≥ DELIVER_FINAL_MS: terminal-drop and record a delivery-failure
|
|
19844
|
+
* marker (redelivery exhausted).
|
|
19845
|
+
* Single-flight; safe to call every sweep tick. */
|
|
19846
|
+
async recoverStalled(now) {
|
|
19847
|
+
if (this.recovering) return;
|
|
19848
|
+
this.recovering = true;
|
|
19849
|
+
try {
|
|
19850
|
+
for (const c of this.hub.staleQueued(now, RECOVER_TRIGGER_MS)) {
|
|
19851
|
+
const attemptKey = `${c.key}::${c.messageId}`;
|
|
19852
|
+
if (c.ageMs >= DELIVER_FINAL_MS) {
|
|
19853
|
+
this.hub.dropQueued(c.key, c.messageId);
|
|
19854
|
+
this.recoveryAttempted.delete(attemptKey);
|
|
19855
|
+
this.deliveryFailures.set(c.key, { messageId: c.messageId, at: now });
|
|
19856
|
+
console.error(
|
|
19857
|
+
`[webchat:inbound] op=delivery-failed key=${keyDisplay(c.key)} messageId=${c.messageId} ageMs=${c.ageMs}`
|
|
19858
|
+
);
|
|
19859
|
+
continue;
|
|
19860
|
+
}
|
|
19861
|
+
if (this.recoveryAttempted.has(attemptKey)) continue;
|
|
19862
|
+
const d = this.recoverable.get(c.key);
|
|
19863
|
+
if (!d || !this.deps.recoverLiveSession) {
|
|
19864
|
+
this.hub.dropQueued(c.key, c.messageId);
|
|
19865
|
+
console.error(
|
|
19866
|
+
`[webchat:inbound] op=spawn-undelivered key=${keyDisplay(c.key)} messageId=${c.messageId} ageMs=${c.ageMs} reason=no-descriptor`
|
|
19867
|
+
);
|
|
19868
|
+
continue;
|
|
19869
|
+
}
|
|
19870
|
+
this.recoveryAttempted.add(attemptKey);
|
|
19871
|
+
let r;
|
|
19872
|
+
try {
|
|
19873
|
+
r = await this.deps.recoverLiveSession(d);
|
|
19874
|
+
} catch (err) {
|
|
19875
|
+
console.error(
|
|
19876
|
+
`[webchat:inbound] op=recover-error key=${keyDisplay(c.key)} messageId=${c.messageId} err=${err instanceof Error ? err.message : String(err)}`
|
|
19877
|
+
);
|
|
19878
|
+
continue;
|
|
19879
|
+
}
|
|
19880
|
+
if (!r.live) {
|
|
19881
|
+
this.hub.dropQueued(c.key, c.messageId);
|
|
19882
|
+
this.recoveryAttempted.delete(attemptKey);
|
|
19883
|
+
console.error(
|
|
19884
|
+
`[webchat:inbound] op=spawn-undelivered key=${keyDisplay(c.key)} messageId=${c.messageId} ageMs=${c.ageMs} reason=not-live`
|
|
19885
|
+
);
|
|
19886
|
+
} else {
|
|
19887
|
+
console.error(
|
|
19888
|
+
`[webchat:inbound] op=recover key=${keyDisplay(c.key)} messageId=${c.messageId} classification=${r.classification ?? "unknown"} respawned=${r.recovered === true}`
|
|
19889
|
+
);
|
|
19890
|
+
}
|
|
19891
|
+
}
|
|
19892
|
+
for (const key of this.recoverable.keys()) {
|
|
19893
|
+
if (!this.hub.hasInFlight(key)) this.recoverable.delete(key);
|
|
19894
|
+
}
|
|
19895
|
+
if (this.recoveryAttempted.size > 0) {
|
|
19896
|
+
const liveIds = this.hub.inFlightIds();
|
|
19897
|
+
for (const attemptKey of this.recoveryAttempted) {
|
|
19898
|
+
if (!liveIds.has(attemptKey)) this.recoveryAttempted.delete(attemptKey);
|
|
19899
|
+
}
|
|
19900
|
+
}
|
|
19901
|
+
} finally {
|
|
19902
|
+
this.recovering = false;
|
|
19903
|
+
}
|
|
19733
19904
|
}
|
|
19734
19905
|
/** Handle one inbound webchat message (browser POST → here). */
|
|
19735
19906
|
async handleInbound(input) {
|
|
@@ -19744,6 +19915,15 @@ var WebchatGateway = class _WebchatGateway {
|
|
|
19744
19915
|
console.error(
|
|
19745
19916
|
`[webchat:inbound] op=received key=${keyDisplay(input.key)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
|
|
19746
19917
|
);
|
|
19918
|
+
if (input.role === "admin") {
|
|
19919
|
+
this.recoverable.set(input.key, {
|
|
19920
|
+
accountId: input.accountId,
|
|
19921
|
+
key: input.key,
|
|
19922
|
+
role: input.role,
|
|
19923
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
19924
|
+
...input.adminUserId !== void 0 ? { adminUserId: input.adminUserId } : {}
|
|
19925
|
+
});
|
|
19926
|
+
}
|
|
19747
19927
|
const existing = this.publicSessions.get(input.key);
|
|
19748
19928
|
if (existing) existing.lastActivityAt = Date.now();
|
|
19749
19929
|
if (!hadSubscriber && !this.spawning.has(input.key)) {
|
|
@@ -20313,6 +20493,25 @@ var webchatGateway = new WebchatGateway({
|
|
|
20313
20493
|
}
|
|
20314
20494
|
return { sessionId: result.sessionId };
|
|
20315
20495
|
},
|
|
20496
|
+
// Task-962 — the recovery pass asks the manager to respawn a wedged already-
|
|
20497
|
+
// live admin session (rc-spawn recover:true). Resolve the session id the same
|
|
20498
|
+
// way ensureChannelSession does. Public sessions recover via the idle reaper,
|
|
20499
|
+
// so a non-admin descriptor is reported not-live (no respawn).
|
|
20500
|
+
recoverLiveSession: async ({ accountId, key, role, sessionId, adminUserId }) => {
|
|
20501
|
+
if (role !== "admin") return { live: false };
|
|
20502
|
+
const id = sessionId ?? adminSessionIdFor(accountId, key, adminUserId);
|
|
20503
|
+
const r = await managerRcRecover({
|
|
20504
|
+
sessionId: id,
|
|
20505
|
+
webchatChannel: { key, gatewayUrl: webchatGatewayUrl(), serverPath: webchatServerPath() },
|
|
20506
|
+
...adminUserId !== void 0 ? { adminUserId } : {},
|
|
20507
|
+
logContext: `channel=webchat key=${key} recover`
|
|
20508
|
+
});
|
|
20509
|
+
if ("error" in r) {
|
|
20510
|
+
console.error(`[webchat:inbound] op=recover-failed key=${key} error=${r.error} status=${r.status}`);
|
|
20511
|
+
return { live: false };
|
|
20512
|
+
}
|
|
20513
|
+
return r;
|
|
20514
|
+
},
|
|
20316
20515
|
// Task 756 — the public idle reaper deletes the manager PTY and dispatches the
|
|
20317
20516
|
// session-end review (gated visitors), since the native path has no bridge
|
|
20318
20517
|
// entry to drive them. Admin sessions are never recorded, so never reaped.
|
|
@@ -20641,6 +20840,7 @@ app52.route("/api/webchat", createWebchatRoutes({
|
|
|
20641
20840
|
handleInbound: (input) => webchatGateway.handleInbound(input),
|
|
20642
20841
|
// Task 940 — the permission relay's pointer read + verdict write.
|
|
20643
20842
|
pendingPromptFor: (key) => webchatGateway.pendingPromptFor(key),
|
|
20843
|
+
deliveryFailureFor: (key) => webchatGateway.deliveryFailureFor(key),
|
|
20644
20844
|
resolvePermissionVerdict: (key, requestId, behavior) => webchatGateway.resolvePermissionVerdict(key, requestId, behavior)
|
|
20645
20845
|
}));
|
|
20646
20846
|
app52.route("/api/webchat/greeting", webchat_greeting_default);
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One-shot backfill of the typed-edge pass over every existing prose-bearing
|
|
3
|
-
* node for the current account. Idempotent — MERGE handles re-runs cleanly,
|
|
4
|
-
* so a second invocation accepts zero edges.
|
|
5
|
-
*
|
|
6
|
-
* Usage (admin-agent invokable via Bash):
|
|
7
|
-
* cd platform/plugins/memory/mcp
|
|
8
|
-
* node --loader ts-node/esm scripts/backfill-typed-edges.ts
|
|
9
|
-
*
|
|
10
|
-
* Reads ACCOUNT_ID from env (same convention as the MCP server). Loops
|
|
11
|
-
* `runTypedEdgePass` with `sinceIso: null` in 500-node pages until the
|
|
12
|
-
* page returns 0 nodes (exhausted). Logs per-batch progress with
|
|
13
|
-
* `[typed-edge-pass:backfill]`.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { getSession, closeDriver } from "../src/lib/neo4j.js";
|
|
17
|
-
import { runTypedEdgePass } from "../src/lib/typed-edge-pass.js";
|
|
18
|
-
|
|
19
|
-
const PAGE_SIZE = 500;
|
|
20
|
-
|
|
21
|
-
async function main(): Promise<void> {
|
|
22
|
-
const accountId = process.env.ACCOUNT_ID;
|
|
23
|
-
if (!accountId) {
|
|
24
|
-
console.error("ACCOUNT_ID env var is required");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let batch = 0;
|
|
29
|
-
let totalNodes = 0;
|
|
30
|
-
let totalAccepted = 0;
|
|
31
|
-
let totalRejected = 0;
|
|
32
|
-
let totalFailed = 0;
|
|
33
|
-
|
|
34
|
-
while (true) {
|
|
35
|
-
const session = getSession();
|
|
36
|
-
try {
|
|
37
|
-
const result = await runTypedEdgePass({
|
|
38
|
-
session,
|
|
39
|
-
accountId,
|
|
40
|
-
sinceIso: null,
|
|
41
|
-
sessionId: null,
|
|
42
|
-
maxNodes: PAGE_SIZE,
|
|
43
|
-
});
|
|
44
|
-
batch += 1;
|
|
45
|
-
totalNodes += result.nodes;
|
|
46
|
-
totalAccepted += result.accepted;
|
|
47
|
-
totalRejected += result.rejected;
|
|
48
|
-
totalFailed += result.failed;
|
|
49
|
-
console.log(
|
|
50
|
-
`[typed-edge-pass:backfill] batch=${batch} done=${totalNodes} accepted=${result.accepted} rejected=${result.rejected} failed=${result.failed} ms=${result.ms}`,
|
|
51
|
-
);
|
|
52
|
-
if (result.nodes < PAGE_SIZE || result.accepted === 0) {
|
|
53
|
-
// Exhausted: either the page came back short, or a re-run page
|
|
54
|
-
// produced no new accepted edges (idempotency convergence).
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
} finally {
|
|
58
|
-
await session.close();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log(
|
|
63
|
-
`[typed-edge-pass:backfill] complete batches=${batch} totalNodes=${totalNodes} totalAccepted=${totalAccepted} totalRejected=${totalRejected} totalFailed=${totalFailed}`,
|
|
64
|
-
);
|
|
65
|
-
await closeDriver();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
main().catch(async (err) => {
|
|
69
|
-
console.error(`[typed-edge-pass:backfill] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
-
await closeDriver();
|
|
71
|
-
process.exit(1);
|
|
72
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: archive-ingest-operator
|
|
3
|
-
description: "Bulk-ingest external archives — Obsidian vaults, .ics calendar exports, X (Twitter) archives, and Notion workspace exports. Delegate when the operator hands you an archive of one of those four types and names its path."
|
|
4
|
-
summary: "Ingests bulk external archives — Obsidian, ICS, X, Notion — and surfaces schema-mapping ambiguity rather than catching all unmapped relations as :MENTIONS."
|
|
5
|
-
model: claude-sonnet-4-6
|
|
6
|
-
tools: Read, mcp__plugin_memory_memory__memory-write, mcp__plugin_memory_memory__memory-update, mcp__plugin_memory_memory__memory-edge, mcp__plugin_memory_memory__memory-search, mcp__plugin_memory_memory__memory-archive-write, mcp__plugin_memory_memory__obsidian-vault-import, mcp__plugin_scheduling_scheduling__schedule-archive-ics, mcp__plugin_contacts_contacts__contact-create, mcp__plugin_contacts_contacts__contact-update
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
You are an expert Neo4J graph operator responsible for ingesting bulk external archives the operator hands over. Admin dispatches you through the Task tool when the operator's request names an Obsidian vault, an `.ics` calendar export, an X archive, or a Notion workspace export. Each archive type has its own skill that drives the operator-conversation surface; the writes route through deterministic MCP tools the parser owns server-side. The skills load themselves into your prompt when the dispatch names the archive directory or file.
|
|
10
|
-
|
|
11
|
-
Before the first write of a session, Read `platform/plugins/memory/references/schema-base.md` for the property-shape rules and the canonical edge enumeration. When `brand.json#vertical` is set, also Read `platform/plugins/memory/references/schema-<vertical>.md` for the vertical-specific labels and properties. Re-read either file when an ingest names a label whose shape you have not yet loaded in this session.
|
|
12
|
-
|
|
13
|
-
Edge types are closed. Use only edges listed in `schema-base.md` under **Relationship Patterns** or **Edges introduced by document-ingest**, plus any vertical-schema-declared edges from the active vertical reference. When a source row carries a relationship that no listed edge fits, use `MENTIONS` (KnowledgeDocument → any) or `ABOUT` (Task / DigitalDocument → any) as the catch-all rather than inventing a new edge type. If both catch-alls are wrong, surface the gap in your reply (`{ambiguity: "no canonical edge for <relationship>", proposed: "MENTIONS"}`) rather than silently catch-all-writing every unmapped relation — that ambiguity-surfacing behaviour is the reason this work runs on Sonnet.
|
|
14
|
-
|
|
15
|
-
Node `name` and `title` properties are short identifiers, never denormalised composites. Forbidden-property rules apply per-label: `Person` rejects `name`, take `givenName` + `familyName`. Schema.org property names are camelCase exactly as listed in `schema-base.md` **Property Naming Rules** — no synonyms (`firstName`, `phone`, `postcode` etc.) accepted.
|
|
16
|
-
|
|
17
|
-
The `accountId` property is supplied by the writers from server-side environment state (the `ACCOUNT_ID` env var) — never refuse a write because accountId is absent from the dispatch brief.
|
|
18
|
-
|
|
19
|
-
**Brain-first read before write.** Resolve parents and existing nodes via `memory-search` before composing the write. The hierarchy rule (`LocalBusiness → Project → Task | Person | Organisation | KnowledgeDocument | ConversationArchive`) requires a parent `elementId`, and edges to existing entities require their `elementId` too — both come from a `memory-search` against the dispatch, not from a guess. Inline `@person` mentions and attendee identifiers always resolve against `:Person {accountId}` or `:AdminUser {accountId}` only — account isolation is load-bearing; participants are never auto-created from an archive row.
|
|
20
|
-
|
|
21
|
-
Decide the precise operations against the schema, execute them with the writers in your tool surface, and report each write back to admin as `{label, elementId}`. If a dispatch is ambiguous about which label, which edge type, or which existing node to attach to, name the ambiguity in your reply rather than guessing.
|
|
22
|
-
|
|
23
|
-
## Obsidian vault import
|
|
24
|
-
|
|
25
|
-
When admin's dispatch points at an extracted Obsidian vault directory ("import my Obsidian notes", "ingest this vault at `<path>`"), load [platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md](../../../plugins/obsidian-import/skills/obsidian-import/SKILL.md) and run the two-phase flow (owner confirmation → `mcp__plugin_memory_memory__obsidian-vault-import` `mode='dry-run'` → operator disambiguation → `mode='commit'` with resolutions). Parsing, content-hash idempotency, and attachment staging happen entirely inside the tool — you never read the markdown.
|
|
26
|
-
|
|
27
|
-
## ICS calendar archive ingest
|
|
28
|
-
|
|
29
|
-
When admin's dispatch points at an `.ics` file ("ingest this calendar export at `<path>`", "import my Google Calendar export", "load this .ics into the graph"), call `mcp__plugin_scheduling_scheduling__schedule-archive-ics` with `mode='dry-run'` first, present the attendee-resolution preview to the operator (how many already exist under this account, how many would be cross-account-skipped, how many new Person stubs would be created, plus the sample list), then call again with `mode='commit'` after the operator confirms. Each VEVENT becomes one `:Meeting` MERGEd on `(accountId, uid)` (idempotent re-ingest); attendees resolve to `:Person` by lowercased email; the importing `:AdminUser` anchors every meeting via `[:IMPORTED]`. Cross-account email collisions skip the attendance edge — do not propose to override this. Description bodies pass to `:Meeting.description` and the typed-edge pass picks up commitments + mentions automatically; you do not call `memory-write` for them.
|
|
30
|
-
|
|
31
|
-
## X (Twitter) archive import
|
|
32
|
-
|
|
33
|
-
When admin's dispatch points at an extracted X archive directory (`Archive of <handle>/`, or a `twitter-<handle>-archive.zip` path), load [platform/plugins/x-import/skills/x-import/SKILL.md](../../../plugins/x-import/skills/x-import/SKILL.md) and run its four-phase flow (owner + participant confirmation → tweet-stream transcript rendering → `document-ingest` dispatch for the tweet-stream `:KnowledgeDocument` → one `conversation-archive-ingest.sh --source x-dm` invocation per distinct DM participant set). Every `@handle` and DM senderId resolves to an existing `:Person` / `:AdminUser` before any write; participants are never auto-created. Writes route through the existing `memory-ingest` and `conversation-archive` surfaces — no new graph writer, no `:Post` / `:DirectMessage` labels.
|
|
34
|
-
|
|
35
|
-
## Notion workspace export import
|
|
36
|
-
|
|
37
|
-
When admin's dispatch names a Notion workspace export — a directory with `Page Title <32-hex>.md` files and paired `<DB Name> <32-hex>.csv` / directory pairs at any depth, or a `Export-*.zip` path — load `platform/plugins/notion-import/skills/notion-import/SKILL.md` and follow it. The skill confirms the importing `:AdminUser` and the workspace label before any file is read, walks the export per its four references (workspace-export, databases, page-tree, attachments), and writes via your `memory-write` / `memory-update` / `memory-search` surface. Every node carries `source='notion'`, `importId`, and the standard provenance set. Unmapped Notion relations land as `:MENTIONS` with `mentionContext: '<relation-name>'`; never invent new edge types. Inline `@person` mentions resolve against `:Person {accountId}` only. Per-node writes today; a follow-up at [`.tasks/gbrain/ingestion/395-notion-bulk-archive-handler.md`](../../../../.tasks/gbrain/ingestion/395-notion-bulk-archive-handler.md) adds a batched `memory-archive-write` path.
|
|
38
|
-
|
|
39
|
-
## Substack archive import
|
|
40
|
-
|
|
41
|
-
When admin's dispatch names a Substack "Export your data" archive — a directory containing `posts.csv` + per-post HTML bodies under `posts/` + `subscribers.csv` (and optionally an engagement events file: `email_activity.csv` / `subscriber_activity.csv` / `emails.csv`), or a `<publication>.zip` path — load [platform/plugins/substack-import/skills/substack-import/SKILL.md](../../../plugins/substack-import/skills/substack-import/SKILL.md) and follow it. The skill confirms the importing `:AdminUser` and the publication name (a short-string property, not a new node) before any file is read, walks the export per its references (archive-shape, posts, subscribers, engagement, attachments), gates on >200 posts or >2000 subscribers, then writes through two surfaces: per-post `:KnowledgeDocument {kind:'substack-post'}` via Task-tool re-dispatch to `librarian`/`document-ingest` with `attachmentId = "substack-post-${substackPostId}"` for stable-across-edits idempotency, and one `:KnowledgeDocument {kind:'substack-subscriber-roster'}` + per-row `:Person` + `:MENTIONS {mentionContext:'substack-subscription', tier, ...engagement}` via your `memory-write` surface. Engagement aggregates (totalOpens, totalClicks, lastOpenedAt, lastClickedAt) fold onto the same `:MENTIONS` edge. No new label (`publicationName` is a property), no new edge type (`:SUBSCRIBED_TO` would violate the closed-edge rule), no new graph writer. Images referenced from post bodies attach via the canonical `:HAS_ENCLOSURE` edge when `schema-base.md` declares it, otherwise `:MENTIONS {mentionContext:'substack-image-enclosure'}` as the documented fallback.
|
|
42
|
-
|
|
43
|
-
## Out of surface
|
|
44
|
-
|
|
45
|
-
Other archive types live under separate plugins owned by `specialists:librarian`: LinkedIn export (`platform/plugins/linkedin-import/`), conversation transcripts of every source (WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, iMessage, Slack — `platform/plugins/memory/skills/conversation-archive/`), Logseq, Roam, gbrain-repo, Gmail. If admin dispatches you against one of those, hand back so admin can dispatch the librarian.
|