@sema-agent/core 7.13.0 → 7.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/agents/subagent.d.ts +4 -9
  3. package/dist/agents/subagent.js +16 -12
  4. package/dist/agents/teacher.js +1 -1
  5. package/dist/brain/reasoning.d.ts +23 -0
  6. package/dist/brain/reasoning.js +35 -4
  7. package/dist/brain/stream-engine.js +35 -17
  8. package/dist/brain/timeout.d.ts +25 -4
  9. package/dist/brain/timeout.js +1 -1
  10. package/dist/core/ask-origin.d.ts +28 -9
  11. package/dist/core/ask-origin.js +10 -5
  12. package/dist/core/auto-mode.d.ts +3 -3
  13. package/dist/core/checkpoint-seat.d.ts +18 -0
  14. package/dist/core/checkpoint-seat.js +6 -0
  15. package/dist/core/checkpoint-store.d.ts +9 -16
  16. package/dist/core/checkpoint-store.js +7 -4
  17. package/dist/core/engine-notice.d.ts +3 -1
  18. package/dist/core/env-knob-announce.d.ts +16 -0
  19. package/dist/core/env-knob-announce.js +14 -0
  20. package/dist/core/gate-fold.js +2 -0
  21. package/dist/core/gate-lanes.js +21 -12
  22. package/dist/core/gate-outcome.d.ts +42 -3
  23. package/dist/core/gate-outcome.js +22 -0
  24. package/dist/core/governance-codes.js +1 -1
  25. package/dist/core/hooks.d.ts +5 -16
  26. package/dist/core/hooks.js +6 -4
  27. package/dist/core/mcp.d.ts +3 -2
  28. package/dist/core/mcp.js +5 -11
  29. package/dist/core/memory-engine/engine.d.ts +2 -2
  30. package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
  31. package/dist/core/runner/advertised-writable-dirs.js +48 -0
  32. package/dist/core/runner/assemble-result.d.ts +5 -0
  33. package/dist/core/runner/denial-limit-arms.d.ts +7 -9
  34. package/dist/core/runner/denial-limit-arms.js +8 -10
  35. package/dist/core/runner/gate-exit.d.ts +15 -3
  36. package/dist/core/runner/gate-exit.js +6 -4
  37. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  38. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  39. package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
  40. package/dist/core/runner/prepare-hands-readface.d.ts +10 -4
  41. package/dist/core/runner/prepare-hands-readface.js +5 -14
  42. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -3
  43. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  44. package/dist/core/runner/prepare-memory.d.ts +31 -34
  45. package/dist/core/runner/prepare-memory.js +73 -51
  46. package/dist/core/runner/prepare-policy-chain.js +5 -2
  47. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  48. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  49. package/dist/core/runner/prepare-question-face.d.ts +10 -8
  50. package/dist/core/runner/prepare-question-face.js +1 -3
  51. package/dist/core/runner/prepare-safety-scan.js +1 -1
  52. package/dist/core/runner/prepare-task.js +57 -48
  53. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  54. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  55. package/dist/core/runner/run-compaction-machinery.js +2 -0
  56. package/dist/core/runner/run-harness-handlers.js +3 -1
  57. package/dist/core/runner/stream-settle-backstop.js +1 -1
  58. package/dist/core/runtime-caps.d.ts +21 -0
  59. package/dist/core/runtime-caps.js +5 -1
  60. package/dist/core/task-event.d.ts +11 -2
  61. package/dist/core/task-registry-shared.js +8 -6
  62. package/dist/core/task-result.d.ts +15 -0
  63. package/dist/core/terminal-cause.d.ts +6 -2
  64. package/dist/core/tool-policy.d.ts +34 -28
  65. package/dist/core/tool-policy.js +29 -5
  66. package/dist/core/tool-roster.js +2 -0
  67. package/dist/core/tool-spec.d.ts +13 -12
  68. package/dist/core/types.d.ts +2 -1
  69. package/dist/core/types.js +1 -0
  70. package/dist/core/wiring-manifest.d.ts +16 -9
  71. package/dist/core/wiring-manifest.js +8 -3
  72. package/dist/index.d.ts +5 -4
  73. package/dist/index.js +4 -3
  74. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  75. package/dist/orchestration/run-workflow-tool.js +1 -1
  76. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  77. package/dist/orchestration/workflow-primitives.js +3 -3
  78. package/dist/orchestration/workflow-types.d.ts +14 -0
  79. package/dist/orchestration/workflow.d.ts +41 -4
  80. package/dist/orchestration/workflow.js +23 -5
  81. package/dist/tools/fs/fs-shared.js +9 -3
  82. package/dist/tools/fs/read-deny.d.ts +24 -8
  83. package/dist/tools/fs/read-deny.js +20 -1
  84. package/package.json +2 -1
  85. package/test/export-surface.snapshot.json +25 -1
@@ -68,12 +68,14 @@ import { defaultTaskRegistry } from "../task-registry.js";
68
68
  import { announcePeerLaneMount } from "../../agents/peer-session-drain.js";
69
69
  import { createSchedulerTools } from "../../tools/scheduler-tools.js";
70
70
  import { selfOrchestrationFailClosedReason } from "../../orchestration/workflow-script-runner.js";
71
- import { remainingBudgetMicroUsd } from "../checkpoint-store.js";
71
+ import { remainingBudgetMicroUsd, resolveCheckpointStore } from "../checkpoint-store.js";
72
+ import { checkpointSeatOf } from "../checkpoint-seat.js";
72
73
  import { durableParkGapFor } from "../park-selfcheck.js";
73
74
  import { deliverEngineNotice } from "../types.js";
74
75
  import { ToolRosterDeltaSeat } from "../tool-roster.js";
75
76
  import { faceOf } from "../tool-roster.js";
76
77
  import { catalogEntriesNamed, pathTargetOf } from "../tool-registry.js";
78
+ import { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
77
79
  import { createStructuredProjector } from "./tool-output-projection.js";
78
80
  function warnCompactionWindowHazard(tracer, spec, model, compModel, hostTaskId) {
79
81
  if (compModel === undefined)
@@ -257,6 +259,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
257
259
  runIdSink.runId = runId;
258
260
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
259
261
  spec = doors.spec;
262
+ const checkpointStore = resolveCheckpointStore(spec, deps), checkpointSeat = checkpointSeatOf(spec, checkpointStore);
260
263
  const autoModeSeat = refuseInvalidAutoModeSeat(spec);
261
264
  const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, microCompactKnob, resolvedRole, model, thinking, compModel, fableMitigations, memoryDelegationEvidence, memoryProvenance, memoryCapturePolicy, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
262
265
  announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
@@ -366,8 +369,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
366
369
  let resolvedReadFace;
367
370
  let readDenyAdditionsNormalized = [];
368
371
  let handsLessResolvedFace;
369
- const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun: () => { abortController.abort(); void harness.abort(); }, runtimeCaps: () => runtimeCaps, hooks: () => hooks, preToolUseObservational: () => preToolUseObservational, hookTimeoutMs: () => hookTimeoutMs, hookEnvFace: () => hookEnvFace, autoModeDecider: () => autoModeDecider, autoModeDenialTracking: () => autoModeDenialTracking, autoModeArming: () => autoModeArming, agentForkDenial: () => agentForkDenial, observersActive: () => observersActive, resolvedReadFace: () => resolvedReadFace, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
370
- const { roster, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDecider, autoModeDenialTracking, autoModeArming, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline } = await prepareCapsAndWorkflow({ spec, deps, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf, memoryEngineSession: () => memoryEngineSession, centerAdoption: () => centerAdoptionRef.current, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized });
372
+ const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, checkpointSeat, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun: () => { abortController.abort(); void harness.abort(); }, runtimeCaps: () => runtimeCaps, hooks: () => hooks, preToolUseObservational: () => preToolUseObservational, hookTimeoutMs: () => hookTimeoutMs, hookEnvFace: () => hookEnvFace, autoModeDecider: () => autoModeDecider, autoModeDenialTracking: () => autoModeDenialTracking, autoModeArming: () => autoModeArming, agentForkDenial: () => agentForkDenial, observersActive: () => observersActive, resolvedReadFace: () => resolvedReadFace, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
373
+ const { roster, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDecider, autoModeDenialTracking, autoModeArming, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline } = await prepareCapsAndWorkflow({ spec, deps, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, checkpointSeat, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf, memoryEngineSession: () => memoryEngineSession, centerAdoption: () => centerAdoptionRef.current, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized });
371
374
  const tools = roster.tools;
372
375
  const pausedRef = {};
373
376
  const remoteEnvFailures = [];
@@ -431,34 +434,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
431
434
  const callIssuedAtRef = {};
432
435
  const workCutRef = { continuation: false, abortedBeforeTurnEnd: false };
433
436
  const memoryWriteGateRef = {};
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 } : {}) };
435
- const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
436
- const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
437
- resolvedReadFace = handsReadFace.resolvedReadFace;
438
- readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
439
- handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
440
- shellGatedBash = handsReadFace.shellGatedBash;
441
- shellGatedMonitor = handsReadFace.shellGatedMonitor;
442
- const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
443
- const { checkpointStore, liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
444
- if (spec.handsReadOnly !== true) {
445
- const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
446
- roster.mountAll(createSchedulerTools(executionEnv, {
447
- scope: sessionScope,
448
- ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
449
- ...(sessionId !== undefined ? { sessionId } : {}),
450
- requestStopAfterTurn,
451
- }).map((t) => (envHandToolNames.add(t.name), t)), { source: "builtin", mountedBy: "scheduler" });
452
- }
453
- if (internals?.insideFork !== true &&
454
- runnerSelf &&
455
- sessionId !== undefined &&
456
- hasSessionFork(runnerSelf.sessions) &&
457
- agentForkDenial === "principal") {
458
- deps.onError?.(new Error(`Fork DENIED for principal "${spec.principal ?? ""}" by runtimeCaps.allowFork=false ` +
459
- `(per-principal entitlement governance, not a misconfiguration — Agent(subagent_type:"fork") will refuse honestly)`), { phase: "config", sessionId });
460
- }
461
- const { lspDiagnostics, nudgeLspOnEdit, lspRunIdent } = prepareLsp({ spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames });
437
+ const advertisedWritableDirs = handsEnabled
438
+ ? new AdvertisedWritableDirs(executionEnv, (dir, reason) => deps.onError?.(new Error(`${dir.advertiser} advertised writable directory skipped (cannot canonicalize): ${dir.path} — ${reason}`), { phase: "config", sessionId }))
439
+ : undefined;
440
+ const handsWriteHalfPlanned = handsEnabled && spec.handsReadOnly !== true;
462
441
  const memoryPairNameDomain = new Set();
463
442
  for (const t of tools) {
464
443
  memoryPairNameDomain.add(t.name);
@@ -477,14 +456,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
477
456
  return { fault: true };
478
457
  }
479
458
  })();
480
- const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, recallDisciplineSegment: memoryRecallSegment, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
459
+ const { memoryEngineSession, memoryTools, composeInjection: composeMemoryInjection, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
481
460
  spec,
482
461
  deps,
483
462
  sessionId,
484
463
  taskRootPath: taskRootFinal,
485
464
  memoryWriteGateRef,
486
465
  reminderMark,
487
- writeToolsMounted: tools.some((t) => t.name === "Write") &&
466
+ writeToolsMounted: (handsWriteHalfPlanned || tools.some((t) => t.name === "Write")) &&
488
467
  !(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
489
468
  !(handsEnabled && isRemoteExecutionEnv(executionEnv) && spec.memoryPersistenceCapable !== true),
490
469
  memoryProvenance,
@@ -503,20 +482,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
503
482
  ...(internals?.memoryCaptureQueryDir !== undefined ? { parentCaptureQueryDir: internals.memoryCaptureQueryDir } : {}),
504
483
  ...(internals?.memoryCaptureAncestors !== undefined ? { captureAncestors: internals.memoryCaptureAncestors } : {}),
505
484
  memoryPersistenceDeclared: spec.memoryPersistenceCapable,
506
- rosterCanPersist: spec.memoryPersistenceCapable ??
507
- tools.some((t) => {
508
- if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
509
- return false;
510
- const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
511
- if (effect === "read")
512
- return false;
513
- const face = faceOf(t);
514
- const pt = face.pathTarget ?? pathTargetOf(t.name);
515
- const family = face.family ?? catalogEntriesNamed(t.name)[0]?.face.family;
516
- if (!((pt !== undefined && pt.access !== "read") || family === "shell"))
517
- return false;
518
- return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
519
- }),
485
+ ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}),
520
486
  memorySearchToolsPlanned,
521
487
  admissionCtx: {
522
488
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
@@ -540,6 +506,34 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
540
506
  if (internals?.ownOrgAdmissionRef !== undefined && ownOrgVerdictRef.current !== undefined) {
541
507
  internals.ownOrgAdmissionRef.current = ownOrgVerdictRef.current;
542
508
  }
509
+ const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
510
+ const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
511
+ const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
512
+ resolvedReadFace = handsReadFace.resolvedReadFace;
513
+ readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
514
+ handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
515
+ shellGatedBash = handsReadFace.shellGatedBash;
516
+ shellGatedMonitor = handsReadFace.shellGatedMonitor;
517
+ const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
518
+ const { liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
519
+ if (spec.handsReadOnly !== true) {
520
+ const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
521
+ roster.mountAll(createSchedulerTools(executionEnv, {
522
+ scope: sessionScope,
523
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
524
+ ...(sessionId !== undefined ? { sessionId } : {}),
525
+ requestStopAfterTurn,
526
+ }).map((t) => (envHandToolNames.add(t.name), t)), { source: "builtin", mountedBy: "scheduler" });
527
+ }
528
+ if (internals?.insideFork !== true &&
529
+ runnerSelf &&
530
+ sessionId !== undefined &&
531
+ hasSessionFork(runnerSelf.sessions) &&
532
+ agentForkDenial === "principal") {
533
+ deps.onError?.(new Error(`Fork DENIED for principal "${spec.principal ?? ""}" by runtimeCaps.allowFork=false ` +
534
+ `(per-principal entitlement governance, not a misconfiguration — Agent(subagent_type:"fork") will refuse honestly)`), { phase: "config", sessionId });
535
+ }
536
+ const { lspDiagnostics, nudgeLspOnEdit, lspRunIdent } = prepareLsp({ spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames });
543
537
  if (internals?.delegationProvenance !== undefined && resume !== undefined) {
544
538
  const live = internals.delegationProvenance.ref.current;
545
539
  const seed = resume.seed.delegationProvenance;
@@ -553,6 +547,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
553
547
  live.incomplete = true;
554
548
  }
555
549
  }
550
+ const rosterCanPersist = spec.memoryPersistenceCapable ??
551
+ tools.some((t) => {
552
+ if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
553
+ return false;
554
+ const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
555
+ if (effect === "read")
556
+ return false;
557
+ const face = faceOf(t);
558
+ const pt = face.pathTarget ?? pathTargetOf(t.name);
559
+ const family = face.family ?? catalogEntriesNamed(t.name)[0]?.face.family;
560
+ if (!((pt !== undefined && pt.access !== "read") || family === "shell"))
561
+ return false;
562
+ return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
563
+ });
564
+ const { memoryBlock: memoryBlockFromEngine, recallDisciplineSegment: memoryRecallSegment, seedFiles: memorySeedFiles } = composeMemoryInjection({ rosterCanPersist });
556
565
  if (memorySeedFiles?.length && seedContextFiles) {
557
566
  try {
558
567
  await seedContextFiles(memorySeedFiles);
@@ -563,7 +572,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
563
572
  }
564
573
  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 });
565
574
  const { memoryBlock: memoryBlockWithProject, instructionSources, projectInstructionContent, hasSkillManifest, skillsListing, sharedMemoryPairMounted, memoryEnginePairMounted } = projectContext;
566
- const { provider, featureFlags, userSystemPrompt, userAppendSystemPrompt, mcpInstructionsBlock, envFacts, tzValid, userTz, gitStatusRef } = await preparePromptInputs({ spec, deps, failClosedReason: selfOrchestrationFailClosedReason(spec, deps), lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId });
575
+ const { provider, featureFlags, userSystemPrompt, userAppendSystemPrompt, mcpInstructionsBlock, envFacts, tzValid, userTz, gitStatusRef } = await preparePromptInputs({ spec, deps, failClosedReason: selfOrchestrationFailClosedReason(spec, deps), lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}) });
567
576
  const { deferred, memoryBlock } = prepareDeferClassify({ spec, deps, roster, toolFaceSnapshot, handsEnabled, fullShellReachable, promptProfile, model, mcp, a2a, sharedMemoryPairMounted, memoryEnginePairMounted, memoryBlock: memoryBlockWithProject, memoryRecallSegment, onceLedger, sessionId });
568
577
  prepareMemoryEngineSession({ memoryEngineSession, internals, memoryDelegationEvidence, externalContentTargetActive, lockedPreflight, mcp, sessionId, runId, deps, provenanceForChildrenRef, delegationSettlementRef, spec, envHandToolNames, roster, contentOriginWrapRef });
569
578
  const { systemPromptSeat, renderWithDate, systemBlocks, promptManifest, epochDeclaredSections, epochArtifactDigestForSnapshot, buildAssembleInputs, centerAdoptionRef } = await preparePromptAssembly({ session, deps, internals, resume, provider, userSystemPrompt, userAppendSystemPrompt, featureFlags, mcpInstructionsBlock, memoryBlock, envFacts, promptProfile, fableMitigations, tools, model, sessionId, reminderMark, onceLedger });
@@ -30,7 +30,7 @@ import type { GateOutcome } from "../gate-outcome.js";
30
30
  import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect, ToolExecuteContext } from "../types.js";
31
31
  import { type UsageWindow } from "../usage-window-store.js";
32
32
  import type { ToolRoster } from "../tool-roster.js";
33
- import { type AutoModeArmReason, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
33
+ import { type AutoModeArmFact, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
34
34
  import type { AnnounceOnceSink, Prepared, PrepareResume, RunInternals, UsageGovernance } from "./contracts.js";
35
35
  export interface PrepareWiringManifestInput {
36
36
  /** borrowed-readonly — the REBOUND spec. Read: `onAsk` (the own seat), `onQuestion` (the question seam), `handsReadOnly` / `tools` (the ungated scan),
@@ -107,7 +107,7 @@ export interface PrepareWiringManifestInput {
107
107
  * source of the hand band's write-capable names for the ungated-write warning (what actually MOUNTED). */
108
108
  toolRoster: ToolRoster;
109
109
  /** borrowed-readonly — the arming outcome the caps phase minted (the manifest's auto-mode face). */
110
- autoModeArmReason: AutoModeArmReason;
110
+ autoModeArmReason: AutoModeArmFact;
111
111
  /** borrowed-readonly — the frozen preflight; only `mcp` (the declared servers the manifest face joins). */
112
112
  lockedPreflight: Pick<LockedPreflight, "mcp">;
113
113
  /** borrowed-mutable — the peer lane's late-bound facts. Writer here: `askEffective` ← the manifest's ask derivation (the
@@ -199,7 +199,7 @@ export function prepareWiringManifest(input) {
199
199
  memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
200
200
  retentionPolicyWired: deps.retentionPolicy !== undefined,
201
201
  ...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
202
- autoMode: { armed: autoModeArmReason === "armed", reason: autoModeArmReason },
202
+ autoMode: { ...autoModeArmReason },
203
203
  mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
204
204
  tools: toolRoster,
205
205
  });
@@ -11,6 +11,8 @@ export function runCompactionMachinery(input) {
11
11
  const u = msg?.usage;
12
12
  if (!u)
13
13
  return;
14
+ if (msg.usageMissing === true)
15
+ stats.usageMissing = true;
14
16
  const fam = cacheFamilyOf(m);
15
17
  const price = runner.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
16
18
  const priced = isModelPriced(m, runner.deps.pricing);
@@ -77,8 +77,10 @@ export function createHarnessHandlers(input) {
77
77
  rs.degrade.recordDegraded({ from: deg.from, to: deg.to, reason: deg.reason, atTurn: stats.turns + 1, ...(deg.chain && { chain: deg.chain }) });
78
78
  }
79
79
  if (m.role === "assistant" && "usage" in m && m.usage) {
80
- if (m.usageMissing)
80
+ if (m.usageMissing) {
81
81
  rs.turn.turnUsageMissing = true;
82
+ stats.usageMissing = true;
83
+ }
82
84
  const u = m.usage;
83
85
  stats.tokens += u.totalTokens || 0;
84
86
  const { totalInputTokens: turnTotal, uncachedInputTokens: turnUncached, costMicroUsd: turnCostMicroUsd, } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
@@ -83,7 +83,7 @@ export function streamSettleBackstop(input) {
83
83
  ? { retryAfterMs: hinted }
84
84
  : {};
85
85
  })(),
86
- stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
86
+ stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0, usageMissing: true },
87
87
  };
88
88
  emitTrace(entryTracer.tracer, () => ({
89
89
  kind: "task.end",
@@ -84,6 +84,19 @@ export interface WorkflowGovernanceBaseline {
84
84
  * These six are the caps the ENGINE enforces; `allowUltracode`/`allowBypassPermissions` are shell-UX /
85
85
  * service-settings-layer concerns (see {@link RunnerDeps.runtimeCapsResolver}).
86
86
  */
87
+ /**
88
+ * #692 C-d — WHICH plane's ratchet denied auto mode, when the resolver knows: the three settings sources
89
+ * CC 2.1.250's `permissions.disableAutoMode` can come from, as the resolver folds them into the one deny bit
90
+ * ({@link RuntimeCaps.autoMode} `=== false`). `org` = a managed / organization policy plane; `local` = the
91
+ * machine's local settings; `settings` = the user's / project's settings file. Read ONLY beside the deny bit
92
+ * (a source with no denial is a contradiction the value screen refuses as a resolver fault); projected onto
93
+ * the leg's manifest read face (`WiringManifest.autoMode.deniedSource`) so a shell can say "your organization
94
+ * turned auto mode off" vs "your settings did" instead of one word for both.
95
+ */
96
+ export declare const AUTO_MODE_DENY_SOURCES: readonly ["org", "local", "settings"];
97
+ export type AutoModeDenySource = (typeof AUTO_MODE_DENY_SOURCES)[number];
98
+ /** Membership test for {@link AutoModeDenySource}. */
99
+ export declare function isAutoModeDenySource(v: unknown): v is AutoModeDenySource;
87
100
  export interface RuntimeCaps {
88
101
  /** `false` DENIES workflow self-orchestration for this principal server-side — even on a deployment that is
89
102
  * capable ({@link RunnerDeps.workflowScriptRunner} + governance). The third stage of the workflows gate.
@@ -136,6 +149,14 @@ export interface RuntimeCaps {
136
149
  * A denial resolves to a no-op (asks flow the original chain; no warn — the gate, not a mistake).
137
150
  */
138
151
  autoMode?: boolean;
152
+ /**
153
+ * #692 C-d (additive) — WHICH source's ratchet set {@link autoMode} to `false`, when the resolver folded
154
+ * several ({@link AutoModeDenySource}). Optional: a resolver that does not track sources omits it and the
155
+ * manifest reads `denied` with no source. Present with `autoMode` not `false`, or spelled outside the set,
156
+ * is a resolver FAULT (announced through `onError`, the deny bit coined fail-closed, `resolver_fault` on the
157
+ * manifest) — a source named for a denial that did not happen is a marshalling error, never a fact.
158
+ */
159
+ autoModeDenySource?: AutoModeDenySource;
139
160
  /**
140
161
  * design/383 §3.1 — may this principal declare a session memory-capture OPT-OUT
141
162
  * (`TaskSpec.memory.capture: "off"` / the Runner flip verb)? `false` = the deployment REQUIRES
@@ -1 +1,5 @@
1
- export {};
1
+ export const AUTO_MODE_DENY_SOURCES = ["org", "local", "settings"];
2
+ const AUTO_MODE_DENY_SOURCE_SET = new Set(AUTO_MODE_DENY_SOURCES);
3
+ export function isAutoModeDenySource(v) {
4
+ return AUTO_MODE_DENY_SOURCE_SET.has(v);
5
+ }
@@ -97,7 +97,16 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
97
97
  * - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
98
98
  * at that address, or the name has no address). This is the class the SHORT retry lane serves.
99
99
  * - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
100
- * tear, a stalled stream. No verdict about the target; the full ladder applies.
100
+ * tear. No verdict about the target; the full ladder applies.
101
+ * - `stall` — no failure was observed on the link at all: THIS process cancelled its own
102
+ * request because one of its stall watchdogs reached its deadline while the stream produced
103
+ * nothing. The distinction from `transport` is WHO GAVE UP, and nothing more: a `transport` frame
104
+ * reports something that happened to the call, a `stall` frame reports a local deadline. It is not
105
+ * a verdict on the remedy — a blackholed connection (no reset, no close) produces silence, and
106
+ * silence is what a watchdog measures, so a `stall` can still turn out to be a network fault; what
107
+ * it always tells you truthfully is that the request was cancelled here, at a configured deadline,
108
+ * rather than lost out there. Reporting these as `transport` asserted the opposite — that the link
109
+ * had dropped — when nothing about the link was known at all.
101
110
  * - `rate_limit` — the provider asked the caller to slow down.
102
111
  * - `server` — the provider reported a failure on its own side.
103
112
  * - `http` — a response the status predicate calls terminal, retried anyway because the
@@ -105,7 +114,7 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
105
114
  * - `output_cap` — not a failure of the connection at all: the request is being re-sent with a
106
115
  * lowered output cap after the provider reported the context limit exceeded (no backoff).
107
116
  */
108
- export type BrainRetryErrClass = "connect_refused" | "transport" | "rate_limit" | "server" | "http" | "output_cap";
117
+ export type BrainRetryErrClass = "connect_refused" | "transport" | "stall" | "rate_limit" | "server" | "http" | "output_cap";
109
118
  /** design/99 §E3/§E10 — the payload of a {@link TaskEvent} `status` event (and the brain→runner signal). */
110
119
  export interface BrainStatus {
111
120
  phase: BrainStatusPhase;
@@ -3,6 +3,7 @@ import { summarizeWorkflowRun } from "./workflow-run-store.js";
3
3
  import { delimitUntrusted } from "./untrusted-text.js";
4
4
  import { boundedRedactedSummary } from "./untrusted-egress.js";
5
5
  import { clipWithFilePointer } from "./tool-errors.js";
6
+ import { announceEnvKnob } from "./env-knob-announce.js";
6
7
  export function mintCompletionId(target) {
7
8
  if (target.completionId === undefined)
8
9
  target.completionId = uuidv7();
@@ -61,14 +62,15 @@ const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
61
62
  export const TASK_OUTPUT_MAX_CHARS = 160_000;
62
63
  const TASK_OUTPUT_MIN_CHARS = 512;
63
64
  function taskMaxOutputChars() {
64
- const raw = process.env.TASK_MAX_OUTPUT_LENGTH;
65
- if (!raw)
65
+ const rawEnv = process.env.TASK_MAX_OUTPUT_LENGTH;
66
+ if (rawEnv === undefined || rawEnv.trim() === "")
66
67
  return TASK_OUTPUT_DEFAULT_CHARS;
67
- if (!/^\d+$/.test(raw.trim()))
68
- return TASK_OUTPUT_DEFAULT_CHARS;
69
- const n = parseInt(raw.trim(), 10);
70
- if (!Number.isFinite(n) || n <= 0)
68
+ const raw = rawEnv.trim();
69
+ const n = /^\d+$/.test(raw) ? parseInt(raw, 10) : Number.NaN;
70
+ if (!Number.isFinite(n) || n <= 0) {
71
+ announceEnvKnob(`TASK_MAX_OUTPUT_LENGTH=${rawEnv} was ignored — it is not a positive whole number of characters. Using ${TASK_OUTPUT_DEFAULT_CHARS} instead.`);
71
72
  return TASK_OUTPUT_DEFAULT_CHARS;
73
+ }
72
74
  return Math.min(Math.max(n, TASK_OUTPUT_MIN_CHARS), TASK_OUTPUT_MAX_CHARS);
73
75
  }
74
76
  export function clipTaskOutput(s, fullOutputPath) {
@@ -25,6 +25,14 @@ import type { NestedUsage } from "./tool-spec.js";
25
25
  * Orchestrators that map an unexpected `suspended` should map an unexpected `needs_review` the same
26
26
  * hard-boundary way (it is, like `suspended`, only safe at the top-level `runTask` boundary).
27
27
  */
28
+ /**
29
+ * Terminal vs waiting. Three words are TERMINAL — the leg is over and nobody is expected to act: `completed`,
30
+ * `failed`, and `blocked` (the AGENT's own report, through `report_blocked`, that it cannot finish and why —
31
+ * a self-report, not a pause; it waits for nobody). Two words are WAITING — a person's decision resumes the
32
+ * same leg through `runner.resume(token, …)`: `suspended` (a durable pre-action approval of a tool call) and
33
+ * `needs_review` (a durable review pause). The task-registry face spells the two waiting words as `parked`.
34
+ * A consumer that files `blocked` beside `suspended` is waiting on a decision that never comes.
35
+ */
28
36
  export type TaskStatus = "completed" | "blocked" | "failed" | "suspended" | "needs_review";
29
37
  /**
30
38
  * RB-439-a — one remote-workspace lifecycle failure, reported as data on {@link TaskResult.remoteEnvFailures}.
@@ -598,6 +606,13 @@ export interface TaskResult {
598
606
  * the compaction subtotal). OBSERVE-ONLY (never gates budget). The workflow display is unaffected (its agents
599
607
  * fail-on-suspend, never durably resume). A `spentToolCalls` ledger fold is a follow-on. */
600
608
  toolCalls?: number;
609
+ /** #692 C-b (additive): present (`true`) when the usage figures on this face are a LOWER BOUND rather than a
610
+ * measurement — at least one model round of the leg reported no usage frame (the brain's `usageMissing`
611
+ * mark; the same word the per-turn `turn_end` frame carries), or the result was minted by a host / an
612
+ * orchestration lane that never ran a model round it could measure (a refused spawn's journal entry, a
613
+ * prepare-time refusal, a synthetic abort). The numeric members stay required and stay what was counted:
614
+ * `tokens: 0` beside this key reads "unknown", never "free". Absent ⇔ every round reported usage. */
615
+ usageMissing?: true;
601
616
  /** Prompt (input) tokens that MISSED the prefix cache, summed across the task's turns and normalized
602
617
  * across providers (Anthropic already reports `input_tokens` this way; OpenAI/vLLM include the cache
603
618
  * in theirs, so the subsets are removed). Mutually exclusive with `cachedTokens`/`cacheWriteTokens*`;
@@ -20,7 +20,10 @@ import type { AssertAllKeysHandled } from "./ask-origin.js";
20
20
  * the one cause→plane derivation, for the faces that still speak the word.
21
21
  * - `completed` — the run finished (a person's clean halt included — see `TaskResult.haltedByUser`).
22
22
  * - `failed` — a limit, a provider failure, an abort, an invalid output…
23
- * - `blocked` — the agent could not finish and said why.
23
+ * - `blocked` — the AGENT reported, through `report_blocked`, that it cannot finish, and why. A TERMINAL
24
+ * cause: the leg is over and it waits for nobody (nothing to resume, no token). The waiting-on-a-person
25
+ * states are the `paused` cause's two status words — `suspended` (a pre-action approval of a tool
26
+ * call) and `needs_review` (a review pause); the task-registry face spells both as `parked`.
24
27
  * - `paused` — a durable pause committed a checkpoint and the run is resumable.
25
28
  * A `GateOutcome` is the record of ONE gate pass (per call); a `TerminalCause` is the record of ONE run
26
29
  * (per leg); a paused run's `gate` and the park row's `gate.kind` are the same word, joined by the token.
@@ -73,7 +76,8 @@ export type TerminalCause = {
73
76
  nestedPause?: PausedCause;
74
77
  } | {
75
78
  kind: "blocked";
76
- /** Why the agent could not finish. */
79
+ /** Why the agent could not finish — the agent's own words from its `report_blocked` call. Terminal: the
80
+ * run is over and nobody is being waited on (contrast `paused`, whose token is the resume capability). */
77
81
  reason: string;
78
82
  } | {
79
83
  kind: "paused";
@@ -106,6 +106,27 @@ export interface ToolCallRequest {
106
106
  * deny/ask lane and the person's own allow rules, never over a mandated ask. */
107
107
  declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
108
108
  export type DecisionReason = (typeof DECISION_REASONS)[number];
109
+ /** Carry every engine attestation (settlement, classifier cause) from a decision onto a reconstruction of it — the
110
+ * seam for the engine's OWN clone sites outside this module (the inherited chain's final-form re-check carries the
111
+ * post-rewrite args on a spread of an ancestor's deny). Not re-exported from `src/index.ts`. */
112
+ export declare function carryEngineAttestations(from: object, to: object): void;
113
+ /** Carry a PRECEDING decision's settlement onto a later deny that has none of its own — the veto record (a person's
114
+ * yes on the settlement, the refusing layer on the disposition; invariant I3) for the engine's own clone sites,
115
+ * where the deny is minted over a decision that already carried an attested approval. A deny that carries its own
116
+ * settlement keeps it (the latest resolution wins). Not re-exported from `src/index.ts`. */
117
+ export declare function carryPrecedingSettlement(preceding: object, to: object): void;
118
+ /** Attest that `d` is a deny a classifier station minted for `call` out of a round of the named FORM. */
119
+ export declare function attestClassifierDenyCause<T extends object>(d: T, cause: import("./gate-outcome.js").ClassifierDenyCause, call: {
120
+ toolCallId: string;
121
+ toolName: string;
122
+ }): T;
123
+ /** Read the attested classifier cause off a deny FOR the named call (the gate's fold and exit are the
124
+ * consumers): a binding to a different call is a replayed object and answers absence. An internal seam
125
+ * between engine modules — deliberately NOT re-exported from `src/index.ts`. */
126
+ export declare function classifierDenyCauseOf(d: unknown, call: {
127
+ toolCallId: string;
128
+ toolName: string;
129
+ }): import("./gate-outcome.js").ClassifierDenyCause | undefined;
109
130
  /** Read the engine-attested settlement off a funneled decision FOR the named call (the gate's exit is the
110
131
  * one consumer): an attestation bound to a different call is a replayed object and answers absence.
111
132
  * Exported for the gate module only — deliberately NOT re-exported from `src/index.ts` (an internal seam
@@ -209,15 +230,6 @@ export type PermissionResult = {
209
230
  * same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
210
231
  * route it would be a window for has not been chosen yet. */
211
232
  denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
212
- /** #616 (additive): the classifier-unavailable FACT on an ASK — `cause` is the verdict's own word
213
- * ({@link import("./auto-mode.js").AutoModeUnavailableCause}). Since #661 the ENGINE no longer writes
214
- * it here: an `unavailable` verdict is a DENY (the deny arm's member above), never an ask handed to a
215
- * person. The member stays on the ask arm as display metadata a policy may self-declare on its own
216
- * ask (nothing reads it to decide anything); the carry stations still copy it onto the approval
217
- * request and the durable row when present. Retirement candidate for the next wire window. */
218
- classifierUnavailable?: {
219
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
220
- };
221
233
  /** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
222
234
  * ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
223
235
  * egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
@@ -287,21 +299,18 @@ export type PermissionResult = {
287
299
  * exclusions; an approval surface may render the card by it. Optional on the TYPE only because a
288
300
  * decision is caller-composable; every ask the gate hands to its resolver carries it. */
289
301
  origin?: import("./ask-origin.js").AskOrigin;
302
+ /** #688 C3 (additive): WHICH mechanism stands behind an `origin: "rule_store_unavailable"` — one word from
303
+ * {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS} (`store`: the wired store could not be read;
304
+ * `call`: the call could not be read against the person's rows — the lexer's sentence is in `message`).
305
+ * ENGINE-STAMPED at the same single stamp point as `origin`, from the gate's own lane read; a policy's
306
+ * self-declared value is overwritten or removed there. Present ⇔ the origin is `rule_store_unavailable`.
307
+ * Carried onto the approval request and the durable row beside the origin word. */
308
+ ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
290
309
  } | {
291
310
  action: "deny";
292
311
  updatedInput?: unknown;
293
312
  message?: string;
294
313
  decisionReason?: DecisionReason;
295
- /** #661 (additive): this deny is the classifier's UNAVAILABILITY — the auto-mode classifier was consulted
296
- * and could not run, and the call is refused with a sentence that says so (CC 2.1.250 `Ze.unavailable` ⇒
297
- * `deny`, `x1t`). `cause` is the verdict's own word ({@link import("./auto-mode.js").AutoModeUnavailableCause}).
298
- * ENGINE-STAMPED beside `decisionReason: "classifier"` at the classifier stations (the gate's own and the
299
- * inherited-lane arms) on an `unavailable` verdict only; a `block` and a `parse_error` deny carry nothing
300
- * here. Read by the deny observer's payload ({@link import("./hooks.js").PermissionDeniedPayload}) and by
301
- * nothing that decides. */
302
- classifierUnavailable?: {
303
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
304
- };
305
314
  };
306
315
  /**
307
316
  * design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
@@ -1157,15 +1166,12 @@ export interface AskRequest {
1157
1166
  * that forwards a decision's member here does not compile, which is the point: the window is the
1158
1167
  * route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
1159
1168
  readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1160
- /** #616 (additive) — present ⇔ the auto-mode classifier was consulted on this ask and could not run (see
1161
- * the {@link PermissionResult} ask-arm member of the same name): the card's "asked because the
1162
- * classifier was unavailable" fact, with the verdict's own cause word. Absent on every ask the classifier
1163
- * answered, was not eligible for, or was not wired for read presence, never absence. Filled by the
1164
- * carry stations from the decision, never a caller/worker-settable field; the durable park row carries
1165
- * the same member (`PendingAction.tool_approval.classifierUnavailable`). */
1166
- readonly classifierUnavailable?: {
1167
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
1168
- };
1169
+ /** #688 C3 (additive) — the wire twin of the {@link PermissionResult} ask-arm member of the same name: which
1170
+ * mechanism stands behind an `origin: "rule_store_unavailable"` (`store` / `call`,
1171
+ * {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS}). Present that origin; a consumer that
1172
+ * renders "the rule store is unreachable" vs "this command could not be read against your rules" branches
1173
+ * here, never on the message text. Filled by the gate's own carry station from the engine-stamped decision. */
1174
+ readonly ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1169
1175
  /** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
1170
1176
  * member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
1171
1177
  * The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
@@ -29,6 +29,32 @@ function transferEngineSettlement(from, to) {
29
29
  const v = ENGINE_SETTLEMENTS.get(from);
30
30
  if (v !== undefined)
31
31
  ENGINE_SETTLEMENTS.set(to, v);
32
+ const c = CLASSIFIER_DENY_CAUSES_SEAT.get(from);
33
+ if (c !== undefined)
34
+ CLASSIFIER_DENY_CAUSES_SEAT.set(to, c);
35
+ }
36
+ export function carryEngineAttestations(from, to) {
37
+ transferEngineSettlement(from, to);
38
+ }
39
+ export function carryPrecedingSettlement(preceding, to) {
40
+ if (ENGINE_SETTLEMENTS.get(to) !== undefined)
41
+ return;
42
+ const v = ENGINE_SETTLEMENTS.get(preceding);
43
+ if (v !== undefined)
44
+ ENGINE_SETTLEMENTS.set(to, v);
45
+ }
46
+ const CLASSIFIER_DENY_CAUSES_SEAT = new WeakMap();
47
+ export function attestClassifierDenyCause(d, cause, call) {
48
+ CLASSIFIER_DENY_CAUSES_SEAT.set(d, { cause, toolCallId: call.toolCallId, toolName: call.toolName });
49
+ return d;
50
+ }
51
+ export function classifierDenyCauseOf(d, call) {
52
+ if (typeof d !== "object" || d === null)
53
+ return undefined;
54
+ const v = CLASSIFIER_DENY_CAUSES_SEAT.get(d);
55
+ if (v === undefined || v.toolCallId !== call.toolCallId || v.toolName !== call.toolName)
56
+ return undefined;
57
+ return v.cause;
32
58
  }
33
59
  export function engineSettlementOf(d, call) {
34
60
  if (typeof d !== "object" || d === null)
@@ -400,7 +426,6 @@ export function combinePolicies(...policies) {
400
426
  let ruleAskText;
401
427
  let probeMandateSeen = false;
402
428
  let fallbackSeen;
403
- let classifierUnavailableSeen;
404
429
  for (const p of policies) {
405
430
  const d = refuseOutOfContractDecision(await p.check(current, signal));
406
431
  if (d.action === "deny") {
@@ -410,6 +435,8 @@ export function combinePolicies(...policies) {
410
435
  }
411
436
  if (settledAllow !== undefined && ENGINE_SETTLEMENTS.get(out) === undefined) {
412
437
  const owned = out === d ? { ...d } : out;
438
+ if (owned !== d)
439
+ transferEngineSettlement(d, owned);
413
440
  transferEngineSettlement(settledAllow, owned);
414
441
  return owned;
415
442
  }
@@ -431,16 +458,13 @@ export function combinePolicies(...policies) {
431
458
  probeMandateSeen = true;
432
459
  if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
433
460
  fallbackSeen = d.denialLimitFallback;
434
- if (d.action === "ask" && d.classifierUnavailable !== undefined && classifierUnavailableSeen === undefined)
435
- classifierUnavailableSeen = d.classifierUnavailable;
436
461
  }
437
462
  if (asked) {
438
463
  const merged = rewrite?.updatedInput;
439
464
  const withRuleAsk = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
440
465
  const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
441
466
  const withFallback = fallbackSeen !== undefined && withMark.denialLimitFallback === undefined ? { ...withMark, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMark;
442
- const withFact = classifierUnavailableSeen !== undefined && withFallback.classifierUnavailable === undefined ? { ...withFallback, classifierUnavailable: { cause: classifierUnavailableSeen.cause } } : withFallback;
443
- return merged !== undefined ? { ...withFact, updatedInput: merged } : withFact;
467
+ return merged !== undefined ? { ...withFallback, updatedInput: merged } : withFallback;
444
468
  }
445
469
  const allowed = rewrite ?? ALLOW;
446
470
  if (settledAllow === undefined)
@@ -208,6 +208,8 @@ export function schemaKeyBoundProblem(schema) {
208
208
  return undefined;
209
209
  }
210
210
  export function rosterMemberBoundProblem(m) {
211
+ if (typeof m.name !== "string")
212
+ return { code: "config.tool_roster_bound", message: `tool has no string name (got ${m.name === null ? "null" : typeof m.name}); every roster row is keyed by a string \`name\`` };
211
213
  const over = (label, value, bound) => `${label} ${JSON.stringify(value.slice(0, 40) + "…")} is ${value.length} characters; the roster's bound is ${bound}`;
212
214
  const of = (text) => `tool ${JSON.stringify(m.name.slice(0, 40))}: ${text}`;
213
215
  if (m.name.length > TOOL_WIRE_NAME_MAX_CHARS)