@sema-agent/core 5.12.0 → 5.14.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 (101) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +4 -0
  4. package/dist/agents/subagent.js +137 -43
  5. package/dist/brain/anthropic.js +33 -10
  6. package/dist/brain/context-overflow.d.ts +20 -0
  7. package/dist/brain/context-overflow.js +58 -0
  8. package/dist/brain/open-responses.js +24 -10
  9. package/dist/brain/openai.js +29 -11
  10. package/dist/brain/request-params.d.ts +2 -0
  11. package/dist/brain/request-params.js +16 -0
  12. package/dist/brain/stream-engine.d.ts +9 -1
  13. package/dist/brain/stream-engine.js +256 -27
  14. package/dist/brain/timeout.d.ts +1 -0
  15. package/dist/brain/timeout.js +1 -0
  16. package/dist/core/a2a.d.ts +2 -2
  17. package/dist/core/a2a.js +3 -3
  18. package/dist/core/ask-question.d.ts +47 -2
  19. package/dist/core/ask-question.js +209 -28
  20. package/dist/core/background-agent-store.d.ts +2 -0
  21. package/dist/core/checkpoint-store.d.ts +49 -18
  22. package/dist/core/checkpoint-store.js +117 -4
  23. package/dist/core/compliance.d.ts +11 -0
  24. package/dist/core/compliance.js +34 -0
  25. package/dist/core/governance-codes.d.ts +12 -0
  26. package/dist/core/governance-codes.js +24 -0
  27. package/dist/core/hooks.d.ts +24 -2
  28. package/dist/core/hooks.js +97 -10
  29. package/dist/core/human-input-projection.d.ts +12 -0
  30. package/dist/core/human-input-projection.js +27 -0
  31. package/dist/core/locked-config.d.ts +27 -0
  32. package/dist/core/locked-config.js +42 -0
  33. package/dist/core/mcp.d.ts +7 -2
  34. package/dist/core/mcp.js +7 -7
  35. package/dist/core/memory-admission.d.ts +51 -0
  36. package/dist/core/memory-admission.js +159 -0
  37. package/dist/core/memory.d.ts +2 -0
  38. package/dist/core/memory.js +3 -2
  39. package/dist/core/retention.d.ts +36 -0
  40. package/dist/core/retention.js +31 -0
  41. package/dist/core/runner/assemble-result.d.ts +1 -0
  42. package/dist/core/runner/assemble-result.js +2 -2
  43. package/dist/core/runner/prepare-memory.d.ts +9 -0
  44. package/dist/core/runner/prepare-memory.js +28 -2
  45. package/dist/core/runner/prepare-task.d.ts +18 -6
  46. package/dist/core/runner/prepare-task.js +428 -38
  47. package/dist/core/runner/runtask.d.ts +3 -6
  48. package/dist/core/runner/runtask.js +258 -77
  49. package/dist/core/runner/tool-output-projection.js +1 -0
  50. package/dist/core/session-store.d.ts +4 -0
  51. package/dist/core/session-store.js +5 -0
  52. package/dist/core/session.d.ts +1 -0
  53. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  54. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  55. package/dist/core/task-notification.d.ts +2 -0
  56. package/dist/core/task-notification.js +5 -3
  57. package/dist/core/task-registry-agent.d.ts +1 -0
  58. package/dist/core/task-registry-agent.js +6 -0
  59. package/dist/core/task-registry.d.ts +1 -0
  60. package/dist/core/task-registry.js +4 -1
  61. package/dist/core/tool-policy.d.ts +5 -0
  62. package/dist/core/tool-policy.js +2 -1
  63. package/dist/core/tool-result-store.d.ts +2 -0
  64. package/dist/core/tool-result-store.js +1 -0
  65. package/dist/core/types.d.ts +38 -2
  66. package/dist/core/wiring-manifest.d.ts +97 -0
  67. package/dist/core/wiring-manifest.js +186 -0
  68. package/dist/engine/compaction/compaction.js +2 -2
  69. package/dist/engine/harness/agent-harness.d.ts +2 -1
  70. package/dist/engine/harness/agent-harness.js +19 -2
  71. package/dist/engine/harness/types.d.ts +3 -1
  72. package/dist/engine/llm/types.d.ts +7 -0
  73. package/dist/engine/llm/types.js +8 -1
  74. package/dist/engine/llm/validation.js +11 -1
  75. package/dist/engine/session/import-validate.d.ts +6 -1
  76. package/dist/engine/session/import-validate.js +29 -6
  77. package/dist/engine/session/memory-repo.d.ts +3 -1
  78. package/dist/engine/session/memory-repo.js +2 -2
  79. package/dist/index.d.ts +12 -4
  80. package/dist/index.js +12 -4
  81. package/dist/internal/harness-types.d.ts +1 -1
  82. package/dist/internal/llm.d.ts +2 -2
  83. package/dist/internal/llm.js +1 -1
  84. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  85. package/dist/orchestration/run-workflow-tool.js +3 -0
  86. package/dist/orchestration/workflow-types.d.ts +8 -0
  87. package/dist/orchestration/workflow-types.js +14 -0
  88. package/dist/orchestration/workflow.d.ts +4 -0
  89. package/dist/orchestration/workflow.js +134 -5
  90. package/dist/prompts/default.js +1 -1
  91. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  92. package/dist/stores/file/checkpoint-store.js +31 -2
  93. package/dist/stores/file/index.js +1 -1
  94. package/dist/stores/file/session-store.d.ts +3 -1
  95. package/dist/stores/file/session-store.js +2 -2
  96. package/dist/stores/file/shared-ledger.js +8 -1
  97. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  98. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  99. package/dist/tools/fs/fs-bash.js +31 -12
  100. package/dist/tools/fs/safety.js +34 -10
  101. package/package.json +1 -1
@@ -81,6 +81,7 @@ export interface RunWorkflowToolDeps {
81
81
  sourceTaskId?: string;
82
82
  principal?: string;
83
83
  oneShot?: boolean;
84
+ parentInteractionPosture?: "interactive" | "headless";
84
85
  workflowDepth?: number;
85
86
  parentCwd?: string;
86
87
  parentThinking?: () => import("../core/types.js").TaskSpec["thinking"];
@@ -88,5 +89,8 @@ export interface RunWorkflowToolDeps {
88
89
  parentGetApiKeyAndHeaders?: import("../core/types.js").TaskSpec["getApiKeyAndHeaders"];
89
90
  forwardEvent?: (event: import("../core/types.js").TaskEvent) => void;
90
91
  inheritedGateForChildren?: () => import("../core/runner/prepare-task.js").InheritedGate;
92
+ autoModeReview?: () => {
93
+ decider: import("../core/auto-mode.js").AutoModeDecider;
94
+ } | undefined;
91
95
  }
92
96
  export declare function createRunWorkflowTool(d: RunWorkflowToolDeps): Promise<AgentTool>;
@@ -388,6 +388,7 @@ export async function createRunWorkflowTool(d) {
388
388
  }
389
389
  let handle;
390
390
  const workflowTaskId = d.taskRegistry?.mintTaskId("workflow");
391
+ const autoModeReview = ctx.autoModeReview ?? d.autoModeReview?.();
391
392
  try {
392
393
  handle = startWorkflow(d.runner, scriptFn, {
393
394
  store: d.store,
@@ -413,6 +414,7 @@ export async function createRunWorkflowTool(d) {
413
414
  ...(d.parentCwd !== undefined ? { parentCwd: d.parentCwd } : {}),
414
415
  parentToolCallId: ctx.toolCallId,
415
416
  ...(sourceTaskId !== undefined ? { parentTaskId: sourceTaskId } : {}),
417
+ ...((ctx.interactionPosture ?? d.parentInteractionPosture) !== undefined ? { interactionPosture: (ctx.interactionPosture ?? d.parentInteractionPosture) } : {}),
416
418
  ...(d.parentModel !== undefined ? { defaultModel: d.parentModel } : {}),
417
419
  ...(d.parentGetApiKeyAndHeaders !== undefined ? { defaultGetApiKeyAndHeaders: d.parentGetApiKeyAndHeaders } : {}),
418
420
  ...(ctx.centerArtifactDigest !== undefined ? { parentCenterArtifactDigest: ctx.centerArtifactDigest } : {}),
@@ -421,6 +423,7 @@ export async function createRunWorkflowTool(d) {
421
423
  ...((ctx.inheritedGateForChildren ?? d.inheritedGateForChildren) !== undefined
422
424
  ? { inheritedGate: (ctx.inheritedGateForChildren ?? d.inheritedGateForChildren)() }
423
425
  : {}),
426
+ ...(autoModeReview !== undefined ? { autoModeReview } : {}),
424
427
  }, { workflowDepth: d.workflowDepth });
425
428
  }
426
429
  catch (err) {
@@ -187,6 +187,14 @@ export declare class WorkflowAgentStalledError extends Error {
187
187
  readonly code = "workflow.agent_stalled";
188
188
  constructor(attempts: number, stallMs: number, lastResult?: TaskResult | undefined);
189
189
  }
190
+ export declare const WORKFLOW_SPAWN_BLOCKED_ERROR_CODE = "autoMode.spawn_blocked";
191
+ export declare class WorkflowAgentBlockedError extends Error {
192
+ readonly label: string;
193
+ readonly category: string;
194
+ readonly reason: string;
195
+ readonly code = "workflow.agent_blocked";
196
+ constructor(label: string, category: string, reason: string);
197
+ }
190
198
  export declare class WorkflowMaxAgentsError extends Error {
191
199
  readonly max: number;
192
200
  readonly code = "workflow.max_agents";
@@ -41,6 +41,20 @@ export class WorkflowAgentStalledError extends Error {
41
41
  this.name = "WorkflowAgentStalledError";
42
42
  }
43
43
  }
44
+ export const WORKFLOW_SPAWN_BLOCKED_ERROR_CODE = "autoMode.spawn_blocked";
45
+ export class WorkflowAgentBlockedError extends Error {
46
+ label;
47
+ category;
48
+ reason;
49
+ code = "workflow.agent_blocked";
50
+ constructor(label, category, reason) {
51
+ super(`workflow agent "${label}" was not started: blocked by the pre-spawn review${reason ? `: ${reason}` : ""}`);
52
+ this.label = label;
53
+ this.category = category;
54
+ this.reason = reason;
55
+ this.name = "WorkflowAgentBlockedError";
56
+ }
57
+ }
44
58
  export class WorkflowMaxAgentsError extends Error {
45
59
  max;
46
60
  code = "workflow.max_agents";
@@ -96,12 +96,16 @@ export interface RunWorkflowOptions {
96
96
  parentCwd?: string;
97
97
  parentToolCallId?: string;
98
98
  parentTaskId?: string;
99
+ interactionPosture?: "interactive" | "headless";
99
100
  defaultModel?: () => import("../internal/llm.js").Model | undefined;
100
101
  defaultGetApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
101
102
  parentCenterArtifactDigest?: string;
102
103
  parentCenterSourceRevision?: string;
103
104
  onForwardEvent?: (event: TaskEvent) => void;
104
105
  inheritedGate?: import("../core/runner/prepare-task.js").InheritedGate;
106
+ autoModeReview?: {
107
+ decider: import("../core/auto-mode.js").AutoModeDecider;
108
+ };
105
109
  phases?: ReadonlyArray<{
106
110
  title: string;
107
111
  detail?: string;
@@ -16,12 +16,43 @@ import { boundedRedactedSummary } from "../core/untrusted-egress.js";
16
16
  import { delimitUntrusted } from "../core/untrusted-text.js";
17
17
  import { OUTPUT_TOOL_NAME } from "../core/runner/synthetic-tools.js";
18
18
  import { compileOutputSchema } from "../core/runner/strict-output-schema.js";
19
- import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError } from "./workflow-types.js";
19
+ import { WorkflowBudgetExceededError, WorkflowNestingError, WorkflowAgentBlockedError, WorkflowAgentSchemaError, WorkflowAgentStalledError, WorkflowMaxAgentsError, WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } from "./workflow-types.js";
20
20
  export * from "./workflow-types.js";
21
21
  const MAX_TRANSCRIPT_CHARS = 4000;
22
22
  const WORKFLOW_RESULT_MAX = 4000;
23
23
  const WORKFLOW_RESULT_FULL_MAX = 200_000;
24
24
  const MAX_ACTIVITY = 30;
25
+ const MAX_REVIEWED_SCHEMA_BYTES = 4096;
26
+ const MAX_REVIEW_REASON_CHARS = 500;
27
+ const MAX_REVIEWED_PROMPT_CHARS = 2_000;
28
+ const MAX_SCHEMA_WALK_DEPTH = 32;
29
+ function isUsableSchema(v) {
30
+ if (v === true)
31
+ return true;
32
+ return typeof v === "object" && v !== null && !Array.isArray(v);
33
+ }
34
+ function isSingleFaced(v, depth = 0) {
35
+ if (typeof v !== "object" || v === null)
36
+ return true;
37
+ if (depth > MAX_SCHEMA_WALK_DEPTH)
38
+ return false;
39
+ if (typeof v.toJSON === "function")
40
+ return false;
41
+ for (const key of Object.keys(v)) {
42
+ if (typeof Object.getOwnPropertyDescriptor(v, key)?.get === "function")
43
+ return false;
44
+ }
45
+ for (const child of Object.values(v)) {
46
+ if (!isSingleFaced(child, depth + 1))
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+ function forReview(text) {
52
+ if (text.length <= MAX_REVIEWED_PROMPT_CHARS)
53
+ return text;
54
+ return `${text.slice(0, MAX_REVIEWED_PROMPT_CHARS)}\n[…TRUNCATED FOR REVIEW: ${text.length - MAX_REVIEWED_PROMPT_CHARS} further characters follow that the child WILL receive and this review did NOT see]`;
55
+ }
25
56
  function workflowModelLabel(spec) {
26
57
  const model = spec.model;
27
58
  if (model === undefined)
@@ -473,6 +504,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
473
504
  const sem = createSemaphore(concurrency);
474
505
  const spawnAttribution = {
475
506
  isDelegatedChild: true,
507
+ ...(opts.interactionPosture !== undefined ? { parentInteractionPosture: opts.interactionPosture } : {}),
476
508
  ...(opts.parentToolCallId !== undefined ? { parentToolCallId: opts.parentToolCallId } : {}),
477
509
  ...(opts.parentTaskId !== undefined ? { parentTaskId: opts.parentTaskId } : {}),
478
510
  ...(opts.parentCenterArtifactDigest !== undefined ? { parentCenterArtifactDigest: opts.parentCenterArtifactDigest } : {}),
@@ -782,6 +814,83 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
782
814
  const model = workflowModelLabel(specForIdentity);
783
815
  return { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model };
784
816
  };
817
+ const reviewSpawnBeforeLaunch = async (lane, label, callKey, runSpec, effectiveSignal) => {
818
+ const review = opts.autoModeReview;
819
+ if (review === undefined)
820
+ return runSpec;
821
+ const schema = runSpec.outputSchema;
822
+ let schemaText;
823
+ let specToLaunch = runSpec;
824
+ if (schema !== undefined) {
825
+ try {
826
+ schemaText = JSON.stringify(schema);
827
+ }
828
+ catch {
829
+ schemaText = undefined;
830
+ }
831
+ if (schemaText === undefined) {
832
+ throw new WorkflowAgentBlockedError(label, "", "the child's output schema could not be serialized for review");
833
+ }
834
+ const schemaBytes = Buffer.byteLength(schemaText, "utf8");
835
+ if (schemaBytes > MAX_REVIEWED_SCHEMA_BYTES) {
836
+ throw new WorkflowAgentBlockedError(label, "", `output schema too large to classify safely (${schemaBytes} bytes, cap ${MAX_REVIEWED_SCHEMA_BYTES})`);
837
+ }
838
+ if (!isSingleFaced(schema)) {
839
+ throw new WorkflowAgentBlockedError(label, "", "the child's output schema cannot be read as one unambiguous value (it overrides its own serialization, or nests deeper than the review can walk)");
840
+ }
841
+ const normalized = JSON.parse(schemaText);
842
+ if (!isUsableSchema(normalized)) {
843
+ throw new WorkflowAgentBlockedError(label, "", "the child's output schema does not normalize to a JSON Schema object");
844
+ }
845
+ specToLaunch = { ...runSpec, outputSchema: normalized };
846
+ }
847
+ const toolNames = (runSpec.tools ?? []).map((t) => t.name);
848
+ const toolsNote = `${toolNames.length > 0 ? toolNames.join(", ") : "(none explicitly listed)"}` +
849
+ ` — this is the EXPLICIT roster only. If this deployment gave the child a real execution environment,` +
850
+ ` its own prepare additionally mounts the standard file/shell toolkit (Read/Edit/Write/Bash/Grep/Glob)` +
851
+ ` on top of this list. Treat the absence of a tool here as unknown, not as denied.`;
852
+ const remoteSources = [
853
+ ...(runSpec.mcp ?? []).map((m) => `mcp:${m.name}`),
854
+ ...(runSpec.a2a ?? []).map((p) => `a2a:${p.name}`),
855
+ ];
856
+ const shownSources = remoteSources.slice(0, 20).map((s) => s.slice(0, 80));
857
+ const remoteNote = remoteSources.length > 0
858
+ ? `\nThis child also mounts tools from ${remoteSources.length} external capability source(s) — ${shownSources.join(", ")}${remoteSources.length > shownSources.length ? `, +${remoteSources.length - shownSources.length} more` : ""}. Each mounts its own tools under a "<source>__*" namespace; those tool names are not resolved yet and are NOT in the roster above. Treat this child as able to reach outside this process.`
859
+ : "";
860
+ const objective = forReview(runSpec.objective ?? "");
861
+ const systemPrompt = runSpec.systemPrompt !== undefined ? forReview(runSpec.systemPrompt) : undefined;
862
+ const imageCount = runSpec.images?.length ?? 0;
863
+ const imagesNote = imageCount > 0
864
+ ? `\n${imageCount} image attachment(s) ride with this child. Their CONTENT was NOT inspected by this review — the classifier leg is text-only. Treat any instruction that defers to attached or embedded content as unreviewed.`
865
+ : "";
866
+ const labelForReview = label.slice(0, 80);
867
+ const verdict = await review.decider
868
+ .decide({
869
+ req: {
870
+ toolName: "Workflow(agent)",
871
+ args: {
872
+ objective,
873
+ tools: toolNames,
874
+ ...(systemPrompt !== undefined ? { systemPrompt } : {}),
875
+ ...(schemaText !== undefined ? { outputSchema: schemaText } : {}),
876
+ ...(imageCount > 0 ? { uninspectedImageAttachments: imageCount } : {}),
877
+ ...(remoteSources.length > 0 ? { externalCapabilitySources: shownSources } : {}),
878
+ },
879
+ toolCallId: `${runId}:${callKey}`,
880
+ },
881
+ askMessage: `Reviewing a workflow-spawned sub-agent ("${labelForReview}", ${lane}) about to be started. ` +
882
+ `Objective: ${objective}\nTools available to it: ${toolsNote}${remoteNote}${imagesNote}`,
883
+ }, effectiveSignal)
884
+ .catch(() => ({ kind: "unavailable", cause: "error" }));
885
+ if (effectiveSignal?.aborted)
886
+ throw new Error("workflow aborted");
887
+ if (finalized)
888
+ throw new Error(`workflow run already finalized — ${lane} cannot spawn after the run ended`);
889
+ if (verdict.kind === "block") {
890
+ throw new WorkflowAgentBlockedError(label, verdict.category, boundedRedactedSummary(verdict.reason, MAX_REVIEW_REASON_CHARS));
891
+ }
892
+ return specToLaunch;
893
+ };
785
894
  const createUsageBeat = (rec) => {
786
895
  let beatTokens = 0;
787
896
  let beatTurns = 0;
@@ -931,15 +1040,16 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
931
1040
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
932
1041
  const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
933
1042
  const authInherit = framedSpec.getApiKeyAndHeaders === undefined && opts.defaultGetApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.defaultGetApiKeyAndHeaders } : {};
934
- const runSpec = agentOpts.schema
1043
+ const preReviewSpec = agentOpts.schema
935
1044
  ? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
936
1045
  : { ...framedSpec, ...authInherit, signal: effectiveSignal };
1046
+ const runSpec = await reviewSpawnBeforeLaunch("ctx.agent", label, callKey, preReviewSpec, effectiveSignal);
937
1047
  const enrichedForward = opts.onForwardEvent !== undefined
938
1048
  ? (e) => {
939
1049
  opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
940
1050
  }
941
1051
  : undefined;
942
- const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
1052
+ const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), delegationTaskType: "workflow", agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
943
1053
  let attempts = 0;
944
1054
  let throttleRetried = false;
945
1055
  let lastAttemptReason;
@@ -1128,6 +1238,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1128
1238
  rec.output = boundedRedactedSummary(salvaged.structuredOutput ?? salvaged.result, MAX_TRANSCRIPT_CHARS) || (rec.errorMessage ?? "");
1129
1239
  rec.stats = { tokens: salvaged.stats.tokens ?? 0, turns: salvaged.stats.turns ?? 0, costMicroUsd: salvaged.stats.costMicroUsd };
1130
1240
  }
1241
+ if (err instanceof WorkflowAgentBlockedError) {
1242
+ rec.errorCode = WORKFLOW_SPAWN_BLOCKED_ERROR_CODE;
1243
+ rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
1244
+ }
1131
1245
  if (err instanceof WorkflowAgentStalledError && err.attempts > 1) {
1132
1246
  rec.attempts = err.attempts;
1133
1247
  rec.lastAttemptReason = "stalled";
@@ -1139,6 +1253,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1139
1253
  sessionId: "",
1140
1254
  status: "failed",
1141
1255
  result: boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500),
1256
+ ...(err instanceof WorkflowAgentBlockedError ? { errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE } : {}),
1142
1257
  stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1143
1258
  }, label).catch(() => undefined);
1144
1259
  bceTerminal(callKey, "failed", rec.output ?? (err instanceof Error ? err.message : String(err)), rec.sessionId, rec.stats);
@@ -1184,7 +1299,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1184
1299
  if (!finalized) {
1185
1300
  rec.status = "failed";
1186
1301
  rec.endedAt = now();
1187
- emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: "failed", ts: rec.endedAt });
1302
+ emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: "failed", ...(rec.errorCode !== undefined ? { errorCode: rec.errorCode } : {}), ts: rec.endedAt });
1188
1303
  void persist("update");
1189
1304
  }
1190
1305
  };
@@ -1205,7 +1320,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1205
1320
  const baseRunSpec = agentOpts.schema
1206
1321
  ? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
1207
1322
  : { ...framedSpec, ...authInherit, signal: effectiveSignal };
1208
- const runSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
1323
+ const preReviewSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
1324
+ const runSpec = await reviewSpawnBeforeLaunch("ctx.agentStream", label, callKey, preReviewSpec, effectiveSignal);
1209
1325
  const enrichedForwardS = opts.onForwardEvent !== undefined
1210
1326
  ? (e) => {
1211
1327
  opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
@@ -1216,6 +1332,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1216
1332
  ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}),
1217
1333
  ...spawnAttribution,
1218
1334
  ...(enrichedForwardS !== undefined ? { onForwardEvent: enrichedForwardS } : {}),
1335
+ delegationTaskType: "workflow",
1219
1336
  agentName: label,
1220
1337
  onActivity,
1221
1338
  onWorkspaceResolved: createWorkspaceObserver(rec),
@@ -1236,6 +1353,18 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1236
1353
  stream = workflowDepthStore.run({ depth: depth + 1 }, () => runner.runTaskStream(runSpec, undefined, runInternals));
1237
1354
  }
1238
1355
  catch (err) {
1356
+ if (!finalized && err instanceof WorkflowAgentBlockedError) {
1357
+ rec.errorCode = WORKFLOW_SPAWN_BLOCKED_ERROR_CODE;
1358
+ rec.errorMessage = boundedRedactedSummary(err.message, MAX_TRANSCRIPT_CHARS);
1359
+ void journalAppend(callKey, {
1360
+ taskId: callKey,
1361
+ sessionId: "",
1362
+ status: "failed",
1363
+ result: boundedRedactedSummary(err.message, 500),
1364
+ errorCode: WORKFLOW_SPAWN_BLOCKED_ERROR_CODE,
1365
+ stats: { turns: 0, tokens: 0, costMicroUsd: 0 },
1366
+ }).catch(() => undefined);
1367
+ }
1239
1368
  recordFailed();
1240
1369
  releaseOnce();
1241
1370
  bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
@@ -183,7 +183,7 @@ Use this directory for ALL temporary file needs:
183
183
 
184
184
  Only use \`/tmp\` if the user explicitly requests it.
185
185
 
186
- The scratchpad directory is session-specific, isolated from the user's project, and can generally be used without permission prompts.`;
186
+ The scratchpad directory is session-specific, isolated from the user's project, and can generally be used without permission prompts. Treat it as ephemeral — it may not survive a long suspension or a resume on a different worker; keep durable outputs in the working directory.`;
187
187
  }
188
188
  export const GIT_STATUS_MAX_CHARS = 2000;
189
189
  export function buildGitSnapshot(p) {
@@ -1,9 +1,10 @@
1
- import { type Checkpoint, type CheckpointFaultMode, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "../../core/checkpoint-store.js";
1
+ import { type Checkpoint, type PendingSteerInput, type CheckpointFaultMode, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "../../core/checkpoint-store.js";
2
2
  export interface FileCheckpointStoreOptions {
3
3
  fsync?: boolean;
4
4
  compactEvery?: number;
5
5
  }
6
6
  export declare class FileCheckpointStore implements CheckpointStore {
7
+ readonly durability: "durable";
7
8
  private readonly fsyncEnabled;
8
9
  private readonly compactEvery;
9
10
  private readonly ledger;
@@ -17,10 +18,7 @@ export declare class FileCheckpointStore implements CheckpointStore {
17
18
  get(token: CheckpointToken): Promise<Checkpoint | null>;
18
19
  resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
19
20
  reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
20
- setPendingSteer(token: CheckpointToken, scope: string, steer: {
21
- text: string;
22
- trusted: boolean;
23
- }): Promise<boolean>;
21
+ setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
24
22
  expire(token: CheckpointToken, scope: string): Promise<boolean>;
25
23
  reap(scope: string, cutoff: number): Promise<number>;
26
24
  listByScope(scope: string): Promise<CheckpointSummary[]>;
@@ -1,7 +1,20 @@
1
1
  import { join } from "node:path";
2
- import { CheckpointError, checkpointOccMatches, checkpointRowMatches, summarizeCheckpoint, validatePendingSteer, winnerFromOutcome, } from "../../core/checkpoint-store.js";
2
+ import { appendPendingSteer, CheckpointError, checkpointOccMatches, checkpointRowMatches, summarizeCheckpoint, validatePendingSteer, winnerFromOutcome, } from "../../core/checkpoint-store.js";
3
3
  import { SharedLedgerTable } from "./shared-ledger.js";
4
+ const CHECKPOINT_LEDGER_EVENT_REGISTRY = {
5
+ put: true,
6
+ resolve: true,
7
+ reopen: true,
8
+ expire: true,
9
+ steer: true,
10
+ steer_append: true,
11
+ };
12
+ const KNOWN_CHECKPOINT_LEDGER_EVENTS = new Set(Object.keys(CHECKPOINT_LEDGER_EVENT_REGISTRY));
4
13
  function applyCheckpointEvent(cps, ev) {
14
+ if (!KNOWN_CHECKPOINT_LEDGER_EVENTS.has(ev.t)) {
15
+ throw new CheckpointError("checkpoint.unsupported_version", `file checkpoint ledger: unrecognised event kind "${String(ev.t)}" — ` +
16
+ `it was written by a newer worker; refusing the replay rather than serving an incomplete authority`);
17
+ }
5
18
  if (ev.t === "put") {
6
19
  cps.set(ev.token, ev.cp);
7
20
  return;
@@ -28,6 +41,17 @@ function applyCheckpointEvent(cps, ev) {
28
41
  case "steer":
29
42
  cp.state.pendingSteer = ev.steer;
30
43
  break;
44
+ case "steer_append": {
45
+ const queue = cp.state.pendingSteerQueue ?? [];
46
+ if (!queue.some((e) => e.inputId === ev.entry.inputId))
47
+ cp.state.pendingSteerQueue = [...queue, ev.entry];
48
+ break;
49
+ }
50
+ default: {
51
+ const _exhaustive = ev;
52
+ void _exhaustive;
53
+ break;
54
+ }
31
55
  }
32
56
  }
33
57
  const checkpointLedgers = new SharedLedgerTable({
@@ -35,6 +59,7 @@ const checkpointLedgers = new SharedLedgerTable({
35
59
  apply: applyCheckpointEvent,
36
60
  });
37
61
  export class FileCheckpointStore {
62
+ durability = "durable";
38
63
  fsyncEnabled;
39
64
  compactEvery;
40
65
  ledger;
@@ -114,7 +139,11 @@ export class FileCheckpointStore {
114
139
  if (!checkpointRowMatches(cp, scope, "pending")) {
115
140
  return false;
116
141
  }
117
- this.commit({ t: "steer", token, steer: clean }, this.fsyncEnabled);
142
+ const next = appendPendingSteer(cp.state, clean);
143
+ const appended = next[next.length - 1];
144
+ if (appended === undefined || appended.inputId !== clean.inputId)
145
+ return true;
146
+ this.commit({ t: "steer_append", token, entry: appended }, this.fsyncEnabled);
118
147
  return true;
119
148
  });
120
149
  }
@@ -45,7 +45,7 @@ export class FileStorageBackend {
45
45
  const corruptRead = opts.onCorruptRead !== undefined ? { onCorruptRead: opts.onCorruptRead } : undefined;
46
46
  const repo = new FileSessionRepo(this.root, corruptRead);
47
47
  this.fileSessions = repo;
48
- this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget" });
48
+ this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget", durability: "durable" });
49
49
  this.sessionStore = this.ttl;
50
50
  this.fileCheckpoints = new FileCheckpointStore(this.root, opts.checkpoint);
51
51
  this.checkpointStore = this.fileCheckpoints;
@@ -24,5 +24,7 @@ export declare class FileSessionRepo implements SessionRepo {
24
24
  delete(metadata: SessionMetadata): Promise<void>;
25
25
  fork(sourceMetadata: SessionMetadata, options?: SessionForkOptions): Promise<Session>;
26
26
  exportEntries(sessionId: string): Promise<SessionTreeEntry[]>;
27
- importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[]): Promise<void>;
27
+ importEntries(sessionId: string, owner: string | undefined, entries: SessionTreeEntry[], options?: {
28
+ preserveActorAssertions?: boolean;
29
+ }): Promise<void>;
28
30
  }
@@ -191,9 +191,9 @@ export class FileSessionRepo {
191
191
  async exportEntries(sessionId) {
192
192
  return this.read(sessionId).entries;
193
193
  }
194
- async importEntries(sessionId, owner, entries) {
194
+ async importEntries(sessionId, owner, entries, options) {
195
195
  void owner;
196
- const validated = validateEntriesForImport(entries);
196
+ const validated = validateEntriesForImport(entries, options);
197
197
  evictSharedSessionStorage(canonicalStoreKey(this.pathFor(sessionId)));
198
198
  const createdAt = new Date().toISOString();
199
199
  const lines = [JSON.stringify({ kind: "meta", id: sessionId, createdAt })];
@@ -88,7 +88,14 @@ export class SharedLedgerTable {
88
88
  }
89
89
  const core = new LedgerCore(key, paths, this.model, this.live);
90
90
  this.live.set(key, core);
91
- core.bootstrap();
91
+ try {
92
+ core.bootstrap();
93
+ }
94
+ catch (e) {
95
+ if (this.live.get(key) === core)
96
+ this.live.delete(key);
97
+ throw e;
98
+ }
92
99
  return core;
93
100
  }
94
101
  }
@@ -21,3 +21,6 @@ export declare function formatOutOfRootReadApprovalOption(directory: string): st
21
21
  export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
22
22
  export declare function classifySimpleCommandReadBoundary(command: string, boundary: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
23
23
  export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
24
+ export declare const POLL_LOOP_MAX_BEATS = 120;
25
+ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
26
+ export declare function classifyBoundedReadonlyPollLoop(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
@@ -462,3 +462,97 @@ export function classifySimpleCommandReadBoundary(command, boundary) {
462
462
  export function classifyCompoundReadonly(command, allow, boundary) {
463
463
  return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
464
464
  }
465
+ export const POLL_LOOP_MAX_BEATS = 120;
466
+ export const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
467
+ const POLL_BODY_HARD_REJECT = /[<>$()`|&{}\n\r\\]/;
468
+ const NON_ASCII_WHITESPACE = /[^\S \t]/;
469
+ const POLL_LOOP_SHAPE = /^for[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]+in[ \t]+(.+?)[ \t]*;[ \t]*do[ \t]+(.+?)[ \t]*;[ \t]*done[ \t]*;?$/;
470
+ function pollLoopQuotesBalanced(s) {
471
+ let open;
472
+ for (const ch of s) {
473
+ if (open === undefined && (ch === '"' || ch === "'"))
474
+ open = ch;
475
+ else if (open === ch)
476
+ open = undefined;
477
+ }
478
+ return open === undefined;
479
+ }
480
+ function pollLoopBeatsOf(iterable) {
481
+ const seq = /^\$\(seq[ \t]+(\d{1,7})[ \t]+(\d{1,7})\)$/.exec(iterable);
482
+ const brace = seq === null ? /^\{(\d{1,7})\.\.(\d{1,7})\}$/.exec(iterable) : null;
483
+ const range = seq ?? brace;
484
+ if (range !== null) {
485
+ const lo = Number(range[1]);
486
+ const hi = Number(range[2]);
487
+ return hi >= lo ? hi - lo + 1 : undefined;
488
+ }
489
+ const items = iterable.split(/[ \t]+/);
490
+ return items.every((t) => /^[A-Za-z0-9_.-]{1,64}$/.test(t)) ? items.length : undefined;
491
+ }
492
+ function pollLoopSleepReason(segment) {
493
+ const toks = segment.split(/[ \t]+/);
494
+ if (toks.length !== 2)
495
+ return "`sleep` in a poll loop must take exactly one literal numeric argument";
496
+ const v = toks[1];
497
+ if (!/^\d+(\.\d+)?$/.test(v) || v.length > 8 || Number(v) > POLL_LOOP_MAX_SLEEP_SECONDS) {
498
+ return `\`sleep ${v}\` is not a literal duration within the ${POLL_LOOP_MAX_SLEEP_SECONDS}s per-beat cap — not auto-allowed`;
499
+ }
500
+ return undefined;
501
+ }
502
+ export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
503
+ if (NON_ASCII_WHITESPACE.test(command)) {
504
+ return "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed";
505
+ }
506
+ const trimmed = command.trim();
507
+ if (!trimmed)
508
+ return "empty command";
509
+ if (!pollLoopQuotesBalanced(trimmed)) {
510
+ return "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed";
511
+ }
512
+ const m = POLL_LOOP_SHAPE.exec(trimmed);
513
+ if (m === null) {
514
+ return "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)";
515
+ }
516
+ const varName = m[1];
517
+ const iterable = m[2];
518
+ const body = m[3];
519
+ if (!/^[a-z]$/.test(varName)) {
520
+ return `the loop variable "${varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve`;
521
+ }
522
+ const beats = pollLoopBeatsOf(iterable);
523
+ if (beats === undefined) {
524
+ return `the loop bound "${iterable}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted`;
525
+ }
526
+ if (beats > POLL_LOOP_MAX_BEATS) {
527
+ return `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow`;
528
+ }
529
+ if (POLL_BODY_HARD_REJECT.test(body)) {
530
+ return "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks";
531
+ }
532
+ const readSegments = [];
533
+ for (const rawSegment of body.split(";")) {
534
+ const segment = rawSegment.trim();
535
+ if (segment.length === 0)
536
+ return "empty command in the loop body";
537
+ const parsed = parseLeadingCommandName(segment);
538
+ if ("reject" in parsed)
539
+ return parsed.reject;
540
+ if (parsed.name === "sleep") {
541
+ const sleepReason = pollLoopSleepReason(segment);
542
+ if (sleepReason !== undefined)
543
+ return sleepReason;
544
+ continue;
545
+ }
546
+ readSegments.push(segment);
547
+ }
548
+ if (readSegments.length === 0) {
549
+ return "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed";
550
+ }
551
+ const verdict = classifyCompoundReadonlyDetailed(readSegments.join("; "), allow, boundary);
552
+ if (verdict.reason !== undefined)
553
+ return verdict.reason;
554
+ if (verdict.undecidedPaths !== undefined) {
555
+ return `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed`;
556
+ }
557
+ return undefined;
558
+ }