@namzu/sdk 52.7.0 → 53.1.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 (135) hide show
  1. package/CHANGELOG.md +45 -26
  2. package/dist/authorization/protected-paths.d.ts +2 -4
  3. package/dist/authorization/protected-paths.d.ts.map +1 -1
  4. package/dist/authorization/protected-paths.js +2 -4
  5. package/dist/authorization/protected-paths.js.map +1 -1
  6. package/dist/authorization/readonly-commands.d.ts +4 -4
  7. package/dist/authorization/readonly-commands.js +7 -7
  8. package/dist/authorization/readonly-commands.js.map +1 -1
  9. package/dist/authorization/readonly-flags.d.ts +1 -2
  10. package/dist/authorization/readonly-flags.d.ts.map +1 -1
  11. package/dist/authorization/readonly-flags.js +1 -2
  12. package/dist/authorization/readonly-flags.js.map +1 -1
  13. package/dist/authorization/removal.d.ts +2 -3
  14. package/dist/authorization/removal.d.ts.map +1 -1
  15. package/dist/authorization/removal.js +3 -4
  16. package/dist/authorization/removal.js.map +1 -1
  17. package/dist/authorization/shell-paths.d.ts +5 -5
  18. package/dist/authorization/shell-paths.js +6 -6
  19. package/dist/authorization/shell-paths.js.map +1 -1
  20. package/dist/compaction/context-window.d.ts +1 -1
  21. package/dist/compaction/context-window.js +1 -1
  22. package/dist/compaction/verifier.js +2 -2
  23. package/dist/compaction/verifier.js.map +1 -1
  24. package/dist/connector/web/page.d.ts +2 -3
  25. package/dist/connector/web/page.d.ts.map +1 -1
  26. package/dist/connector/web/page.js +2 -3
  27. package/dist/connector/web/page.js.map +1 -1
  28. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  29. package/dist/execution/workflow/prelude.js +29 -5
  30. package/dist/execution/workflow/prelude.js.map +1 -1
  31. package/dist/execution/workflow/runtime.d.ts +2 -0
  32. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  33. package/dist/execution/workflow/runtime.js +8 -3
  34. package/dist/execution/workflow/runtime.js.map +1 -1
  35. package/dist/execution/workflow/types.d.ts +3 -3
  36. package/dist/execution/workflow/types.d.ts.map +1 -1
  37. package/dist/manager/agent/lifecycle.d.ts +1 -1
  38. package/dist/manager/agent/lifecycle.js +1 -1
  39. package/dist/plugin/shell-hook.d.ts +1 -1
  40. package/dist/plugin/shell-hook.js +4 -4
  41. package/dist/plugin/shell-hook.js.map +1 -1
  42. package/dist/provider/errors.js +2 -2
  43. package/dist/provider/errors.js.map +1 -1
  44. package/dist/public-runtime.d.ts +1 -0
  45. package/dist/public-runtime.d.ts.map +1 -1
  46. package/dist/public-runtime.js.map +1 -1
  47. package/dist/runtime/jobs/monitor-stream.d.ts +1 -2
  48. package/dist/runtime/jobs/monitor-stream.d.ts.map +1 -1
  49. package/dist/runtime/jobs/monitor-stream.js +1 -2
  50. package/dist/runtime/jobs/monitor-stream.js.map +1 -1
  51. package/dist/runtime/jobs/websocket-source.d.ts +1 -1
  52. package/dist/runtime/jobs/websocket-source.d.ts.map +1 -1
  53. package/dist/runtime/jobs/websocket-source.js +1 -1
  54. package/dist/runtime/jobs/websocket-source.js.map +1 -1
  55. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  56. package/dist/runtime/query/checkpoint.js +3 -1
  57. package/dist/runtime/query/checkpoint.js.map +1 -1
  58. package/dist/runtime/query/iteration/index.js +3 -3
  59. package/dist/runtime/query/iteration/index.js.map +1 -1
  60. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  61. package/dist/runtime/query/iteration/outstanding-work.js +10 -2
  62. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  63. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +2 -2
  64. package/dist/runtime/query/iteration/phases/compaction-breaker.js +2 -2
  65. package/dist/runtime/query/iteration/phases/compaction.d.ts +3 -5
  66. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/phases/compaction.js +6 -8
  68. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  69. package/dist/runtime/query/iteration/request-repair.d.ts +1 -1
  70. package/dist/runtime/query/iteration/request-repair.js +2 -2
  71. package/dist/runtime/query/iteration/request-repair.js.map +1 -1
  72. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  73. package/dist/runtime/query/path-escalation.js +2 -3
  74. package/dist/runtime/query/path-escalation.js.map +1 -1
  75. package/dist/scheduler/completion-inbox.d.ts +21 -1
  76. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  77. package/dist/scheduler/completion-inbox.js +20 -1
  78. package/dist/scheduler/completion-inbox.js.map +1 -1
  79. package/dist/session/workspace/driver.d.ts +1 -1
  80. package/dist/store/checkpoint/memory.d.ts +1 -1
  81. package/dist/store/checkpoint/memory.js +1 -1
  82. package/dist/store/memory/markdown-format.d.ts +1 -1
  83. package/dist/store/memory/markdown-format.js +1 -1
  84. package/dist/store/memory/markdown.js +1 -1
  85. package/dist/store/session-log/core.d.ts.map +1 -1
  86. package/dist/store/session-log/core.js +28 -0
  87. package/dist/store/session-log/core.js.map +1 -1
  88. package/dist/store/session-log/spilled-input.d.ts +34 -0
  89. package/dist/store/session-log/spilled-input.d.ts.map +1 -0
  90. package/dist/store/session-log/spilled-input.js +68 -0
  91. package/dist/store/session-log/spilled-input.js.map +1 -0
  92. package/dist/tools/builtins/bash.d.ts.map +1 -1
  93. package/dist/tools/builtins/bash.js +1 -2
  94. package/dist/tools/builtins/bash.js.map +1 -1
  95. package/dist/tools/builtins/edit-match.d.ts +5 -6
  96. package/dist/tools/builtins/edit-match.d.ts.map +1 -1
  97. package/dist/tools/builtins/edit-match.js +5 -6
  98. package/dist/tools/builtins/edit-match.js.map +1 -1
  99. package/dist/types/provider/errors.js +1 -1
  100. package/dist/types/provider/errors.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/authorization/protected-paths.ts +2 -4
  103. package/src/authorization/readonly-commands.ts +7 -7
  104. package/src/authorization/readonly-flags.ts +1 -2
  105. package/src/authorization/removal.ts +3 -4
  106. package/src/authorization/shell-paths.ts +6 -6
  107. package/src/compaction/context-window.ts +1 -1
  108. package/src/compaction/verifier.ts +2 -2
  109. package/src/connector/web/page.ts +2 -3
  110. package/src/execution/workflow/prelude.ts +29 -5
  111. package/src/execution/workflow/runtime.ts +9 -3
  112. package/src/execution/workflow/types.ts +3 -3
  113. package/src/manager/agent/lifecycle.ts +1 -1
  114. package/src/plugin/shell-hook.ts +4 -4
  115. package/src/provider/errors.ts +2 -2
  116. package/src/public-runtime.ts +1 -0
  117. package/src/runtime/jobs/monitor-stream.ts +1 -2
  118. package/src/runtime/jobs/websocket-source.ts +1 -1
  119. package/src/runtime/query/checkpoint.ts +6 -1
  120. package/src/runtime/query/iteration/index.ts +3 -3
  121. package/src/runtime/query/iteration/outstanding-work.ts +9 -2
  122. package/src/runtime/query/iteration/phases/compaction-breaker.ts +2 -2
  123. package/src/runtime/query/iteration/phases/compaction.ts +6 -8
  124. package/src/runtime/query/iteration/request-repair.ts +3 -3
  125. package/src/runtime/query/path-escalation.ts +2 -3
  126. package/src/scheduler/completion-inbox.ts +31 -1
  127. package/src/session/workspace/driver.ts +1 -1
  128. package/src/store/checkpoint/memory.ts +1 -1
  129. package/src/store/memory/markdown-format.ts +1 -1
  130. package/src/store/memory/markdown.ts +1 -1
  131. package/src/store/session-log/core.ts +28 -0
  132. package/src/store/session-log/spilled-input.ts +90 -0
  133. package/src/tools/builtins/bash.ts +1 -2
  134. package/src/tools/builtins/edit-match.ts +5 -6
  135. package/src/types/provider/errors.ts +1 -1
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Which shell lines only read, so that they run without a prompt.
3
3
  *
4
- * The reference terminal runs `ls`, `cat`, `git status` and `git diff` without
5
- * asking, because each is on a vetted list and nothing in the line is decided
6
- * at run time. This is that list and those conditions (version 2.1.302):
4
+ * `ls`, `cat`, `git status` and `git diff` run without asking, because each is
5
+ * on a vetted list and nothing in the line is decided at run time. The list and
6
+ * the conditions are these:
7
7
  *
8
8
  * - every command of the line is on the list, the line parses completely, and
9
9
  * nothing in it is read at run time: no `$var` outside single quotes, no
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Not ported, and so never read-only here: `sed`, `ps`, `lsof`, `ss`, `date`,
24
24
  * `man`, `xargs`, `fd`, `gh`, `docker logs|inspect`, `printf`, `[[`, `test`
25
- * and the shell builtins other than `cd`. The reference's check of the working
25
+ * and the shell builtins other than `cd`. The check of the working
26
26
  * directory for a planted bare repository needs the directory, which the tool
27
27
  * predicate does not get; the trusted-project gate is what stands in for it.
28
28
  *
@@ -35,7 +35,7 @@ import type { ShellDialect } from '../types/tool/index.js'
35
35
  import { FLAG_TABLES, type FlagTable } from './readonly-flags.js'
36
36
  import { type ShellWord, lexShellCommandLine } from './shell-lexer.js'
37
37
 
38
- /** Lines longer than this are not analysed (the reference's budget). */
38
+ /** Lines longer than this are not analysed (the budget). */
39
39
  const MAX_LINE_LENGTH = 20_000
40
40
 
41
41
  /** Variables whose value cannot change what a command does. */
@@ -80,7 +80,7 @@ const SAFE_ENVIRONMENT: ReadonlySet<string> = new Set([
80
80
  'GIT_TERMINAL_PROMPT',
81
81
  ])
82
82
 
83
- /** Commands that read and print, with any arguments (the reference's bare-safe list). */
83
+ /** Commands that read and print, with any arguments (the bare-safe list). */
84
84
  const ANY_ARGUMENTS: ReadonlySet<string> = new Set([
85
85
  'cal',
86
86
  'uptime',
@@ -570,7 +570,7 @@ const GIT_CHECKS: Readonly<Record<string, Dangerous>> = {
570
570
  },
571
571
  }
572
572
 
573
- // -- the commands with a regex of their own in the reference ----------------
573
+ // -- the commands with a regex of their own ----------------
574
574
 
575
575
  function uniqIsReadOnly(args: readonly string[]): boolean {
576
576
  for (let i = 0; i < args.length; i += 1) {
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Per-flag allowlists for the shell commands that may run without a prompt.
3
3
  *
4
- * Read from the reference terminal (version 2.1.302): for each command, the
5
- * flags it vets and what each takes, `none` (a switch), `string` or `number`.
4
+ * For each command, the flags it vets and what each takes, `none` (a switch), `string` or `number`.
6
5
  * A flag not listed here is not read-only, which is how `git diff --output=f`
7
6
  * (writes a file), `git -c`, `--exec-path` and `--config-env` stay out: they
8
7
  * are not in any table. Space-separated lists per kind keep the file readable;
@@ -7,8 +7,7 @@
7
7
  * `rm -rf ~`, `rm -rf $HOME`, `rm -rf "$BUILD/"` (empty variable, so `/`) and
8
8
  * `rm -rf ..` all went past it. The flags were never the point. A removal is
9
9
  * catastrophic because of WHAT it removes, so this reads the targets of `rm`
10
- * and `rmdir` whatever the flags, as the reference terminal's dangerous-removal
11
- * check does (version 2.1.302).
10
+ * and `rmdir` whatever the flags.
12
11
  *
13
12
  * A removal is flagged when a target is
14
13
  *
@@ -26,7 +25,7 @@
26
25
  *
27
26
  * The answer is a reason, which the kernel turns into an escalation: asked
28
27
  * every time, in every mode, and refused when nobody can be asked. It is not a
29
- * denial: the reference asks, because there are legitimate removals a person
28
+ * denial: it asks, because there are legitimate removals a person
30
29
  * can approve.
31
30
  */
32
31
 
@@ -250,7 +249,7 @@ function checkTarget(
250
249
  const expanded = expandTilde(spelled, scope.home)
251
250
  const relative = !isAbsolute(expanded)
252
251
  if (expandsAtRuntime(word) && /[$`]/.test(expanded)) {
253
- // A variable the check cannot read: the reference treats it as a relative
252
+ // A variable the check cannot read: it is treated as a relative
254
253
  // name, which is also what the shell does when the variable is unset and
255
254
  // the rest is a plain name. Nothing more to learn from it.
256
255
  return undefined
@@ -6,15 +6,15 @@
6
6
  * and `echo x > ~/.profile` carry their paths inside one string, and a
7
7
  * boundary that only reads declared arguments let both through.
8
8
  *
9
- * This is the reference terminal's answer, ported: a table of the commands
10
- * whose positional arguments are paths (its path-command table and
11
- * per-command extractors, version 2.1.302), the kind of access each makes (read,
12
- * write or create), and the output redirections of the line. It is a
9
+ * This reads the paths a shell line touches: a table of the commands whose
10
+ * positional arguments are paths, with an extractor for each command, the kind
11
+ * of access each makes (read, write or create), and the output redirections of
12
+ * the line. It is a
13
13
  * reading of the text, not a model of the shell:
14
14
  *
15
15
  * - a word that expands at runtime (`$VAR`, `$(...)`) has no path to read;
16
16
  * it is reported with `unresolved` so the caller decides what an unreadable
17
- * write target is worth (the reference asks);
17
+ * write target is worth;
18
18
  * - a `cd` earlier in the line moves every relative path after it, which the
19
19
  * reading does not follow, so the caller is told one happened;
20
20
  * - a command that is not in the table contributes nothing.
@@ -279,7 +279,7 @@ interface FlagTable {
279
279
  readonly valuedLong: ReadonlySet<string>
280
280
  }
281
281
 
282
- /** Operands of a command whose flags are known: an unknown flag stops flag parsing, as in the reference. */
282
+ /** Operands of a command whose flags are known: an unknown flag stops flag parsing. */
283
283
  function operandsByTable(table: FlagTable): (args: Args) => ShellWord[] {
284
284
  const kind = (flag: string): 'boolean' | 'valued' | 'attached' | 'unknown' => {
285
285
  if (flag.startsWith('--')) {
@@ -121,7 +121,7 @@ export const MIN_DERIVED_TRIGGER_FRACTION = 0.5
121
121
  * The fraction of the window at which older history is summarised.
122
122
  *
123
123
  * A host that set `triggerThreshold` gets exactly that. Otherwise the
124
- * trigger is the one Claude Code uses: the window, less room kept for the
124
+ * trigger is the window, less room kept for the
125
125
  * reply (the model's output cap, at most 20,000 tokens), less a 13,000-token
126
126
  * buffer. On a 200,000-token window that is 167,000 tokens (83.5%); on a
127
127
  * 1,000,000-token window, 967,000 (96.7%).
@@ -255,8 +255,8 @@ async function buildVerifiedSummaryWithProvider(
255
255
  return serialized
256
256
  }
257
257
 
258
- // The verifier's own request can overflow a small window. The reference
259
- // terminal answers that with a ladder: send less, and when nothing is left to
258
+ // The verifier's own request can overflow a small window. That is answered
259
+ // with a ladder: send less, and when nothing is left to
260
260
  // cut, work from everything it already has. Here "less" is the older half of
261
261
  // the excerpt dropped behind a placeholder, at most three times, and
262
262
  // "everything it already has" is the extracted state, which the verifier only
@@ -16,14 +16,13 @@ export const WEB_PAGE_CACHE_MAX_BYTES = 50 * 1024 * 1024
16
16
  export const WEB_PAGE_CONVERT_MAX_CHARS = 1024 * 1024
17
17
  /** Most page text a model is asked about in one call. */
18
18
  export const WEB_PAGE_MODEL_MAX_CHARS = 100_000
19
- /** Marker the reference-shaped cuts end with. */
19
+ /** Marker that truncated content ends with. */
20
20
  export const WEB_PAGE_TRUNCATED_MARKER = '[Content truncated due to length...]'
21
21
 
22
22
  /**
23
23
  * A redirect stays on the site when only the path or query changes (and
24
24
  * `www.` comes or goes). `http:` to `https:` on the same host and default
25
- * port is the same site too: the reference upgrades every URL to `https:`
26
- * before it asks, so that hop never reaches it.
25
+ * port is the same site too.
27
26
  */
28
27
  export function isSameSiteRedirect(from: URL, to: URL): boolean {
29
28
  const upgrade = from.protocol === 'http:' && to.protocol === 'https:'
@@ -44,6 +44,15 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
44
44
  if (t === 'object') return typeof value.then === 'function' ? 'a promise' : 'an object';
45
45
  return 'a ' + t;
46
46
  }
47
+ function overBudget() {
48
+ return config.tokenLimit !== null && config.tokenLimit > 0 && spent.tokensUsed >= config.tokenLimit;
49
+ }
50
+ function budgetError() {
51
+ var e = new Error('Workflow token budget exceeded (' + spent.tokensUsed.toLocaleString('en-US') + ' / ' + config.tokenLimit.toLocaleString('en-US') + ' output tokens). Stopping further agent() calls. Agents already running will finish and their results are kept.');
52
+ e.name = 'WorkflowBudgetExceededError';
53
+ return e;
54
+ }
55
+ function isBudgetError(e) { return !!e && e.name === 'WorkflowBudgetExceededError'; }
47
56
  async function agent(prompt, opts) {
48
57
  if (typeof prompt !== 'string' || prompt.length === 0) {
49
58
  throw new TypeError('agent(prompt, options?): the prompt must be a non-empty string, got ' + kind(prompt));
@@ -52,6 +61,7 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
52
61
  if (typeof o !== 'object' || Array.isArray(o)) {
53
62
  throw new TypeError('agent(prompt, options?): options must be an object, got ' + kind(o));
54
63
  }
64
+ if (overBudget()) throw budgetError();
55
65
  var request = { prompt: prompt, cursor: cursor };
56
66
  if (o.label != null) request.label = clean(o.label, 120);
57
67
  if (o.phase != null) request.phase = clean(o.phase, 120);
@@ -72,12 +82,16 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
72
82
  throw new TypeError('parallel(thunks): item ' + (p + 1) + ' must be a function (wrap the work as () => agent(...)), got ' + kind(thunks[p]));
73
83
  }
74
84
  }
75
- return Promise.all(thunks.map(function (t, n) {
85
+ var dropped = 0;
86
+ var results = await Promise.all(thunks.map(function (t, n) {
76
87
  return Promise.resolve().then(t).catch(function (e) {
77
- log('parallel item ' + (n + 1) + ' failed: ' + clean(e && e.message ? e.message : e, 300));
88
+ if (isBudgetError(e)) { dropped++; return null; }
89
+ log('Step ' + (n + 1) + ' stopped: ' + clean(e && e.message ? e.message : e, 300));
78
90
  return null;
79
91
  });
80
92
  }));
93
+ if (dropped > 0) log('parallel: ' + dropped + (dropped === 1 ? ' item' : ' items') + ' dropped because the token budget is spent.');
94
+ return results;
81
95
  }
82
96
  async function pipeline(items) {
83
97
  if (!Array.isArray(items)) {
@@ -89,21 +103,31 @@ const PRELUDE_SOURCE = String.raw`(function (config) {
89
103
  throw new TypeError('pipeline(items, ...stages): stage ' + (s + 1) + ' must be a function, got ' + kind(stages[s]));
90
104
  }
91
105
  }
92
- return Promise.all(items.map(async function (item, index) {
106
+ if (items.length > 0 && overBudget()) throw budgetError();
107
+ var dropped = 0;
108
+ var results = await Promise.all(items.map(async function (item, index) {
93
109
  var value = item;
94
110
  for (var i = 0; i < stages.length; i++) {
95
111
  try {
96
- value = await stages[i](value, index, item);
112
+ value = await stages[i](value, item, index);
97
113
  } catch (e) {
98
- log('pipeline item ' + (index + 1) + ' stopped at stage ' + (i + 1) + ': ' + clean(e && e.message ? e.message : e, 300));
114
+ if (isBudgetError(e)) { dropped++; return null; }
115
+ log('Item ' + (index + 1) + ' stopped at step ' + (i + 1) + ': ' + clean(e && e.message ? e.message : e, 300));
99
116
  return null;
100
117
  }
101
118
  if (value === null || value === undefined) return null;
102
119
  }
103
120
  return value;
104
121
  }));
122
+ if (dropped > 0) log('pipeline: ' + dropped + (dropped === 1 ? ' item' : ' items') + ' dropped because the token budget is spent.');
123
+ return results;
105
124
  }
106
125
  var budget = Object.freeze({
126
+ get total() { return config.tokenLimit; },
127
+ spent: function () { return spent.tokensUsed; },
128
+ remaining: function () {
129
+ return config.tokenLimit === null ? Infinity : Math.max(0, config.tokenLimit - spent.tokensUsed);
130
+ },
107
131
  get tokensUsed() { return spent.tokensUsed; },
108
132
  get tokenLimit() { return config.tokenLimit === null ? undefined : config.tokenLimit; },
109
133
  });
@@ -46,6 +46,12 @@ const AGENT_CALL_KEYS = new Set([
46
46
  'effort',
47
47
  ])
48
48
 
49
+ /** The row title of an agent the script did not label: the first 60 characters of its prompt, whitespace squeezed. */
50
+ export function defaultAgentLabel(prompt: string, index: number): string {
51
+ const label = prompt.slice(0, 60).replace(/\s+/g, ' ').trim()
52
+ return label.length > 0 ? label : `Agent ${index}`
53
+ }
54
+
49
55
  function integer(name: string, value: number, min: number, max: number): number {
50
56
  if (!Number.isSafeInteger(value) || value < min || value > max) {
51
57
  throw new RangeError(`${name} must be an integer between ${min} and ${max}.`)
@@ -54,10 +60,10 @@ function integer(name: string, value: number, min: number, max: number): number
54
60
  }
55
61
 
56
62
  export function resolveWorkflowLimits(limits: WorkflowLimits = {}): WorkflowResolvedLimits {
57
- const defaultConcurrency = Math.min(8, Math.max(1, cpus().length - 2))
63
+ const defaultConcurrency = Math.min(16, Math.max(2, cpus().length - 2))
58
64
  return {
59
65
  maxConcurrency: integer('maxConcurrency', limits.maxConcurrency ?? defaultConcurrency, 1, 16),
60
- maxAgents: integer('maxAgents', limits.maxAgents ?? 200, 1, 1000),
66
+ maxAgents: integer('maxAgents', limits.maxAgents ?? 1000, 1, 1000),
61
67
  maxSourceBytes: integer(
62
68
  'maxSourceBytes',
63
69
  limits.maxSourceBytes ?? 256 * 1024,
@@ -502,7 +508,7 @@ export class WorkflowRuntime {
502
508
  const queuedAt = now()
503
509
  const agent: AgentRecord = {
504
510
  index,
505
- label: call.label || `Agent ${index}`,
511
+ label: call.label || defaultAgentLabel(call.prompt, index),
506
512
  prompt: call.prompt,
507
513
  ...(phase ? { phaseIndex: phase.index, phaseTitle: phase.title } : {}),
508
514
  ...(call.agentType !== undefined ? { agentType: call.agentType } : {}),
@@ -232,9 +232,9 @@ export interface WorkflowJournal {
232
232
  }
233
233
 
234
234
  export interface WorkflowLimits {
235
- /** Agents running at once. Default `min(8, cpus - 2)`, at least 1; at most 16. */
235
+ /** Agents running at once. Default `min(16, cpus - 2)`, at least 2; at most 16. */
236
236
  readonly maxConcurrency?: number
237
- /** Agents one run may start. Default 200; at most 1000. */
237
+ /** Agents one run may start. Default 1000; at most 1000. */
238
238
  readonly maxAgents?: number
239
239
  /** UTF-8 script size. Default 256 KiB. */
240
240
  readonly maxSourceBytes?: number
@@ -242,7 +242,7 @@ export interface WorkflowLimits {
242
242
  readonly memoryLimitBytes?: number
243
243
  /** One value crossing the bridge, either way. Default 1 MiB. */
244
244
  readonly maxValueBytes?: number
245
- /** Stop admitting new agents once this many tokens are spent. Default: none. */
245
+ /** Stop admitting new agents once this many tokens are spent; also the script's `budget.total`. Default: none. */
246
246
  readonly tokenBudget?: number
247
247
  /** Wall clock for the whole run. Default 6 hours. */
248
248
  readonly maxDurationMs?: number
@@ -348,7 +348,7 @@ export class AgentManager {
348
348
 
349
349
  /**
350
350
  * The refusal for a delegation past the depth limit. It tells the model what
351
- * to do instead (the reference terminal's rule): a bare limit leaves it
351
+ * to do instead: a bare limit leaves it
352
352
  * retrying the same spawn, while an instruction to do the work itself ends the
353
353
  * attempt. The limit is `AgentManagerConfig.maxDepth`; there is no env override.
354
354
  */
@@ -57,7 +57,7 @@
57
57
  * - `systemMessage` — a warning for the operator, reported through the logger.
58
58
  * - `suppressOutput` — keep this hook's stdout out of the debug log.
59
59
  * - `hookSpecificOutput` with `hookEventName` naming this event (Namzu's
60
- * name or the reference spelling `PreToolUse`, `Stop`, ...):
60
+ * name or the PascalCase spelling `PreToolUse`, `Stop`, ...):
61
61
  * `permissionDecision` (`allow`, `deny` or `ask`) with
62
62
  * `permissionDecisionReason`, `updatedInput` and `additionalContext` for
63
63
  * `pre_tool_use`; `additionalContext` for `user_prompt_submit`,
@@ -458,8 +458,8 @@ function pathOf(toolInput: unknown): string | undefined {
458
458
  return undefined
459
459
  }
460
460
 
461
- /** The reference spelling of each event name, which a hook's JSON may use. */
462
- const REFERENCE_EVENT_NAMES: Readonly<Partial<Record<ShellHookEvent, string>>> = {
461
+ /** The PascalCase spelling of each event name, which a hook's JSON may use. */
462
+ const PASCAL_EVENT_NAMES: Readonly<Partial<Record<ShellHookEvent, string>>> = {
463
463
  user_prompt_submit: 'UserPromptSubmit',
464
464
  session_start: 'SessionStart',
465
465
  session_end: 'SessionEnd',
@@ -565,7 +565,7 @@ function readShellHookJson(event: ShellHookEvent, stdout: string): ShellHookRead
565
565
  if (typeof named !== 'string') {
566
566
  return { invalid: '`hookSpecificOutput` is missing its `hookEventName`' }
567
567
  }
568
- if (named !== event && named !== REFERENCE_EVENT_NAMES[event]) {
568
+ if (named !== event && named !== PASCAL_EVENT_NAMES[event]) {
569
569
  return {
570
570
  invalid: `\`hookSpecificOutput.hookEventName\` is "${named}" but this hook ran for ${event}`,
571
571
  }
@@ -260,8 +260,8 @@ const OVERFLOW_BODY_PATTERNS: readonly RegExp[] = [
260
260
  // length" — so a pattern anchored on "exceeded" misses it entirely.
261
261
  /exceeds?\s+(?:the\s+)?(?:model'?s?\s+)?context\s+(?:length|window|size)/i,
262
262
  /context\s+(?:length|window|size)\s+exceed/i,
263
- // The reference terminal's own list matches the bare phrase "context
264
- // window". Here it must sit next to a verb that says the request did not
263
+ // The bare phrase "context
264
+ // window" is too broad on its own. Here it must sit next to a verb that says the request did not
265
265
  // fit ("larger than the model context window of N tokens"), so a 400 that
266
266
  // only names the window ("does not support this parameter with the 1M
267
267
  // context window beta") stays an invalid request. The same pattern lives in
@@ -722,6 +722,7 @@ export {
722
722
  CompletionInbox,
723
723
  formatCompletionNotification,
724
724
  } from './scheduler/completion-inbox.js'
725
+ export type { CompletionInboxOptions } from './scheduler/completion-inbox.js'
725
726
 
726
727
  // Scheduled jobs: WHEN something is due, and nothing about how a host stores
727
728
  // or runs a job. The time engine and the evaluator are pure (`Intl` for time
@@ -15,8 +15,7 @@
15
15
  * work without any injection), which is what lets the tests advance a window
16
16
  * by calling a function rather than by waiting for one.
17
17
  *
18
- * The constants and the wording come from the reference implementation of this
19
- * tool; deviations are named where they are made.
18
+ * The constants and the wording are fixed here.
20
19
  */
21
20
 
22
21
  /** A longer line keeps its first this-many characters. */
@@ -29,7 +29,7 @@ import type { JobHost, JobSource } from './registry.js'
29
29
  * asks a person first (see `tools/builtins/monitor.ts`).
30
30
  */
31
31
 
32
- /** The largest frame accepted; a bigger one ends the watch. Same as the reference. */
32
+ /** The largest frame accepted; a bigger one ends the watch. */
33
33
  export const WEBSOCKET_MAX_PAYLOAD_BYTES = 1_048_576
34
34
  export const WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 15_000
35
35
  /** After a polite close, how long a peer gets to answer before the socket is destroyed. */
@@ -2,6 +2,7 @@ import type { WorkingStateSnapshot } from '../../compaction/wire.js'
2
2
  import { type TurnRecorder, readFoldedHistory } from '../../manager/session/turn-recorder.js'
3
3
  import type { CheckpointScope, SessionCheckpointStore } from '../../store/checkpoint/index.js'
4
4
  import type { SessionLog } from '../../store/session-log/index.js'
5
+ import { restoreSpilledRequestInputs } from '../../store/session-log/spilled-input.js'
5
6
  import type { SerializedSpanContext } from '../../telemetry/attributes.js'
6
7
  import { NamzuError } from '../../types/errors/index.js'
7
8
  import type {
@@ -148,7 +149,11 @@ export async function readParks(
148
149
  decisionId: r.decisionId,
149
150
  turnId: r.turnId as TurnId,
150
151
  checkpointId: r.checkpointId,
151
- request: r.request as unknown as HITLDecisionRequest,
152
+ // A call input too large for a line was spilled; put it back.
153
+ request: (await restoreSpilledRequestInputs(
154
+ structuredClone(r.request) as unknown as HITLDecisionRequest & { toolCalls?: unknown },
155
+ (ref) => log.readSpill(ref),
156
+ )) as HITLDecisionRequest,
152
157
  parkedAt: Date.parse(r.ts),
153
158
  ...(r.deadlineAt !== undefined ? { deadlineAt: Date.parse(r.deadlineAt) } : {}),
154
159
  })
@@ -526,7 +526,7 @@ export class IterationOrchestrator {
526
526
  await this.ctx.checkpointMgr.markLoopStart?.(recorder)
527
527
 
528
528
  // Context-overflow relief is a two-rung ladder per *stuck point*, not per
529
- // turn, as in the reference terminal (at most two reactive compactions per
529
+ // turn (at most two reactive compactions per
530
530
  // overflow): the first relief, then one deeper pass if the retry still
531
531
  // overflows.
532
532
  //
@@ -2121,7 +2121,7 @@ export class IterationOrchestrator {
2121
2121
  classifyProviderError(err, this.ctx.provider.id).code === 'context_length_exceeded'
2122
2122
  ) {
2123
2123
  // The rapid-refill check comes first, before the first reactive relief
2124
- // of an overflow, as in the reference terminal. A relief that follows
2124
+ // of an overflow. A relief that follows
2125
2125
  // the previous compaction within the refill window counts toward the
2126
2126
  // limit, so a tool that re-fills the window every time it is called
2127
2127
  // stops the turn instead of being cleared and called again without end.
@@ -2155,7 +2155,7 @@ export class IterationOrchestrator {
2155
2155
  continue
2156
2156
  }
2157
2157
  // Out of rungs. The conversation is kept; say why compaction could not
2158
- // make room when it errored, as the reference does.
2158
+ // make room when it errored.
2159
2159
  const cause = compactionErrorNote(this.ctx)
2160
2160
  if (cause !== undefined && err instanceof Error) {
2161
2161
  try {
@@ -163,7 +163,7 @@ export async function* holdForOutstandingWork(
163
163
  hasToolCalls: boolean,
164
164
  deliverInbound: () => number | Promise<number>,
165
165
  ): AsyncGenerator<SessionEvent, boolean> {
166
- const inbox = ctx.completionInbox?.hasPendingWork ? ctx.completionInbox : undefined
166
+ const inbox = ctx.completionInbox?.holdsTurn ? ctx.completionInbox : undefined
167
167
  const jobs = ctx.awaitedJobs?.hasPendingWork ? ctx.awaitedJobs : undefined
168
168
  if (!inbox && !jobs) return false
169
169
 
@@ -370,7 +370,14 @@ export function settleOutstandingWork(ctx: IterationContext): void {
370
370
  /** Work this turn walked away from. See {@link settleOutstandingWork}. */
371
371
  export function recordAbandonedWork(ctx: IterationContext): void {
372
372
  const abandoned = ctx.completionInbox?.outstandingTaskIds ?? []
373
- if (abandoned.length > 0) {
373
+ if (abandoned.length > 0 && ctx.completionInbox?.holdsTurnOpen === false) {
374
+ // Handed over on purpose: the host keeps these running and tells the
375
+ // model when they end, so the turn did not walk away from them.
376
+ ctx.log.info('Turn ended with delegated work left running in the background', {
377
+ [NAMZU.TURN_ID]: ctx.recorder.turnId,
378
+ 'namzu.runtime.tasks': abandoned,
379
+ })
380
+ } else if (abandoned.length > 0) {
374
381
  ctx.log.warn('Turn ended with delegated work still running', {
375
382
  [NAMZU.TURN_ID]: ctx.recorder.turnId,
376
383
  'namzu.runtime.tasks': abandoned,
@@ -30,8 +30,8 @@ export const COMPACTION_REFILL_LIMIT = 3
30
30
 
31
31
  /**
32
32
  * One short line, shown once, when the turn stops on refills. It names the
33
- * cause and a step that keeps the conversation: the reference terminal also
34
- * offers to start over here, and Namzu never does.
33
+ * cause and a step that keeps the conversation: starting over is never
34
+ * offered here.
35
35
  */
36
36
  export const COMPACTION_REFILL_MESSAGE =
37
37
  'The context filled up again right after being compacted, three times in a row. A file being read or a tool output is probably too large for the context window. Try reading it in smaller parts.'
@@ -221,8 +221,7 @@ export function measureContext(ctx: IterationContext): {
221
221
  }
222
222
 
223
223
  /**
224
- * Reactive compactions allowed for one overflow, as in the reference
225
- * terminal: the first relief, then at most one deeper pass.
224
+ * Reactive compactions allowed for one overflow: the first relief, then at most one deeper pass.
226
225
  */
227
226
  export const OVERFLOW_RELIEF_LIMIT = 2
228
227
 
@@ -350,8 +349,8 @@ export async function relieveOverflow(
350
349
 
351
350
  /**
352
351
  * Why the last compaction attempt errored, kept for the turn so that when
353
- * relief runs out the failure it ends on can say so, as the reference
354
- * terminal's " · automatic compaction failed: <cause>" does.
352
+ * relief runs out the failure it ends on can say so, and it reads
353
+ * " · automatic compaction failed: <cause>".
355
354
  */
356
355
  const compactionErrors = new WeakMap<object, string>()
357
356
  const MAX_COMPACTION_CAUSE_CHARS = 300
@@ -430,9 +429,8 @@ async function declined(
430
429
  /**
431
430
  * The third refill in a row: stop the turn instead of compacting again.
432
431
  * Returns true when this pass must not run. Only with `circuitBreaker`.
433
- * The loop also asks it before the first reactive relief of an overflow, as
434
- * the reference terminal does (`rhr` runs first in its withheld-overflow
435
- * handling), so a re-read loop made only of overflows is counted too.
432
+ * The loop also asks it before the first reactive relief of an overflow, so a re-read loop made
433
+ * only of overflows is counted too.
436
434
  */
437
435
  export async function stopOnRefill(ctx: IterationContext): Promise<boolean> {
438
436
  const breaker = compactionBreaker(ctx)
@@ -721,7 +719,7 @@ async function runCompactionCheckInner(
721
719
  const breaker = compactionBreaker(ctx)
722
720
  if (breaker?.failuresOpen && !options?.force) {
723
721
  if (breaker.takeFailureNotice()) {
724
- // Silent for the person, as in the reference terminal: a log line and the
722
+ // Silent for the person: a log line and the
725
723
  // event for a host that wants it, but no sentence to show. Only the
726
724
  // proactive pass stops; a real overflow still runs the reactive ladder.
727
725
  ctx.log.warn('Compaction failed three times in a row — switched off for this turn', {
@@ -21,7 +21,7 @@ import { isProviderRejectedImageError } from './provider-rejected-image.js'
21
21
  * Repair-and-retry for a request a provider rejects as invalid (a 400 that is
22
22
  * not a context overflow), so a conversation does not dead-end on it.
23
23
  *
24
- * Mirrors the reference terminal's table of silent 400 recoveries: pair tool
24
+ * The silent 400 recoveries, in order: pair tool
25
25
  * calls with results before a send, strip rejected reasoning, drop a
26
26
  * parameter the model refuses, and take rejected media out of the OUTGOING
27
27
  * request. Stored history is never edited.
@@ -77,7 +77,7 @@ type Rejection =
77
77
  /** Most repair resends for one original request, across every send it makes. */
78
78
  export const MAX_REPAIRS_PER_REQUEST = 5
79
79
  const MAX_STRIPS_PER_INDEX = 3
80
- /** The reference drops media in these steps for an unprocessable request. */
80
+ /** Media is dropped in these steps for an unprocessable request. */
81
81
  const GENERIC_MEDIA_LIMITS: readonly number[] = [510, 255, 127]
82
82
 
83
83
  const PARAMETER_CODES: ReadonlySet<string> = new Set([
@@ -608,7 +608,7 @@ export class RequestRepair {
608
608
  victims = slots.filter((slot) => slot.messageIndex === newest.messageIndex)
609
609
  }
610
610
  } else {
611
- // The reference's steps, then halving for smaller requests, down to
611
+ // The media steps, then halving for smaller requests, down to
612
612
  // none: each step keeps fewer than the request held, newest first.
613
613
  const limit =
614
614
  GENERIC_MEDIA_LIMITS.find((candidate) => candidate < slots.length) ??
@@ -46,8 +46,7 @@ export interface PathEscalationContext {
46
46
  * a root that itself sits under a protected name is not a reason to ask about
47
47
  * every file in it. The case that matters is the managed worktree a sub-agent
48
48
  * runs in, `~/.namzu/projects/<slug>/worktrees/<name>`, which lies under
49
- * `.namzu` by construction; the reference terminal makes the same exception
50
- * for its own worktrees directory. A path in no root is looked at whole.
49
+ * `.namzu` by construction. A path in no root is looked at whole.
51
50
  */
52
51
  function protectedScope(absolute: string, roots: readonly string[]): string {
53
52
  let best: string | undefined
@@ -194,7 +193,7 @@ export async function shellEscalations(
194
193
  if (!context.reviewOutside) continue
195
194
  if (unresolved) {
196
195
  // A write whose target is decided at runtime cannot be checked; a
197
- // read of one is left, as the reference leaves it.
196
+ // read of one is left alone.
198
197
  if (use.access === 'write') outside.add(spelled)
199
198
  continue
200
199
  }
@@ -43,6 +43,11 @@ const UNOWNED_BUFFER_LIMIT = 32
43
43
  */
44
44
  const OWNED_WORK_DISPLAY_LIMIT = 16
45
45
 
46
+ export interface CompletionInboxOptions {
47
+ /** See {@link CompletionInbox.holdsTurnOpen}. Default `true`. */
48
+ readonly holdsTurnOpen?: boolean
49
+ }
50
+
46
51
  /**
47
52
  * Completions that finished with nobody left to hear them.
48
53
  *
@@ -151,7 +156,23 @@ export class CompletionInbox {
151
156
  * `log` is optional and unresolved until it is actually needed (the
152
157
  * eviction-warning path in `hold()`), same reason as `LocalTaskScheduler`.
153
158
  */
154
- constructor(private readonly log?: Logger) {}
159
+ constructor(
160
+ private readonly log?: Logger,
161
+ options: CompletionInboxOptions = {},
162
+ ) {
163
+ this.holdsTurnOpen = options.holdsTurnOpen !== false
164
+ }
165
+
166
+ /**
167
+ * Whether a turn about to end waits here for the tasks still running.
168
+ *
169
+ * `true` (the default) is the original contract: the turn parks at its settle
170
+ * point until the tasks it launched report back, so their results enter the
171
+ * same turn. `false` lets the turn end with them still running; the host owns
172
+ * their completion from then on (it keeps them alive and wakes the model when
173
+ * they end). Completions that have already arrived are delivered either way.
174
+ */
175
+ readonly holdsTurnOpen: boolean
155
176
 
156
177
  /**
157
178
  * Start listening.
@@ -349,6 +370,15 @@ export class CompletionInbox {
349
370
  return this.unheard.size > 0 || this.outstanding.size > 0
350
371
  }
351
372
 
373
+ /**
374
+ * Whether the turn has a reason to wait at its settle point: anything
375
+ * pending when it {@link holdsTurnOpen}, else only a completion already in
376
+ * hand (waiting for its persistence gate).
377
+ */
378
+ get holdsTurn(): boolean {
379
+ return this.holdsTurnOpen ? this.hasPendingWork : this.unheard.size > 0
380
+ }
381
+
352
382
  /**
353
383
  * Wait for the next completion, deadline or abort, whichever comes first.
354
384
  * Aborting releases only this waiter; work and undelivered results remain owned.
@@ -22,7 +22,7 @@ export interface CreateWorkspaceParams {
22
22
  /**
23
23
  * Traceable name below the configured workspace root. Nested names are
24
24
  * accepted; absolute paths, traversal, and paths rewritten by a filesystem
25
- * link are refused by the reference Git driver.
25
+ * link are refused by the Git driver.
26
26
  */
27
27
  label?: string
28
28
  }
@@ -26,7 +26,7 @@ export interface InMemorySessionCheckpointStoreOptions {
26
26
  *
27
27
  * It keeps the serialised bytes rather than the object, so `restore` hashes
28
28
  * exactly what `write` hashed and refuses exactly what the disk store
29
- * refuses. It is the parity partner of the disk store and the store an
29
+ * refuses. It is the counterpart of the disk store and the store an
30
30
  * in-memory session keeps its checkpoints in.
31
31
  */
32
32
  export class InMemorySessionCheckpointStore implements SessionCheckpointStore {
@@ -249,7 +249,7 @@ export interface MemoryFileFields {
249
249
  /**
250
250
  * The file for one memory. Fields that repeat another are left out — a title
251
251
  * equal to the name, a summary equal to the description, the default format —
252
- * so an ordinary file carries the reference shape and nothing else.
252
+ * so an ordinary file carries the canonical shape and nothing else.
253
253
  */
254
254
  export function formatMemoryFile(fields: MemoryFileFields, body: string): string {
255
255
  const lines: string[] = ['---']