pi-supernova 0.0.11 → 0.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.
package/CHANGELOG.md CHANGED
@@ -2,11 +2,82 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.0] - 2026-09-04
6
+
7
+ ### Fixed
8
+
9
+ - Each program uses a separate worker and transaction. Late callbacks, idle worker errors, concurrent runs, startup cancellation, and startup deadlines no longer share state.
10
+ - Command deadlines stop descendants. Signal exits report failure. Native commits recover partial replacements; writes reject missing content and edits reject overlapping matches.
11
+ - Unified patches handle zero-context insertions, empty files, final-newline changes, and CRLF. Reads see external changes; evidence search includes staged files.
12
+ - OMP tool calls use the current session’s enabled, permission-aware registry. Discovery supplies complete schemas. Batch options, failure envelopes, scheduling, and call identifiers remain consistent.
13
+ - Output limits cover aggregate batch text, markers, and spill footers. Details remain valid JSON; logs report truncation. DataView ranges and own `__proto__` values survive serialization.
14
+
15
+ ### Changed
16
+
17
+ - Acorn parses supported function and body forms. Return hints ignore comments and nested callbacks. Invalid collection arguments report errors.
18
+ - The result ledger checks text equality, retains explicit-read pins across aliases, expires source records, and accepts a zero window.
19
+ - Terminal width checks use Unicode graphemes. Expanded cards wrap all bounded result and log text without a preview-line limit.
20
+ - Pi 0.85.0 native adapters respect active-tool changes. Pi has no cross-extension execution API; the README states this limit.
21
+ - Live checks cover Pi 0.85.0 and OMP 18.1.10, including expanded terminal output.
22
+
23
+ ## [0.0.15] - 2026-09-04
24
+
25
+ ### Removed
26
+
27
+ - Dead code (−638 lines net): the static `extractOperationsFromCode` source-regex preview (the live trace superseded it), `SafeText`/`fitOutputLines`/`wrapPlainToWidth` (the call slot is always empty), `shutdownGuestWorkers`, `decode.js` helpers nobody imported, `vfs.clear`, the `tools` alias for `nova`, unused bridge methods (`hasExecutor`, `clearVfsCache`, `isMutating`), `test/verify-width-crash.mjs`, and unused status-header knobs (`spinnerFrame`, `iconOverride`).
28
+ - `edit` no longer accepts a unified diff (`patch`/`oldText` starting with `@@`); `apply_patch` / `patch()` is the one way to apply a diff.
29
+ - `nova.surface`/`nova.snap` host-side unwrapping and their dedicated RPC methods: both route through `nova.call` and the guest unwraps once.
30
+
31
+ ### Changed
32
+
33
+ - One `relativeSlash`, one `isTestPath`, one declaration-span computation (`WorkspaceIndex.spansOf`, cached per file) instead of three copies each; search adapters (fuzzy find, grep, glob listing) live in `search.js`; internal-only names are no longer exported.
34
+ - Render computes the card body once per width (was twice); fuzzy find stats only matched paths (was every file); results shorter than a collapsible run skip ledger hashing.
35
+
36
+ ### Fixed
37
+
38
+ - `nova.speculate` end-to-end (an end-to-end test now covers rollback and commit through the worker RPC).
39
+
40
+ ## [0.0.14] - 2026-09-04
41
+
42
+ ### Added
43
+
44
+ - **Seen-ledger.** The model's context window is treated as memory: a result never re-sends a run of lines (≥6, mostly substantive) that an earlier result in the session already contained. The run collapses to `⋯ N lines same as #12 · path:a–b ⋯`, citing the earlier program and, when the lines came from a file, the exact range, so one `read(path, a, n)` recovers them. Changed lines are never collapsed, so a re-read after an edit is exactly the delta. Lines the current program read with an explicit `offset`/`limit` are pinned and always shown. Programs are numbered (`ok #12 3ms`) to anchor the citations; the window is `seenWindow` programs (default 40) and resets on `session_start`. Not compression: every collapsed line already exists verbatim in the model's context. `/supernova` reports the session's returned vs. not-re-sent tokens.
45
+ - **Edits close the loop.** `edit` returns the post-edit lines with numbers (±2 context, ≤40 lines) so no verification re-read is needed; a quick structural check (`check: unclosed '{' opened at line 1`, JSON parsed exactly; brackets/strings/templates/regex-aware, 0 false positives over 6,100 source files); and `X also referenced in a.js:12, b.js:40` for every declaration the edit changed, so callers are not forgotten.
46
+ - **Failures carry their source.** A failing `bash` appends `--- source` with ±2 lines around each `path:line` it printed (≤4 sites), so a stack trace or test failure does not cost a read turn.
47
+ - **Outlines carry relations.** Expanded spans in `read(path, {about})` end with `// used by: a.js:12, b.js:40`.
48
+
49
+ Measured on a read→edit→verify→edit→verify→outline→outline loop: 20,967 tokens naive vs **3,098** (−85%); the verification re-read costs 49 tokens instead of 1,078.
50
+
51
+ ### Changed
52
+
53
+ - README documents the research (Zero-Mem, Agent Zero Memory, Harness-of-Harness, SPACE) and the fff port precisely: which formulas and constants are fff's, what was not ported, and that the code was reimplemented in JavaScript after reading fff's Rust sources.
54
+
55
+ ## [0.0.13] - 2026-09-04
56
+
57
+ ### Fixed
58
+
59
+ - A program that made no host calls rendered an empty frame (`╭─ nova: complete ─╮ / ╰──╯`); it is now a single status line. Expanded view still frames the returned value.
60
+
61
+ ## [0.0.12] - 2026-09-04
62
+
63
+ ### Added
64
+
65
+ - `read(path, {about})`: one call returns the whole file as an outline: every declaration (including nested object/class methods) with its signature and line range, and only the bodies relevant to `about` expanded, with line numbers, under a character budget (default 8000, 6 spans, weak-match cutoff at 40% of the best span). A folded body reads ` 75 function applyReplacements(target, content, edits) … 26 lines`, so the follow-up `read(path, 75, 26)` is known without another search. Across 5 files/questions: **65% fewer tokens** than reading the file (23,968 → 8,477), correct spans expanded.
66
+ - fff (dmtrKovalenko/fff) ported to plain JS in `fuzzy.js`, no binary and no spawn:
67
+ - `glob`/`find` with free text (no glob characters) is a typo-tolerant, frecency-ranked path search: up to 2 skipped characters, smart-case, +40%/+20% exact/any filename bonus, frecency boost `base·f/100` with fff's AI-mode decay (3-day half-life, 7-day window, 30s…4h modification steps), +15% for git-modified files, directory-distance penalty from the last touched file.
68
+ - `grep` is smart-case, groups rows under one path header, lists files that *declare* the name first with declaration lines marked `*` (definition-first hinting), accepts `limit`, and falls back to a fuzzy line match when the literal has no hits (`CausualVfs` → `class CausalVfs`).
69
+ - The workspace index watches the tree with `fs.watch` (recursive) and refreshes on change instead of every 10s; TTL remains the fallback when watching is unavailable.
70
+ - Structural surface detects indented methods (`async bash(params, signal) {`, `name: (a) => {`), so adapters and class members are their own spans for `evidence`, `snap`, and outlines.
71
+
72
+ ### Changed
73
+
74
+ - Tool guidance: `evidence(question)` across the repo or `read(path, {about})` for one file; plain `read(path)` only for lines you will edit.
75
+
5
76
  ## [0.0.11] - 2026-09-04
6
77
 
7
78
  ### Added
8
79
 
9
- - `evidence(query, {k?, path?, maxChars?})` — zero-token evidence selection over the codebase after Zero-Mem (arXiv:2607.29377), implemented 1:1 with the paper's non-generative pipeline: declared spans are the context units and identifiers the entities (eq.3), entity–span weights `w(d,e)=c(e,d)/Σc` (eq.4), file→span→line hierarchy (eq.5, eq.11), a deterministic query profile and relational/local route (eq.6–7), lexical entity alignment and one IDF-damped co-occurrence propagation step (eq.8–9), personalized PageRank `π=(1−γ)r+γPᵀπ` over spans (eq.10, γ=0.85, 10 iterations, factored through the entity layer so it is O(nnz)), per-view min-max normalisation and ρ-weighted fusion (eq.12–13, ρ=0.7), closure with definition bridges and in-file neighbours (eq.14), and calibration that filters by boundary/answer type/lexical support and ranks by type compatibility (eq.15). Returns top-K (default 5, per the paper's Top-5 ≈ Top-10 finding) verbatim source spans with path and line provenance under a 6000-char budget. Across 8 understanding questions on this repo the correct span ranks first and the result costs **68% fewer tokens** than reading the files the question spans (43,916 → 14,147). Warm latency 1–4ms.
80
+ - `evidence(query, {k?, path?, maxChars?})`: zero-token evidence selection over the codebase after Zero-Mem (arXiv:2607.29377), implemented 1:1 with the paper's non-generative pipeline: declared spans are the context units and identifiers the entities (eq.3), entity–span weights `w(d,e)=c(e,d)/Σc` (eq.4), file→span→line hierarchy (eq.5, eq.11), a deterministic query profile and relational/local route (eq.6–7), lexical entity alignment and one IDF-damped co-occurrence propagation step (eq.8–9), personalized PageRank `π=(1−γ)r+γPᵀπ` over spans (eq.10, γ=0.85, 10 iterations, factored through the entity layer so it is O(nnz)), per-view min-max normalisation and ρ-weighted fusion (eq.12–13, ρ=0.7), closure with definition bridges and in-file neighbours (eq.14), and calibration that filters by boundary/answer type/lexical support and ranks by type compatibility (eq.15). Returns top-K (default 5, per the paper's Top-5 ≈ Top-10 finding) verbatim source spans with path and line provenance under a 6000-char budget. Across 8 understanding questions on this repo the correct span ranks first and the result costs **68% fewer tokens** than reading the files the question spans (43,916 → 14,147). Warm latency 1–4ms.
10
81
  - Structural surface now records column-0 `const/let/var` bindings, so module-level tables are their own spans (also sharpens `snap`).
11
82
 
12
83
  ### Changed
@@ -37,7 +108,7 @@
37
108
  ### Changed
38
109
 
39
110
  - Result card redesigned as an aligned ledger: one row per call (status · tool · duration · `exit N` · `+a/-r` · target), no tree stems or spacer rows, durations humanized (`6.2s`), multi-line commands shown as their first line plus `…+N lines`, and paths fitted to width with the basename kept. Trace records now carry per-call `ms` and non-zero `exitCode`.
40
- - Guest programs run in a warm worker thread. A hard timeout or abort now terminates synchronous loops (`while (true) {}`), `process.exit()` only ends the program, and `maxHeapMb` (V8 `resourceLimits` plus a process-RSS watchdog for Bun) stops memory blow-ups — none of these can take the host down anymore. Warm-worker overhead is ~0.1ms per program and ~20µs per `nova.call`.
111
+ - Guest programs run in a warm worker thread. A hard timeout or abort now terminates synchronous loops (`while (true) {}`), `process.exit()` only ends the program, and `maxHeapMb` (V8 `resourceLimits` plus a process-RSS watchdog for Bun) stops memory blow-ups. None of these can take the host down anymore. Warm-worker overhead is ~0.1ms per program and ~20µs per `nova.call`.
41
112
  - Result text is a compact JS literal (`ok 12ms` header, unquoted keys, no separator whitespace, one item per line only past 120 columns): ~43% fewer tokens than the previous pretty JSON. Logs appear under `--- logs` only when present.
42
113
  - `maxReturnChars` default lowered from 200000 to 32000.
43
114
  - Return values that JSON cannot express are rendered instead of collapsing to `[object Object]`: circular references, `Map`, `Set`, `BigInt`, `Error`, functions, typed arrays.
@@ -96,7 +167,7 @@
96
167
 
97
168
  ### Fixed
98
169
 
99
- - OMP no longer hangs on "Loading plugins…" — removed top-level import of `@oh-my-pi/pi-coding-agent/tui` from the extension (portable framed chrome only).
170
+ - OMP no longer hangs on "Loading plugins…": removed top-level import of `@oh-my-pi/pi-coding-agent/tui` from the extension (portable framed chrome only).
100
171
 
101
172
  ## [0.0.3] - 2026-09-03
102
173
 
package/README.md CHANGED
@@ -12,7 +12,13 @@ pi install npm:pi-supernova
12
12
  omp install npm:pi-supernova
13
13
  ```
14
14
 
15
- Load **before** other tool-owning packages so `registerTool` capture works. Restart the host after install.
15
+ Restart the host after install. OMP uses the current session’s enabled tool registry, independent of extension load order.
16
+
17
+ Pi 0.85.0 supplies tool schemas but has no cross-extension execution API. Supernova uses native adapters and executors registered through its API instance.
18
+
19
+ An unrelated Pi extension’s tools are not callable through Supernova. OMP 18.1.10 supports those calls through its session registry.
20
+
21
+ The runtime, transaction, patch, output-limit, and renderer fixes share the same code on both hosts. Disabled configured tools are not callable.
16
22
 
17
23
  ---
18
24
 
@@ -25,7 +31,11 @@ Load **before** other tool-owning packages so `registerTool` capture works. Rest
25
31
  | Parallel reads | Ad hoc | `callMany` Auto / `parallel()` |
26
32
  | Hosts | Separate packages | Same tarball for Pi **and** OMP |
27
33
 
28
- Guest code runs as an `AsyncFunction` in a worker thread (same trust class as host `bash`) — not an OS/VM sandbox. Tool calls go through `nova.call` RPC to the host thread; a hard timeout, abort, `process.exit`, or a memory blow-up terminates the worker without touching the harness.
34
+ Guest code runs as an `AsyncFunction` in a new worker for each program. It has the same trust level as host `bash`.
35
+
36
+ The host parses the bounded source with Acorn before execution. Tool calls use RPC. Deadlines and cancellation cover worker startup, tool discovery, and execution.
37
+
38
+ A completed worker cannot supply callbacks or globals to another program. Concurrent programs have separate transactions, traces, cancellation signals, and call budgets.
29
39
 
30
40
  ---
31
41
 
@@ -46,13 +56,17 @@ async () => {
46
56
  }
47
57
  ```
48
58
 
49
- Globals: `nova` / `tools`, `parallel`, `pipeline`, `console`, plus shorthand `read` (path or path array), `write`, `edit`, `patch`, `evidence`, `surface`, `snap`, `bash`, and `exec`.
59
+ Globals: `nova`, `parallel`, `pipeline`, `console`, plus shorthand `read` (path or path array), `write`, `edit`, `patch`, `evidence`, `surface`, `snap`, `bash`, and `exec`.
60
+
61
+ Read discipline that keeps context small: `evidence(question)` across the repo, or `read(path, {about: question})` for one file (full structure, only relevant bodies expanded, ~65% fewer tokens than the file) → `read(path, offset, limit)` only for the lines you will edit.
50
62
 
51
- Read discipline that keeps context small: `surface(path)` (names only) → `evidence(question)` (the spans that answer it) → `read(path, offset, limit)` only for the lines you will edit.
63
+ Results never repeat what the model already saw: a run of lines from an earlier result collapses to `⋯ 23 lines same as #12 · host-bridge.js:40–62 ⋯`; changed lines always show; `read(path, a, n)` shows a cited range again. `edit` returns the post-edit lines, a structural check, and who else references a changed declaration; a failing `bash` attaches the source behind the `path:line` it printed. A read→edit→verify loop costs ~15% of the naive token count.
52
64
 
53
- The returned value is rendered as a compact JS literal (unquoted keys, one item per line only when a container exceeds 120 columns) and capped at `maxReturnChars`. Strings are returned raw. This costs ~43% fewer tokens than pretty JSON — return small shaped values, not raw file dumps.
65
+ File search is an in-process port of [fff](https://github.com/dmtrKovalenko/fff): `glob("hostbrdge")` finds `host-bridge.js` (typo-tolerant, frecency- and git-status-ranked), `grep` is smart-case with declaration lines first and a fuzzy fallback. None of it spawns a process.
54
66
 
55
- Unified Pi/OMP card — one aligned row per call (status · tool · duration · target) with bounded mutation diffs:
67
+ The returned value is rendered as a compact JS literal (unquoted keys, one item per line only when a container exceeds 120 columns) and capped at `maxReturnChars`. Strings are returned raw. This costs ~43% fewer tokens than pretty JSON. Return small shaped values, not raw file dumps.
68
+
69
+ Unified Pi/OMP card: one aligned row per call (status · tool · duration · target) with bounded mutation diffs.
56
70
 
57
71
  ```text
58
72
  ╭─── nova: 4 calls · 6.9s ─────────────────────────────────────╮
@@ -65,7 +79,9 @@ Unified Pi/OMP card — one aligned row per call (status · tool · duration ·
65
79
  ╰───────────────────────────────────────────────────────────────╯
66
80
  ```
67
81
 
68
- Multi-line commands show their first line plus a hidden-line count. Press Enter for a larger hunk budget, the full returned value, and logs.
82
+ Multi-line commands show their first line and a hidden-line count. Expand the card to show the complete bounded return value and logs.
83
+
84
+ Expanded text wraps at the terminal width without a preview-line limit. Operation lists and mutation diffs keep their separate 24-row budgets.
69
85
 
70
86
  ---
71
87
 
@@ -74,16 +90,23 @@ Multi-line commands show their first line plus a hidden-line count. Press Enter
74
90
  | API | Role |
75
91
  |-----|------|
76
92
  | `nova.search(query, limit?)` | Thin catalog hits |
77
- | `nova.describe(name)` | Parameter summary on demand |
93
+ | `nova.describe(name)` | Complete JSON input schema on demand; explicit failure when no usable schema is available |
78
94
  | `nova.call(name, args)` | Host tool or native adapter |
79
- | `nova.callMany([{name,args}])` | Auto parallel wave — iterable array with `.mode` / `.results` |
80
- | `nova.evidence(query, {k?, path?, maxChars?})` | Top-K source spans (path, lines, verbatim text) that answer a question — zero-token evidence selection after Zero-Mem; ~68% fewer tokens than reading the files |
95
+ | `nova.callMany([{name,args}])` | Auto parallel wave; iterable array with `.mode` / `.results` |
96
+ | `nova.evidence(query, {k?, path?, maxChars?})` | Top-K source spans (path, lines, verbatim text) that answer a question. Zero-token evidence selection after Zero-Mem; ~68% fewer tokens than reading the files |
97
+ | `read(path, {about})` | Whole-file outline with only the relevant bodies expanded; folded bodies show `line … N lines` |
81
98
  | `nova.surface(path)` | Structural outline for a source file |
82
99
  | `nova.snap(query, searchRoot?)` | Defining file (workspace-relative), line, signature, confidence, and context for a concept; served from the in-process index in well under 1ms |
83
100
  | `nova.has(name)` | Whether a catalog or native tool is callable (sync) |
84
- | `parallel(thunks)` / `pipeline(items, …stages)` | Raw `Promise.all` helpers |
101
+ | `parallel(thunks)` / `pipeline(items, …stages)` | Array-based helpers; pipeline stages must be functions |
85
102
  | `nova.speculate(fn)` | Counterfactual branch (rollback / commit) |
86
103
 
104
+ `nova.call` returns an explicit `{ok, value}` envelope. Convenience helpers throw when a host tool reports failure.
105
+
106
+ `callMany` runs known read-only tools concurrently. Unknown tools and mutating actions run in order, including LSP rename operations.
107
+
108
+ `nova.describe` preserves required fields, unions, enums, nested objects, and numeric constraints. OMP ArkType and Zod schemas convert to JSON Schema.
109
+
87
110
  Root Snap searches ignore hidden files. Passing a hidden search root includes hidden files beneath that root; Git metadata is always excluded.
88
111
 
89
112
  ---
@@ -101,16 +124,23 @@ Optional `~/.pi/agent/supernova.json` or `~/.omp/agent/supernova.json`
101
124
  "maxCallResultChars": 65536,
102
125
  "maxReturnChars": 32000,
103
126
  "maxHeapMb": 512,
127
+ "seenWindow": 40,
104
128
  "maxSearchResults": 12,
105
129
  "spillDir": null
106
130
  }
107
131
  ```
108
132
 
109
- `maxHeapMb` caps the guest worker heap (V8 `resourceLimits` on Node) and arms a process-RSS watchdog that terminates a runaway program on both Node and Bun.
133
+ `maxCallResultChars` bounds text per call. A batch shares one text budget across all items; it does not repeat a joined copy.
110
134
 
111
- Defaults also set `excludeTools` (includes `supernova` and DCE helpers). An empty `"excludeTools": []` **replaces** those defaults — omit the key unless you mean that.
135
+ Envelope fields and JSON encoding add overhead. Details have a separate 2,000-character JSON budget. Truncation flags cover returns, raw objects, and logs.
112
136
 
113
- Slash: `/supernova` — catalog size + captured executors.
137
+ An optional `spillDir` retains complete truncated text. Inline spill footers and truncation markers fit within the configured text limit.
138
+
139
+ `seenWindow` is how many programs back the seen-ledger remembers (set 0 to disable collapsing). `maxHeapMb` caps the guest worker heap (V8 `resourceLimits` on Node) and arms a process-RSS watchdog that terminates a runaway program on both Node and Bun.
140
+
141
+ Defaults also set `excludeTools` (includes `supernova` and DCE helpers). An empty `"excludeTools": []` **replaces** those defaults, so omit the key unless you mean that.
142
+
143
+ Slash command `/supernova`: callable catalog size, external tools, native adapters, and session token stats.
114
144
 
115
145
  ---
116
146
 
@@ -135,20 +165,41 @@ Pair with DCE last if you use it: `omp install npm:pi-deferred-context-engine`.
135
165
 
136
166
  | Symptom | Fix |
137
167
  |---------|-----|
138
- | `unknown tool "…"` | Follow the `Did you mean` hint, or `nova.search("")`; for host tools install supernova **first**; restart; `/supernova` |
139
- | `Rendered line exceeds terminal width` | ≥0.0.1 and restart so `render.js` reloads |
140
- | `callMany` / not iterable | ≥0.0.1 — return is an array with `.mode` / `.results` |
168
+ | `unknown tool "…"` | Use `nova.search("")` and `/supernova`. Enable the tool in the current session. Restart after package changes. |
169
+ | `Rendered line exceeds terminal width` | Install the current package and restart so the Unicode width code reloads. |
170
+ | `callMany` / not iterable | ≥0.0.1; the return is an array with `.mode` / `.results` |
141
171
  | Extension missing on OMP | `omp install npm:pi-supernova` (needs `"omp".extensions`) |
142
172
 
143
173
  ---
144
174
 
145
175
  ## Limitations
146
176
 
147
- - Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`. The worker only contains hangs, exits, and memory — not intent.
177
+ - Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`. The worker only contains hangs, exits, and memory, not intent.
148
178
  - Guest error messages carry `(line:col)` on Node; Bun's engine does not expose guest-relative positions.
149
- - `bash` / mutating tools flush speculative writes (transaction barrier); error rollback cannot undo that.
150
- - The workspace index refreshes its file list every 10s or on any supernova mutation; a file created by an external process can take up to 10s to appear in `glob`/`snap` (`read` is never stale).
151
- - Pre-1.0 package — APIs and TUI may still evolve between minor releases.
179
+
180
+ ## Transactions and file freshness
181
+
182
+ - `bash` and mutating host tools commit pending writes before execution. Later rollback cannot undo those changes or external effects.
183
+ - Native commits stage replacements and backups before installation. A commit failure restores earlier replacements; a recovery failure reports retained backup paths.
184
+ - A nested `nova.speculate` branch cannot call external mutators. Await each branch before returning.
185
+ - Native reads use current disk content unless a staged write replaces it. Evidence search includes new staged files.
186
+ - Workspace file-list updates use filesystem watchers. Without a working watcher, external new files can take 10 seconds to appear in indexed searches.
187
+
188
+ This is a pre-1.0 package. APIs and the terminal display can change between minor releases.
189
+
190
+ ## Research and prior art
191
+
192
+ Supernova's retrieval and result shaping implement published methods. Where a paper's mechanism needs a model call it stays out of the tool; only the deterministic parts are implemented, and each is cited at the code that implements it.
193
+
194
+ | Work | What we use it for | Where |
195
+ |------|--------------------|-------|
196
+ | **Zero-Mem: Zero-Token Memory Operations for LLM Agents**, Xiao, Zhu, Zhang, Chen, Hong, Zhuang, Zhang, Chen, Ouyang, Ren, Huang (arXiv:2607.29377) | `evidence(query)`: entity–context graph with co-occurrence weights (eq. 3–4), turn/window/episode hierarchy as line/span/file (eq. 5, 11), query profile and relational/local routing (eq. 6–7), lexical entity alignment and one propagation step (eq. 8–9), personalized PageRank over spans (eq. 10), per-view normalisation and ρ-weighted fusion (eq. 12–13), closure with bridges and neighbours (eq. 14), deterministic calibration (eq. 15). Top-K = 5 follows the paper's Top-5 ≈ Top-10 finding. | `evidence.js` |
197
+ | **Agent Zero Memory: Provenance-Aware Long-Term Memory for LLM Agents**, Zhu, Wu (arXiv:2608.29606) | Every returned unit carries provenance (path, line range, verbatim text); the L0→L1→L2 read discipline (`surface` → `evidence`/`read(path, {about})` → `read(path, offset, limit)`); the citation-lock idea that a model should only cite what it actually opened. | `evidence.js`, `outline.js`, tool guidance |
198
+ | **Harness-of-Harness: Multi-Day Autonomous Software Development with Continual Improvement**, Yan, Su, et al. (arXiv:2609.01481) | Progressive disclosure (index first, detail on demand) and carrying evidence forward instead of reconstructing it from code. | outline / result shaping |
199
+ | **Act More, Decide Less: Skill-Guided Adaptive Action Chunking for Long-Horizon LLM Agents**, Yang, Jin, Zhao, et al. (arXiv:2609.02042) | Framing: one supernova program is an action chunk (one model decision, many primitive actions, stop at the first failing one). | runtime design |
200
+ | **fff**, Dmitriy Kovalenko, MIT, <https://github.com/dmtrKovalenko/fff> | File search. We reimplemented fff's ranking in plain JavaScript after reading its Rust sources (`crates/fff-core/src/score.rs`, `dbs/frecency.rs`, `path_utils.rs`); the formulas and constants are fff's, the code is ours, and nothing runs out of process. Ported: typo-tolerant fuzzy path matching with boundary/consecutive/case bonuses and smart-case; exact-filename +40% and filename +20% bonuses; frecency boost `base·f/100` with fff's AI-mode decay (3-day half-life, 7-day window) and modification-recency steps (30s/5m/15m/1h/4h); git-modified +15%; directory-distance penalty from the current file (−1 per hop, floor −20); definition-first result hinting; fuzzy fallback on zero literal matches; weak-match cutoff; watcher-driven index refresh. Not ported: fff's SIMD/frizbee matcher (ours is an fzf-style greedy match with backward tightening), LMDB persistence (frecency is per session), and the MCP/Neovim surfaces. | `fuzzy.js`, `repo-index.js`, `host-bridge.js` |
201
+
202
+ fff is © Dmitriy Kovalenko and contributors, released under the MIT License; this package is also MIT. If you install fff's own Pi extension (`@ff-labs/pi-fff`) alongside supernova, its `ffgrep`/`fffind` tools are captured and callable through `nova.call` like any other host tool.
152
203
 
153
204
  ## License
154
205
 
package/bottleneck.js CHANGED
@@ -4,120 +4,121 @@ import { randomUUID } from "node:crypto";
4
4
  import { isString, isObject } from "./decode.js";
5
5
  import { truncateChars, formatValue } from "./format.js";
6
6
 
7
- export function serializeBounded(value, maxChars, label = "value") {
8
- let serialized;
9
- try {
10
- serialized = isString(value) ? value : JSON.stringify(value);
11
- } catch {
12
- serialized = String(value);
13
- }
14
- if (serialized === undefined) serialized = "null";
15
- return truncateChars(serialized, maxChars, label);
7
+ function json(value) {
8
+ try { return JSON.stringify(value) ?? "null"; } catch { return JSON.stringify(String(value)); }
16
9
  }
17
-
18
- function extractContentText(content) {
19
- if (!Array.isArray(content)) return "";
20
- return content
21
- .filter((part) => part && isObject(part) && part.type === "text" && isString(part.text))
22
- .map((part) => part.text)
23
- .join("\n");
10
+ function detailsOf(raw) {
11
+ const details = raw?.details;
12
+ if (!isString(details)) return details;
13
+ try { return JSON.parse(details); } catch { return details; }
24
14
  }
25
-
26
- function extractRawString(raw, maxChars) {
15
+ export function hostResultFailed(raw) {
16
+ const details = detailsOf(raw);
17
+ return raw?.isError === true || details?.ok === false || (Number.isInteger(details?.exitCode) && details.exitCode !== 0);
18
+ }
19
+ function extractRawString(raw) {
27
20
  if (raw == null) return "";
28
21
  if (isString(raw)) return raw;
29
22
  if (!isObject(raw)) return String(raw);
30
- if (Array.isArray(raw.content)) return extractContentText(raw.content);
23
+ if (Array.isArray(raw.content)) return raw.content.filter(part => part?.type === "text" && isString(part.text)).map(part => part.text).join("\n");
31
24
  if (isString(raw.text)) return raw.text;
32
- return serializeBounded(raw, maxChars, "host-result").text;
33
- }
34
-
35
- function summarizeDetails(details, maxChars) {
36
- return serializeBounded(details, maxChars, "details").text;
25
+ return json(raw);
37
26
  }
38
27
 
39
- function maybeSpill(cappedText, fullText, config) {
40
- if (fullText.length <= (config.maxCallResultChars ?? 65536)) return null;
41
- if (!isString(config.spillDir) || config.spillDir.length === 0) return null;
42
- try {
43
- const dir = config.spillDir;
44
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
45
- const filePath = path.join(dir, `${Date.now()}-${randomUUID().slice(0, 8)}.txt`);
46
- fs.writeFileSync(filePath, fullText, { encoding: "utf8", mode: 0o600 });
47
- return { pointer: filePath };
48
- } catch {
49
- return null;
50
- }
51
- }
52
-
53
- function batchItems(details, maxChars) {
54
- if (!isObject(details) || details.batch !== true) return undefined;
55
- if (!Array.isArray(details.items)) return undefined;
56
- return details.items.map((item) => truncateChars(item, maxChars, "host-result").text);
28
+ /** Bound JSON before serialization; preserve small scalar fields such as exitCode. */
29
+ function summarizeDetails(value, budget = 2000) {
30
+ const encoded = json(value);
31
+ if (encoded.length <= budget) return encoded;
32
+ const snapshot = JSON.parse(encoded);
33
+ const fit = (input, limit) => {
34
+ const serialized = json(input);
35
+ if (serialized.length <= limit) return input;
36
+ if (isString(input)) {
37
+ let low = 0;
38
+ let high = Math.min(input.length, limit);
39
+ while (low < high) {
40
+ const mid = Math.ceil((low + high) / 2);
41
+ if (json(truncateChars(input, mid).text).length <= limit) low = mid;
42
+ else high = mid - 1;
43
+ }
44
+ return truncateChars(input, low).text;
45
+ }
46
+ if (!isObject(input) && !Array.isArray(input)) return null;
47
+ const out = Array.isArray(input) ? [] : { truncated: true };
48
+ const entries = Object.entries(input);
49
+ if (!Array.isArray(input)) entries.sort((a, b) => json(a[1]).length - json(b[1]).length);
50
+ for (const [key, child] of entries) {
51
+ const used = json(out).length;
52
+ const overhead = Array.isArray(out) ? 1 : json(key).length + 2;
53
+ const available = limit - used - overhead;
54
+ if (available < 4) break;
55
+ const bounded = fit(child, available);
56
+ if (Array.isArray(out)) out.push(bounded);
57
+ else Object.defineProperty(out, key, { value: bounded, enumerable: true, configurable: true });
58
+ if (json(out).length > limit) {
59
+ if (Array.isArray(out)) out.pop();
60
+ else delete out[key];
61
+ }
62
+ }
63
+ return out;
64
+ };
65
+ return json(fit(snapshot, budget));
57
66
  }
58
67
 
59
- function spillSuffix(capped, text, config) {
60
- const spill = maybeSpill(capped.text, text, config);
61
- if (!spill?.pointer) return { spill, value: capped.text };
62
- return { spill, value: `${capped.text}\n\n[full output spilled to ${spill.pointer}]` };
68
+ function spill(fullText, config) {
69
+ if (!isString(config.spillDir) || !config.spillDir) return undefined;
70
+ fs.mkdirSync(config.spillDir, { recursive: true, mode: 0o700 });
71
+ const file = path.join(config.spillDir, Date.now() + "-" + randomUUID().slice(0, 8) + ".txt");
72
+ fs.writeFileSync(file, fullText, { encoding: "utf8", mode: 0o600, flag: "wx" });
73
+ return file;
63
74
  }
64
75
 
65
76
  export function packageHostResult(raw, config) {
66
77
  const maxChars = config.maxCallResultChars ?? 65536;
67
- const isError = isObject(raw) && raw.isError === true;
68
- const details = isObject(raw) ? raw.details : undefined;
69
- const upstreamTruncated = isObject(details) && details.outputTruncated === true;
70
- const text = extractRawString(raw, maxChars);
71
- const items = batchItems(details, maxChars);
72
- // Batch items travel as their own field; keep the details summary small and parseable.
73
- const summarizedDetails =
74
- details === undefined ? undefined : summarizeDetails(items ? { ...details, items: undefined } : details, 2000);
75
-
78
+ const details = detailsOf(raw);
79
+ const batch = details?.batch === true && Array.isArray(details.items) ? details.items : undefined;
80
+ const text = batch ? "" : extractRawString(raw);
76
81
  const capped = truncateChars(text, maxChars, "host-result");
77
- if (!capped.truncated) {
78
- return {
79
- ok: !isError,
80
- value: capped.text,
81
- truncated: upstreamTruncated,
82
- details: summarizedDetails,
83
- items,
84
- };
82
+ let truncated = capped.truncated || details?.outputTruncated === true;
83
+ const result = { ok: !hostResultFailed(raw), value: capped.text, truncated };
84
+ if (details !== undefined) result.details = summarizeDetails(batch ? { ...details, items: undefined } : details);
85
+ if (batch) {
86
+ let remaining = maxChars;
87
+ result.items = batch.map((item, index) => {
88
+ const bounded = truncateChars(item, Math.floor(remaining / (batch.length - index)), "host-result");
89
+ remaining -= bounded.text.length;
90
+ truncated ||= bounded.truncated;
91
+ return bounded.text;
92
+ });
85
93
  }
86
-
87
- const { spill, value } = spillSuffix(capped, text, config);
88
-
89
- return {
90
- ok: !isError,
91
- value,
92
- truncated: true,
93
- originalChars: capped.originalChars,
94
- spill: spill?.pointer,
95
- details: summarizedDetails,
96
- items,
97
- };
98
- }
99
-
100
- function clipLogs(logs, config) {
101
- const logLines = Array.isArray(logs) ? logs : [];
102
- const maxLogLines = config.maxLogLines ?? 100;
103
- const maxLogLineChars = config.maxLogLineChars ?? 4096;
104
- const clippedLogs = logLines.slice(0, maxLogLines).map((line) => {
105
- const s = isString(line) ? line : String(line);
106
- if (s.length <= maxLogLineChars) return s;
107
- return s.slice(0, maxLogLineChars) + "…";
108
- });
109
- return { clippedLogs, logTruncated: logLines.length > maxLogLines };
94
+ result.truncated = truncated;
95
+ if (truncated) {
96
+ result.originalChars = batch ? batch.reduce((sum, item) => sum + String(item).length, 0) : text.length;
97
+ if (config.spillDir) {
98
+ const pointer = spill(batch ? batch.join("\n---\n") : text, config);
99
+ if (pointer) {
100
+ result.spill = pointer;
101
+ if (!batch) {
102
+ const footer = "\n[full output spilled to " + pointer + "]";
103
+ result.value = footer.length <= maxChars
104
+ ? truncateChars(text, maxChars - footer.length, "host-result").text + footer
105
+ : capped.text;
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return result;
110
111
  }
111
112
 
112
113
  export function packageFinalReturn(value, logs, config) {
113
- const maxReturn = config.maxReturnChars ?? 32000;
114
- const serialized = truncateChars(isString(value) ? value.replace(/\n+$/, "") : formatValue(value), maxReturn, "return");
115
- const { clippedLogs, logTruncated } = clipLogs(logs, config);
116
- return {
117
- returnValue: serialized.truncated ? serialized.text : value,
118
- returnText: serialized.text,
119
- returnTruncated: serialized.truncated,
120
- logs: clippedLogs,
121
- logTruncated,
122
- };
114
+ const serialized = truncateChars(isString(value) ? value : formatValue(value), config.maxReturnChars ?? 32000, "return");
115
+ const maxLines = config.maxLogLines ?? 100;
116
+ let logTruncated = logs.length > maxLines;
117
+ const clipped = logs.slice(0, maxLines).map(line => {
118
+ const result = truncateChars(line, config.maxLogLineChars ?? 4096, "log");
119
+ logTruncated ||= result.truncated;
120
+ return result.text;
121
+ });
122
+ return { returnValue: serialized.truncated ? serialized.text : value, returnText: serialized.text,
123
+ returnTruncated: serialized.truncated, logs: clipped, logTruncated };
123
124
  }
package/catalog.js CHANGED
@@ -1,15 +1,16 @@
1
1
 
2
2
  import { isString, isObject } from "./decode.js";
3
3
 
4
- export const NATIVE_TOOL_DEFINITIONS = [
4
+ const NATIVE_TOOL_DEFINITIONS = [
5
5
  {
6
6
  name: "read",
7
7
  description: "Read UTF-8 workspace files by path, or resolve a concept query to source. Supports path arrays, offset, and limit.",
8
8
  parameters: { type: "object", properties: {
9
- path: { type: "string", description: "Workspace-relative file path or concept query" },
9
+ path: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], description: "Workspace-relative file path, concept query, or array of paths" },
10
10
  target: { anyOf: [{ type: "string" }, { type: "array" }], description: "File path/query or array of paths" },
11
11
  offset: { type: "number", description: "One-based starting line" },
12
12
  limit: { type: "number", description: "Maximum lines to return" },
13
+ about: { type: "string", description: "Question or symbol: returns the whole file as an outline with only the relevant bodies expanded" },
13
14
  } },
14
15
  },
15
16
  {
@@ -17,9 +18,9 @@ export const NATIVE_TOOL_DEFINITIONS = [
17
18
  parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] },
18
19
  },
19
20
  {
20
- name: "edit", description: "Apply unique text replacements, or a unified diff, to a workspace file.",
21
+ name: "edit", description: "Apply unique text replacements to a workspace file; returns the post-edit lines, a structural check, and references to changed declarations.",
21
22
  parameters: { type: "object", properties: {
22
- path: { type: "string" }, oldText: { type: "string" }, newText: { type: "string" }, edits: { type: "array" }, patch: { type: "string" },
23
+ path: { type: "string" }, oldText: { type: "string" }, newText: { type: "string" }, edits: { type: "array", description: "[{oldText, newText}] for several replacements in one call" },
23
24
  }, required: ["path"] },
24
25
  },
25
26
  {
@@ -51,13 +52,13 @@ export const NATIVE_TOOL_DEFINITIONS = [
51
52
  parameters: { type: "object", properties: { command: { type: "string" }, cwd: { type: "string" }, timeoutMs: { type: "number" } }, required: ["command"] },
52
53
  },
53
54
  {
54
- name: "grep", description: "Search workspace file contents by pattern.",
55
+ name: "grep", description: "Search file contents. Smart-case regex; definition lines first (marked *); fuzzy fallback when nothing matches literally.",
55
56
  parameters: { type: "object", properties: {
56
- pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" }, caseSensitive: { type: "boolean" },
57
+ pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" }, caseSensitive: { type: "boolean" }, limit: { type: "number" },
57
58
  }, required: ["pattern"] },
58
59
  },
59
60
  {
60
- name: "glob", description: "List workspace files matching a glob pattern.",
61
+ name: "glob", description: "Find files: a glob pattern, or free text for typo-tolerant, frecency-ranked path search.",
61
62
  parameters: { type: "object", properties: { pattern: { type: "string" } }, required: ["pattern"] },
62
63
  },
63
64
  {
@@ -109,6 +110,7 @@ function normalizeTool(tool) {
109
110
  description,
110
111
  descLower: description.toLowerCase(),
111
112
  parameters: tool.parameters,
113
+ schemaError: tool.schemaError,
112
114
  sourcePath: sourcePathOf(tool),
113
115
  };
114
116
  }
@@ -157,33 +159,6 @@ export function searchCatalog(catalog, query, limit = 12) {
157
159
  return scored.slice(0, Math.max(1, limit)).map(({ score: _s, ...hit }) => hit);
158
160
  }
159
161
 
160
- function fieldSummary(key, schema, required) {
161
- const s = schema && isObject(schema) ? schema : {};
162
- // Only signal-bearing keys: `required:false` and empty descriptions cost tokens and say nothing.
163
- return {
164
- type: s.type || (Array.isArray(s.anyOf) ? "union" : "unknown"),
165
- required: required.has(key) || undefined,
166
- description: isString(s.description) ? s.description.slice(0, 120) : undefined,
167
- };
168
- }
169
-
170
- function schemaSummary(parameters) {
171
- if (!parameters || !isObject(parameters)) return { type: "unknown" };
172
- const props = parameters.properties;
173
- if (!props || !isObject(props)) {
174
- return {
175
- type: parameters.type || "object",
176
- note: "schema present (no enumerable properties)",
177
- };
178
- }
179
- const required = new Set(Array.isArray(parameters.required) ? parameters.required : []);
180
- const fields = {};
181
- for (const [key, schema] of Object.entries(props)) {
182
- fields[key] = fieldSummary(key, schema, required);
183
- }
184
- return { type: "object", fields };
185
- }
186
-
187
162
  /** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
188
163
  function editDistance(a, b) {
189
164
  const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...new Array(b.length).fill(0)]);
@@ -200,7 +175,7 @@ function editDistance(a, b) {
200
175
  }
201
176
 
202
177
  /** Closest tool names for a mistyped name: substring hits first, then a length-scaled edit distance. */
203
- export function suggestNames(name, candidates, limit = 3) {
178
+ function suggestNames(name, candidates, limit = 3) {
204
179
  const needle = String(name || "").toLowerCase();
205
180
  if (!needle) return [];
206
181
  const maxDistance = Math.max(1, Math.floor(needle.length / 3));
@@ -231,12 +206,13 @@ export function describeTool(catalog, name) {
231
206
  if (!row) {
232
207
  return { ok: false, error: unknownToolMessage(name, catalog.map((t) => t.name)) };
233
208
  }
209
+ if (!isObject(row.parameters)) return { ok: false, name, error: "tool schema unavailable: " + (row.schemaError ?? name) };
234
210
  if (!row._described) {
235
211
  row._described = {
236
212
  ok: true,
237
213
  name: row.name,
238
214
  description: row.description,
239
- parameters: schemaSummary(row.parameters),
215
+ parameters: row.parameters,
240
216
  sourcePath: row.sourcePath,
241
217
  };
242
218
  }