@sema-agent/core 7.18.0 → 7.20.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 (137) hide show
  1. package/CHANGELOG.md +276 -2
  2. package/dist/agents/subagent.js +12 -20
  3. package/dist/agents/teacher.js +2 -5
  4. package/dist/agents/verify.js +21 -10
  5. package/dist/brain/anthropic.js +8 -3
  6. package/dist/brain/open-responses.js +12 -4
  7. package/dist/brain/openai.js +7 -2
  8. package/dist/brain/reasoning.d.ts +13 -13
  9. package/dist/brain/reasoning.js +2 -2
  10. package/dist/brain/stream-engine.d.ts +4 -0
  11. package/dist/brain/stream-engine.js +6 -1
  12. package/dist/brain/stream-shared.d.ts +3 -0
  13. package/dist/brain/stream-shared.js +1 -0
  14. package/dist/core/ask-origin.d.ts +16 -0
  15. package/dist/core/ask-origin.js +8 -0
  16. package/dist/core/auto-mode-arming.d.ts +6 -0
  17. package/dist/core/auto-mode-arming.js +5 -5
  18. package/dist/core/auto-mode-defaults.d.ts +43 -15
  19. package/dist/core/auto-mode-defaults.js +4 -1
  20. package/dist/core/auto-mode.d.ts +74 -26
  21. package/dist/core/auto-mode.js +80 -11
  22. package/dist/core/checkpoint-store.d.ts +7 -0
  23. package/dist/core/checkpoint-store.js +2 -2
  24. package/dist/core/child-clamp-seats.d.ts +79 -0
  25. package/dist/core/child-clamp-seats.js +29 -0
  26. package/dist/core/engine-notice.d.ts +89 -41
  27. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  28. package/dist/core/fs-write-gate-policy.js +51 -4
  29. package/dist/core/gate-bounded-await.d.ts +38 -0
  30. package/dist/core/gate-bounded-await.js +53 -0
  31. package/dist/core/gate-fold.js +20 -48
  32. package/dist/core/gate-lanes.js +17 -9
  33. package/dist/core/gate-outcome.d.ts +34 -6
  34. package/dist/core/gate-outcome.js +7 -0
  35. package/dist/core/governance-codes.d.ts +16 -1
  36. package/dist/core/governance-codes.js +18 -0
  37. package/dist/core/hooks.d.ts +8 -8
  38. package/dist/core/memory-engine/engine.d.ts +14 -9
  39. package/dist/core/memory-engine/engine.js +21 -8
  40. package/dist/core/memory-engine/layout.d.ts +31 -0
  41. package/dist/core/memory-engine/layout.js +68 -3
  42. package/dist/core/memory-engine/types.d.ts +24 -0
  43. package/dist/core/memory.d.ts +19 -0
  44. package/dist/core/memory.js +12 -0
  45. package/dist/core/read-root-candidate.d.ts +12 -0
  46. package/dist/core/read-root-candidate.js +24 -0
  47. package/dist/core/remote-env.d.ts +32 -0
  48. package/dist/core/remote-env.js +4 -1
  49. package/dist/core/roles.d.ts +9 -1
  50. package/dist/core/roles.js +4 -0
  51. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  52. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  53. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  54. package/dist/core/runner/announce-once-ledger.js +8 -0
  55. package/dist/core/runner/classifier-ladder.d.ts +154 -11
  56. package/dist/core/runner/classifier-ladder.js +170 -10
  57. package/dist/core/runner/contracts.d.ts +46 -1
  58. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  59. package/dist/core/runner/denial-limit-arms.js +7 -6
  60. package/dist/core/runner/env-capability-census.d.ts +30 -0
  61. package/dist/core/runner/env-capability-census.js +38 -0
  62. package/dist/core/runner/forward-sink.d.ts +32 -0
  63. package/dist/core/runner/forward-sink.js +41 -0
  64. package/dist/core/runner/gate-exit.d.ts +4 -0
  65. package/dist/core/runner/gate-exit.js +4 -1
  66. package/dist/core/runner/git-status-frame.d.ts +0 -9
  67. package/dist/core/runner/listing-fold.d.ts +57 -0
  68. package/dist/core/runner/listing-fold.js +75 -0
  69. package/dist/core/runner/prepare-ask-lane.js +1 -0
  70. package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
  71. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  72. package/dist/core/runner/prepare-config-doors.js +8 -10
  73. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  74. package/dist/core/runner/prepare-execution-env.js +2 -0
  75. package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
  76. package/dist/core/runner/prepare-gate-stations.js +6 -2
  77. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  78. package/dist/core/runner/prepare-hands-readface.js +3 -5
  79. package/dist/core/runner/prepare-listings.d.ts +3 -0
  80. package/dist/core/runner/prepare-listings.js +4 -2
  81. package/dist/core/runner/prepare-memory.js +7 -1
  82. package/dist/core/runner/prepare-policy-chain.js +5 -5
  83. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  84. package/dist/core/runner/prepare-project-context.js +3 -2
  85. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  86. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  87. package/dist/core/runner/prepare-run-refs.js +5 -4
  88. package/dist/core/runner/prepare-task.js +8 -6
  89. package/dist/core/runner/resume-apply.js +3 -3
  90. package/dist/core/runner/run-clock-and-content.js +3 -7
  91. package/dist/core/runner/run-harness-handlers.js +10 -10
  92. package/dist/core/runner/run-leg.js +31 -16
  93. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  94. package/dist/core/runner/run-turn-boundary.js +44 -25
  95. package/dist/core/runner/runtask.js +2 -1
  96. package/dist/core/runner/synthetic-tools.d.ts +41 -29
  97. package/dist/core/runner/synthetic-tools.js +25 -40
  98. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  99. package/dist/core/runner/task-root-canonical.js +18 -0
  100. package/dist/core/runner/turn-attachments.d.ts +160 -53
  101. package/dist/core/runner/turn-attachments.js +153 -51
  102. package/dist/core/safe-notify.d.ts +5 -0
  103. package/dist/core/safe-notify.js +24 -2
  104. package/dist/core/sensitive-path-policy.d.ts +84 -17
  105. package/dist/core/sensitive-path-policy.js +37 -19
  106. package/dist/core/skill-listing-budget.d.ts +106 -0
  107. package/dist/core/skill-listing-budget.js +57 -0
  108. package/dist/core/skills-directory.js +17 -10
  109. package/dist/core/task-registry.js +2 -1
  110. package/dist/core/task-spec.d.ts +29 -2
  111. package/dist/core/tool-policy.d.ts +31 -5
  112. package/dist/core/tool-policy.js +45 -32
  113. package/dist/core/tool-spec.d.ts +37 -4
  114. package/dist/core/types.d.ts +1 -1
  115. package/dist/core/write-protect.d.ts +105 -20
  116. package/dist/core/write-protect.js +89 -32
  117. package/dist/index.d.ts +8 -7
  118. package/dist/index.js +6 -5
  119. package/dist/orchestration/workflow-primitives.js +10 -20
  120. package/dist/stores/file/memory-store.d.ts +5 -2
  121. package/dist/stores/file/memory-store.js +8 -4
  122. package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -2
  123. package/dist/tools/fs/bash-readonly-classifier.js +376 -41
  124. package/dist/tools/fs/fs-bash.d.ts +1 -1
  125. package/dist/tools/fs/fs-bash.js +19 -28
  126. package/dist/tools/fs/fs-shared.d.ts +22 -0
  127. package/dist/tools/fs/fs-shared.js +30 -0
  128. package/dist/tools/fs/index.d.ts +1 -1
  129. package/dist/tools/fs/index.js +1 -1
  130. package/dist/tools/fs/read-deny.d.ts +4 -3
  131. package/dist/tools/fs/safety.d.ts +15 -0
  132. package/dist/tools/fs/safety.js +7 -0
  133. package/package.json +2 -1
  134. package/test/export-surface.snapshot.json +31 -5
  135. package/wire-changes/core@7.18.0.json +1273 -0
  136. package/wire-changes/core@7.19.0.json +457 -0
  137. package/wire-changes/core@7.20.0.json +568 -0
@@ -78,6 +78,7 @@ import { ToolRosterDeltaSeat } from "../tool-roster.js";
78
78
  import { faceOf } from "../tool-roster.js";
79
79
  import { catalogEntriesNamed, pathTargetOf } from "../tool-registry.js";
80
80
  import { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
81
+ import { resolveTaskRootCanonical } from "./task-root-canonical.js";
81
82
  import { createStructuredProjector } from "./tool-output-projection.js";
82
83
  function warnCompactionWindowHazard(tracer, spec, model, compModel, hostTaskId) {
83
84
  if (compModel === undefined)
@@ -365,6 +366,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
365
366
  }
366
367
  const ownedEnv = ownedEnvSeat.current;
367
368
  const { taskRootFinal, rebaseRestoredPath } = await prepareWorkspaceRestore({ ownedEnv, deps, spec, internals, resume, taskRootInitial, abortController, sessionId });
369
+ const { canonical: taskRootCanonical } = await resolveTaskRootCanonical({ env: executionEnv, given: taskRootFinal, onNotice: deps.onNotice, sessionId, runId });
368
370
  const { trackFileEdit, fileHistoryBoundary, restoredFilePaths, rewindNotes, fileHistoryEnabled, historyScope, historyRoot, historyFs } = await prepareFileHistory({ deps, spec, internals, executionEnv, taskRootFinal, handsEnabled, sessionId, session, resumeAtBeforeParentId, abortController });
369
371
  const { noteFileEdited, editedFilesSnapshot, harnessRef, toolProgressRef, skillScope, forwardEvent, reviewRequestRef, requestReview, stopRequestedRef, requestStopAfterTurn, planModeRef, enterPlanMode, subagentRetain, worktreeIsolation } = prepareRunRefs({ spec, internals, executionEnv, taskRootFinal });
370
372
  let resolvedReadFace;
@@ -438,7 +440,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
438
440
  const callIssuedAtRef = {};
439
441
  const workCutRef = { continuation: false, abortedBeforeTurnEnd: false };
440
442
  const advertisedWritableDirs = handsEnabled
441
- ? new AdvertisedWritableDirs(executionEnv, (dir, reason) => deps.onError?.(new Error(`${dir.advertiser} advertised writable directory skipped (cannot canonicalize): ${dir.path} — ${reason}`), { phase: "config", sessionId }))
443
+ ? new AdvertisedWritableDirs(executionEnv, (dir, reason) => deps.onError?.(new Error(`${dir.advertiser} advertised writable directory skipped: ${dir.path} — ${reason}`), { phase: "config", sessionId }), taskRootCanonical, [...(spec.additionalDirectories ?? []), ...(spec.additionalReadDirectories ?? [])])
442
444
  : undefined;
443
445
  const handsWriteHalfPlanned = handsEnabled && handsWriteHalfMounts({ readOnly: spec.handsReadOnly === true });
444
446
  const memoryPairNameDomain = new Set();
@@ -508,7 +510,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
508
510
  if (internals?.ownOrgAdmissionRef !== undefined && ownOrgVerdictRef.current !== undefined) {
509
511
  internals.ownOrgAdmissionRef.current = ownOrgVerdictRef.current;
510
512
  }
511
- const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGate, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, harnessRef, toolProgressRef, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
513
+ const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, taskRootCanonical, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGate, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, harnessRef, toolProgressRef, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
512
514
  const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
513
515
  const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
514
516
  resolvedReadFace = handsReadFace.resolvedReadFace;
@@ -574,7 +576,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
574
576
  deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId });
575
577
  }
576
578
  }
577
- const projectContext = await prepareProjectContext({ deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume });
579
+ const projectContext = await prepareProjectContext({ deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume, skillListingBudget: doors.skillListingBudget });
578
580
  const { memoryBlock: memoryBlockWithProject, instructionSources, projectInstructionContent, hasSkillManifest, skillsListing, sharedMemoryPairMounted, memoryEnginePairMounted } = projectContext;
579
581
  const { provider, featureFlags, userSystemPrompt, userAppendSystemPrompt, mcpInstructionsBlock, envFacts, tzValid, userTz, gitStatusRef } = await preparePromptInputs({ spec, deps, failClosedReason: selfOrchestrationFailClosedReason(spec, deps), lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, resolvedWriteFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}) });
580
582
  const { deferred, memoryBlock, toolSearch: toolSearchPolicy } = prepareDeferClassify({ spec, deps, roster, toolFaceSnapshot, handsEnabled, fullShellReachable, promptProfile, model, mcp, a2a, sharedMemoryPairMounted, memoryEnginePairMounted, memoryBlock: memoryBlockWithProject, memoryRecallSegment, onceLedger, sessionId });
@@ -590,7 +592,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
590
592
  declaredCards: toolRoster.entries.flatMap((e) => e.renderHints?.resultCards ?? []),
591
593
  onUndeclared: (type) => deliverEngineNotice(deps.onNotice, { code: "config.tool_card_undeclared", message: `a tool minted a \`details.type\` card ${JSON.stringify(type)} its definition never declared — dropped from tool_end.structured (declare it on the definition's catalog row or the caller's renderHints.resultCards)`, detail: { type, sessionId, runId } }),
592
594
  });
593
- const { agentListing, announcedListingsRef, dateChange } = prepareListings({ spec, tools, onceLedger, sessionId, toolFaceSnapshot, activeTools, resume, skillsListing, listingRideRef, renderWithDate, envFacts, tzValid, userTz });
595
+ const { agentListing, announcedListingsRef, dateChange } = prepareListings({ spec, tools, onceLedger, sessionId, toolFaceSnapshot, activeTools, resume, skillsListing, listingBudget: doors.skillListingBudget, listingRideRef, renderWithDate, envFacts, tzValid, userTz });
594
596
  const lastBrainContextRef = {};
595
597
  const requestLossyRef = { current: false };
596
598
  const staleOffloadWrittenRefs = new Set();
@@ -699,12 +701,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
699
701
  const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
700
702
  const { suspendForResource, suspendForPlatformLimit, suspendForReview } = prepareBoundaryParks({ saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, pausedRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter });
701
703
  const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
702
- prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
704
+ prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, handsEnv: handsEnabled ? executionEnv : undefined, taskRootFinal, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
703
705
  const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef });
704
706
  const preparedHolder = {};
705
707
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveWriteFaceObserved, 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, carrierWriteFace, readDenyAdditionsNormalized, preparedHolder });
706
708
  await onceLedger.settle(session, announcedListingsRef);
707
- 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, effectiveWriteFace: effectiveWriteFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, toolProgressRef, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), ...(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, undeliveredContinuationRef, 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 } : {}), ...(toolDisclosureSeat !== undefined ? { toolDisclosure: toolDisclosureSeat } : {}) });
709
+ 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, effectiveWriteFace: effectiveWriteFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, toolProgressRef, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), ...(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, undeliveredContinuationRef, 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, frameReceipts: { pending: [], listingSnapshotOwed: false }, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}), ...(toolDisclosureSeat !== undefined ? { toolDisclosure: toolDisclosureSeat } : {}) });
708
710
  rollback.commit();
709
711
  const prepared = buildPrepared();
710
712
  preparedHolder.current = prepared;
@@ -2,6 +2,7 @@ import { snapshotActorAssertion } from "../../internal/llm.js";
2
2
  import { isAskOrigin } from "../ask-origin.js";
3
3
  import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
4
4
  import { CheckpointError, remainingBudgetMicroUsd } from "../checkpoint-store.js";
5
+ import { settledByPerson } from "../gate-outcome.js";
5
6
  import { cloneObserverInput, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
6
7
  import { checkToolPolicyProjection, refuseOutOfContractDecision } from "../tool-policy.js";
7
8
  import { callFaceSeat, parkedCallFaceOf } from "../tool-roster.js";
@@ -170,10 +171,9 @@ export async function resumeApply(input) {
170
171
  throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending", { reason: "origin_missing" });
171
172
  }
172
173
  const facts = plainPolicyOutcome.hostDecision;
173
- const approverCell = facts.approver !== undefined ? { approver: facts.approver } : {};
174
174
  const settlement = facts.decidedBy === "sla_timeout"
175
- ? { kind: "park_sla_expired", who: { party: "host", ...approverCell }, when: Date.now() }
176
- : { kind: plainPolicyOutcome.decision === "allow" ? "human_allowed" : "human_refused", who: { party: "person", ...approverCell }, when: Date.now(), ...(plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason !== undefined ? { note: plainPolicyOutcome.reason } : {}) };
175
+ ? { kind: "park_sla_expired", who: { party: "host", ...(facts.approver !== undefined ? { approver: facts.approver } : {}) }, when: Date.now() }
176
+ : settledByPerson(plainPolicyOutcome.decision === "allow" ? "human_allowed" : "human_refused", facts.approver, plainPolicyOutcome.decision === "deny" ? plainPolicyOutcome.reason : undefined);
177
177
  plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintGateOutcome({ ...(plainPolicyOutcome.decision === "deny" ? { deniedBy: "ask_resolution" } : {}), settled: { settlement, origin: parkedOrigin } }) };
178
178
  }
179
179
  const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
@@ -1,4 +1,5 @@
1
1
  import { startTimeout } from "./clock-and-limits.js";
2
+ import { forwardSafely } from "./forward-sink.js";
2
3
  export function runClockAndContent(input) {
3
4
  const { spec, prepared, queue, internals, rs, ident, parentToolCallId, startedToolCallIds } = input;
4
5
  const effectiveTimeoutMs = spec.limits?.maxWalltimeMs;
@@ -7,13 +8,8 @@ export function runClockAndContent(input) {
7
8
  const timeout = startTimeout(prepared.harness, prepared.abortController, walltimeMonotonicDeadline !== undefined ? walltimeMonotonicDeadline - performance.now() : undefined, prepared.suspendForResource !== undefined);
8
9
  const pushContent = (e) => {
9
10
  queue.push(e);
10
- if (parentToolCallId !== undefined && internals?.onForwardEvent) {
11
- try {
12
- internals.onForwardEvent(e);
13
- }
14
- catch {
15
- }
16
- }
11
+ if (parentToolCallId !== undefined)
12
+ forwardSafely(internals, e);
17
13
  };
18
14
  const ownCommittedTailRef = { current: undefined };
19
15
  const emitCommitted = (entryId, role, toolCallId) => {
@@ -1,4 +1,5 @@
1
1
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
2
+ import { forwardSafely } from "./forward-sink.js";
2
3
  import { emitTrace } from "../trace.js";
3
4
  import { primaryActivityArg } from "../arg-summary.js";
4
5
  import { readDegradation } from "../../brain/degrading.js";
@@ -58,17 +59,20 @@ export function createHarnessHandlers(input) {
58
59
  }
59
60
  if (event.entryId !== undefined && m.role === "user")
60
61
  prepared.fileHistoryBoundary?.begin(event.entryId);
61
- if (event.entryId !== undefined && m.role === "user" && prepared.gitStatusRef.pendingReceipt !== undefined) {
62
- const pr = prepared.gitStatusRef.pendingReceipt;
62
+ if (event.entryId !== undefined && m.role === "user" && prepared.frameReceipts.pending.length > 0) {
63
63
  const c = m.content;
64
64
  const text = typeof c === "string"
65
65
  ? c
66
66
  : Array.isArray(c) && c.length >= 1 && c[0].type === "text"
67
67
  ? c[0].text
68
68
  : undefined;
69
- if (text === pr.text) {
70
- delete prepared.gitStatusRef.pendingReceipt;
71
- pr.commit(event.entryId);
69
+ if (text !== undefined) {
70
+ const settled = prepared.frameReceipts.pending.filter((r) => r.text === text);
71
+ if (settled.length > 0) {
72
+ prepared.frameReceipts.pending = prepared.frameReceipts.pending.filter((r) => r.text !== text);
73
+ for (const r of settled)
74
+ r.commit(event.entryId);
75
+ }
72
76
  }
73
77
  }
74
78
  if (rs.degrade.degraded === undefined && m.role === "assistant") {
@@ -309,11 +313,7 @@ export function createHarnessHandlers(input) {
309
313
  ...ident(),
310
314
  };
311
315
  queue.push(progress);
312
- try {
313
- internals?.onForwardEvent?.(progress);
314
- }
315
- catch {
316
- }
316
+ forwardSafely(internals, progress);
317
317
  }
318
318
  emitTrace(rs.telemetry.tracer, () => ({ kind: "turn.end", version: 1, taskId: rs.telemetry.taskId, turn: stats.turns, ts: Date.now() }));
319
319
  rs.turn.turnUsage = undefined;
@@ -26,7 +26,9 @@ import { maybeHumanRejectionHalt } from "./prepare-gate-stations.js";
26
26
  import { nextHumanInputSeq } from "./steer-admission.js";
27
27
  import { settleTeardownLeg } from "./teardown-bounded.js";
28
28
  import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
29
- import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingDelta, renderOrphanedBackgroundTasks, renderSkillsListingDelta, stampWriteAnchor } from "./turn-attachments.js";
29
+ import { announceListingTruncation } from "../skill-listing-budget.js";
30
+ import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingFrame, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
31
+ import { flushOwedAnnouncedSnapshot } from "./announce-once-ledger.js";
30
32
  export async function runLeg(input) {
31
33
  const { spec, queue, prepared, resume, rs, isDelegatedChild, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
32
34
  let final;
@@ -233,7 +235,8 @@ export async function runLeg(input) {
233
235
  if (gitResumeDelivered !== undefined) {
234
236
  const delivered = gitResumeDelivered;
235
237
  prepared.gitStatusRef.protectedText = delivered.wrapped;
236
- prepared.gitStatusRef.pendingReceipt = {
238
+ prepared.frameReceipts.pending.push({
239
+ source: "git_status",
237
240
  text: continuation,
238
241
  commit: (entryId) => {
239
242
  prepared.gitStatusRef.announced = { kind: delivered.kind, hash: delivered.hash, entryId };
@@ -241,7 +244,7 @@ export async function runLeg(input) {
241
244
  rs.attach.attachmentsInjected += 1;
242
245
  prepared.gitStatusRef.mirrorOwed = { kind: delivered.kind, hash: delivered.hash, entryId };
243
246
  },
244
- };
247
+ });
245
248
  }
246
249
  const pending = resume.cp.pendingAction;
247
250
  const rejectionHalt = pending.kind === "tool_approval" && resume.outcome.gate === "policy_ask"
@@ -335,31 +338,33 @@ export async function runLeg(input) {
335
338
  const attach = rs.attach.attachState;
336
339
  if (rs.attach.agentListingOn && prepared.agentListing !== undefined) {
337
340
  const listing = prepared.agentListing;
338
- const body = renderAgentListingDelta(attach, listing.entries, listing.toolName, listing.models);
339
- if (body !== undefined) {
341
+ const frame = renderAgentListingFrame(attach, listing.entries, listing.toolName, listing.models, listing.budget);
342
+ if (frame !== undefined) {
340
343
  firstFrames.push({
341
344
  source: "agent_listing",
342
- body,
345
+ body: frame.text,
343
346
  commit: () => {
344
347
  commitAgentListing(attach, listing.entries, listing.models);
345
348
  prepared.announcedListingsRef.agents = listing.entries.map((e) => e.name);
346
349
  if (listing.models !== undefined)
347
350
  prepared.announcedListingsRef.models = [...listing.models];
348
351
  },
352
+ disclose: () => announceListingTruncation(runner.deps.onNotice, { listing: "agents", total: frame.listed, nameOnly: frame.nameOnly, dropped: frame.dropped, budget: listing.budget, sessionId: prepared.sessionId }),
349
353
  });
350
354
  }
351
355
  }
352
356
  if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined) {
353
357
  const listing = prepared.skillsListing;
354
- const body = renderSkillsListingDelta(attach, listing.entries);
355
- if (body !== undefined) {
358
+ const frame = renderSkillsListingFrame(attach, listing.entries, listing.budget);
359
+ if (frame !== undefined) {
356
360
  firstFrames.push({
357
361
  source: "skills_listing",
358
- body,
362
+ body: frame.text,
359
363
  commit: () => {
360
364
  commitSkillsListing(attach, listing.entries);
361
365
  prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
362
366
  },
367
+ disclose: () => announceListingTruncation(runner.deps.onNotice, { listing: "skills", total: frame.listed, nameOnly: frame.nameOnly, dropped: frame.dropped, budget: listing.budget, sessionId: prepared.sessionId }),
363
368
  });
364
369
  }
365
370
  }
@@ -415,7 +420,8 @@ export async function runLeg(input) {
415
420
  if (delivered.standalone)
416
421
  await prepared.harness.nextTurn(delivered.wrapped, { engineMinted: true });
417
422
  prepared.gitStatusRef.protectedText = delivered.wrapped;
418
- prepared.gitStatusRef.pendingReceipt = {
423
+ prepared.frameReceipts.pending.push({
424
+ source: "git_status",
419
425
  text: delivered.standalone ? delivered.wrapped : effectiveObjective,
420
426
  commit: (entryId) => {
421
427
  prepared.gitStatusRef.announced = { kind: delivered.kind, hash: delivered.hash, entryId };
@@ -423,15 +429,21 @@ export async function runLeg(input) {
423
429
  rs.attach.attachmentsInjected += 1;
424
430
  prepared.gitStatusRef.mirrorOwed = { kind: delivered.kind, hash: delivered.hash, entryId };
425
431
  },
426
- };
432
+ });
427
433
  }
428
434
  for (const f of firstFrames) {
429
- f.commit();
430
- queue.push({ type: "steering_injected", source: f.source, preview: redactThenCut(f.body, 220), ...ident() });
435
+ prepared.frameReceipts.pending.push({
436
+ source: f.source,
437
+ text: effectiveObjective,
438
+ commit: () => {
439
+ f.commit();
440
+ queue.push({ type: "steering_injected", source: f.source, preview: redactThenCut(f.body, 220), ...ident() });
441
+ rs.attach.attachmentsInjected += 1;
442
+ prepared.frameReceipts.listingSnapshotOwed = true;
443
+ f.disclose();
444
+ },
445
+ });
431
446
  }
432
- rs.attach.attachmentsInjected += firstFrames.length;
433
- if (firstFrames.length > 0 && prepared.announcedSnapshotRecovered)
434
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
435
447
  const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
436
448
  queue.push({
437
449
  ...buildHumanInputEvent({
@@ -475,6 +487,9 @@ export async function runLeg(input) {
475
487
  timeout.clear();
476
488
  prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
477
489
  prepared.toolProgressRef.current?.clear();
490
+ if (prepared.frameReceipts.listingSnapshotOwed) {
491
+ await settleTeardownLeg(() => flushOwedAnnouncedSnapshot(prepared), "announced-listing snapshot (leg exit)", (e) => runner.deps.onError?.(e, { phase: "degraded", sessionId: prepared.sessionId }));
492
+ }
478
493
  prepared.abortController.abort();
479
494
  prepared.releaseSignal();
480
495
  strandedHumanAnswers = prepared.settleContentAskBindings();
@@ -12,6 +12,7 @@ import { emitTrace } from "../trace.js";
12
12
  import { deliverEngineNotice } from "../types.js";
13
13
  import { assembleResult } from "./assemble-result.js";
14
14
  import { discloseUnconsumedHalt } from "./halt-attribution.js";
15
+ import { forwardSafely } from "./forward-sink.js";
15
16
  import { TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
16
17
  import { resumeDecisionWasNegative } from "./decide-continuation.js";
17
18
  import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
@@ -276,11 +277,7 @@ export async function runSettleAndTeardown(input) {
276
277
  ...ident(),
277
278
  };
278
279
  queue.push(terminalTick);
279
- try {
280
- internals?.onForwardEvent?.(terminalTick);
281
- }
282
- catch {
283
- }
280
+ forwardSafely(internals, terminalTick);
284
281
  }
285
282
  if (manualCompactRef.waiters.length > 0) {
286
283
  queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "task_ending", ...ident() });
@@ -4,7 +4,8 @@ import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
4
4
  import { DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
5
5
  import { emitTrace } from "../trace.js";
6
6
  import { redactThenCut } from "../../agents/subagent-steps.js";
7
- import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, rebaseCadenceWindows, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta } from "./turn-attachments.js";
7
+ import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, isIntactSource, rebaseCadenceWindows, renderAgentListingFrame, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch, renderSkillsListingFrame, renderToolsDelta } from "./turn-attachments.js";
8
+ import { announceListingTruncation } from "../skill-listing-budget.js";
8
9
  import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
9
10
  import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
10
11
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
@@ -202,6 +203,7 @@ export function createTurnBoundary(input) {
202
203
  }
203
204
  let boundaryAttachmentBytes = 0;
204
205
  let attachmentsPayload;
206
+ const listingCommits = [];
205
207
  if ((rs.attach.dateState !== undefined ||
206
208
  rs.attach.instrState !== undefined ||
207
209
  rs.attach.sizeGuidelineState !== undefined ||
@@ -213,6 +215,7 @@ export function createTurnBoundary(input) {
213
215
  !prepared.abortController.signal.aborted) {
214
216
  const due = [];
215
217
  if (rs.attach.attachState !== undefined) {
218
+ const attachState = rs.attach.attachState;
216
219
  const changedFilesOn = Boolean(rs.attach.attachmentsCfg?.changedFiles);
217
220
  let changed;
218
221
  if (changedFilesOn && prepared.detectExternalChanges !== undefined) {
@@ -263,6 +266,10 @@ export function createTurnBoundary(input) {
263
266
  const undiscoveredTools = toolSearchReminderOn
264
267
  ? [...prepared.deferredToolNames].filter((n) => !prepared.activeTools.has(n)).sort()
265
268
  : undefined;
269
+ const receiptPending = (source) => prepared.frameReceipts.pending.some((r) => r.source === source);
270
+ const agentListingDue = rs.attach.agentListingOn && prepared.agentListing !== undefined && !receiptPending("agent_listing");
271
+ const skillsListingDue = rs.attach.skillsListingOn && prepared.skillsListing !== undefined && !receiptPending("skills_listing");
272
+ const listingBudget = prepared.skillsListing?.budget ?? prepared.agentListing?.budget;
266
273
  due.push(...collectDueAttachments(rs.attach.attachState, {
267
274
  turn: stats.turns,
268
275
  cadenceTurn: rs.counters.cadenceTurns,
@@ -306,14 +313,15 @@ export function createTurnBoundary(input) {
306
313
  ? { readdedToolsSwappedUnderStatic: pendingReaddedTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
307
314
  : {}),
308
315
  ...(mcpToolsDelta !== undefined ? { mcpToolsDelta } : {}),
309
- ...(rs.attach.agentListingOn && prepared.agentListing !== undefined
316
+ ...(agentListingDue
310
317
  ? {
311
318
  agentListing: prepared.agentListing.entries,
312
319
  agentToolName: prepared.agentListing.toolName,
313
320
  ...(prepared.agentListing.models !== undefined ? { agentModels: prepared.agentListing.models } : {}),
314
321
  }
315
322
  : {}),
316
- ...(rs.attach.skillsListingOn && prepared.skillsListing !== undefined ? { skillsListing: prepared.skillsListing.entries } : {}),
323
+ ...(skillsListingDue ? { skillsListing: prepared.skillsListing.entries } : {}),
324
+ ...(listingBudget !== undefined ? { listingBudget } : {}),
317
325
  ...(mcpAdds !== undefined || mcpRemovals !== undefined
318
326
  ? { mcpInstructions: { added: mcpAdds ?? [], removed: mcpRemovals ?? [] } }
319
327
  : {}),
@@ -363,30 +371,36 @@ export function createTurnBoundary(input) {
363
371
  announcedOnceDirty = true;
364
372
  }
365
373
  }
366
- if (rs.attach.agentListingOn && prepared.agentListing !== undefined && due.some((a) => a.source === "agent_listing")) {
374
+ if (agentListingDue && due.some((a) => a.source === "agent_listing")) {
367
375
  const listing = prepared.agentListing;
368
- const intactBody = renderAgentListingDelta(rs.attach.attachState, listing.entries, listing.toolName, listing.models);
369
- if (intactBody !== undefined && due.some((a) => a.source === "agent_listing" && a.body === intactBody)) {
370
- commitAgentListing(rs.attach.attachState, listing.entries, listing.models);
371
- prepared.announcedListingsRef.agents = listing.entries.map((e) => e.name);
372
- if (listing.models !== undefined)
373
- prepared.announcedListingsRef.models = [...listing.models];
374
- if (prepared.announcedSnapshotRecovered)
375
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
376
- }
376
+ const frame = renderAgentListingFrame(attachState, listing.entries, listing.toolName, listing.models, listing.budget);
377
+ listingCommits.push({
378
+ source: "agent_listing",
379
+ commit: () => {
380
+ commitAgentListing(attachState, listing.entries, listing.models);
381
+ prepared.announcedListingsRef.agents = listing.entries.map((e) => e.name);
382
+ if (listing.models !== undefined)
383
+ prepared.announcedListingsRef.models = [...listing.models];
384
+ prepared.frameReceipts.listingSnapshotOwed = true;
385
+ announceListingTruncation(deps.runner.deps.onNotice, { listing: "agents", total: frame.listed, nameOnly: frame.nameOnly, dropped: frame.dropped, budget: listing.budget, sessionId: prepared.sessionId });
386
+ },
387
+ });
377
388
  }
378
- if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined && due.some((a) => a.source === "skills_listing")) {
389
+ if (skillsListingDue && due.some((a) => a.source === "skills_listing")) {
379
390
  const listing = prepared.skillsListing;
380
- const intactBody = renderSkillsListingDelta(rs.attach.attachState, listing.entries);
381
- if (intactBody !== undefined && due.some((a) => a.source === "skills_listing" && a.body === intactBody)) {
382
- commitSkillsListing(rs.attach.attachState, listing.entries);
383
- prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
384
- if (prepared.announcedSnapshotRecovered)
385
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
386
- }
391
+ const frame = renderSkillsListingFrame(attachState, listing.entries, listing.budget);
392
+ listingCommits.push({
393
+ source: "skills_listing",
394
+ commit: () => {
395
+ commitSkillsListing(attachState, listing.entries);
396
+ prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
397
+ prepared.frameReceipts.listingSnapshotOwed = true;
398
+ announceListingTruncation(deps.runner.deps.onNotice, { listing: "skills", total: frame.listed, nameOnly: frame.nameOnly, dropped: frame.dropped, budget: listing.budget, sessionId: prepared.sessionId });
399
+ },
400
+ });
387
401
  }
388
- if (announcedOnceDirty && prepared.announcedSnapshotRecovered)
389
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
402
+ if (announcedOnceDirty)
403
+ prepared.frameReceipts.listingSnapshotOwed = true;
390
404
  }
391
405
  if (rs.attach.dateState !== undefined && rs.turn.lastTurnHadToolCalls) {
392
406
  const today = prepared.dateChange.today();
@@ -429,7 +443,8 @@ export function createTurnBoundary(input) {
429
443
  if (due.length > 0) {
430
444
  attachmentsPayload = due.map((a) => mintSystemReminder(sanitizeUntrustedText(a.body, attachmentEnvelopeTags(a.source)), prepared.reminderMark)).join("\n");
431
445
  for (const a of due)
432
- boundaryAttachmentBytes += Buffer.byteLength(a.body, "utf8");
446
+ if (!isIntactSource(a.source))
447
+ boundaryAttachmentBytes += Buffer.byteLength(a.body, "utf8");
433
448
  rs.attach.attachmentsInjected += due.length;
434
449
  for (const a of due) {
435
450
  queue.push({ type: "steering_injected", source: a.source, preview: redactThenCut(a.body, 220), ...ident() });
@@ -473,7 +488,11 @@ export function createTurnBoundary(input) {
473
488
  }
474
489
  if ((attachmentsPayload !== undefined || batchContextBlock !== undefined) && prepared.batchHaltRef.current === undefined) {
475
490
  const payload = attachmentsPayload !== undefined && batchContextBlock !== undefined ? `${attachmentsPayload}\n${batchContextBlock}` : (attachmentsPayload ?? batchContextBlock);
476
- void prepared.harness.steer(payload, { engineMinted: true }).catch(() => { });
491
+ for (const c of listingCommits)
492
+ prepared.frameReceipts.pending.push({ source: c.source, text: payload, commit: c.commit });
493
+ void prepared.harness.steer(payload, { engineMinted: true }).catch(() => {
494
+ prepared.frameReceipts.pending = prepared.frameReceipts.pending.filter((r) => r.text !== payload);
495
+ });
477
496
  }
478
497
  const forceManual = manualCompactRef.requested;
479
498
  if ((!withinTaskCompaction && !forceManual) ||
@@ -50,6 +50,7 @@ import { runToolMountFacts } from "./run-tool-mount-facts.js";
50
50
  import { runReasoningSeat } from "./run-reasoning-seat.js";
51
51
  import { runClockAndContent } from "./run-clock-and-content.js";
52
52
  import { createToolProgressLane } from "./tool-progress-lane.js";
53
+ import { createForwardSinkScope } from "./forward-sink.js";
53
54
  import { runBrainSinks } from "./run-brain-sinks.js";
54
55
  import { runCompactionMachinery } from "./run-compaction-machinery.js";
55
56
  import { runStopAndFinalVerify } from "./run-stop-and-final-verify.js";
@@ -344,7 +345,7 @@ export class Runner {
344
345
  manualCompactRef,
345
346
  taskIdRef,
346
347
  resume,
347
- internals: { ...(internals ?? {}), detachHub },
348
+ internals: { ...(internals ?? {}), detachHub, forwardScope: createForwardSinkScope(this.deps.onNotice) },
348
349
  notifyRef,
349
350
  captureOptOutRef,
350
351
  entryActor,
@@ -1,6 +1,7 @@
1
1
  import { type TSchema } from "typebox";
2
2
  import type { AgentTool } from "../../internal/harness-types.js";
3
3
  import type { SkillManifest } from "../types.js";
4
+ import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
4
5
  import type { ActiveSkillScope } from "./active-skill-scope.js";
5
6
  /** Reserved name of the injected structured-output tool (CC 2.1.187 `StructuredOutput`; batch-A rename — old
6
7
  * "submit_output" is canonicalized for pre-rename durable state). */
@@ -57,15 +58,6 @@ export declare const SKILL_TOOL_NAME = "Skill";
57
58
  * an oversized skill is excluded from the disclosure block and the tool, with an operator-visible config error. The compaction-time retention caps are a
58
59
  * different door, deliberately. */
59
60
  export declare const SKILL_CONTENT_MAX_CHARS: number;
60
- /** Total listing-block cap in UTF-8 BYTES — [c209-C] R2 C4①: the old 8000-CHAR cap measured a
61
- * different coordinate than the bundle's `ATTACHMENT_BYTE_CAP` (8192 UTF-8 bytes) and counted only
62
- * the description lines (fence framing + name-only tail rode free), so a near-cap frame could
63
- * exceed the bundle cap, get clip-mangled every boundary, never pass the intact-survival commit
64
- * predicate, and REPLAY FOREVER. The cap is now enforced byte-exact over the WHOLE rendered block
65
- * (fences, headers, markers, joiners included), so `buildSkillsBlock` output always fits the
66
- * bundle cap solo and an intact commit is always reachable. MUST stay ≤ ATTACHMENT_BYTE_CAP
67
- * (turn-attachments.ts; not imported here — that module imports this one — pinned by test instead). */
68
- export declare const SKILLS_BLOCK_MAX_BYTES = 8000;
69
61
  /** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face budget, distinct from the (gone) body truncation door:
70
62
  * the body is delivered untruncated at invoke time (CC parity), while attachments ride along bounded and truncation-disclosed. */
71
63
  export declare const SKILL_ATTACHMENTS_MAX_CHARS = 50000;
@@ -123,31 +115,51 @@ export declare const SKILLS_LISTING_PROBE_HEADER = "The following skills are ava
123
115
  /** [c209-C] One skill's disclosure line — `- name: description[(files 尾缀)]` with the per-skill
124
116
  * description cap. Single-sourced so the skills_listing DELTA frame (turn-attachments) renders adds
125
117
  * with byte-identical line shape to the initial `<skills>` block. */
126
- export declare function skillListingLine(s: SkillListingEntry): string;
118
+ export declare function skillListingLine(s: SkillListingEntry, descMaxChars?: number): string;
119
+ /** What one {@link renderSkillsBlock} call produced: the block, and which skills the budget degraded. */
120
+ export interface SkillsBlockRender {
121
+ /** The rendered block — what {@link buildSkillsBlock} returns. */
122
+ text: string;
123
+ /** Skills listed WITHOUT their description (tier B's unspent candidates, or every listed name under
124
+ * tier C), in display order. Raw names: this is host-facing structured data, not prompt text. */
125
+ nameOnly: readonly string[];
126
+ /** Skills the block could not list at all (tier C's overflow tail). The `Skill` tool still serves them. */
127
+ dropped: readonly string[];
128
+ }
129
+ /** The text-only face of {@link renderSkillsBlock} — the `<skills>` BLOCK's face, not the listing
130
+ * family's: the drift frame renders its own arms through the shared tier engine
131
+ * ({@link renderSkillLinesWithin}), which is where "one ruler for every frame" lives. */
132
+ export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined;
127
133
  /** Render the `<skills>` disclosure block (metadata only — bodies stay behind the tool).
128
134
  * [c209-C] REVERSAL: no longer composed into the stable system prompt — this is now the RENDERER for
129
135
  * the `skills_listing` initial frame (first-user-turn reminder / boundary re-announce), so the
130
136
  * cached prefix carries zero skill bytes (design/C §2.3; CC 209 classic ships the same listing as a
131
- * first-turn reminder; Q6: 8KB cap ≈ CC's observed 5.9KB listing).
137
+ * first-turn reminder). `budget` absent ⇒ the seat reader's no-knob answer ({@link SKILL_LISTING_DEFAULT_BUDGET}).
138
+ *
139
+ * The fence and its four header lines are this lane's frame; the budget arithmetic and the three
140
+ * degradation tiers belong to {@link renderSkillLinesWithin}, which the drift frame renders through
141
+ * as well. {@link buildSkillsBlock} is the text-only face; this one additionally reports what the
142
+ * budget cost, for the disclosure lane. */
143
+ export declare function renderSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): SkillsBlockRender | undefined;
144
+ /**
145
+ * The skills VOCABULARY of the one folding engine (`listing-fold.ts` `foldListingWithin`): the full
146
+ * `- name: description` line, the name-only line, the block's cap marker and its overflow count line,
147
+ * measured byte-exact inside a caller-supplied FRAME — the engine both {@link renderSkillsBlock} and
148
+ * the drift frame's ADD arm render through, so the initial `<skills>` block and a later "new skills are
149
+ * now available" announcement degrade by the same rules against the same number — the number the seat
150
+ * reader resolved (`@contract skills.listing_budget_enforced_as_declared`), which is the frame's whole
151
+ * bound: the listing is an intact-class attachment, so no delivery lane measures it a second time.
132
152
  *
133
- * [c209-C] R2 C4① — the cap is BYTE-EXACT over the whole rendered block (see
134
- * {@link SKILLS_BLOCK_MAX_BYTES}): three honesty tiers, each chosen with the LATER tiers' fixed
135
- * overhead (cap marker / name-only tail / overflow count line) already reserved so the final
136
- * render can never blow the budget it was sized against —
137
- * A. everything fits → every skill gets its full `- name: description` line (byte-identical to
138
- * the pre-C4 render for every block that was legal under the old cap);
139
- * B. (CC 220 `sbs` @317547-317586) — reserve the cap marker + EVERY skill's name-only line
140
- * first, then spend the remaining slack on descriptions in PRIORITY order
141
- * ({@link SkillListingEntry.declaredRank}), admitting each description whose incremental cost
142
- * still fits and skipping (not stopping at) the ones that do not. Every skill stays listed;
143
- * the ones that lose their description are the low-priority ones, not the alphabetically late
144
- * ones. This replaces the prior "longest full-line PREFIX" cut, which had two defects the
145
- * anchor does not: the surviving set was decided by name sort order (a skill named `zip-deploy`
146
- * lost its description to `aardvark-lint` for no reason connected to either skill), and one
147
- * oversized description truncated the block at itself instead of being skipped over;
148
- * C. even all-name-only overflows (pathological name census) → list what fits and close with an
149
- * honest `(… +N more skills …)` count line — the tool still serves all of them. */
150
- export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry>): string | undefined;
153
+ * Tier B follows CC 220 `sbs` @317547-317586 (reserve the marker + every name-only line, then spend
154
+ * the slack on descriptions in {@link SkillListingEntry.declaredRank} order); tier C closes with an
155
+ * honest `(… +N more skills …)` count line — the tool still serves all of them.
156
+ */
157
+ export declare function renderSkillLinesWithin(skills: ReadonlyArray<SkillListingEntry>, opts: {
158
+ /** The whole frame's byte budget — honoured as given; the seat reader is the one place that decides it. */
159
+ blockMaxBytes: number;
160
+ descMaxChars: number;
161
+ frame: (lines: readonly string[]) => string;
162
+ }): SkillsBlockRender | undefined;
151
163
  /**
152
164
  * The injected `skill` tool: returns a skill's full body as a tool result.
153
165
  *