@sema-agent/server 7.70.0 → 7.72.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 (110) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +8 -4
  4. package/dist/approval-card.d.ts +16 -17
  5. package/dist/approval-card.js +0 -9
  6. package/dist/approval-content-kind.d.ts +13 -7
  7. package/dist/approval-content-kind.js +3 -1
  8. package/dist/approval.js +3 -2
  9. package/dist/bench/s1/live-deps.js +2 -0
  10. package/dist/boot/execution-env.js +12 -2
  11. package/dist/boot/memory-consolidation.d.ts +2 -1
  12. package/dist/boot/parked-revive-gate.d.ts +2 -0
  13. package/dist/boot/parked-revive-gate.js +5 -1
  14. package/dist/boot/resolve-spec.js +8 -3
  15. package/dist/boot/runner-deps.d.ts +48 -59
  16. package/dist/boot/runner-deps.js +10 -4
  17. package/dist/brain.d.ts +3 -1
  18. package/dist/brain.js +2 -8
  19. package/dist/budget.d.ts +12 -1
  20. package/dist/budget.js +1 -1
  21. package/dist/config-catalog.js +13 -11
  22. package/dist/config-center/apply-effective.js +2 -4
  23. package/dist/config-center/facade.d.ts +1 -1
  24. package/dist/config-center/restart-signal.js +2 -2
  25. package/dist/config-center/skills-mcp.js +15 -0
  26. package/dist/config-types.d.ts +86 -4
  27. package/dist/config-types.js +19 -0
  28. package/dist/config.d.ts +42 -0
  29. package/dist/config.js +71 -22
  30. package/dist/deployment-governance.d.ts +6 -3
  31. package/dist/deployment-governance.js +2 -1
  32. package/dist/execution-lane-caps.d.ts +51 -37
  33. package/dist/execution-lane-caps.js +23 -0
  34. package/dist/fleet/fleet-bus.d.ts +22 -1
  35. package/dist/fleet/fleet-bus.js +2 -1
  36. package/dist/http/durable-run-leg.d.ts +64 -0
  37. package/dist/http/durable-run-leg.js +63 -0
  38. package/dist/http/route-ctx.d.ts +14 -0
  39. package/dist/http/route-ctx.js +3 -0
  40. package/dist/http/routes/approvals-assistant.js +3 -3
  41. package/dist/http/routes/capabilities.js +2 -2
  42. package/dist/http/routes/runs.js +10 -58
  43. package/dist/http/routes/workflows.js +1 -0
  44. package/dist/http/server.js +72 -22
  45. package/dist/index.d.ts +1 -1
  46. package/dist/leader/wire.js +4 -3
  47. package/dist/main.js +8 -3
  48. package/dist/memory-operator-faces.d.ts +1 -1
  49. package/dist/memory-scope.d.ts +24 -0
  50. package/dist/memory-scope.js +12 -3
  51. package/dist/observability/fail-open.d.ts +1 -1
  52. package/dist/observability/fail-open.js +1 -1
  53. package/dist/observability/metrics.d.ts +4 -0
  54. package/dist/observability/metrics.js +3 -1
  55. package/dist/observability/run-terminal-log.d.ts +4 -3
  56. package/dist/observability/run-terminal-log.js +4 -15
  57. package/dist/observability/tool-trace.d.ts +18 -1
  58. package/dist/observability/tool-trace.js +4 -3
  59. package/dist/orchestration/workflow-agent-session-index.d.ts +85 -2
  60. package/dist/orchestration/workflow-agent-session-index.js +38 -5
  61. package/dist/parked-decide.d.ts +40 -1
  62. package/dist/parked-decide.js +22 -4
  63. package/dist/plugins/checkpoint-store-sql.js +5 -5
  64. package/dist/plugins/e2b-orphan-reclaim.d.ts +4 -4
  65. package/dist/plugins/e2b-orphan-reclaim.js +18 -20
  66. package/dist/plugins/local-checkpoint-store.js +3 -3
  67. package/dist/plugins/remote-env-adb.d.ts +11 -0
  68. package/dist/plugins/remote-env-adb.js +2 -0
  69. package/dist/plugins/remote-env-device.d.ts +9 -0
  70. package/dist/plugins/remote-env-device.js +3 -0
  71. package/dist/plugins/remote-env-e2b.d.ts +11 -0
  72. package/dist/plugins/remote-env-e2b.js +2 -0
  73. package/dist/plugins/remote-env-host.d.ts +11 -0
  74. package/dist/plugins/remote-env-host.js +2 -0
  75. package/dist/plugins/remote-env-k8s.d.ts +11 -0
  76. package/dist/plugins/remote-env-k8s.js +2 -0
  77. package/dist/plugins/remote-env-local-docker.d.ts +11 -0
  78. package/dist/plugins/remote-env-local-docker.js +2 -0
  79. package/dist/plugins/remote-env-ssh.d.ts +11 -0
  80. package/dist/plugins/remote-env-ssh.js +2 -0
  81. package/dist/plugins/store-backend.d.ts +34 -12
  82. package/dist/plugins/store-backend.js +12 -3
  83. package/dist/plugins/store-contracts.d.ts +81 -18
  84. package/dist/plugins/store-contracts.js +18 -30
  85. package/dist/plugins/workflow-run-store-sql.d.ts +31 -9
  86. package/dist/plugins/workflow-run-store-sql.js +28 -1
  87. package/dist/run-cancel-context.js +1 -1
  88. package/dist/run-local.js +7 -1
  89. package/dist/runs.d.ts +16 -2
  90. package/dist/runs.js +2 -1
  91. package/dist/runtime-caps-resolver.js +5 -3
  92. package/dist/task-a2a.js +2 -1
  93. package/dist/task-mcp.js +2 -1
  94. package/dist/task-settings.d.ts +21 -8
  95. package/dist/task-settings.js +2 -2
  96. package/dist/tool-approval.d.ts +26 -21
  97. package/dist/tool-approval.js +2 -2
  98. package/dist/trace/core-keyset-guard.d.ts +136 -10
  99. package/dist/trace/engine-notice-wire.d.ts +1 -1
  100. package/dist/trace/engine-notice-wire.js +1 -0
  101. package/dist/trace/ledger-events.d.ts +5 -1
  102. package/dist/trace/ledger-sink.js +8 -1
  103. package/dist/trace/project.d.ts +33 -4
  104. package/dist/trace/project.js +24 -6
  105. package/dist/trace/redact.js +10 -5
  106. package/dist/usage-analytics.d.ts +9 -1
  107. package/dist/usage-analytics.js +8 -0
  108. package/dist/wire-name-bound.d.ts +14 -0
  109. package/dist/wire-name-bound.js +13 -0
  110. package/package.json +3 -3
@@ -6,13 +6,10 @@ import { isTerminalRunStatus, isParkedRunStatus } from "../../plugins/store-cont
6
6
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
7
7
  import { redactSecrets } from "../../trace/redact.js";
8
8
  import { HttpError, verifiedPrincipal, isUuidV7, encodeCheckpointScope } from "../../security.js";
9
- import { runInBackground } from "../../runs.js";
10
9
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
11
10
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
12
- import { fleetRunPublisher, fleetRunLabels } from "../../fleet/fleet-bus.js";
13
11
  import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../../observability/cost-taxonomy.js";
14
12
  import { recordFailOpen } from "../../observability/fail-open.js";
15
- import { cascadeConfig, runMeta } from "../run-meta.js";
16
13
  import { createHash } from "node:crypto";
17
14
  import { scopedIdempotencyKey } from "../idempotency.js";
18
15
  import { streamSseLog } from "../sse-log.js";
@@ -22,12 +19,12 @@ import { ASK_DECISION_CONSUMER_ABSENT, resolveStreamApprovalGate } from "../../t
22
19
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
23
20
  import { normalizeRunEventType } from "../../trace/project.js";
24
21
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
25
- import { buildActiveRunConflict } from "../active-run-conflict.js";
26
- import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
22
+ import { readTurnActivityMs } from "../../turn-activity.js";
27
23
  import { mintCancelledResult } from "../../run-cancel-context.js";
28
- import { buildCrossSliceUsage, readResourceWindow, recordResourceWindow } from "../../resource-window.js";
24
+ import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
29
25
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
30
26
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
27
+ import { mintAndDriveDurableRun } from "../durable-run-leg.js";
31
28
  import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
32
29
  import { withPrincipal } from "../../observability/principal-context.js";
33
30
  export const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
@@ -1559,58 +1556,13 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1559
1556
  }
1560
1557
  if ((await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
1561
1558
  return null;
1562
- const resp = await idemCache.run(idemKey, async () => {
1563
- const sessionId = prepared.spec.sessionId ?? uuidv7();
1564
- const taskId = clientTaskId ?? uuidv7();
1565
- const created = await runStore.createRun(taskId, sessionId, prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
1566
- if (created.ok)
1567
- clearTurnActivity(taskId);
1568
- if (created.ok)
1569
- recordResourceWindow(taskId, prepared.spec.resourceSuspend);
1570
- if (created.ok)
1571
- deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
1572
- if (!created.ok) {
1573
- if (clientTaskId && created.activeTaskId === clientTaskId) {
1574
- return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
1575
- }
1576
- return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
1577
- }
1578
- if (deps.checkpointStore) {
1579
- await deps.checkpointStore.putCtx(sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
1580
- }
1581
- const anchorOwner = prepared.auth?.principal ?? null;
1582
- const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
1583
- const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
1584
- ? async (eventId) => {
1585
- const leaf = await getLeafId(sessionId);
1586
- if (leaf)
1587
- await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner);
1588
- }
1589
- : undefined;
1590
- const captureUserMessageAnchor = deps.resumeAnchorStore
1591
- ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
1592
- : undefined;
1593
- const fleetPub = fleetRunPublisher(deps.fleetBus, {
1594
- runId: taskId,
1595
- scope: gatedPrincipal(req, deps.config) ?? "default",
1596
- rootTaskId: taskId,
1597
- ...fleetRunLabels(prepared.spec.objective),
1598
- });
1599
- void runInBackground(prepared.runner, { ...prepared.spec, sessionId }, runStore, taskId, deps.metrics, prepared.auth?.principal, prepared.verify, prepared.cascade ? cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd) : undefined, deps.instrumentDegenerate, deps.planCacheProbe, deps.config.traceThinking, inflightRuns, preemptableRuns, steerableRuns, deps.modelUsage, deps.elicitation, gatedPrincipal(req, deps.config) ?? null, captureTurnAnchor, fleetPub, captureUserMessageAnchor, deps.question, deps.workflowCompletionInbox, deps.subagentSteerRegistry, (m, x) => deps.logger?.info?.(m, x), deps.sendUserFile, deps.promptManifests, deps.toolApproval
1600
- ? {
1601
- coordinator: deps.toolApproval,
1602
- streamApprovalOn: resolveStreamApprovalGate({
1603
- toolApprovalEnabled: true,
1604
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
1605
- backend: deps.backend,
1606
- parkFacility: deps.checkpointStore !== undefined,
1607
- }).active,
1608
- windowMs: deps.config.streamApproval?.windowMs,
1609
- windowMarginMs: deps.config.streamAskWindowMarginMs,
1610
- }
1611
- : undefined, deps.checkpointStore, prepared.engineNotices);
1612
- return { status: 202, body: { taskId, sessionId, status: "running" } };
1613
- }, (r) => r.status === 202);
1559
+ const resp = await idemCache.run(idemKey, async () => await mintAndDriveDurableRun({ deps, registry: ctx.registry, helpers: ctx.helpers, local: ctx.local, legs: ctx.legs }, runStore, {
1560
+ prepared,
1561
+ ...(clientTaskId !== undefined ? { clientTaskId } : {}),
1562
+ fleetScope: gatedPrincipal(req, deps.config) ?? "default",
1563
+ elicitOwner: gatedPrincipal(req, deps.config) ?? null,
1564
+ source,
1565
+ }), (r) => r.status === 202);
1614
1566
  return resp;
1615
1567
  }
1616
1568
  //# sourceMappingURL=runs.js.map
@@ -338,6 +338,7 @@ function summarizeWorkflowDetail(run) {
338
338
  durationMs: durationOf(run),
339
339
  rev: run.rev,
340
340
  ...(run.error !== undefined ? { error: redactErrorMessage(run.error) ?? "" } : {}),
341
+ ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
341
342
  ...(run.result !== undefined ? { result: redactSecrets(run.result) } : {}),
342
343
  phases,
343
344
  ...(unphasedView ? { unphased: unphasedView } : {}),
@@ -3,7 +3,7 @@ import { redactErrorMessage, redactHead } from "../observability/run-terminal-lo
3
3
  import { once } from "node:events";
4
4
  import { createHash } from "node:crypto";
5
5
  import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
6
- import { decideParkedAgent, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
6
+ import { decideParkedAgent, denyWorkflowParkIdentityLost, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
7
7
  import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
8
8
  import {} from "../config-center/facade.js";
9
9
  import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isUuidV7, isDestructiveSessionWrite, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../security.js";
@@ -33,7 +33,7 @@ import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
33
33
  import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
34
34
  import {} from "../observability/rate-limit.js";
35
35
  import { withPrincipal } from "../observability/principal-context.js";
36
- import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
36
+ import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, toolRosterDeltaEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
37
37
  import { redactSecrets } from "../trace/redact.js";
38
38
  import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
39
39
  import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js";
@@ -43,6 +43,8 @@ import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
43
43
  export { scopedIdempotencyKey };
44
44
  import { streamSseLog } from "./sse-log.js";
45
45
  import { verifyRoundsFromBody } from "./verify-rounds.js";
46
+ import { acceptedResumingReceipt } from "./route-ctx.js";
47
+ import { mintAndDriveDurableRun } from "./durable-run-leg.js";
46
48
  import { RUN_NOT_FOUND_MESSAGE } from "./route-ctx.js";
47
49
  import { handleCapabilities } from "./routes/capabilities.js";
48
50
  import { handleObservability } from "./routes/observability.js";
@@ -85,6 +87,7 @@ export { coarseStatusForState, errorCodeForExit };
85
87
  import { sendJson, sendError, httpErrorCode, msg, isSseResponse } from "./send.js";
86
88
  import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator } from "./principal-gate.js";
87
89
  import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
90
+ import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
88
91
  export { explicitOperatorOk, isOperator };
89
92
  export function flattenServiceDeps(deps) {
90
93
  const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
@@ -1143,7 +1146,7 @@ export function createHttpServer(rawDeps) {
1143
1146
  };
1144
1147
  }
1145
1148
  const paForGrant = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction : undefined;
1146
- const loadedGrantTool = paForGrant !== undefined && paForGrant.toolName && paForGrant.toolName !== "AskUserQuestion"
1149
+ const loadedGrantTool = paForGrant !== undefined && paForGrant.toolName && paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME
1147
1150
  ? paForGrant.toolName
1148
1151
  : null;
1149
1152
  const boundGrant = onResumeCommitted
@@ -1208,27 +1211,23 @@ export function createHttpServer(rawDeps) {
1208
1211
  noteLateSettleIfAccepted(parked);
1209
1212
  return parked;
1210
1213
  }
1214
+ if (origin?.kind === "workflow_identity_lost") {
1215
+ void ctxPromise.catch(() => { });
1216
+ return denyWorkflowParkIdentityLost(origin);
1217
+ }
1211
1218
  if (origin?.kind === "workflow") {
1212
1219
  void ctxPromise.catch(() => { });
1213
1220
  const plan = planWorkflowParkedDecide(origin, parkedReq);
1214
1221
  if (!plan.ok)
1215
1222
  return plan.deny;
1223
+ const parkedResume = { workflowParkedResume: [{ runId: plan.runId, token, outcome: plan.outcome }] };
1216
1224
  const hostToken = await cs
1217
1225
  .findPendingTokenBySession(plan.hostSessionId, undefined, { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" })
1218
1226
  .catch(() => undefined);
1219
- if (!hostToken) {
1220
- return {
1221
- status: 409,
1222
- body: {
1223
- error: "the workflow's originating session is not parked awaiting this run — the deployment can only deliver the decision on the host's next turn; retry once the host session parks, or resume the workflow run yourself",
1224
- errorCode: "decide.workflow_host_not_parked",
1225
- runId: plan.runId,
1226
- },
1227
- };
1228
- }
1229
- return await resumeWake(plan.hostSessionId, plan.message, decider?.principal, req, acceptEarly, [
1230
- { runId: plan.runId, token, outcome: plan.outcome },
1231
- ]);
1227
+ const admittedPayer = decodeCheckpointScope(cp.scope);
1228
+ return hostToken
1229
+ ? await resumeWake(plan.hostSessionId, plan.message, decider?.principal, req, acceptEarly, parkedResume.workflowParkedResume, admittedPayer)
1230
+ : await startFreshHostTurn(plan.hostSessionId, plan.message, admittedPayer, req, parkedResume, plan.runId);
1232
1231
  }
1233
1232
  }
1234
1233
  let ctx;
@@ -1256,10 +1255,10 @@ export function createHttpServer(rawDeps) {
1256
1255
  throw e;
1257
1256
  }
1258
1257
  const pendingTool = cp.pendingAction.toolName;
1259
- if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
1258
+ if (decision === "approve" && pendingTool === ASK_USER_QUESTION_TOOL_NAME && !answer) {
1260
1259
  return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
1261
1260
  }
1262
- if (answer && (decision !== "approve" || pendingTool !== "AskUserQuestion")) {
1261
+ if (answer && (decision !== "approve" || pendingTool !== ASK_USER_QUESTION_TOOL_NAME)) {
1263
1262
  return { status: 400, body: { error: `body.answer is only valid when approving a pending AskUserQuestion (decision: "${decision}", pending: "${pendingTool ?? "unknown"}")`, errorCode: "request.field_conflict" } };
1264
1263
  }
1265
1264
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
@@ -1281,9 +1280,11 @@ export function createHttpServer(rawDeps) {
1281
1280
  noteLateSettleIfAccepted(driven);
1282
1281
  return driven;
1283
1282
  }
1283
+ const ONE_SHOT_HISTORY_KEYS = ["resumeAt", "restoreFiles", "acceptPartialRestore", "rewindFilesTo", "rewindFiles"];
1284
+ const ONE_SHOT_HISTORY_STRIP = Object.fromEntries(ONE_SHOT_HISTORY_KEYS.map((k) => [k, undefined]));
1284
1285
  const resolveResumeSpec = async (body, req, auth) => {
1285
1286
  const engineNotices = [];
1286
- const spec = await deps.resolveSpec({ ...body, resumeAt: undefined }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
1287
+ const spec = await deps.resolveSpec({ ...body, ...ONE_SHOT_HISTORY_STRIP }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
1287
1288
  return { spec, engineNotices };
1288
1289
  };
1289
1290
  async function driveResumeIntoRunLog(args) {
@@ -1633,7 +1634,7 @@ export function createHttpServer(rawDeps) {
1633
1634
  steerableRuns.set(taskId, stream);
1634
1635
  if (!taskId || !rs)
1635
1636
  return await stream.result();
1636
- onAccepted?.({ status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } });
1637
+ onAccepted?.(acceptedResumingReceipt(taskId, sessionId));
1637
1638
  for await (const ev of stream) {
1638
1639
  recordTurnActivity(taskId);
1639
1640
  fleetPub?.onEvent(ev);
@@ -1691,6 +1692,13 @@ export function createHttpServer(rawDeps) {
1691
1692
  await flush();
1692
1693
  await append("wiring_manifest", wiringManifestEventData(ev));
1693
1694
  break;
1695
+ case "tool_roster_delta": {
1696
+ await flush();
1697
+ const rosterDelta = toolRosterDeltaEventData(ev);
1698
+ if (rosterDelta !== undefined)
1699
+ await append("tool_roster_delta", rosterDelta);
1700
+ break;
1701
+ }
1694
1702
  case "human_input":
1695
1703
  await flush();
1696
1704
  await append("human_input", humanInputEventData(ev));
@@ -1820,7 +1828,7 @@ export function createHttpServer(rawDeps) {
1820
1828
  (e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
1821
1829
  if (claimedRow && taskId && deps.runStore) {
1822
1830
  if (retriable && freshRunClaimed) {
1823
- const c = { taskId, sessionId, terminal: { kind: "failed", code: e.code, message: e.message }, stats: { turns: 0, tokens: 0 } };
1831
+ const c = { taskId, sessionId, terminal: { kind: "failed", code: e.code, message: e.message }, stats: { turns: 0, tokens: 0, usageMissing: true } };
1824
1832
  await deps.runStore.setTerminal(taskId, "failed", c, e.message).catch((err) => {
1825
1833
  deps.logger?.warn?.("wake_synthetic_run_release_failed", { taskId, sessionId, errorCode: e.code, error: err instanceof Error ? err.message : String(err), note: "the synthetic run row for this refused fresh-run resume could not be settled — its task_active claim is still held and the next attempt on this session will 409 conflict.session_active_run until reapStale collects it (runStaleSec)" });
1826
1834
  });
@@ -1975,7 +1983,48 @@ export function createHttpServer(rawDeps) {
1975
1983
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1976
1984
  return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
1977
1985
  }
1978
- async function resumeWake(sessionId, message, caller, req, acceptEarly, workflowParkedResume) {
1986
+ async function startFreshHostTurn(sessionId, objective, principal, req, trustedInternals, runId) {
1987
+ const cs = deps.checkpointStore;
1988
+ if (!deps.runStore) {
1989
+ return { status: 501, body: { error: "delivering a workflow decision on the host's next turn requires a durable run store (DB_BACKEND=mysql|pg|local)", errorCode: "capability.run_store_required" } };
1990
+ }
1991
+ const ctx = await cs.getCtx(sessionId).catch(() => null);
1992
+ if (!ctx) {
1993
+ return {
1994
+ status: 409,
1995
+ body: {
1996
+ error: "the workflow's originating session no longer has the rebuild inputs for a new turn: a session's resume context is garbage-collected once it is stale AND that session itself has no pending checkpoint and no active run (REAP_RUN_STALE_SEC, default 120s) — a workflow CHILD's park does not hold the HOST session's row open. Raise REAP_RUN_STALE_SEC to cover how long approvals may sit before someone decides them, or resume the workflow run yourself with the runId below",
1997
+ errorCode: "conflict.resume_context_unavailable",
1998
+ runId,
1999
+ },
2000
+ };
2001
+ }
2002
+ const auth = { sessionId, principal, memoryScope: ctx.memoryScope };
2003
+ let spec;
2004
+ let engineNotices = [];
2005
+ try {
2006
+ ({ spec, engineNotices } = await resolveResumeSpec(ctx.body, req, auth));
2007
+ }
2008
+ catch (e) {
2009
+ if (e instanceof HttpError) {
2010
+ return { status: 409, body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) } };
2011
+ }
2012
+ throw e;
2013
+ }
2014
+ const hostSpec = { ...spec, objective, sessionId };
2015
+ const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
2016
+ prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
2017
+ fleetScope: resumeFleetScope(req, auth),
2018
+ elicitOwner: principal ?? null,
2019
+ source: "workflow_parked_decide",
2020
+ trustedInternals,
2021
+ });
2022
+ if (out.status !== 202)
2023
+ return { status: out.status, body: out.body };
2024
+ const minted = out.body;
2025
+ return acceptedResumingReceipt(minted.taskId, minted.sessionId);
2026
+ }
2027
+ async function resumeWake(sessionId, message, caller, req, acceptEarly, workflowParkedResume, ownerLeaseAdmittedFor) {
1979
2028
  const cs = deps.checkpointStore;
1980
2029
  const wakeBinding = { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" };
1981
2030
  const token = (await cs.findPendingTokenBySession(sessionId, undefined, wakeBinding)) ?? (await cs.findPendingTokenBySession(sessionId));
@@ -2046,6 +2095,7 @@ export function createHttpServer(rawDeps) {
2046
2095
  ...(acceptEarly === true ? { acceptEarly: true } : {}),
2047
2096
  ...(req !== undefined ? { callerInitiated: true } : {}),
2048
2097
  ...(workflowParkedResume !== undefined ? { workflowParkedResume } : {}),
2098
+ ...(ownerLeaseAdmittedFor !== undefined && auth.principal === ownerLeaseAdmittedFor ? { ownerLeaseAdmitted: true } : {}),
2049
2099
  });
2050
2100
  }
2051
2101
  async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { loadConfig, type ServiceConfig } from "./config.js";
2
2
  export { createBrain } from "./brain.js";
3
3
  export { createSessionStore } from "./plugins/session-store.js";
4
- export { createStoreBackend, type StoreBackend } from "./plugins/store-backend.js";
4
+ export { createStoreBackend, type StoreBackend, type SessionCaptureRecordSource } from "./plugins/store-backend.js";
5
5
  export { CachingSessionStore, type CachingOptions } from "./plugins/caching-session-store.js";
6
6
  export type { RunRecord, RunEvent } from "./plugins/store-contracts.js";
7
7
  export { runInBackground } from "./runs.js";
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
8
8
  import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
9
+ import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
9
10
  import { createDurableAskPolicy } from "../approval.js";
10
11
  import { presignS3Url } from "../plugins/s3-presign.js";
11
12
  import { buildUploadScript, uploadStepSuffix, fetchUploadedDiff, withStaging } from "./diffup.js";
@@ -376,7 +377,7 @@ export function createLeaderRunner(cfg) {
376
377
  }
377
378
  if (!cfg.e2bApiKey)
378
379
  throw new Error("leader wire: no envFactory+s3 (factory mode) and no e2bApiKey (static mode)");
379
- const env = new RemoteContainerExecutionEnv({ apiKey: cfg.e2bApiKey, timeoutMs: leaderTimeoutMs });
380
+ const env = new RemoteContainerExecutionEnv({ apiKey: cfg.e2bApiKey, timeoutMs: leaderTimeoutMs, homeDir: E2B_SANDBOX_HOME });
380
381
  if (!(await env.connect()).ok)
381
382
  throw new Error(`connect failed for ${sub.workerId}`);
382
383
  await sh(env)(`${body.seedCmd}; cd ${repo}; git checkout -q -b ${sub.branch}`);
@@ -410,7 +411,7 @@ export function createLeaderRunner(cfg) {
410
411
  }
411
412
  if (!cfg.e2bApiKey)
412
413
  throw new Error("leader wire: no envFactory and no e2bApiKey");
413
- const env = new RemoteContainerExecutionEnv({ apiKey: cfg.e2bApiKey, timeoutMs: leaderTimeoutMs });
414
+ const env = new RemoteContainerExecutionEnv({ apiKey: cfg.e2bApiKey, timeoutMs: leaderTimeoutMs, homeDir: E2B_SANDBOX_HOME });
414
415
  if (!(await env.connect()).ok) {
415
416
  await env.destroy().catch(() => { });
416
417
  throw new Error("integ connect failed");
@@ -449,7 +450,7 @@ export function createLeaderRunner(cfg) {
449
450
  else {
450
451
  if (!cfg.e2bApiKey)
451
452
  throw new Error("leader wire: repair-loop grader needs an envFactory or e2bApiKey");
452
- const e2b = new RemoteContainerExecutionEnv({ apiKey: cfg.e2bApiKey, timeoutMs: leaderTimeoutMs });
453
+ const e2b = new RemoteContainerExecutionEnv({ apiKey: cfg.e2bApiKey, timeoutMs: leaderTimeoutMs, homeDir: E2B_SANDBOX_HOME });
453
454
  if (!(await e2b.connect()).ok) {
454
455
  await e2b.destroy().catch(() => { });
455
456
  throw new Error("repair-loop grader connect failed");
package/dist/main.js CHANGED
@@ -7,7 +7,7 @@ import { posIntEnv } from "./session-watch.js";
7
7
  import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
8
8
  import { subagentSendUserFileExtraTools } from "./capabilities/send-user-file-tool.js";
9
9
  import { brainSummary } from "./brain.js";
10
- import { loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv, parseNumOrFailNonNegative } from "./config.js";
10
+ import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, resolveBindHost, splitLocalRoots, parseCapEnv, parseNumOrFailNonNegative } from "./config.js";
11
11
  import { buildWriteProtectionPosture } from "./write-protection.js";
12
12
  import { cardRuleScopeRoot } from "./task-cwd.js";
13
13
  import { isPricingConfigured } from "./budget.js";
@@ -129,9 +129,13 @@ async function main() {
129
129
  }
130
130
  const configCenter = await createConfigCenterRuntime({ config, logger, metrics, localRoot });
131
131
  await configCenter.applyLocalRemoteExec();
132
+ const unpricedCeiling = buildUnpricedCeilingNotice(config.models, config);
133
+ if (unpricedCeiling)
134
+ logger.warn(unpricedCeiling.event, unpricedCeiling.fields);
132
135
  const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, sqlEngineFacts, } = await openStores({ config, logger, metrics, localRoot });
133
136
  const instanceId = uuidv7();
134
- const sessionCaptureRecordStore = backend?.sessionCaptureRecords();
137
+ const sessionCaptureRecords = backend?.sessionCaptureRecords();
138
+ const sessionCaptureRecordStore = sessionCaptureRecords?.();
135
139
  const memoryBundleFaces = memoryEngine ? createMemoryBundleFaces(memoryEngine, { ...(sessionCaptureRecordStore !== undefined ? { captureRecordStore: sessionCaptureRecordStore } : {}) }) : undefined;
136
140
  const memoryComplianceFaces = memoryEngine
137
141
  ? createMemoryComplianceFaces(memoryEngine, {
@@ -271,7 +275,7 @@ async function main() {
271
275
  sessionShellGateExplicitlyOff: sessionShellGateRegistry.explicitlyOff,
272
276
  config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
273
277
  pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
274
- sessionCaptureRecordStore,
278
+ sessionCaptureRecords,
275
279
  memorySyncRunner, toolResultStore: runnerOffloadStore, checkpointStore, sessionPolicyStore, runtimeCapsResolver, fileHistoryStore,
276
280
  legacyRewindBoundaryProbe: backend?.legacyRewindBoundaryProbe ? backend.legacyRewindBoundaryProbe.bind(backend) : undefined,
277
281
  permissionRuleStore: permissionRuleProvider,
@@ -330,6 +334,7 @@ async function main() {
330
334
  const subRunnerSessions = new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 }));
331
335
  const subRunnerDeps = {
332
336
  ...createSharedRunnerDeps({
337
+ metrics,
333
338
  config,
334
339
  logger,
335
340
  hands: commitHands,
@@ -227,7 +227,7 @@ export interface SessionMemoryStatusFace {
227
227
  *
228
228
  * `captureRecordStore` = [ref]② 的 SQL 载体(main.ts 单实例,与 Runner/其余引擎同源 —— 全站同批注入律,
229
229
  * 机器钉 capture-optout-lane ②):`captureOptedOut` / `optOutSource` 两键经它读,多副本部署上答的才是
230
- * 跨副本的真记录。缺席(local 车道)⇒ core 文件三腿,单机字节不变。
230
+ * 跨副本的真记录。S-215 起 local 车道在引擎接线时也供(core 文件三腿,同一只控制面,单机字节不变);缺席 ⇒ 记忆面暗。
231
231
  *
232
232
  * 面**不抛**(core 契约:sessionMemoryStatus never throws —— 每个事实源各自 catch 折缺席键);
233
233
  * 不接 onIncident:这条只读路径上今天零 sink 站点,而且缺席键本身就是它的披露形。
@@ -104,6 +104,30 @@ export declare function memoryEngineRootFor(config: Pick<ServiceConfig, "memoryE
104
104
  * 所以它的根必须走这条链而不是 `memoryEngineRootFor` 的分派(codex 修复验证轮 [medium]:分派会把 run-local 的
105
105
  * 记忆搬到 `<localDataRoot>/memory-work`,存量记忆与控制面升级后隐形)。 */
106
106
  export declare function fileMemoryEngineRoot(config: Pick<ServiceConfig, "memoryEngineDir">, fallbackRoot?: string): string;
107
+ /** 本部署 file 记忆引擎的**平面三坐标**(根 / 记忆目录 / 控制面目录)—— {@link fileMemoryPlaneFor} 一次算齐。 */
108
+ export interface FileMemoryPlane {
109
+ readonly root: string;
110
+ readonly memoryDir: string;
111
+ readonly controlDir: string;
112
+ }
113
+ /**
114
+ * S-215(B-085)—— **file 记忆引擎平面的单铸点**,兼这条腿的**接线门**。
115
+ *
116
+ * 为什么是一只函数而不是两处各自推导:平面坐标有**两个**消费者 —— ① {@link memoryEngineBackendFor}
117
+ * 钉给 `FileMemoryEngineBackend` 的 `memoryDir` + `controlDir`;② local 车道的 capture opt-out **记录店**
118
+ * (`StoreBackend.sessionCaptureRecords()` 喂 core 的 `fileSessionCaptureRecordStore(controlDir)`)。
119
+ * 两者必须是**同一只目录**,否则声明写进 A、引擎读 B ⇒ 一条被「受理」的隐私声明在引擎面根本不存在,
120
+ * 那比一句诚实的 409 更坏。两处各写一份推导链 = 哪天根链改了只改一处的必然漂移,所以推导(根 → 记忆目录 →
121
+ * 控制面目录,三步都在这里、只写一遍)与门合并在此(合并复审 R37:此前 `join(root, "memory")` 在本函数与
122
+ * `memoryEngineBackendFor` 各写一份 —— 记忆目录规则有了第二个写者,现已收回一处)。
123
+ *
124
+ * 返回 `undefined` ⇔ 本部署上 file 记忆引擎**不接线**(多租户 / `MEMORY_ENGINE=off` / 平面分裂车道未 allow)。
125
+ * 门与 `memoryEngineBackendFor` 逐字同源(它就读本函数),所以「有引擎 ⇔ 有控制面 ⇔ 供记录店」三者
126
+ * 在构造上不可能分家。
127
+ */
128
+ export declare function fileMemoryPlaneFor(config: ServiceConfig, fallbackRoot?: string): FileMemoryPlane | undefined;
129
+ /** 控制面目录那一坐标(local 车道 capture 记录店的依托)—— 只是 {@link fileMemoryPlaneFor} 的投影,不是第二条推导。 */
130
+ export declare function fileMemoryControlDirFor(config: ServiceConfig, fallbackRoot?: string): string | undefined;
107
131
  export declare function memoryEngineBackendFor(config: ServiceConfig, fallbackRoot?: string): {
108
132
  backend: FileMemoryEngineBackend;
109
133
  root: string;
@@ -68,14 +68,23 @@ export function memoryEngineRootFor(config, fallbackRoot) {
68
68
  export function fileMemoryEngineRoot(config, fallbackRoot) {
69
69
  return resolveMemoryEngineRoot(config.memoryEngineDir ?? fallbackRoot);
70
70
  }
71
- export function memoryEngineBackendFor(config, fallbackRoot) {
71
+ export function fileMemoryPlaneFor(config, fallbackRoot) {
72
72
  if (config.requirePrincipal === true || !config.memoryEngineEnabled)
73
73
  return undefined;
74
74
  if (memoryEngineRemoteLanePosture(config)?.posture === "dark")
75
75
  return undefined;
76
76
  const root = fileMemoryEngineRoot(config, fallbackRoot);
77
- const dir = join(root, "memory");
78
- return { backend: new FileMemoryEngineBackend(dir, { controlDir: deriveControlPlaneDir(root, dir) }), root };
77
+ const memoryDir = join(root, "memory");
78
+ return { root, memoryDir, controlDir: deriveControlPlaneDir(root, memoryDir) };
79
+ }
80
+ export function fileMemoryControlDirFor(config, fallbackRoot) {
81
+ return fileMemoryPlaneFor(config, fallbackRoot)?.controlDir;
82
+ }
83
+ export function memoryEngineBackendFor(config, fallbackRoot) {
84
+ const plane = fileMemoryPlaneFor(config, fallbackRoot);
85
+ if (plane === undefined)
86
+ return undefined;
87
+ return { backend: new FileMemoryEngineBackend(plane.memoryDir, { controlDir: plane.controlDir }), root: plane.root };
79
88
  }
80
89
  export function memorySpecForRequest(scope, memoryWrite, defaultScopes, originPolicy, memoryCapture) {
81
90
  if (!scope)
@@ -19,7 +19,7 @@ export declare const FAIL_OPEN_TAGS: {
19
19
  };
20
20
  readonly "server.trace.gate-outcome-defective": {
21
21
  readonly cls: "F";
22
- readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
22
+ readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;两条耐久读腿(账本回放 `toolResultFieldsOf` / `/decide` 幂等回放 `executionOutcome`)经 `screenedStoredGate` 认过退役词旧拼法后同走这一只筛):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
23
23
  };
24
24
  readonly "server.trace.tool-roster-malformed": {
25
25
  readonly cls: "F";
@@ -10,7 +10,7 @@ export const FAIL_OPEN_TAGS = {
10
10
  },
11
11
  "server.trace.gate-outcome-defective": {
12
12
  cls: "F",
13
- note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`,写腿与两条读腿共用):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
13
+ note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;两条耐久读腿(账本回放 `toolResultFieldsOf` / `/decide` 幂等回放 `executionOutcome`)经 `screenedStoredGate` 认过退役词旧拼法后同走这一只筛):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
14
14
  },
15
15
  "server.trace.tool-roster-malformed": {
16
16
  cls: "F",
@@ -45,6 +45,10 @@ export interface MetricsSummary {
45
45
  rateLimited: number;
46
46
  costQuotaRejected: number;
47
47
  budgetExceeded: Record<string, number>;
48
+ /** 答后撞顶(walltime/turns)的计数。⚠️ 与 {@link budgetExceeded} **口径不同**:本枚在 deps 级通告席上铸,
49
+ * 覆盖**每一条**引擎 run(后台子代 / 委派嵌套腿 / leader 车道);`budgetExceeded` 只由 HTTP 提交腿的
50
+ * `finalizeTaskResult` 递增(= 顶层 HTTP 提交的 run)。两枚相加只在后者那个较窄的总体里成立。 */
51
+ limitCeilingAfterAnswer: Record<string, number>;
48
52
  degraded: Record<string, number>;
49
53
  degenerate: Record<string, number>;
50
54
  memoryConsolidationOps: Record<string, number>;
@@ -174,6 +174,7 @@ export class Metrics {
174
174
  rateLimited: ctotal("rate_limited_total"),
175
175
  costQuotaRejected: ctotal("cost_quota_rejected_total"),
176
176
  budgetExceeded: cby("budget_exceeded_total", "code"),
177
+ limitCeilingAfterAnswer: cby("limit_ceiling_after_answer_total", "origin"),
177
178
  degraded: cby("degraded_total", "reason"),
178
179
  degenerate: cby("degenerate_total", "class"),
179
180
  memoryConsolidationOps: cby("memory_consolidation_ops_total", "op"),
@@ -215,6 +216,7 @@ export function createMetrics() {
215
216
  ]);
216
217
  m.counter("tool_calls_total", "Tool invocations by name and outcome");
217
218
  m.counter("budget_exceeded_total", "Tasks failed by a budget/limit ceiling, by errorCode");
219
+ m.counter("limit_ceiling_after_answer_total", "Runs where a walltime/turns ceiling fired AFTER the run had already answered (engine 7.13.0: these end completed with NO limits.* code). Dual of budget_exceeded_total, but NOT over the same population: this one is minted on the deps-level notice sink so it sees EVERY engine run (background children, delegated/nested legs, leader lanes); budget_exceeded_total is incremented only by the HTTP submit legs finalizeTaskResult, i.e. top-level HTTP-submitted runs. Sum them only within that narrower population; see the forwarder comment, by origin");
218
220
  m.counter("cost_quota_rejected_total", "Tasks refused because the principal is over its cumulative cost quota");
219
221
  m.counter("degraded_total", "Tasks degraded to a cheaper model (1.40), by reason (budget/rate_limit/breaker_open)");
220
222
  m.counter("task_model_not_allowlisted_resume_total", "Resumed legs whose model/compactionModel is outside the @-mention allowlist (allowed to keep running by design — the allowlist gates fresh picks only)");
@@ -261,7 +263,7 @@ export function createMetrics() {
261
263
  m.counter("stale_cache_conflict_total", "Warm session-cache entries evicted by a cross-instance write while cached (S25)");
262
264
  m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
263
265
  m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
264
- m.counter("auto_mode_classifier_unavailable_total", "Auto-mode gate denials caused by a classifier OUTAGE, by cause (core AUTO_MODE_UNAVAILABLE_CAUSES: error/timeout; 'other' = this build cannot name the word). Distinct from permission_denied_total{deniedBy=\"classifier\"}, which also counts genuine safety blocks — a spike here means the classifier itself is failing (gateway/credential fault, or too slow), not that the model kept asking for dangerous things");
266
+ m.counter("auto_mode_classifier_deny_cause_total", "Auto-mode gate denials where the classifier did NOT rule, by the FORM of the deny (core CLASSIFIER_DENY_CAUSES on GateDisposition.cause: 'unavailable' = the classifier could not run at all (threw/rejected/ran past the cap — retryable), 'parse_error' = it ran but answered outside its verdict contract (blocked for safety); 'other' = this build cannot name the word). 🔴 BREAKING in server 7.72.0: this counter was named auto_mode_classifier_unavailable_total through 7.71.0 and is RENAMED with NO alias (a counter that also counts parse_error is not an 'unavailable' counter); its label vocabulary was 'error'/'timeout' (WHY it was unavailable) — those two collapsed into 'unavailable' (the WHY now rides the deny sentence and the auto_mode.classified trace frame), and 'parse_error' is a bucket the old counter could not see, so by-cause series do NOT splice across the upgrade. Distinct from permission_denied_total{deniedBy=\"classifier\"}, which also counts genuine safety blocks (the classifier's own <block> ruling carries NO cause) — a spike here means the classifier itself is not working (gateway/credential fault, too slow, or answering off-contract), not that the model kept asking for dangerous things");
265
267
  m.counter("permission_denied_total", "Tool-gate denials by the LAYER whose verdict is the deny (core's DeniedBy: policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution/persisted_rule; 'other' = this build cannot name the layer; label is `deniedBy` since server 7.64.0 — it was `source` over core's retired PermissionDeniedSource; goal B4 always-on deny meter)");
266
268
  m.counter("permission_rule_events_total", buildPermissionRuleEventsHelp());
267
269
  m.counter("compaction_events_total", "Compaction lifecycle events by outcome (started/completed/failed/skipped) and trigger");
@@ -1,4 +1,4 @@
1
- import type { RunRecord } from "../plugins/store-contracts.js";
1
+ import { type RunRecord } from "../plugins/store-contracts.js";
2
2
  /** 一次终局写口交给观测面的原料(store 侧装配,**未脱敏** —— 脱敏在本模块内做,见 {@link notifyRunTerminal})。 */
3
3
  export interface RunTerminalRecord {
4
4
  /** 这条 run 的账本 id = `GET /v1/runs/:id` 的 id(本仓 run 行主键名为 taskId,同一个值)。 */
@@ -20,8 +20,9 @@ export type RunTerminalObserver = (level: "error" | "info", fields: Record<strin
20
20
  /**
21
21
  * 终局行的级别判据。`undefined` = 不打行。
22
22
  *
23
- * `blocked` 刻意不打:它是**治理判决**(门拒绝了这次执行),不是失败——它有自己的门遥测与
24
- * `GET /v1/runs/:id` 读面,混进 `run_failed` 会把「模型/网络出事了」这条读法冲淡。
23
+ * 一句话:**非成功终局才出声**(`failed` / `blocked`),用户主动取消降 `info`。词的分类不在本模块做 ——
24
+ * 它的唯一属主是 `plugins/store-contracts.ts` 的 `RUN_STATUS_CLASS` 表(S-213③:本模块此前手抄了一张
25
+ * 穷举 switch,并在 `blocked` 那一格给出与 fleet 读面互斥的答案)。表的编译期双向钉在属主处。
25
26
  */
26
27
  export declare function runTerminalLogLevel(status: RunRecord["status"], errorCode: string | undefined): "error" | "info" | undefined;
27
28
  /** 装配点(`main.ts` 唯一)。传 `undefined` 卸载(测试隔离)。 */
@@ -1,25 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { redactSecrets } from "../trace/redact.js";
3
3
  import { isParkLedgerEventType } from "../trace/ledger-events.js";
4
+ import { isUnsuccessfulRunStatus } from "../plugins/store-contracts.js";
4
5
  import { errorText } from "./err-text.js";
5
6
  import { recordFailOpen } from "./fail-open.js";
6
7
  const MESSAGE_CAP = 1024;
7
8
  export function runTerminalLogLevel(status, errorCode) {
8
- switch (status) {
9
- case "failed":
10
- return errorCode === "cancelled" ? "info" : "error";
11
- case "blocked":
12
- case "completed":
13
- case "running":
14
- case "suspended":
15
- case "needs_review":
16
- return undefined;
17
- default: {
18
- const unhandled = status;
19
- void unhandled;
20
- return undefined;
21
- }
22
- }
9
+ if (!isUnsuccessfulRunStatus(status))
10
+ return undefined;
11
+ return errorCode === "cancelled" ? "info" : "error";
23
12
  }
24
13
  let observer;
25
14
  export function setRunTerminalObserver(fn) {
@@ -15,7 +15,8 @@ import type { Metrics } from "./metrics.js";
15
15
  * (`policy/hook/safety/shellGate/planMode/classifier/org`)换成八词
16
16
  * (`policy/hook/org/classifier/plan_mode/compliance/write_protection/ask_resolution`)。仪表盘/告警规则要改。
17
17
  *
18
- * 🔴 **第二个计数器 `auto_mode_classifier_unavailable_total{cause}`(server 7.70.0,core 7.12.0 [ref])**
18
+ * 🔴 **第二个计数器 `auto_mode_classifier_deny_cause_total{cause}`(server 7.70.0 生为
19
+ * `auto_mode_classifier_unavailable_total`,core 7.12.0 [ref];**7.72.0 / core 7.14.0 [ref] 改名、换座、换词**,见本段末)**
19
20
  * —— 分类器**停摆**的常开信号。7.12.0 之前这件事有两条可观测路:熔断跳闸时的
20
21
  * `auto_mode_breaker_open_total`,以及「`unavailable` 的 ask 会走到人面前」(审批 inbox 里看得见)。
21
22
  * 该版把熔断族整族退役、又把 `unavailable` 改成**当场 deny** ⇒ 两条路同时没了,而
@@ -26,6 +27,22 @@ import type { Metrics } from "./metrics.js";
26
27
  * 两个计数刻意**不合并成一个标签**:它们答两个问题(「谁拒的」vs「这次拒是不是因为分类器停摆、哪一类」),
27
28
  * 而运维对停摆要立刻动手(改配置 / 等重试 / 看后端),对安全拦截不要。
28
29
  * ⛔ 纯计量:不改任何裁决、不新开观察面、事实缺席时**不铸**(缺席 ≠ 「分类器好着呢」,只是这次拒不是它)。
30
+ *
31
+ * 🔴 **7.72.0(core 7.14.0 [ref] C-a)—— 同一只计数器换了名、换了座、也换了词表,运维面 BREAKING**:
32
+ * · **名**(合并复审 R2):`auto_mode_classifier_unavailable_total` ⇒ `auto_mode_classifier_deny_cause_total`,
33
+ * **无别名** —— 一只也数 `parse_error`(分类器跑了、答得出格)的计数器再叫 unavailable 就是名不副实;
34
+ * 与下面两条是同一次运维 BREAKING,只痛一次。
35
+ * · **座**:`payload.classifierUnavailable.cause`(整族退役)⇒ `payload.gate.disposition.cause`。同一条
36
+ * 门记录的三张脸(`tool_end.gate` / 本观察者的 `payload.gate` / 行的 `resolvedOutcome`)core 一次铸、
37
+ * 三处投,所以这一位与 `deniedBy` 现在**读自同一只对象**——两个标签不可能再各说各话。
38
+ * · **词表**:`AUTO_MODE_UNAVAILABLE_CAUSES`(`error` / `timeout` = 分类器**为什么**没跑成)
39
+ * ⇒ `CLASSIFIER_DENY_CAUSES`(`unavailable` / `parse_error` = **这次 deny 是什么形**)。
40
+ * ⚠️ 两代不是同一个问题的两组词:`unavailable` 覆盖了旧的 error+timeout 两格(「为什么」降级成散文,
41
+ * 只在 deny 句与 `auto_mode.classified` 诊断帧上),而 `parse_error`(分类器跑了、但答得出格 ⇒ 为安全
42
+ * 起见拦下)是**旧计数器根本数不到**的一格。看板/告警的 `cause=` 过滤要改,按旧词 by-cause 分组的
43
+ * 历史序列与新序列**不可直接拼**。
44
+ * · **计数总量方向**:新座只在「分类器那一轮没有裁决」时在场(`<block>yes` 的真裁决恒缺席),与旧座的
45
+ * 「分类器停摆」同向;多出来的那一格是 `parse_error`,它也确实是「分类器没正常工作」的一种。
29
46
  */
30
47
  export declare function createPermissionDeniedMeter(metrics: Pick<Metrics, "inc">): Hooks;
31
48
  /**