@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.
Files changed (97) hide show
  1. package/dist/index.js +134 -1
  2. package/package.json +1 -1
  3. package/payload/platform/lib/embed-client/dist/index.d.ts +2 -0
  4. package/payload/platform/lib/embed-client/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/embed-client/dist/index.js +50 -10
  6. package/payload/platform/lib/embed-client/dist/index.js.map +1 -1
  7. package/payload/platform/lib/embed-client/src/index.ts +57 -9
  8. package/payload/platform/plugins/admin/skills/insight/SKILL.md +1 -1
  9. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +39 -5
  10. package/payload/platform/plugins/docs/references/admin-ui.md +12 -0
  11. package/payload/platform/plugins/docs/references/deployment.md +20 -0
  12. package/payload/platform/plugins/docs/references/internals.md +2 -0
  13. package/payload/platform/plugins/docs/references/plugins-guide.md +4 -4
  14. package/payload/platform/plugins/memory/PLUGIN.md +0 -1
  15. package/payload/platform/plugins/memory/mcp/dist/index.js +0 -45
  16. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +6 -6
  18. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js +41 -2
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +34 -4
  23. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  24. package/payload/platform/plugins/notion-import/.claude-plugin/plugin.json +1 -1
  25. package/payload/platform/plugins/notion-import/PLUGIN.md +6 -6
  26. package/payload/platform/plugins/notion-import/skills/notion-import/SKILL.md +1 -1
  27. package/payload/platform/plugins/notion-import/skills/notion-import/references/databases.md +1 -1
  28. package/payload/platform/plugins/obsidian-import/.claude-plugin/plugin.json +1 -1
  29. package/payload/platform/plugins/obsidian-import/PLUGIN.md +5 -5
  30. package/payload/platform/plugins/obsidian-import/skills/obsidian-import/SKILL.md +1 -1
  31. package/payload/platform/plugins/substack-import/.claude-plugin/plugin.json +1 -1
  32. package/payload/platform/plugins/substack-import/PLUGIN.md +8 -8
  33. package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +2 -2
  34. package/payload/platform/plugins/substack-import/skills/substack-import/references/posts.md +1 -1
  35. package/payload/platform/plugins/x-import/.claude-plugin/plugin.json +1 -1
  36. package/payload/platform/plugins/x-import/PLUGIN.md +5 -5
  37. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +1 -1
  38. package/payload/platform/scripts/check-no-esm-require.mjs +3 -0
  39. package/payload/platform/scripts/identity-forbidden-token-check.mjs +0 -1
  40. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -1
  42. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/http-server.js +75 -2
  45. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/index.js +3 -3
  47. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
  49. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +8 -4
  50. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
  51. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +16 -3
  52. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  53. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +29 -12
  54. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  55. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -4
  56. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  57. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  58. package/payload/platform/services/claude-session-manager/dist/reaper.js +6 -6
  59. package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
  60. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  61. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +0 -1
  62. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  63. package/payload/platform/templates/specialists/agents/coding-assistant.md +1 -1
  64. package/payload/platform/templates/specialists/agents/database-operator.md +2 -6
  65. package/payload/platform/templates/specialists/agents/librarian.md +20 -7
  66. package/payload/server/power-health.cjs +129 -0
  67. package/payload/server/public/assets/{AdminLoginScreens-DKwWudsP.js → AdminLoginScreens-BBtA0mi8.js} +1 -1
  68. package/payload/server/public/assets/AdminShell-CZW9x0-A.js +1 -0
  69. package/payload/server/public/assets/{Checkbox-V5ubVZG1.js → Checkbox-CezYRnKL.js} +1 -1
  70. package/payload/server/public/assets/{OperatorConversations-DOGCHZhv.css → OperatorConversations-Bhyf22Ct.css} +1 -1
  71. package/payload/server/public/assets/OperatorConversations-D6BawEz3.js +9 -0
  72. package/payload/server/public/assets/{admin-Cfqj5N4P.js → admin-BVpCUS4j.js} +1 -1
  73. package/payload/server/public/assets/{browser-CtMwlpyW.js → browser-ByLzuh0X.js} +1 -1
  74. package/payload/server/public/assets/chat-DTvzNb92.js +1 -0
  75. package/payload/server/public/assets/data-Da1oTowD.js +1 -0
  76. package/payload/server/public/assets/{graph-Bez5QF37.js → graph-iYrodF_0.js} +3 -3
  77. package/payload/server/public/assets/{graph-labels-BkbaBPIP.js → graph-labels-DM9u5VTf.js} +1 -1
  78. package/payload/server/public/assets/operator-BPplyxq-.js +1 -0
  79. package/payload/server/public/assets/page-oogapuCz.js +30 -0
  80. package/payload/server/public/assets/{public-wy93nfmL.js → public-wBFC1mQO.js} +1 -1
  81. package/payload/server/public/browser.html +4 -4
  82. package/payload/server/public/chat.html +5 -5
  83. package/payload/server/public/data.html +4 -4
  84. package/payload/server/public/graph.html +6 -6
  85. package/payload/server/public/index.html +6 -6
  86. package/payload/server/public/operator.html +7 -7
  87. package/payload/server/public/public.html +5 -5
  88. package/payload/server/server-init.cjs +40 -0
  89. package/payload/server/server.js +212 -12
  90. package/payload/platform/plugins/memory/mcp/scripts/backfill-typed-edges.ts +0 -72
  91. package/payload/platform/templates/specialists/agents/archive-ingest-operator.md +0 -45
  92. package/payload/server/public/assets/AdminShell-BsPsXvJv.js +0 -1
  93. package/payload/server/public/assets/OperatorConversations-DPRRlS_z.js +0 -9
  94. package/payload/server/public/assets/chat-Bm11OGsU.js +0 -1
  95. package/payload/server/public/assets/data-7_dN5vN5.js +0 -1
  96. package/payload/server/public/assets/operator-F5KHg5WG.js +0 -1
  97. 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-Bm11OGsU.js"></script>
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-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
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-CExyEcvh.js">
15
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
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-7_dN5vN5.js"></script>
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-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/graph-labels-BkbaBPIP.js">
12
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
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-Bez5QF37.js"></script>
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-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/graph-labels-BkbaBPIP.js">
12
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
13
- <link rel="modulepreload" crossorigin href="/assets/Checkbox-V5ubVZG1.js">
14
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
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-Cfqj5N4P.js"></script>
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-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
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-V5ubVZG1.js">
14
- <link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-DKwWudsP.js">
15
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
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-F5KHg5WG.js"></script>
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-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
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-V5ubVZG1.js">
14
- <link rel="modulepreload" crossorigin href="/assets/AdminLoginScreens-DKwWudsP.js">
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-CExyEcvh.js">
17
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
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-wy93nfmL.js"></script>
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-DPRRlS_z.js">
11
- <link rel="modulepreload" crossorigin href="/assets/AdminShell-BsPsXvJv.js">
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-CExyEcvh.js">
15
- <link rel="stylesheet" crossorigin href="/assets/OperatorConversations-DOGCHZhv.css">
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
 
@@ -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
- * Each message is emitted at most once (removed when emitted). */
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(() => this.reconcile(Date.now()), intervalMs);
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.