@sema-agent/core 7.12.0 → 7.13.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 (57) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/core/ask-origin.d.ts +55 -0
  3. package/dist/core/ask-origin.js +21 -0
  4. package/dist/core/engine-notice.d.ts +18 -0
  5. package/dist/core/gate-lanes.d.ts +0 -40
  6. package/dist/core/gate-lanes.js +3 -17
  7. package/dist/core/governance-codes.d.ts +1 -1
  8. package/dist/core/governance-codes.js +2 -0
  9. package/dist/core/hooks.d.ts +1 -1
  10. package/dist/core/hooks.js +1 -1
  11. package/dist/core/runner/assemble-result.d.ts +45 -1
  12. package/dist/core/runner/assemble-result.js +4 -1
  13. package/dist/core/runner/compaction-seams.d.ts +42 -0
  14. package/dist/core/runner/compaction-seams.js +80 -0
  15. package/dist/core/runner/contracts.d.ts +44 -6
  16. package/dist/core/runner/denial-limit-arms.js +3 -2
  17. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  18. package/dist/core/runner/permission-rule-lanes.js +2 -1
  19. package/dist/core/runner/prepare-policy-chain.js +3 -2
  20. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  21. package/dist/core/runner/prepare-safety-scan.js +11 -16
  22. package/dist/core/runner/prepare-task.d.ts +9 -1
  23. package/dist/core/runner/prepare-task.js +6 -1
  24. package/dist/core/runner/resume-claim.d.ts +2 -2
  25. package/dist/core/runner/resume-preflight.d.ts +2 -2
  26. package/dist/core/runner/run-attachment-seats.d.ts +2 -2
  27. package/dist/core/runner/run-git-lane.d.ts +1 -1
  28. package/dist/core/runner/run-harness-handlers.js +2 -0
  29. package/dist/core/runner/run-identity-wiring.d.ts +7 -0
  30. package/dist/core/runner/run-identity-wiring.js +3 -1
  31. package/dist/core/runner/run-leg.d.ts +7 -5
  32. package/dist/core/runner/run-leg.js +256 -5
  33. package/dist/core/runner/run-notification-lane.d.ts +4 -3
  34. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  35. package/dist/core/runner/run-recovery-lanes.js +5 -4
  36. package/dist/core/runner/run-settle-and-teardown.d.ts +14 -10
  37. package/dist/core/runner/run-settle-and-teardown.js +112 -4
  38. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  39. package/dist/core/runner/run-terminal-adoption.d.ts +16 -16
  40. package/dist/core/runner/run-terminal-adoption.js +87 -7
  41. package/dist/core/runner/run-turn-boundary.js +5 -4
  42. package/dist/core/runner/runtask.d.ts +10 -90
  43. package/dist/core/runner/runtask.js +77 -596
  44. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  45. package/dist/core/task-stream.d.ts +10 -1
  46. package/dist/core/tool-face.d.ts +8 -0
  47. package/dist/core/tool-face.js +1 -0
  48. package/dist/core/tool-policy.d.ts +5 -3
  49. package/dist/core/tool-registry.d.ts +11 -3
  50. package/dist/core/tool-registry.js +7 -1
  51. package/dist/core/tool-roster.d.ts +26 -0
  52. package/dist/core/tool-roster.js +38 -7
  53. package/dist/index.d.ts +3 -3
  54. package/dist/index.js +3 -3
  55. package/dist/server/http.js +2 -1
  56. package/package.json +1 -1
  57. package/test/export-surface.snapshot.json +11 -1
@@ -652,6 +652,24 @@ export interface Prepared {
652
652
  callIssuedAtRef: {
653
653
  current?: number;
654
654
  };
655
+ /** #677 — was the run's WORK cut by a stop? Two observation points, one record, read by the answer-settled
656
+ * verdict (`answerSettled`) beside the harness's own undrained-input account:
657
+ * - `continuation` — the loop decided to CONTINUE past its last assistant turn and the abort caught it
658
+ * before that continuation dispatched. Written by the loop-trace sink from the loop's own steps: a
659
+ * `continue` step (a drained steer / follow-up, a truncated-output or thinking-only or degenerate-output
660
+ * nudge, a next tool turn) sets it; the terminal step clears it unless the loop exited at its
661
+ * pre-dispatch abort guard (`aborted_before_stream`) — the one exit where `final` is still the PREVIOUS
662
+ * turn's message. A clean "stop" the loop itself had decided was not the end is not a settled answer.
663
+ * - `abortedBeforeTurnEnd` — the run's abort was already in force when a turn ENDED (written by the
664
+ * `turn_end` handler before the turns cap fires its own abort). The ruling's line is the last
665
+ * `turn_end`: a stop that landed before it landed on work — even when the brain ignored the signal
666
+ * and delivered a full "stop" answer through it (the loop then ends at its own decision, so the
667
+ * continuation bit never sets); a stop that landed after it landed on the tail.
668
+ * Always present; both false on a run nothing stopped. */
669
+ workCutRef: {
670
+ continuation: boolean;
671
+ abortedBeforeTurnEnd: boolean;
672
+ };
655
673
  /** RB-458 — records the FIRST brain call this run's outer guardrail gave up on (see
656
674
  * {@link import("../../brain/timeout.js").withBrainCallGuardrail}). Always present; `timedOut`
657
675
  * stays absent unless the guardrail fired. The run loop reads it AFTER the loop settles and gives
@@ -2332,13 +2350,11 @@ export interface TurnBoundaryDeps {
2332
2350
  drainManualCompact: (outcome: CompactOutcome) => void;
2333
2351
  runnerHooks: {
2334
2352
  onError: RunnerDeps["onError"];
2335
- seamCCompactionOptions: (prepared: Prepared) => Pick<MaybeCompactOptions, "summaryProvider" | "onCompaction" | "maxConsecutiveProviderReuse" | "consecutiveProviderReuse"> | undefined;
2336
- compactionHookOptions: (spec: TaskSpec, sessionId: string, trigger: "auto" | "manual" | "forced") => Pick<MaybeCompactOptions, "trigger" | "preCompact" | "postCompact">;
2337
- recordCompactionReuse: (prepared: Prepared, comp: {
2338
- compacted: boolean;
2339
- reused?: boolean;
2340
- }) => void;
2341
2353
  };
2354
+ /** The Runner's deployment deps, read LIVE ({@link RunnerDepsSeat}): the compaction seams (compaction-seams.ts) the
2355
+ * boundary's compaction pass reaches down for read the summary provider, the hooks slot and the error sink through it
2356
+ * on every pass — the three Runner methods that used to sit on `runnerHooks` as delegates (design/393 S7). */
2357
+ runner: RunnerDepsSeat;
2342
2358
  }
2343
2359
  /** design/157 B15 尾件 — R5(harness 事件处理器族)的依赖包。全部为 runLocked 内声明顺序早于
2344
2360
  * 工厂调用点的 const 稳定引用 + 三个 runLocked 形参;this 面(deps.onError)经 runnerHooks 打包
@@ -2423,6 +2439,28 @@ export interface RunnerSelfSeat {
2423
2439
  /** Streaming form of {@link RunnerSelfSeat.resume}: the pre-CAS guards and the CAS run first, then the live stream is returned. */
2424
2440
  resumeStream(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, internals?: RunInternals): Promise<TaskStream>;
2425
2441
  }
2442
+ /**
2443
+ * design/393 S7 (#675) — the orchestrator's entry (`prepareTask`, prepare-task.ts) as ONE contract: the notification lane's
2444
+ * `prepareTask` seat names it, and the orchestrator pins its own declaration against it (`PrepareTaskIsTheContract`), so a
2445
+ * signature change reds at the declaration — not at the driver's hand-in, and not as a seat spelled a second time. It lives
2446
+ * on the floor because a lane may not name the orchestrator (docs/LAYERING.json: layer 4 sits above the lanes). Positional,
2447
+ * exactly as the entry is declared — the seat carries the orchestrator's function itself, not a lane Input.
2448
+ */
2449
+ export type PrepareTaskFn = (spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals, runnerSelf?: RunnerSelfSeat, runIdSink?: {
2450
+ runId?: string;
2451
+ }) => Promise<Prepared>;
2452
+ /**
2453
+ * design/393 S7 (#670) — the Result of an INSTALLING phase: a lane whose every product is installed on a seat it borrowed
2454
+ * (the harness's stop gate and recovery chain, the run state's counter groups, the stream's result setter, the claimed
2455
+ * row's registries) hands nothing back, and says so by extending this marker. The marker is machine-read by gate:phase-api
2456
+ * (design/238 R-1's third clause): a Result with no members MUST extend it, a Result with members MAY NOT, and no Result
2457
+ * inherits anything else — so an empty Result is a stated fact about the phase, never an interface someone forgot to
2458
+ * fill, and a phase that starts handing a product back must drop the declaration in the same edit. Six lanes carry it:
2459
+ * the attachment seats, the stop gate, the recovery lanes, the settle and teardown (run-), the preflight and the claim
2460
+ * (resume-).
2461
+ */
2462
+ export interface InstallingPhaseResult {
2463
+ }
2426
2464
  /** The live-task handle `runLocked` publishes once the harness exists (design/47): the harness + abort
2427
2465
  * controller, the loop-liveness latch (`ended` flips when the single `harness.prompt` settles; `userInterrupted`
2428
2466
  * / `userHalted` are the interrupt and halt verbs' attribution seats), the run's reminder mark, its session and
@@ -2,7 +2,7 @@ import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage
2
2
  import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
3
3
  import { deliverEngineNotice } from "../types.js";
4
4
  import { inlineUntrusted } from "../untrusted-text.js";
5
- import { askOriginOf, classifierMayAnswer } from "../ask-origin.js";
5
+ import { askOriginOf, classifierMayAnswer, probeMandatedAsk } from "../ask-origin.js";
6
6
  export function attachRebuiltDenialTrackers(entries, denialLimit) {
7
7
  if (entries === undefined)
8
8
  return undefined;
@@ -76,8 +76,9 @@ export function inheritedAskCarry(judged, liveApprover, tracker) {
76
76
  export async function judgeInheritedClassifier(opts) {
77
77
  const { autoMode, ask, req } = opts;
78
78
  const origin = askOriginOf(ask, INHERITED_STATION_FACTS);
79
- if (autoMode === undefined || !classifierMayAnswer(origin))
79
+ if (autoMode === undefined || !classifierMayAnswer(origin) || probeMandatedAsk(ask)) {
80
80
  return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
81
+ }
81
82
  const verdict = await autoMode.decider
82
83
  .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal)
83
84
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -26,7 +26,8 @@
26
26
  * silence the ungated-write warning for deployments that wired no policy at all.
27
27
  */
28
28
  import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
29
- import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
29
+ import { persistedRuleMandateOf } from "../ask-origin.js";
30
+ import { type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
30
31
  import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
31
32
  import { type OrgRuleResolution } from "../permission-rule-org.js";
32
33
  import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
@@ -1,4 +1,5 @@
1
- import { persistedRuleMandateOf } from "../hooks.js";
1
+ import { persistedRuleMandateOf } from "../ask-origin.js";
2
+ import {} from "../hooks.js";
2
3
  import { adjudicatePersistedPathRules, adjudicatePersistedRules, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
4
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
@@ -10,6 +10,7 @@ import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
10
10
  import { NAMESPACED_NAME_SHAPES, protocolOf } from "../protocol-table.js";
11
11
  import { emitTrace } from "../trace.js";
12
12
  import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
13
+ import { probeMandatedAsk } from "../ask-origin.js";
13
14
  import { headlessDenyAtFold, headlessDenyAtRecheck, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
14
15
  import { askGrantShapeOf } from "./inherited-ask-grants.js";
15
16
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
@@ -374,7 +375,7 @@ export async function preparePolicyChain(input) {
374
375
  if (judged.kind === "deny")
375
376
  return judged.result;
376
377
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
377
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
378
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName) && !probeMandatedAsk(inheritedAsk)) {
378
379
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
379
380
  return { action: "allow" };
380
381
  }
@@ -454,7 +455,7 @@ export async function preparePolicyChain(input) {
454
455
  if (judged.kind === "deny")
455
456
  return judged.result;
456
457
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
457
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
458
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName) && !probeMandatedAsk(inheritedAsk)) {
458
459
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
459
460
  return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
460
461
  }
@@ -3,12 +3,17 @@ import { materializeMcpTools } from "../mcp.js";
3
3
  import { materializeA2aTools } from "../a2a.js";
4
4
  import { deliverEngineNotice } from "../types.js";
5
5
  import { RosterBuilder } from "../tool-roster.js";
6
+ import { TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
6
7
  import { REFRESH_MCP_TOOLS_TOOL_NAME, toolFace } from "../tool-catalog-entries.js";
7
8
  import { MCP_NAMESPACE, A2A_NAMESPACE } from "../protocol-table.js";
8
9
  import { mintNamespacePrefix } from "../protocol-naming.js";
9
10
  import { applyMcpToolFaces } from "./tool-face-overlay.js";
10
11
  export async function prepareProtocolTools(input) {
11
12
  const { lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, roster, rosterSeat, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback } = input;
13
+ const overPeer = (lockedPreflight.mcp ?? []).find((s) => s.name.length > TOOL_WIRE_NAME_MAX_CHARS);
14
+ if (overPeer !== undefined) {
15
+ throw Object.assign(new Error(`MCP server name ${JSON.stringify(overPeer.name.slice(0, 40) + "…")} is ${overPeer.name.length} characters; a peer rides every roster row of its tools and the roster's bound on a name is ${TOOL_WIRE_NAME_MAX_CHARS}.`), { code: "config.tool_name_too_long" });
16
+ }
12
17
  const mcp = lockedPreflight.mcp?.length
13
18
  ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps, runId))
14
19
  : { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
@@ -1,7 +1,6 @@
1
1
  import { NAMESPACED_NAME_SHAPES } from "../protocol-table.js";
2
2
  import { engineCardTypes, handBandEffects } from "../tool-registry.js";
3
- import { TOOL_CONTRACT_MAX_CHARS, TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
4
- import { callerCardIdOf, offendingResultCard, schemaKeyBoundProblem } from "../tool-roster.js";
3
+ import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKeyBoundProblem } from "../tool-roster.js";
5
4
  import { toolFaceProblem } from "./tool-face-overlay.js";
6
5
  import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
7
6
  import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
@@ -16,23 +15,19 @@ export function prepareSafetyScan(input) {
16
15
  const axisExplicitNegatives = new Map();
17
16
  const reversibilityProbes = new Map();
18
17
  for (const t of spec.tools ?? []) {
19
- if (t.name.length > TOOL_WIRE_NAME_MAX_CHARS) {
20
- const e = new Error(`Tool name "${t.name.slice(0, 40)}…" is invalid: ${t.name.length} characters, the roster's bound is ${TOOL_WIRE_NAME_MAX_CHARS}.`);
21
- e.code = "config.tool_name_too_long";
22
- throw e;
23
- }
24
- for (const alias of t.aliases ?? []) {
25
- if (alias.length > TOOL_WIRE_NAME_MAX_CHARS) {
26
- const e = new Error(`Tool alias "${alias.slice(0, 40)}…" (of "${t.name}") is invalid: ${alias.length} characters, the roster's bound is ${TOOL_WIRE_NAME_MAX_CHARS}.`);
27
- e.code = "config.tool_name_too_long";
18
+ {
19
+ const bound = rosterMemberBoundProblem({
20
+ name: t.name,
21
+ aliases: t.aliases,
22
+ ...(t.contract !== undefined ? { contractId: t.contract.contractId, implementationRevision: t.contract.implementationRevision } : {}),
23
+ ...(typeof t.modelGate === "string" ? { modelGate: t.modelGate } : {}),
24
+ });
25
+ if (bound !== undefined) {
26
+ const e = new Error(`Tool "${t.name.slice(0, 40)}" cannot ride a roster row: ${bound.message}.`);
27
+ e.code = bound.code;
28
28
  throw e;
29
29
  }
30
30
  }
31
- if (t.contract !== undefined && (t.contract.contractId.length > TOOL_CONTRACT_MAX_CHARS || t.contract.implementationRevision.length > TOOL_CONTRACT_MAX_CHARS)) {
32
- const e = new Error(`Tool "${t.name}" declares a contract member over ${TOOL_CONTRACT_MAX_CHARS} characters (contractId ${t.contract.contractId.length}, implementationRevision ${t.contract.implementationRevision.length}) — the roster's bound.`);
33
- e.code = "config.tool_contract_too_long";
34
- throw e;
35
- }
36
31
  {
37
32
  const keyProblem = schemaKeyBoundProblem(t.parameters);
38
33
  if (keyProblem !== undefined) {
@@ -8,7 +8,7 @@ export { mcpManifestEntries } from "./prepare-wiring-manifest.js";
8
8
  export { __resetMaterializeEnvAnnouncements } from "./prepare-tool-disclosure-mount.js";
9
9
  export { fileHistoryFilesystemIdentity, resolveFileHistoryScope } from "./prepare-file-history.js";
10
10
  import type { RunnerSelfSeat } from "./contracts.js";
11
- import type { Prepared, PrepareResume, RunInternals } from "./contracts.js";
11
+ import type { Prepared, PrepareResume, PrepareTaskFn, RunInternals } from "./contracts.js";
12
12
  export type { FileHistoryBoundarySeat, InheritedGate, Prepared, PreparedMicroCompact, PrepareResume, ResolvedWorkspace, RunInternals, UsageGovernance } from "./contracts.js";
13
13
  import type { ExecutionEnv } from "../../internal/harness.js";
14
14
  import type { RunnerDeps, TaskSpec } from "../types.js";
@@ -114,3 +114,11 @@ runnerSelf?: RunnerSelfSeat,
114
114
  runIdSink?: {
115
115
  runId?: string;
116
116
  }): Promise<Prepared>;
117
+ /** design/393 S7 (#675) — the entry IS the contract the notification lane's seat names ({@link PrepareTaskFn}, contracts.ts):
118
+ * a signature change reds HERE, at the declaration, as `false` failing the `true` bound. IDENTITY, not assignability (review
119
+ * round 1): the parameter tuple (arity, optionality, each type) and the return type must be the SAME type in both directions —
120
+ * one-way `extends` let a dropped trailing parameter, an added optional one or a narrowed return slip through. Type-level only —
121
+ * nothing is emitted. */
122
+ type Holds<T extends true> = T;
123
+ type Same<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
124
+ export type PrepareTaskIsTheContract = Holds<Same<Parameters<typeof prepareTask>, Parameters<PrepareTaskFn>>> & Holds<Same<ReturnType<typeof prepareTask>, ReturnType<PrepareTaskFn>>> & Holds<Same<ThisParameterType<typeof prepareTask>, ThisParameterType<PrepareTaskFn>>> & Holds<Same<typeof prepareTask, PrepareTaskFn>>;
@@ -429,6 +429,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
429
429
  const rosterSeat = new ToolRosterDeltaSeat();
430
430
  const { mcp, a2a, rebuildHarnessToolsRef, contentOriginWrapRef, toolCallGateArmedRef } = await prepareProtocolTools({ lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, roster, rosterSeat, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback });
431
431
  const callIssuedAtRef = {};
432
+ const workCutRef = { continuation: false, abortedBeforeTurnEnd: false };
432
433
  const memoryWriteGateRef = {};
433
434
  const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
434
435
  const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
@@ -617,6 +618,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
617
618
  },
618
619
  ...(spec.resilience !== undefined ? { resilience: spec.resilience } : {}),
619
620
  loopTrace: (step) => {
621
+ if (step.kind === "continue")
622
+ workCutRef.continuation = true;
623
+ else
624
+ workCutRef.continuation = workCutRef.continuation && step.reason === "aborted_before_stream";
620
625
  if (step.kind !== "continue")
621
626
  return;
622
627
  if (step.reason === "next_turn" || step.reason === "steer_injected" || step.reason === "follow_up_injected")
@@ -686,7 +691,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
686
691
  const preparedHolder = {};
687
692
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, readDenyAdditionsNormalized, preparedHolder });
688
693
  await onceLedger.settle(session, announcedListingsRef);
689
- const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
694
+ const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
690
695
  rollback.commit();
691
696
  const prepared = buildPrepared();
692
697
  preparedHolder.current = prepared;
@@ -9,7 +9,7 @@
9
9
  * — the post-consume hook fires on the tick the win was read — and returns what that rung returns.
10
10
  */
11
11
  import { type Checkpoint, type CheckpointStore, type CheckpointToken } from "../checkpoint-store.js";
12
- import type { InheritedGate, ResumeRun } from "./contracts.js";
12
+ import type { InheritedGate, InstallingPhaseResult, ResumeRun } from "./contracts.js";
13
13
  export interface ResumeClaimInput<R> {
14
14
  /** borrowed-readonly — the checkpoint token being claimed. */
15
15
  token: CheckpointToken;
@@ -27,6 +27,6 @@ export interface ResumeClaimInput<R> {
27
27
  next: (claimed: ResumeClaimResult) => Promise<R>;
28
28
  }
29
29
  /** The claim's completion is its whole result: a lost CAS throws, so reaching `next` IS the win. */
30
- export interface ResumeClaimResult {
30
+ export interface ResumeClaimResult extends InstallingPhaseResult {
31
31
  }
32
32
  export declare function resumeClaim<R>(input: ResumeClaimInput<R>): Promise<R>;
@@ -10,7 +10,7 @@
10
10
  * returns what that rung returns.
11
11
  */
12
12
  import { type Checkpoint, type CheckpointStore, type CheckpointToken } from "../checkpoint-store.js";
13
- import type { InheritedGate, RunnerDepsSeat, SuspendReap } from "./contracts.js";
13
+ import type { InheritedGate, InstallingPhaseResult, RunnerDepsSeat, SuspendReap } from "./contracts.js";
14
14
  export interface ResumePreflightInput<R> {
15
15
  /** borrowed-readonly — the checkpoint token (handed to the deployment and to the settling CAS). */
16
16
  token: CheckpointToken;
@@ -35,6 +35,6 @@ export interface ResumePreflightInput<R> {
35
35
  next: (screened: ResumePreflightResult) => Promise<R>;
36
36
  }
37
37
  /** The preflight's completion is its whole result: every refusal throws, so reaching `next` IS the fact. */
38
- export interface ResumePreflightResult {
38
+ export interface ResumePreflightResult extends InstallingPhaseResult {
39
39
  }
40
40
  export declare function resumePreflight<R>(input: ResumePreflightInput<R>): Promise<R>;
@@ -1,5 +1,5 @@
1
1
  import type { TaskSpec } from "../types.js";
2
- import type { Prepared, ResumeRun, RunnerDepsSeat, RunState } from "./contracts.js";
2
+ import type { InstallingPhaseResult, Prepared, ResumeRun, RunnerDepsSeat, RunState } from "./contracts.js";
3
3
  export interface RunAttachmentSeatsInput<R> {
4
4
  /** borrowed-readonly — the task spec: the attachments config and the `finalVerification` opt-in. */
5
5
  spec: TaskSpec;
@@ -17,6 +17,6 @@ export interface RunAttachmentSeatsInput<R> {
17
17
  next: (seats: RunAttachmentSeatsResult) => Promise<R>;
18
18
  }
19
19
  /** Nothing comes back: the lane's products are the `counters` / `attach` groups it wrote on the borrowed run state. */
20
- export interface RunAttachmentSeatsResult {
20
+ export interface RunAttachmentSeatsResult extends InstallingPhaseResult {
21
21
  }
22
22
  export declare function runAttachmentSeats<R>(input: RunAttachmentSeatsInput<R>): Promise<R>;
@@ -52,7 +52,7 @@ export interface RunGitLaneInput {
52
52
  runner: RunnerDepsSeat;
53
53
  }
54
54
  export interface RunGitLaneResult {
55
- /** The parked-mirror flush — awaited at the serialization points (the owed-frame retry here; prompt settle in the driver). */
55
+ /** The parked-mirror flush — awaited at the serialization points: the `turn_boundary` subscription here is the regular station (the final boundary of a gracefully ending run included); the leg's post-prompt call is the fallback for a terminal that skipped it. */
56
56
  flushGitMirror: () => Promise<void>;
57
57
  /** The owed-frame retry's `turn_boundary` subscription; the driver's teardown releases it first. */
58
58
  unsubGitRetry: () => void;
@@ -281,6 +281,8 @@ export function createHarnessHandlers(input) {
281
281
  };
282
282
  const onTurnEnd = () => {
283
283
  stats.turns += 1;
284
+ if (prepared.abortController.signal.aborted)
285
+ prepared.workCutRef.abortedBeforeTurnEnd = true;
284
286
  const stopExtra = rs.turn.turnStopReason !== undefined ? { stopReason: rs.turn.turnStopReason } : {};
285
287
  queue.push(rs.turn.turnUsage
286
288
  ? { type: "turn_end", usage: rs.turn.turnUsage, ...(rs.turn.turnUsageMissing ? { usageMissing: true } : {}), ...stopExtra, ...ident() }
@@ -73,5 +73,12 @@ export interface RunIdentityWiringResult {
73
73
  followUp: number;
74
74
  } | undefined;
75
75
  };
76
+ /** #677 — did the run END with a person's steer / follow-up still pending (queued, or drained at the final boundary and
77
+ * never served)? A READ FACE over this lane's own `let`, written by the same undrained-inputs sink on BOTH its arms
78
+ * (park or not): the terminal-adoption lane folds it into the answer-settled verdict — a "stop" answer the person had
79
+ * already asked to continue past is not a settled answer, so a stop that cut that continuation is a stop on work. */
80
+ userInputLostAtEnd: {
81
+ readonly current: boolean;
82
+ };
76
83
  }
77
84
  export declare function runIdentityWiring(input: RunIdentityWiringInput): RunIdentityWiringResult;
@@ -41,7 +41,9 @@ export function runIdentityWiring(input) {
41
41
  };
42
42
  prepared.harness.engineInjectionsHeld = () => prepared.batchHaltRef.current !== undefined;
43
43
  let undrainedUserAtEnd;
44
+ let userInputLostAtEnd = false;
44
45
  prepared.harness.onUndrainedUserInputs = (counts) => {
46
+ userInputLostAtEnd = counts.steer + counts.followUp > 0;
45
47
  if (prepared.pausedRef.current !== undefined) {
46
48
  undrainedUserAtEnd = counts;
47
49
  return;
@@ -81,5 +83,5 @@ export function runIdentityWiring(input) {
81
83
  const loopLatch = { ended: false, userInterrupted: false, userHalted: false };
82
84
  onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, runId: prepared.runId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
83
85
  const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
84
- return { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, undrainedUserAtEnd: { get current() { return undrainedUserAtEnd; } } };
86
+ return { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, undrainedUserAtEnd: { get current() { return undrainedUserAtEnd; } }, userInputLostAtEnd: { get current() { return userInputLostAtEnd; } } };
85
87
  }
@@ -5,7 +5,10 @@
5
5
  * assembly — orphaned background tasks, the git frame, the parked steer queue and the wake message each under its own trust
6
6
  * framing and redelivery screen — and the prompt itself) and the OBJECTIVE leg (the UserPromptSubmit screen, the git
7
7
  * frame's primary delivery, the first-frame listings, the pre-call budget / token / entry-governance gates, the image
8
- * conversion, the objective's human-input frame and the prompt itself). The tail captures the abort facts; the catch
8
+ * conversion, the objective's human-input frame and the prompt itself). The resumed decision's application
9
+ * (`applyResumeDecision`) and the gated call's resolver (`resolvePendingCall` — the post-CAS belts, the execution record)
10
+ * are the lane's own module-private functions since design/393 S7, verbatim from the Runner's private methods: the resume
11
+ * leg is their one caller, and a sibling file would be a lane-to-lane reach (the S6 legs' reading). The tail captures the abort facts; the catch
9
12
  * re-throws the resume-equivalent env failures after a bounded teardown and adopts everything else as the leg's throw;
10
13
  * the finally releases the timer, the diagnostics, the signal, the content-ask bindings, flips the notification lane
11
14
  * and releases the four subscriptions in their order.
@@ -79,11 +82,10 @@ export interface RunLegInput<R> {
79
82
  unsubBoundary: () => void;
80
83
  /** borrowed-readonly — the git lane's harness-event release, called third. */
81
84
  unsub: () => void;
82
- /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `hooks`, `onNotice`, `allowImageUrl`); the one home of
83
- * why it is a seat and not a captured object is {@link RunnerDepsSeat}. */
85
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `hooks`, `onNotice`, `allowImageUrl`; the resumed
86
+ * decision's resolver files its execution record's word through `onNotice` too); the one home of why it is a seat and not a
87
+ * captured object is {@link RunnerDepsSeat}. */
84
88
  runner: RunnerDepsSeat;
85
- /** borrowed-readonly — the Runner's application of the resumed decision (its private method, as a delegate). */
86
- applyResumeDecision: (prepared: Prepared, resume: ResumeRun, emit: (e: TaskEvent) => void, emitCommitted: (entryId: string, role: "user" | "assistant" | "toolResult", toolCallId?: string) => void, onResolvedToolSuccess?: (toolName: string, details: unknown) => void, onExecuteStart?: (toolCallId: string) => void) => Promise<void>;
87
89
  /** borrowed-readonly — the terminal adoption, entered in this lane's last continuation with the leg's facts. */
88
90
  next: (leg: RunLegResult) => Promise<R>;
89
91
  }