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 +74 -3
- package/README.md +72 -21
- package/bottleneck.js +98 -97
- package/catalog.js +12 -36
- package/check.js +166 -0
- package/config.default.json +1 -0
- package/config.js +1 -2
- package/decode.js +57 -6
- package/evidence.js +27 -35
- package/format.js +16 -17
- package/fuzzy.js +182 -0
- package/guest-worker.js +38 -157
- package/host-bridge.js +246 -124
- package/index.js +95 -123
- package/ledger.js +150 -0
- package/omp-frame.js +7 -23
- package/outline.js +80 -0
- package/package.json +11 -2
- package/parallel.js +42 -34
- package/patch.js +62 -72
- package/render-measure.js +46 -144
- package/render.js +43 -158
- package/repo-index.js +106 -11
- package/runtime.js +204 -227
- package/search.js +141 -0
- package/snap.js +1 -6
- package/surface.js +22 -13
- package/vfs.js +114 -75
- package/workspace.js +51 -33
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?})
|
|
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
|
|
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…"
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)` |
|
|
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
|
|
80
|
-
| `nova.evidence(query, {k?, path?, maxChars?})` | Top-K source spans (path, lines, verbatim text) that answer a question
|
|
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)` |
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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 "…"` |
|
|
139
|
-
| `Rendered line exceeds terminal width` |
|
|
140
|
-
| `callMany` / not iterable | ≥0.0.1
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
19
|
-
if (!
|
|
20
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function summarizeDetails(details, maxChars) {
|
|
36
|
-
return serializeBounded(details, maxChars, "details").text;
|
|
25
|
+
return json(raw);
|
|
37
26
|
}
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
68
|
-
const
|
|
69
|
-
const
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
215
|
+
parameters: row.parameters,
|
|
240
216
|
sourcePath: row.sourcePath,
|
|
241
217
|
};
|
|
242
218
|
}
|