@sema-agent/server 7.76.1 → 7.77.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 (61) hide show
  1. package/USAGE.md +47 -21
  2. package/dist/boot/engine-lease.d.ts +89 -11
  3. package/dist/boot/engine-lease.js +26 -11
  4. package/dist/boot/exit-last.d.ts +24 -0
  5. package/dist/boot/exit-last.js +46 -0
  6. package/dist/boot/shutdown.js +28 -4
  7. package/dist/config-catalog.js +3 -2
  8. package/dist/config-types.d.ts +23 -11
  9. package/dist/config.js +8 -8
  10. package/dist/fleet/subagent-tail-bus.d.ts +28 -2
  11. package/dist/fleet/subagent-tail-bus.js +24 -2
  12. package/dist/http/principal-gate.d.ts +43 -1
  13. package/dist/http/principal-gate.js +23 -1
  14. package/dist/http/routes/a2a-serve.js +2 -2
  15. package/dist/http/routes/admin-config-refresh.js +2 -2
  16. package/dist/http/routes/admin-drain.js +2 -2
  17. package/dist/http/routes/admin-memory-optout.js +2 -2
  18. package/dist/http/routes/adoption.js +2 -2
  19. package/dist/http/routes/approvals-assistant.js +4 -4
  20. package/dist/http/routes/attachments.js +2 -2
  21. package/dist/http/routes/config-catalog.js +2 -2
  22. package/dist/http/routes/devices.js +2 -2
  23. package/dist/http/routes/fleet.js +2 -2
  24. package/dist/http/routes/leader.js +2 -2
  25. package/dist/http/routes/memory-bundle.js +2 -2
  26. package/dist/http/routes/memory-compliance.js +2 -2
  27. package/dist/http/routes/memory-consolidation.js +2 -2
  28. package/dist/http/routes/memory-origin.js +2 -2
  29. package/dist/http/routes/memory-policy.js +2 -2
  30. package/dist/http/routes/notify-wake.js +3 -3
  31. package/dist/http/routes/observability.js +2 -2
  32. package/dist/http/routes/retention-ops.js +2 -2
  33. package/dist/http/routes/run-memory-capture.js +2 -2
  34. package/dist/http/routes/runs.js +13 -13
  35. package/dist/http/routes/sessions.js +53 -12
  36. package/dist/http/routes/side-query.js +2 -2
  37. package/dist/http/routes/tasks.js +14 -10
  38. package/dist/http/routes/workflows.js +3 -3
  39. package/dist/http/server.js +10 -23
  40. package/dist/observability/fail-open.d.ts +4 -0
  41. package/dist/observability/fail-open.js +4 -0
  42. package/dist/parent-watch.d.ts +32 -18
  43. package/dist/parent-watch.js +12 -10
  44. package/dist/parked-decide.js +1 -1
  45. package/dist/plugins/file-run-store.d.ts +6 -1
  46. package/dist/plugins/file-run-store.js +10 -0
  47. package/dist/plugins/memory-run-store.d.ts +6 -1
  48. package/dist/plugins/memory-run-store.js +10 -0
  49. package/dist/plugins/run-store-sql.d.ts +13 -2
  50. package/dist/plugins/run-store-sql.js +5 -0
  51. package/dist/plugins/store-contracts.d.ts +16 -0
  52. package/dist/runs.js +4 -10
  53. package/dist/tool-approval.js +2 -2
  54. package/dist/trace/ledger-sink.js +3 -14
  55. package/dist/trace/model-segment.d.ts +55 -0
  56. package/dist/trace/model-segment.js +58 -0
  57. package/dist/trace/project.d.ts +17 -0
  58. package/dist/trace/project.js +9 -1
  59. package/dist/trace/redact.d.ts +23 -1
  60. package/dist/trace/redact.js +49 -15
  61. package/package.json +1 -1
@@ -23,7 +23,7 @@ import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
23
23
  import { readTurnActivityMs } from "../../turn-activity.js";
24
24
  import { mintCancelledResult } from "../../run-cancel-context.js";
25
25
  import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
26
- import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
26
+ import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
27
27
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
28
28
  import { mintAndDriveDurableRun } from "../durable-run-leg.js";
29
29
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
@@ -82,7 +82,7 @@ export const RUN_TASK_STOP_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/stop$/;
82
82
  async function subagentReadGate(req, res, deps, runId) {
83
83
  const principal = gatedPrincipal(req, deps.config);
84
84
  if (deps.config.requirePrincipal && principal === undefined) {
85
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
85
+ sendPrincipalRefusal(res, req, deps.config);
86
86
  return null;
87
87
  }
88
88
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -197,7 +197,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
197
197
  }
198
198
  const taskId = runMatch[1];
199
199
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
200
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
200
+ sendPrincipalRefusal(res, req, deps.config);
201
201
  return;
202
202
  }
203
203
  const run = await deps.runStore.getRun(taskId);
@@ -279,7 +279,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
279
279
  return;
280
280
  }
281
281
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
282
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
282
+ sendPrincipalRefusal(res, req, deps.config);
283
283
  return;
284
284
  }
285
285
  const taskId = cancelMatch[1];
@@ -407,7 +407,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
407
407
  }
408
408
  const principal = gatedPrincipal(req, deps.config);
409
409
  if (deps.config.requirePrincipal && principal === undefined) {
410
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
410
+ sendPrincipalRefusal(res, req, deps.config);
411
411
  return;
412
412
  }
413
413
  const taskId = steerMatch[1];
@@ -686,7 +686,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
686
686
  }
687
687
  const principal = gatedPrincipal(req, deps.config);
688
688
  if (deps.config.requirePrincipal && principal === undefined) {
689
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
689
+ sendPrincipalRefusal(res, req, deps.config);
690
690
  return;
691
691
  }
692
692
  const taskId = interruptMatch[1];
@@ -832,7 +832,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
832
832
  }
833
833
  const principal = gatedPrincipal(req, deps.config);
834
834
  if (deps.config.requirePrincipal && principal === undefined) {
835
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
835
+ sendPrincipalRefusal(res, req, deps.config);
836
836
  return;
837
837
  }
838
838
  const taskId = compactMatch[1];
@@ -899,7 +899,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
899
899
  }
900
900
  const principal = gatedPrincipal(req, deps.config);
901
901
  if (deps.config.requirePrincipal && principal === undefined) {
902
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
902
+ sendPrincipalRefusal(res, req, deps.config);
903
903
  return;
904
904
  }
905
905
  const taskId = detachMatch[1];
@@ -1117,7 +1117,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1117
1117
  }
1118
1118
  const principal = gatedPrincipal(req, deps.config);
1119
1119
  if (deps.config.requirePrincipal && principal === undefined) {
1120
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
1120
+ sendPrincipalRefusal(res, req, deps.config);
1121
1121
  return;
1122
1122
  }
1123
1123
  const runId = safeDecode(subVerbMatch[1]);
@@ -1214,7 +1214,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1214
1214
  }
1215
1215
  const principal = gatedPrincipal(req, deps.config);
1216
1216
  if (deps.config.requirePrincipal && principal === undefined) {
1217
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
1217
+ sendPrincipalRefusal(res, req, deps.config);
1218
1218
  return;
1219
1219
  }
1220
1220
  const id = elicitMatch[1];
@@ -1240,7 +1240,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1240
1240
  }
1241
1241
  const principal = gatedPrincipal(req, deps.config);
1242
1242
  if (deps.config.requirePrincipal && principal === undefined) {
1243
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
1243
+ sendPrincipalRefusal(res, req, deps.config);
1244
1244
  return;
1245
1245
  }
1246
1246
  const id = questionMatch[1];
@@ -1266,7 +1266,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1266
1266
  }
1267
1267
  const principal = gatedPrincipal(req, deps.config);
1268
1268
  if (deps.config.requirePrincipal && principal === undefined) {
1269
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
1269
+ sendPrincipalRefusal(res, req, deps.config);
1270
1270
  return;
1271
1271
  }
1272
1272
  const id = approvalMatch[1];
@@ -1303,7 +1303,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1303
1303
  }
1304
1304
  const principal = gatedPrincipal(req, deps.config);
1305
1305
  if (deps.config.requirePrincipal && principal === undefined) {
1306
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
1306
+ sendPrincipalRefusal(res, req, deps.config);
1307
1307
  return;
1308
1308
  }
1309
1309
  let rawBody;
@@ -3,14 +3,16 @@ import { a2aForScenario, mcpForScenario } from "../../config-center/facade.js";
3
3
  import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js";
4
4
  import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
+ import { recordFailOpen } from "../../observability/fail-open.js";
6
7
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
7
8
  import { sendJson, sendError, sseHeaders } from "../send.js";
8
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
9
+ import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
9
10
  const MCP_STATUS_TTL_MS = 5_000;
10
11
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
11
12
  const A2A_STATUS_TTL_MS = 5_000;
12
13
  const A2A_MATERIALIZE_TIMEOUT_MS = 10_000;
13
14
  const MEMORY_STATUS_TTL_MS = 5_000;
15
+ const LAST_LEG_LEDGER_TIMEOUT_MS = 2_000;
14
16
  const A2A_PANEL_MAX_TOOL_NAMES = 200;
15
17
  export function createSessionsLocal() {
16
18
  return {
@@ -21,6 +23,44 @@ export function createSessionsLocal() {
21
23
  memoryStatusCache: new Map(),
22
24
  };
23
25
  }
26
+ async function lastLegMcpFor(deps, sessionId, principal) {
27
+ let timer;
28
+ try {
29
+ return await Promise.race([
30
+ readLastLegMcp(deps, sessionId, principal),
31
+ new Promise((r) => { timer = setTimeout(() => { recordFailOpen("server.mcp-panel.last-leg-unreadable", `ledger read exceeded ${LAST_LEG_LEDGER_TIMEOUT_MS}ms`); r(undefined); }, LAST_LEG_LEDGER_TIMEOUT_MS); }),
32
+ ]);
33
+ }
34
+ catch (e) {
35
+ recordFailOpen("server.mcp-panel.last-leg-unreadable", e instanceof Error ? e.message : String(e ?? ""));
36
+ return undefined;
37
+ }
38
+ finally {
39
+ if (timer)
40
+ clearTimeout(timer);
41
+ }
42
+ }
43
+ async function readLastLegMcp(deps, sessionId, principal) {
44
+ const runStore = deps.runStore;
45
+ if (!runStore || sessionId === null)
46
+ return undefined;
47
+ const latest = await runStore.latestRunForSession(sessionId);
48
+ if (latest === undefined)
49
+ return undefined;
50
+ if (!runOwnerReadable(principal, latest.owner, deps.config) && !explicitOperatorOk(principal, deps.config.operatorPrincipals))
51
+ return undefined;
52
+ const events = await runStore.getEvents(latest.taskId, 0);
53
+ for (let i = events.length - 1; i >= 0; i--) {
54
+ const ev = events[i];
55
+ if (ev.type !== "wiring_manifest")
56
+ continue;
57
+ const mcp = ev.data?.mcp;
58
+ if (!Array.isArray(mcp))
59
+ return undefined;
60
+ return { runId: latest.taskId, at: ev.ts, mcp };
61
+ }
62
+ return undefined;
63
+ }
24
64
  export async function handleSessions(req, res, url, ctx) {
25
65
  const miss = { fell: false };
26
66
  await handleSessionsBody(req, res, url, ctx, miss);
@@ -36,7 +76,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
36
76
  if (im && req.method === "GET") {
37
77
  const principal = gatedPrincipal(req, deps.config);
38
78
  if (deps.config.requirePrincipal && !principal) {
39
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
79
+ sendPrincipalRefusal(res, req, deps.config);
40
80
  return;
41
81
  }
42
82
  sendJson(res, 200, {
@@ -52,7 +92,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
52
92
  if (stm && req.method === "GET") {
53
93
  const principal = gatedPrincipal(req, deps.config);
54
94
  if (deps.config.requirePrincipal && !principal) {
55
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
95
+ sendPrincipalRefusal(res, req, deps.config);
56
96
  return;
57
97
  }
58
98
  sendJson(res, 200, {
@@ -71,17 +111,18 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
71
111
  {
72
112
  const mm = /^\/v1\/sessions\/([^/]+)\/mcp$/.exec(url);
73
113
  if (mm && req.method === "GET") {
74
- if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
75
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
114
+ const principal = gatedPrincipal(req, deps.config);
115
+ if (deps.config.requirePrincipal && !principal) {
116
+ sendPrincipalRefusal(res, req, deps.config);
76
117
  return;
77
118
  }
119
+ const lastLegMcp = await lastLegMcpFor(deps, safeDecode(mm[1]), principal);
78
120
  const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
121
+ const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
79
122
  if (!specs || specs.length === 0) {
80
- sendJson(res, 200, { servers: [], asOf: new Date().toISOString() });
123
+ sendJson(res, 200, { servers: [], asOf: new Date().toISOString(), ...(lastLegMcp === undefined ? {} : { lastLegMcp }) });
81
124
  return;
82
125
  }
83
- const principal = gatedPrincipal(req, deps.config);
84
- const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
85
126
  const cached = mcpStatusCache.get(cacheKey);
86
127
  if (!cached || Date.now() - cached.at > MCP_STATUS_TTL_MS) {
87
128
  if (mcpStatusCache.size > 1024)
@@ -114,7 +155,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
114
155
  })();
115
156
  mcpStatusCache.set(cacheKey, { at: Date.now(), panel });
116
157
  }
117
- sendJson(res, 200, await mcpStatusCache.get(cacheKey).panel);
158
+ sendJson(res, 200, { ...(await mcpStatusCache.get(cacheKey).panel), ...(lastLegMcp === undefined ? {} : { lastLegMcp }) });
118
159
  return;
119
160
  }
120
161
  }
@@ -122,7 +163,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
122
163
  const am = /^\/v1\/sessions\/([^/]+)\/a2a$/.exec(url);
123
164
  if (am && req.method === "GET") {
124
165
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
125
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
166
+ sendPrincipalRefusal(res, req, deps.config);
126
167
  return;
127
168
  }
128
169
  const specs = a2aForScenario(deps.config.a2aPeers, deps.config.defaultScenario);
@@ -420,7 +461,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
420
461
  return;
421
462
  }
422
463
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
423
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
464
+ sendPrincipalRefusal(res, req, deps.config);
424
465
  return;
425
466
  }
426
467
  if (!deps.sessionAudit) {
@@ -480,7 +521,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
480
521
  return;
481
522
  }
482
523
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
483
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
524
+ sendPrincipalRefusal(res, req, deps.config);
484
525
  return;
485
526
  }
486
527
  if (!deps.sessionAudit) {
@@ -3,7 +3,7 @@ import { HttpError } from "../../security.js";
3
3
  import { isModelAllowlisted } from "../../model-select.js";
4
4
  import { redactSecrets } from "../../trace/redact.js";
5
5
  import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
6
- import { gatedPrincipal } from "../principal-gate.js";
6
+ import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
7
7
  const CORE_PREFLIGHT_VALIDATION_PREFIXES = ["Unknown model ref ", "No model for role "];
8
8
  export function isCorePreflightValidationError(message) {
9
9
  return CORE_PREFLIGHT_VALIDATION_PREFIXES.some((prefix) => message.startsWith(prefix));
@@ -43,7 +43,7 @@ async function handleSideQueryBody(req, res, url, ctx, miss) {
43
43
  return;
44
44
  const principal = gatedPrincipal(req, deps.config);
45
45
  if (deps.config.requirePrincipal && !principal) {
46
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
46
+ sendPrincipalRefusal(res, req, deps.config);
47
47
  return;
48
48
  }
49
49
  let body;
@@ -4,7 +4,8 @@ import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStat
4
4
  import { stripResumeToken } from "../../terminal.js";
5
5
  import { withPrincipal } from "../../observability/principal-context.js";
6
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
7
- import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-tail-bus.js";
7
+ import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
8
+ import { createReasoningSegments, reasoningStreamOfTask } from "../../trace/model-segment.js";
8
9
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
9
10
  import { createLedgerSink } from "../../trace/ledger-sink.js";
10
11
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
@@ -313,23 +314,21 @@ async function handleTasksBody(req, res, url, ctx, miss) {
313
314
  const streamBody = async () => {
314
315
  await withPrincipal(principal, async () => {
315
316
  const tailContentMode = prepared.spec.forwardSubagentEvents === true ? "on" : "progress_only";
317
+ const childReasoning = createReasoningSegments();
318
+ const topReasoning = createReasoningSegments();
316
319
  const fwdInternals = {
317
320
  onForwardEvent: (e) => {
318
321
  fleetPub?.onForwardEvent(e);
319
322
  ledgerSink?.onForwardEvent(e);
320
323
  {
321
324
  const bg = e.bgAgentId;
322
- if (bg !== undefined) {
323
- defaultSubagentTailBus.noteHandleContentMode(bg, tailContentMode);
324
- if (defaultSubagentTailBus.hasSubscribers(bg)) {
325
- const f = projectTailFrame(e);
326
- if (f)
327
- defaultSubagentTailBus.publish(bg, f);
328
- }
329
- }
325
+ if (bg !== undefined)
326
+ defaultSubagentTailBus.onForwardFrame(bg, tailContentMode, e);
330
327
  }
331
328
  if (res.writableEnded)
332
329
  return;
330
+ for (const end of childReasoning.onEvent(e))
331
+ sseData(res, end);
333
332
  const t = e.type;
334
333
  if (t === "task_progress") {
335
334
  sseData(res, { type: "task_progress", ...taskProgressEventData(e) });
@@ -359,7 +358,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
359
358
  onTaskNotification: (n, opts) => {
360
359
  if (n.task_type === "workflow")
361
360
  return;
362
- defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
361
+ if (!res.writableEnded)
362
+ for (const end of childReasoning.cut(reasoningStreamOfTask(n.task_id, n.sessionId)))
363
+ sseData(res, end);
364
+ defaultSubagentTailBus.forgetHandle(n.task_id);
363
365
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
364
366
  defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
365
367
  }
@@ -393,6 +395,8 @@ async function handleTasksBody(req, res, url, ctx, miss) {
393
395
  for await (const ev of liveStream) {
394
396
  if (closed)
395
397
  break;
398
+ for (const end of ev.type === "done" ? [...childReasoning.cut(), ...topReasoning.cut()] : topReasoning.onEvent(ev))
399
+ sseData(res, end);
396
400
  if (ev.type === "message_committed" && ev.role === "user")
397
401
  userMsgEntryId = ev.entryId;
398
402
  const cancelVerbAtDone = ev.type === "done" && durableTaskId !== undefined && cancelledViaVerb.has(durableTaskId);
@@ -9,7 +9,7 @@ import { redactErrorMessage, redactHead } from "../../observability/run-terminal
9
9
  const JOURNAL_PROJECTION_MEMO_CAP = 1024;
10
10
  const journalProjectionMemo = new Map();
11
11
  import { sendJson, sendError, sseHeaders } from "../send.js";
12
- import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
12
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
13
13
  const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/;
14
14
  const WORKFLOW_AGENT_STEER_RE = /^\/v1\/workflows\/([^/]+)\/agents\/([^/]+)\/steer$/;
15
15
  export async function handleWorkflows(req, res, url, ctx) {
@@ -29,7 +29,7 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
29
29
  if (wfMatch) {
30
30
  const principal = gatedPrincipal(req, deps.config);
31
31
  if (deps.config.requirePrincipal && !principal) {
32
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
32
+ sendPrincipalRefusal(res, req, deps.config);
33
33
  return;
34
34
  }
35
35
  if (!deps.workflowRunStore) {
@@ -163,7 +163,7 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
163
163
  }
164
164
  const principal = gatedPrincipal(req, deps.config);
165
165
  if (deps.config.requirePrincipal && principal === undefined) {
166
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
166
+ sendPrincipalRefusal(res, req, deps.config);
167
167
  return;
168
168
  }
169
169
  const wfRunId = safeDecode(wfSteerMatch[1]);
@@ -26,7 +26,7 @@ import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
26
26
  import {} from "../orchestration/workflow-agent-steer.js";
27
27
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
28
28
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
29
- import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
29
+ import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
30
30
  import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
31
31
  import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
32
32
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
@@ -85,7 +85,7 @@ export { cascadeConfig };
85
85
  import { handleImages, createImagesLocal, coarseStatusForState, errorCodeForExit } from "./routes/images.js";
86
86
  export { coarseStatusForState, errorCodeForExit };
87
87
  import { sendJson, sendError, httpErrorCode, msg, isSseResponse } from "./send.js";
88
- import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator } from "./principal-gate.js";
88
+ import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
89
89
  import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
90
90
  import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
91
91
  export { explicitOperatorOk, isOperator };
@@ -1453,14 +1453,8 @@ export function createHttpServer(rawDeps) {
1453
1453
  fleetPub?.onForwardEvent(e);
1454
1454
  {
1455
1455
  const bg = e.bgAgentId;
1456
- if (bg !== undefined) {
1457
- defaultSubagentTailBus.noteHandleContentMode(bg, verifyTailContentMode);
1458
- if (defaultSubagentTailBus.hasSubscribers(bg)) {
1459
- const f = projectTailFrame(e);
1460
- if (f)
1461
- defaultSubagentTailBus.publish(bg, f);
1462
- }
1463
- }
1456
+ if (bg !== undefined)
1457
+ defaultSubagentTailBus.onForwardFrame(bg, verifyTailContentMode, e);
1464
1458
  }
1465
1459
  if (rsV && taskId)
1466
1460
  verifySink.onForwardEvent(e);
@@ -1474,7 +1468,7 @@ export function createHttpServer(rawDeps) {
1474
1468
  onTaskNotification: (n, opts) => {
1475
1469
  if (n.task_type === "workflow")
1476
1470
  return;
1477
- defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
1471
+ defaultSubagentTailBus.forgetHandle(n.task_id);
1478
1472
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
1479
1473
  defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
1480
1474
  }
@@ -1578,14 +1572,8 @@ export function createHttpServer(rawDeps) {
1578
1572
  fleetPub?.onForwardEvent(e);
1579
1573
  {
1580
1574
  const bg = e.bgAgentId;
1581
- if (bg !== undefined) {
1582
- defaultSubagentTailBus.noteHandleContentMode(bg, tailContentMode);
1583
- if (defaultSubagentTailBus.hasSubscribers(bg)) {
1584
- const f = projectTailFrame(e);
1585
- if (f)
1586
- defaultSubagentTailBus.publish(bg, f);
1587
- }
1588
- }
1575
+ if (bg !== undefined)
1576
+ defaultSubagentTailBus.onForwardFrame(bg, tailContentMode, e);
1589
1577
  }
1590
1578
  if (!rs || !taskId)
1591
1579
  return;
@@ -1604,7 +1592,7 @@ export function createHttpServer(rawDeps) {
1604
1592
  onTaskNotification: (n, opts) => {
1605
1593
  if (n.task_type === "workflow")
1606
1594
  return;
1607
- defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
1595
+ defaultSubagentTailBus.forgetHandle(n.task_id);
1608
1596
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
1609
1597
  defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
1610
1598
  }
@@ -2331,11 +2319,10 @@ export function createHttpServer(rawDeps) {
2331
2319
  function runOwnerOk(req, res, owner, notFoundForm = "run") {
2332
2320
  const principal = gatedPrincipal(req, deps.config);
2333
2321
  if (deps.config.requirePrincipal && !principal) {
2334
- sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
2322
+ sendPrincipalRefusal(res, req, deps.config);
2335
2323
  return false;
2336
2324
  }
2337
- const legacyShellOwner = !deps.config.requirePrincipal && owner === "anon:shell-live" && (principal === undefined || principal === null);
2338
- if (!legacyShellOwner && owner !== null && principal !== owner) {
2325
+ if (!runOwnerReadable(principal, owner, deps.config)) {
2339
2326
  if (notFoundForm === "session")
2340
2327
  sendError(res, 404, "not_found.session", "session not found");
2341
2328
  else
@@ -13,6 +13,10 @@ export declare const FAIL_OPEN_TAGS: {
13
13
  readonly cls: "F";
14
14
  readonly note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。";
15
15
  };
16
+ readonly "server.mcp-panel.last-leg-unreadable": {
17
+ readonly cls: "F";
18
+ readonly note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。";
19
+ };
16
20
  readonly "server.workflow-park-index.sync-failed": {
17
21
  readonly cls: "P-DEBT";
18
22
  readonly note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到,`/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`)——**方向是 fail-closed**(不误配、不伪造、不放行任何东西),丢的是「人能不能把这条批准送出去」这个能力。归 P 类的理由是它决定**车道选择**(缺行 ⇒ 选中 legacy 腿),归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车)。计数非零 = 有 park 因此决不掉,运维要按 detail 里的 runId 去查。";
@@ -4,6 +4,10 @@ export const FAIL_OPEN_TAGS = {
4
4
  cls: "F",
5
5
  note: "S-136 合并重扫确认项(`terminal.ts persistedPlaneOf`,持久 blob 的平面读面):盘上一条 `terminal` 因由形不合(`paused` 缺 gate / 词出闭集)让 core 的 `terminalProjection` 抛 ⇒ 本读面对这一行答**全格缺席**而不是整只 500。放行的最坏后果=一条坏行在列表/舰队读面上显示为无终局词;计数 + probe 带原句,便于按行回溯。",
6
6
  },
7
+ "server.mcp-panel.last-leg-unreadable": {
8
+ cls: "F",
9
+ note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。",
10
+ },
7
11
  "server.workflow-park-index.sync-failed": {
8
12
  cls: "P-DEBT",
9
13
  note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到,`/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`)——**方向是 fail-closed**(不误配、不伪造、不放行任何东西),丢的是「人能不能把这条批准送出去」这个能力。归 P 类的理由是它决定**车道选择**(缺行 ⇒ 选中 legacy 腿),归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车)。计数非零 = 有 park 因此决不掉,运维要按 detail 里的 runId 去查。",
@@ -9,23 +9,31 @@
9
9
  * (core `FileStorageBackend` 的 pidfile,失效判定是 `process.kill(pid,0)` ⇒ **活孤儿是合法持锁人**),
10
10
  * 于是此后每一只新壳起自己的引擎都撞锁 fatal「another instance (pid N) owns this data dir」—— 用户看到
11
11
  * 的是「壳崩过一次以后就再也起不来了」。持锁 pid 不存在时 core 已自动剪陈旧锁,缺的只有「父死、子活」
12
- * 这一形。所以引擎给自己装一只**自查**腿:定期问一句"生我的那个还在吗",不在就**自己**走正常停机。
12
+ * 这一形。所以引擎给自己装一只**自查**腿:定期问一句"生我的那个还在吗"。
13
+ *
14
+ * ## 本腿**不**做什么(S-306,硬 breaking)
15
+ *
16
+ * 它**不判死、不 drain、不退进程**。父没了只说明这台引擎成了**孤儿**,而「孤儿该不该退」是**租约**
17
+ * 的问题:还有没有人附着、有没有在飞的 turn、静默够不够久 —— 那是 `boot/engine-lease.ts` 那一条腿,
18
+ * 孤儿态只是把它的窗从 `ENGINE_LINGER_MS` 换成 `ENGINE_ORPHAN_LINGER_MS`(默认 2s)。
19
+ * 旧出口(父死**即** drain,单拍、零附着感知)在「A 起引擎、B 按 engine.port 复用、A 正常退出」这一形
20
+ * 下会在 B 的 turn 中间把引擎拆掉 —— 自退律从此**只有一条**,本腿退化成它的一个输入。
13
21
  *
14
22
  * ## 机制(逐条都是契约)
15
23
  *
16
24
  * 1. **opt-in**:`SEMA_PARENT_PID` 缺席 ⇒ 整件不装配(`boot/shutdown.ts` 连 `createParentWatch` 都不调)。
17
25
  * 存量部署逐字零变化 —— systemd / launchd / 容器托管的引擎本来就 reparent 到 1,自退是事故。
18
26
  * 坏值(非正整数 / 超 int32)⇒ **启动期拒**,在 `config.ts` 的 `parentPidEnv()`。
19
- * 2. **判死 = 两支合取口一句,任一成立即判死**,每 {@link PARENT_WATCH_INTERVAL_MS} 一拍:
27
+ * 2. **判据 = 两支合取口一句,任一成立即标孤儿**,每 {@link PARENT_WATCH_INTERVAL_MS} 一拍:
20
28
  *
21
29
  * · **强身份支 `reparented`**:`process.ppid` 不再是装配那一刻的那个值。生我的进程没了,内核就把
22
30
  * 我过继给 init(或容器里的 subreaper)—— 这是**内核对亲子关系的直答**,不依赖 pid 的身份。
23
31
  * 判据刻意**不写成 `ppid === 1`**:① 收养者未必是 1(PID 1 是 subreaper 的容器里不是);
24
32
  * ② 壳本身就是 pid 1 的容器形里(`SEMA_PARENT_PID=1`),`=== 1` 会在**开机第一拍**把一台健康
25
- * 引擎判死。「不再是生我的那个」两种形都答得对,而且不需要任何特判。
33
+ * 引擎标成孤儿。「不再是生我的那个」两种形都答得对,而且不需要任何特判。
26
34
  * 🔴 **前提**(codex 复审 R1 [high],验真后修):这一支只有在「被监视的 pid 就是生我的那个」时
27
35
  * 才等价于「它没了」。壳经一层 wrapper spawn 引擎却仍传**自己**的 pid 时,引擎的直接父是
28
- * wrapper —— wrapper 先退会让这一支把一台**壳还活着**的健康引擎连同 in-flight run 一起关掉。
36
+ * wrapper —— wrapper 先退会让这一支把一台**壳还活着**的健康引擎标成孤儿(于是它的租约窗被无故收短)。
29
37
  * 前提在**装配那一刻**就能一次判完(`ppidAtArm === pid`),所以它是**装配期的开关**而不是每拍
30
38
  * 的特判:不成立 ⇒ 强身份支整条不装,只留弱身份支(它在那一形下语义仍然正确),并且 armed 那行
31
39
  * 改打 **warn** —— 覆盖面被降级了不许静默。
@@ -37,16 +45,16 @@
37
45
  *
38
46
  * 强身份支**先判**:它成立时这一拍连探活都不发 —— 弱判据没有资格给一个已经被过继走的进程改口
39
47
  * (僵尸父那一形里 `kill(pid,0)` 恰恰恒成功)。
40
- * 3. **单拍即判,不去抖**。[ref] 定稿是「15s 一拍、连续两拍才判死」(上界 30s),S-292 整条退役:
41
- * 部署面的判据是「壳挂了 ≤5s 内数据根重新可用」,而 `ESRCH` / `ppid` 都不是会抖的读数(两者都是
42
- * 内核的直答,不是采样值)。去抖买不到确定性,只买到 15s 的延迟和一个 30s 长的 pid-复用窗口。
43
- * 4. **判死后走 `drain`**,即 `boot/shutdown.ts` 里 SIGTERM 用的那一条 `drainThenShutdown` —— in-flight
44
- * run 的结算 / park 语义与人工停机**逐字一致**,而且收尾链里的 `backend.close()` 才是把 `LOCK`
45
- * 真正还回去的那一步。本模块自己**从不** `process.exit`。已有停机属主(`isStopped`)时站下不重入。
48
+ * 3. **单拍即判,不去抖**。[ref] 定稿是「15s 一拍、连续两拍才判定」(上界 30s),S-292 整条退役:
49
+ * 部署面的判据是「壳挂了以后数据根要在秒级重新可用」,而 `ESRCH` / `ppid` 都不是会抖的读数(两者
50
+ * 都是内核的直答,不是采样值)。去抖买不到确定性,只买到 15s 的延迟和一个 30s 长的 pid-复用窗口。
51
+ * 4. **命中一次即停探**(父不会复活),出口是 {@link ParentWatchDeps.onOrphaned} —— 装配层据此记下
52
+ * 「孤儿了 + 是哪一支判的」,租约腿下一拍读到它就换窗。本模块自己**从不** drain、**从不**
53
+ * `process.exit`。已有停机属主(`isStopped`)时站下不重入(一次正在进行的优雅停机不必再被标孤儿)。
46
54
  * 5. **interval 必须 unref**:监视腿不得成为"进程本该退出却退不掉"的理由(与 drain tick 刻意不 unref
47
55
  * 的取向相反 —— 那一只 tick 就是停机驱动,这一只只是观察者)。
48
- * 6. **日志分档**([ref] 响亮律):装配 = `info`;判死与探活自坏 = `warn`(非计划内的停机、以及"这条腿
49
- * 此刻是失效的",都不许混在 info 流里)。档位是**闭集** {@link ParentWatchLogLevel},装配层用
56
+ * 6. **日志分档**([ref] 响亮律):装配 = `info`;标孤儿与探活自坏 = `warn`(非计划内的生命周期事件、
57
+ * 以及"这条腿此刻是失效的",都不许混在 info 流里)。档位是**闭集** {@link ParentWatchLogLevel},装配层用
50
58
  * `Record<ParentWatchLogLevel, …>` 派发 ⇒ 新增一档是编译错,不会静默落进 info。
51
59
  *
52
60
  * ## 已知残余(成文,现版不收窄)
@@ -64,8 +72,9 @@
64
72
  export declare const PARENT_WATCH_INTERVAL_MS = 2000;
65
73
  /** 日志档位闭集。装配层按 `Record<ParentWatchLogLevel, …>` 派发 —— 新增一档必须在那里显式安置(编译错)。 */
66
74
  export type ParentWatchLogLevel = "info" | "warn";
67
- /** 判死缘由闭集(上 `parent_watch_exit` 的 `reason` 键,运维按它区分两支)。 */
68
- export type ParentDeathReason = "reparented" | "esrch";
75
+ /** 成孤儿的缘由闭集(上 `parent_watch_orphaned` 的 `reason` 键,运维按它区分两支;也是租约自退留痕
76
+ * `exit-last.json` 与 `engine_lease_auto_exit` 事件里 `reason` 的孤儿那两个词)。 */
77
+ export type ParentOrphanReason = "reparented" | "esrch";
69
78
  export interface ParentWatchDeps {
70
79
  /** 被监视的父 pid(`config.parentPid`,已在 config 层校验为正整数)。 */
71
80
  pid: number;
@@ -75,11 +84,16 @@ export interface ParentWatchDeps {
75
84
  /** 亲子关系 seam:缺省 = `process.ppid`(**活读**,不是 boot 时的快照 —— 实测过继后当场变)。
76
85
  * 存在的理由与 `probe` 相同:reparent 在进程内测试里没有确定性的触发法。 */
77
86
  readPpid?: () => number;
78
- /** 已有停机属主(`closing || draining`)时监视腿站下:一次正在进行的优雅 drain 不该被"父也没了"
79
- * 升级成第二信号硬停(那会斩掉 in-flight leg —— 与本件"和人工停机逐字一致"的取向相反)。 */
87
+ /** 已有停机属主(`closing || draining`)时监视腿站下:停机已经在走了,再标一次孤儿改变不了任何事,
88
+ * 只会给一次计划内的停机添一条误导的 warn。 */
80
89
  isStopped: () => boolean;
81
- /** 判死出口 = SIGTERM 那条 graceful drain。 */
82
- drain: () => void;
90
+ /**
91
+ * **唯一出口**(S-306):父不在了 ⇒ 把这台引擎标成孤儿。**至多调用一次**(命中即停探)。
92
+ *
93
+ * 🔴 它**不是** drain:退不退由 `boot/engine-lease.ts` 的租约律按「无附着 ∧ 无在飞 ∧ 静默满
94
+ * `orphanLingerMs`」判。装配层拿到 reason 之后要做的只有一件事 —— 记下孤儿态(时刻 + 缘由)。
95
+ */
96
+ onOrphaned: (reason: ParentOrphanReason) => void;
83
97
  log: (level: ParentWatchLogLevel, event: string, fields: Record<string, unknown>) => void;
84
98
  }
85
99
  /** 装配并**立刻**起监视(装配即 `parent_watch_armed`)。返回停机链用的 `stop()`。 */
@@ -12,11 +12,11 @@ export function createParentWatch(deps) {
12
12
  directParent,
13
13
  intervalMs: PARENT_WATCH_INTERVAL_MS,
14
14
  note: directParent
15
- ? "engine watches this pid; it enters the same graceful drain as SIGTERM as soon as EITHER the engine is " +
16
- "reparented away from it OR process.kill(pid,0) reports ESRCH (EPERM = alive). " +
17
- "Unset SEMA_PARENT_PID to disable."
15
+ ? "engine watches this pid; it marks ITSELF an orphan as soon as EITHER the engine is reparented away from " +
16
+ "it OR process.kill(pid,0) reports ESRCH (EPERM = alive). Being orphaned does NOT shut the engine down — " +
17
+ "it only shortens the attachment-lease window to ENGINE_ORPHAN_LINGER_MS. Unset SEMA_PARENT_PID to disable."
18
18
  : "SEMA_PARENT_PID is NOT this engine's direct parent (ppid differs at arm time) — a wrapper process sits " +
19
- "in between. The reparent arm is therefore NOT armed (it would stop a healthy engine when the wrapper " +
19
+ "in between. The reparent arm is therefore NOT armed (it would orphan a healthy engine when the wrapper " +
20
20
  "exits while the watched process is still alive); only the process.kill(pid,0)/ESRCH arm is active, so a " +
21
21
  "watched parent that exits WITHOUT being reaped (zombie) will not be detected. Spawn the engine directly " +
22
22
  "from the process whose pid you pass.",
@@ -30,7 +30,7 @@ export function createParentWatch(deps) {
30
30
  }
31
31
  const ppid = readPpid();
32
32
  if (directParent && ppid !== ppidAtArm) {
33
- fire("reparented", { ppidAtArm, ppid });
33
+ markOrphaned("reparented", { ppidAtArm, ppid });
34
34
  return;
35
35
  }
36
36
  try {
@@ -39,7 +39,7 @@ export function createParentWatch(deps) {
39
39
  catch (err) {
40
40
  const code = err?.code;
41
41
  if (code === "ESRCH") {
42
- fire("esrch", { ppid });
42
+ markOrphaned("esrch", { ppid });
43
43
  return;
44
44
  }
45
45
  if (code !== "EPERM" && !probeErrorReported) {
@@ -56,15 +56,17 @@ export function createParentWatch(deps) {
56
56
  }
57
57
  }, PARENT_WATCH_INTERVAL_MS);
58
58
  timer.unref?.();
59
- function fire(reason, fields) {
59
+ function markOrphaned(reason, fields) {
60
60
  stop();
61
- deps.log("warn", "parent_watch_exit", {
61
+ deps.log("warn", "parent_watch_orphaned", {
62
62
  pid: deps.pid,
63
63
  reason,
64
64
  ...fields,
65
- note: "the parent that spawned this engine is gone — entering the SIGTERM graceful drain path",
65
+ note: "the parent that spawned this engine is gone — this engine is now an ORPHAN. It does NOT shut down here: " +
66
+ "it stays up while anything is attached or in flight, and self-exits via the SIGTERM graceful drain once " +
67
+ "the attachment lease has been idle for ENGINE_ORPHAN_LINGER_MS (see engine_lease_auto_exit).",
66
68
  });
67
- deps.drain();
69
+ deps.onOrphaned(reason);
68
70
  }
69
71
  function stop() {
70
72
  if (stopped)