@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.
- package/USAGE.md +47 -21
- package/dist/boot/engine-lease.d.ts +89 -11
- package/dist/boot/engine-lease.js +26 -11
- package/dist/boot/exit-last.d.ts +24 -0
- package/dist/boot/exit-last.js +46 -0
- package/dist/boot/shutdown.js +28 -4
- package/dist/config-catalog.js +3 -2
- package/dist/config-types.d.ts +23 -11
- package/dist/config.js +8 -8
- package/dist/fleet/subagent-tail-bus.d.ts +28 -2
- package/dist/fleet/subagent-tail-bus.js +24 -2
- package/dist/http/principal-gate.d.ts +43 -1
- package/dist/http/principal-gate.js +23 -1
- package/dist/http/routes/a2a-serve.js +2 -2
- package/dist/http/routes/admin-config-refresh.js +2 -2
- package/dist/http/routes/admin-drain.js +2 -2
- package/dist/http/routes/admin-memory-optout.js +2 -2
- package/dist/http/routes/adoption.js +2 -2
- package/dist/http/routes/approvals-assistant.js +4 -4
- package/dist/http/routes/attachments.js +2 -2
- package/dist/http/routes/config-catalog.js +2 -2
- package/dist/http/routes/devices.js +2 -2
- package/dist/http/routes/fleet.js +2 -2
- package/dist/http/routes/leader.js +2 -2
- package/dist/http/routes/memory-bundle.js +2 -2
- package/dist/http/routes/memory-compliance.js +2 -2
- package/dist/http/routes/memory-consolidation.js +2 -2
- package/dist/http/routes/memory-origin.js +2 -2
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/notify-wake.js +3 -3
- package/dist/http/routes/observability.js +2 -2
- package/dist/http/routes/retention-ops.js +2 -2
- package/dist/http/routes/run-memory-capture.js +2 -2
- package/dist/http/routes/runs.js +13 -13
- package/dist/http/routes/sessions.js +53 -12
- package/dist/http/routes/side-query.js +2 -2
- package/dist/http/routes/tasks.js +14 -10
- package/dist/http/routes/workflows.js +3 -3
- package/dist/http/server.js +10 -23
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/parent-watch.d.ts +32 -18
- package/dist/parent-watch.js +12 -10
- package/dist/parked-decide.js +1 -1
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +10 -0
- package/dist/plugins/memory-run-store.d.ts +6 -1
- package/dist/plugins/memory-run-store.js +10 -0
- package/dist/plugins/run-store-sql.d.ts +13 -2
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/store-contracts.d.ts +16 -0
- package/dist/runs.js +4 -10
- package/dist/tool-approval.js +2 -2
- package/dist/trace/ledger-sink.js +3 -14
- package/dist/trace/model-segment.d.ts +55 -0
- package/dist/trace/model-segment.js +58 -0
- package/dist/trace/project.d.ts +17 -0
- package/dist/trace/project.js +9 -1
- package/dist/trace/redact.d.ts +23 -1
- package/dist/trace/redact.js +49 -15
- package/package.json +1 -1
package/dist/http/routes/runs.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
const wfRunId = safeDecode(wfSteerMatch[1]);
|
package/dist/http/server.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2322
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
2335
2323
|
return false;
|
|
2336
2324
|
}
|
|
2337
|
-
|
|
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 去查。",
|
package/dist/parent-watch.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* 4.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
49
|
-
*
|
|
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
|
-
/**
|
|
68
|
-
|
|
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`)
|
|
79
|
-
*
|
|
87
|
+
/** 已有停机属主(`closing || draining`)时监视腿站下:停机已经在走了,再标一次孤儿改变不了任何事,
|
|
88
|
+
* 只会给一次计划内的停机添一条误导的 warn。 */
|
|
80
89
|
isStopped: () => boolean;
|
|
81
|
-
/**
|
|
82
|
-
|
|
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()`。 */
|
package/dist/parent-watch.js
CHANGED
|
@@ -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
|
|
16
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
59
|
+
function markOrphaned(reason, fields) {
|
|
60
60
|
stop();
|
|
61
|
-
deps.log("warn", "
|
|
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 —
|
|
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.
|
|
69
|
+
deps.onOrphaned(reason);
|
|
68
70
|
}
|
|
69
71
|
function stop() {
|
|
70
72
|
if (stopped)
|