@sema-agent/server 7.47.0 → 7.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +10 -0
- package/dist/boot/stores.js +13 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +15 -1
- package/dist/fleet/fleet-bus.d.ts +93 -6
- package/dist/fleet/fleet-bus.js +40 -1
- package/dist/hooks/hook-runner.d.ts +10 -10
- package/dist/hooks/hook-runner.js +142 -23
- package/dist/http/route-ctx.d.ts +24 -0
- package/dist/http/route-ctx.js +8 -0
- package/dist/http/routes/approvals-assistant.js +4 -11
- package/dist/http/routes/capabilities.js +1 -1
- package/dist/http/routes/fleet.js +2 -2
- package/dist/http/routes/notify-wake.js +2 -3
- package/dist/http/routes/runs.js +26 -2
- package/dist/http/routes/tasks.js +5 -0
- package/dist/http/server.js +4 -0
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/plugins/approval-ask-store-memory.js +2 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +50 -1
- package/dist/plugins/approval-ask-store-sql.js +40 -5
- package/dist/plugins/permission-rule-store-sql.d.ts +33 -0
- package/dist/plugins/permission-rule-store-sql.js +1 -8
- package/dist/plugins/sql-errors.d.ts +18 -0
- package/dist/plugins/sql-errors.js +10 -0
- package/dist/resource-window.d.ts +111 -0
- package/dist/resource-window.js +80 -0
- package/dist/rules-consent.d.ts +29 -1
- package/dist/rules-consent.js +22 -1
- package/dist/tool-approval.d.ts +47 -1
- package/dist/tool-approval.js +131 -11
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { hostShell, resolveHostShell } from "../plugins/host-platform.js";
|
|
3
3
|
import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, } from "@sema-agent/registry-core/hooks";
|
|
4
|
+
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
5
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
4
6
|
import { redactSecrets } from "../trace/redact.js";
|
|
5
7
|
import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
|
|
6
8
|
import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
|
|
@@ -144,6 +146,52 @@ const DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS = 120;
|
|
|
144
146
|
function clip(s, n) {
|
|
145
147
|
return s.length > n ? `${s.slice(0, n)}…(${s.length})` : s;
|
|
146
148
|
}
|
|
149
|
+
function hookDisplayName(entry) {
|
|
150
|
+
if (typeof entry.statusMessage === "string" && entry.statusMessage.length > 0)
|
|
151
|
+
return entry.statusMessage;
|
|
152
|
+
switch (entry.type) {
|
|
153
|
+
case "command":
|
|
154
|
+
return entry.command;
|
|
155
|
+
case "http":
|
|
156
|
+
return entry.url.split(/[?#]/, 1)[0] ?? entry.url;
|
|
157
|
+
case "prompt":
|
|
158
|
+
case "agent":
|
|
159
|
+
return entry.prompt;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function toolNameOf(payload) {
|
|
163
|
+
const v = payload?.tool_name;
|
|
164
|
+
return typeof v === "string" ? v : undefined;
|
|
165
|
+
}
|
|
166
|
+
function eventOfPayload(payload) {
|
|
167
|
+
const v = payload?.hook_event_name;
|
|
168
|
+
return typeof v === "string" ? v : "";
|
|
169
|
+
}
|
|
170
|
+
function emitHookNotice(ctx, n) {
|
|
171
|
+
try {
|
|
172
|
+
ctx.onHookNotice?.(n);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
recordFailOpen("server.hooks.notice-sink-threw", `kind=${n.kind} event=${n.event}`);
|
|
176
|
+
ctx.logger.warn("hook_notice_sink_threw", { kind: n.kind, event: n.event, error: clip(String(e), 200) });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function noteHookFailure(ctx, args) {
|
|
180
|
+
if (!ctx.onHookNotice)
|
|
181
|
+
return;
|
|
182
|
+
const stderr = boundedNoticeText((args.stderr ?? "").trim());
|
|
183
|
+
emitHookNotice(ctx, {
|
|
184
|
+
kind: "hook_non_blocking_failure",
|
|
185
|
+
event: args.event,
|
|
186
|
+
reason: args.reason,
|
|
187
|
+
hookName: boundedNoticeText(hookDisplayName(args.entry)),
|
|
188
|
+
entryType: args.entry.type,
|
|
189
|
+
...(typeof args.exitCode === "number" ? { exitCode: args.exitCode } : {}),
|
|
190
|
+
...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
|
|
191
|
+
...(stderr.length > 0 ? { stderr } : {}),
|
|
192
|
+
...(args.detail !== undefined ? { detail: boundedNoticeText(args.detail) } : {}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
147
195
|
export function buildHookContext(parts) {
|
|
148
196
|
const present = parts.filter((p) => p.length > 0).map(readHookContextPart);
|
|
149
197
|
if (present.length === 0)
|
|
@@ -276,7 +324,9 @@ function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
276
324
|
stdio: ["pipe", "ignore", rewake ? "pipe" : "ignore"],
|
|
277
325
|
detached: true,
|
|
278
326
|
});
|
|
327
|
+
let killedByTimeout = false;
|
|
279
328
|
const timer = setTimeout(() => {
|
|
329
|
+
killedByTimeout = true;
|
|
280
330
|
try {
|
|
281
331
|
if (child.pid)
|
|
282
332
|
process.kill(-child.pid, "SIGKILL");
|
|
@@ -296,9 +346,14 @@ function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
296
346
|
});
|
|
297
347
|
child.stderr?.unref?.();
|
|
298
348
|
}
|
|
299
|
-
child.on("error", () =>
|
|
349
|
+
child.on("error", (e) => {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: clip(String(e), 300) });
|
|
352
|
+
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
|
|
353
|
+
});
|
|
300
354
|
child.on("close", (code) => {
|
|
301
355
|
clearTimeout(timer);
|
|
356
|
+
noteAsyncOutcome(entry, ctx, event, payload, { code, rewake, killedByTimeout });
|
|
302
357
|
if (!rewake || code !== 2)
|
|
303
358
|
return;
|
|
304
359
|
const text = stderr.trim();
|
|
@@ -321,9 +376,32 @@ function fireAsyncCommandHook(entry, payload, ctx, event) {
|
|
|
321
376
|
child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
|
|
322
377
|
child.unref();
|
|
323
378
|
}
|
|
324
|
-
catch {
|
|
379
|
+
catch (e) {
|
|
380
|
+
ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: clip(String(e), 300) });
|
|
381
|
+
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
|
|
325
382
|
}
|
|
326
383
|
}
|
|
384
|
+
function noteAsyncOutcome(entry, ctx, event, payload, outcome) {
|
|
385
|
+
const { code, rewake, killedByTimeout } = outcome;
|
|
386
|
+
if (killedByTimeout) {
|
|
387
|
+
ctx.logger.warn("hook_async_failed", { event, reason: "timeout", timeoutSec: Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) });
|
|
388
|
+
noteHookFailure(ctx, { event, entry, reason: "timeout", toolName: toolNameOf(payload) });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (code === 0 || code === null)
|
|
392
|
+
return;
|
|
393
|
+
if (rewake && code === 2)
|
|
394
|
+
return;
|
|
395
|
+
ctx.logger.warn("hook_async_failed", { event, reason: "exit_nonzero", code });
|
|
396
|
+
noteHookFailure(ctx, {
|
|
397
|
+
event,
|
|
398
|
+
entry,
|
|
399
|
+
reason: "exit_nonzero",
|
|
400
|
+
exitCode: code,
|
|
401
|
+
toolName: toolNameOf(payload),
|
|
402
|
+
...(code === 2 ? { detail: "exit 2 on a fire-and-forget async hook: there is no blocking or wake face to honor it (asyncRewake would wake the model)" } : {}),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
327
405
|
async function runLlmHook(entry, payload, ctx, extra) {
|
|
328
406
|
const call = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
|
|
329
407
|
if (!call)
|
|
@@ -349,17 +427,19 @@ async function runLlmHook(entry, payload, ctx, extra) {
|
|
|
349
427
|
hardTop,
|
|
350
428
|
]).catch((e) => ({ ok: false, error: String(e) }));
|
|
351
429
|
let res = await invoke();
|
|
430
|
+
let noticed = false;
|
|
352
431
|
if (extra && !res.ok && res.code === "no_content") {
|
|
353
432
|
ctx.logger.warn("hook_llm_no_content_retry", { event: extra.event });
|
|
354
433
|
res = await invoke();
|
|
355
434
|
if (!res.ok && res.code === "no_content") {
|
|
356
|
-
ctx
|
|
435
|
+
emitHookNotice(ctx, { kind: "hook_decision_unavailable", event: extra.event, reason: "no_content", detail: "carrier returned no content (after one retry)" });
|
|
436
|
+
noticed = true;
|
|
357
437
|
}
|
|
358
438
|
}
|
|
359
439
|
if (!res.ok) {
|
|
360
440
|
return res.code === "hard_timeout"
|
|
361
|
-
? { code: null, stdout: "", stderr: "", timedOut: true }
|
|
362
|
-
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError: clip(res.error, 500) };
|
|
441
|
+
? { code: null, stdout: "", stderr: "", timedOut: true, ...(noticed ? { noticed } : {}) }
|
|
442
|
+
: { code: null, stdout: "", stderr: "", timedOut: false, spawnError: clip(res.error, 500), ...(noticed ? { noticed } : {}) };
|
|
363
443
|
}
|
|
364
444
|
return { code: 0, stdout: clip(res.text, 1024 * 1024), stderr: "", timedOut: false };
|
|
365
445
|
}
|
|
@@ -416,6 +496,14 @@ async function runHttpHook(entry, payload, ctx) {
|
|
|
416
496
|
return { code: 0, stdout: "{}", stderr: "", timedOut: false };
|
|
417
497
|
if (!parseHookStdout(body)) {
|
|
418
498
|
ctx.logger.warn("hook_http_non_json_response", { url: clip(entry.url, 300), preview: clip(trimmed, 200) });
|
|
499
|
+
noteHookFailure(ctx, {
|
|
500
|
+
event: eventOfPayload(payload),
|
|
501
|
+
entry,
|
|
502
|
+
reason: "bad_json",
|
|
503
|
+
exitCode: 0,
|
|
504
|
+
stderr: trimmed,
|
|
505
|
+
toolName: toolNameOf(payload),
|
|
506
|
+
});
|
|
419
507
|
return { code: 0, stdout: "", stderr: "", timedOut: false };
|
|
420
508
|
}
|
|
421
509
|
return { code: 0, stdout: body, stderr: "", timedOut: false };
|
|
@@ -452,6 +540,17 @@ function boundedToolInputPayload(payload) {
|
|
|
452
540
|
const bounded = boundedInputValue(p.tool_input);
|
|
453
541
|
return bounded === p.tool_input ? payload : { ...p, tool_input: bounded };
|
|
454
542
|
}
|
|
543
|
+
function readHookStdout(run, entry, event, ctx, payload) {
|
|
544
|
+
const out = parseHookStdout(run.stdout);
|
|
545
|
+
if (out !== undefined)
|
|
546
|
+
return out;
|
|
547
|
+
const t = run.stdout.trim();
|
|
548
|
+
if (t.startsWith("{") || t.startsWith("[")) {
|
|
549
|
+
ctx.logger.warn("hook_bad_json_output", { event, type: entry.type, preview: clip(t, 200) });
|
|
550
|
+
noteHookFailure(ctx, { event, entry, reason: "bad_json", exitCode: run.code, stderr: run.stderr, toolName: toolNameOf(payload) });
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
455
554
|
function parseHookStdout(stdout) {
|
|
456
555
|
const t = stdout.trim();
|
|
457
556
|
if (!t.startsWith("{"))
|
|
@@ -528,10 +627,14 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
|
|
|
528
627
|
: await runCommandHook(entry, payload, ctx);
|
|
529
628
|
if (run.spawnError) {
|
|
530
629
|
ctx.logger.warn(entry.type === "http" ? "hook_http_request_failed" : entry.type === "prompt" || entry.type === "agent" ? "hook_llm_failed" : "hook_command_spawn_failed", { event, type: entry.type, error: clip(run.spawnError, 300) });
|
|
630
|
+
if (!run.noticed)
|
|
631
|
+
noteHookFailure(ctx, { event, entry, reason: "spawn_failed", exitCode: run.code, stderr: run.spawnError, toolName: toolNameOf(payload) });
|
|
531
632
|
continue;
|
|
532
633
|
}
|
|
533
634
|
if (run.timedOut) {
|
|
534
635
|
ctx.logger.warn("hook_command_timeout", { event, timeoutSec: Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) });
|
|
636
|
+
if (!run.noticed)
|
|
637
|
+
noteHookFailure(ctx, { event, entry, reason: "timeout", exitCode: run.code, stderr: run.stderr, toolName: toolNameOf(payload) });
|
|
535
638
|
continue;
|
|
536
639
|
}
|
|
537
640
|
results.push({ entry, run });
|
|
@@ -552,12 +655,21 @@ function ignoreMatchers(groups) {
|
|
|
552
655
|
}
|
|
553
656
|
async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, onceFired) {
|
|
554
657
|
const singles = await runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
555
|
-
for (const { run } of singles) {
|
|
658
|
+
for (const { entry, run } of singles) {
|
|
556
659
|
if (run.code !== 0) {
|
|
557
660
|
ctx.logger.warn("hook_command_failed", { event, code: run.code, stderr: clip(run.stderr, 300) });
|
|
661
|
+
noteHookFailure(ctx, {
|
|
662
|
+
event,
|
|
663
|
+
entry,
|
|
664
|
+
reason: "exit_nonzero",
|
|
665
|
+
exitCode: run.code,
|
|
666
|
+
stderr: run.stderr,
|
|
667
|
+
toolName: toolNameOf(payload),
|
|
668
|
+
...(run.code === 2 ? { detail: `exit 2 on ${event}: this event has no blocking face, so the hook's blocking intent could not be honored` } : {}),
|
|
669
|
+
});
|
|
558
670
|
continue;
|
|
559
671
|
}
|
|
560
|
-
const out =
|
|
672
|
+
const out = readHookStdout(run, entry, event, ctx, payload);
|
|
561
673
|
if (!out)
|
|
562
674
|
continue;
|
|
563
675
|
if (typeof out.systemMessage === "string")
|
|
@@ -621,16 +733,17 @@ export function createTaskHooks(config, ctx) {
|
|
|
621
733
|
tool_use_id: tctx.toolCallId,
|
|
622
734
|
};
|
|
623
735
|
const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
|
|
624
|
-
for (const { run } of singles) {
|
|
736
|
+
for (const { entry, run } of singles) {
|
|
625
737
|
if (run.code === 2) {
|
|
626
738
|
const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
|
|
627
739
|
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
628
740
|
}
|
|
629
741
|
if (run.code !== 0) {
|
|
630
742
|
ctx.logger.warn("hook_command_failed", { event: "PreToolUse", code: run.code, stderr: clip(run.stderr, 300) });
|
|
743
|
+
noteHookFailure(ctx, { event: "PreToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
631
744
|
continue;
|
|
632
745
|
}
|
|
633
|
-
const out =
|
|
746
|
+
const out = readHookStdout(run, entry, "PreToolUse", ctx, payload);
|
|
634
747
|
if (!out)
|
|
635
748
|
continue;
|
|
636
749
|
if (typeof out.systemMessage === "string")
|
|
@@ -716,7 +829,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
716
829
|
tool_use_id: tctx.toolCallId,
|
|
717
830
|
};
|
|
718
831
|
const singles = await runMatchingEntries("PostToolUse", post, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
719
|
-
for (const { run } of singles) {
|
|
832
|
+
for (const { entry, run } of singles) {
|
|
720
833
|
if (run.code === 2) {
|
|
721
834
|
const fb = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
722
835
|
if (fb)
|
|
@@ -725,9 +838,10 @@ export function createTaskHooks(config, ctx) {
|
|
|
725
838
|
}
|
|
726
839
|
if (run.code !== 0) {
|
|
727
840
|
ctx.logger.warn("hook_command_failed", { event: "PostToolUse", code: run.code, stderr: clip(run.stderr, 300) });
|
|
841
|
+
noteHookFailure(ctx, { event: "PostToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
728
842
|
continue;
|
|
729
843
|
}
|
|
730
|
-
const out =
|
|
844
|
+
const out = readHookStdout(run, entry, "PostToolUse", ctx, payload);
|
|
731
845
|
if (!out)
|
|
732
846
|
continue;
|
|
733
847
|
if (typeof out.systemMessage === "string")
|
|
@@ -765,7 +879,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
765
879
|
is_interrupt: f.isInterrupt,
|
|
766
880
|
};
|
|
767
881
|
const singles = await runMatchingEntries("PostToolUseFailure", postFailure, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
768
|
-
for (const { run } of singles) {
|
|
882
|
+
for (const { entry, run } of singles) {
|
|
769
883
|
if (run.code === 2) {
|
|
770
884
|
const fb = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
771
885
|
if (fb)
|
|
@@ -774,9 +888,10 @@ export function createTaskHooks(config, ctx) {
|
|
|
774
888
|
}
|
|
775
889
|
if (run.code !== 0) {
|
|
776
890
|
ctx.logger.warn("hook_command_failed", { event: "PostToolUseFailure", code: run.code, stderr: clip(run.stderr, 300) });
|
|
891
|
+
noteHookFailure(ctx, { event: "PostToolUseFailure", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
|
|
777
892
|
continue;
|
|
778
893
|
}
|
|
779
|
-
const out =
|
|
894
|
+
const out = readHookStdout(run, entry, "PostToolUseFailure", ctx, payload);
|
|
780
895
|
if (!out)
|
|
781
896
|
continue;
|
|
782
897
|
if (typeof out.systemMessage === "string")
|
|
@@ -811,7 +926,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
811
926
|
})),
|
|
812
927
|
};
|
|
813
928
|
const singles = await runMatchingEntries("PostToolBatch", ignoreMatchers(postBatch), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
814
|
-
for (const { run } of singles) {
|
|
929
|
+
for (const { entry, run } of singles) {
|
|
815
930
|
if (run.code === 2) {
|
|
816
931
|
const fb = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
|
|
817
932
|
if (fb)
|
|
@@ -820,9 +935,10 @@ export function createTaskHooks(config, ctx) {
|
|
|
820
935
|
}
|
|
821
936
|
if (run.code !== 0) {
|
|
822
937
|
ctx.logger.warn("hook_command_failed", { event: "PostToolBatch", code: run.code, stderr: clip(run.stderr, 300) });
|
|
938
|
+
noteHookFailure(ctx, { event: "PostToolBatch", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
823
939
|
continue;
|
|
824
940
|
}
|
|
825
|
-
const out =
|
|
941
|
+
const out = readHookStdout(run, entry, "PostToolBatch", ctx, payload);
|
|
826
942
|
if (!out)
|
|
827
943
|
continue;
|
|
828
944
|
if (typeof out.systemMessage === "string")
|
|
@@ -844,16 +960,17 @@ export function createTaskHooks(config, ctx) {
|
|
|
844
960
|
const contexts = [];
|
|
845
961
|
const payload = { ...basePayload(ctx), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
|
|
846
962
|
const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
847
|
-
for (const { run } of singles) {
|
|
963
|
+
for (const { entry, run } of singles) {
|
|
848
964
|
if (run.code === 2) {
|
|
849
965
|
const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a UserPromptSubmit hook";
|
|
850
966
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
851
967
|
}
|
|
852
968
|
if (run.code !== 0) {
|
|
853
969
|
ctx.logger.warn("hook_command_failed", { event: "UserPromptSubmit", code: run.code, stderr: clip(run.stderr, 300) });
|
|
970
|
+
noteHookFailure(ctx, { event: "UserPromptSubmit", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
854
971
|
continue;
|
|
855
972
|
}
|
|
856
|
-
const out =
|
|
973
|
+
const out = readHookStdout(run, entry, "UserPromptSubmit", ctx, payload);
|
|
857
974
|
if (!out)
|
|
858
975
|
continue;
|
|
859
976
|
if (typeof out.systemMessage === "string")
|
|
@@ -886,7 +1003,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
886
1003
|
if (wantsPrompt) {
|
|
887
1004
|
if (ctx.stopPromptTranscript === false) {
|
|
888
1005
|
ctx.logger.warn("hook_entries_skipped", { event: "Stop", type: "prompt", reason: "stop_prompt_transcript_disabled" });
|
|
889
|
-
ctx
|
|
1006
|
+
emitHookNotice(ctx, { kind: "hook_decision_unavailable", event: "Stop", reason: "skipped", detail: "transcript disabled" });
|
|
890
1007
|
}
|
|
891
1008
|
else {
|
|
892
1009
|
let branch;
|
|
@@ -902,7 +1019,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
902
1019
|
}
|
|
903
1020
|
else {
|
|
904
1021
|
ctx.logger.warn("hook_entries_skipped", { event: "Stop", type: "prompt", reason: `stop_prompt_no_evidence:${rendered.reason}` });
|
|
905
|
-
ctx
|
|
1022
|
+
emitHookNotice(ctx, { kind: "hook_decision_unavailable", event: "Stop", reason: "skipped", detail: `no evidence: ${rendered.reason}` });
|
|
906
1023
|
}
|
|
907
1024
|
}
|
|
908
1025
|
}
|
|
@@ -922,6 +1039,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
922
1039
|
}
|
|
923
1040
|
if (run.code !== 0) {
|
|
924
1041
|
ctx.logger.warn("hook_command_failed", { event: "Stop", code: run.code, stderr: clip(run.stderr, 300) });
|
|
1042
|
+
noteHookFailure(ctx, { event: "Stop", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
925
1043
|
continue;
|
|
926
1044
|
}
|
|
927
1045
|
if (llmExtra && entry.type === "prompt" && parseCcVerdict(run.stdout)) {
|
|
@@ -936,11 +1054,11 @@ export function createTaskHooks(config, ctx) {
|
|
|
936
1054
|
block = clip(v.reason?.trim() || "stop condition not satisfied", MAX_HOOK_FEEDBACK_CHARS);
|
|
937
1055
|
continue;
|
|
938
1056
|
}
|
|
939
|
-
const out = parseHookStdout(run.stdout);
|
|
1057
|
+
const out = llmExtra && entry.type === "prompt" ? parseHookStdout(run.stdout) : readHookStdout(run, entry, "Stop", ctx, payload);
|
|
940
1058
|
if (!out) {
|
|
941
1059
|
if (llmExtra && entry.type === "prompt") {
|
|
942
1060
|
ctx.logger.warn("hook_llm_unparsed_verdict", { event: "Stop", head: clip(run.stdout.trim(), 200) });
|
|
943
|
-
ctx
|
|
1061
|
+
emitHookNotice(ctx, { kind: "hook_decision_unavailable", event: "Stop", reason: "unparsed" });
|
|
944
1062
|
}
|
|
945
1063
|
continue;
|
|
946
1064
|
}
|
|
@@ -991,15 +1109,16 @@ export function createTaskHooks(config, ctx) {
|
|
|
991
1109
|
custom_instructions: pctx.customInstructions ?? null,
|
|
992
1110
|
};
|
|
993
1111
|
const singles = await runMatchingEntries("PreCompact", preCompactEntries, pctx.trigger, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
|
|
994
|
-
for (const { run } of singles) {
|
|
1112
|
+
for (const { entry, run } of singles) {
|
|
995
1113
|
if (run.code === 2) {
|
|
996
1114
|
return { block: clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
|
|
997
1115
|
}
|
|
998
1116
|
if (run.code !== 0) {
|
|
999
1117
|
ctx.logger.warn("hook_command_failed", { event: "PreCompact", code: run.code, stderr: clip(run.stderr, 300) });
|
|
1118
|
+
noteHookFailure(ctx, { event: "PreCompact", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
|
|
1000
1119
|
continue;
|
|
1001
1120
|
}
|
|
1002
|
-
const out =
|
|
1121
|
+
const out = readHookStdout(run, entry, "PreCompact", ctx, payload);
|
|
1003
1122
|
if (!out)
|
|
1004
1123
|
continue;
|
|
1005
1124
|
if (typeof out.systemMessage === "string")
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -237,6 +237,30 @@ export declare function noteResumeClientGone(res: ServerResponse, out: {
|
|
|
237
237
|
}, logger: {
|
|
238
238
|
info?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
239
239
|
} | undefined): void;
|
|
240
|
+
/**
|
|
241
|
+
* #359②(A-075.6)—— resume 族四腿(`/decide` / `/assistant/tasks/:id/resume` / `/plan_review` /
|
|
242
|
+
* `/wake`)的**唯一**回执发送口。
|
|
243
|
+
*
|
|
244
|
+
* 为什么是一个函数而不是「在四处各加一行 `res.setHeader`」:这四条腿此前已经各自记得调
|
|
245
|
+
* {@link noteResumeClientGone},而那正是「靠人记得」的形 —— S-1 落地时就漏挂过一腿(三腿有、
|
|
246
|
+
* `/decide` 没有,见 `approvals-assistant.ts` 那条注)。把留痕 + 退避头 + 发送折进一次调用,
|
|
247
|
+
* 第五条腿加进来时只有一个正确写法。
|
|
248
|
+
*
|
|
249
|
+
* 🔴 **`Retry-After` 的判据与体里那份同源**:头只在体**真带** `retryAfterSec` 且是**有限非负数**时发。
|
|
250
|
+
* 全仓每一处带 `retryAfterSec` 的响应(429 `limit.rate_exceeded` / `limit.cost_quota_exceeded` /
|
|
251
|
+
* `quota_exhausted` / `usage.window_exhausted` / rule-import retry)都同发这个标准头,唯独 resume 族的
|
|
252
|
+
* retriable 409 漏了。头与体不是重复,是**两个读者**:体那份给应用逻辑,头那份给通道
|
|
253
|
+
* (代理 / SDK / fetch 重试中间件读的是头,读不到就只能盲等或立刻重投 —— 而立刻重投必然又是一次空转)。
|
|
254
|
+
* 体里没有等待 ⇒ 头也不发:凭空补一个退避值就是编造一条服务端并没有做出的时间承诺。
|
|
255
|
+
*
|
|
256
|
+
* @param extraBody 这条腿自己要并进回执的键(今天只有 `/decide` 的 `rememberApplied`)。
|
|
257
|
+
*/
|
|
258
|
+
export declare function sendResumeOutcome(res: ServerResponse, out: {
|
|
259
|
+
status: number;
|
|
260
|
+
body: object;
|
|
261
|
+
}, logger: {
|
|
262
|
+
info?: (msg: string, fields?: Record<string, unknown>) => void;
|
|
263
|
+
} | undefined, extraBody?: Record<string, unknown>): void;
|
|
240
264
|
export interface RouteCtxBase {
|
|
241
265
|
deps: FlatServiceDeps;
|
|
242
266
|
registry: RunRegistry;
|
package/dist/http/route-ctx.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sendJson } from "./send.js";
|
|
1
2
|
export function noteResumeClientGone(res, out, logger) {
|
|
2
3
|
if (out.status !== 200 || out.body.status !== "resuming")
|
|
3
4
|
return;
|
|
@@ -9,6 +10,13 @@ export function noteResumeClientGone(res, out, logger) {
|
|
|
9
10
|
...(out.body.sessionId !== undefined ? { sessionId: out.body.sessionId } : {}),
|
|
10
11
|
});
|
|
11
12
|
}
|
|
13
|
+
export function sendResumeOutcome(res, out, logger, extraBody) {
|
|
14
|
+
noteResumeClientGone(res, out, logger);
|
|
15
|
+
const wait = out.body.retryAfterSec;
|
|
16
|
+
if (typeof wait === "number" && Number.isFinite(wait) && wait >= 0)
|
|
17
|
+
res.setHeader("retry-after", String(Math.ceil(wait)));
|
|
18
|
+
sendJson(res, out.status, extraBody === undefined ? out.body : { ...out.body, ...extraBody });
|
|
19
|
+
}
|
|
12
20
|
export const RUN_NOT_FOUND_MESSAGE = "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)";
|
|
13
21
|
export function runNotFoundMessage(id) {
|
|
14
22
|
const aStar = /^(?:a|wa)[0-9a-f]{4,}$/i.test(id);
|
|
@@ -9,7 +9,7 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
|
9
9
|
import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
|
|
10
10
|
import { gatedPrincipal, explicitOperatorOk, isOperator } from "../principal-gate.js";
|
|
11
11
|
import { governanceOriginOf } from "../active-run-conflict.js";
|
|
12
|
-
import {
|
|
12
|
+
import { sendResumeOutcome } from "../route-ctx.js";
|
|
13
13
|
export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
|
|
14
14
|
export const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
|
|
15
15
|
export const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
|
|
@@ -249,8 +249,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
251
251
|
const out = await resumePreempted(run.sessionId, req, true);
|
|
252
|
-
|
|
253
|
-
sendJson(res, out.status, out.body);
|
|
252
|
+
sendResumeOutcome(res, out, deps.logger);
|
|
254
253
|
return;
|
|
255
254
|
}
|
|
256
255
|
const planReviewMatch = req.method === "POST" ? ASSISTANT_PLAN_REVIEW_RE.exec(url) : null;
|
|
@@ -316,8 +315,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
316
315
|
return;
|
|
317
316
|
}
|
|
318
317
|
const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true);
|
|
319
|
-
|
|
320
|
-
sendJson(res, out.status, out.body);
|
|
318
|
+
sendResumeOutcome(res, out, deps.logger);
|
|
321
319
|
return;
|
|
322
320
|
}
|
|
323
321
|
const m = /^\/v1\/approvals\/([^/]+)\/decide$/.exec(url);
|
|
@@ -442,12 +440,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
442
440
|
}
|
|
443
441
|
: undefined;
|
|
444
442
|
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, "human", req, answer, binding, grantOnCommit, true);
|
|
445
|
-
|
|
446
|
-
if (remember && deps.approvalExemptionStore) {
|
|
447
|
-
sendJson(res, out.status, { ...out.body, rememberApplied });
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
sendJson(res, out.status, out.body);
|
|
443
|
+
sendResumeOutcome(res, out, deps.logger, remember && deps.approvalExemptionStore ? { rememberApplied } : undefined);
|
|
451
444
|
return;
|
|
452
445
|
}
|
|
453
446
|
sendError(res, 404, "not_found.route", "not found");
|
|
@@ -49,7 +49,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
49
49
|
sessions: Boolean(deps.sessionAudit),
|
|
50
50
|
sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
|
|
51
51
|
fleet: deps.fleetBus
|
|
52
|
-
? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory" }
|
|
52
|
+
? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory", hookFailureNotice: true }
|
|
53
53
|
: false,
|
|
54
54
|
workspace: (() => {
|
|
55
55
|
const wfs = deps.fileSnapshotStore;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hookNoticeWire } from "../../fleet/fleet-bus.js";
|
|
1
2
|
import { FLEET_SNAPSHOT_TERMINAL_MAX_ROWS, isTerminalWorkflowRowStatus, recentTerminalWorkflowRows } from "../../fleet/fleet-terminal-window.js";
|
|
2
3
|
import { taskNotificationFoldKey } from "../../orchestration/workflow-completion-inbox.js";
|
|
3
4
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
@@ -142,8 +143,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
|
|
|
142
143
|
const hScopeOk = callerScope === null || hn.ownerScope === callerScope;
|
|
143
144
|
const hSessionOk = callerSession === null || hn.ownerSessionId === callerSession;
|
|
144
145
|
if (hScopeOk && hSessionOk) {
|
|
145
|
-
|
|
146
|
-
send("hook_notice", { type: "hook_notice", ...wire, ts: frame.ts });
|
|
146
|
+
send("hook_notice", { type: "hook_notice", ...hookNoticeWire(hn), ts: frame.ts });
|
|
147
147
|
}
|
|
148
148
|
break;
|
|
149
149
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { taskNotificationInboxEntry } from "../../orchestration/workflow-completion-inbox.js";
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
3
|
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
4
|
-
import {
|
|
4
|
+
import { sendResumeOutcome } from "../route-ctx.js";
|
|
5
5
|
export const SESSION_WAKE_RE = /^\/v1\/sessions\/[^/]+\/wake$/;
|
|
6
6
|
export async function handleNotifyWake(req, res, url, ctx) {
|
|
7
7
|
const miss = { fell: false };
|
|
@@ -129,8 +129,7 @@ async function handleNotifyWakeBody(req, res, url, ctx, miss) {
|
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
const out = await resumeWake(wakeSession, wakeBody.message, principal, req, true);
|
|
132
|
-
|
|
133
|
-
sendJson(res, out.status, out.body);
|
|
132
|
+
sendResumeOutcome(res, out, deps.logger);
|
|
134
133
|
return;
|
|
135
134
|
}
|
|
136
135
|
miss.fell = true;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -21,6 +21,7 @@ import { normalizeRunEventType } from "../../trace/project.js";
|
|
|
21
21
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
22
22
|
import { buildActiveRunConflict } from "../active-run-conflict.js";
|
|
23
23
|
import { clearTurnActivity, readTurnActivityMs } from "../../turn-activity.js";
|
|
24
|
+
import { buildCrossSliceUsage, readResourceWindow, recordResourceWindow } from "../../resource-window.js";
|
|
24
25
|
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
25
26
|
import { ActorAssertionWire } from "@sema-agent/registry-core";
|
|
26
27
|
import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
|
|
@@ -192,7 +193,20 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
192
193
|
const infraRates = deps.config.infraCostRates;
|
|
193
194
|
const needCost = Boolean(run.result?.stats && !stale && infraRates && hasInfraPricing(infraRates));
|
|
194
195
|
const needSuggestions = !stale && run.status === "completed";
|
|
195
|
-
const
|
|
196
|
+
const resourceWindow = run.status === "running" && !stale ? readResourceWindow(run.taskId) : undefined;
|
|
197
|
+
let events;
|
|
198
|
+
let eventsUnavailable = false;
|
|
199
|
+
if (needCost || needSuggestions || resourceWindow !== undefined) {
|
|
200
|
+
if (deps.runStore.getEvents) {
|
|
201
|
+
events = await deps.runStore.getEvents(taskId, 0).catch(() => {
|
|
202
|
+
eventsUnavailable = true;
|
|
203
|
+
return [];
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
eventsUnavailable = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
196
210
|
let supervisorCost;
|
|
197
211
|
if (needCost && events && run.result?.stats) {
|
|
198
212
|
const durMs = new Date(run.updatedAt).getTime() - new Date(run.createdAt).getTime();
|
|
@@ -208,11 +222,19 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
208
222
|
suggestions = arr.map((s) => String(s));
|
|
209
223
|
}
|
|
210
224
|
const lastActivityAt = run.status === "running" && !stale ? readTurnActivityMs(run.taskId) : undefined;
|
|
225
|
+
let crossSliceUsage;
|
|
226
|
+
if (resourceWindow !== undefined) {
|
|
227
|
+
if (eventsUnavailable || events === undefined)
|
|
228
|
+
recordFailOpen("server.runs.cross-slice-usage-unavailable");
|
|
229
|
+
else
|
|
230
|
+
crossSliceUsage = buildCrossSliceUsage(resourceWindow, events);
|
|
231
|
+
}
|
|
211
232
|
sendJson(res, 200, {
|
|
212
233
|
taskId: run.taskId,
|
|
213
234
|
sessionId: run.sessionId,
|
|
214
235
|
status: stale ? "failed" : run.status,
|
|
215
236
|
...(lastActivityAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastActivityAt) } : {}),
|
|
237
|
+
...(crossSliceUsage !== undefined ? { crossSliceUsage } : {}),
|
|
216
238
|
result: run.result ?? undefined,
|
|
217
239
|
supervisorCost,
|
|
218
240
|
suggestions,
|
|
@@ -1070,7 +1092,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1070
1092
|
}
|
|
1071
1093
|
const { status, body: respBody } = await deps.toolApproval.respond(id, principal, body, req);
|
|
1072
1094
|
const retryAfterSec = respBody.retryAfterSec;
|
|
1073
|
-
if (
|
|
1095
|
+
if (typeof retryAfterSec === "number" && Number.isFinite(retryAfterSec) && retryAfterSec >= 0) {
|
|
1074
1096
|
res.setHeader("retry-after", String(Math.ceil(retryAfterSec)));
|
|
1075
1097
|
}
|
|
1076
1098
|
sendJson(res, status, respBody);
|
|
@@ -1330,6 +1352,8 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
|
|
|
1330
1352
|
const created = await runStore.createRun(taskId, sessionId, prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
1331
1353
|
if (created.ok)
|
|
1332
1354
|
clearTurnActivity(taskId);
|
|
1355
|
+
if (created.ok)
|
|
1356
|
+
recordResourceWindow(taskId, prepared.spec.resourceSuspend);
|
|
1333
1357
|
if (created.ok)
|
|
1334
1358
|
deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
1335
1359
|
if (!created.ok) {
|
|
@@ -7,6 +7,7 @@ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotific
|
|
|
7
7
|
import { createLedgerSink } from "../../trace/ledger-sink.js";
|
|
8
8
|
import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
|
|
9
9
|
import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
|
|
10
|
+
import { recordResourceWindow } from "../../resource-window.js";
|
|
10
11
|
import { redactSecrets } from "../../trace/redact.js";
|
|
11
12
|
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
|
|
12
13
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
@@ -192,6 +193,8 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
192
193
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
193
194
|
if (created.ok)
|
|
194
195
|
clearTurnActivity(tid);
|
|
196
|
+
if (created.ok)
|
|
197
|
+
recordResourceWindow(tid, prepared.spec.resourceSuspend);
|
|
195
198
|
if (created.ok)
|
|
196
199
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
197
200
|
if (!created.ok) {
|
|
@@ -715,6 +718,8 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
715
718
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
716
719
|
if (created.ok)
|
|
717
720
|
clearTurnActivity(tid);
|
|
721
|
+
if (created.ok)
|
|
722
|
+
recordResourceWindow(tid, prepared.spec.resourceSuspend);
|
|
718
723
|
if (!created.ok)
|
|
719
724
|
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) };
|
|
720
725
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
package/dist/http/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import { normalizeApproachNotice, validateTaskAgents } from "../spec-fields.js";
|
|
|
14
14
|
import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
15
15
|
import { runInBackground, evictIfConflict, stripCheckpointToken, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
16
16
|
import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
|
|
17
|
+
import { recordResumeResourceWindow } from "../resource-window.js";
|
|
17
18
|
import { publicStoreProbeError } from "../store-live-probe.js";
|
|
18
19
|
import { looksLikeJwt } from "@sema-agent/registry-core/api/auth-bridge";
|
|
19
20
|
import {} from "../orchestration/workflow-agent-steer.js";
|
|
@@ -1140,6 +1141,8 @@ export function createHttpServer(rawDeps) {
|
|
|
1140
1141
|
? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: taskId, ...fleetRunLabels(resumeObjective) })
|
|
1141
1142
|
: undefined;
|
|
1142
1143
|
const resumeTaskConfig = taskId ? { ...taskConfig, taskId } : taskConfig;
|
|
1144
|
+
if (taskId && resumeTaskConfig.resourceSuspend)
|
|
1145
|
+
recordResumeResourceWindow(taskId, resumeTaskConfig.resourceSuspend);
|
|
1143
1146
|
let fleetSettled = false;
|
|
1144
1147
|
const settleFleet = (status, residuals) => {
|
|
1145
1148
|
if (fleetSettled)
|
|
@@ -1608,6 +1611,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1608
1611
|
return {
|
|
1609
1612
|
status,
|
|
1610
1613
|
body: {
|
|
1614
|
+
...(taskId !== undefined ? { taskId } : {}),
|
|
1611
1615
|
error: e.message,
|
|
1612
1616
|
errorCode: e.code === "checkpoint.invalid_outcome"
|
|
1613
1617
|
? (outcome.gate === "policy_ask" ? "approval_binding_mismatch" : "resume_outcome_invalid")
|