@adhdev/daemon-standalone 1.0.18-rc.2 → 1.0.18-rc.3

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/public/index.html CHANGED
@@ -7,9 +7,9 @@
7
7
  <meta name="description" content="ADHDev self-hosted dashboard for controlling AI agents" />
8
8
  <link rel="icon" href="/otter-logo.png" />
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
10
- <script type="module" crossorigin src="/assets/index-D8z4TEna.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-Be0j0_PU.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendor-DyCWA2YZ.js">
12
- <link rel="stylesheet" crossorigin href="/assets/index-DU4BBoUD.css">
12
+ <link rel="stylesheet" crossorigin href="/assets/index-CQVaCclg.css">
13
13
  </head>
14
14
  <body>
15
15
  <!-- Apply theme immediately to prevent FOIT (Flash of Incorrect Theme) -->
@@ -21,6 +21,7 @@ declare class SessionHostServer extends EventEmitter {
21
21
  private recentTransitions;
22
22
  private exitWaiters;
23
23
  private lastNoOutputInputWarnAt;
24
+ private stopRequests;
24
25
  constructor(options?: SessionHostServerOptions);
25
26
  start(): Promise<void>;
26
27
  stop(): Promise<void>;
@@ -47,6 +48,12 @@ declare class SessionHostServer extends EventEmitter {
47
48
  private restorePersistedRuntimes;
48
49
  private pruneDuplicateRuntime;
49
50
  private startRuntime;
51
+ /**
52
+ * Handle a PTY termination: classify the (exitCode, signal) pair, stamp the
53
+ * record + tombstone, emit exactly one structured diagnostic, and schedule
54
+ * cleanup of the live persistence file (the tombstone is retained).
55
+ */
56
+ private handleRuntimeExit;
50
57
  }
51
58
 
52
59
  export { SessionHostServer, type SessionHostServerOptions };
@@ -21,6 +21,7 @@ declare class SessionHostServer extends EventEmitter {
21
21
  private recentTransitions;
22
22
  private exitWaiters;
23
23
  private lastNoOutputInputWarnAt;
24
+ private stopRequests;
24
25
  constructor(options?: SessionHostServerOptions);
25
26
  start(): Promise<void>;
26
27
  stop(): Promise<void>;
@@ -47,6 +48,12 @@ declare class SessionHostServer extends EventEmitter {
47
48
  private restorePersistedRuntimes;
48
49
  private pruneDuplicateRuntime;
49
50
  private startRuntime;
51
+ /**
52
+ * Handle a PTY termination: classify the (exitCode, signal) pair, stamp the
53
+ * record + tombstone, emit exactly one structured diagnostic, and schedule
54
+ * cleanup of the live persistence file (the tombstone is retained).
55
+ */
56
+ private handleRuntimeExit;
50
57
  }
51
58
 
52
59
  export { SessionHostServer, type SessionHostServerOptions };
@@ -279,13 +279,16 @@ var PtySessionRuntime = class {
279
279
  this.respondToTerminalQueries(data);
280
280
  this.onDataCallback(data);
281
281
  });
282
- this.ptyProcess.onExit(({ exitCode }) => {
282
+ this.ptyProcess.onExit(({ exitCode, signal }) => {
283
283
  this.ptyProcess = null;
284
284
  this.screenMirror?.dispose();
285
285
  this.screenMirror = null;
286
286
  this.pendingQueryScanTail = "";
287
287
  this.terminalModeScanTail = "";
288
- this.onExitCallback(exitCode ?? null);
288
+ this.onExitCallback(
289
+ typeof exitCode === "number" ? exitCode : null,
290
+ typeof signal === "number" ? signal : null
291
+ );
289
292
  });
290
293
  return this.ptyProcess.pid;
291
294
  }
@@ -397,10 +400,12 @@ var path = __toESM(require("path"));
397
400
  var SessionHostStorage = class {
398
401
  rootDir;
399
402
  runtimesDir;
403
+ tombstonesDir;
400
404
  constructor(options = {}) {
401
405
  const appName = options.appName || "adhdev";
402
406
  this.rootDir = path.join(os2.homedir(), ".adhdev", "session-host", appName);
403
407
  this.runtimesDir = path.join(this.rootDir, "runtimes");
408
+ this.tombstonesDir = path.join(this.rootDir, "tombstones");
404
409
  }
405
410
  loadAll() {
406
411
  if (!fs2.existsSync(this.runtimesDir)) return [];
@@ -436,6 +441,52 @@ var SessionHostStorage = class {
436
441
  } catch {
437
442
  }
438
443
  }
444
+ /**
445
+ * Persist a compact termination tombstone. Kept in a separate directory from
446
+ * live runtimes so it survives the post-exit cleanup of the runtime file and
447
+ * remains inspectable for post-mortem diagnostics.
448
+ */
449
+ saveTombstone(sessionId, termination) {
450
+ fs2.mkdirSync(this.tombstonesDir, { recursive: true });
451
+ const filePath = path.join(this.tombstonesDir, `${sessionId}.json`);
452
+ const payload = {
453
+ sessionId,
454
+ termination,
455
+ updatedAt: Date.now()
456
+ };
457
+ fs2.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
458
+ }
459
+ loadTombstone(sessionId) {
460
+ const filePath = path.join(this.tombstonesDir, `${sessionId}.json`);
461
+ try {
462
+ const parsed = JSON.parse(fs2.readFileSync(filePath, "utf8"));
463
+ return parsed?.sessionId ? parsed : null;
464
+ } catch {
465
+ return null;
466
+ }
467
+ }
468
+ loadAllTombstones() {
469
+ if (!fs2.existsSync(this.tombstonesDir)) return [];
470
+ const entries = fs2.readdirSync(this.tombstonesDir, { withFileTypes: true });
471
+ const states = [];
472
+ for (const entry of entries) {
473
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
474
+ const fullPath = path.join(this.tombstonesDir, entry.name);
475
+ try {
476
+ const parsed = JSON.parse(fs2.readFileSync(fullPath, "utf8"));
477
+ if (parsed?.sessionId) states.push(parsed);
478
+ } catch {
479
+ }
480
+ }
481
+ return states.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
482
+ }
483
+ removeTombstone(sessionId) {
484
+ const filePath = path.join(this.tombstonesDir, `${sessionId}.json`);
485
+ try {
486
+ fs2.unlinkSync(filePath);
487
+ } catch {
488
+ }
489
+ }
439
490
  };
440
491
 
441
492
  // src/session-diagnostics.ts
@@ -646,6 +697,9 @@ var SessionHostServer = class extends import_events.EventEmitter {
646
697
  recentTransitions = [];
647
698
  exitWaiters = /* @__PURE__ */ new Map();
648
699
  lastNoOutputInputWarnAt = /* @__PURE__ */ new Map();
700
+ // Records the most recent explicit stop/delete/restart/prune request per
701
+ // session so the termination diagnostic can attribute the exit to it.
702
+ stopRequests = /* @__PURE__ */ new Map();
649
703
  constructor(options = {}) {
650
704
  super();
651
705
  this.endpoint = options.endpoint || (0, import_session_host_core4.getDefaultSessionHostEndpoint)(options.appName || "adhdev");
@@ -843,6 +897,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
843
897
  return { success: true, result: this.registry.getSession(request.payload.sessionId) };
844
898
  }
845
899
  case "stop_session": {
900
+ this.stopRequests.set(request.payload.sessionId, "stop");
846
901
  this.registry.setLifecycle(request.payload.sessionId, "stopping");
847
902
  this.persistNow(request.payload.sessionId);
848
903
  this.requireRuntime(request.payload.sessionId).stop();
@@ -857,6 +912,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
857
912
  if (!request.payload.force) {
858
913
  return { success: false, error: `Session ${record.sessionId} is still running; pass force to stop and delete it` };
859
914
  }
915
+ this.stopRequests.set(record.sessionId, "delete");
860
916
  this.registry.setLifecycle(record.sessionId, "stopping");
861
917
  this.persistNow(record.sessionId);
862
918
  this.requireRuntime(record.sessionId).stop();
@@ -866,6 +922,8 @@ var SessionHostServer = class extends import_events.EventEmitter {
866
922
  }
867
923
  this.registry.deleteSession(record.sessionId);
868
924
  this.storage.remove(record.sessionId);
925
+ this.storage.removeTombstone(record.sessionId);
926
+ this.stopRequests.delete(record.sessionId);
869
927
  this.emitEvent({ type: "session_deleted", sessionId: record.sessionId });
870
928
  this.recordRuntimeTransition(record.sessionId, "delete_session", record.lifecycle, void 0, true);
871
929
  return { success: true, result: { sessionId: record.sessionId, deleted: true } };
@@ -1173,6 +1231,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
1173
1231
  throw new Error(`Unknown session: ${sessionId}`);
1174
1232
  }
1175
1233
  if (this.runtimes.has(sessionId)) {
1234
+ this.stopRequests.set(sessionId, "restart");
1176
1235
  this.registry.setLifecycle(sessionId, "stopping");
1177
1236
  this.persistNow(sessionId);
1178
1237
  this.recordRuntimeTransition(sessionId, "restart_requested", "stopping", void 0, true);
@@ -1243,6 +1302,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
1243
1302
  true
1244
1303
  );
1245
1304
  if (this.runtimes.has(record.sessionId)) {
1305
+ this.stopRequests.set(record.sessionId, "prune");
1246
1306
  this.registry.setLifecycle(record.sessionId, "stopping");
1247
1307
  this.persistNow(record.sessionId);
1248
1308
  this.requireRuntime(record.sessionId).stop();
@@ -1252,6 +1312,8 @@ var SessionHostServer = class extends import_events.EventEmitter {
1252
1312
  }
1253
1313
  this.registry.deleteSession(record.sessionId);
1254
1314
  this.storage.remove(record.sessionId);
1315
+ this.storage.removeTombstone(record.sessionId);
1316
+ this.stopRequests.delete(record.sessionId);
1255
1317
  }
1256
1318
  startRuntime(record, payload, startEventType) {
1257
1319
  const runtime = new PtySessionRuntime({
@@ -1262,22 +1324,7 @@ var SessionHostServer = class extends import_events.EventEmitter {
1262
1324
  this.schedulePersist(record.sessionId);
1263
1325
  this.emitEvent({ type: "session_output", sessionId: record.sessionId, seq, data });
1264
1326
  },
1265
- onExit: (exitCode) => {
1266
- this.registry.markStopped(record.sessionId, exitCode === 0 ? "stopped" : "failed");
1267
- this.runtimes.delete(record.sessionId);
1268
- this.resolveExitWaiters(record.sessionId, exitCode);
1269
- this.persistNow(record.sessionId);
1270
- this.emitEvent({ type: "session_exit", sessionId: record.sessionId, exitCode });
1271
- this.recordRuntimeTransition(
1272
- record.sessionId,
1273
- "session_exit",
1274
- exitCode === 0 ? "stopped" : "failed",
1275
- void 0,
1276
- exitCode === 0,
1277
- exitCode === 0 ? void 0 : `exitCode=${exitCode}`
1278
- );
1279
- setTimeout(() => this.storage.remove(record.sessionId), 5e3);
1280
- }
1327
+ onExit: (exitCode, signal) => this.handleRuntimeExit(record, exitCode, signal)
1281
1328
  });
1282
1329
  this.registry.setLifecycle(record.sessionId, "starting");
1283
1330
  const pid = runtime.start();
@@ -1288,6 +1335,58 @@ var SessionHostServer = class extends import_events.EventEmitter {
1288
1335
  this.recordRuntimeTransition(record.sessionId, startEventType, startedRecord.lifecycle, `pid=${pid}`, true);
1289
1336
  return startedRecord;
1290
1337
  }
1338
+ /**
1339
+ * Handle a PTY termination: classify the (exitCode, signal) pair, stamp the
1340
+ * record + tombstone, emit exactly one structured diagnostic, and schedule
1341
+ * cleanup of the live persistence file (the tombstone is retained).
1342
+ */
1343
+ handleRuntimeExit(record, exitCode, signal) {
1344
+ const priorRecord = this.registry.getSession(record.sessionId);
1345
+ const termination = (0, import_session_host_core4.classifyTermination)({
1346
+ exitCode,
1347
+ signal,
1348
+ osPid: priorRecord?.osPid,
1349
+ previousLifecycle: priorRecord?.lifecycle,
1350
+ lastOutputAt: priorRecord?.lastActivityAt,
1351
+ requestedStop: this.stopRequests.get(record.sessionId),
1352
+ terminatedAt: Date.now()
1353
+ });
1354
+ this.stopRequests.delete(record.sessionId);
1355
+ this.registry.markStopped(record.sessionId, termination.lifecycle, termination);
1356
+ this.runtimes.delete(record.sessionId);
1357
+ this.resolveExitWaiters(record.sessionId, exitCode);
1358
+ this.persistNow(record.sessionId);
1359
+ this.storage.saveTombstone(record.sessionId, termination);
1360
+ this.emitEvent({ type: "session_exit", sessionId: record.sessionId, exitCode, signal, termination });
1361
+ const summary = `reason=${termination.reason} exitCode=${termination.exitCode === null ? "unknown" : termination.exitCode} signal=${termination.signal ?? "none"}`;
1362
+ this.recordHostLog(
1363
+ termination.lifecycle === "stopped" ? "info" : "warn",
1364
+ `session terminated: ${summary}`,
1365
+ record.sessionId,
1366
+ {
1367
+ runtimeKey: record.runtimeKey,
1368
+ providerType: record.providerType,
1369
+ osPid: termination.osPid,
1370
+ exitCode: termination.exitCode,
1371
+ signal: termination.signal,
1372
+ reason: termination.reason,
1373
+ previousLifecycle: termination.previousLifecycle,
1374
+ lifecycle: termination.lifecycle,
1375
+ lastOutputAt: termination.lastOutputAt,
1376
+ requestedStop: termination.requestedStop
1377
+ }
1378
+ );
1379
+ this.recordRuntimeTransition(
1380
+ record.sessionId,
1381
+ "session_exit",
1382
+ termination.lifecycle,
1383
+ summary,
1384
+ termination.lifecycle === "stopped",
1385
+ termination.lifecycle === "stopped" ? void 0 : summary
1386
+ );
1387
+ setTimeout(() => this.storage.remove(record.sessionId), 5e3).unref?.();
1388
+ return termination;
1389
+ }
1291
1390
  };
1292
1391
 
1293
1392
  // src/index.ts