@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.
@@ -25,6 +25,7 @@ import * as fs3 from "fs";
25
25
  import * as net from "net";
26
26
  import {
27
27
  SessionHostRegistry,
28
+ classifyTermination,
28
29
  createLineParser,
29
30
  createResponseEnvelope,
30
31
  getDefaultSessionHostEndpoint
@@ -268,13 +269,16 @@ var PtySessionRuntime = class {
268
269
  this.respondToTerminalQueries(data);
269
270
  this.onDataCallback(data);
270
271
  });
271
- this.ptyProcess.onExit(({ exitCode }) => {
272
+ this.ptyProcess.onExit(({ exitCode, signal }) => {
272
273
  this.ptyProcess = null;
273
274
  this.screenMirror?.dispose();
274
275
  this.screenMirror = null;
275
276
  this.pendingQueryScanTail = "";
276
277
  this.terminalModeScanTail = "";
277
- this.onExitCallback(exitCode ?? null);
278
+ this.onExitCallback(
279
+ typeof exitCode === "number" ? exitCode : null,
280
+ typeof signal === "number" ? signal : null
281
+ );
278
282
  });
279
283
  return this.ptyProcess.pid;
280
284
  }
@@ -386,10 +390,12 @@ import * as path from "path";
386
390
  var SessionHostStorage = class {
387
391
  rootDir;
388
392
  runtimesDir;
393
+ tombstonesDir;
389
394
  constructor(options = {}) {
390
395
  const appName = options.appName || "adhdev";
391
396
  this.rootDir = path.join(os2.homedir(), ".adhdev", "session-host", appName);
392
397
  this.runtimesDir = path.join(this.rootDir, "runtimes");
398
+ this.tombstonesDir = path.join(this.rootDir, "tombstones");
393
399
  }
394
400
  loadAll() {
395
401
  if (!fs2.existsSync(this.runtimesDir)) return [];
@@ -425,6 +431,52 @@ var SessionHostStorage = class {
425
431
  } catch {
426
432
  }
427
433
  }
434
+ /**
435
+ * Persist a compact termination tombstone. Kept in a separate directory from
436
+ * live runtimes so it survives the post-exit cleanup of the runtime file and
437
+ * remains inspectable for post-mortem diagnostics.
438
+ */
439
+ saveTombstone(sessionId, termination) {
440
+ fs2.mkdirSync(this.tombstonesDir, { recursive: true });
441
+ const filePath = path.join(this.tombstonesDir, `${sessionId}.json`);
442
+ const payload = {
443
+ sessionId,
444
+ termination,
445
+ updatedAt: Date.now()
446
+ };
447
+ fs2.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
448
+ }
449
+ loadTombstone(sessionId) {
450
+ const filePath = path.join(this.tombstonesDir, `${sessionId}.json`);
451
+ try {
452
+ const parsed = JSON.parse(fs2.readFileSync(filePath, "utf8"));
453
+ return parsed?.sessionId ? parsed : null;
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+ loadAllTombstones() {
459
+ if (!fs2.existsSync(this.tombstonesDir)) return [];
460
+ const entries = fs2.readdirSync(this.tombstonesDir, { withFileTypes: true });
461
+ const states = [];
462
+ for (const entry of entries) {
463
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
464
+ const fullPath = path.join(this.tombstonesDir, entry.name);
465
+ try {
466
+ const parsed = JSON.parse(fs2.readFileSync(fullPath, "utf8"));
467
+ if (parsed?.sessionId) states.push(parsed);
468
+ } catch {
469
+ }
470
+ }
471
+ return states.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
472
+ }
473
+ removeTombstone(sessionId) {
474
+ const filePath = path.join(this.tombstonesDir, `${sessionId}.json`);
475
+ try {
476
+ fs2.unlinkSync(filePath);
477
+ } catch {
478
+ }
479
+ }
428
480
  };
429
481
 
430
482
  // src/session-diagnostics.ts
@@ -640,6 +692,9 @@ var SessionHostServer = class extends EventEmitter {
640
692
  recentTransitions = [];
641
693
  exitWaiters = /* @__PURE__ */ new Map();
642
694
  lastNoOutputInputWarnAt = /* @__PURE__ */ new Map();
695
+ // Records the most recent explicit stop/delete/restart/prune request per
696
+ // session so the termination diagnostic can attribute the exit to it.
697
+ stopRequests = /* @__PURE__ */ new Map();
643
698
  constructor(options = {}) {
644
699
  super();
645
700
  this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || "adhdev");
@@ -837,6 +892,7 @@ var SessionHostServer = class extends EventEmitter {
837
892
  return { success: true, result: this.registry.getSession(request.payload.sessionId) };
838
893
  }
839
894
  case "stop_session": {
895
+ this.stopRequests.set(request.payload.sessionId, "stop");
840
896
  this.registry.setLifecycle(request.payload.sessionId, "stopping");
841
897
  this.persistNow(request.payload.sessionId);
842
898
  this.requireRuntime(request.payload.sessionId).stop();
@@ -851,6 +907,7 @@ var SessionHostServer = class extends EventEmitter {
851
907
  if (!request.payload.force) {
852
908
  return { success: false, error: `Session ${record.sessionId} is still running; pass force to stop and delete it` };
853
909
  }
910
+ this.stopRequests.set(record.sessionId, "delete");
854
911
  this.registry.setLifecycle(record.sessionId, "stopping");
855
912
  this.persistNow(record.sessionId);
856
913
  this.requireRuntime(record.sessionId).stop();
@@ -860,6 +917,8 @@ var SessionHostServer = class extends EventEmitter {
860
917
  }
861
918
  this.registry.deleteSession(record.sessionId);
862
919
  this.storage.remove(record.sessionId);
920
+ this.storage.removeTombstone(record.sessionId);
921
+ this.stopRequests.delete(record.sessionId);
863
922
  this.emitEvent({ type: "session_deleted", sessionId: record.sessionId });
864
923
  this.recordRuntimeTransition(record.sessionId, "delete_session", record.lifecycle, void 0, true);
865
924
  return { success: true, result: { sessionId: record.sessionId, deleted: true } };
@@ -1167,6 +1226,7 @@ var SessionHostServer = class extends EventEmitter {
1167
1226
  throw new Error(`Unknown session: ${sessionId}`);
1168
1227
  }
1169
1228
  if (this.runtimes.has(sessionId)) {
1229
+ this.stopRequests.set(sessionId, "restart");
1170
1230
  this.registry.setLifecycle(sessionId, "stopping");
1171
1231
  this.persistNow(sessionId);
1172
1232
  this.recordRuntimeTransition(sessionId, "restart_requested", "stopping", void 0, true);
@@ -1237,6 +1297,7 @@ var SessionHostServer = class extends EventEmitter {
1237
1297
  true
1238
1298
  );
1239
1299
  if (this.runtimes.has(record.sessionId)) {
1300
+ this.stopRequests.set(record.sessionId, "prune");
1240
1301
  this.registry.setLifecycle(record.sessionId, "stopping");
1241
1302
  this.persistNow(record.sessionId);
1242
1303
  this.requireRuntime(record.sessionId).stop();
@@ -1246,6 +1307,8 @@ var SessionHostServer = class extends EventEmitter {
1246
1307
  }
1247
1308
  this.registry.deleteSession(record.sessionId);
1248
1309
  this.storage.remove(record.sessionId);
1310
+ this.storage.removeTombstone(record.sessionId);
1311
+ this.stopRequests.delete(record.sessionId);
1249
1312
  }
1250
1313
  startRuntime(record, payload, startEventType) {
1251
1314
  const runtime = new PtySessionRuntime({
@@ -1256,22 +1319,7 @@ var SessionHostServer = class extends EventEmitter {
1256
1319
  this.schedulePersist(record.sessionId);
1257
1320
  this.emitEvent({ type: "session_output", sessionId: record.sessionId, seq, data });
1258
1321
  },
1259
- onExit: (exitCode) => {
1260
- this.registry.markStopped(record.sessionId, exitCode === 0 ? "stopped" : "failed");
1261
- this.runtimes.delete(record.sessionId);
1262
- this.resolveExitWaiters(record.sessionId, exitCode);
1263
- this.persistNow(record.sessionId);
1264
- this.emitEvent({ type: "session_exit", sessionId: record.sessionId, exitCode });
1265
- this.recordRuntimeTransition(
1266
- record.sessionId,
1267
- "session_exit",
1268
- exitCode === 0 ? "stopped" : "failed",
1269
- void 0,
1270
- exitCode === 0,
1271
- exitCode === 0 ? void 0 : `exitCode=${exitCode}`
1272
- );
1273
- setTimeout(() => this.storage.remove(record.sessionId), 5e3);
1274
- }
1322
+ onExit: (exitCode, signal) => this.handleRuntimeExit(record, exitCode, signal)
1275
1323
  });
1276
1324
  this.registry.setLifecycle(record.sessionId, "starting");
1277
1325
  const pid = runtime.start();
@@ -1282,6 +1330,58 @@ var SessionHostServer = class extends EventEmitter {
1282
1330
  this.recordRuntimeTransition(record.sessionId, startEventType, startedRecord.lifecycle, `pid=${pid}`, true);
1283
1331
  return startedRecord;
1284
1332
  }
1333
+ /**
1334
+ * Handle a PTY termination: classify the (exitCode, signal) pair, stamp the
1335
+ * record + tombstone, emit exactly one structured diagnostic, and schedule
1336
+ * cleanup of the live persistence file (the tombstone is retained).
1337
+ */
1338
+ handleRuntimeExit(record, exitCode, signal) {
1339
+ const priorRecord = this.registry.getSession(record.sessionId);
1340
+ const termination = classifyTermination({
1341
+ exitCode,
1342
+ signal,
1343
+ osPid: priorRecord?.osPid,
1344
+ previousLifecycle: priorRecord?.lifecycle,
1345
+ lastOutputAt: priorRecord?.lastActivityAt,
1346
+ requestedStop: this.stopRequests.get(record.sessionId),
1347
+ terminatedAt: Date.now()
1348
+ });
1349
+ this.stopRequests.delete(record.sessionId);
1350
+ this.registry.markStopped(record.sessionId, termination.lifecycle, termination);
1351
+ this.runtimes.delete(record.sessionId);
1352
+ this.resolveExitWaiters(record.sessionId, exitCode);
1353
+ this.persistNow(record.sessionId);
1354
+ this.storage.saveTombstone(record.sessionId, termination);
1355
+ this.emitEvent({ type: "session_exit", sessionId: record.sessionId, exitCode, signal, termination });
1356
+ const summary = `reason=${termination.reason} exitCode=${termination.exitCode === null ? "unknown" : termination.exitCode} signal=${termination.signal ?? "none"}`;
1357
+ this.recordHostLog(
1358
+ termination.lifecycle === "stopped" ? "info" : "warn",
1359
+ `session terminated: ${summary}`,
1360
+ record.sessionId,
1361
+ {
1362
+ runtimeKey: record.runtimeKey,
1363
+ providerType: record.providerType,
1364
+ osPid: termination.osPid,
1365
+ exitCode: termination.exitCode,
1366
+ signal: termination.signal,
1367
+ reason: termination.reason,
1368
+ previousLifecycle: termination.previousLifecycle,
1369
+ lifecycle: termination.lifecycle,
1370
+ lastOutputAt: termination.lastOutputAt,
1371
+ requestedStop: termination.requestedStop
1372
+ }
1373
+ );
1374
+ this.recordRuntimeTransition(
1375
+ record.sessionId,
1376
+ "session_exit",
1377
+ termination.lifecycle,
1378
+ summary,
1379
+ termination.lifecycle === "stopped",
1380
+ termination.lifecycle === "stopped" ? void 0 : summary
1381
+ );
1382
+ setTimeout(() => this.storage.remove(record.sessionId), 5e3).unref?.();
1383
+ return termination;
1384
+ }
1285
1385
  };
1286
1386
 
1287
1387
  // src/index.ts