@namzu/cli 25.0.0 → 25.0.1

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 CHANGED
@@ -1,5 +1,70 @@
1
1
  # @namzu/cli
2
2
 
3
+ ## 25.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f33c62b: A run now suspends for a background job the model said it was waiting on, instead of settling over it. When the model stops calling tools and a job named by `wait_for_job` is still running, the run waits — no provider request, no tokens — for the job's exit, an operator message, or the settle grace, whichever comes first. On an exit the model gets one more turn with the `[Background job update]` line in front of it; on neither, the run settles and names the job.
8
+
9
+ This is the same bounded, zero-token wait `CompletionInbox` already gave a delegated task, and it shares the delegated task's grace — half of what the run has left before it must start finishing — under a ceiling of its own: two minutes, or `NAMZU_JOB_HOLD_MAX_MS`. On a run with a `timeoutMs` the grace comes out of what is left rather than being added to it, so time a `wait_for_job` call already spent shortens the hold by the same amount. On a run WITHOUT one — no run deadline, which is what the CLI ships — there is no remainder to take a share of, and the task ceiling would be a flat hour; that hour is sound for a task, which cannot outlive it, and wrong for a job, which can run forever. The two-minute job ceiling is what bounds that case, so a `wait_for_job` that ran its own bound out is followed by two more minutes at most, not by a second hour. The iteration limit still bounds all of it, and the wait starts nothing and stops nothing.
10
+
11
+ **Wait-intent is explicit.** Only a job `wait_for_job` named is awaited, and only for the rest of the run that named it. A job nobody waited on — a dev server, a watcher — never holds a run open, and there is no opt-in flag on `bash run_in_background` that changes that.
12
+
13
+ **Why this is `minor` and not `major`.** The signal is new: no run that exists today can have an awaited job, because nothing before this could mark one. A host that never calls `wait_for_job` sees the loop it saw before, so no default changes and no existing behaviour is withdrawn.
14
+
15
+ Additive API:
16
+
17
+ - `Run.abandonedJobIds` — awaited jobs still running when the run ended, the job-side counterpart to `abandonedTaskIds`. Naming them is not stopping them: a run-owned job is still stopped by the run's own teardown, and one bound to the host's session keeps running.
18
+ - `RUNTIME_CONTEXT_MESSAGE_KINDS` gains `'job-exit'`, the provenance on the message that carries an exit delivered by the wait. Consumers that exhaustively switch on `RuntimeContextMessageKind` need a case for it.
19
+ - `BackgroundJobRegistryRef` gains an optional `markAwaited(id)`, and `bindOwner`'s options take an `onAwaited(id)` callback that backs it. Both are optional; a host that wires neither gets the previous behaviour, which is no hold.
20
+ - `NAMZU_JOB_HOLD_MAX_MS` sets the job ceiling above, in milliseconds, beside the `NAMZU_JOB_WAIT_*` knobs `wait_for_job` already reads. Unset is two minutes.
21
+
22
+ - 6ae4072: The repeat-call advisory (notices, then escalates, when a tool is called with identical arguments over and over) now reaches the model even when the repeated tool's result is structured content — an image, a document, an MCP resource block — rather than plain text. `attachRepeatNotice` previously required the trailing tool result to be a string and silently dropped the notice otherwise; it now falls back to delivering the advisory as its own runtime-context message immediately after the tool-result batch. No thresholds changed, and a repeat that keeps succeeding is still only ever noticed, never refused.
23
+
24
+ `RuntimeContextMessageKind` gains a `'repeat-call'` member for this fallback message. A consumer that exhaustively switches over the union (the CLI's transcript labeling did) needs a case for it; `@namzu/cli` adds one in this release.
25
+
26
+ - 92ab1d9: A resumed conversation keeps the file witnesses it earned. The observation ledger is process memory, and every resume path handed the run an empty one: the derived work context could admit nothing, and the first thing a resumed agent did was read back a file whose whole body was in the transcript it had just been given.
27
+
28
+ The new export `seedObservationLedger(messages, tracker, { workingDirectory, additionalDirectories, sandboxed })` rebuilds a ledger from a conversation's own history. `resumeRun` and `query`'s checkpoint resume call it for you, from the history as repaired rather than as checkpointed, so the ledger describes exactly what the model is about to be shown; the CLI calls it the first time a turn asks for a conversation's tracker, which covers `/resume`, `namzu run --resume`/`--continue`, and a forked conversation — each seeded from its own messages, once. Call it directly if you keep a tracker per conversation and restore one yourself. Nothing is persisted and no session-store schema changes; a host that does nothing sees exactly today's behaviour.
29
+
30
+ What a replay may conclude is what the projection would admit, by the same predicates and the same bounded replay. A `write` whose call and successful receipt are both intact restores its body and its witness; the `edit` calls above it are replayed hop by hop and restore the chain. A `read` never supplies a body — the line numbering is never undone to recover one — and can only confirm one already reconstructed, by rendering it forward through the read tool's own renderer and comparing the whole rendering with the receipt. A windowed read, a read that shows something else, a cleared receipt, a hop that no longer applies, a body past the bounds and a call whose arguments run past what a replay reads as evidence each withdraw whatever the pass held for that path. So do the two cases where the transcript settles no outcome: a call it never answered — the unknown-outcome result the kernel's own repair writes for one included — may have landed with the file half written, and a mutation it refused is a tool's own report about that path, a drift refusal above all, made after reading the disk. Each of those costs the path it names and no other. A path whose walk ends holding no body is entered in the ledger nowhere, and a path this conversation only ever read establishes nothing.
31
+
32
+ No file's content is read. The one thing the seed does touch the filesystem for is the key each entry is filed under: a ledger entry identifies a file rather than a spelling, so `read`, `write` and `edit` all key on the path canonicalized through its symlinks, and entries filed any other way would be entries no mutation ever checks and no drift refusal can ever withdraw. The paths named in the history are therefore resolved exactly as the tools resolve them — `additionalDirectories` included — before the walk begins. Under a sandbox the keys are the paths as written and no host path is consulted.
33
+
34
+ Only content-backed observations are restored, so a seeded ledger is never weaker than the empty one a resume starts from. A path whose body could not be reconstructed is left OUT of the ledger rather than entered without a fingerprint: `hasRead` is the read-before-overwrite refusal, and granting it with no body to compare would let a full overwrite of a file that changed while the session was closed through with nothing checked. Every path the replay does not restore therefore behaves exactly as it does today. A fingerprint it does restore is a claim derived from history and is still compared with the real file at mutation time, so a file changed while the session was closed is refused there and the refusal withdraws the path from the projection.
35
+
36
+ Three things seed nothing at all, each leaving today's empty ledger: a history naming more than 1,024 distinct path spellings — the ones only `read` names included, and two spellings of one file counting twice — which is resolved whole or not at all rather than in a prefix that cannot say what a mutation replaced; a tool call id claimed by two calls or answered by two receipts, `read` included, since the receipt that was hidden could be the observation that withdrew a claim; and a mutation no path can be recovered from, whatever came back to it — one declaring no `path`, one whose path no longer resolves inside the directories the run may reach (a refused write to a path outside them is one of these: a key is what withdrawing one path rather than the whole pass takes), or one the provider stream cut off mid-JSON, whose arguments are recorded as `{}`. A merely large call is none of these: the argument bound governs what may be believed, not what may be attributed, so an oversize `write` withdraws its own path's body and leaves every other witness standing.
37
+
38
+ `read`'s numbering and windowing move to `tools/builtins/read-render.ts` as pure functions, which is what lets the forward-render comparison run the tool's own renderer rather than a copy of it. The tool's output is unchanged, byte for byte.
39
+
40
+ - 7ca8c7d: Add a `wait_for_job` builtin tool: it blocks on a background job's exit under a run-length bound and an idle bound that resets on new output, and returns the job's accumulated output in one call — the shell-job counterpart to the existing `wait_for_task`. Neither bound stops the job; a timeout reports which clock ran out and the output read so far, with a `next_offset` to resume from. Ships by default alongside `job` and `bash`, and refuses cleanly on a host with no background job registry.
41
+
42
+ `job`'s own description no longer instructs polling with `action: "read"` in a loop; it now points at `wait_for_job` instead. `read` and `list` are unchanged.
43
+
44
+ `BackgroundJobRegistry` gains a public `waitForExit(id, { signal })`, resolving immediately for a job that has already exited and honouring an abort signal. `BackgroundJobRegistryRef` (the tool-context surface) gains an optional `waitForExit` of the same shape — additive, so an existing host implementing this interface directly keeps working without it; `wait_for_job` refuses cleanly when it is absent.
45
+
46
+ - Updated dependencies [68e535b]
47
+ - Updated dependencies [a9e4b19]
48
+ - Updated dependencies [a54dc71]
49
+ - Updated dependencies [86a3818]
50
+ - Updated dependencies [03630cd]
51
+ - Updated dependencies [f33c62b]
52
+ - Updated dependencies [a8df193]
53
+ - Updated dependencies [8bfe291]
54
+ - Updated dependencies [6ae4072]
55
+ - Updated dependencies [dd8702d]
56
+ - Updated dependencies [92ab1d9]
57
+ - Updated dependencies [e6d6d1e]
58
+ - Updated dependencies [7ca8c7d]
59
+ - Updated dependencies [6551d15]
60
+ - @namzu/sdk@40.0.0
61
+ - @namzu/zen@1.0.2
62
+ - @namzu/computer-use@1.4.2
63
+ - @namzu/anthropic@5.1.1
64
+ - @namzu/ollama@2.2.2
65
+ - @namzu/openai@3.1.1
66
+ - @namzu/openrouter@2.4.0
67
+
3
68
  ## 25.0.0
4
69
 
5
70
  ### Major Changes
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-message.d.ts","sourceRoot":"","sources":["../../src/context/runtime-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAE3D,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,yBAAyB,GAAG,MAAM,CAmB3E"}
1
+ {"version":3,"file":"runtime-message.d.ts","sourceRoot":"","sources":["../../src/context/runtime-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAE3D,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,yBAAyB,GAAG,MAAM,CAuB3E"}
@@ -9,8 +9,12 @@ export function runtimeContextLabel(kind) {
9
9
  return 'Answer review feedback';
10
10
  case 'auto-continuation':
11
11
  return 'Automatic continuation';
12
+ case 'job-exit':
13
+ return 'Background job exit';
12
14
  case 'limit-finalization':
13
15
  return 'Limit finalization request';
16
+ case 'repeat-call':
17
+ return 'Repeat-call advisory';
14
18
  case 'steering':
15
19
  return 'Runtime steering';
16
20
  case 'structured-output':
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-message.js","sourceRoot":"","sources":["../../src/context/runtime-message.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IAClE,QAAQ,IAAI,EAAE,CAAC;QACd,KAAK,cAAc;YAClB,OAAO,cAAc,CAAA;QACtB,KAAK,UAAU;YACd,OAAO,iBAAiB,CAAA;QACzB,KAAK,eAAe;YACnB,OAAO,wBAAwB,CAAA;QAChC,KAAK,mBAAmB;YACvB,OAAO,wBAAwB,CAAA;QAChC,KAAK,oBAAoB;YACxB,OAAO,4BAA4B,CAAA;QACpC,KAAK,UAAU;YACd,OAAO,kBAAkB,CAAA;QAC1B,KAAK,mBAAmB;YACvB,OAAO,yBAAyB,CAAA;QACjC,KAAK,iBAAiB;YACrB,OAAO,yBAAyB,CAAA;IAClC,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"runtime-message.js","sourceRoot":"","sources":["../../src/context/runtime-message.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IAClE,QAAQ,IAAI,EAAE,CAAC;QACd,KAAK,cAAc;YAClB,OAAO,cAAc,CAAA;QACtB,KAAK,UAAU;YACd,OAAO,iBAAiB,CAAA;QACzB,KAAK,eAAe;YACnB,OAAO,wBAAwB,CAAA;QAChC,KAAK,mBAAmB;YACvB,OAAO,wBAAwB,CAAA;QAChC,KAAK,UAAU;YACd,OAAO,qBAAqB,CAAA;QAC7B,KAAK,oBAAoB;YACxB,OAAO,4BAA4B,CAAA;QACpC,KAAK,aAAa;YACjB,OAAO,sBAAsB,CAAA;QAC9B,KAAK,UAAU;YACd,OAAO,kBAAkB,CAAA;QAC1B,KAAK,mBAAmB;YACvB,OAAO,yBAAyB,CAAA;QACjC,KAAK,iBAAiB;YACrB,OAAO,yBAAyB,CAAA;IAClC,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAON,KAAK,OAAO,EAiBZ,MAAM,YAAY,CAAA;AAyInB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAoDvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAa/D,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IAC1D,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAA;CAC/C;AAOD,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAC5B,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AAuPrB,6EAA6E;AAC7E,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,MAAM,EAAE,CAM7F;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,MAAM,EAAE,MAAM,MAAM,EACpB,iBAAiB,GAAE,SAAS,MAAM,EAAO,GACvC,SAAS,iBAAiB,EAAE,CAqE9B;AAkHD,wBAAgB,GAAG,CAAC,EACnB,GAAG,EAAE,UAAU,EACf,aAAa,EACb,cAAsC,GACtC,EAAE,QAAQ,2CA4qNV"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAON,KAAK,OAAO,EAiBZ,MAAM,YAAY,CAAA;AAyInB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAoDvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAa/D,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;IACxB,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IAC1D,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAA;CAC/C;AAOD,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAC5B,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AAuPrB,6EAA6E;AAC7E,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,MAAM,EAAE,CAM7F;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,MAAM,EAAE,MAAM,MAAM,EACpB,iBAAiB,GAAE,SAAS,MAAM,EAAO,GACvC,SAAS,iBAAiB,EAAE,CAqE9B;AAkHD,wBAAgB,GAAG,CAAC,EACnB,GAAG,EAAE,UAAU,EACf,aAAa,EACb,cAAsC,GACtC,EAAE,QAAQ,2CAkrNV"}
package/dist/tui/App.js CHANGED
@@ -405,10 +405,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
405
405
  summaries: 0,
406
406
  reclaimedTokens: 0,
407
407
  });
408
- // Jobs that ended while no turn was running. The kernel's notice needs a
409
- // tool result to ride on, so between turns the exit is held here and
410
- // handed to the next send as part of the system text; the transcript
411
- // row is written at once.
408
+ // Jobs that ended while no turn was running.
409
+ //
410
+ // Only those: an exit that lands mid-turn belongs to the kernel, which
411
+ // rides it out on the next tool result or, for a job the model awaited,
412
+ // delivers it as the message that releases its own suspend. Queueing one
413
+ // here as well would announce the same exit twice, so the `abortRef`
414
+ // check below is the boundary between the two owners rather than an
415
+ // optimisation. Between turns the kernel is not listening, so the exit is
416
+ // held here and handed to the next send as part of the system text; the
417
+ // transcript row is written at once.
412
418
  const idleJobNoticesRef = useRef([]);
413
419
  const drainIdleJobNotices = () => {
414
420
  if (idleJobNoticesRef.current.length === 0)