@addai/node 0.4.0

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 (133) hide show
  1. package/README.md +96 -0
  2. package/dist/agent-auth.d.ts +9 -0
  3. package/dist/agent-auth.js +96 -0
  4. package/dist/attachments.d.ts +20 -0
  5. package/dist/attachments.js +164 -0
  6. package/dist/capabilities.d.ts +64 -0
  7. package/dist/capabilities.js +406 -0
  8. package/dist/claude-binary.d.ts +16 -0
  9. package/dist/claude-binary.js +145 -0
  10. package/dist/claude-config.d.ts +11 -0
  11. package/dist/claude-config.js +144 -0
  12. package/dist/claude-print.d.ts +35 -0
  13. package/dist/claude-print.js +312 -0
  14. package/dist/claude-spawn.d.ts +40 -0
  15. package/dist/claude-spawn.js +220 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +137 -0
  18. package/dist/codex-binary.d.ts +4 -0
  19. package/dist/codex-binary.js +70 -0
  20. package/dist/codex-spawn.d.ts +47 -0
  21. package/dist/codex-spawn.js +320 -0
  22. package/dist/command-runner.d.ts +9 -0
  23. package/dist/command-runner.js +592 -0
  24. package/dist/config.d.ts +7 -0
  25. package/dist/config.js +38 -0
  26. package/dist/control-server.d.ts +5 -0
  27. package/dist/control-server.js +292 -0
  28. package/dist/diskguard.d.ts +96 -0
  29. package/dist/diskguard.js +435 -0
  30. package/dist/error-classify.d.ts +20 -0
  31. package/dist/error-classify.js +159 -0
  32. package/dist/events.d.ts +26 -0
  33. package/dist/events.js +97 -0
  34. package/dist/gemini-binary.d.ts +4 -0
  35. package/dist/gemini-binary.js +64 -0
  36. package/dist/gemini-spawn.d.ts +52 -0
  37. package/dist/gemini-spawn.js +369 -0
  38. package/dist/git-identity.d.ts +18 -0
  39. package/dist/git-identity.js +104 -0
  40. package/dist/grok-binary.d.ts +4 -0
  41. package/dist/grok-binary.js +70 -0
  42. package/dist/grok-spawn.d.ts +41 -0
  43. package/dist/grok-spawn.js +495 -0
  44. package/dist/guardrails.d.ts +32 -0
  45. package/dist/guardrails.js +125 -0
  46. package/dist/hang-watchdog.d.ts +17 -0
  47. package/dist/hang-watchdog.js +48 -0
  48. package/dist/harness-registry.d.ts +55 -0
  49. package/dist/harness-registry.js +180 -0
  50. package/dist/heartbeat.d.ts +13 -0
  51. package/dist/heartbeat.js +95 -0
  52. package/dist/index.d.ts +12 -0
  53. package/dist/index.js +440 -0
  54. package/dist/install.d.ts +63 -0
  55. package/dist/install.js +467 -0
  56. package/dist/jsonl-tail.d.ts +20 -0
  57. package/dist/jsonl-tail.js +192 -0
  58. package/dist/kimi-binary.d.ts +4 -0
  59. package/dist/kimi-binary.js +68 -0
  60. package/dist/kimi-spawn.d.ts +47 -0
  61. package/dist/kimi-spawn.js +319 -0
  62. package/dist/lockfile.d.ts +30 -0
  63. package/dist/lockfile.js +169 -0
  64. package/dist/mcp-config.d.ts +23 -0
  65. package/dist/mcp-config.js +133 -0
  66. package/dist/memory-capture.d.ts +20 -0
  67. package/dist/memory-capture.js +45 -0
  68. package/dist/memory-pack.d.ts +34 -0
  69. package/dist/memory-pack.js +171 -0
  70. package/dist/model-filter.d.ts +4 -0
  71. package/dist/model-filter.js +34 -0
  72. package/dist/node-health.d.ts +18 -0
  73. package/dist/node-health.js +74 -0
  74. package/dist/pairing.d.ts +20 -0
  75. package/dist/pairing.js +129 -0
  76. package/dist/paths.d.ts +9 -0
  77. package/dist/paths.js +48 -0
  78. package/dist/precompact-hook.d.ts +27 -0
  79. package/dist/precompact-hook.js +136 -0
  80. package/dist/projects.d.ts +25 -0
  81. package/dist/projects.js +255 -0
  82. package/dist/prompt-keystrokes.d.ts +3 -0
  83. package/dist/prompt-keystrokes.js +56 -0
  84. package/dist/pty-helper.d.ts +26 -0
  85. package/dist/pty-helper.js +156 -0
  86. package/dist/request-pump.d.ts +12 -0
  87. package/dist/request-pump.js +249 -0
  88. package/dist/run-retry.d.ts +28 -0
  89. package/dist/run-retry.js +81 -0
  90. package/dist/self-update.d.ts +75 -0
  91. package/dist/self-update.js +192 -0
  92. package/dist/session-runner.d.ts +130 -0
  93. package/dist/session-runner.js +2271 -0
  94. package/dist/sleep-detector.d.ts +11 -0
  95. package/dist/sleep-detector.js +32 -0
  96. package/dist/state-migrate.d.ts +8 -0
  97. package/dist/state-migrate.js +91 -0
  98. package/dist/store.d.ts +5 -0
  99. package/dist/store.js +82 -0
  100. package/dist/supabase-client.d.ts +15 -0
  101. package/dist/supabase-client.js +119 -0
  102. package/dist/time-context.d.ts +10 -0
  103. package/dist/time-context.js +71 -0
  104. package/dist/tokens.d.ts +4 -0
  105. package/dist/tokens.js +33 -0
  106. package/dist/tui/app.d.ts +35 -0
  107. package/dist/tui/app.js +82 -0
  108. package/dist/tui/console-capture.d.ts +13 -0
  109. package/dist/tui/console-capture.js +62 -0
  110. package/dist/tui/dashboard.d.ts +42 -0
  111. package/dist/tui/dashboard.js +205 -0
  112. package/dist/tui/data.d.ts +57 -0
  113. package/dist/tui/data.js +62 -0
  114. package/dist/tui/harnesses.d.ts +16 -0
  115. package/dist/tui/harnesses.js +354 -0
  116. package/dist/tui/render.d.ts +21 -0
  117. package/dist/tui/render.js +116 -0
  118. package/dist/tui/requests.d.ts +18 -0
  119. package/dist/tui/requests.js +127 -0
  120. package/dist/tui/run.d.ts +13 -0
  121. package/dist/tui/run.js +178 -0
  122. package/dist/tui/transcript.d.ts +39 -0
  123. package/dist/tui/transcript.js +234 -0
  124. package/dist/tui.d.ts +1 -0
  125. package/dist/tui.js +314 -0
  126. package/dist/types.d.ts +65 -0
  127. package/dist/types.js +3 -0
  128. package/dist/win.d.ts +67 -0
  129. package/dist/win.js +317 -0
  130. package/package.json +60 -0
  131. package/scripts/fix-pty-helper.js +28 -0
  132. package/scripts/precompact-capture.js +292 -0
  133. package/scripts/smoke-test.sh +74 -0
package/dist/index.js ADDED
@@ -0,0 +1,440 @@
1
+ "use strict";
2
+ // ainode — the +Ai Node background daemon that pairs with +Ai (Supabase)
3
+ // and runs Claude / Codex sessions on behalf of authenticated requests.
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.VERSION = void 0;
39
+ exports.start = start;
40
+ const path = __importStar(require("path"));
41
+ const control_server_1 = require("./control-server");
42
+ const paths_1 = require("./paths");
43
+ const state_migrate_1 = require("./state-migrate");
44
+ const lockfile_1 = require("./lockfile");
45
+ const mcp_config_1 = require("./mcp-config");
46
+ const store_1 = require("./store");
47
+ const supabase_client_1 = require("./supabase-client");
48
+ const pairing_1 = require("./pairing");
49
+ const heartbeat_1 = require("./heartbeat");
50
+ const command_runner_1 = require("./command-runner");
51
+ const self_update_1 = require("./self-update");
52
+ const sleep_detector_1 = require("./sleep-detector");
53
+ const claude_config_1 = require("./claude-config");
54
+ const request_pump_1 = require("./request-pump");
55
+ const projects_1 = require("./projects");
56
+ const session_runner_1 = require("./session-runner");
57
+ const diskguard_1 = require("./diskguard");
58
+ const pty_helper_1 = require("./pty-helper");
59
+ // Read version from package.json at module-load. Previously hardcoded to
60
+ // '0.1.0' and never updated when package.json bumped — the CLI banner lied
61
+ // about which version was actually running, making upgrade verification
62
+ // hard ("addai-entity-runtime --version" always said 0.1.0).
63
+ const VERSION = (() => {
64
+ try {
65
+ // Resolves from compiled dist/ → ../package.json.
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
67
+ const pkg = require('../package.json');
68
+ if (typeof pkg.version === 'string' && pkg.version.length > 0)
69
+ return pkg.version;
70
+ }
71
+ catch { /* fall through */ }
72
+ return '0.0.0';
73
+ })();
74
+ exports.VERSION = VERSION;
75
+ // ── Daemon crash guard ──────────────────────────────────────────────────────
76
+ // A long-running daemon that spawns one child process per request must never
77
+ // die because a single request's child emitted an 'error' event with no
78
+ // listener, or a fire-and-forget promise rejected. Node's default is to crash
79
+ // the whole process — which kills every other session and wedges in-flight
80
+ // requests at 'running'. There is no OS supervisor on Windows to restart us,
81
+ // so we log and stay up; each request already has its own failure path
82
+ // (failRequest → 'spawn_failed'/etc.). This is the backstop behind the
83
+ // per-spawn guards in claude-print.ts / claude-spawn.ts / *-binary.ts.
84
+ process.on('uncaughtException', (err) => {
85
+ console.error('[runtime] uncaughtException — daemon staying up:', err?.stack ?? err);
86
+ });
87
+ process.on('unhandledRejection', (reason) => {
88
+ console.error('[runtime] unhandledRejection — daemon staying up:', reason instanceof Error ? reason.stack : reason);
89
+ });
90
+ /**
91
+ * Boot the daemon. If we don't have a pairing yet, run the user through
92
+ * the 5-letter code flow first; once paired, kick off the heartbeat.
93
+ */
94
+ async function start() {
95
+ const migration = (0, state_migrate_1.migrateLegacyStateDir)();
96
+ if (migration.migrated) {
97
+ const parts = [
98
+ migration.copied.length ? `copied ${migration.copied.join(', ')}` : '',
99
+ migration.moved.length ? `moved ${migration.moved.join(', ')}` : '',
100
+ ].filter(Boolean).join('; ');
101
+ console.log(`[ainode] state migrated to ~/.ainode (${parts})`);
102
+ }
103
+ (0, mcp_config_1.cleanupAllSessionMcpConfigs)();
104
+ // Pre-accept claude's bypass-permissions warning once so daemon-spawned
105
+ // TUIs don't block on the dialog. Idempotent + best-effort.
106
+ (0, claude_config_1.ensureBypassPermissionsAccepted)();
107
+ // node-pty's spawn-helper can arrive without its +x bit (npm extraction;
108
+ // reproducible under npx). Without it EVERY pty spawn — harness logins
109
+ // and claude TUI runs — dies with "posix_spawnp failed.". Repair here so
110
+ // a node that installed broken heals on restart, not on reinstall.
111
+ {
112
+ const line = (0, pty_helper_1.describeRepair)((0, pty_helper_1.ensurePtyHelperExecutable)());
113
+ if (line)
114
+ console.log(line);
115
+ }
116
+ const control = await (0, control_server_1.startControlServer)();
117
+ let lock;
118
+ try {
119
+ lock = (0, lockfile_1.acquireLockfile)({ port: control.port, version: VERSION, startedAt: Date.now() });
120
+ }
121
+ catch (err) {
122
+ await control.close();
123
+ throw err;
124
+ }
125
+ if (!(0, store_1.isPaired)()) {
126
+ try {
127
+ const code = await (0, pairing_1.requestPairCode)();
128
+ (0, pairing_1.printPairingInstructions)(code);
129
+ const result = await (0, pairing_1.awaitPairing)(code);
130
+ console.log(` ✓ Paired (runtime_id: ${result.runtime_id})`);
131
+ console.log('');
132
+ }
133
+ catch (err) {
134
+ console.error('Pairing failed:', err.message || err);
135
+ await control.close();
136
+ (0, lockfile_1.releaseLockfile)();
137
+ throw err;
138
+ }
139
+ }
140
+ // Begin the heartbeat — this also pushes the first capabilities probe
141
+ // up to Supabase so the runtime appears "online" immediately. When the
142
+ // server reports queued install/login commands, the command runner
143
+ // drains them.
144
+ (0, heartbeat_1.setPendingCommandsHook)(command_runner_1.wake);
145
+ (0, heartbeat_1.start)();
146
+ // If we are the daemon a remote `update_runtime` restarted INTO, close out
147
+ // that command now — with the version we actually came up as. The process
148
+ // that issued the restart could not report this: it had to exit to make
149
+ // room for us. A restart that never came back leaves the command `running`
150
+ // and the node silent, which is exactly the signal we want.
151
+ void finalizeRestartHandoff();
152
+ // Crash recovery: if we restarted while requests were still
153
+ // 'starting'/'running', fail them so paused flow runs can resume.
154
+ // Best effort — pairing failures shouldn't block startup.
155
+ reclaimOrphanedRequests('startup').catch(err => console.error('[startup] reclaim error:', err.message));
156
+ // Sweep stale session dirs (>7 days old). Each request mints a fresh
157
+ // dir under ~/.ainode/sessions/<reqId>/ which can hold a
158
+ // 50-500 MB git clone. Without periodic cleanup, a chatty runtime
159
+ // fills the user's home disk silently over months. Run async at
160
+ // startup; if it fails, we just keep using the host disk.
161
+ sweepStaleSessionDirs().catch(err => console.error('[startup] session sweep error:', err.message));
162
+ // DiskGuard: CCTV-style watermark eviction of the per-run Codex scratch
163
+ // dirs under os.tmpdir(). codex-spawn cleans up its own dir on exit; this
164
+ // is the safety net for SIGKILL/crash/orphan cases that skip that path.
165
+ // On a dedicated runtime host these dirs once grew to ~158 GiB and filled
166
+ // the disk. Sweeps at startup and on its own interval; unref'd so it never
167
+ // keeps the process alive.
168
+ const diskGuard = (0, diskguard_1.getDiskGuard)();
169
+ diskGuard.start();
170
+ // Long-lived zombie sweeper: even when the daemon stays alive, a
171
+ // spawn can die silently (no onExit) or hang past the JSONL timeout
172
+ // and never reach a terminal status. The RPC's 5-minute stale-after
173
+ // window means it's safe to call periodically; in-flight legitimate
174
+ // requests are protected by recent activity timestamps.
175
+ const reclaimInterval = setInterval(() => {
176
+ reclaimOrphanedRequests('periodic').catch(err => console.error('[periodic] reclaim error:', err.message));
177
+ }, 5 * 60 * 1000);
178
+ // Don't keep the process alive just for this timer.
179
+ reclaimInterval.unref?.();
180
+ // Sleep/wake detector: a laptop runtime (e.g. a MacBook) suspends when the
181
+ // lid closes — timers freeze, in-flight runs stall, and the server watchdog
182
+ // eventually fails them. On resume, wall-clock has jumped far past the last
183
+ // probe. When we detect that, immediately push a heartbeat (so the server
184
+ // stops treating us as offline) and reclaim our own in-flight runs (they
185
+ // went stale while suspended) so they re-dispatch — to a backup node if
186
+ // we're still coming back to life.
187
+ const sleepMonitor = (0, sleep_detector_1.startSleepMonitor)({
188
+ intervalMs: 30_000,
189
+ thresholdMs: 90_000, // >90s late = we were suspended (matches server offline window)
190
+ onWake: (gapMs) => {
191
+ console.warn(`[wake] detected ~${Math.round(gapMs / 1000)}s suspend — re-heartbeating and reclaiming in-flight runs`);
192
+ void (0, heartbeat_1.beat)().catch(() => { });
193
+ reclaimOrphanedRequests('wake').catch(err => console.error('[wake] reclaim error:', err.message));
194
+ },
195
+ });
196
+ // Begin polling for pending requests.
197
+ (0, request_pump_1.start)();
198
+ // …and for pending projects (git clones, future MCP/skill installs).
199
+ (0, projects_1.start)();
200
+ let stopped = false;
201
+ // Outcome of the last drain, so a remote roll can report whether it left
202
+ // work unfinished instead of claiming a clean restart either way.
203
+ let lastDrain = { drained: 0, timedOut: 0 };
204
+ const stop = async (drainTimeoutMs = 15_000) => {
205
+ if (stopped)
206
+ return;
207
+ stopped = true;
208
+ // Stop accepting new work first — otherwise the pump would queue
209
+ // another request between drain start and process exit.
210
+ (0, projects_1.stop)();
211
+ (0, request_pump_1.stop)();
212
+ (0, heartbeat_1.stop)();
213
+ sleepMonitor.stop();
214
+ diskGuard.stop();
215
+ // Graceful drain: wait up to drainTimeoutMs for in-flight requests
216
+ // to settle so child spawns get a chance to finish their final
217
+ // setStatus → notifyFlows webhooks. Without this, a SIGTERM would
218
+ // leave paused flow runs stuck `waiting` until server-side reclaim
219
+ // (5 min) requeued or failed them.
220
+ try {
221
+ const { drained, timedOut } = await (0, request_pump_1.drain)(drainTimeoutMs);
222
+ lastDrain = { drained, timedOut };
223
+ if (drained || timedOut) {
224
+ console.log(`[shutdown] drained ${drained} request(s); ${timedOut} still in-flight (timed out)`);
225
+ }
226
+ }
227
+ catch (err) {
228
+ console.error('[shutdown] drain error:', err.message);
229
+ }
230
+ // Announce offline AFTER draining, so in-flight runs finish on us first
231
+ // (marking offline earlier could let failover reassign a run we're about
232
+ // to complete — a needless double-run). Anything still unfinished past the
233
+ // drain timeout now fails over instantly instead of waiting the 90s window.
234
+ await (0, heartbeat_1.goOffline)();
235
+ await control.close();
236
+ (0, lockfile_1.releaseLockfile)();
237
+ };
238
+ // Remote roll: drain like a clean shutdown, install the target version if
239
+ // this launch mode can, then hand over to a detached replacement. The
240
+ // handoff file is written only AFTER the replacement is spawned, so a
241
+ // failure anywhere before that leaves this daemon running and the command
242
+ // is failed by the caller instead of silently taking the node down.
243
+ (0, command_runner_1.setRestartHook)(async ({ commandId, version }) => {
244
+ const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
245
+ const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
246
+ if (plan.installFirst) {
247
+ const err = await (0, self_update_1.installGlobal)(version);
248
+ if (err)
249
+ throw new Error(err);
250
+ }
251
+ // Drain generously — a restart is elective, so it should never guillotine
252
+ // a live entity turn the way a 15s SIGTERM drain would.
253
+ await stop(RESTART_DRAIN_MS);
254
+ // Point of no return. Past this line a replacement daemon exists, so we
255
+ // MUST exit — two live daemons on one node is worse than a command left
256
+ // unreported. Hence the handoff write (which only affects reporting) can
257
+ // never abort the exit.
258
+ (0, self_update_1.spawnReplacement)(plan, path.join(paths_1.RUNTIME_HOME, 'daemon.log'), process.cwd());
259
+ try {
260
+ (0, self_update_1.writeHandoff)(paths_1.RUNTIME_HOME, {
261
+ command_id: commandId,
262
+ from_version: VERSION,
263
+ target_version: version,
264
+ mode,
265
+ at: new Date().toISOString(),
266
+ drained: lastDrain.drained,
267
+ timed_out: lastDrain.timedOut,
268
+ });
269
+ }
270
+ catch (err) {
271
+ console.error('[restart] handoff write failed — the roll still happened, it just cannot self-report:', err.message);
272
+ }
273
+ console.log(`[restart] handed over to ${plan.file} (${mode} → ${version}); exiting`);
274
+ setTimeout(() => process.exit(0), 250).unref?.();
275
+ });
276
+ process.once('SIGINT', () => { stop().finally(() => process.exit(0)); });
277
+ process.once('SIGTERM', () => { stop().finally(() => process.exit(0)); });
278
+ if (process.platform === 'win32') {
279
+ // SIGTERM never fires on Windows. The signals that DO exist there:
280
+ // SIGBREAK (Ctrl+Break) and SIGHUP (console window closed — Windows
281
+ // gives ~5s before hard-killing, enough to release the lockfile and
282
+ // start the drain). Without these, any non-Ctrl+C stop skips cleanup
283
+ // and leaves a stale lockfile that blocks the next daemon start until
284
+ // the pid-liveness check catches up.
285
+ process.once('SIGBREAK', () => { stop().finally(() => process.exit(0)); });
286
+ process.once('SIGHUP', () => { stop().finally(() => process.exit(0)); });
287
+ }
288
+ process.once('beforeExit', () => { (0, lockfile_1.releaseLockfile)(); });
289
+ return { port: lock.port, pid: lock.pid, stop };
290
+ }
291
+ async function sweepStaleSessionDirs() {
292
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
293
+ const fs = require('fs');
294
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
295
+ const path = require('path');
296
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
297
+ const { RUNTIME_SESSIONS_DIR } = require('./paths');
298
+ if (!fs.existsSync(RUNTIME_SESSIONS_DIR))
299
+ return;
300
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
301
+ const cutoff = Date.now() - SEVEN_DAYS_MS;
302
+ // A directory's own mtime only moves when an ENTRY is added or removed, and
303
+ // a continuation turn merely overwrites the files already in place
304
+ // (.entities-runtime.mcp.json, .grok/config.toml, skills). So a chat used
305
+ // every day still looks "last touched" on the day it was created, and a pure
306
+ // conversation — one that never creates or deletes a file — ages into the
307
+ // cutoff while fully live. Take the newest mtime among the dir and its
308
+ // immediate children instead: the per-run config rewrite lands there, which
309
+ // makes this a real liveness signal rather than a creation date.
310
+ const effectiveMtimeMs = (dir, dirMtimeMs) => {
311
+ let newest = dirMtimeMs;
312
+ try {
313
+ for (const child of fs.readdirSync(dir)) {
314
+ try {
315
+ const st = fs.statSync(path.join(dir, child));
316
+ if (st.mtimeMs > newest)
317
+ newest = st.mtimeMs;
318
+ }
319
+ catch { /* skip unreadable child */ }
320
+ }
321
+ }
322
+ catch { /* unreadable dir — fall back to its own mtime */ }
323
+ return newest;
324
+ };
325
+ let removed = 0;
326
+ let kept = 0;
327
+ for (const name of fs.readdirSync(RUNTIME_SESSIONS_DIR)) {
328
+ const p = path.join(RUNTIME_SESSIONS_DIR, name);
329
+ try {
330
+ const st = fs.statSync(p);
331
+ if (!st.isDirectory())
332
+ continue;
333
+ if (effectiveMtimeMs(p, st.mtimeMs) < cutoff) {
334
+ // Deleting a session dir un-homes any chat still resuming a session
335
+ // keyed to it, so say which ones went — the old count-only line made
336
+ // that class of breakage invisible.
337
+ console.log(`[startup] sweeping stale session dir: ${p}`);
338
+ fs.rmSync(p, { recursive: true, force: true });
339
+ removed++;
340
+ }
341
+ else {
342
+ kept++;
343
+ }
344
+ }
345
+ catch { /* skip unreadable entry */ }
346
+ }
347
+ if (removed > 0) {
348
+ console.log(`[startup] swept ${removed} session dir(s) unused for 7d; ${kept} kept`);
349
+ }
350
+ }
351
+ /** How long a remote restart waits for live turns to finish before handing
352
+ * over. Elective, so far more generous than the 15s SIGTERM drain. */
353
+ const RESTART_DRAIN_MS = 10 * 60_000;
354
+ /**
355
+ * Complete the `update_runtime` command that restarted us, if any.
356
+ *
357
+ * Reports the version this process ACTUALLY came up as, which is the whole
358
+ * point: an update that installed but relaunched the old bits (the npx cache
359
+ * trap) reports the old version rather than a cheerful "completed".
360
+ */
361
+ async function finalizeRestartHandoff() {
362
+ // Read the token BEFORE consuming the handoff: takeHandoff deletes the file,
363
+ // so bailing after it would strand the command as `running` forever with
364
+ // nothing left on disk to retry from.
365
+ const t = (0, store_1.readPairing)()?.daemonToken;
366
+ if (!t)
367
+ return;
368
+ const h = (0, self_update_1.takeHandoff)(paths_1.RUNTIME_HOME);
369
+ if (!h)
370
+ return;
371
+ const changed = h.from_version !== VERSION;
372
+ const wanted = h.target_version;
373
+ const missed = wanted !== 'latest' && wanted !== VERSION;
374
+ try {
375
+ await (0, supabase_client_1.rpc)('runtime_command_update', {
376
+ p_token: t,
377
+ p_id: h.command_id,
378
+ p_status: missed ? 'failed' : 'completed',
379
+ p_payload: {
380
+ step: 'done',
381
+ from_version: h.from_version,
382
+ version: VERSION,
383
+ mode: h.mode,
384
+ restarted: true,
385
+ version_changed: changed,
386
+ drained: h.drained ?? 0,
387
+ // >0 means the drain ceiling expired with work still running — the
388
+ // roll went ahead over the top of it. Visible rather than implied.
389
+ interrupted_runs: h.timed_out ?? 0,
390
+ },
391
+ p_error: missed
392
+ ? `restarted, but came up as ${VERSION} (wanted ${wanted})`
393
+ : null,
394
+ });
395
+ console.log(`[restart] back up as ${VERSION} (was ${h.from_version}); command ${h.command_id} closed`);
396
+ }
397
+ catch (err) {
398
+ console.error('[restart] could not close command:', err.message);
399
+ }
400
+ }
401
+ async function reclaimOrphanedRequests(source) {
402
+ // Honour the shared circuit breaker — if Supabase is overloaded, the
403
+ // periodic reclaim isn't urgent (server-side reclaim runs anyway). Skip.
404
+ if ((0, supabase_client_1.rpcShouldSkip)())
405
+ return;
406
+ const pairing = (0, store_1.readPairing)();
407
+ if (!pairing)
408
+ return;
409
+ // v2 RPC: returns {request_id, prior_status, requeued}.
410
+ // requeued=true → row is back to 'pending', the request-pump will
411
+ // pick it up and try again. No webhook fires yet —
412
+ // the flow run is still waiting on a terminal state.
413
+ // requeued=false → retry budget exhausted, row is now 'failed'. We
414
+ // notify flows so the paused run resumes with the
415
+ // failure.
416
+ let rows = null;
417
+ try {
418
+ rows = await (0, supabase_client_1.rpc)('runtime_reclaim_inflight_requests', {
419
+ p_token: pairing.daemonToken,
420
+ });
421
+ }
422
+ catch (err) {
423
+ console.error(`[${source}] reclaim rpc failed:`, err.message);
424
+ return;
425
+ }
426
+ if (!rows || rows.length === 0)
427
+ return;
428
+ const requeued = rows.filter(r => r.requeued).length;
429
+ const failed = rows.length - requeued;
430
+ console.log(`[${source}] reclaimed ${rows.length} orphaned request(s)`
431
+ + (requeued ? ` · ${requeued} requeued for retry` : '')
432
+ + (failed ? ` · ${failed} failed (retry budget exhausted)` : ''));
433
+ // Only notify flows for the failure-final case. Requeued rows will
434
+ // notify normally via setStatus when they reach a terminal state.
435
+ for (const r of rows) {
436
+ if (r.requeued)
437
+ continue;
438
+ void (0, session_runner_1.notifyFlowsIfFlowRequest)(r.request_id).catch(err => console.error(`[${source}] reclaim notify [reqId=${r.request_id}] error: ${err.message}`));
439
+ }
440
+ }
@@ -0,0 +1,63 @@
1
+ /** Hidden per-session MCP config written next to the agent's working dir. */
2
+ export declare const MCP_CONFIG_FILENAME = ".ainode.mcp.json";
3
+ /** Pre-rename filename; removed when found so agents don't read a stale copy. */
4
+ export declare const LEGACY_MCP_CONFIG_FILENAME = ".entities-runtime.mcp.json";
5
+ export interface InstallSet {
6
+ /** Registry MCP ids to install (packages are expanded server-side). */
7
+ mcpIds: string[];
8
+ /** Registry skill ids to install. */
9
+ skillIds: string[];
10
+ /** Registry package ids to expand into their member MCPs + skills. */
11
+ packageIds: string[];
12
+ }
13
+ interface ResolvedMcp {
14
+ id: string;
15
+ name: string;
16
+ slug: string;
17
+ command: string;
18
+ args: string[] | null;
19
+ /** Resolved env values from the user's vault credential for this MCP.
20
+ * Empty object when the MCP doesn't need credentials or the user
21
+ * hasn't picked one. */
22
+ env: Record<string, string> | null;
23
+ /** Spec from codeflows_mcps.env_vars — what credentials the MCP wants.
24
+ * Surface for future UI; not used by the daemon directly. */
25
+ env_spec: Array<{
26
+ key: string;
27
+ label?: string;
28
+ required?: boolean;
29
+ help?: string;
30
+ }> | null;
31
+ npm_package: string | null;
32
+ version: string | null;
33
+ }
34
+ export interface InstallResult {
35
+ /** Path to a written .mcp.json file (suitable for --mcp-config). null = no MCPs installed. */
36
+ mcpConfigPath: string | null;
37
+ /** Slugs of skills written to <cwd>/.claude/skills/. */
38
+ skillSlugs: string[];
39
+ /** Resolved MCP rows (with env merged) so non-claude agents (codex)
40
+ * can build their own MCP config from the same source of truth. */
41
+ mcps: ResolvedMcp[];
42
+ /** Cleanup callback — removes everything this install wrote. */
43
+ dispose: () => Promise<void>;
44
+ }
45
+ /**
46
+ * Run install side-effects against `cwd` and return paths to clean up
47
+ * later. Safe to call with an empty set — returns a no-op result.
48
+ */
49
+ export declare function install(cwd: string, set: InstallSet, credentials?: Record<string, unknown>,
50
+ /** Per-session env overrides keyed by MCP slug. Merged into each resolved
51
+ * MCP's env BEFORE writing .mcp.json, overriding credential-supplied
52
+ * values for the same env var. Used by widgetstudio for dynamic
53
+ * streaming auth tokens that can't live in a static vault credential. */
54
+ envOverrides?: Record<string, Record<string, string>>,
55
+ /** Per-skill variable values keyed by skill id → { varName: value }.
56
+ * Overlaid onto each skill's variable defaults and substituted into the
57
+ * skill content via {{var.<name>}} tokens before SKILL.md is written. */
58
+ skillVarValues?: Record<string, Record<string, string>>,
59
+ /** The entity_runtime_requests row being installed. Passed to the server
60
+ * so credential resolution is scoped to THIS request instead of the
61
+ * latest in-flight request on the runtime. */
62
+ requestId?: string): Promise<InstallResult>;
63
+ export {};