agentfootprint 9.86.1 → 9.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1011 -0
- package/README.md +14 -0
- package/dist/artifacts/placement.js +4 -0
- package/dist/artifacts/placement.js.map +1 -1
- package/dist/artifacts/present.js +6 -0
- package/dist/artifacts/present.js.map +1 -1
- package/dist/artifacts/wants.js +8 -0
- package/dist/artifacts/wants.js.map +1 -1
- package/dist/core/Agent.js +53 -7
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/LLMCall.js +9 -11
- package/dist/core/LLMCall.js.map +1 -1
- package/dist/core/agent/buildAgentMessageApiChart.js +2 -4
- package/dist/core/agent/buildAgentMessageApiChart.js.map +1 -1
- package/dist/core/agent/buildDynamicAgentChart.js +10 -0
- package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/core/agent/buildMessageApiChart.js +4 -5
- package/dist/core/agent/buildMessageApiChart.js.map +1 -1
- package/dist/core/agent/buildToolRegistry.js +4 -0
- package/dist/core/agent/buildToolRegistry.js.map +1 -1
- package/dist/core/agent/composeRequest.js +134 -0
- package/dist/core/agent/composeRequest.js.map +1 -0
- package/dist/core/agent/coverage/answer.js +4 -0
- package/dist/core/agent/coverage/answer.js.map +1 -1
- package/dist/core/agent/evidence/evidenceIndex.js +3 -0
- package/dist/core/agent/evidence/evidenceIndex.js.map +1 -1
- package/dist/core/agent/evidence/gate.js +3 -0
- package/dist/core/agent/evidence/gate.js.map +1 -1
- package/dist/core/agent/outputEnforcement.js +3 -0
- package/dist/core/agent/outputEnforcement.js.map +1 -1
- package/dist/core/agent/presentTool.js +3 -0
- package/dist/core/agent/presentTool.js.map +1 -1
- package/dist/core/agent/repeatedCall.js +3 -0
- package/dist/core/agent/repeatedCall.js.map +1 -1
- package/dist/core/agent/resultCeiling.js +3 -0
- package/dist/core/agent/resultCeiling.js.map +1 -1
- package/dist/core/agent/selfCallNotice.js +5 -0
- package/dist/core/agent/selfCallNotice.js.map +1 -1
- package/dist/core/agent/stagedRefs.js +5 -0
- package/dist/core/agent/stagedRefs.js.map +1 -1
- package/dist/core/agent/stages/callLLM.js +71 -25
- package/dist/core/agent/stages/callLLM.js.map +1 -1
- package/dist/core/agent/stages/evidenceRecheck.js +6 -0
- package/dist/core/agent/stages/evidenceRecheck.js.map +1 -1
- package/dist/core/agent/stages/seed.js +12 -0
- package/dist/core/agent/stages/seed.js.map +1 -1
- package/dist/core/agent/stages/stepNudge.js +5 -0
- package/dist/core/agent/stages/stepNudge.js.map +1 -1
- package/dist/core/agent/stages/toolCalls.js +29 -4
- package/dist/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/core/agent/stages/wrapUp.js +4 -0
- package/dist/core/agent/stages/wrapUp.js.map +1 -1
- package/dist/core/agent/window/notice.js +6 -0
- package/dist/core/agent/window/notice.js.map +1 -1
- package/dist/core/agent/window/summarize.js +4 -0
- package/dist/core/agent/window/summarize.js.map +1 -1
- package/dist/core/flowchartAsTool.js.map +1 -1
- package/dist/core/runbook/verdicts.js +3 -0
- package/dist/core/runbook/verdicts.js.map +1 -1
- package/dist/core/slots/buildToolsSlot.js +11 -0
- package/dist/core/slots/buildToolsSlot.js.map +1 -1
- package/dist/esm/artifacts/placement.js +4 -0
- package/dist/esm/artifacts/placement.js.map +1 -1
- package/dist/esm/artifacts/present.js +6 -0
- package/dist/esm/artifacts/present.js.map +1 -1
- package/dist/esm/artifacts/wants.js +8 -0
- package/dist/esm/artifacts/wants.js.map +1 -1
- package/dist/esm/core/Agent.d.ts +3 -0
- package/dist/esm/core/Agent.js +53 -7
- package/dist/esm/core/Agent.js.map +1 -1
- package/dist/esm/core/LLMCall.js +9 -11
- package/dist/esm/core/LLMCall.js.map +1 -1
- package/dist/esm/core/agent/buildAgentMessageApiChart.js +2 -4
- package/dist/esm/core/agent/buildAgentMessageApiChart.js.map +1 -1
- package/dist/esm/core/agent/buildDynamicAgentChart.js +10 -0
- package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
- package/dist/esm/core/agent/buildMessageApiChart.d.ts +2 -1
- package/dist/esm/core/agent/buildMessageApiChart.js +4 -5
- package/dist/esm/core/agent/buildMessageApiChart.js.map +1 -1
- package/dist/esm/core/agent/buildToolRegistry.js +4 -0
- package/dist/esm/core/agent/buildToolRegistry.js.map +1 -1
- package/dist/esm/core/agent/composeRequest.d.ts +121 -0
- package/dist/esm/core/agent/composeRequest.js +127 -0
- package/dist/esm/core/agent/composeRequest.js.map +1 -0
- package/dist/esm/core/agent/coverage/answer.js +4 -0
- package/dist/esm/core/agent/coverage/answer.js.map +1 -1
- package/dist/esm/core/agent/evidence/evidenceIndex.js +3 -0
- package/dist/esm/core/agent/evidence/evidenceIndex.js.map +1 -1
- package/dist/esm/core/agent/evidence/gate.js +3 -0
- package/dist/esm/core/agent/evidence/gate.js.map +1 -1
- package/dist/esm/core/agent/outputEnforcement.js +3 -0
- package/dist/esm/core/agent/outputEnforcement.js.map +1 -1
- package/dist/esm/core/agent/presentTool.js +3 -0
- package/dist/esm/core/agent/presentTool.js.map +1 -1
- package/dist/esm/core/agent/repeatedCall.js +3 -0
- package/dist/esm/core/agent/repeatedCall.js.map +1 -1
- package/dist/esm/core/agent/resultCeiling.js +3 -0
- package/dist/esm/core/agent/resultCeiling.js.map +1 -1
- package/dist/esm/core/agent/selfCallNotice.js +5 -0
- package/dist/esm/core/agent/selfCallNotice.js.map +1 -1
- package/dist/esm/core/agent/stagedRefs.js +5 -0
- package/dist/esm/core/agent/stagedRefs.js.map +1 -1
- package/dist/esm/core/agent/stages/callLLM.d.ts +28 -0
- package/dist/esm/core/agent/stages/callLLM.js +70 -24
- package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
- package/dist/esm/core/agent/stages/evidenceRecheck.js +6 -0
- package/dist/esm/core/agent/stages/evidenceRecheck.js.map +1 -1
- package/dist/esm/core/agent/stages/seed.d.ts +26 -0
- package/dist/esm/core/agent/stages/seed.js +12 -0
- package/dist/esm/core/agent/stages/seed.js.map +1 -1
- package/dist/esm/core/agent/stages/stepNudge.js +5 -0
- package/dist/esm/core/agent/stages/stepNudge.js.map +1 -1
- package/dist/esm/core/agent/stages/toolCalls.js +29 -4
- package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
- package/dist/esm/core/agent/stages/wrapUp.js +4 -0
- package/dist/esm/core/agent/stages/wrapUp.js.map +1 -1
- package/dist/esm/core/agent/types.d.ts +64 -0
- package/dist/esm/core/agent/window/notice.js +6 -0
- package/dist/esm/core/agent/window/notice.js.map +1 -1
- package/dist/esm/core/agent/window/summarize.js +4 -0
- package/dist/esm/core/agent/window/summarize.js.map +1 -1
- package/dist/esm/core/flowchartAsTool.d.ts +15 -7
- package/dist/esm/core/flowchartAsTool.js.map +1 -1
- package/dist/esm/core/runbook/verdicts.js +3 -0
- package/dist/esm/core/runbook/verdicts.js.map +1 -1
- package/dist/esm/core/slots/buildToolsSlot.js +11 -0
- package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
- package/dist/esm/events/eventTail.js +4 -0
- package/dist/esm/events/eventTail.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/integrity/disposition/ledger.js +0 -0
- package/dist/esm/integrity/disposition/ledger.js.map +1 -1
- package/dist/esm/lib/context-bisect/llmEdgeWeigher.d.ts +8 -0
- package/dist/esm/lib/context-bisect/llmEdgeWeigher.js +0 -0
- package/dist/esm/lib/context-bisect/llmEdgeWeigher.js.map +1 -1
- package/dist/esm/lib/context-bisect/trajectory.js +3 -7
- package/dist/esm/lib/context-bisect/trajectory.js.map +1 -1
- package/dist/esm/lib/context-ledger/contextLedger.js +8 -12
- package/dist/esm/lib/context-ledger/contextLedger.js.map +1 -1
- package/dist/esm/lib/injection-engine/evaluator.js +3 -0
- package/dist/esm/lib/injection-engine/evaluator.js.map +1 -1
- package/dist/esm/lib/injection-engine/promptTemplate.js +4 -0
- package/dist/esm/lib/injection-engine/promptTemplate.js.map +1 -1
- package/dist/esm/lib/injection-engine/skillGraph.js +17 -0
- package/dist/esm/lib/injection-engine/skillGraph.js.map +1 -1
- package/dist/esm/lib/injection-engine/skillSteps.js +20 -0
- package/dist/esm/lib/injection-engine/skillSteps.js.map +1 -1
- package/dist/esm/lib/injection-engine/skillToolDescriptors.js +26 -0
- package/dist/esm/lib/injection-engine/skillToolDescriptors.js.map +1 -1
- package/dist/esm/lib/iterationBudget.js +4 -0
- package/dist/esm/lib/iterationBudget.js.map +1 -1
- package/dist/esm/lib/rag/defineRAG.js +4 -0
- package/dist/esm/lib/rag/defineRAG.js.map +1 -1
- package/dist/esm/lib/saidByPerson.js +4 -0
- package/dist/esm/lib/saidByPerson.js.map +1 -1
- package/dist/esm/lib/semantics/envelope.js +3 -0
- package/dist/esm/lib/semantics/envelope.js.map +1 -1
- package/dist/esm/lib/spokenIds.js +5 -0
- package/dist/esm/lib/spokenIds.js.map +1 -1
- package/dist/esm/lib/time-travel/epochs.d.ts +210 -0
- package/dist/esm/lib/time-travel/epochs.js +274 -0
- package/dist/esm/lib/time-travel/epochs.js.map +1 -0
- package/dist/esm/lib/time-travel/index.d.ts +14 -0
- package/dist/esm/lib/time-travel/index.js +15 -0
- package/dist/esm/lib/time-travel/index.js.map +1 -0
- package/dist/esm/lib/time-travel/keyedFold.d.ts +112 -0
- package/dist/esm/lib/time-travel/keyedFold.js +286 -0
- package/dist/esm/lib/time-travel/keyedFold.js.map +1 -0
- package/dist/esm/lib/time-travel/milestoneStops.d.ts +128 -0
- package/dist/esm/lib/time-travel/milestoneStops.js +179 -0
- package/dist/esm/lib/time-travel/milestoneStops.js.map +1 -0
- package/dist/esm/lib/time-travel/receipt.d.ts +452 -0
- package/dist/esm/lib/time-travel/receipt.js +388 -0
- package/dist/esm/lib/time-travel/receipt.js.map +1 -0
- package/dist/esm/lib/time-travel/servedView.d.ts +525 -0
- package/dist/esm/lib/time-travel/servedView.js +820 -0
- package/dist/esm/lib/time-travel/servedView.js.map +1 -0
- package/dist/esm/lib/time-travel/sha256.d.ts +24 -0
- package/dist/esm/lib/time-travel/sha256.js +103 -0
- package/dist/esm/lib/time-travel/sha256.js.map +1 -0
- package/dist/esm/lib/trace-toolpack/debugPrompt.js +3 -0
- package/dist/esm/lib/trace-toolpack/debugPrompt.js.map +1 -1
- package/dist/esm/lib/trace-toolpack/traceToolpack.js +5 -0
- package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/esm/maps/engagement/lease.js +4 -0
- package/dist/esm/maps/engagement/lease.js.map +1 -1
- package/dist/esm/maps/engagement/parkCard.js +5 -0
- package/dist/esm/maps/engagement/parkCard.js.map +1 -1
- package/dist/esm/memory/beats/formatAsNarrative.js +4 -0
- package/dist/esm/memory/beats/formatAsNarrative.js.map +1 -1
- package/dist/esm/memory/causal/loadSnapshot.js +4 -0
- package/dist/esm/memory/causal/loadSnapshot.js.map +1 -1
- package/dist/esm/memory/facts/formatFacts.js +4 -0
- package/dist/esm/memory/facts/formatFacts.js.map +1 -1
- package/dist/esm/memory/stages/formatDefault.js +4 -0
- package/dist/esm/memory/stages/formatDefault.js.map +1 -1
- package/dist/esm/recorders/observability/BoundaryRecorder.d.ts +21 -0
- package/dist/esm/recorders/observability/BoundaryRecorder.js +6 -0
- package/dist/esm/recorders/observability/BoundaryRecorder.js.map +1 -1
- package/dist/esm/recorders/observability/FlowchartRecorder.js +3 -0
- package/dist/esm/recorders/observability/FlowchartRecorder.js.map +1 -1
- package/dist/esm/recorders/observability/recordRun.js +5 -0
- package/dist/esm/recorders/observability/recordRun.js.map +1 -1
- package/dist/esm/tool-providers/gatedTools.js +7 -0
- package/dist/esm/tool-providers/gatedTools.js.map +1 -1
- package/dist/esm/tool-providers/skillScopedTools.js +6 -0
- package/dist/esm/tool-providers/skillScopedTools.js.map +1 -1
- package/dist/events/eventTail.js +4 -0
- package/dist/events/eventTail.js.map +1 -1
- package/dist/index.js +135 -102
- package/dist/index.js.map +1 -1
- package/dist/integrity/disposition/ledger.js +0 -0
- package/dist/integrity/disposition/ledger.js.map +1 -1
- package/dist/lib/context-bisect/llmEdgeWeigher.js +0 -0
- package/dist/lib/context-bisect/llmEdgeWeigher.js.map +1 -1
- package/dist/lib/context-bisect/trajectory.js +3 -7
- package/dist/lib/context-bisect/trajectory.js.map +1 -1
- package/dist/lib/context-ledger/contextLedger.js +8 -12
- package/dist/lib/context-ledger/contextLedger.js.map +1 -1
- package/dist/lib/injection-engine/evaluator.js +3 -0
- package/dist/lib/injection-engine/evaluator.js.map +1 -1
- package/dist/lib/injection-engine/promptTemplate.js +4 -0
- package/dist/lib/injection-engine/promptTemplate.js.map +1 -1
- package/dist/lib/injection-engine/skillGraph.js +17 -0
- package/dist/lib/injection-engine/skillGraph.js.map +1 -1
- package/dist/lib/injection-engine/skillSteps.js +20 -0
- package/dist/lib/injection-engine/skillSteps.js.map +1 -1
- package/dist/lib/injection-engine/skillToolDescriptors.js +26 -0
- package/dist/lib/injection-engine/skillToolDescriptors.js.map +1 -1
- package/dist/lib/iterationBudget.js +4 -0
- package/dist/lib/iterationBudget.js.map +1 -1
- package/dist/lib/rag/defineRAG.js +4 -0
- package/dist/lib/rag/defineRAG.js.map +1 -1
- package/dist/lib/saidByPerson.js +4 -0
- package/dist/lib/saidByPerson.js.map +1 -1
- package/dist/lib/semantics/envelope.js +3 -0
- package/dist/lib/semantics/envelope.js.map +1 -1
- package/dist/lib/spokenIds.js +5 -0
- package/dist/lib/spokenIds.js.map +1 -1
- package/dist/lib/time-travel/epochs.js +284 -0
- package/dist/lib/time-travel/epochs.js.map +1 -0
- package/dist/lib/time-travel/index.js +44 -0
- package/dist/lib/time-travel/index.js.map +1 -0
- package/dist/lib/time-travel/keyedFold.js +290 -0
- package/dist/lib/time-travel/keyedFold.js.map +1 -0
- package/dist/lib/time-travel/milestoneStops.js +184 -0
- package/dist/lib/time-travel/milestoneStops.js.map +1 -0
- package/dist/lib/time-travel/receipt.js +395 -0
- package/dist/lib/time-travel/receipt.js.map +1 -0
- package/dist/lib/time-travel/servedView.js +826 -0
- package/dist/lib/time-travel/servedView.js.map +1 -0
- package/dist/lib/time-travel/sha256.js +107 -0
- package/dist/lib/time-travel/sha256.js.map +1 -0
- package/dist/lib/trace-toolpack/debugPrompt.js +3 -0
- package/dist/lib/trace-toolpack/debugPrompt.js.map +1 -1
- package/dist/lib/trace-toolpack/traceToolpack.js +5 -0
- package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
- package/dist/maps/engagement/lease.js +4 -0
- package/dist/maps/engagement/lease.js.map +1 -1
- package/dist/maps/engagement/parkCard.js +5 -0
- package/dist/maps/engagement/parkCard.js.map +1 -1
- package/dist/memory/beats/formatAsNarrative.js +4 -0
- package/dist/memory/beats/formatAsNarrative.js.map +1 -1
- package/dist/memory/causal/loadSnapshot.js +4 -0
- package/dist/memory/causal/loadSnapshot.js.map +1 -1
- package/dist/memory/facts/formatFacts.js +4 -0
- package/dist/memory/facts/formatFacts.js.map +1 -1
- package/dist/memory/stages/formatDefault.js +4 -0
- package/dist/memory/stages/formatDefault.js.map +1 -1
- package/dist/recorders/observability/BoundaryRecorder.js +6 -0
- package/dist/recorders/observability/BoundaryRecorder.js.map +1 -1
- package/dist/recorders/observability/FlowchartRecorder.js +3 -0
- package/dist/recorders/observability/FlowchartRecorder.js.map +1 -1
- package/dist/recorders/observability/recordRun.js +5 -0
- package/dist/recorders/observability/recordRun.js.map +1 -1
- package/dist/tool-providers/gatedTools.js +7 -0
- package/dist/tool-providers/gatedTools.js.map +1 -1
- package/dist/tool-providers/skillScopedTools.js +6 -0
- package/dist/tool-providers/skillScopedTools.js.map +1 -1
- package/dist/types/artifacts/placement.d.ts.map +1 -1
- package/dist/types/artifacts/present.d.ts.map +1 -1
- package/dist/types/artifacts/wants.d.ts.map +1 -1
- package/dist/types/core/Agent.d.ts +3 -0
- package/dist/types/core/Agent.d.ts.map +1 -1
- package/dist/types/core/LLMCall.d.ts.map +1 -1
- package/dist/types/core/agent/buildAgentMessageApiChart.d.ts.map +1 -1
- package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
- package/dist/types/core/agent/buildMessageApiChart.d.ts +2 -1
- package/dist/types/core/agent/buildMessageApiChart.d.ts.map +1 -1
- package/dist/types/core/agent/buildToolRegistry.d.ts.map +1 -1
- package/dist/types/core/agent/composeRequest.d.ts +122 -0
- package/dist/types/core/agent/composeRequest.d.ts.map +1 -0
- package/dist/types/core/agent/coverage/answer.d.ts.map +1 -1
- package/dist/types/core/agent/evidence/evidenceIndex.d.ts.map +1 -1
- package/dist/types/core/agent/evidence/gate.d.ts.map +1 -1
- package/dist/types/core/agent/outputEnforcement.d.ts.map +1 -1
- package/dist/types/core/agent/presentTool.d.ts.map +1 -1
- package/dist/types/core/agent/repeatedCall.d.ts.map +1 -1
- package/dist/types/core/agent/resultCeiling.d.ts.map +1 -1
- package/dist/types/core/agent/selfCallNotice.d.ts.map +1 -1
- package/dist/types/core/agent/stagedRefs.d.ts.map +1 -1
- package/dist/types/core/agent/stages/callLLM.d.ts +28 -0
- package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
- package/dist/types/core/agent/stages/evidenceRecheck.d.ts.map +1 -1
- package/dist/types/core/agent/stages/seed.d.ts +26 -0
- package/dist/types/core/agent/stages/seed.d.ts.map +1 -1
- package/dist/types/core/agent/stages/stepNudge.d.ts.map +1 -1
- package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
- package/dist/types/core/agent/stages/wrapUp.d.ts.map +1 -1
- package/dist/types/core/agent/types.d.ts +64 -0
- package/dist/types/core/agent/types.d.ts.map +1 -1
- package/dist/types/core/agent/window/notice.d.ts.map +1 -1
- package/dist/types/core/agent/window/summarize.d.ts.map +1 -1
- package/dist/types/core/flowchartAsTool.d.ts +15 -7
- package/dist/types/core/flowchartAsTool.d.ts.map +1 -1
- package/dist/types/core/runbook/verdicts.d.ts.map +1 -1
- package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
- package/dist/types/events/eventTail.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/integrity/disposition/ledger.d.ts.map +1 -1
- package/dist/types/lib/context-bisect/llmEdgeWeigher.d.ts +8 -0
- package/dist/types/lib/context-bisect/llmEdgeWeigher.d.ts.map +1 -1
- package/dist/types/lib/context-bisect/trajectory.d.ts.map +1 -1
- package/dist/types/lib/context-ledger/contextLedger.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/evaluator.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/promptTemplate.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/skillGraph.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/skillSteps.d.ts.map +1 -1
- package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts.map +1 -1
- package/dist/types/lib/iterationBudget.d.ts.map +1 -1
- package/dist/types/lib/rag/defineRAG.d.ts.map +1 -1
- package/dist/types/lib/saidByPerson.d.ts.map +1 -1
- package/dist/types/lib/semantics/envelope.d.ts.map +1 -1
- package/dist/types/lib/spokenIds.d.ts.map +1 -1
- package/dist/types/lib/time-travel/epochs.d.ts +211 -0
- package/dist/types/lib/time-travel/epochs.d.ts.map +1 -0
- package/dist/types/lib/time-travel/index.d.ts +15 -0
- package/dist/types/lib/time-travel/index.d.ts.map +1 -0
- package/dist/types/lib/time-travel/keyedFold.d.ts +113 -0
- package/dist/types/lib/time-travel/keyedFold.d.ts.map +1 -0
- package/dist/types/lib/time-travel/milestoneStops.d.ts +129 -0
- package/dist/types/lib/time-travel/milestoneStops.d.ts.map +1 -0
- package/dist/types/lib/time-travel/receipt.d.ts +453 -0
- package/dist/types/lib/time-travel/receipt.d.ts.map +1 -0
- package/dist/types/lib/time-travel/servedView.d.ts +526 -0
- package/dist/types/lib/time-travel/servedView.d.ts.map +1 -0
- package/dist/types/lib/time-travel/sha256.d.ts +25 -0
- package/dist/types/lib/time-travel/sha256.d.ts.map +1 -0
- package/dist/types/lib/trace-toolpack/debugPrompt.d.ts.map +1 -1
- package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
- package/dist/types/maps/engagement/lease.d.ts.map +1 -1
- package/dist/types/maps/engagement/parkCard.d.ts.map +1 -1
- package/dist/types/memory/beats/formatAsNarrative.d.ts.map +1 -1
- package/dist/types/memory/causal/loadSnapshot.d.ts.map +1 -1
- package/dist/types/memory/facts/formatFacts.d.ts.map +1 -1
- package/dist/types/memory/stages/formatDefault.d.ts.map +1 -1
- package/dist/types/recorders/observability/BoundaryRecorder.d.ts +21 -0
- package/dist/types/recorders/observability/BoundaryRecorder.d.ts.map +1 -1
- package/dist/types/recorders/observability/FlowchartRecorder.d.ts.map +1 -1
- package/dist/types/recorders/observability/recordRun.d.ts.map +1 -1
- package/dist/types/tool-providers/gatedTools.d.ts.map +1 -1
- package/dist/types/tool-providers/skillScopedTools.d.ts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,1017 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [9.88.0] - 2026-09-07
|
|
11
|
+
|
|
12
|
+
**The receipt at the stop.** Stand on an `llm-turn` stop, ask what the model
|
|
13
|
+
read, and until now the honest answer was "most of it". The request a provider
|
|
14
|
+
receives is assembled from committed pieces and is itself never committed — the
|
|
15
|
+
`call-llm` bundle holds the response, not the ask. Two things now stand at every
|
|
16
|
+
turn, and one law binds them:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
hash(servedAt(k)) === receiptAt(k).hash
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`servedAt(snapshot, epoch)` rebuilds the request from the committed pieces;
|
|
23
|
+
`receiptAt(snapshot, epoch)` reads the hashes-and-references record the call
|
|
24
|
+
itself left behind. Agreement means the record is complete. Disagreement means
|
|
25
|
+
something reached the model that the run never wrote down — a defect in the
|
|
26
|
+
record, not in the check.
|
|
27
|
+
|
|
28
|
+
Writing the conformance test found **five** places where the committed pieces
|
|
29
|
+
and the sent request had drifted apart. A four-lens review then found eleven
|
|
30
|
+
more, and two independent verifiers found five more after that — two of them
|
|
31
|
+
NEW instances of the very laws the previous pass was enforcing. Every one is
|
|
32
|
+
reproduced and closed below, each with a test that fails without the fix. None
|
|
33
|
+
was closed by loosening an assertion.
|
|
34
|
+
|
|
35
|
+
And then a fourth pass found two more, in the same place, for the same reason:
|
|
36
|
+
the gap catalogue was checked by a person reading it against the two shapes, and
|
|
37
|
+
three careful readings came up short three times. That is not a run of bad luck.
|
|
38
|
+
It is the defect this library diagnosed in 9.86 — **a hand-counted list is short
|
|
39
|
+
the day after** — so the catalogue is no longer counted. It is WALKED:
|
|
40
|
+
`test/lib/time-travel/gap-catalogue-walk.test.ts` derives every field of both
|
|
41
|
+
shapes and requires each one to be named by a gap or excused in writing, then
|
|
42
|
+
damages a real recording the way each gap describes and requires every field
|
|
43
|
+
that moves to be named by that gap.
|
|
44
|
+
|
|
45
|
+
Then a **fourth review round read the walk instead of trusting it**. The walk
|
|
46
|
+
was green and bit on five attack
|
|
47
|
+
probes — and three shipped sentences a renderer prints were still FALSE, which
|
|
48
|
+
is the half no walk can check. The one that matters most said only `Agent` has a
|
|
49
|
+
run id to salt the hashes with, in a paragraph explaining why `LLMCall` mints no
|
|
50
|
+
receipt; `LLMCall.ts` · `createExecutor` mints `runId: makeRunId()` exactly as
|
|
51
|
+
`Agent` does. The other two were the same defect in the account itself: a gap
|
|
52
|
+
naming a field whose absence its own mechanism does not cause. And the walk's
|
|
53
|
+
DIVERGENCE half turned out to be vacuous for two of its four rows — the damages
|
|
54
|
+
moved nothing, so both rows passed while proving nothing, and one gap's field
|
|
55
|
+
list could be emptied outright with the file still green.
|
|
56
|
+
|
|
57
|
+
And then a SEVENTH round drove a real view for every sentence in the catalogue
|
|
58
|
+
instead of reading them, which is how this release actually ends. It found four
|
|
59
|
+
more printed sentences that mislead a reader — one of them written by the round
|
|
60
|
+
before it, and MEASURED false: `no-run-log` said its fields "could not be fully
|
|
61
|
+
recovered here" on a view where the damaged rebuild is byte-identical to the
|
|
62
|
+
intact one. It also overturned that round's own conclusion, that a reduced
|
|
63
|
+
sentence cannot go false. Ten of the eleven still can. What ends the class is
|
|
64
|
+
not a rule about wording but an ASSERTION per claim, driven on a real run, and
|
|
65
|
+
that file now exists. All of it is closed below, at the root, additively.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **`servedAt(source, epoch)` / `servedViews(source)`** — the request an epoch
|
|
70
|
+
was served, rebuilt from the log: the joined system text and its pieces, the
|
|
71
|
+
conversation as sent, the request-only lines, the tool names and schemas, the
|
|
72
|
+
forced tool and the wrap-up withholding. Works on a live snapshot and on a
|
|
73
|
+
recording read back from JSON, in both chart shapes.
|
|
74
|
+
- **`receiptAt(source, epoch)`** — the receipt, or `undefined` on a recording
|
|
75
|
+
made before this release. A missing receipt never makes an epoch unreadable:
|
|
76
|
+
`servedAt` rebuilds it either way, so an old recording stays readable. (It
|
|
77
|
+
does return `undefined` for an epoch the run does not have, which is
|
|
78
|
+
`epochAt`'s answer rather than a fact about receipts — the docstring says so
|
|
79
|
+
now and used to say the opposite.)
|
|
80
|
+
- **`ServedView.gaps`** — what the log honestly cannot rebuild, each entry
|
|
81
|
+
naming the receipt field it explains. `SERVED_GAPS` is the catalogue those
|
|
82
|
+
sentences come from, so a renderer prints the library's own wording. A rebuild
|
|
83
|
+
that quietly omits a piece looks exactly like one that proved the piece
|
|
84
|
+
absent; this is what keeps them apart.
|
|
85
|
+
- **`epochAt` / `epochLocations`** — the ONE owner of where an iteration's
|
|
86
|
+
pieces live: the run's own log under `reactMode: 'dynamic'`, the turn's inner
|
|
87
|
+
`sf-llm-call` history under `'dynamic-grouped'`. `contextLedger` and
|
|
88
|
+
`context-bisect`'s trajectory assembler each carried a private copy of that
|
|
89
|
+
fork; both now ask this one and their own copies are gone. An `EpochLocation`
|
|
90
|
+
carries the FOLD SOURCE its log belongs to — the log plus the base it was
|
|
91
|
+
recorded against — and that fold's `basis` verdict.
|
|
92
|
+
- **`keyedFold(source)`** — the value of one state key at one commit, folded
|
|
93
|
+
from the run's own base. Built because a RESUMED run replays a fiction
|
|
94
|
+
without it: a resume is a fresh executor seeded from `checkpoint.sharedState`,
|
|
95
|
+
so the whole pre-pause world is the resumed run's fold BASE and not its log,
|
|
96
|
+
and `commitValueAt` says in its own docstring that it cannot see a base.
|
|
97
|
+
Measured on a paused-and-resumed agent under `reactMode: 'dynamic'`: the old
|
|
98
|
+
read returned an EMPTY system prompt and a one-message window for a call that
|
|
99
|
+
really went out with 27 characters of prompt and three messages, and declared
|
|
100
|
+
no gap. It now rebuilds both and matches the wire, in both chart shapes.
|
|
101
|
+
`keyed-fold-equivalence.test.ts` pins every answer against footprintjs's own
|
|
102
|
+
`stateAt`, which is used here for the base and the `basis` verdict.
|
|
103
|
+
- **`receipt.params`** — the sampling knobs the call went out with:
|
|
104
|
+
`temperature`, `maxTokens`, `thinkingBudget`, `stop`, `toolChoice`, read off
|
|
105
|
+
the request the provider PORT was handed. The same context at
|
|
106
|
+
`temperature: 0` and at `1.2` is a different call, and "why did this turn
|
|
107
|
+
ramble?" is unanswerable from a record that kept the prompt and dropped the
|
|
108
|
+
dial. Scalars and short strings; no bytes, no privacy change.
|
|
109
|
+
- **`receipt.cache.markersApplied`** — which `cache_control` breakpoints the
|
|
110
|
+
strategy actually applied, three scalars each (`field`, `boundaryIndex`,
|
|
111
|
+
`ttl`). `transformHash` is a digest over the whole prepared request: inside a
|
|
112
|
+
run it says only "something changed", and across epochs it is not comparable
|
|
113
|
+
at all — so it could not answer *did the breakpoints move between call 3 and
|
|
114
|
+
call 4?*, which is the question that decides an Anthropic bill. Two receipts'
|
|
115
|
+
`markersApplied` answer it by inspection.
|
|
116
|
+
- **`receipt.cache.transform`** — `'unchanged' | 'rewritten' | 'unknown'`.
|
|
117
|
+
Branch on this, never on `transformHash === null`.
|
|
118
|
+
- **`RECEIPT_BOUNDARY`** — the one sentence every receipt field is true at, so
|
|
119
|
+
a renderer prints the library's own wording: *a receipt describes the request
|
|
120
|
+
as this library last saw it.* Where that is — the provider port — is in the
|
|
121
|
+
comment beside the constant, because the sentence is printed and the sixth
|
|
122
|
+
round's rule is that a printed sentence names no mechanism.
|
|
123
|
+
- **`ServedView.basis`** — `{ model, provider, runId }`, read off the receipt.
|
|
124
|
+
A served view could not previously say WHICH model saw this, only that
|
|
125
|
+
something did. Absent when no receipt was committed, and deliberately not
|
|
126
|
+
part of the law: there is no committed counterpart to check it against.
|
|
127
|
+
- **`AgentOptions.recordReceipt`** — the receipt's OFF SWITCH. Default ON.
|
|
128
|
+
There is no privacy reason to decline it (it carries no bytes) but there is a
|
|
129
|
+
cost reason: one commit-log value per iteration plus a SHA-256 per system
|
|
130
|
+
piece, per message and per tool schema. An offline eval loop scoring ten
|
|
131
|
+
thousand turns nobody will scrub is entitled to skip all of it. `servedAt`
|
|
132
|
+
still rebuilds every epoch; `receiptAt` returns `undefined`, exactly as on a
|
|
133
|
+
pre-9.88 recording.
|
|
134
|
+
- **Four more `ServedGapKind`s**, because a view must be able to say what it
|
|
135
|
+
cannot prove: `no-fold-base` (the recording travelled without
|
|
136
|
+
`initialState` — most of a resumed run is then unreadable),
|
|
137
|
+
`no-conversation-on-record` (neither `history` nor `messagesInjections` was
|
|
138
|
+
committed: the turns are UNKNOWN, not empty), `no-run-log` (a subtree handed
|
|
139
|
+
in on its own loses every run constant) and `provider-defaults` (the sampling
|
|
140
|
+
dials are the PORT's values; a vendor may resolve its own). Seven kinds ship,
|
|
141
|
+
with `no-receipt-on-chart` below.
|
|
142
|
+
- **`UNGAPPED_FIELDS`** — the other half of the account. `SERVED_GAPS` says what
|
|
143
|
+
a rebuild cannot prove; this says which fields need no gap and, in one
|
|
144
|
+
sentence each, why (`omittedForAttention`, `callRuntimeStageId`, `gaps`).
|
|
145
|
+
Between them they cover every field of a `Receipt` and a `ServedView`, and a
|
|
146
|
+
walk is what keeps that true rather than a person's reading. TWO reasons
|
|
147
|
+
qualify a field for this list and they are not the same reason: no fold can
|
|
148
|
+
fail to produce it (`callRuntimeStageId`), or its absence is universal and has
|
|
149
|
+
nothing to do with this recording (`omittedForAttention` — no chart supplies
|
|
150
|
+
it, on any run). A field a gap DOES name never belongs here, whatever else is
|
|
151
|
+
true of it, which is why `epoch` left.
|
|
152
|
+
- **`ServedGap` kind `no-receipt-on-chart`** — an epoch that minted no receipt
|
|
153
|
+
now says so. `LLMCall` and the two message-API charts run a `call-llm` stage
|
|
154
|
+
and rebuild perfectly, but mint nothing, so `ServedView.basis` was dropped and
|
|
155
|
+
NOTHING explained it — a Lens denying rather than omitting, on a source
|
|
156
|
+
`servedAt`'s own `@param` names as supported. The gap names the fields only a
|
|
157
|
+
receipt carries (`basis.*` — all four, `params`, `cache.*`) and says what
|
|
158
|
+
follows for them. WHY there is no receipt is carried as data, not prose — see
|
|
159
|
+
`ServedGap.cause` below.
|
|
160
|
+
- **`ServedGap.cause` and `ServedGapCause`** — the discriminating fact, computed
|
|
161
|
+
where it is known instead of guessed at in a sentence.
|
|
162
|
+
`'no-receipt-committed'` when nothing was written under the receipt key;
|
|
163
|
+
`'receipt-shape-rejected'` when something WAS and it carries no basis, so the
|
|
164
|
+
read refused it — the value that means the recording is damaged rather than
|
|
165
|
+
that the run never minted. A renderer prints the gap's structural sentence
|
|
166
|
+
and, if it wants, the cause. Absent on every other gap: a gap carries one when
|
|
167
|
+
the site that raised it read something that told it.
|
|
168
|
+
- **`receiptHash` / `messageDigestInput`** — the digest halves of the law, so a
|
|
169
|
+
consumer checks the rebuild with the library's own rule instead of a fourth
|
|
170
|
+
copy of it.
|
|
171
|
+
- **`BoundaryRecorder`** carries `systemPromptText` and the tool catalog through
|
|
172
|
+
from `stream.llm_start`. Both were dropped there, which meant the one opt-in
|
|
173
|
+
that puts the prompt on the record (`recordSystemPrompt`) reached every sink
|
|
174
|
+
except the ordered boundary stream a replay reads.
|
|
175
|
+
|
|
176
|
+
### Fixed
|
|
177
|
+
|
|
178
|
+
- **The system-prompt join is one function.** `joinSystemPrompt` — pieces with
|
|
179
|
+
content, `'\n\n'` between them — was written inline five times (`callLLM`,
|
|
180
|
+
`LLMCall`, both message-API charts, the tool-calls self-call frame). The
|
|
181
|
+
joined string is never committed, so a rebuild rests entirely on the join
|
|
182
|
+
being one rule; a sixth copy in the reader is exactly the defect this closes.
|
|
183
|
+
- **`seed` records two build-time facts** it never had to before, both
|
|
184
|
+
value-conditional so every other agent's committed key set is byte-identical:
|
|
185
|
+
`forcedOutputToolName` (so a rebuild can NAME the forced tool without reading
|
|
186
|
+
the receipt it is checking) and `toolWantsByName` (the last input to the
|
|
187
|
+
staged-refs nudge that was build-time-only — with it, the one model-facing
|
|
188
|
+
line written to no history is recomposed from committed state instead of
|
|
189
|
+
being declared a gap).
|
|
190
|
+
- **`stepOutputText` skips the receipt.** A run-salted digest is semantically
|
|
191
|
+
empty by construction; left in a step's output text it is hundreds of
|
|
192
|
+
characters of noise inside a character budget that then has less room for the
|
|
193
|
+
assistant's own words. Measured: it alone reordered `localizeContextBug`'s
|
|
194
|
+
suspects and demoted a planted fact below a tool. A text corpus scored by an
|
|
195
|
+
embedder must contain only text somebody wrote.
|
|
196
|
+
- **A resumed run no longer replays a fiction.** `readAtCall` /
|
|
197
|
+
`readRunConstant` folded with `commitValueAt`, which cannot see a run's
|
|
198
|
+
initial state. Every key not re-`set` after a resume folded to absent, so
|
|
199
|
+
`servedAt` returned an empty system prompt and a truncated conversation while
|
|
200
|
+
declaring NO gap. Both now fold through `keyedFold`, from the base the
|
|
201
|
+
recording carries; when that base did not travel, the view raises
|
|
202
|
+
`no-fold-base` instead of a confident empty one.
|
|
203
|
+
- **`servedAt` no longer asserts an empty conversation on an `LLMCall` run** —
|
|
204
|
+
a source its own JSDoc named. Those charts have no `history`: the messages
|
|
205
|
+
slot IS the conversation, and `?? []` swallowed the difference. The
|
|
206
|
+
messages-slot join is now one exported function (`messagesFromInjections`),
|
|
207
|
+
called by `LLMCall` on the way out and by the rebuild on the way back — the
|
|
208
|
+
same argument that made the system-prompt join one function. A chart that
|
|
209
|
+
committed neither source raises `no-conversation-on-record`.
|
|
210
|
+
- **`SERVED_GAPS['cache-transform']` no longer claims more than a receipt can
|
|
211
|
+
know.** It said a null `transformHash` "is the proof that this particular
|
|
212
|
+
call was not rewritten". It is not: the receipt is minted at the provider
|
|
213
|
+
PORT, and a decorated provider, a vendor adapter or a consumer's own
|
|
214
|
+
`complete()` rewrites downstream of it. Reproduced with an injected system
|
|
215
|
+
suffix and an unregistered tool, both invisible to a record reading
|
|
216
|
+
`'unchanged'`. The sentence is now scoped to the CACHE STRATEGY and carries
|
|
217
|
+
`RECEIPT_BOUNDARY`.
|
|
218
|
+
- **`messageDigestInput` covers the join key — BOTH of them.** `toolCallId` —
|
|
219
|
+
`tool_use_id` on Anthropic's wire, `tool_call_id` on OpenAI's — pairs a tool
|
|
220
|
+
result to the call that asked for it, and was excluded. Two parallel calls
|
|
221
|
+
whose results happen to be byte-identical hashed the SAME, so filing one
|
|
222
|
+
call's answer under another was invisible to the law. `toolName` was excluded
|
|
223
|
+
too, and on two shipped providers it is the join key: `GeminiProvider` ·
|
|
224
|
+
`toGeminiContents` pairs a `functionResponse` to its call BY NAME (and drops
|
|
225
|
+
a non-real id), and `OllamaProvider` · `toOllamaMessages` puts `tool_name` on
|
|
226
|
+
the wire. There, two `role:'tool'` messages with identical text and SWAPPED
|
|
227
|
+
names still fingerprinted identically — the exact mis-pairing the id was
|
|
228
|
+
added to catch, invisible on the providers that need it most. It rides as its
|
|
229
|
+
own field beside the id, so neither can absorb the other's bytes. It also now covers `thinkingBlocks` and
|
|
230
|
+
`toolCalls[].providerMeta` as `stableJson` fingerprints: `adapters/types.ts`
|
|
231
|
+
is explicit that a signed thinking block must be echoed byte-exact or the API
|
|
232
|
+
rejects the turn, so two requests that differ only there are not the same
|
|
233
|
+
request. Signatures are opaque tokens, not content — no bytes are added.
|
|
234
|
+
- **`stableJson` no longer collapses the unreadable to the empty string.** It
|
|
235
|
+
returns `undefined`, and callers substitute a mark (`UNSERIALIZABLE`) or
|
|
236
|
+
branch. Before, two DIFFERENT requests that could not be serialized compared
|
|
237
|
+
equal, and the receipt wrote `transformHash: null` — "the cache strategy
|
|
238
|
+
changed nothing" — about a pair it had never read. That case is now
|
|
239
|
+
`cache.transform: 'unknown'`.
|
|
240
|
+
- **`readRunConstant` on a subtree says so.** Handed a recording with no run
|
|
241
|
+
log, it read every run constant as absent; the view now raises `no-run-log`.
|
|
242
|
+
- **The per-epoch scrub is a constant factor over the batch form.**
|
|
243
|
+
`epochLocations` re-located every epoch on every `servedAt` call, and the
|
|
244
|
+
iteration number was resolved by rescanning the log per bundle. Measured
|
|
245
|
+
against that shape: the scrub cost 2.4x the batch form at 13 epochs, 3.7x at
|
|
246
|
+
49 and 5.0x at 97 — the factor grew with the run, which is what turned a
|
|
247
|
+
600-turn scrub into 20.8 s. Epochs are now located once per recording
|
|
248
|
+
(a module-level `WeakMap`), the iteration is read from the same one-pass index
|
|
249
|
+
every other key uses, and the fold resumes forward rather than replaying from
|
|
250
|
+
its anchor. The overhead is 1.0-1.1x at every size, and the absolute scrub of
|
|
251
|
+
a 96-turn run (1,651 commits, 97 epochs) fell from 50.2 ms to 17.8 ms. The
|
|
252
|
+
batch form pays for the correctness fix: 10.0 ms to 16.2 ms on the same run,
|
|
253
|
+
because every read now folds from the base.
|
|
254
|
+
- **Control characters in `receipt.ts` are written as escapes.** The digest
|
|
255
|
+
separators were literal `U+001C`-`U+001F` bytes in the source — invisible in a
|
|
256
|
+
terminal, in a diff and in review, and one `sed` away from being eaten. Same
|
|
257
|
+
bytes, same hashes; they can now be read.
|
|
258
|
+
- **`test/lib/time-travel/receipt-conformance.test.ts` · `describe('a redacted
|
|
259
|
+
run')` was a NO-OP, and two READMEs documented what it pretended to prove.**
|
|
260
|
+
It passed `redact: [...]` to `Agent.create`, which has no such option;
|
|
261
|
+
`tsconfig.json` excludes `test/`, so the unknown key was never typechecked and
|
|
262
|
+
was silently dropped. The run was not redacted, and the READMEs' "honest edge"
|
|
263
|
+
told a reader a recording was safe to pass on. The case is replaced by what is
|
|
264
|
+
true — an agent recording carries the plaintext, the receipt carries
|
|
265
|
+
unredacted run-salted hashes of it, and redaction here is EXECUTOR-level
|
|
266
|
+
(`flowchartAsTool({ redact })` scrubs an inner run's commit log; note that the
|
|
267
|
+
same snapshot's live `sharedState` is not scrubbed). Both READMEs now say so.
|
|
268
|
+
`test/type-regressions/AgentOptionsRedaction.assignability.test.ts` pins it at
|
|
269
|
+
the compiler: `redact` is not an `AgentOptions` key and an excess property on
|
|
270
|
+
the literal is refused. The false sentence itself is swept out of the two
|
|
271
|
+
places the previous pass missed — `receipt.ts`'s THIRD LAW, which said the
|
|
272
|
+
salt "is the reason a recording is safe to pass on", and the PRINTED takeaway
|
|
273
|
+
of `examples/observability/24-receipt-at-the-stop.ts`, which a reader copies.
|
|
274
|
+
Both now say what a recording actually contains: the salt protects the
|
|
275
|
+
fingerprints and only the fingerprints.
|
|
276
|
+
|
|
277
|
+
#### The five the verifiers found — two of them new instances of these laws
|
|
278
|
+
|
|
279
|
+
- **`servedAt` raises `no-fold-base` off BOTH folds, not one.** An epoch has
|
|
280
|
+
two: the log holding its call, and the RUN log holding its build-time
|
|
281
|
+
constants (the forced tool's name, the `wants` the staged-refs nudge is
|
|
282
|
+
composed from). Under `reactMode: 'dynamic-grouped'` those are different logs
|
|
283
|
+
with different bases, and `EpochLocation.runBasis` — added by the previous
|
|
284
|
+
pass for exactly this — was computed, exported on a public type, and read by
|
|
285
|
+
nobody. So a grouped recording that travelled without its RUN base read every
|
|
286
|
+
run constant as absent and declared NO gap: the Lens denying rather than
|
|
287
|
+
omitting, which is the law the two blocking fixes before it were about. The
|
|
288
|
+
gap now also names what a missing run base costs — `tools.names`,
|
|
289
|
+
`tools.forced`, `messages.requestOnly`.
|
|
290
|
+
- **A fold's answers are DETACHED.** `keyedFold` memoizes: the same object comes
|
|
291
|
+
back for every read of the same question, and the forward cursor seeds every
|
|
292
|
+
later epoch's replay from that very object. `servedAt` then aliased those
|
|
293
|
+
objects straight into `ServedView` (`tools.schemas`, and `messages.asSent` in
|
|
294
|
+
the common case), so a consumer that edited what it was handed silently
|
|
295
|
+
rewrote what LATER epochs reported was served. This was new in 9.88.0 — the
|
|
296
|
+
reader it replaced, footprintjs's `commitValueAt`, clones per call, so the
|
|
297
|
+
same edit was harmless before; the memo introduced it, and the memo is where
|
|
298
|
+
it is closed. Every answer is now deep-frozen before it is cached, which is
|
|
299
|
+
what footprintjs's own `stateAt` already does, and `servedAt` copies the two
|
|
300
|
+
containers it would otherwise alias so a `ServedView` is a value in its own
|
|
301
|
+
right. `receiptAt` and `epochLocations` (array and locations) are frozen for
|
|
302
|
+
the same reason. Cost, measured at 601 epochs / 10,219 commits: see below.
|
|
303
|
+
- **`receipt.params` describes the request the PORT got.** It was read from
|
|
304
|
+
`baseRequest` — the request handed TO the cache strategy — so a strategy that
|
|
305
|
+
rewrote `maxTokens` or `temperature` left five receipt fields describing
|
|
306
|
+
something the port was never handed, no gap named them, and
|
|
307
|
+
`SERVED_GAPS['provider-defaults']` asserted the falsehood in words ("the
|
|
308
|
+
sampling knobs on the receipt are the values the PORT was handed"). It now
|
|
309
|
+
reads `preparedRequest`, which is what `ReceiptParams` and `RECEIPT_BOUNDARY`
|
|
310
|
+
already promised, and the `provider-defaults` sentence is true.
|
|
311
|
+
- **`SERVED_GAPS['cache-transform']` names everything a rewrite could have
|
|
312
|
+
changed.** It listed the three `cache.*` fields — the report — and excused
|
|
313
|
+
nothing it reports on. A strategy is handed the whole composed request, and
|
|
314
|
+
both the receipt and the rebuild describe the version it was GIVEN, so the
|
|
315
|
+
gap now also covers `system.*`, `messages.*` and `tools.*`. `params` is
|
|
316
|
+
deliberately not among them: that one is read past the strategy.
|
|
317
|
+
- **`FlowchartAsToolOptions.redact` no longer claims a kept record is safe to
|
|
318
|
+
serve back to a model.** The commit-log half of the claim is true; the
|
|
319
|
+
`sharedState` of a kept recording is the live view and holds the plaintext.
|
|
320
|
+
The behaviour is a different subsystem's decision and is recorded, with its
|
|
321
|
+
reproduction and what a fix would cost, as entry 6 of
|
|
322
|
+
`docs/design/2026-09-recorded-not-built.md`.
|
|
323
|
+
- **`ServedGap.fields` says which spelling it uses.** The catalogue is rendered
|
|
324
|
+
beside a `ServedView` but names `Receipt` paths, and three of them differ
|
|
325
|
+
(`system.hash`/`chars` is the view's `system.text`,
|
|
326
|
+
`messages.entries`/`count` is `messages.asSent`, `tools.schemaHashes` is
|
|
327
|
+
`tools.schemas`). The mapping is now on the field's own docstring instead of
|
|
328
|
+
in a renderer's head.
|
|
329
|
+
- **`SERVED_GAPS['no-fold-base']` names the COUNTS, and the epoch number.** It
|
|
330
|
+
named `system.hash` and `messages.entries` and not `system.chars` or
|
|
331
|
+
`messages.count` — the counts of the very things it named. The two counts
|
|
332
|
+
were not unnamed: `cache-transform` names them, for an unrelated reason, and
|
|
333
|
+
its docstring said of exactly those fields that the rebuild "produces them
|
|
334
|
+
and they DO agree with the receipt". So a base-less rebuild reported a
|
|
335
|
+
shorter prompt over fewer turns while the only entry covering the counts told
|
|
336
|
+
a reader they agreed. Measured on a resumed run whose base was stripped: a real system
|
|
337
|
+
prompt and a real window become a shorter prompt and a shorter window, and
|
|
338
|
+
both counts move with them. `tools.withheld` joins them, because it is folded
|
|
339
|
+
from `wrapUpAsked` like any other key, and the VIEW's own `epoch` too, because
|
|
340
|
+
a fold that cannot read `iteration` numbers the turn by its POSITION instead —
|
|
341
|
+
so the view and the receipt can disagree about which turn this is. (That last
|
|
342
|
+
entry shipped as `basis.epoch`, the RECEIPT's number, which a missing base
|
|
343
|
+
cannot touch. Corrected in the round below.)
|
|
344
|
+
- **`cache-transform`'s docstring no longer asserts agreement it cannot have.**
|
|
345
|
+
It said of its composition fields that "the rebuild produces them and they
|
|
346
|
+
agree with the receipt", which is false on every view that also raises
|
|
347
|
+
`no-fold-base`. Both the docstring and the sentence a renderer prints are now
|
|
348
|
+
scoped: this entry says the rebuild stops AT the cache strategy, never that
|
|
349
|
+
the rebuild got that far, and where another gap on the same view names the
|
|
350
|
+
same field that one is the stronger claim.
|
|
351
|
+
- **`no-conversation-on-record` names `messages.requestOnly`.** The staged-refs
|
|
352
|
+
nudge is recomposed FROM the conversation, so a rebuild with no conversation
|
|
353
|
+
finds no refs and reports no nudge — indistinguishable from a call that had
|
|
354
|
+
none.
|
|
355
|
+
- **A `ServedView` is a value, all of it.** Two of its six containers were
|
|
356
|
+
frozen (`messages.asSent`, `tools.schemas`, the two that would otherwise alias
|
|
357
|
+
the fold's memo); `system.pieces`, `messages.requestOnly`, `tools.names`,
|
|
358
|
+
`gaps` and the view object itself were plain, while the type said `readonly`
|
|
359
|
+
throughout. No leak — the other four are built per call — but a promise that
|
|
360
|
+
holds for two containers out of six is one a reader cannot use. The whole view
|
|
361
|
+
is frozen now, down to the pieces and gaps, and the docstring says which two
|
|
362
|
+
are also copies and why.
|
|
363
|
+
|
|
364
|
+
#### The fourth round — three false sentences and one vacuous half
|
|
365
|
+
|
|
366
|
+
The walk was green and bit on five attack probes. What it cannot see is whether
|
|
367
|
+
a sentence is TRUE, and three of them were not.
|
|
368
|
+
|
|
369
|
+
- **A shipped sentence said `LLMCall` has no run id to salt hashes with, and it
|
|
370
|
+
does.** `servedView.ts`'s module comment, and the `no-receipt-on-chart` `why` a
|
|
371
|
+
renderer prints verbatim, both explained the refusal to mint a receipt on the
|
|
372
|
+
three non-agent charts with THE SALT: "only `Agent` has a run id to give". But
|
|
373
|
+
`LLMCall.ts` · `createExecutor` mints `runId: makeRunId()` exactly as `Agent`
|
|
374
|
+
does and owns its own executor, so the salt is there for the taking. The
|
|
375
|
+
DECISION is unchanged — declare, do not mint — because the other reason holds
|
|
376
|
+
for all three charts: a receipt carries a cache verdict about a strategy none
|
|
377
|
+
of them runs, and `cache.transform` has no value meaning "no strategy ran".
|
|
378
|
+
The salt clause is now SCOPED to the two message-API charts, where it is true
|
|
379
|
+
and is the reason wiring cannot fix them: `buildMessageApiChart` and
|
|
380
|
+
`buildAgentMessageApiChart` are exported chart BUILDERS whose deps carry no run
|
|
381
|
+
id and no way to ask for one, run on a consumer's own executor. The printed
|
|
382
|
+
sentence gives the cache verdict and nothing else, because that is the half
|
|
383
|
+
that holds everywhere the gap fires.
|
|
384
|
+
- **The epoch account was INVERTED.** `no-fold-base` named `basis.epoch` — the
|
|
385
|
+
RECEIPT's number, minted live and carried in the call's own bundle, which no
|
|
386
|
+
missing base can move — while the number that CAN be fabricated, the view's
|
|
387
|
+
own `epoch`, was excused in `UNGAPPED_FIELDS` on the ground that "a caller
|
|
388
|
+
passes it to `servedAt` and gets it back". True of `servedAt`; untrue of
|
|
389
|
+
`servedViews()`, which returns whatever the fold produced, and
|
|
390
|
+
`EpochLocation.epoch` falls back to POSITION when it cannot read `iteration`.
|
|
391
|
+
Measured: a resumed run whose base and `iteration` writes had both gone
|
|
392
|
+
rebuilt its second turn as epoch 1 while that turn's own receipt still said 2.
|
|
393
|
+
Now `no-fold-base` names `epoch` and `no-receipt-on-chart` names `basis.epoch`
|
|
394
|
+
(a receipt-only field like the other three on `basis`), the `UNGAPPED_FIELDS`
|
|
395
|
+
key is gone, and `ServedGap.fields` names the ONE place the two shapes hold
|
|
396
|
+
two records of one fact rather than two spellings of it. Pinned by a new
|
|
397
|
+
conformance case that measures the disagreement.
|
|
398
|
+
- **`omittedForAttention` was blamed on the missing receipt.** It was in
|
|
399
|
+
`no-receipt-on-chart.fields`, but its absence has nothing to do with a
|
|
400
|
+
receipt: no chart in this library supplies it, on any recording, so it is absent on views that
|
|
401
|
+
HAVE a receipt too — where nothing explained it at all. It is a key of
|
|
402
|
+
`UNGAPPED_FIELDS` now with the true reason: a slot writes its budget drops to
|
|
403
|
+
`slotCompositions` inside its own subflow and no boundary bubbles them out, so
|
|
404
|
+
`buildReceipt` is never handed one. Recorded as entry 9 of
|
|
405
|
+
`docs/design/2026-09-recorded-not-built.md`.
|
|
406
|
+
- **The walk's divergence half was VACUOUS for two of four rows.** It asks "is
|
|
407
|
+
every field that MOVED named?", which a damage that moves nothing satisfies
|
|
408
|
+
for free. `no-run-log`'s damage moved nothing at all and
|
|
409
|
+
`no-conversation-on-record`'s never reached `messages.requestOnly`: measured,
|
|
410
|
+
emptying `no-run-log.fields` to `[]` and deleting `messages.requestOnly` from
|
|
411
|
+
`no-conversation-on-record` left the file green. The previous round wrote a
|
|
412
|
+
guard for exactly this hazard, for ONE row. It is general now: the loop
|
|
413
|
+
collects the moved paths per damage and FAILS on an empty set, and both rows
|
|
414
|
+
are driven on a run that composes a staged-refs nudge, which is the one shape
|
|
415
|
+
where losing the run log or the conversation really costs a request-only line.
|
|
416
|
+
"A damage row that damages nothing" joins the header's own blind-spot list.
|
|
417
|
+
- **`no-fold-base`'s "mechanical rule" named a reader it does not apply to.**
|
|
418
|
+
The comment said a field belongs on the list when the rebuild derives it from
|
|
419
|
+
`readAtCall`, `readAfterCall` or `readRunConstant`. `readAfterCall` reads the
|
|
420
|
+
receipt, which the call's own bundle commits — no missing base can cost it,
|
|
421
|
+
and naming it is what let the receipt's own `basis.epoch` onto a list it does
|
|
422
|
+
not belong on. The rule is now the two readers that fold over values the log
|
|
423
|
+
may never have written, with the third named as deliberately excluded.
|
|
424
|
+
- **`cache-transform` says it is unconditional.** It is raised on every view,
|
|
425
|
+
including the three charts that can run no cache strategy at all — where the
|
|
426
|
+
same view's `no-receipt-on-chart` says exactly that. The `why` now OPENS with
|
|
427
|
+
the condition: raised unconditionally, vacuous where no strategy ran, a
|
|
428
|
+
boundary rather than a claim that anything was rewritten. Raising it
|
|
429
|
+
conditionally instead would mean inferring "no strategy ran" from a recording,
|
|
430
|
+
which is the absence-of-evidence reading this whole feature refuses; recorded
|
|
431
|
+
as entry 8 of `docs/design/2026-09-recorded-not-built.md`.
|
|
432
|
+
#### The fifth and sixth rounds — a printed gap sentence stops describing code
|
|
433
|
+
|
|
434
|
+
Five review rounds, and each one found NEW false prose in the sentences the
|
|
435
|
+
round before had just written, at a roughly constant rate. That is not a run of
|
|
436
|
+
careless writing. It is the law this library named in 9.84–9.86 — **a sentence
|
|
437
|
+
composed once and read many times is a PREDICTION** — one surface over: composed
|
|
438
|
+
once, and read against every later version of the code it describes.
|
|
439
|
+
|
|
440
|
+
The reproduction is one shipped string. `SERVED_GAPS['no-receipt-on-chart'].why`
|
|
441
|
+
said *"THREE causes and none of them is a hole in this view: …"*. A fourth path
|
|
442
|
+
was then added — a value under the receipt key refused because it carries no
|
|
443
|
+
basis — and BOTH halves went false at once: four causes, and that one IS a hole.
|
|
444
|
+
Nobody edited the string. Nobody had to.
|
|
445
|
+
|
|
446
|
+
**THE RULE, AND THEN THE RULE THAT REPLACED IT.** The fifth round allowed a
|
|
447
|
+
gap sentence three things: which fields it covers, what MECHANICALLY could not
|
|
448
|
+
be established, and what therefore follows. The sixth round deleted the middle
|
|
449
|
+
one, and the reason is the whole story of this release.
|
|
450
|
+
|
|
451
|
+
Five rounds tried to write TRUE mechanism sentences and the rate of new
|
|
452
|
+
falsehoods held constant. So the sixth put one question to all ten printed
|
|
453
|
+
sentences — *could this become false without anyone editing it?* — and NINE
|
|
454
|
+
could, two of them being false the day they shipped. Exactly one could not:
|
|
455
|
+
|
|
456
|
+
> `UNGAPPED_FIELDS.gaps` — *"The account itself rather than a fact about the
|
|
457
|
+
> request: a gap naming this list would be the account excusing its own
|
|
458
|
+
> absence."*
|
|
459
|
+
|
|
460
|
+
It survives because it makes **no claim about code**. It says what the field
|
|
461
|
+
means inside the account, and nothing outside the sentence can falsify it. Every
|
|
462
|
+
other sentence described a MECHANISM — "the fold could not read", "only its
|
|
463
|
+
inputs are on the record", "the request-only lines are recomposed from the
|
|
464
|
+
conversation" — and a mechanism is code, and code moves. The conclusion is not
|
|
465
|
+
to write them better. It is to STOP WRITING THEM.
|
|
466
|
+
|
|
467
|
+
**A printed gap sentence may now say only three things: WHICH FIELDS it covers,
|
|
468
|
+
WHAT THEY MEAN ON THIS VIEW for the person reading, and WHAT TO DO
|
|
469
|
+
DIFFERENTLY.** It may not name a module, a function, a key, a version, a chart,
|
|
470
|
+
a strategy, an option, or any mechanism at all — not `initialState`, not "the
|
|
471
|
+
fold", not "the cache strategy", not "recomposed from". A sentence that needs
|
|
472
|
+
one of those words to be understood is explaining WHY the gap exists, which is
|
|
473
|
+
not the printed sentence's job. NINE of the ten got shorter — the tenth is the
|
|
474
|
+
one that already had the shape, and it is unchanged to the byte.
|
|
475
|
+
|
|
476
|
+
**None of it is lost.** The mechanism moved into the code comment above each
|
|
477
|
+
catalogue entry, phrased for a maintainer and carrying the `file · symbol`
|
|
478
|
+
pointers that are correct there and banned in printed prose. The CAUSE is
|
|
479
|
+
already data (`ServedGap.cause`). The docs still explain the mechanism at
|
|
480
|
+
length, because a doc is versioned with the code and its reader can open the
|
|
481
|
+
file — so `src/lib/time-travel/README.md` and the docs-site page are now
|
|
482
|
+
deliberately LONGER than what a renderer prints, and both say so.
|
|
483
|
+
|
|
484
|
+
**WHAT IT RULES OUT — AND THE CLAIM A SEVENTH ROUND OVERTURNED.** The sixth
|
|
485
|
+
round shipped this paragraph saying the reduction had made the prose
|
|
486
|
+
UNROTTABLE: a sentence with no code claim in it cannot go false when the code
|
|
487
|
+
changes, so the class is closed outright rather than merely thinned. A verifier
|
|
488
|
+
then read all eleven printed sentences one at a time, and that is false. **TEN
|
|
489
|
+
of them still make a claim a code edit falsifies.** Exactly one does not —
|
|
490
|
+
`UNGAPPED_FIELDS.gaps` — and it does not because it is SELF-REFERENTIAL: it says
|
|
491
|
+
what its field is inside the account, not anything about the request. The other
|
|
492
|
+
ten cannot copy that shape, because a sentence that tells a reader something
|
|
493
|
+
USEFUL — *may be SHORT*, *absent means unknown*, *the tool list is complete and
|
|
494
|
+
the schemas are one short* — is a claim about how the rebuild behaves, and the
|
|
495
|
+
rebuild is code. **The reduction changed the VOCABULARY of the claims, not their
|
|
496
|
+
CLASS.**
|
|
497
|
+
|
|
498
|
+
What the reduction really buys is smaller and still worth the rows: the
|
|
499
|
+
sentences are short and readable, and the enumerations that went false in five
|
|
500
|
+
rounds have nowhere to come back through. It also makes the rule enforceable.
|
|
501
|
+
The fifth round's checker was PHRASING-shaped and near-synonyms walked through
|
|
502
|
+
it; the sixth round's printed surface admits no code-shaped token — dotted path,
|
|
503
|
+
`.ts` file, camelCase, PascalCase, `SCREAMING_SNAKE`, a call with parens, a
|
|
504
|
+
quoted option name, a version number — and no mechanism verb from a closed list
|
|
505
|
+
of ten. That is close to a whitelist, and a whitelist has no synonyms.
|
|
506
|
+
|
|
507
|
+
**WHAT ACTUALLY CLOSES THE CLASS IS A RUN, and the evidence is in the same
|
|
508
|
+
report that overturned the claim.** Driving one real view per gap, the verifier
|
|
509
|
+
recorded seven sentences HOLDING and four MISLEADING — and caught a BRAND-NEW
|
|
510
|
+
false sentence in the very round written to end false sentences. No rule caught
|
|
511
|
+
it. Measurement caught it. So a gap sentence MAY make a code claim, because a
|
|
512
|
+
sentence that makes none cannot inform, and **every claim it makes is now
|
|
513
|
+
ASSERTED against a real view in a test that sits beside it**:
|
|
514
|
+
`test/lib/time-travel/gap-sentences.test.ts` drives one run per catalogue entry,
|
|
515
|
+
decomposes each sentence into quoted clauses, pairs every clause with its own
|
|
516
|
+
assertion, and requires the clauses to PARTITION the sentence so no printed word
|
|
517
|
+
sits outside a checked claim. The prose rule stays and is no longer sold as the
|
|
518
|
+
thing that makes the sentences true.
|
|
519
|
+
|
|
520
|
+
**THE HONEST LIMIT**, in the new file's header and in the checker's: a claim
|
|
521
|
+
nobody wrote an assertion for. The partition guarantees each clause has a test;
|
|
522
|
+
it cannot guarantee the test is as strong as the clause. That is a smaller blind
|
|
523
|
+
spot than six rounds of rewriting produced, and it is the whole of it.
|
|
524
|
+
|
|
525
|
+
- **The rule is a checker, not a habit.** `test/helpers/gapProseClaims.ts` is
|
|
526
|
+
the reader-facing sibling of `modelFacingClaims.ts`: **six** banned shapes now
|
|
527
|
+
— the two REDUCTION rows the sixth round added (code shape, mechanism verb),
|
|
528
|
+
then the four PHRASING rows the fifth round wrote (cardinality, benignity,
|
|
529
|
+
discrimination, cross-module), kept as the second line of defence and
|
|
530
|
+
redundant on the printed surface by design. Each carries the reason an edit
|
|
531
|
+
elsewhere falsifies it, and a structurally-required exemption argument (the
|
|
532
|
+
same discriminated union, proven the same way in
|
|
533
|
+
`test/type-regressions/GapProseClaims.assignability.test.ts`). The three
|
|
534
|
+
strong rows stand down on `'prose-doc'`, because naming the mechanism is what
|
|
535
|
+
a doc is FOR; the other three do not, because a cause count goes stale in a
|
|
536
|
+
doc exactly as it does in a constant.
|
|
537
|
+
- **Every printed gap sentence is one or two sentences and names nothing.**
|
|
538
|
+
`no-receipt-on-chart` now reads in full: *"Nothing on this view has been
|
|
539
|
+
checked against what went out. Every field below is missing as a whole, and an
|
|
540
|
+
absence among them says nothing about the call — not even that a dial was left
|
|
541
|
+
unset."* `forced-tool-schema` is 26 words. `callRuntimeStageId`'s excuse is 12.
|
|
542
|
+
- **TWO SENTENCES WERE FALSE THE DAY THEY SHIPPED, and both were mechanism
|
|
543
|
+
claims, so the rule deletes the category rather than the instances.**
|
|
544
|
+
`cache-transform` said *"only its INPUTS are on the record"* while three of
|
|
545
|
+
the fields it covers are OUTPUTS that are on the record — `cache.transform`
|
|
546
|
+
(the verdict of comparing what the strategy was given against what it handed
|
|
547
|
+
back), `cache.transformHash` (the digest of the result, when they differed)
|
|
548
|
+
and `cache.markersApplied` (the breakpoints actually applied, as against the
|
|
549
|
+
candidates in `scope.cacheMarkers`, which are the inputs). And
|
|
550
|
+
`no-receipt-on-chart` opened *"No receipt was found for this epoch"* and
|
|
551
|
+
closed *"absent here means unrecorded"* — both false under
|
|
552
|
+
`'receipt-shape-rejected'`, where a receipt WAS written and the read refused
|
|
553
|
+
it, which is to say the sentence asserted which cause applied and was wrong
|
|
554
|
+
for one of two. Neither can be written under the new rule. Both are corrected
|
|
555
|
+
in the two doc tables and in the comment beside each entry, where the
|
|
556
|
+
mechanism now lives, rather than merely dropped.
|
|
557
|
+
- **`RECEIPT_BOUNDARY` was the one printed sentence exempted from the rule, and
|
|
558
|
+
the exemption is gone.** It named `LLMProvider.complete` and `complete()` — a
|
|
559
|
+
module and a call, printed to a reader who cannot open either. It now reads
|
|
560
|
+
*"A receipt describes the request as this library last saw it. Whatever
|
|
561
|
+
handled it after that could have changed it, and nothing on the receipt would
|
|
562
|
+
show that."* The port, the decorated provider, the vendor adapter and the
|
|
563
|
+
vendor's own defaults are in the comment above the constant. One string, one
|
|
564
|
+
rule set: the walk no longer strips it before judging, and asserts it on its
|
|
565
|
+
own as well as inside the two entries that quote it.
|
|
566
|
+
- **THE CAUSE IS A VALUE.** The enumeration was prose doing DATA'S job: a frozen
|
|
567
|
+
constant cannot know which cause applied at the site it is printed beside, so
|
|
568
|
+
it listed them all and hoped. `ServedGap.cause` carries the answer now,
|
|
569
|
+
computed in the one function that has it — the receipt read reports whether
|
|
570
|
+
the key held nothing (`'no-receipt-committed'`) or held something it refused
|
|
571
|
+
(`'receipt-shape-rejected'`). Same "one fact, one owner" move that fixed the
|
|
572
|
+
`read_skill` refusals in 9.86. One test per cause: the first on a real
|
|
573
|
+
`LLMCall` run, the second on a crafted recording whose receipt key holds a
|
|
574
|
+
value with no basis, since no run produces one. The set is closed AT THE SITE
|
|
575
|
+
and deliberately no wider — a pre-9.88 recording, `recordReceipt: false` and a
|
|
576
|
+
chart that mints none all leave the same record, and claiming to separate them
|
|
577
|
+
would be this field repeating the defect it was added to fix.
|
|
578
|
+
- **`ServedView.basis`'s docstring claimed the gap discriminates.** It said the
|
|
579
|
+
gap "says which of the three it was", which it never could. It points at
|
|
580
|
+
`cause` now.
|
|
581
|
+
- **`receiptAt`'s docstring merged two causes, omitted a third, and denied a
|
|
582
|
+
fourth.** It said `undefined` has three causes, gave `recordReceipt: false` as
|
|
583
|
+
a gloss on "this chart mints none" (two different causes), left out the
|
|
584
|
+
malformed-receipt refusal, and asserted that none of them is "the epoch is
|
|
585
|
+
missing" — which is false, because `epochAt` returning nothing is exactly one
|
|
586
|
+
of the ways it returns `undefined`. It now gives the same account
|
|
587
|
+
`ServedGapCause` gives, so the two exported accounts cannot disagree, and says
|
|
588
|
+
plainly that a missing epoch is a separate answer.
|
|
589
|
+
- **`UNGAPPED_FIELDS.omittedForAttention` said "no chart supplies it".** True of
|
|
590
|
+
this library and not of the world: `buildReceipt` is a pure exported mint, so
|
|
591
|
+
a consumer can hand it the fact. Narrowed to **no chart in this library** —
|
|
592
|
+
and then, in the sixth round, moved out of the printed sentence altogether,
|
|
593
|
+
because a dated measurement over a set of charts is a claim about code. The
|
|
594
|
+
measurement is unchanged and still re-taken by the walk on every run; it reads
|
|
595
|
+
the claim from the COMMENT beside the entry now, so a chart that starts
|
|
596
|
+
supplying one still fails the suite instead of aging the sentence. What a
|
|
597
|
+
reader is shown is what the field means: *"Absent means nobody recorded a
|
|
598
|
+
drop, never that nothing was dropped."*
|
|
599
|
+
- **`examples/observability/24-receipt-at-the-stop.ts` clipped a printed reason
|
|
600
|
+
mid-version-number.** It printed `reason.split('.')[0]`, so "measured on
|
|
601
|
+
9.88.0" reached a reader as "measured on 9." — a fragment that reads as a
|
|
602
|
+
complete sentence. It wraps on word boundaries now, and prints the gap's
|
|
603
|
+
`cause` beside its kind.
|
|
604
|
+
- **The walk's divergence half now STATES ITS MEASUREMENT instead of implying
|
|
605
|
+
coverage.** The header claimed the vacuous-row guard was closed "by driving
|
|
606
|
+
each row on a run that reaches its own fields". Measured, that clause is
|
|
607
|
+
false: `no-fold-base` moves 7 of the 11 fields it names, `no-run-log` 1 of 4,
|
|
608
|
+
`no-receipt-on-chart` 3 of 8, and only `no-conversation-on-record` reaches all
|
|
609
|
+
3. The reasons are structural — a receipt-only field never appears on a view,
|
|
610
|
+
so removing the receipt cannot MOVE it — so the header names the numbers per
|
|
611
|
+
row, a test pins them so the table cannot go stale, and it says plainly that a
|
|
612
|
+
field claim divergence does not reach is carried by the coverage half and by
|
|
613
|
+
`receipt-conformance.test.ts`. No new machinery was built for this; the honest
|
|
614
|
+
fix was to say what was measured.
|
|
615
|
+
- **The gap tables in `src/lib/time-travel/README.md` and
|
|
616
|
+
`docs-next/content/docs/debug/time-travel.mdx` were short AND over-broad on the
|
|
617
|
+
day they shipped** — `no-run-log` missing `tools.schemaHashes`,
|
|
618
|
+
`cache-transform` claiming all of `tools.*` when it names two of the four
|
|
619
|
+
fields under `tools`. That is this release's own defect one layer out, where
|
|
620
|
+
the walk was not looking. Both tables now carry the LITERAL field lists, and
|
|
621
|
+
the walk parses them: every row must match `Object.keys(SERVED_GAPS)`, every
|
|
622
|
+
field cell must match that entry's `fields` exactly, and no cell may name a
|
|
623
|
+
field the catalogue has moved to `UNGAPPED_FIELDS`. A doc that restates a
|
|
624
|
+
frozen exported constant is checked against it, not retyped.
|
|
625
|
+
|
|
626
|
+
#### The seventh round — every sentence is asserted against a real run
|
|
627
|
+
|
|
628
|
+
The rounds above all ended by READING the sentences. This one drove a view for
|
|
629
|
+
each of them, which is how the library closes everything else, and it found a
|
|
630
|
+
different class of defect: not prose that names a mechanism, but prose that is
|
|
631
|
+
plain, short, rule-abiding and UNTRUE OF THE VIEW IT IS PRINTED BESIDE.
|
|
632
|
+
|
|
633
|
+
- **`no-run-log` claimed a loss that a run says did not happen.** It read *"The
|
|
634
|
+
fields below could not be fully recovered here"* — an assertion that recovery
|
|
635
|
+
DID fail. Measured on the ordinary view that raises it, a
|
|
636
|
+
`'dynamic-grouped'` agent with one plain tool and its `commitLog` emptied:
|
|
637
|
+
`tools.names`, `tools.schemas`, `tools.forced` and `messages.requestOnly` all
|
|
638
|
+
come back BYTE-IDENTICAL to the intact view. The gap fires and costs nothing,
|
|
639
|
+
because that run has no forced tool name and no `wants` to lose. It now reads
|
|
640
|
+
*"The fields below may be SHORT: a name can be missing from the tool list, and
|
|
641
|
+
a line that went out with the request can be missing too. An absence below is
|
|
642
|
+
not evidence that there was nothing there — read the whole recording rather
|
|
643
|
+
than a piece of it."* **The repair is the sentence and not the condition**,
|
|
644
|
+
and the reason is that the condition cannot be narrowed by anything the read
|
|
645
|
+
can see: whether the run had a constant to lose is recorded in the log whose
|
|
646
|
+
absence raises the gap. Both directions are asserted — emptying the run log
|
|
647
|
+
takes a forced-output run's tool list from one name to none and a staged-refs
|
|
648
|
+
run's request-only line to nothing, and takes nothing at all from the plain
|
|
649
|
+
run. Its second clause was loose as well: it said a line could go missing
|
|
650
|
+
*"from the conversation"*, and a request-only line is by construction in no
|
|
651
|
+
conversation — asserted now against the wire, which carries it as the last
|
|
652
|
+
message of the request while the rebuilt `messages.asSent` does not contain it.
|
|
653
|
+
- **Three sentences were true where they were composed and misleading where they
|
|
654
|
+
were PRINTED.** One sentence, several contexts: the library's own Honest
|
|
655
|
+
Sentence law says it has to hold in all of them.
|
|
656
|
+
- `cache-transform` ended by quoting `RECEIPT_BOUNDARY` — *"A receipt
|
|
657
|
+
describes the request as this library last saw it"* — and it is raised on
|
|
658
|
+
EVERY view, including a receipt-less one. Measured: an `LLMCall` view
|
|
659
|
+
carries exactly `no-receipt-on-chart` and `cache-transform`, so the reader
|
|
660
|
+
was told what a receipt describes beside a view that has none. The quote is
|
|
661
|
+
gone from it and the claim survives in the entry's own words, in the
|
|
662
|
+
vocabulary of a view: *"…and nothing on this view would show it."*
|
|
663
|
+
`provider-defaults` keeps the quote and is the only entry that may have it —
|
|
664
|
+
it is pushed inside `if (receipt !== undefined)`, so a view carrying it
|
|
665
|
+
always has a receipt for the sentence to be about. Asserted across every run
|
|
666
|
+
in the new file: a gap whose `why` includes the boundary appears only on a
|
|
667
|
+
view whose `basis` is defined.
|
|
668
|
+
- `no-fold-base` said the view's number *"may differ from the one the receipt
|
|
669
|
+
for this turn carries"* — printed on views that carry no receipt (a
|
|
670
|
+
base-less `LLMCall` recording raises both gaps at once). It now says what
|
|
671
|
+
the NUMBER means: *"The turn number below may be this turn's place in run
|
|
672
|
+
order rather than the number the run itself gave it."* Asserted on the
|
|
673
|
+
resumed run whose base and `iteration` writes are gone: the view calls the
|
|
674
|
+
second turn 1 while the run's own count for it was 2.
|
|
675
|
+
- `no-receipt-on-chart` closed *"their absence here is a gap in the record,
|
|
676
|
+
never a call made without them"*. True of each field AS A WHOLE and false
|
|
677
|
+
one level down, which is the level a reader reads at: a receipt always
|
|
678
|
+
carries `params` and always carries a `cache.transform` verdict, and an
|
|
679
|
+
absence INSIDE `params` — measured `{}` on an agent that set no dials — IS a
|
|
680
|
+
call made without one. It now claims nothing about what is inside a field it
|
|
681
|
+
cannot see: *"…an absence among them says nothing about the call — not even
|
|
682
|
+
that a dial was left unset."*
|
|
683
|
+
|
|
684
|
+
- **`test/lib/time-travel/gap-sentences.test.ts` — the assertion, beside the
|
|
685
|
+
sentence.** One real run per catalogue entry and per `UNGAPPED_FIELDS` key,
|
|
686
|
+
and an assertion for each claim the sentence makes — not that the gap fired,
|
|
687
|
+
but that what it says about the view HOLDS. Each sentence is decomposed into
|
|
688
|
+
clauses quoted verbatim from the constant, each clause carries its own
|
|
689
|
+
assertion, and three contract tests hold the binding shut: every catalogue key
|
|
690
|
+
is an entry, every quote is verbatim and in order, and **the clauses PARTITION
|
|
691
|
+
the sentence** — strike them out and only punctuation is left, so no printed
|
|
692
|
+
word sits outside a checked claim. Rewrite a sentence and the partition fails,
|
|
693
|
+
which sends the author back to write the assertion for what it now claims. The
|
|
694
|
+
measurements the round took by hand are transcribed into it rather than
|
|
695
|
+
re-derived, and the rest were driven to fill the gaps between them.
|
|
696
|
+
- **`UNGAPPED_FIELDS.gaps` is labelled as the one clause class that is not a
|
|
697
|
+
claim about the request.** It is the only sentence no code edit can falsify,
|
|
698
|
+
and it is that way because it is self-referential — a statement about what its
|
|
699
|
+
field is inside the account. The new file carries a flag naming that category
|
|
700
|
+
rather than an assertion pretending to cover it, and a contract test requires
|
|
701
|
+
exactly that one entry to be flagged.
|
|
702
|
+
|
|
703
|
+
### Changed
|
|
704
|
+
|
|
705
|
+
### The receipt's three laws
|
|
706
|
+
|
|
707
|
+
1. **Hashes and references, never bytes.** No message text, no prompt text, no
|
|
708
|
+
schema bodies. Those bytes are already governed — `recordSystemPrompt` is
|
|
709
|
+
opt-in for exactly this reason, redaction scrubs the committed mirror, a
|
|
710
|
+
window strategy decides what survives — and a receipt carrying content would
|
|
711
|
+
quietly reopen all three.
|
|
712
|
+
2. **Run-salted digests.** `sha256(runId + '\u001f' + content)`, first 16 hex
|
|
713
|
+
characters. Hashes are NOT redacted, and the salt is why that is safe: an
|
|
714
|
+
unsalted hash of a one-line prompt or a two-word user turn is a dictionary
|
|
715
|
+
lookup away from being read back, and receipts travel inside recordings.
|
|
716
|
+
3. **No authority omissions.** A receipt never names — and never counts — what a
|
|
717
|
+
caller's role was not allowed to see. Committed state is readable by the
|
|
718
|
+
trace toolpack's debugging tools, so a receipt carrying `hiddenSkillIds`, or
|
|
719
|
+
even "3 skills withheld", would restate a permission decision one layer down
|
|
720
|
+
where nobody is checking.
|
|
721
|
+
|
|
722
|
+
### Measured
|
|
723
|
+
|
|
724
|
+
- `test/lib/time-travel/receipt-conformance.test.ts` — 51 tests over real runs:
|
|
725
|
+
both chart shapes, a pause and a resume in both, a skill-graph hop, a stepped
|
|
726
|
+
skill, a parked map, a wrap-up call, a forced output tool, a staged-refs
|
|
727
|
+
nudge, a cache strategy that rewrites the composition and one that rewrites a
|
|
728
|
+
sampling dial, a marker-applying one, a provider decorated past the port, an
|
|
729
|
+
`LLMCall` chart, a JSON round-trip, a pre-9.88 recording, a recording with no
|
|
730
|
+
fold base on either of its two folds, a recording whose base AND whose
|
|
731
|
+
`iteration` writes are gone (where the view numbers the turn by position and
|
|
732
|
+
its own receipt still says otherwise), a subtree with no run log, a reader
|
|
733
|
+
that tries to edit what it was handed, and the off switch. The vendored
|
|
734
|
+
SHA-256 is checked against `node:crypto` on every shape it hashes. Two
|
|
735
|
+
mutation tests drop a committed piece from the replay and require the law to
|
|
736
|
+
go red naming the epoch and the field.
|
|
737
|
+
- `test/lib/time-travel/gap-catalogue-walk.test.ts` — 44 tests, and the reason
|
|
738
|
+
this release has one more file than it planned. `SERVED_GAPS` was hand-checked
|
|
739
|
+
against the two shapes three times and came up short three times, which is not
|
|
740
|
+
a run of bad luck: A HAND-COUNTED LIST IS SHORT THE DAY AFTER. So the
|
|
741
|
+
correspondence is WALKED, the way `userTurnProducers.test.ts` walks every
|
|
742
|
+
`role:'user'` producer and `toolDivergenceWalk.test.ts` crosses every
|
|
743
|
+
claimant. It derives the field list twice — from the declarations, with the
|
|
744
|
+
TypeScript parser, and from five real runs — and requires every field to be
|
|
745
|
+
named by a gap or to be a key of `UNGAPPED_FIELDS` with a written reason;
|
|
746
|
+
requires every `fields` entry and every `UNGAPPED_FIELDS` key to resolve to a
|
|
747
|
+
field that exists, so a rename cannot leave a gap pointing at nothing; and
|
|
748
|
+
requires the kinds `viewOf` can push and the kinds in the catalogue to be the
|
|
749
|
+
same set. Then it DAMAGES a real recording the way each gap describes,
|
|
750
|
+
rebuilds, and requires every field that MOVES to be named by that gap — which
|
|
751
|
+
is the half that catches a field named by the wrong gap for the wrong reason,
|
|
752
|
+
and the half the first three checks could not. Verified by reverting each fix
|
|
753
|
+
and watching the right row go red.
|
|
754
|
+
|
|
755
|
+
**The two halves, and what each cannot prove.** The ACCOUNT half (coverage,
|
|
756
|
+
resolution, reachability, and the two doc tables) proves every field of both
|
|
757
|
+
shapes is named by a gap or excused in writing, that every pointer lands on a
|
|
758
|
+
field that exists, and that the prose copies say what the constant says. It
|
|
759
|
+
cannot prove any of it is TRUE: a gap can name a field for a mechanism that
|
|
760
|
+
does not cause its absence, and this release shipped two of those. The
|
|
761
|
+
DIVERGENCE half proves that for the four damages it can apply, no field moves
|
|
762
|
+
without the responsible gap naming it — and, since this round, that each
|
|
763
|
+
damage moves SOMETHING, so a row cannot pass by damaging nothing. It cannot
|
|
764
|
+
prove a row moved everything the gap is about, it has no damage at all for the
|
|
765
|
+
three gaps that are conditions of the RUN rather than of the recording
|
|
766
|
+
(`cache-transform`, `provider-defaults`, `forced-tool-schema` —
|
|
767
|
+
`receipt-conformance.test.ts` drives those), and the damage table is
|
|
768
|
+
hand-listed, so a gap whose damage nobody wrote down still gets only the
|
|
769
|
+
account half. Neither half reads a `why`. That is a person's job, and it is
|
|
770
|
+
where this release's last three defects came from.
|
|
771
|
+
- `test/lib/time-travel/keyed-fold-equivalence.test.ts` — 9 tests: every key at
|
|
772
|
+
every commit of a real run, checked against `stateAt` itself, including a
|
|
773
|
+
merge with no `set` anchor (which cannot be folded at all without the base),
|
|
774
|
+
a resumed agent in both chart shapes, and a replayed value that comes back
|
|
775
|
+
frozen.
|
|
776
|
+
- **The detachment freeze costs 23%, and the alternative costs more.** Measured
|
|
777
|
+
on a 601-epoch run (10,219 commits): the whole per-epoch scrub is 1,141 ms
|
|
778
|
+
with the freeze and 930 ms without, against 20.8 s two fixes ago; the batch
|
|
779
|
+
form moves 919 ms to 1,116 ms. Copying instead is dearer, not cheaper — on
|
|
780
|
+
the same 1,200-message structure `structuredClone` costs 0.94 ms against
|
|
781
|
+
`freezeDeep`'s 0.26 ms, and a copy would run once per READ where the freeze
|
|
782
|
+
runs once per memoized answer.
|
|
783
|
+
- `test/lib/time-travel/served-view-complexity.test.ts` — 2 tests: epochs are
|
|
784
|
+
located once per recording (checked by identity, no clock), and the per-epoch
|
|
785
|
+
scrub stays within 2x the batch form at both 13 and 49 epochs without
|
|
786
|
+
drifting. Against the shape this release replaced those ratios are 2.4x and
|
|
787
|
+
3.7x, climbing to 5.0x at 97 epochs.
|
|
788
|
+
- Full suite: 10,348 passing, 20 skipped, 624 files.
|
|
789
|
+
- `test/lib/time-travel/served-view-complexity.test.ts` is a RATIO guard, and a
|
|
790
|
+
ratio is immune to the machine but not to contention: it failed once inside a
|
|
791
|
+
fully parallel `vitest run` and passes alone and in a clean full run. The
|
|
792
|
+
freeze does not touch its subject — the scrub and the batch form do the same
|
|
793
|
+
folding in the same order, so both pay it identically (measured: scrub/batch
|
|
794
|
+
is 1.02 with the freeze and 1.01 without). Recorded rather than widened.
|
|
795
|
+
- Runnable: `examples/observability/24-receipt-at-the-stop.ts` — three turns, the
|
|
796
|
+
law checked against the rebuild AND against the request the provider really
|
|
797
|
+
received, with the staged-refs nudge printed back from the record, the
|
|
798
|
+
sampling dials, and the port boundary printed where a reader meets it.
|
|
799
|
+
|
|
800
|
+
### A stated limit
|
|
801
|
+
|
|
802
|
+
The `@wire` clauses of the conformance test are not an INDEPENDENT witness.
|
|
803
|
+
They compare the receipt against the request the provider stub really received,
|
|
804
|
+
which catches a rebuild that drifts from the request and a receipt that
|
|
805
|
+
describes something the provider never got. It does not catch a defect in the
|
|
806
|
+
shared assembly: the receipt and the request are minted from the same locals
|
|
807
|
+
inside `callLLM`, a few lines apart, so a change that alters both symmetrically
|
|
808
|
+
leaves every `@wire` clause green. A genuinely independent witness would have to
|
|
809
|
+
come from outside the process that composed the request — a recorded HTTP body
|
|
810
|
+
from a real adapter, or a second implementation written against the vendor's own
|
|
811
|
+
schema. Neither exists; the limit is stated in the test's own header rather than
|
|
812
|
+
left to be discovered.
|
|
813
|
+
|
|
814
|
+
Typechecking the whole of `test/` would have closed the redaction hole at its
|
|
815
|
+
root. It was tried and surfaces 1,054 pre-existing errors across the suite and
|
|
816
|
+
the examples it pulls in — a repair of its own, not a line item in this release.
|
|
817
|
+
The hole is closed instead in `test/type-regressions/`, which already compiles
|
|
818
|
+
under `npm run test:types`.
|
|
819
|
+
|
|
820
|
+
`ServedGap.fields` carries TWO relations on one list. Most entries mean *the
|
|
821
|
+
rebuild cannot produce this field*; `cache-transform`'s composition fields mean
|
|
822
|
+
*it can, and both sides agree, but only up to the cache strategy*. A checker
|
|
823
|
+
that granted the second as an excuse would stop checking fields the record
|
|
824
|
+
proves perfectly well — which is exactly what happened when the composition
|
|
825
|
+
fields were added, and it silently disabled one clause of the law. The clause
|
|
826
|
+
now asks its narrower question against its own list of gaps, and the field's
|
|
827
|
+
docstring names both relations. A second array on the public type is the
|
|
828
|
+
cleaner shape; adding a public field in a fix pass is not, so it is written
|
|
829
|
+
down rather than shipped.
|
|
830
|
+
|
|
831
|
+
**A green `gap-catalogue-walk` proves the fields are ACCOUNTED FOR, not that the
|
|
832
|
+
account is TRUE.** A gap can name a field for the wrong reason and the walk will
|
|
833
|
+
call it covered; a `why` can be a fluent sentence about the wrong mechanism; an
|
|
834
|
+
`UNGAPPED_FIELDS` reason can be wishful. Only a person reading the `why` catches
|
|
835
|
+
that — which is how the defect the walk was built for was found in the first
|
|
836
|
+
place — three times in this release, the last of them after the walk was
|
|
837
|
+
already green. Five narrower blind spots are named in the file's own header
|
|
838
|
+
rather than left to be discovered: the reachability half reads `gapOf('…')`
|
|
839
|
+
literals out of the source (and so fails on any `gapOf` call whose argument is
|
|
840
|
+
not a literal, which is the only shape that could hide one); the static half
|
|
841
|
+
follows type references by name and does not expand an alias, a mapped type or
|
|
842
|
+
an intersection; the runtime half only produces what its five scenarios reach;
|
|
843
|
+
the DAMAGES table is hand-listed, so a gap whose damage nobody wrote down gets
|
|
844
|
+
the coverage check and not the divergence one; and A DAMAGE ROW THAT DAMAGES
|
|
845
|
+
NOTHING passed as a green row for one release, which is why the loop now
|
|
846
|
+
requires each damage to move something and still says that "something" is not
|
|
847
|
+
"everything". Three gaps have no damage at all,
|
|
848
|
+
because they are conditions of the RUN rather than of the recording —
|
|
849
|
+
`cache-transform`, `provider-defaults`, `forced-tool-schema` — and
|
|
850
|
+
`receipt-conformance.test.ts` drives each of those on a real run instead.
|
|
851
|
+
|
|
852
|
+
**A recording stops being JSON-serializable at about 600 epochs**, and it is the
|
|
853
|
+
same reader at the same size that the epoch memo was built for. Measured on the
|
|
854
|
+
looping run the complexity guard uses: 301 epochs serialize to 145,238,645
|
|
855
|
+
characters; 601 epochs throw `RangeError: Invalid string length` — V8's maximum
|
|
856
|
+
string length, not a library limit. Nothing here fails before then, and none of
|
|
857
|
+
these readers needs `JSON.stringify` to work: `servedAt`, `receiptAt` and
|
|
858
|
+
`epochLocations` all read the live object. But a recording that cannot be
|
|
859
|
+
written to a file cannot be handed to anybody, so at that size the answer is to
|
|
860
|
+
persist per-epoch views rather than the whole snapshot. Stated rather than
|
|
861
|
+
worked around, beside the memo it shares a size with.
|
|
862
|
+
|
|
863
|
+
`flowchartAsTool({ redact })`'s kept inner recording carries an UNREDACTED
|
|
864
|
+
`sharedState` while the commit log is scrubbed. It is a different subsystem and
|
|
865
|
+
every available fix changes behaviour for runs that work today, so this release
|
|
866
|
+
corrects the option's own claim and records the defect — reproduction, cause
|
|
867
|
+
and what each fix would cost — as entry 6 of
|
|
868
|
+
`docs/design/2026-09-recorded-not-built.md`.
|
|
869
|
+
|
|
870
|
+
**Three more are RECORDED rather than built**, as entries 7-9 of the same file,
|
|
871
|
+
because the last review round's job was to make every printed sentence true
|
|
872
|
+
rather than to close every hole: `cache-transform` does not name `tools.forced`
|
|
873
|
+
or `tools.withheld` although a strategy that rewrote `toolChoice` could make
|
|
874
|
+
both stale (the receipt builds them from assembly's decision, not from the
|
|
875
|
+
prepared request); `cache-transform` stays unconditional, because raising it
|
|
876
|
+
conditionally would mean inferring "no strategy ran" from a recording; and a
|
|
877
|
+
slot's attention drops never reach `Receipt.omittedForAttention`, because no
|
|
878
|
+
boundary bubbles `slotCompositions` out of the slot subflow. Each entry carries
|
|
879
|
+
its reproduction, its cause, and what a fix would cost.
|
|
880
|
+
|
|
881
|
+
## [9.87.1] - 2026-09-06
|
|
882
|
+
|
|
883
|
+
9.87.0 never reached npm. Its publish job failed the docs site's byte budget —
|
|
884
|
+
`export bytes: 621.21 MB exceeds 617.00 MB` — after six new routes (the
|
|
885
|
+
time-travel guide and the API pages for `milestoneStops`,
|
|
886
|
+
`milestoneStopsStrategy`, `milestoneOf`) added 5.47 MB to a static export
|
|
887
|
+
that had 1.26 MB of headroom left. No library code changes in this release.
|
|
888
|
+
|
|
889
|
+
### Fixed
|
|
890
|
+
|
|
891
|
+
- **The site-budget ceilings are re-baselined** the way the script's own history
|
|
892
|
+
says to: ~2% over the measured export (634 MB, 6,750 files), with the reason
|
|
893
|
+
written beside the number in `docs-next/scripts/check-site-budget.mjs`. The
|
|
894
|
+
gate did its job — growth nobody was watching is exactly what it exists to
|
|
895
|
+
stop — and the number to look at next is the ~0.9 MB a single docs route
|
|
896
|
+
costs, not the ceiling.
|
|
897
|
+
|
|
898
|
+
## [9.87.0] - 2026-09-06
|
|
899
|
+
|
|
900
|
+
The agent now supplies the stops for its own runs.
|
|
901
|
+
|
|
902
|
+
footprintjs 9.17.0 opens a reader's cursor over a finished commit log —
|
|
903
|
+
`timeTravel(snapshot, { strategy })` — with a fold at every stop, and a seam that
|
|
904
|
+
says where the cursor may rest. It ships one strategy, `commitStops`, which stops
|
|
905
|
+
on every executed stage: the truth, and unreadable. A two-turn agent in
|
|
906
|
+
`reactMode: 'dynamic'` commits **42** bundles, and most of them are called
|
|
907
|
+
`context`, `sf-cache` or `sf-thinking`.
|
|
908
|
+
|
|
909
|
+
`milestoneFor` has classified stage ids into `iteration` · `slot` · `llm-turn` ·
|
|
910
|
+
`tool-call` · `decision` for releases, and every consumer that wanted a milestone
|
|
911
|
+
slider mapped that classifier onto commits itself. This release does that join
|
|
912
|
+
once, on the seam: the same 42-commit run yields **15** stops — Run start →
|
|
913
|
+
Iteration → System prompt → Messages → Tools → LLM turn → Route → Tool call →
|
|
914
|
+
Iteration → … → Run end.
|
|
915
|
+
|
|
916
|
+
### Added
|
|
917
|
+
|
|
918
|
+
- **`milestoneStops` / `milestoneStopsStrategy` / `milestoneOf`** (`src/lib/time-travel/`,
|
|
919
|
+
exported from the root barrel). A footprintjs `TimeTravelStrategy` built by
|
|
920
|
+
FILTERING footprintjs's own per-stage axis rather than re-deriving it: it calls
|
|
921
|
+
`commitStops` and keeps the stages `milestoneFor` classifies, so the collapsing
|
|
922
|
+
that axis already solved — one stop per `runtimeStageId` at its first commit
|
|
923
|
+
(mounts and fork children commit more than once), the mount set read off the
|
|
924
|
+
execution tree, the `'start'` / `'end'` bookends, the id-less commit that
|
|
925
|
+
carries a subflow's `inputMapper` seed — is used, not repeated. A stage that
|
|
926
|
+
classifies `null` contributes no stop and its commits fold into the stop before
|
|
927
|
+
it, so the survivors still partition the log end to end and `stateAt(stop)`
|
|
928
|
+
stays "the state that existed when the next milestone started".
|
|
929
|
+
|
|
930
|
+
- **Both chart shapes, one strategy.** The classifier reads the LOCAL segment of
|
|
931
|
+
a stage id, so nothing has to tell it which log it is holding. Under
|
|
932
|
+
`reactMode: 'dynamic'` the `call-llm` bundle is on the run's own log and the
|
|
933
|
+
llm-turn stop is on the outer cursor. Under `'dynamic-grouped'` the same run
|
|
934
|
+
commits **10** bundles outside — the `sf-llm-call` mounts, which become the
|
|
935
|
+
iteration stops — and `drill(mountRuntimeStageId)` opens that turn's own
|
|
936
|
+
cursor, where the same strategy finds **7** stops including its LLM turn.
|
|
937
|
+
|
|
938
|
+
- **`examples/observability/23-time-travel-milestones.ts`** — the same agent run
|
|
939
|
+
at both chart shapes, printing each axis, the skill the run stood in at each
|
|
940
|
+
turn, `changedSince` between two turns, a mark that survives a jump, and a
|
|
941
|
+
refused jump that leaves the cursor put. It also calls `milestoneStops` on the
|
|
942
|
+
log directly, for the reader who holds a recording rather than a cursor, and
|
|
943
|
+
shows the two axes agree. Offline, mock provider.
|
|
944
|
+
|
|
945
|
+
- **`docs-next/content/docs/debug/time-travel.mdx`** — why milestones rather than
|
|
946
|
+
one-stop-per-stage, the three questions the cursor answers, and where the LLM
|
|
947
|
+
turn lives in each chart shape.
|
|
948
|
+
|
|
949
|
+
### Changed
|
|
950
|
+
|
|
951
|
+
- **`footprintjs` is now `^9.17.0`** (peer + dev), for `timeTravel`,
|
|
952
|
+
`commitStops` and the `TimeTravelStrategy` seam.
|
|
953
|
+
|
|
954
|
+
### Honest notes
|
|
955
|
+
|
|
956
|
+
- **The milestone kind is a function, not a field.** footprintjs's `Stop` is a
|
|
957
|
+
closed shape with no slot a strategy may write its own vocabulary into, and its
|
|
958
|
+
`kind` is the port's own `StopKind` (`'commit' | 'mount' | 'start' | 'end'`),
|
|
959
|
+
not ours to overload. So the kind travels the only way it honestly can:
|
|
960
|
+
`milestoneOf(stop)` re-derives it from the stop's `runtimeStageId` with the
|
|
961
|
+
same classifier that put the stop on the axis — one source of truth, read
|
|
962
|
+
twice. If a later footprintjs gives `Stop` an extension slot, `milestoneOf`
|
|
963
|
+
becomes a one-line reader of it.
|
|
964
|
+
|
|
965
|
+
- **Which keys are visible where, in the grouped shape.** The settled skill
|
|
966
|
+
cursor for turn *k* is on the OUTER axis, at iteration *k* (`currentSkillId`).
|
|
967
|
+
Inside the drill, `currentSkillId` is the value the turn STARTED from — it
|
|
968
|
+
crosses the mount as a read-only input — and the move the turn made is
|
|
969
|
+
`nextSkillCursor`, merged back out by the outputMapper. Both logs are truthful
|
|
970
|
+
about different questions, and `test/lib/time-travel/milestone-stops.test.ts`
|
|
971
|
+
pins both rather than picking the flattering one.
|
|
972
|
+
|
|
973
|
+
- **`'start'` is not the fold base on this axis.** footprintjs's `'start'`
|
|
974
|
+
bookend is the state before any stage ran; this one also absorbs every stage
|
|
975
|
+
that ran before the FIRST milestone, because the stops must still partition
|
|
976
|
+
the log. Measured on a two-turn `dynamic` run: `commitStops`' start folds
|
|
977
|
+
commits `-1..-1` and 0 keys, `milestoneStops`' start folds `-1..0` and 32 —
|
|
978
|
+
`seed`'s writes have already landed. So `stateAt(startStop)` here is the state
|
|
979
|
+
the first milestone READ, not the run's raw base, and a renderer keyed on
|
|
980
|
+
`kind === 'start'` to show "what the run began with" is showing post-seed
|
|
981
|
+
state. Said in the folder README, on the docs page, and pinned by a test.
|
|
982
|
+
|
|
983
|
+
- **A resumed run gets an axis of its own.** The cursor reads the snapshot it is
|
|
984
|
+
handed, and a resume is its own execution with its own log: after
|
|
985
|
+
`agent.resume(checkpoint, answer)`, `getSnapshot()` carries the resumed half —
|
|
986
|
+
the axis begins at the stage the resume re-entered and the pre-pause
|
|
987
|
+
milestones are not on it. They are on the snapshot taken at the pause. The
|
|
988
|
+
strategy itself holds across the break (both axes tile, both keep unique
|
|
989
|
+
stops); it is the snapshot that split, not the cursor.
|
|
990
|
+
|
|
991
|
+
- **A log with no milestones is not an empty log.** A non-empty log the
|
|
992
|
+
classifier recognises nothing in — a non-agent chart handed this strategy —
|
|
993
|
+
yields the two bookends and nothing between them, and every `jumpTo` refuses
|
|
994
|
+
with `'miss'`. An EMPTY log yields `[]`. Two different facts, two different
|
|
995
|
+
answers.
|
|
996
|
+
|
|
997
|
+
- **The performance guards get a stated allowance under coverage.** v8
|
|
998
|
+
instrumentation is not machine load: it is a per-call counter, so it taxes the
|
|
999
|
+
two sides of a ratio in proportion to how many calls each makes, and a
|
|
1000
|
+
comparison between two differently-shaped paths moves even on an idle machine.
|
|
1001
|
+
Measured on this suite, the tightest guard sat ~2.8× under its ceiling
|
|
1002
|
+
uninstrumented and ~1.1× under it with `--coverage` — which is why it went red
|
|
1003
|
+
under whole-suite parallelism and green on a re-run. `test:coverage` now sets
|
|
1004
|
+
`AF_COVERAGE=1` and `test/helpers/perf.ts` widens the ceilings by a documented
|
|
1005
|
+
3× when it sees it. The plain `npm test` ceilings are unchanged, which is where
|
|
1006
|
+
a real regression is still caught; the multiplier is a floor on effort, not a
|
|
1007
|
+
promise that instrumentation costs exactly 3×.
|
|
1008
|
+
|
|
1009
|
+
- **17 tests over real runs**, not fixtures: the axis in both chart shapes, the
|
|
1010
|
+
partition property (every commit belongs to exactly one stop), a jump that
|
|
1011
|
+
lands and a miss that names a nearest without moving, the skill graph read
|
|
1012
|
+
along the commits with the wire as witness (the skill whose body rode turn *k*
|
|
1013
|
+
is the one that iteration's own log settled on), marks that survive jumps and
|
|
1014
|
+
never appear in the recording, what `'start'` folds on this axis versus the
|
|
1015
|
+
port's, a resumed run's axis, and a log with no milestones in it. One more
|
|
1016
|
+
file — 5 tests in `milestone-stops-contract.test.ts` — mocks `commitStops` to check the one
|
|
1017
|
+
assumption this strategy makes of footprintjs — that a non-empty log yields
|
|
1018
|
+
`[start, …stages, end]` — is refused loudly rather than silently mistaken for
|
|
1019
|
+
an empty axis.
|
|
1020
|
+
|
|
10
1021
|
## [9.86.1] - 2026-09-06
|
|
11
1022
|
|
|
12
1023
|
The release that removed hand-counted lists shipped with one, and with main red.
|