@namzu/sdk 56.1.0 → 57.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +1 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/index.d.ts +1 -0
  6. package/dist/agents/index.d.ts.map +1 -1
  7. package/dist/agents/index.js +1 -0
  8. package/dist/agents/index.js.map +1 -1
  9. package/dist/agents/plan.d.ts +20 -0
  10. package/dist/agents/plan.d.ts.map +1 -0
  11. package/dist/agents/plan.js +27 -0
  12. package/dist/agents/plan.js.map +1 -0
  13. package/dist/compaction/context-breakdown.d.ts +1 -1
  14. package/dist/compaction/context-breakdown.d.ts.map +1 -1
  15. package/dist/compaction/context-breakdown.js +9 -2
  16. package/dist/compaction/context-breakdown.js.map +1 -1
  17. package/dist/compaction/dangling.d.ts +12 -1
  18. package/dist/compaction/dangling.d.ts.map +1 -1
  19. package/dist/compaction/dangling.js +17 -3
  20. package/dist/compaction/dangling.js.map +1 -1
  21. package/dist/compaction/index.d.ts +2 -2
  22. package/dist/compaction/index.d.ts.map +1 -1
  23. package/dist/compaction/index.js.map +1 -1
  24. package/dist/compaction/manager.d.ts +16 -0
  25. package/dist/compaction/manager.d.ts.map +1 -1
  26. package/dist/compaction/manager.js +55 -0
  27. package/dist/compaction/manager.js.map +1 -1
  28. package/dist/compaction/manual.d.ts +2 -0
  29. package/dist/compaction/manual.d.ts.map +1 -1
  30. package/dist/compaction/manual.js +33 -4
  31. package/dist/compaction/manual.js.map +1 -1
  32. package/dist/compaction/restore.d.ts +51 -0
  33. package/dist/compaction/restore.d.ts.map +1 -0
  34. package/dist/compaction/restore.js +123 -0
  35. package/dist/compaction/restore.js.map +1 -0
  36. package/dist/compaction/types.d.ts +22 -0
  37. package/dist/compaction/types.d.ts.map +1 -1
  38. package/dist/compaction/verifier.d.ts.map +1 -1
  39. package/dist/compaction/verifier.js +3 -1
  40. package/dist/compaction/verifier.js.map +1 -1
  41. package/dist/compaction/wire.d.ts +3 -1
  42. package/dist/compaction/wire.d.ts.map +1 -1
  43. package/dist/compaction/wire.js +16 -0
  44. package/dist/compaction/wire.js.map +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/plugin/shell-hook.d.ts +19 -1
  48. package/dist/plugin/shell-hook.d.ts.map +1 -1
  49. package/dist/plugin/shell-hook.js +21 -5
  50. package/dist/plugin/shell-hook.js.map +1 -1
  51. package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
  52. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  53. package/dist/prompt/coding-agent-doctrine.js +3 -2
  54. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  55. package/dist/provider/errors.d.ts +3 -0
  56. package/dist/provider/errors.d.ts.map +1 -1
  57. package/dist/provider/errors.js +23 -3
  58. package/dist/provider/errors.js.map +1 -1
  59. package/dist/provider/fallback.d.ts.map +1 -1
  60. package/dist/provider/fallback.js +34 -3
  61. package/dist/provider/fallback.js.map +1 -1
  62. package/dist/provider/retry.d.ts +12 -5
  63. package/dist/provider/retry.d.ts.map +1 -1
  64. package/dist/provider/retry.js +22 -8
  65. package/dist/provider/retry.js.map +1 -1
  66. package/dist/public-runtime.d.ts +5 -3
  67. package/dist/public-runtime.d.ts.map +1 -1
  68. package/dist/public-runtime.js +5 -2
  69. package/dist/public-runtime.js.map +1 -1
  70. package/dist/public-types.d.ts +1 -1
  71. package/dist/public-types.d.ts.map +1 -1
  72. package/dist/runtime/jobs/registry.d.ts +42 -0
  73. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  74. package/dist/runtime/jobs/registry.js +77 -0
  75. package/dist/runtime/jobs/registry.js.map +1 -1
  76. package/dist/runtime/query/calls-that-never-started.d.ts +12 -0
  77. package/dist/runtime/query/calls-that-never-started.d.ts.map +1 -0
  78. package/dist/runtime/query/calls-that-never-started.js +41 -0
  79. package/dist/runtime/query/calls-that-never-started.js.map +1 -0
  80. package/dist/runtime/query/executor/tool-call-admission.d.ts +2 -2
  81. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  82. package/dist/runtime/query/executor/tool-call-admission.js +13 -9
  83. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  84. package/dist/runtime/query/executor.d.ts +16 -1
  85. package/dist/runtime/query/executor.d.ts.map +1 -1
  86. package/dist/runtime/query/executor.js +45 -2
  87. package/dist/runtime/query/executor.js.map +1 -1
  88. package/dist/runtime/query/file-evidence-context.d.ts +21 -4
  89. package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
  90. package/dist/runtime/query/file-evidence-context.js +70 -8
  91. package/dist/runtime/query/file-evidence-context.js.map +1 -1
  92. package/dist/runtime/query/index.d.ts +6 -0
  93. package/dist/runtime/query/index.d.ts.map +1 -1
  94. package/dist/runtime/query/index.js +7 -11
  95. package/dist/runtime/query/index.js.map +1 -1
  96. package/dist/runtime/query/iteration/index.d.ts +1 -2
  97. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  98. package/dist/runtime/query/iteration/index.js +30 -19
  99. package/dist/runtime/query/iteration/index.js.map +1 -1
  100. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  101. package/dist/runtime/query/iteration/phases/compaction.js +20 -0
  102. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  103. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  104. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  105. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  106. package/dist/runtime/query/iteration/recorded-context.d.ts +18 -0
  107. package/dist/runtime/query/iteration/recorded-context.d.ts.map +1 -0
  108. package/dist/runtime/query/iteration/recorded-context.js +37 -0
  109. package/dist/runtime/query/iteration/recorded-context.js.map +1 -0
  110. package/dist/runtime/query/iteration/step-context.d.ts +15 -51
  111. package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
  112. package/dist/runtime/query/iteration/step-context.js +24 -134
  113. package/dist/runtime/query/iteration/step-context.js.map +1 -1
  114. package/dist/runtime/query/iteration/step-shaping.d.ts +20 -8
  115. package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
  116. package/dist/runtime/query/iteration/step-shaping.js +52 -26
  117. package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
  118. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  119. package/dist/runtime/query/iteration/stream-turn.js +45 -25
  120. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  121. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  122. package/dist/runtime/query/prepare-turn.js +10 -1
  123. package/dist/runtime/query/prepare-turn.js.map +1 -1
  124. package/dist/scheduler/completion-inbox.d.ts +27 -0
  125. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  126. package/dist/scheduler/completion-inbox.js +90 -23
  127. package/dist/scheduler/completion-inbox.js.map +1 -1
  128. package/dist/store/session-index/fts.d.ts.map +1 -1
  129. package/dist/store/session-index/fts.js +6 -0
  130. package/dist/store/session-index/fts.js.map +1 -1
  131. package/dist/test-support/file-evidence-held.d.ts +11 -0
  132. package/dist/test-support/file-evidence-held.d.ts.map +1 -0
  133. package/dist/test-support/file-evidence-held.js +28 -0
  134. package/dist/test-support/file-evidence-held.js.map +1 -0
  135. package/dist/test-support/request-only.d.ts +10 -0
  136. package/dist/test-support/request-only.d.ts.map +1 -0
  137. package/dist/test-support/request-only.js +15 -0
  138. package/dist/test-support/request-only.js.map +1 -0
  139. package/dist/tools/admit-facts.d.ts.map +1 -1
  140. package/dist/tools/admit-facts.js +1 -0
  141. package/dist/tools/admit-facts.js.map +1 -1
  142. package/dist/tools/builtins/bash.d.ts +42 -1
  143. package/dist/tools/builtins/bash.d.ts.map +1 -1
  144. package/dist/tools/builtins/bash.js +115 -48
  145. package/dist/tools/builtins/bash.js.map +1 -1
  146. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  147. package/dist/tools/builtins/read-file.js +55 -5
  148. package/dist/tools/builtins/read-file.js.map +1 -1
  149. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  150. package/dist/tools/file-read-tracker.js +28 -0
  151. package/dist/tools/file-read-tracker.js.map +1 -1
  152. package/dist/tools/untrusted-envelope.d.ts +7 -0
  153. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  154. package/dist/tools/untrusted-envelope.js +3 -1
  155. package/dist/tools/untrusted-envelope.js.map +1 -1
  156. package/dist/turn/evidence-recall.d.ts +19 -8
  157. package/dist/turn/evidence-recall.d.ts.map +1 -1
  158. package/dist/turn/evidence-recall.js +134 -53
  159. package/dist/turn/evidence-recall.js.map +1 -1
  160. package/dist/turn/memory-recall.d.ts +18 -0
  161. package/dist/turn/memory-recall.d.ts.map +1 -1
  162. package/dist/turn/memory-recall.js +43 -4
  163. package/dist/turn/memory-recall.js.map +1 -1
  164. package/dist/types/agent/base.d.ts +6 -0
  165. package/dist/types/agent/base.d.ts.map +1 -1
  166. package/dist/types/errors/catalog.d.ts +3 -0
  167. package/dist/types/errors/catalog.d.ts.map +1 -1
  168. package/dist/types/errors/catalog.js +24 -2
  169. package/dist/types/errors/catalog.js.map +1 -1
  170. package/dist/types/message/index.d.ts +46 -1
  171. package/dist/types/message/index.d.ts.map +1 -1
  172. package/dist/types/message/index.js +78 -1
  173. package/dist/types/message/index.js.map +1 -1
  174. package/dist/types/provider/error.d.ts +7 -0
  175. package/dist/types/provider/error.d.ts.map +1 -1
  176. package/dist/types/provider/errors.d.ts +24 -0
  177. package/dist/types/provider/errors.d.ts.map +1 -1
  178. package/dist/types/provider/errors.js +101 -1
  179. package/dist/types/provider/errors.js.map +1 -1
  180. package/dist/types/provider/stream.d.ts +6 -0
  181. package/dist/types/provider/stream.d.ts.map +1 -1
  182. package/dist/types/session/events.d.ts +8 -0
  183. package/dist/types/session/events.d.ts.map +1 -1
  184. package/dist/types/session/events.js.map +1 -1
  185. package/dist/types/session/prepare-step.d.ts +24 -0
  186. package/dist/types/session/prepare-step.d.ts.map +1 -1
  187. package/dist/types/tool/index.d.ts +39 -0
  188. package/dist/types/tool/index.d.ts.map +1 -1
  189. package/dist/types/tool/index.js.map +1 -1
  190. package/dist/types/tool/presentation.d.ts +4 -0
  191. package/dist/types/tool/presentation.d.ts.map +1 -1
  192. package/package.json +1 -1
  193. package/src/agents/QueryAgent.ts +1 -0
  194. package/src/agents/index.ts +1 -0
  195. package/src/agents/plan.ts +30 -0
  196. package/src/compaction/context-breakdown.ts +11 -3
  197. package/src/compaction/dangling.ts +35 -3
  198. package/src/compaction/index.ts +4 -0
  199. package/src/compaction/manager.ts +53 -0
  200. package/src/compaction/manual.ts +42 -10
  201. package/src/compaction/restore.ts +159 -0
  202. package/src/compaction/types.ts +25 -0
  203. package/src/compaction/verifier.ts +3 -1
  204. package/src/compaction/wire.ts +26 -1
  205. package/src/index.ts +2 -0
  206. package/src/plugin/shell-hook.ts +33 -5
  207. package/src/prompt/coding-agent-doctrine.ts +3 -2
  208. package/src/provider/errors.ts +27 -3
  209. package/src/provider/fallback.ts +34 -2
  210. package/src/provider/retry.ts +30 -10
  211. package/src/public-runtime.ts +15 -0
  212. package/src/public-types.ts +3 -0
  213. package/src/runtime/jobs/registry.ts +105 -0
  214. package/src/runtime/query/calls-that-never-started.ts +37 -0
  215. package/src/runtime/query/executor/tool-call-admission.ts +12 -9
  216. package/src/runtime/query/executor.ts +57 -2
  217. package/src/runtime/query/file-evidence-context.ts +79 -11
  218. package/src/runtime/query/index.ts +14 -10
  219. package/src/runtime/query/iteration/index.ts +32 -26
  220. package/src/runtime/query/iteration/phases/compaction.ts +19 -0
  221. package/src/runtime/query/iteration/phases/context.ts +3 -0
  222. package/src/runtime/query/iteration/recorded-context.ts +48 -0
  223. package/src/runtime/query/iteration/step-context.ts +27 -155
  224. package/src/runtime/query/iteration/step-shaping.ts +64 -40
  225. package/src/runtime/query/iteration/stream-turn.ts +47 -25
  226. package/src/runtime/query/prepare-turn.ts +10 -1
  227. package/src/scheduler/completion-inbox.ts +89 -22
  228. package/src/store/session-index/fts.ts +5 -0
  229. package/src/test-support/file-evidence-held.ts +27 -0
  230. package/src/test-support/request-only.ts +19 -0
  231. package/src/tools/admit-facts.ts +1 -0
  232. package/src/tools/builtins/bash.ts +149 -54
  233. package/src/tools/builtins/read-file.ts +66 -4
  234. package/src/tools/file-read-tracker.ts +25 -1
  235. package/src/tools/untrusted-envelope.ts +10 -1
  236. package/src/turn/evidence-recall.ts +189 -58
  237. package/src/turn/memory-recall.ts +59 -5
  238. package/src/types/agent/base.ts +7 -0
  239. package/src/types/errors/catalog.ts +31 -2
  240. package/src/types/message/index.ts +108 -1
  241. package/src/types/provider/error.ts +7 -0
  242. package/src/types/provider/errors.ts +120 -1
  243. package/src/types/provider/stream.ts +6 -0
  244. package/src/types/session/events.ts +8 -0
  245. package/src/types/session/prepare-step.ts +26 -0
  246. package/src/types/tool/index.ts +40 -0
  247. package/src/types/tool/presentation.ts +4 -0
@@ -6,6 +6,7 @@ import { unknownProgramInLine } from '../../authorization/program.js'
6
6
  import { type SkillGrantSet, compileSkillGrant } from '../../authorization/skill-grant.js'
7
7
  import { extractFromToolCall, extractFromToolResult } from '../../compaction/extractor.js'
8
8
  import type { WorkingStateManager } from '../../compaction/manager.js'
9
+ import { recordRestorable } from '../../compaction/restore.js'
9
10
  import { GENAI, NAMZU } from '../../constants/telemetry/index.js'
10
11
  import type { PluginLifecycleManager } from '../../plugin/lifecycle.js'
11
12
  import { buildProbeContext } from '../../probe/context.js'
@@ -13,6 +14,7 @@ import { ProbeVetoError } from '../../probe/errors.js'
13
14
  import { probe as defaultProbeRegistry } from '../../probe/registry.js'
14
15
  import type { ProbeEnforcement } from '../../probe/registry.js'
15
16
  import type { ActivityStore } from '../../store/activity/memory.js'
17
+ import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
16
18
  import { sandboxShellSpawn, withoutBashStartup } from '../../tools/command-shell.js'
17
19
  import { isAlwaysDestructive } from '../../tools/defineTool.js'
18
20
  import { createFileReadTracker } from '../../tools/file-read-tracker.js'
@@ -72,7 +74,11 @@ import {
72
74
  runPreToolHook,
73
75
  unknownToolMessage,
74
76
  } from './executor/tool-call-admission.js'
75
- import { describeVisibleFileEvidence } from './file-evidence-context.js'
77
+ import {
78
+ type FileEvidence,
79
+ describeVisibleFileEvidence,
80
+ visibleFileEvidence,
81
+ } from './file-evidence-context.js'
76
82
  import { seedObservationLedger } from './file-evidence-seed.js'
77
83
  import { DEFAULT_TOOL_RESULT_GUARDRAILS } from './guardrail-presets.js'
78
84
  import { declaredWriteEscalation, shellEscalations } from './path-escalation.js'
@@ -696,6 +702,7 @@ export class ToolExecutor {
696
702
  private emitEvent: EmitEvent
697
703
  private log: Logger
698
704
  private workingStateManager?: WorkingStateManager
705
+ private historyView?: () => readonly Message[]
699
706
  private probes: ProbeEnforcement
700
707
  private parentSpan?: Span
701
708
  private readonly toolCallBudget?: ToolCallBudget
@@ -745,6 +752,30 @@ export class ToolExecutor {
745
752
  this.workingStateManager = manager
746
753
  }
747
754
 
755
+ /**
756
+ * Read the conversation the model is shown, as it is now. Lets a tool ask
757
+ * whether an earlier receipt is still there (see `ToolContext.receiptVisible`)
758
+ * without being handed the history.
759
+ */
760
+ setHistoryView(view: () => readonly Message[]): void {
761
+ this.historyView = view
762
+ }
763
+
764
+ private receiptVisible = (callId: string, renderedFingerprint: string): boolean => {
765
+ const history = this.historyView?.()
766
+ if (!history) return false
767
+ for (let index = history.length - 1; index >= 0; index -= 1) {
768
+ const message = history[index]
769
+ if (message?.role !== 'tool' || message.toolCallId !== callId) continue
770
+ return (
771
+ typeof message.content === 'string' &&
772
+ message.isError !== true &&
773
+ fingerprintContent(message.content) === renderedFingerprint
774
+ )
775
+ }
776
+ return false
777
+ }
778
+
748
779
  setSandbox(sandbox: Sandbox): void {
749
780
  this.config = { ...this.config, sandbox }
750
781
  this.sandboxGate = undefined
@@ -845,7 +876,21 @@ export class ToolExecutor {
845
876
  })
846
877
  }
847
878
 
848
- /** Request-only evidence from the same ledger used by mutation admission. No filesystem I/O. */
879
+ /**
880
+ * The entries that hold now, from the same ledger used by mutation
881
+ * admission. No filesystem I/O. The loop records the difference from what
882
+ * the conversation already says.
883
+ */
884
+ fileEvidenceEntries(messages: readonly Message[]): FileEvidence[] {
885
+ return visibleFileEvidence(
886
+ messages,
887
+ this.fileReadTracker,
888
+ this.config.workingDirectory,
889
+ this.sandboxed,
890
+ )
891
+ }
892
+
893
+ /** The whole of the evidence as one text, from the same ledger. No filesystem I/O. */
849
894
  describeFileEvidence(messages: readonly Message[]): string | undefined {
850
895
  return describeVisibleFileEvidence(
851
896
  messages,
@@ -2167,6 +2212,7 @@ export class ToolExecutor {
2167
2212
  this.dispatchNested(name, input, context, recordObservation, undefined, options),
2168
2213
  sandbox: this.config.sandbox,
2169
2214
  fileReadTracker: this.fileReadTracker,
2215
+ receiptVisible: this.receiptVisible,
2170
2216
  ...(this.config.codeAnalysis ? { codeAnalysis: this.config.codeAnalysis } : {}),
2171
2217
  // Bound to this turn, once. Binding here rather than passing the
2172
2218
  // owner from the tool is what makes the scoping structural: there
@@ -2566,6 +2612,15 @@ export class ToolExecutor {
2566
2612
 
2567
2613
  if (this.workingStateManager) {
2568
2614
  extractFromToolResult(this.workingStateManager, toolName, output, effectiveIsError)
2615
+ // What the model was shown, kept whole so a compaction can put it back.
2616
+ recordRestorable(this.workingStateManager, {
2617
+ toolName,
2618
+ input,
2619
+ output: typeof modelContent === 'string' ? modelContent : output,
2620
+ success: !effectiveIsError,
2621
+ unchanged: result.facts?.kind === 'read' && result.facts.unchanged === true,
2622
+ workingDirectory: this.config.workingDirectory,
2623
+ })
2569
2624
  for (const pin of this.config.abortSignal.aborted ? [] : (result.workingState ?? [])) {
2570
2625
  this.workingStateManager.pin(pin.key, pin.text, toolName)
2571
2626
  }
@@ -29,14 +29,13 @@ import {
29
29
  */
30
30
  const MAX_RECEIPT_UNITS = 32_000
31
31
 
32
- /** The heading of the file-evidence block; a later block under it replaces an earlier one. */
33
- export const FILE_EVIDENCE_HEADING =
34
- 'Visible file evidence (as of this step; a later block with this heading replaces this one)'
32
+ /** The words every file-evidence entry opens with. */
33
+ export const FILE_EVIDENCE_HEADING = 'Visible file evidence'
35
34
 
36
- /** Said when the entries of an earlier block no longer hold and none replaces them. */
37
- export const FILE_EVIDENCE_NONE = `${FILE_EVIDENCE_HEADING}: no entry holds any longer.`
35
+ /** Starts the line of an entry that names the paths whose earlier entries no longer hold. */
36
+ const NO_LONGER_HOLDS = 'No longer holds: '
38
37
 
39
- interface FileEvidence {
38
+ export interface FileEvidence {
40
39
  readonly path: string
41
40
  /** Absent on the write-rooted entries; `'read'` on a body that is a receipt. */
42
41
  readonly kind?: 'read'
@@ -46,14 +45,14 @@ interface FileEvidence {
46
45
  }
47
46
 
48
47
  /** Join visible call bodies, successful receipts and this executor's observation ledger. */
49
- export function describeVisibleFileEvidence(
48
+ export function visibleFileEvidence(
50
49
  messages: readonly Message[],
51
50
  tracker: FileReadTracker,
52
51
  workingDirectory: string,
53
52
  sandboxed: boolean,
54
- ): string | undefined {
53
+ ): FileEvidence[] {
55
54
  // Execution-owned witnesses survive transparent wrappers; a name alone proves nothing.
56
- if (!tracker.writeCallId || !tracker.fingerprint) return
55
+ if (!tracker.writeCallId || !tracker.fingerprint) return []
57
56
  // Lexical, as this projection has always looked entries up: a spelling that
58
57
  // does not resolve to the tool's canonical key finds nothing and the path is
59
58
  // simply not admitted. Writing the ledger is the side that has to key the
@@ -85,8 +84,77 @@ export function describeVisibleFileEvidence(
85
84
  // Malformed retained inputs or a custom tracker cannot establish evidence.
86
85
  }
87
86
  }
88
- if (files.size === 0) return
89
- return `${FILE_EVIDENCE_HEADING}: each entry's current body is the complete body in the named successful write call, with the edit calls in editsInCalls — when present — applied in that order. An entry marked kind:"read" instead names a successful read call whose own receipt shows the file WHOLE, rendered with every line behind its own N<tab> prefix; that receipt is the body, and such an entry never carries editsInCalls. This runtime performed that reconstruction and checked it against its own file observation; it is not a derivation left to you. Reuse the body for a targeted edit; a read solely to recall it is unnecessary. This is NOT a fresh disk check, and after a resume the observation behind an entry may itself have been rebuilt from this conversation's own earlier calls rather than made while this process ran. Built-in edit/write still compare the disk body at mutation admission and refuse observed drift; on refusal inspect the current file and replan. Missing entries establish nothing.\n${JSON.stringify([...files.values()])}`
87
+ return [...files.values()]
88
+ }
89
+
90
+ const FILE_EVIDENCE_GUIDANCE = `each entry names a file (path), the successful write call that holds its complete current body (bodyInCall, that call's id), the edit calls to apply to that body in order (editsInCalls, when present), and a short code for the file as this runtime last observed it (observedFingerprint, which changes when the content does). An entry marked kind:"read" instead names a successful read call whose own receipt shows the whole file, every line behind its own N<tab> prefix; that receipt is the body, and such an entry never carries edit calls. The runtime did that reconstruction and checked it against its own file observation. Reuse the body for a targeted edit; a read only to recall it is unnecessary. This is not a fresh disk check, and after a resume the observation behind an entry may have been rebuilt from this conversation's earlier calls rather than made while this process ran. The built-in edit and write tools still compare the disk body when they run and refuse on drift; on refusal, inspect the current file and replan. Missing entries establish nothing. An entry for a path replaces every earlier entry for that path, and a path listed under "No longer holds" has no entry any more.`
91
+
92
+ /** The whole of what holds now: every entry, with the guidance for reading them. */
93
+ export function describeVisibleFileEvidence(
94
+ messages: readonly Message[],
95
+ tracker: FileReadTracker,
96
+ workingDirectory: string,
97
+ sandboxed: boolean,
98
+ ): string | undefined {
99
+ const entries = visibleFileEvidence(messages, tracker, workingDirectory, sandboxed)
100
+ if (entries.length === 0) return
101
+ return `${FILE_EVIDENCE_HEADING}: ${FILE_EVIDENCE_GUIDANCE}\n${JSON.stringify(entries)}`
102
+ }
103
+
104
+ /**
105
+ * The entries an earlier record still holds, by path, as the JSON each was
106
+ * written with. Read from the record's text, in order, so a later entry for a
107
+ * path replaces an earlier one and a retracted path is gone.
108
+ */
109
+ function heldFileEvidence(recorded: readonly string[]): Map<string, string> {
110
+ const held = new Map<string, string>()
111
+ for (const text of recorded) {
112
+ for (const line of text.split('\n')) {
113
+ try {
114
+ if (line.startsWith(NO_LONGER_HOLDS)) {
115
+ const paths: unknown = JSON.parse(line.slice(NO_LONGER_HOLDS.length))
116
+ if (Array.isArray(paths))
117
+ for (const path of paths) if (typeof path === 'string') held.delete(path)
118
+ } else if (line.startsWith('[')) {
119
+ const entries: unknown = JSON.parse(line)
120
+ if (Array.isArray(entries))
121
+ for (const entry of entries)
122
+ if (typeof entry?.path === 'string') held.set(entry.path, JSON.stringify(entry))
123
+ }
124
+ } catch {
125
+ // a line of the record that is not an entry list
126
+ }
127
+ }
128
+ }
129
+ return held
130
+ }
131
+
132
+ /**
133
+ * What to record now about the files, given what holds (`current`) and what
134
+ * the entries already in the conversation say (`recorded`, oldest first):
135
+ * only the entries that are new or changed, and the paths whose earlier entry
136
+ * no longer holds. An earlier entry is never edited; this says what replaces
137
+ * it. The first record of a conversation carries the guidance for reading
138
+ * entries, later ones do not repeat it. Nothing to say returns undefined.
139
+ */
140
+ export function recordFileEvidence(
141
+ current: readonly FileEvidence[],
142
+ recorded: readonly string[],
143
+ ): string | undefined {
144
+ const held = heldFileEvidence(recorded)
145
+ const changed = current.filter((entry) => held.get(entry.path) !== JSON.stringify(entry))
146
+ const now = new Set(current.map((entry) => entry.path))
147
+ const gone = [...held.keys()].filter((path) => !now.has(path))
148
+ if (changed.length === 0 && gone.length === 0) return
149
+ const lead =
150
+ recorded.length === 0
151
+ ? `${FILE_EVIDENCE_HEADING}: ${FILE_EVIDENCE_GUIDANCE}`
152
+ : `${FILE_EVIDENCE_HEADING}, changed since the last entry; \`bodyInCall\` and \`observedFingerprint\` read as before. An entry for a path replaces the earlier entries for that path.`
153
+ return [
154
+ lead,
155
+ ...(gone.length > 0 ? [`${NO_LONGER_HOLDS}${JSON.stringify(gone)}`] : []),
156
+ ...(changed.length > 0 ? [JSON.stringify(changed)] : []),
157
+ ].join('\n')
90
158
  }
91
159
 
92
160
  /** A full body that arrived whole in one call and is still the file's body. */
@@ -64,6 +64,7 @@ import {
64
64
  type AssistantMessage,
65
65
  type Message,
66
66
  createSystemMessage,
67
+ lastPromptText,
67
68
  } from '../../types/message/index.js'
68
69
  import type { AgentPersona } from '../../types/persona/index.js'
69
70
  import type { LLMProvider } from '../../types/provider/index.js'
@@ -813,6 +814,13 @@ export interface QueryParams {
813
814
  */
814
815
  projectInstructionContext?: ProjectInstructionContext
815
816
 
817
+ /**
818
+ * Text the host wants carried across a compaction, such as where its plan
819
+ * file is. Asked once per pass, after the summary is written; the answer is
820
+ * appended to the restored context. Returning nothing adds nothing.
821
+ */
822
+ postCompactionContext?: () => string | undefined | Promise<string | undefined>
823
+
816
824
  /**
817
825
  * Where this conversation's durable state lives.
818
826
  *
@@ -1655,6 +1663,8 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1655
1663
  const compactionConfig = params.compactionConfig
1656
1664
  ? CompactionConfigSchema.parse(params.compactionConfig)
1657
1665
  : undefined
1666
+ // The live history, so a tool can ask whether an earlier receipt is still in it.
1667
+ toolExecutor.setHistoryView(() => ctx.recorder.messages)
1658
1668
  if (compactionConfig && compactionConfig.strategy !== 'disabled') {
1659
1669
  workingStateManager = new WorkingStateManager(compactionConfig)
1660
1670
  toolExecutor.setWorkingStateManager(workingStateManager)
@@ -1828,6 +1838,9 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1828
1838
  ...(params.projectInstructionContext
1829
1839
  ? { projectInstructionContext: params.projectInstructionContext }
1830
1840
  : {}),
1841
+ ...(params.postCompactionContext
1842
+ ? { postCompactionContext: params.postCompactionContext }
1843
+ : {}),
1831
1844
  ...(providerContextWindow !== undefined ? { providerContextWindow } : {}),
1832
1845
  resolveModelContextWindow: async (model) => {
1833
1846
  if (!modelContextWindows.has(model)) {
@@ -2028,7 +2041,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2028
2041
  {
2029
2042
  turnId: ctx.turnId,
2030
2043
  sessionId: params.sessionId,
2031
- prompt: lastUserPrompt(initialMessages),
2044
+ prompt: lastPromptText(initialMessages),
2032
2045
  signal: ctx.abortController.signal,
2033
2046
  },
2034
2047
  eventTranslator.emitEvent,
@@ -2893,15 +2906,6 @@ async function settleAbandonedTurn(
2893
2906
  }
2894
2907
  }
2895
2908
 
2896
- /** The text of the newest user turn, which is what a prompt hook is asked about. */
2897
- function lastUserPrompt(messages: readonly Message[]): string {
2898
- for (let i = messages.length - 1; i >= 0; i--) {
2899
- const m = messages[i]
2900
- if (m?.role === 'user' && typeof m.content === 'string') return m.content
2901
- }
2902
- return ''
2903
- }
2904
-
2905
2909
  type DrainQueryParams = Omit<QueryParams, 'resumeHandler'> & {
2906
2910
  resumeHandler?: ResumeHandler
2907
2911
  }
@@ -28,7 +28,6 @@ import {
28
28
  import { getTracer } from '../../../telemetry/runtime-accessors.js'
29
29
  import { STRUCTURED_OUTPUT_TOOL_NAME } from '../../../tools/builtins/structuredOutput.js'
30
30
  import type { ToolManager } from '../../../toolsets/manager.js'
31
- import { splitAtEvidenceBlocks } from '../../../turn/evidence-recall.js'
32
31
  import type { CostInfo, TokenUsage } from '../../../types/common/index.js'
33
32
  import { NamzuError } from '../../../types/errors/index.js'
34
33
  import type { MessageId, SessionId, TurnId } from '../../../types/ids/index.js'
@@ -58,7 +57,6 @@ import { createCallbackInference } from '../callback-inference.js'
58
57
  import { SandboxAcquisitionStopped } from '../deferred-sandbox.js'
59
58
  import { deliverDurableInbound } from '../durable-inbound.js'
60
59
  import type { ToolCallOutcome } from '../executor.js'
61
- import { FILE_EVIDENCE_NONE } from '../file-evidence-context.js'
62
60
  import { projectObservationContext } from '../observation-context.js'
63
61
  import { applyLifecycleHookResults } from '../plugin-hooks.js'
64
62
  import {
@@ -103,17 +101,16 @@ import {
103
101
  type TextOnlyModelRefusal,
104
102
  streamWithProviderRejectedImageRecovery,
105
103
  } from './provider-rejected-image.js'
104
+ import { appendRecordedContext } from './recorded-context.js'
106
105
  import { type MediaRemoval, RequestRepair } from './request-repair.js'
107
- import { TurnContextCarry } from './step-context.js'
106
+ import { placeRequestContext } from './step-context.js'
108
107
  import {
109
- FILE_EVIDENCE_KEY,
110
108
  type StepShaping,
111
- appendWorkContext,
112
109
  beforeStep,
113
110
  prepareStep,
114
111
  selectContextModel,
115
112
  stepContextMessage,
116
- workContextBlocks,
113
+ workContext,
117
114
  } from './step-shaping.js'
118
115
  import { streamProviderTurn } from './stream-turn.js'
119
116
 
@@ -194,8 +191,7 @@ export class IterationOrchestrator {
194
191
  * one process must not suppress each other's first envelope.
195
192
  */
196
193
  private lastEnvelopeKey: string | undefined
197
- /** Where this turn's request-only data first went, so it stays there. */
198
- private readonly turnContext = new TurnContextCarry()
194
+ /** The previous request's context, so a pre-call hook is told what changed. */
199
195
  private previousRequestContext: RequestContextSnapshot | undefined
200
196
 
201
197
  private projectObservations(messages: Message[]): Message[] {
@@ -838,6 +834,11 @@ export class IterationOrchestrator {
838
834
  'limit-finalization',
839
835
  )
840
836
  : undefined
837
+ // What the step asked to have kept in the conversation joins the
838
+ // history now, after its last message, so this request and every
839
+ // later one begin with it. Nothing is inserted ahead of a message
840
+ // that is already recorded.
841
+ appendRecordedContext(recorder, step.recordedContext, this.ctx.log)
841
842
  const baseMessages = recorder.messages
842
843
 
843
844
  // Step guidance is appended to the REQUEST, never pushed onto
@@ -911,24 +912,30 @@ export class IterationOrchestrator {
911
912
  DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
912
913
  { imagesReadable: this.modelReadsImages(stepModel) },
913
914
  )
914
- // The blocks of data that belong to this turn (recalled memory and
915
- // evidence, the context contributions, the work context) sit just
916
- // before the operator's latest message the first time the model
917
- // answers it, so the request is the last thing it reads. They stay
918
- // there on every later call of the turn, and a block that first
919
- // appears on a later call follows the history of that moment, so each
920
- // request begins with the whole of the one before it. The short
921
- // reminders and notices stay after the history.
915
+ // Request-only data (the context contributions, notes about the
916
+ // search, the work context) goes last on every call, after the
917
+ // whole history, so the history a caching driver reads never
918
+ // changes shape from one call to the next. What a step wants
919
+ // kept is recorded in the history above instead. The short
920
+ // reminders and notices follow it.
922
921
  const messages = [...projected]
923
- this.turnContext.place(
922
+ // What the loop learns about files and finished work holds until it
923
+ // changes, so it is recorded, after the history as it stands, and
924
+ // carried in this request where it will sit in the next one's history.
925
+ const work = workContext(shaping, projected, iterationNum, step)
926
+ const recordedNow = appendRecordedContext(recorder, work.recorded, this.ctx.log)
927
+ placeRequestContext(
924
928
  messages,
925
- requestHistory,
926
- workingMemory.history.at(-1),
927
- [contextText, ...(step.context ? splitAtEvidenceBlocks(step.context) : [])]
928
- .filter((text): text is string => Boolean(text))
929
- .map(stepContextMessage),
930
- workContextBlocks(shaping, projected, iterationNum, step),
931
- { [FILE_EVIDENCE_KEY]: stepContextMessage(FILE_EVIDENCE_NONE) },
929
+ recordedNow,
930
+ // The working-memory block and the policy notice already end the
931
+ // request; the entries just recorded belong ahead of them.
932
+ (workingMemory.context ? 1 : 0) + (policyNotice ? 1 : 0),
933
+ [
934
+ ...[contextText, step.context]
935
+ .filter((text): text is string => Boolean(text))
936
+ .map(stepContextMessage),
937
+ ...work.requestOnly,
938
+ ],
932
939
  )
933
940
  if (closingDirective) messages.push(closingDirective)
934
941
  await this.reportUnsupportedToolResults(messages)
@@ -2896,9 +2903,8 @@ export class IterationOrchestrator {
2896
2903
  this.ctx.turnConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
2897
2904
  { imagesReadable: this.modelReadsImages(model) },
2898
2905
  )
2899
- appendWorkContext(this.stepShaping(), finalMessages, this.steps.length + 1, { model })
2900
2906
  // The closing directive is the last thing the model reads: after
2901
- // the working memory and any work context above.
2907
+ // the working memory above.
2902
2908
  finalMessages.push(
2903
2909
  createRuntimeContextMessage(
2904
2910
  `[SYSTEM] Turn is ending due to ${reason}. ${CLOSING_RESPONSE_GUIDANCE}`,
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../../../compaction/plan.js'
12
12
  import type { ContextReducer, ContextReduction } from '../../../../compaction/reducer.js'
13
13
  import { createSlidingWindowReducer } from '../../../../compaction/reducer.js'
14
+ import { renderRestoredContext } from '../../../../compaction/restore.js'
14
15
  import { findRetainedIndices } from '../../../../compaction/retention.js'
15
16
  import { serializeState } from '../../../../compaction/serializer.js'
16
17
  import {
@@ -1003,6 +1004,24 @@ async function runCompactionCheckInner(
1003
1004
  compactedContent = serializeState(manager.getState())
1004
1005
  }
1005
1006
 
1007
+ // Skills the model loaded and files it read go back verbatim after the
1008
+ // summary, with whatever the host wants carried across (its plan file).
1009
+ let hostNote: string | undefined
1010
+ try {
1011
+ hostNote = await ctx.postCompactionContext?.()
1012
+ } catch (error) {
1013
+ // A host note is an addition; the pass does not depend on it.
1014
+ ctx.log.warn('The host note for a compaction could not be read — leaving it out', {
1015
+ [NAMZU.TURN_ID]: ctx.recorder.turnId,
1016
+ 'exception.message': error instanceof Error ? error.message : String(error),
1017
+ })
1018
+ }
1019
+ const restored = renderRestoredContext(manager.getState(), {
1020
+ contextWindowTokens: budget,
1021
+ ...(hostNote ? { hostNote } : {}),
1022
+ })
1023
+ if (restored) compactedContent = `${compactedContent}\n\n${restored}`
1024
+
1006
1025
  // The pointer is only worth a line when both tools it names are mounted.
1007
1026
  const mounted = new Set(ctx.tools?.listNames?.() ?? [])
1008
1027
  const compactionMessage = buildCompactionMessage(
@@ -256,6 +256,9 @@ export interface IterationContext {
256
256
  /** Live project policy; separate from human inbound continuation. */
257
257
  readonly projectInstructionContext?: ProjectInstructionContext
258
258
 
259
+ /** Host text appended to the restored context after a compaction. */
260
+ readonly postCompactionContext?: () => string | undefined | Promise<string | undefined>
261
+
259
262
  readonly workingStateManager?: WorkingStateManager
260
263
 
261
264
  /**
@@ -0,0 +1,48 @@
1
+ import { NAMZU } from '../../../constants/telemetry/index.js'
2
+ import type { TurnRecorder } from '../../../manager/session/turn-recorder.js'
3
+ import {
4
+ type Message,
5
+ createTurnContextEntry,
6
+ isTurnContextKey,
7
+ turnContextEntries,
8
+ } from '../../../types/message/index.js'
9
+ import type { RecordedContextItem } from '../../../types/session/index.js'
10
+ import type { Logger } from '../../../utils/logger.js'
11
+
12
+ /**
13
+ * Append what a step asked to have recorded, after whatever the history
14
+ * ends with.
15
+ *
16
+ * Only at the end: an entry slipped in ahead of the operator's message would
17
+ * put the live array out of log order, and the recorder would then write a
18
+ * whole compaction record to re-base it on every turn. An item adds nothing
19
+ * when its text is empty or equals the last entry under its key, so a step
20
+ * that offers the same text on every call records it once. Returns the
21
+ * entries it added, so a request built before they were added can carry them
22
+ * at the place they will hold.
23
+ */
24
+ export function appendRecordedContext(
25
+ recorder: Pick<TurnRecorder, 'messages' | 'pushMessage' | 'turnId'>,
26
+ items: readonly RecordedContextItem[] | undefined,
27
+ log: Pick<Logger, 'warn'>,
28
+ at: Date = new Date(),
29
+ ): Message[] {
30
+ const appended: Message[] = []
31
+ for (const item of items ?? []) {
32
+ const text = item.text.trim()
33
+ if (text.length === 0) continue
34
+ if (!isTurnContextKey(item.key)) {
35
+ log.warn('A step recorded context under a key that is not a name; it was skipped', {
36
+ [NAMZU.TURN_ID]: recorder.turnId,
37
+ 'namzu.runtime.key': String(item.key).slice(0, 80),
38
+ })
39
+ continue
40
+ }
41
+ const earlier = turnContextEntries(recorder.messages, item.key)
42
+ if (earlier.at(-1) === text) continue
43
+ const entry = createTurnContextEntry(item.key, text, at, { later: earlier.length > 0 })
44
+ recorder.pushMessage(entry)
45
+ appended.push(entry)
46
+ }
47
+ return appended
48
+ }