@sema-agent/server 7.108.0 → 7.110.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 (109) hide show
  1. package/MIGRATION.md +60 -1
  2. package/README.md +8 -0
  3. package/README.zh-CN.md +5 -0
  4. package/USAGE.md +19 -29
  5. package/dist/approval-ask-machine.d.ts +9 -25
  6. package/dist/approval-ask-machine.js +5 -2
  7. package/dist/approval-card.d.ts +33 -3
  8. package/dist/approval-card.js +26 -2
  9. package/dist/bench/s1/live-deps.d.ts +4 -5
  10. package/dist/bench/s1/live-deps.js +9 -10
  11. package/dist/boot/leader.js +0 -2
  12. package/dist/boot/reapers.d.ts +3 -0
  13. package/dist/boot/reapers.js +3 -2
  14. package/dist/boot/resolve-spec.d.ts +1 -1
  15. package/dist/boot/resolve-spec.js +6 -12
  16. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  17. package/dist/boot/stage-07-capability-layer.js +17 -3
  18. package/dist/boot/stage-08-reapers.d.ts +1 -0
  19. package/dist/boot/stage-08-reapers.js +2 -2
  20. package/dist/boot/stage-09-leader.d.ts +1 -0
  21. package/dist/boot/stage-10-http-server.d.ts +1 -0
  22. package/dist/boot/stage-10-http-server.js +2 -1
  23. package/dist/capabilities/scenarios.d.ts +3 -2
  24. package/dist/config-catalog.js +2 -4
  25. package/dist/config-provider.js +1 -0
  26. package/dist/config-types.d.ts +5 -11
  27. package/dist/config.js +21 -12
  28. package/dist/deployment-governance.d.ts +8 -2
  29. package/dist/deployment-governance.js +9 -8
  30. package/dist/hooks/hook-runner.d.ts +2 -2
  31. package/dist/hooks/hook-runner.js +40 -26
  32. package/dist/http/admission.js +8 -2
  33. package/dist/http/durable-run-leg.js +1 -1
  34. package/dist/http/resume-legs.js +6 -1
  35. package/dist/http/route-ctx.d.ts +3 -3
  36. package/dist/http/routes/capabilities.js +1 -0
  37. package/dist/http/routes/runs.d.ts +0 -1
  38. package/dist/http/routes/runs.js +22 -0
  39. package/dist/http/routes/sessions.js +12 -2
  40. package/dist/http/routes/tasks.js +5 -1
  41. package/dist/http/server.d.ts +7 -2
  42. package/dist/http/wire-types.d.ts +8 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +1 -0
  45. package/dist/instruction-source-notice.d.ts +3 -3
  46. package/dist/leader/diffup.d.ts +53 -0
  47. package/dist/leader/diffup.js +95 -1
  48. package/dist/leader/leader.d.ts +0 -6
  49. package/dist/leader/leader.js +1 -1
  50. package/dist/leader/repair-wire.js +1 -1
  51. package/dist/leader/wire.d.ts +3 -10
  52. package/dist/leader/wire.js +39 -13
  53. package/dist/memory-layer-legacy-lock.d.ts +3 -3
  54. package/dist/model-entry-headers.d.ts +5 -2
  55. package/dist/model-entry-headers.js +4 -4
  56. package/dist/model-unreachable-notice.d.ts +2 -2
  57. package/dist/observability/corrupt-read-seat.d.ts +6 -0
  58. package/dist/observability/corrupt-read-seat.js +6 -0
  59. package/dist/observability/fail-open.d.ts +13 -5
  60. package/dist/observability/fail-open.js +13 -5
  61. package/dist/observability/metrics.js +2 -1
  62. package/dist/observability/run-terminal.js +10 -0
  63. package/dist/operator-ask.d.ts +9 -8
  64. package/dist/operator-ask.js +9 -1
  65. package/dist/plugins/approval-ask-store-file.d.ts +2 -1
  66. package/dist/plugins/approval-ask-store-file.js +3 -0
  67. package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
  68. package/dist/plugins/approval-ask-store-memory.js +7 -1
  69. package/dist/plugins/approval-ask-store-sql.d.ts +18 -0
  70. package/dist/plugins/approval-ask-store-sql.js +7 -0
  71. package/dist/plugins/file-run-store.js +2 -2
  72. package/dist/plugins/memory-run-store.d.ts +1 -1
  73. package/dist/plugins/memory-run-store.js +2 -1
  74. package/dist/plugins/pg-pool.js +15 -0
  75. package/dist/plugins/run-store-sql.d.ts +1 -1
  76. package/dist/plugins/run-store-sql.js +3 -2
  77. package/dist/plugins/schema-indexes.js +2 -0
  78. package/dist/plugins/store-backend.d.ts +4 -0
  79. package/dist/plugins/store-backend.js +8 -0
  80. package/dist/plugins/store-contracts.d.ts +7 -0
  81. package/dist/plugins/store-contracts.js +1 -0
  82. package/dist/plugins/tidb-pool.js +17 -0
  83. package/dist/plugins/user-input-ledger-file.d.ts +33 -0
  84. package/dist/plugins/user-input-ledger-file.js +98 -0
  85. package/dist/plugins/user-input-ledger-sql.d.ts +44 -0
  86. package/dist/plugins/user-input-ledger-sql.js +101 -0
  87. package/dist/request-key-closure.js +1 -1
  88. package/dist/run-local.d.ts +18 -5
  89. package/dist/run-local.js +26 -12
  90. package/dist/runs.d.ts +8 -1
  91. package/dist/runs.js +6 -2
  92. package/dist/runtime-governance.d.ts +23 -11
  93. package/dist/runtime-governance.js +7 -12
  94. package/dist/session-records-skipped-notice.d.ts +4 -3
  95. package/dist/task-settings.d.ts +0 -6
  96. package/dist/task-settings.js +0 -1
  97. package/dist/tool-approval.d.ts +44 -15
  98. package/dist/tool-approval.js +12 -8
  99. package/dist/trace/core-keyset-guard.d.ts +17 -2
  100. package/dist/trace/engine-notice-wire.d.ts +30 -35
  101. package/dist/trace/engine-notice-wire.js +23 -23
  102. package/dist/trace/project.js +2 -3
  103. package/dist/trace/self-minted-wire.d.ts +14 -0
  104. package/dist/trace/self-minted-wire.js +6 -0
  105. package/dist/unattended-permission-mode.d.ts +54 -93
  106. package/dist/unattended-permission-mode.js +8 -61
  107. package/dist/user-input-ledger.d.ts +296 -0
  108. package/dist/user-input-ledger.js +337 -0
  109. package/package.json +4 -4
@@ -648,12 +648,12 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
648
648
  }
649
649
  return results;
650
650
  }
651
- function basePayload(ctx) {
651
+ function basePayload(ctx, permissionMode) {
652
652
  return {
653
653
  session_id: ctx.sessionId,
654
654
  transcript_path: "",
655
655
  cwd: ctx.cwd,
656
- ...(ctx.permissionMode ? { permission_mode: ctx.permissionMode } : {}),
656
+ ...(permissionMode !== undefined ? { permission_mode: permissionMode } : {}),
657
657
  };
658
658
  }
659
659
  function ignoreMatchers(groups) {
@@ -732,7 +732,7 @@ export function createTaskHooks(config, ctx) {
732
732
  if (!m)
733
733
  continue;
734
734
  const payload = {
735
- ...basePayload(ctx),
735
+ ...basePayload(ctx, tctx.permissionMode),
736
736
  hook_event_name: "PreToolUse",
737
737
  tool_name: toolName,
738
738
  tool_input: currentInput,
@@ -827,7 +827,7 @@ export function createTaskHooks(config, ctx) {
827
827
  .map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
828
828
  .join("");
829
829
  const payload = {
830
- ...basePayload(ctx),
830
+ ...basePayload(ctx, tctx.permissionMode),
831
831
  hook_event_name: "PostToolUse",
832
832
  tool_name: toolName,
833
833
  tool_input: input,
@@ -876,7 +876,7 @@ export function createTaskHooks(config, ctx) {
876
876
  const postToolUseFailureHook = async (toolName, input, f, tctx) => {
877
877
  const contexts = [];
878
878
  const payload = {
879
- ...basePayload(ctx),
879
+ ...basePayload(ctx, tctx.permissionMode),
880
880
  hook_event_name: "PostToolUseFailure",
881
881
  tool_name: toolName,
882
882
  tool_input: input,
@@ -919,10 +919,10 @@ export function createTaskHooks(config, ctx) {
919
919
  hooks.postToolUseFailure = postToolUseFailureHook;
920
920
  }
921
921
  if (postBatch) {
922
- const postToolBatchHook = async (calls) => {
922
+ const postToolBatchHook = async (calls, _meta, bctx) => {
923
923
  const contexts = [];
924
924
  const payload = {
925
- ...basePayload(ctx),
925
+ ...basePayload(ctx, bctx?.permissionMode),
926
926
  hook_event_name: "PostToolBatch",
927
927
  tool_calls: calls.map((c) => ({
928
928
  tool_name: c.toolName,
@@ -962,9 +962,9 @@ export function createTaskHooks(config, ctx) {
962
962
  hooks.postToolBatch = postToolBatchHook;
963
963
  }
964
964
  if (promptSubmit) {
965
- const userPromptSubmitHook = async (prompt) => {
965
+ const userPromptSubmitHook = async (prompt, uctx) => {
966
966
  const contexts = [];
967
- const payload = { ...basePayload(ctx), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
967
+ const payload = { ...basePayload(ctx, uctx?.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
968
968
  const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
969
969
  for (const { entry, run } of singles) {
970
970
  if (run.code === 2) {
@@ -1003,7 +1003,7 @@ export function createTaskHooks(config, ctx) {
1003
1003
  }
1004
1004
  if (stopEntries) {
1005
1005
  const stopHook = async (sctx) => {
1006
- const payload = { ...basePayload(ctx), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1006
+ const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1007
1007
  let llmExtra;
1008
1008
  const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
1009
1009
  if (wantsPrompt) {
@@ -1097,7 +1097,7 @@ export function createTaskHooks(config, ctx) {
1097
1097
  const stopFailureHook = async (fctx) => {
1098
1098
  const turnError = toAssistantTurnError(fctx.errorKind);
1099
1099
  const payload = {
1100
- ...basePayload(ctx),
1100
+ ...basePayload(ctx, undefined),
1101
1101
  hook_event_name: "StopFailure",
1102
1102
  error: turnError,
1103
1103
  ...(fctx.error ? { error_details: clip(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
@@ -1109,7 +1109,7 @@ export function createTaskHooks(config, ctx) {
1109
1109
  if (preCompactEntries) {
1110
1110
  const preCompactHook = async (pctx) => {
1111
1111
  const payload = {
1112
- ...basePayload(ctx),
1112
+ ...basePayload(ctx, undefined),
1113
1113
  hook_event_name: "PreCompact",
1114
1114
  trigger: pctx.trigger,
1115
1115
  custom_instructions: pctx.customInstructions ?? null,
@@ -1143,7 +1143,7 @@ export function createTaskHooks(config, ctx) {
1143
1143
  if (postCompactEntries) {
1144
1144
  const postCompactHook = async (cctx) => {
1145
1145
  const payload = {
1146
- ...basePayload(ctx),
1146
+ ...basePayload(ctx, undefined),
1147
1147
  hook_event_name: "PostCompact",
1148
1148
  trigger: cctx.trigger,
1149
1149
  compact_summary: clip(cctx.summary, MAX_TOOL_RESPONSE_CHARS),
@@ -1191,16 +1191,30 @@ export function composeHooks(deployment, task) {
1191
1191
  if (!slot)
1192
1192
  continue;
1193
1193
  const r = await slot(toolName, current, ctx);
1194
- if (!r)
1194
+ if (r === undefined || Object.is(r, null) || Object.is(r, false))
1195
1195
  continue;
1196
+ if (typeof r !== "object")
1197
+ return r;
1196
1198
  if (r.additionalContext)
1197
1199
  contexts.push(r.additionalContext);
1198
- if (r.action === "deny")
1199
- return { ...r, ...buildHookContextField(contexts) };
1200
- if (r.action === "ask")
1201
- asked = r;
1202
- if (r.action === "allow" && r.updatedInput !== undefined) {
1203
- current = r.updatedInput;
1200
+ const action = r.action;
1201
+ const updatedInput = r.updatedInput;
1202
+ switch (action) {
1203
+ case "deny":
1204
+ return { ...r, ...buildHookContextField(contexts) };
1205
+ case "ask":
1206
+ asked = r;
1207
+ break;
1208
+ case "allow":
1209
+ case undefined:
1210
+ if (updatedInput !== undefined)
1211
+ current = updatedInput;
1212
+ break;
1213
+ default: {
1214
+ const unread = action;
1215
+ void unread;
1216
+ return r;
1217
+ }
1204
1218
  }
1205
1219
  }
1206
1220
  if (asked) {
@@ -1245,20 +1259,20 @@ export function composeHooks(deployment, task) {
1245
1259
  };
1246
1260
  }
1247
1261
  if (d.postToolBatch || t.postToolBatch) {
1248
- out.postToolBatch = async (calls) => {
1249
- const rd = d.postToolBatch ? await d.postToolBatch(calls) : undefined;
1250
- const rt = t.postToolBatch ? await t.postToolBatch(calls) : undefined;
1262
+ out.postToolBatch = async (calls, meta, bctx) => {
1263
+ const rd = d.postToolBatch ? await d.postToolBatch(calls, meta, bctx) : undefined;
1264
+ const rt = t.postToolBatch ? await t.postToolBatch(calls, meta, bctx) : undefined;
1251
1265
  const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1252
1266
  const additionalContext = buildHookContext(contexts);
1253
1267
  return additionalContext === undefined ? undefined : { additionalContext };
1254
1268
  };
1255
1269
  }
1256
1270
  if (d.userPromptSubmit || t.userPromptSubmit) {
1257
- out.userPromptSubmit = async (prompt) => {
1258
- const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt) : undefined;
1271
+ out.userPromptSubmit = async (prompt, uctx) => {
1272
+ const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt, uctx) : undefined;
1259
1273
  if (rd?.block)
1260
1274
  return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1261
- const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt) : undefined;
1275
+ const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt, uctx) : undefined;
1262
1276
  if (!rd && !rt)
1263
1277
  return undefined;
1264
1278
  const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
@@ -458,8 +458,14 @@ export function createAdmission(ctx) {
458
458
  sendError(res, 400, "request.field_invalid", "retainBackgroundProcesses must be a boolean");
459
459
  return null;
460
460
  }
461
- if (body.excludeAllTools !== undefined && typeof body.excludeAllTools !== "boolean") {
462
- sendError(res, 400, "request.field_invalid", "excludeAllTools must be a boolean");
461
+ for (const key of ["excludeAllTools", "disableSkills"]) {
462
+ if (body[key] !== undefined && typeof body[key] !== "boolean") {
463
+ sendError(res, 400, "request.field_invalid", `${key} must be a boolean`);
464
+ return null;
465
+ }
466
+ }
467
+ if (body.disableSkills === true && ((body.skills?.length ?? 0) > 0 || (body.agents ?? []).some((a) => (a.skills?.length ?? 0) > 0))) {
468
+ sendError(res, 400, "request.field_conflict", "disableSkills: true means this run installs no skill at all (not the deployment's, not the request's), so it cannot come with a non-empty skills or agents[].skills — send one or the other");
463
469
  return null;
464
470
  }
465
471
  for (const key of ["excludeTools", "deferTools"]) {
@@ -63,7 +63,7 @@ export async function mintAndDriveDurableRun(ctx, runStore, args) {
63
63
  windowMs: deps.config.streamApproval?.windowMs,
64
64
  windowMarginMs: deps.config.streamAskWindowMarginMs,
65
65
  }
66
- : undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals);
66
+ : undefined, deps.checkpointStore, args.prepared.engineNotices, args.trustedInternals, deps.userInputLedger);
67
67
  return { status: 202, body: { taskId, sessionId, status: "running" } };
68
68
  }
69
69
  //# sourceMappingURL=durable-run-leg.js.map
@@ -622,11 +622,14 @@ export function createResumeLegs(ctx) {
622
622
  unregisterEngineNotice = registerEngineNoticeLeg({
623
623
  sessionId,
624
624
  ...(args.engineNotices !== undefined && args.engineNotices.length > 0 ? { pending: [...args.engineNotices] } : {}),
625
- durable: (row) => void append("engine_notice", row).catch((err) => {
625
+ durable: (row) => append("engine_notice", row).then(() => true, (err) => {
626
626
  recordFailOpen("server.engine-notice.durable-append-failed", `leg=resume code=${row.code} task=${taskId ?? ""}`);
627
627
  deps.logger?.warn?.("engine_notice_append_failed", { taskId, noticeCode: row.code, sessionId: row.sessionId, err: err instanceof Error ? err.message : String(err) });
628
+ return false;
628
629
  }),
629
630
  });
631
+ if (deps.userInputLedger && taskId)
632
+ await deps.userInputLedger.announceUndrained({ sessionId, taskId });
630
633
  const anchorOwner = principal ?? null;
631
634
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
632
635
  const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
@@ -964,6 +967,8 @@ export function createResumeLegs(ctx) {
964
967
  finally {
965
968
  unregisterEngineNotice?.();
966
969
  if (taskId) {
970
+ if (resumeStreamRef !== undefined)
971
+ await deps.userInputLedger?.settleLeg(resumeStreamRef);
967
972
  if (inflightRuns.get(taskId) === cancelCtrl)
968
973
  inflightRuns.delete(taskId);
969
974
  if (preemptableRuns.get(taskId) === preemptCtrl)
@@ -150,9 +150,9 @@ export interface CheckpointCtx {
150
150
  /**
151
151
  * 🔴 S-877 候裁 18(codex r3 F1)—— 这条会话的 run 是**无人值守腿**铸的(今天唯一的写者 = leader worker 的 durable 块,
152
152
  * `leader/wire.ts`),**服务端写、不来自请求体**(`body` 是提交体;本位是它的兄弟键,客户端够不着)。
153
- * 续跑家族据此请 `resolveSpec` 带 `unattendedLeg: true`,由它按续跑 Runner 的同名座经 `createUnattendedLeg` / `withUnattendedLeg`
154
- * 重挂整条腿(模式 = 旋钮、写面 roots、模式释放写保护时的写保护拒层)—— 拒层是活闭包、进不了 checkpoint,而 core 在 spec 不说
155
- * 模式时取 checkpoint 记下的那一个(`seat ?? recorded`),不重挂 = 续跑落 bypass 而拒层缺席。写回口(`DriveResumeArgs.resumeCtx`)
153
+ * 续跑家族据此请 `resolveSpec` 带 `unattendedLeg: true`,由它经首跑同一只合并口 `withUnattendedLeg` 重挂整条腿(S-934 ①:`default` +
154
+ * 按类作答的 onAsk)—— onAsk 是活闭包、进不了 checkpoint,而 core 在 spec 不说模式时取 checkpoint 记下的那一个(`seat ?? recorded`),
155
+ * 不重挂 = 续跑落 checkpoint 记下的旧模式(7.106.0–7.108.0 的行记 `bypassPermissions`)而按类作答缺席。写回口(`DriveResumeArgs.resumeCtx`)
156
156
  * 传原 ctx / 其展开 ⇒ 标随行。缺席 = 不是无人值守腿(HTTP 提交腿;7.106.0 前落盘的行 —— 那时这些腿不铸模式,checkpoint 里也没有)。
157
157
  */
158
158
  unattendedLeg?: true;
@@ -138,6 +138,7 @@ export async function handleCapabilities(req, res, match, ctx) {
138
138
  taskAgents: deps.config.requirePrincipal !== true,
139
139
  retainBackgroundProcesses: deps.config.requirePrincipal !== true,
140
140
  interactiveTools: true,
141
+ disableSkills: true,
141
142
  projectContext: cwdHonored(deps.config),
142
143
  callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
143
144
  taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyOverrides(deps.config.autonomy).writeFace !== "roots",
@@ -6,7 +6,6 @@ import type { RouteCtx, PreparedTaskSubmission, RouteMatch, RouteIdsOf } from ".
6
6
  * —— 导出的是**行为**而不是 schema 对象:schema 的推断类型会牵出 registry-core 内嵌的 zod 路径
7
7
  * (TS2742,不可移植),而这道门要钉的本来就是行为。非 wire 面。 */
8
8
  export declare function projectDecisionActor(raw: unknown): ActorAssertionWire | undefined;
9
- /** 子代理 / 任务句柄 / HITL 应答段——`handle()` 里它在 SVC-5 workflow steer **之后**,故是独立的域入口。 */
10
9
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
11
10
  * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
12
11
  export declare function handleRuns(req: IncomingMessage, res: ServerResponse, match: RouteMatch<RunsRouteId>, ctx: RouteCtx): Promise<void>;
@@ -134,6 +134,18 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
134
134
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
135
135
  }, taskId, staleMs);
136
136
  }
137
+ async function acceptLiveUserInput(deps, res, at) {
138
+ if (!deps.userInputLedger)
139
+ return undefined;
140
+ try {
141
+ return await deps.userInputLedger.accept({ input: "steer", messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
142
+ }
143
+ catch (e) {
144
+ deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
145
+ sendError(res, 503, "steering.ledger_unavailable", "this steer was NOT delivered: the server could not record it in its user-input ledger (an acceptance receipt is only given once the acceptance is durable, so a crash before the run consumes it can be announced) — nothing reached the engine; retry later");
146
+ return "refused";
147
+ }
148
+ }
137
149
  export async function handleRuns(req, res, match, ctx) {
138
150
  const { deps } = ctx;
139
151
  const { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints } = ctx.registry;
@@ -468,12 +480,17 @@ export async function handleRuns(req, res, match, ctx) {
468
480
  const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
469
481
  const live = steerableRuns.get(taskId);
470
482
  if (live) {
483
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live });
484
+ if (acceptance === "refused")
485
+ return;
471
486
  try {
472
487
  await live.steer(text, { trusted, ...(steerInputId !== undefined ? { inputId: steerInputId } : {}) });
473
488
  sendJson(res, 200, { taskId, status: "running", delivery: "applied", messageId, ...(priority ? { priority } : {}) });
474
489
  return;
475
490
  }
476
491
  catch (e) {
492
+ if (acceptance !== undefined)
493
+ await deps.userInputLedger.withdraw(acceptance);
477
494
  const code = e.code;
478
495
  if (code === "steering.invalid_content") {
479
496
  sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
@@ -776,10 +793,15 @@ export async function handleRuns(req, res, match, ctx) {
776
793
  });
777
794
  return;
778
795
  }
796
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream });
797
+ if (acceptance === "refused")
798
+ return;
779
799
  try {
780
800
  await liveStream.steer(text, { trusted, priority: "now", ...(interruptInputId !== undefined ? { inputId: interruptInputId } : {}) });
781
801
  }
782
802
  catch (e) {
803
+ if (acceptance !== undefined)
804
+ await deps.userInputLedger.withdraw(acceptance);
783
805
  const code = e.code;
784
806
  if (code === "steering.invalid_content") {
785
807
  sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
@@ -618,8 +618,18 @@ export async function handleSessions(req, res, match, ctx) {
618
618
  const tail = qi("tail");
619
619
  const before = qi("before");
620
620
  const blobCap = qi("blobCap");
621
+ let undrained;
622
+ if (deps.userInputLedger) {
623
+ try {
624
+ undrained = await deps.userInputLedger.readSession(match.params.sessionId);
625
+ }
626
+ catch (e) {
627
+ recordFailOpen("server.user-input-ledger.read-failed", `op=session-view ${e instanceof Error ? e.message : String(e)}`);
628
+ }
629
+ }
630
+ const undrainedKey = undrained !== undefined ? { userInputUndrained: undrained } : {};
621
631
  if (tail === undefined && before === undefined && blobCap === undefined) {
622
- sendJson(res, 200, audit);
632
+ sendJson(res, 200, { ...audit, ...undrainedKey });
623
633
  return;
624
634
  }
625
635
  let messages = audit.messages;
@@ -631,7 +641,7 @@ export async function handleSessions(req, res, match, ctx) {
631
641
  }
632
642
  if (blobCap !== undefined && blobCap > 0)
633
643
  messages = truncateMessageBlobs(messages, blobCap);
634
- sendJson(res, 200, { ...audit, messages, ...(window ? { window } : {}) });
644
+ sendJson(res, 200, { ...audit, messages, ...(window ? { window } : {}), ...undrainedKey });
635
645
  return;
636
646
  }
637
647
  }
@@ -333,10 +333,12 @@ export async function handleTasks(req, res, match, ctx) {
333
333
  },
334
334
  ...(noticeLedger
335
335
  ? {
336
- durable: (row) => void noticeLedger.append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`)),
336
+ durable: (row) => noticeLedger.append("engine_notice", row).then(() => true, () => (recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`), false)),
337
337
  }
338
338
  : {}),
339
339
  });
340
+ if (deps.userInputLedger && durableTaskId && prepared.spec.sessionId)
341
+ await deps.userInputLedger.announceUndrained({ sessionId: prepared.spec.sessionId, taskId: durableTaskId });
340
342
  try {
341
343
  const streamBody = async () => {
342
344
  await withPrincipal(principal, async () => {
@@ -700,6 +702,8 @@ export async function handleTasks(req, res, match, ctx) {
700
702
  clearInterval(durableHeartbeat);
701
703
  if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
702
704
  steerableRuns.delete(durableTaskId);
705
+ if (liveStreamRef !== undefined)
706
+ await deps.userInputLedger?.settleLeg(liveStreamRef);
703
707
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
704
708
  inflightRuns.delete(durableTaskId);
705
709
  cancelledViaVerb.delete(durableTaskId);
@@ -105,8 +105,8 @@ export interface ServiceCoreDeps {
105
105
  * ⚠️ 7.69.0(R-1)起 **resume 家族也递这一位** —— 四条 resume 腿收敛到 `resolveResumeSpec` 一只 resolver,
106
106
  * 通告口接一次、四腿同得(见 http/server.ts 的 `resolveResumeSpec` 顶注);此处旧注写作「resume 家族缺席」
107
107
  * 已过期,按新行为改。
108
- * `opts.unattendedLeg`(S-877 候裁 18,codex r3 F1):续跑家族在 ctx 带无人值守标(`CheckpointCtx.unattendedLeg`)时递 `true` ——
109
- * 装配器在**全部折叠之后**经 `createUnattendedLeg` / `withUnattendedLeg` 重挂那条腿(表 / 根取续跑 Runner 的同名座)。只由续跑腿
108
+ * `opts.unattendedLeg`(S-877 候裁 18,codex r3 F1;S-934 ① 改形):续跑家族在 ctx 带无人值守标(`CheckpointCtx.unattendedLeg`)时递 `true` ——
109
+ * 装配器在**全部折叠之后**经首跑同一只合并口 `withUnattendedLeg` 重挂那条腿(`default` + 按类作答的 onAsk)。只由续跑腿
110
110
  * 按服务端写的 ctx 位递,绝不从请求体读。 */
111
111
  resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
112
112
  leg?: "fresh" | "resume";
@@ -217,6 +217,11 @@ export interface ServiceStoreDeps {
217
217
  /** SendUserFile 账本(多租户治理面):scope↔object 映射的读面 GET /v1/sendfile-links。present ⇔ 签发面
218
218
  * 已配 AND 有 store backend(main.ts 建;写点在两条 send lane 的 withLedgerRecording)。 */
219
219
  sendFileLedger?: import("../plugins/send-file-ledger.js").SendFileLedger;
220
+ /** S-928 用户输入受理账本(「200 applied」受理的影子;单写者 = `src/user-input-ledger.ts` 的活对象,boot 装配在 stage-07 并跑一次收敛)。
221
+ * 在场 ⇔ 有 store backend(生产恒与 `runStore` 同源于 backend)。消费点:steer 的 live 腿受理前写 `accepted`(写不进 ⇒ 503
222
+ * `steering.ledger_unavailable`)、三条 run 腿开通告口那一拍铸 `crashed` 通告并在收场时 settle、`GET /v1/sessions/:id` 的
223
+ * `userInputUndrained` 读面。缺席只出现在不接 backend 的 stub-harness 上(那时没有这一面,行为与修前逐字相同)。 */
224
+ userInputLedger?: import("../user-input-ledger.js").UserInputLedger;
220
225
  /** 2c session-sync (P1d) — the WHOLE durable backend, so the `/v1/sessions/:id/sync/*` routes can fan out across
221
226
  * ALL four session subsystems (session entries + fileSnapshot blobs/manifests + sessionPolicy + resumeAnchor)
222
227
  * in the §8 fail-closed atomic order.
@@ -293,6 +293,14 @@ export interface TaskRequestBody {
293
293
  * 探测:本键在不在受理面由**键闭集**自己回答 —— 没接线的 server 版本对它 400 `request.body_shape`
294
294
  * 并逐字列名,所以**不加能力位**(一个键一条探测路,不长第二条)。 */
295
295
  excludeAllTools?: boolean;
296
+ /** S-946(cli L-920 请托;CC 2.1.280 `--disable-slash-commands`「Disable all skills」,cli280.js:240760 整张命令表置空):
297
+ * **这一发一只技能都不装** —— 场景出厂技能(启动期烤入 + 中心下发)、调用方 `skills`、子代定义的 `agents[].skills`
298
+ * 一个都不进 `TaskSpec`。`true` ⇒ `TaskSpec.skills = []`(core 对空表:零 `<skills>` 块、`Skill` 工具不挂);
299
+ * `false` / 缺席 = 技能照旧。值形门与 {@link excludeAllTools} **同一只**(非 boolean ⇒ 400 `request.field_invalid`);
300
+ * 与非空 `skills` 或任一非空 `agents[].skills` 同带 ⇒ 400 `request.field_conflict`(体内自相矛盾,拒在受理门)。
301
+ * 续跑重放逐字重放存量体 ⇒ 同一只写点同判(坏形 defensive:只认 literal `true`)。无租户门(收窄,不跨租户)。
302
+ * 探测位 `GET /v1/capabilities` 的 `disableSkills`(与本键同一个词)。 */
303
+ disableSkills?: boolean;
296
304
  /** [ref]② (core 1.314 工具面控制批): roster 真卸载清单(schema 不上 wire,manifest 收窄)。wire 工具名;
297
305
  * tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。 */
298
306
  excludeTools?: string[];
package/dist/index.d.ts CHANGED
@@ -28,4 +28,6 @@ export { createSideQueryLane, createPerModelKeyBrain, type KeyResolver, type Sid
28
28
  export { createKeyResolver, resolveModelApiKey, type ModelKeyRef } from "./key-resolver.js";
29
29
  export { isModelKeyPoison, ModelKeyPoisonedError, type ModelKeyPoison, type ModelKeyPoisonReason } from "./key-resolver.js";
30
30
  export { RETIRED_ENVS, type RetiredEnv, type RetiredEnvKind, type RetiredEnvWire } from "./config.js";
31
+ export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES, type CatalogEntryHeaderName } from "./model-entry-headers.js";
32
+ export type { UserInputLedgerStore, UserInputAcceptance, NewUserInputAcceptance, OpenScanCursor, SettleHow, WriterGoneEvidence } from "./user-input-ledger.js";
31
33
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -25,4 +25,5 @@ export { createSideQueryLane, createPerModelKeyBrain } from "./boot/side-query-l
25
25
  export { createKeyResolver, resolveModelApiKey } from "./key-resolver.js";
26
26
  export { isModelKeyPoison, ModelKeyPoisonedError } from "./key-resolver.js";
27
27
  export { RETIRED_ENVS } from "./config.js";
28
+ export { CATALOG_ENTRY_HEADER_LANES, CATALOG_ENTRY_HEADER_NAMES } from "./model-entry-headers.js";
28
29
  //# sourceMappingURL=index.js.map
@@ -28,9 +28,9 @@
28
28
  * sessionId / phase)—— 与 `mcp.injection_dropped`「装配期铸、无 runId」同形,run 归属由投递腿承载(这条 run 的 SSE /
29
29
  * 这条 run 的账本)。向 core 要 ctx 加 runId 已登记为未闭环项。
30
30
  */
31
- import type { EngineNotice } from "@sema-agent/core";
32
- import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
33
- export declare const INSTRUCTION_SOURCE_CHANGED: ServerNoticeCode;
31
+ import type { EngineNotice, EngineNoticeCode } from "@sema-agent/core";
32
+ /** 码常量型 = core 码册(#1240 入册:受众 user、铸者 `NOTICE_MINTER` = `"server"`)。 */
33
+ export declare const INSTRUCTION_SOURCE_CHANGED: EngineNoticeCode;
34
34
  /** 基线读数。`recorded: false` = 这个会话还没有任何 run 记过基线(首 run ⇒ 只写不发);`source: null` = 记过,且那一次
35
35
  * 没有项目指令文件被选中。两态必须可判别:把「没记过」读成「记过没有」会让新会话首 run 发一帧 `null → CLAUDE.md`。 */
36
36
  export type InstructionSourceBaseline = {
@@ -27,6 +27,59 @@ export declare function buildUploadScript(p: {
27
27
  }): string;
28
28
  /** The objective suffix that makes the upload the worker's last task step (no URL through the model). */
29
29
  export declare function uploadStepSuffix(workDir: string): string;
30
+ /**
31
+ * S-949 —— the INCREMENTAL upload loop's lifetime protocol (single writer; the wire only runs its two verbs).
32
+ *
33
+ * While a worker env lives, a background loop re-runs the staged upload script every {@link UPLOAD_LOOP_PERIOD_SEC}
34
+ * seconds, so the worker's fixed diff key tracks its latest COMMITTED progress and salvage survives an env hard-crash
35
+ * (#16 tail: run 019eca71's "4 commits vaporized"). The loop must NOT outlive the env: the pre-S-949 form
36
+ * (`nohup sh -c 'while true; …' &`) relied on "the pod dies, the loop dies", which holds only where the env IS a
37
+ * pod — on an env whose shell is the host's (`RemoteHostExecutionEnv`, core `NodeExecutionEnv`) teardown does not
38
+ * reap detached processes (by contract), and the loop became a ppid-1 orphan (254 of them on one box, 2026-10-01).
39
+ *
40
+ * ONE mechanism for every lane, carried entirely by the env's ordinary `exec` (no lane branch, no process-group or
41
+ * `/proc` assumption, never a signal to a pid that might have been reused):
42
+ * - **run token** `.leader-upload-loop.gen` — the generation allowed to run. `start` draws the next number from
43
+ * the monotonic counter `.leader-upload-loop.seq` (never reset by `stop` — a counter that restarted at 1 after a
44
+ * `stop` past its bound would hand a fresh loop the generation of a revoked loop still finishing its upload, and
45
+ * that one would carry on), writes it here BEFORE forking and hands it to the loop; the loop runs only while this
46
+ * file still names its generation. So a later `start` (a resumed leg re-staging the same workspace) supersedes an
47
+ * earlier loop, `stop` (which removes the file) ends every loop, and a workspace deleted out from under it ends it
48
+ * too — the token check IS the loop's only exit, there is no second mechanism;
49
+ * - **one pid file per generation** `.leader-upload-loop.pid.<gen>` — written by `start` right after the fork (the
50
+ * wire awaits `start`, so it is on disk before any `stop` can run) and removed by that loop as it exits. `stop`
51
+ * waits until no generation is left, so a superseded loop whose upload was still in flight at the re-stage is
52
+ * waited for too (codex r1 F2). It only PROBES a pid (`kill -0`): a loop that died abnormally does not hold it up;
53
+ * - **bounded uploads** — the loop runs each upload with a PUT time bound ({@link UPLOAD_LOOP_UPLOAD_MAX_SEC},
54
+ * `curl --max-time`), so a hung object store cannot pin a revoked loop alive (codex r1 F1: on a host env that was
55
+ * an orphan stuck in curl). `stopWaitSec` must exceed that bound plus one tick (the builder refuses otherwise);
56
+ * - **ordering** — the loop checks the token right before every upload and runs it in the foreground, so once
57
+ * `stop` returns 0 no loop upload of any generation is running or can start: the teardown belt upload that follows
58
+ * is the last write to the diff key. A `stop` past its bound (a hung `git format-patch`, or a pid file whose pid
59
+ * now belongs to someone else) exits non-zero — the wire warns and still runs the belt (dropping the belt would
60
+ * lose the uncommitted-work capture it exists for).
61
+ */
62
+ /** Seconds between two loop runs of the upload script. Single writer: the wire passes THIS constant; tests inject a
63
+ * short period through {@link buildUploadLoopScript}'s parameter. Deliberately not a deployment knob. */
64
+ export declare const UPLOAD_LOOP_PERIOD_SEC = 90;
65
+ /** Where the loop script is staged — next to the upload script, same `workDir` rule. */
66
+ export declare function uploadLoopScriptPath(workDir: string): string;
67
+ /** The two lifecycle verbs the wire runs through the env's ordinary exec (closed set — `run` is the loop's own,
68
+ * internal entry point and is never issued from outside). */
69
+ export type UploadLoopVerb = "start" | "stop";
70
+ /** The shell line for one lifecycle verb. `stop` revokes the run token FIRST and independently of the script (a loop
71
+ * whose script file went missing still sees its token gone and exits), then lets the script's `stop` wait for it. */
72
+ export declare function uploadLoopCommand(workDir: string, verb: UploadLoopVerb): string;
73
+ /** The staged loop script (see the protocol note above). The three bounds are interpolated into the shell ⇒
74
+ * validated here, fail-closed: positive integers, and `stopWaitSec > uploadMaxSec + 1` (stop outlasts one bounded
75
+ * upload plus the loop's one-second tick — otherwise "stop returned ⇒ no upload in flight" would be false by
76
+ * construction). */
77
+ export declare function buildUploadLoopScript(p: {
78
+ workDir: string;
79
+ periodSec: number;
80
+ uploadMaxSec?: number;
81
+ stopWaitSec?: number;
82
+ }): string;
30
83
  /**
31
84
  * Fetch the worker-uploaded patch over a presigned GET (control plane; creds stay adapter-held). Returns the
32
85
  * same result shape as pullDiff so merge consumes either path. 🔴 The presigned URL is a short-lived bearer
@@ -8,9 +8,10 @@ export function buildUploadScript(p) {
8
8
  "#!/bin/sh",
9
9
  "# leader diff-out (design/68 F2(a)) — exports this worker's branch as a patch series. Do not edit.",
10
10
  "set -e",
11
+ 'case "$1" in ""|*[!0-9]*) M="" ;; *) M="--max-time $1" ;; esac',
11
12
  `cd ${p.repoDir}`,
12
13
  `git format-patch --stdout ${p.baseSha}..HEAD > ${p.workDir}/.leader-out.patch`,
13
- `curl -fsS -X PUT -T ${p.workDir}/.leader-out.patch '${putUrl}'`,
14
+ `curl -fsS $M -X PUT -T ${p.workDir}/.leader-out.patch '${putUrl}'`,
14
15
  `echo leader-diff-uploaded`,
15
16
  "",
16
17
  ].join("\n");
@@ -18,6 +19,99 @@ export function buildUploadScript(p) {
18
19
  export function uploadStepSuffix(workDir) {
19
20
  return `\n\nMANDATORY FINAL STEP: after your final commit, run exactly: sh ${workDir}/.leader-upload.sh — it exports your branch diff for integration. Do not modify, inspect, or skip it; if it fails, fix your git state (commit your work) and run it again.`;
20
21
  }
22
+ export const UPLOAD_LOOP_PERIOD_SEC = 90;
23
+ const UPLOAD_LOOP_UPLOAD_MAX_SEC = 25;
24
+ const UPLOAD_LOOP_STOP_WAIT_SEC = 30;
25
+ export function uploadLoopScriptPath(workDir) {
26
+ return `${workDir}/.leader-upload-loop.sh`;
27
+ }
28
+ const uploadLoopTokenPath = (workDir) => `${workDir}/.leader-upload-loop.gen`;
29
+ export function uploadLoopCommand(workDir, verb) {
30
+ const script = uploadLoopScriptPath(workDir);
31
+ switch (verb) {
32
+ case "start":
33
+ return `sh ${script} start`;
34
+ case "stop":
35
+ return `rm -f ${uploadLoopTokenPath(workDir)}; sh ${script} stop`;
36
+ }
37
+ }
38
+ export function buildUploadLoopScript(p) {
39
+ const uploadMaxSec = p.uploadMaxSec ?? UPLOAD_LOOP_UPLOAD_MAX_SEC;
40
+ const stopWaitSec = p.stopWaitSec ?? UPLOAD_LOOP_STOP_WAIT_SEC;
41
+ for (const [name, n] of [["periodSec", p.periodSec], ["uploadMaxSec", uploadMaxSec], ["stopWaitSec", stopWaitSec]]) {
42
+ if (!Number.isInteger(n) || n < 1)
43
+ throw new Error(`buildUploadLoopScript: ${name} must be a positive integer, got ${JSON.stringify(n)}`);
44
+ }
45
+ if (stopWaitSec <= uploadMaxSec + 1) {
46
+ throw new Error(`buildUploadLoopScript: stopWaitSec (${stopWaitSec}) must exceed uploadMaxSec (${uploadMaxSec}) + 1 — stop has to outlast one bounded upload plus the loop's tick`);
47
+ }
48
+ const script = uploadLoopScriptPath(p.workDir);
49
+ return [
50
+ "#!/bin/sh",
51
+ "# leader incremental upload loop (S-949) — re-runs .leader-upload.sh while this env lives. Do not edit.",
52
+ "# start: launch ONE detached loop (an earlier loop here sees its generation superseded and exits); record its pid.",
53
+ "# stop: revoke the run token, then wait until no loop of any generation is left (uploads in flight finish first).",
54
+ "# run: the loop itself (internal) — runs only while the token file still names its generation.",
55
+ `G=${uploadLoopTokenPath(p.workDir)}`,
56
+ `Q=${p.workDir}/.leader-upload-loop.seq`,
57
+ `P=${p.workDir}/.leader-upload-loop.pid`,
58
+ "stop_all() {",
59
+ ' rm -f "$G"',
60
+ " i=0",
61
+ " while :; do",
62
+ " live=",
63
+ ' for f in "$P".*; do',
64
+ ' [ -f "$f" ] || continue',
65
+ " p=",
66
+ ' read -r p 2>/dev/null < "$f"',
67
+ ' if [ -n "$p" ] && kill -0 "$p" 2>/dev/null; then live="$live $p"; else rm -f "$f"; fi',
68
+ " done",
69
+ ' [ -z "$live" ] && return 0',
70
+ ` if [ "$i" -ge ${stopWaitSec} ]; then`,
71
+ ` echo "leader upload loop(s) still running ${stopWaitSec}s after the run token was revoked (pid$live)" >&2`,
72
+ " return 1",
73
+ " fi",
74
+ " sleep 1",
75
+ " i=$((i + 1))",
76
+ " done",
77
+ "}",
78
+ 'case "$1" in',
79
+ "start)",
80
+ ' command -v nohup >/dev/null 2>&1 || { echo "nohup not found — upload loop not started" >&2; exit 1; }',
81
+ " g=0",
82
+ ' [ -f "$Q" ] && read -r g < "$Q"',
83
+ " case $g in ''|*[!0-9]*) g=0 ;; esac",
84
+ " g=$((g + 1))",
85
+ ' echo "$g" > "$Q"',
86
+ ' echo "$g" > "$G"',
87
+ ` nohup sh ${script} run "$g" </dev/null >/dev/null 2>&1 &`,
88
+ ' echo "$!" > "$P.$g"',
89
+ " ;;",
90
+ "run)",
91
+ " n=0",
92
+ ' while read -r g 2>/dev/null < "$G" && [ "$g" = "$2" ]; do',
93
+ ` if [ "$n" -ge ${p.periodSec} ]; then`,
94
+ " n=0",
95
+ ` sh ${p.workDir}/.leader-upload.sh ${uploadMaxSec} >/dev/null 2>&1 || true`,
96
+ " else",
97
+ " sleep 1",
98
+ " n=$((n + 1))",
99
+ " fi",
100
+ " done",
101
+ ' rm -f "$P.$2"',
102
+ " exit 0",
103
+ " ;;",
104
+ "stop)",
105
+ " stop_all",
106
+ " ;;",
107
+ "*)",
108
+ ' echo "usage: sh $0 start|stop" >&2',
109
+ " exit 2",
110
+ " ;;",
111
+ "esac",
112
+ "",
113
+ ].join("\n");
114
+ }
21
115
  export async function fetchUploadedDiff(getUrl, opts = {}) {
22
116
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_DIFF_BYTES;
23
117
  const f = opts.fetchImpl ?? fetch;
@@ -16,7 +16,6 @@ import { type RunnerLike, type WorkerReport } from "./fanout.js";
16
16
  import { type ExecStreamLike } from "./diffout.js";
17
17
  import { type MergeDeps, type MergeResult } from "./merge.js";
18
18
  import { type TaskSpec, type VerifyConfig, type CheckpointToken, type TerminalCause, type ToolSpec, type RepairTerminal, type OracleTier, type RepairResult } from "@sema-agent/core";
19
- import { type UnattendedLeg } from "../unattended-permission-mode.js";
20
19
  /** [ref] Slice 6: drive a worker's resource-suspend auto-resume to a terminal (or bound) state. Exported
21
20
  * for unit tests. Loops `resume(token, slice)` WHILE the result's terminal cause is a park on a
22
21
  * `resource_limit` gate. Exits on: a terminal cause that is not a park, a park on any OTHER gate
@@ -68,11 +67,6 @@ export interface ProvisionedWorker {
68
67
  export interface LeaderDeps {
69
68
  /** Plan a task into N disjoint-file sub-tasks (MVP: simple/hand-authored; adaptive replan = scale). */
70
69
  plan: (task: string) => Promise<SubtaskSpec[]>;
71
- /** S-877 候裁 15 / 18 —— 本车道有手的 run 并进的无人值守腿(模式 = 运维旋钮 `UNATTENDED_PERMISSION_MODE`、写面 roots、模式释放写保护时的
72
- * 写保护拒层),由 `wire.ts` 经 `createUnattendedLeg` 铸一次递进来。本文件自己起的有手 run(模块门修复)与 `repair-wire.ts` 的单代理修复环按
73
- * `sub.spec` 起 run、不经 provisionWorker —— 两处都经 `withUnattendedLeg` 并进这一份。**必填**(缺席 = core「Absent = default」⇒ 无人可答的腿上
74
- * 写与非只读命令被当场拒,codex F1 的病)。 */
75
- unattendedLeg: UnattendedLeg;
76
70
  /** Provision a leader-owned worker (its env seeded with base at `branch` + an env-bound runner). */
77
71
  provisionWorker: (sub: SubtaskSpec) => Promise<ProvisionedWorker>;
78
72
  /** Ephemeral no-creds integration sandbox + the credentialed Coordinator push (merge(b) deps). */