@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.
- package/MIGRATION.md +60 -1
- package/README.md +8 -0
- package/README.zh-CN.md +5 -0
- package/USAGE.md +19 -29
- package/dist/approval-ask-machine.d.ts +9 -25
- package/dist/approval-ask-machine.js +5 -2
- package/dist/approval-card.d.ts +33 -3
- package/dist/approval-card.js +26 -2
- package/dist/bench/s1/live-deps.d.ts +4 -5
- package/dist/bench/s1/live-deps.js +9 -10
- package/dist/boot/leader.js +0 -2
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +3 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +6 -12
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.js +17 -3
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/capabilities/scenarios.d.ts +3 -2
- package/dist/config-catalog.js +2 -4
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +5 -11
- package/dist/config.js +21 -12
- package/dist/deployment-governance.d.ts +8 -2
- package/dist/deployment-governance.js +9 -8
- package/dist/hooks/hook-runner.d.ts +2 -2
- package/dist/hooks/hook-runner.js +40 -26
- package/dist/http/admission.js +8 -2
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/resume-legs.js +6 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/runs.d.ts +0 -1
- package/dist/http/routes/runs.js +22 -0
- package/dist/http/routes/sessions.js +12 -2
- package/dist/http/routes/tasks.js +5 -1
- package/dist/http/server.d.ts +7 -2
- package/dist/http/wire-types.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/instruction-source-notice.d.ts +3 -3
- package/dist/leader/diffup.d.ts +53 -0
- package/dist/leader/diffup.js +95 -1
- package/dist/leader/leader.d.ts +0 -6
- package/dist/leader/leader.js +1 -1
- package/dist/leader/repair-wire.js +1 -1
- package/dist/leader/wire.d.ts +3 -10
- package/dist/leader/wire.js +39 -13
- package/dist/memory-layer-legacy-lock.d.ts +3 -3
- package/dist/model-entry-headers.d.ts +5 -2
- package/dist/model-entry-headers.js +4 -4
- package/dist/model-unreachable-notice.d.ts +2 -2
- package/dist/observability/corrupt-read-seat.d.ts +6 -0
- package/dist/observability/corrupt-read-seat.js +6 -0
- package/dist/observability/fail-open.d.ts +13 -5
- package/dist/observability/fail-open.js +13 -5
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/run-terminal.js +10 -0
- package/dist/operator-ask.d.ts +9 -8
- package/dist/operator-ask.js +9 -1
- package/dist/plugins/approval-ask-store-file.d.ts +2 -1
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +7 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +18 -0
- package/dist/plugins/approval-ask-store-sql.js +7 -0
- package/dist/plugins/file-run-store.js +2 -2
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/pg-pool.js +15 -0
- package/dist/plugins/run-store-sql.d.ts +1 -1
- package/dist/plugins/run-store-sql.js +3 -2
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/store-backend.d.ts +4 -0
- package/dist/plugins/store-backend.js +8 -0
- package/dist/plugins/store-contracts.d.ts +7 -0
- package/dist/plugins/store-contracts.js +1 -0
- package/dist/plugins/tidb-pool.js +17 -0
- package/dist/plugins/user-input-ledger-file.d.ts +33 -0
- package/dist/plugins/user-input-ledger-file.js +98 -0
- package/dist/plugins/user-input-ledger-sql.d.ts +44 -0
- package/dist/plugins/user-input-ledger-sql.js +101 -0
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.d.ts +18 -5
- package/dist/run-local.js +26 -12
- package/dist/runs.d.ts +8 -1
- package/dist/runs.js +6 -2
- package/dist/runtime-governance.d.ts +23 -11
- package/dist/runtime-governance.js +7 -12
- package/dist/session-records-skipped-notice.d.ts +4 -3
- package/dist/task-settings.d.ts +0 -6
- package/dist/task-settings.js +0 -1
- package/dist/tool-approval.d.ts +44 -15
- package/dist/tool-approval.js +12 -8
- package/dist/trace/core-keyset-guard.d.ts +17 -2
- package/dist/trace/engine-notice-wire.d.ts +30 -35
- package/dist/trace/engine-notice-wire.js +23 -23
- package/dist/trace/project.js +2 -3
- package/dist/trace/self-minted-wire.d.ts +14 -0
- package/dist/trace/self-minted-wire.js +6 -0
- package/dist/unattended-permission-mode.d.ts +54 -93
- package/dist/unattended-permission-mode.js +8 -61
- package/dist/user-input-ledger.d.ts +296 -0
- package/dist/user-input-ledger.js +337 -0
- 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
|
-
...(
|
|
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 (
|
|
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
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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);
|
package/dist/http/admission.js
CHANGED
|
@@ -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
|
-
|
|
462
|
-
|
|
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
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -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) =>
|
|
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)
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -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
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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>;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -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) =>
|
|
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);
|
package/dist/http/server.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
33
|
-
export declare const INSTRUCTION_SOURCE_CHANGED:
|
|
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 = {
|
package/dist/leader/diffup.d.ts
CHANGED
|
@@ -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
|
package/dist/leader/diffup.js
CHANGED
|
@@ -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;
|
package/dist/leader/leader.d.ts
CHANGED
|
@@ -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). */
|