better-dsh 0.2.4-c → 0.2.5-a
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/cordis.patch.yml +11 -0
- package/docs/50_test-reports/2026-09-28-ctx-scheme/351/225/277/344/274/232/350/257/235utility/345/256/236/346/265/213/346/212/245/345/221/212.md +155 -0
- package/docs/50_test-reports/2026-10-01-v0.2.4d-web-password-gate/345/256/236/346/265/213/346/212/245/345/221/212.md +105 -0
- package/docs/50_test-reports/2026-10-02-ctx-navigation/345/256/236/346/265/213/346/212/245/345/221/212.md +137 -0
- package/docs/50_test-reports/2026-10-02-v0.2.0-rc.2/345/257/271/351/275/220/350/275/256/345/256/236/346/265/213/346/212/245/345/221/212.md +38 -0
- package/docs/specs/ctx/spec.md +78 -6
- package/docs/specs/model-picker-ios-blur/spec.md +168 -0
- package/docs/superpowers/plans/2026-10-02-ctx-navigation.md +319 -0
- package/docs/superpowers/specs/2026-10-02-ctx-navigation-design.md +209 -0
- package/lib/fs-aware/sandbox-plugin.js +2 -2
- package/lib/url-schemes/index.js +194 -37
- package/lib/web-password.d.ts +156 -0
- package/lib/web-password.js +562 -0
- package/lib/{wrap-Dr8XtIak.js → wrap-Bzdimp4p.js} +1 -1
- package/package.json +18 -3
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# ctx:// Long-Session Navigability — Design
|
|
2
|
+
|
|
3
|
+
> Status: approved (user, 2026-10-02). Implements backlog N1–N6 plus findings F1/F2 from
|
|
4
|
+
> `docs/50_test-reports/2026-09-28-ctx-scheme长会话utility实测报告.md` §八.
|
|
5
|
+
> Live spec delta: `docs/specs/ctx/spec.md`. Plan: `docs/superpowers/plans/2026-10-02-ctx-navigation.md`.
|
|
6
|
+
|
|
7
|
+
## 1. Scope ruling (carried from the report, §八)
|
|
8
|
+
|
|
9
|
+
This tool family is **text-keyword recallable navigation** — the phone book + index + pointer
|
|
10
|
+
for a long session. Semantics is out of scope: no embeddings, no semantic ranking, no
|
|
11
|
+
"fuzzy" retrieval. The model is a language model; synonym/substitution/keyword work is its
|
|
12
|
+
job. We guarantee only: **原文可取 + 可寻址** (原文 retrievable, and addressable).
|
|
13
|
+
|
|
14
|
+
Every number this scheme shows the model MUST either be directly usable as a selector in the
|
|
15
|
+
same URL family, or be explicitly labeled metadata with its unit. A bare, unlabeled number
|
|
16
|
+
that *looks* like a coordinate but is not addressable is the failure mode this change
|
|
17
|
+
eliminates.
|
|
18
|
+
|
|
19
|
+
## 2. Coordinate model (the spine)
|
|
20
|
+
|
|
21
|
+
Two coordinate systems, both labeled wherever shown:
|
|
22
|
+
|
|
23
|
+
| Unit | Meaning | Used by |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `seq` | event sequence number in the session log | `[<n\|seq>]` element/label addressing |
|
|
26
|
+
| `line` | transcript line number (1-based) | `:N-M` windows and `grep … ctx://session/transcript` |
|
|
27
|
+
|
|
28
|
+
The transcript is the **canonical coordinate space**. `grep` on `ctx://session/transcript`
|
|
29
|
+
materializes the transcript and returns transcript line numbers; therefore every exposed
|
|
30
|
+
`lines=` figure is a transcript line and pastes directly into `:N-M`, and episode windows are
|
|
31
|
+
transcript-relative.
|
|
32
|
+
|
|
33
|
+
**Decision (user, 2026-10-02): transcript-relative everywhere.** `compactions[label]:5300-5310`
|
|
34
|
+
means transcript lines 5300–5310, identical to `transcript:5300-5310`. The first episode starts
|
|
35
|
+
at transcript line 1, so episode-relative and transcript-relative coincide for it.
|
|
36
|
+
|
|
37
|
+
## 3. Data model changes (in `better-dsh/src/url-schemes/handlers/ctx.ts`)
|
|
38
|
+
|
|
39
|
+
### 3.1 Transcript line index (structural, N1)
|
|
40
|
+
|
|
41
|
+
Build the transcript **once** and carry per-entry line ranges, instead of the current two
|
|
42
|
+
independent renders (`transcript()` and each episode's `originalText`):
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
interface TranscriptIndex {
|
|
46
|
+
text: string // the joined transcript (canonical face)
|
|
47
|
+
totalLines: number
|
|
48
|
+
lineOfSeq: Map<number, { start: number; end: number }> // per rendered entry, transcript lines
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildTranscript(events, toolNameOf): TranscriptIndex {
|
|
52
|
+
const entries = events
|
|
53
|
+
.map(e => ({ seq: e.seq as number, text: renderEntry(e, toolNameOf) }))
|
|
54
|
+
.filter(t => t.text !== '')
|
|
55
|
+
const lineOfSeq = new Map()
|
|
56
|
+
let line = 1
|
|
57
|
+
for (let i = 0; i < entries.length; i++) {
|
|
58
|
+
const n = entries[i].text.split('\n').length
|
|
59
|
+
lineOfSeq.set(entries[i].seq, { start: line, end: line + n - 1 })
|
|
60
|
+
line += n + (i < entries.length - 1 ? 1 : 0) // one blank separator line between entries
|
|
61
|
+
}
|
|
62
|
+
return { text: entries.map(e => e.text).join('\n\n'), totalLines: line - 1, lineOfSeq }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The episode's canonical span is then **derived** from the index (guarantees the invariant by
|
|
67
|
+
construction):
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// episode lines = min/max transcript line over the shadowed events that rendered
|
|
71
|
+
const lineStart = min(lineOfSeq.get(seq).start for shadowed seqs that rendered)
|
|
72
|
+
const lineEnd = max(lineOfSeq.get(seq).end for shadowed seqs that rendered)
|
|
73
|
+
const originalText = transcript.text.split('\n').slice(lineStart - 1, lineEnd).join('\n')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If no shadowed event rendered (degenerate), the episode has an empty span (`lines: null`).
|
|
77
|
+
|
|
78
|
+
### 3.2 Episode shape
|
|
79
|
+
|
|
80
|
+
Each episode gains `lines: { start, end } | null`, `fidelity: { userTurns, toolCalls }`, and a
|
|
81
|
+
computed `landmarks` block. The manifest line, the snapshot `compacted[]` entry, the snapshot
|
|
82
|
+
`segments[]` entry, and the episode prepared face all carry `lines=` and `seq=` labeled figures.
|
|
83
|
+
|
|
84
|
+
### 3.3 `resultTextOf` shape robustness (F2)
|
|
85
|
+
|
|
86
|
+
`tool/result` messages in the current session format store **flat** text blocks
|
|
87
|
+
(`data.message.content: [{type:"text", text:"…"}]`); the nested `tool-result` block form is
|
|
88
|
+
the older shape. Today `resultTextOf` only flattens the nested form, so every result renders
|
|
89
|
+
empty. Fix: prefer nested, fall back to flat:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
function resultTextOf(event: PersistenceEvent): string {
|
|
93
|
+
const blocks = blocksOf(event)
|
|
94
|
+
const nested = blocks
|
|
95
|
+
.flatMap(b => (b as { content?: ContentBlock[] }).content ?? [])
|
|
96
|
+
.map(c => c.text ?? '').join('\n')
|
|
97
|
+
if (nested !== '') return nested
|
|
98
|
+
return textOf(blocks) // flat text blocks (current session format)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 4. Backlog implementation
|
|
103
|
+
|
|
104
|
+
### N1 — addressable line coordinates + explicit bounds (F7/F10)
|
|
105
|
+
|
|
106
|
+
- Manifest line per episode: `seq=<start>..<end> (N items, T tokens)` **and**
|
|
107
|
+
`lines=<lineStart>..<lineEnd>` (or `lines=<none>`), plus `fidelity=ut:tc`.
|
|
108
|
+
- Snapshot: `segments[]` → `{ segment, seq_start, seq_end, lines: {start,end}|null, items }`;
|
|
109
|
+
`compacted[]` gains `lines`, `fidelity`.
|
|
110
|
+
- Episode prepared face header prints `lines=` and `seq=` labeled.
|
|
111
|
+
- Episode line windows are transcript-relative and **clamped with an explicit boundary note**
|
|
112
|
+
when a requested range reaches past the span (never silent-empty):
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// transcript coords -> episode-local coords
|
|
116
|
+
function applyEpisodeLines(slice, lineStart, lineEnd, ranges): string
|
|
117
|
+
// out-of-span tail appends: [ctx:// note: episode span is transcript lines <s>-<e>]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A window entirely outside the span returns the note alone (not empty). `:raw` still returns
|
|
121
|
+
the full span.
|
|
122
|
+
|
|
123
|
+
### N2 — digest residence rule (design position §八.4)
|
|
124
|
+
|
|
125
|
+
- `latest` = the last episode in the chain; its digest is the compact-checkpoint message
|
|
126
|
+
already resident in live context.
|
|
127
|
+
- Compute the checkpoint's transcript lines (the `user/message` with `source.plugin === 'compact'`
|
|
128
|
+
at `checkpointSeq`; fall back to the seq when no line is mapped).
|
|
129
|
+
- Latest episode prepared face = **navigation block** (no digest dump):
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
[ctx://session/compactions[<label>] — digest is already in your live context]
|
|
133
|
+
lines=<ls>..<le> seq=<ss>..<se> items=<n> tokens=<t>
|
|
134
|
+
fidelity=<ut>:<tc>
|
|
135
|
+
digest resident at seq=<checkpointSeq> (transcript:<start>-<end> when mapped) — read it there,
|
|
136
|
+
or use :raw here for the shadowed span
|
|
137
|
+
<landmarks>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- Older episode prepared face = **full digest** (`summaryText`) followed by the same pointer
|
|
141
|
+
block. `:raw` on any episode always returns the span, so a digest is never lost.
|
|
142
|
+
|
|
143
|
+
### N3 — landmark roster (direction markers, all as line coordinates)
|
|
144
|
+
|
|
145
|
+
Computed over the episode's shadowed events; capped to stay one screen:
|
|
146
|
+
|
|
147
|
+
- `user turns (<n>): <line>, <line>, …` — transcript line of each rendered `user/message`
|
|
148
|
+
with `source.kind === 'user'` (cap 30, then `… +N more`).
|
|
149
|
+
- `failures (<n>): <line> <tool>, …` — `tool/result` events with
|
|
150
|
+
`data.message.isError === true` or a present `data.error` (cap 20).
|
|
151
|
+
- `touched paths: <path>×<count>, …` — path-valued strings extracted from `tool/call`
|
|
152
|
+
`arguments` (keys `path`/`file_path`/`filePath`/`file`, plus `edits[].path` and
|
|
153
|
+
`files[].path`; args larger than 64 000 chars skipped). Best-effort pointer, not an index.
|
|
154
|
+
(cap 12, sorted by count desc).
|
|
155
|
+
- `prior checkpoints in span: <n>` — count of `user/message` with `source.plugin === 'compact'`.
|
|
156
|
+
|
|
157
|
+
### N4 — fidelity line
|
|
158
|
+
|
|
159
|
+
`fidelity=<userTurns>:<toolCalls>` per episode (tool-heavy ⇒ digest trustworthy and detail
|
|
160
|
+
regenerable; user-intent-heavy ⇒ must drill down). Inline in manifest, snapshot `compacted[]`,
|
|
161
|
+
and the episode pointer block.
|
|
162
|
+
|
|
163
|
+
### N5 — `:path/` and `?q=` in ctx (F5/F8) + error echo
|
|
164
|
+
|
|
165
|
+
The selector grammar already parses `path`/`query` and `applySelector()` (selector.ts) already
|
|
166
|
+
implements JSON dot-path + line filter. Wire them into `applyFace`:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
if (selector.kind === 'path' || selector.kind === 'query') {
|
|
170
|
+
return applySelector(face === 'canonical' ? canonical : prepared, selector)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
So `ctx://session:path/totals.tool_calls` narrow-reads the snapshot JSON, and
|
|
175
|
+
`ctx://session/injections?q=subagent-settled` filters the index. The manifest route also runs
|
|
176
|
+
through `applySelector` (so `?q=` and `:N-M` windowing work on it too). Update
|
|
177
|
+
`CTX_BAD_SELECTOR` to echo the URL and the supported selector set.
|
|
178
|
+
|
|
179
|
+
### N6 — snapshot `asOf`
|
|
180
|
+
|
|
181
|
+
Snapshot gains `asOf: { seq: <max seq>, line: <totalLines> }` and a hint that totals are live
|
|
182
|
+
at read time (a cached snapshot is knowingly stale).
|
|
183
|
+
|
|
184
|
+
### F1 — element-collection index faces + error echo
|
|
185
|
+
|
|
186
|
+
- `user_prompts` bare → index list (one line per prompt: `seq=<s> <preview>`); canonical
|
|
187
|
+
`:raw` = all joined.
|
|
188
|
+
- `tool_calls` / `agent_responses` bare → bounded index (first 20 lines + `… +N more — use
|
|
189
|
+
:raw for all, ?q= to filter, [n|seq] for one`); canonical `:raw` = all joined.
|
|
190
|
+
- `[n|seq]` behavior unchanged.
|
|
191
|
+
- Error text for a bad element stops echoing an empty-bracket URL; it names the collection,
|
|
192
|
+
its count, and the 0-based/seq addressing (already mostly present — align the message).
|
|
193
|
+
|
|
194
|
+
## 5. Out of scope
|
|
195
|
+
|
|
196
|
+
- F3 (`read` offset/limit silently ignored on URIs) — harness read-tool contract, not this handler.
|
|
197
|
+
- Semantic/vector retrieval; embeddings; "fuzzy" search (see §1).
|
|
198
|
+
- F4 (compaction label drift) — already mitigated by the error listing current labels; the
|
|
199
|
+
`compactionId` (uuid) stable-key idea is recorded but not implemented this wave.
|
|
200
|
+
|
|
201
|
+
## 6. Verification
|
|
202
|
+
|
|
203
|
+
Unit: extend `better-dsh/test/url-schemes/ctx.spec.ts` (and add focused specs) with the
|
|
204
|
+
invariants above — notably **episode span ≡ transcript slice** and **episode `:N-M` ≡
|
|
205
|
+
`transcript:N-M`** for non-first episodes. Full `vitest run` + `tsc --noEmit` green.
|
|
206
|
+
|
|
207
|
+
First-person (4999 rig, dsh 0.2.0-rc.2): a real long-session instance reads the snapshot
|
|
208
|
+
(asOf + lines present), windows a deep episode by transcript line, filters an index with
|
|
209
|
+
`?q=`, narrow-reads with `:path/`, and confirms a large tool result is no longer empty.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { f as UrlSchemesError, t as buildFsLayerResolver } from "../wrap-
|
|
1
|
+
import { f as UrlSchemesError, t as buildFsLayerResolver } from "../wrap-Bzdimp4p.js";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
import { dirname, sep } from "node:path";
|
|
4
|
+
import { stat } from "node:fs/promises";
|
|
4
5
|
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
5
6
|
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
6
7
|
import { writableRoots } from "@deepseek-ai/dsh-sandbox";
|
|
7
|
-
import { stat } from "node:fs/promises";
|
|
8
8
|
|
|
9
9
|
//#region ../upstream/deepseek-harness/packages/fs/fs-sandbox/lib/index.js
|
|
10
10
|
/**
|
package/lib/url-schemes/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as captureNativeTools } from "../native-capture-x_Hos2e8.js";
|
|
2
|
-
import { a as createHttpHandler, c as listDvcDevices, d as UrlResolver, f as UrlSchemesError, i as HTTP_SCHEMES, l as registerDvcDevice, m as
|
|
2
|
+
import { a as createHttpHandler, c as listDvcDevices, d as UrlResolver, f as UrlSchemesError, h as SCHEME_NAMES, i as HTTP_SCHEMES, l as registerDvcDevice, m as parseUrl, n as wrapFsWithSchemes, o as createDvcHandler, p as applySelector, r as resolveDocsDir, s as dispatchDvcWrite, u as createDshHandler } from "../wrap-Bzdimp4p.js";
|
|
3
3
|
import { a as primaryServerForFile, c as serversForFile, n as findWorkspaceRoot, o as resolveCommandPath, r as installHintFor, s as serverByName } from "../lsp-server-registry-946ysdqg.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -11,10 +11,10 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { homedir, tmpdir } from "node:os";
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
13
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
14
|
-
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
15
|
-
import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from "@deepseek-ai/dsh-sandbox";
|
|
16
14
|
import * as fsPromises from "node:fs/promises";
|
|
17
15
|
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
16
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
17
|
+
import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from "@deepseek-ai/dsh-sandbox";
|
|
18
18
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
19
19
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
20
20
|
import "file-type";
|
|
@@ -2445,7 +2445,9 @@ function sourcePlugin(event) {
|
|
|
2445
2445
|
return event.data?.source?.plugin ?? "";
|
|
2446
2446
|
}
|
|
2447
2447
|
function resultTextOf(event) {
|
|
2448
|
-
|
|
2448
|
+
const blocks = blocksOf(event);
|
|
2449
|
+
const nested = blocks.flatMap((b) => b.content ?? []).map((c) => c.text ?? "").join("\n");
|
|
2450
|
+
return nested !== "" ? nested : textOf(blocks);
|
|
2449
2451
|
}
|
|
2450
2452
|
/** All reasoning blocks in event order (one message may carry several). */
|
|
2451
2453
|
function thinkingItems(events) {
|
|
@@ -2530,13 +2532,137 @@ function sectionPreviews(text, n = 100) {
|
|
|
2530
2532
|
}
|
|
2531
2533
|
function applyLines(canonical, ranges) {
|
|
2532
2534
|
const lines = canonical.split("\n");
|
|
2533
|
-
|
|
2535
|
+
const total = lines.length;
|
|
2536
|
+
const out = [];
|
|
2537
|
+
let noted = false;
|
|
2538
|
+
for (const [aRaw, bRaw] of ranges) {
|
|
2539
|
+
const a = Math.max(1, aRaw);
|
|
2540
|
+
const b = bRaw === Infinity ? total : bRaw;
|
|
2541
|
+
if (b < 1 || a > total) {
|
|
2542
|
+
noted = true;
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
out.push(...lines.slice(a - 1, Math.min(b, total)));
|
|
2546
|
+
if (bRaw !== Infinity && b > total) noted = true;
|
|
2547
|
+
}
|
|
2548
|
+
const body = out.join("\n");
|
|
2549
|
+
if (!noted) return body;
|
|
2550
|
+
const note = `[ctx:// note: canonical content ends at line ${total}]`;
|
|
2551
|
+
return body === "" ? note : `${body}\n\n${note}`;
|
|
2534
2552
|
}
|
|
2535
2553
|
function applyFace(prepared, canonical, selector, face = "prepared") {
|
|
2536
2554
|
if (selector === void 0 || selector === null) return face === "canonical" ? canonical : prepared;
|
|
2537
2555
|
if (selector.kind === "raw") return canonical;
|
|
2538
2556
|
if (selector.kind === "lines") return applyLines(canonical, selector.ranges);
|
|
2539
|
-
|
|
2557
|
+
if (selector.kind === "path" || selector.kind === "query") return applySelector(face === "canonical" ? canonical : prepared, selector);
|
|
2558
|
+
throw new UrlSchemesError("CTX_BAD_SELECTOR", `ctx://: unsupported selector kind "${selector.kind}" (supported: :raw, :N-M, :path/, ?q=)`);
|
|
2559
|
+
}
|
|
2560
|
+
function buildTranscript(events, toolNameOf) {
|
|
2561
|
+
const entries = events.map((e) => ({
|
|
2562
|
+
seq: e.seq,
|
|
2563
|
+
text: renderEntry(e, toolNameOf)
|
|
2564
|
+
})).filter((t) => t.text !== "");
|
|
2565
|
+
const lineOfSeq = /* @__PURE__ */ new Map();
|
|
2566
|
+
let line = 1;
|
|
2567
|
+
entries.forEach((entry, i) => {
|
|
2568
|
+
const n = entry.text.split("\n").length;
|
|
2569
|
+
lineOfSeq.set(entry.seq, {
|
|
2570
|
+
start: line,
|
|
2571
|
+
end: line + n - 1
|
|
2572
|
+
});
|
|
2573
|
+
line += n + (i < entries.length - 1 ? 1 : 0);
|
|
2574
|
+
});
|
|
2575
|
+
return {
|
|
2576
|
+
text: entries.map((e) => e.text).join("\n\n"),
|
|
2577
|
+
totalLines: line - 1,
|
|
2578
|
+
lineOfSeq
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
/** Apply transcript-relative line ranges to one episode span slice, with a boundary note. */
|
|
2582
|
+
function applyEpisodeLines(slice, lineStart, lineEnd, ranges) {
|
|
2583
|
+
const lines = slice.split("\n");
|
|
2584
|
+
const out = [];
|
|
2585
|
+
let noted = false;
|
|
2586
|
+
for (const [aRaw, bRaw] of ranges) {
|
|
2587
|
+
const a = Math.max(1, aRaw);
|
|
2588
|
+
const b = bRaw === Infinity ? lineEnd : bRaw;
|
|
2589
|
+
if (b < lineStart || a > lineEnd) {
|
|
2590
|
+
noted = true;
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
const la = Math.max(a, lineStart) - lineStart + 1;
|
|
2594
|
+
const lb = Math.min(b, lineEnd) - lineStart + 1;
|
|
2595
|
+
out.push(...lines.slice(la - 1, lb));
|
|
2596
|
+
if (b > lineEnd || a < lineStart) noted = true;
|
|
2597
|
+
}
|
|
2598
|
+
return out.join("\n") + (noted ? `\n\n[ctx:// note: episode span is transcript lines ${lineStart}-${lineEnd}]` : "");
|
|
2599
|
+
}
|
|
2600
|
+
function isErrorOf(event) {
|
|
2601
|
+
return event.data?.message?.isError === true || event.data?.error !== void 0;
|
|
2602
|
+
}
|
|
2603
|
+
const PATH_KEYS = new Set([
|
|
2604
|
+
"path",
|
|
2605
|
+
"file_path",
|
|
2606
|
+
"filePath",
|
|
2607
|
+
"file"
|
|
2608
|
+
]);
|
|
2609
|
+
function collectPaths(node, out, depth) {
|
|
2610
|
+
if (depth > 4 || node === null || node === void 0) return;
|
|
2611
|
+
if (typeof node === "string") return;
|
|
2612
|
+
if (Array.isArray(node)) {
|
|
2613
|
+
for (const x of node) collectPaths(x, out, depth + 1);
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
if (typeof node === "object") for (const [k, v] of Object.entries(node)) if (PATH_KEYS.has(k) && typeof v === "string" && v !== "") out.add(v);
|
|
2617
|
+
else collectPaths(v, out, depth + 1);
|
|
2618
|
+
}
|
|
2619
|
+
function touchedPathsOf(events) {
|
|
2620
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2621
|
+
for (const e of events) {
|
|
2622
|
+
if (e.type !== "tool/call") continue;
|
|
2623
|
+
const args = e.data?.arguments;
|
|
2624
|
+
if (args === void 0 || args.length > 64e3) continue;
|
|
2625
|
+
let parsed;
|
|
2626
|
+
try {
|
|
2627
|
+
parsed = JSON.parse(args);
|
|
2628
|
+
} catch {
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
const found = /* @__PURE__ */ new Set();
|
|
2632
|
+
collectPaths(parsed, found, 0);
|
|
2633
|
+
for (const p of found) counts.set(p, (counts.get(p) ?? 0) + 1);
|
|
2634
|
+
}
|
|
2635
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
2636
|
+
}
|
|
2637
|
+
function landmarkBlock(ep, tIdx, toolNameOf) {
|
|
2638
|
+
const lineOf = (e) => tIdx.lineOfSeq.get(e.seq)?.start;
|
|
2639
|
+
const userTurns = ep.shadowEvents.filter((e) => e.type === "user/message" && sourceKind(e) === "user").map(lineOf).filter((x) => x !== void 0);
|
|
2640
|
+
const failures = ep.shadowEvents.filter((e) => e.type === "tool/result" && isErrorOf(e)).map((e) => `${lineOf(e) ?? "?"} ${toolNameOf(e.sourceEventSeqs?.[0]) ?? "tool"}`);
|
|
2641
|
+
const paths = touchedPathsOf(ep.shadowEvents).slice(0, 12).map(([p, n]) => `${p}×${n}`);
|
|
2642
|
+
const priorCheckpoints = ep.shadowEvents.filter((e) => e.type === "user/message" && sourcePlugin(e) === "compact").length;
|
|
2643
|
+
const cap = (xs, n) => xs.length > n ? `${xs.slice(0, n).join(", ")} … +${xs.length - n} more` : xs.join(", ");
|
|
2644
|
+
return [
|
|
2645
|
+
`user turns (${userTurns.length}): ${cap(userTurns, 30)}`,
|
|
2646
|
+
`failures (${failures.length}): ${cap(failures, 20)}`,
|
|
2647
|
+
`touched paths: ${paths.length > 0 ? paths.join(", ") : "(none)"}`,
|
|
2648
|
+
`prior checkpoints in span: ${priorCheckpoints}`
|
|
2649
|
+
].join("\n");
|
|
2650
|
+
}
|
|
2651
|
+
function episodePrepared(ep, list, tIdx, toolNameOf) {
|
|
2652
|
+
const isLatest = list.length > 0 && list[list.length - 1] === ep;
|
|
2653
|
+
const ls = ep.lines === null ? "<none>" : `${ep.lines.start}..${ep.lines.end}`;
|
|
2654
|
+
const pointer = [
|
|
2655
|
+
`[ctx://session/compactions[${ep.label}]${isLatest ? " — digest is already in your live context" : ""}]`,
|
|
2656
|
+
`lines=${ls} seq=${ep.shadowedRange.start}..${ep.shadowedRange.end} items=${ep.shadowedSeqs.length} tokens=${ep.shadowedTokenCount}`,
|
|
2657
|
+
`fidelity=${ep.fidelity.userTurns}:${ep.fidelity.toolCalls}`
|
|
2658
|
+
];
|
|
2659
|
+
if (isLatest) {
|
|
2660
|
+
const ck = tIdx.lineOfSeq.get(ep.checkpointSeq);
|
|
2661
|
+
pointer.push(`digest resident at seq=${ep.checkpointSeq}${ck !== void 0 ? ` (transcript:${ck.start}-${ck.end})` : ""} — read it there, or use :raw here for the shadowed span`);
|
|
2662
|
+
} else if (ep.replacesCheckpoint !== null) pointer.push(`replaces_checkpoint=${ep.replacesCheckpoint}`);
|
|
2663
|
+
pointer.push(landmarkBlock(ep, tIdx, toolNameOf));
|
|
2664
|
+
const block = pointer.join("\n");
|
|
2665
|
+
return isLatest ? block : `${ep.summaryText}\n\n${block}`;
|
|
2540
2666
|
}
|
|
2541
2667
|
function createCtxHandler(deps) {
|
|
2542
2668
|
const persistence = deps.sessionPersistence;
|
|
@@ -2575,15 +2701,25 @@ function createCtxHandler(deps) {
|
|
|
2575
2701
|
const toolNameOf = (seq) => seq === void 0 ? void 0 : toolNameByCallSeq.get(seq);
|
|
2576
2702
|
const userPromptEvents = events.filter((e) => e.type === "user/message" && sourceKind(e) === "user");
|
|
2577
2703
|
const agentResponseEvents = events.filter((e) => e.type === "assistant/message");
|
|
2578
|
-
const
|
|
2704
|
+
const tIdx = buildTranscript(events, toolNameOf);
|
|
2579
2705
|
const episodes = [];
|
|
2580
2706
|
{
|
|
2581
2707
|
let prevCheckpoint = null;
|
|
2582
|
-
const nameOf = toolNameOf;
|
|
2583
2708
|
for (const e of events) {
|
|
2584
2709
|
if (e.type !== "compaction/summary") continue;
|
|
2585
2710
|
const d = e.data;
|
|
2586
|
-
const
|
|
2711
|
+
const shadowSet = new Set(d.shadowedSeqs);
|
|
2712
|
+
const shadowEvents = events.filter((ev) => shadowSet.has(ev.seq));
|
|
2713
|
+
const shadowLines = d.shadowedSeqs.map((seq) => tIdx.lineOfSeq.get(seq)).filter((x) => x !== void 0);
|
|
2714
|
+
const lines = shadowLines.length === 0 ? null : {
|
|
2715
|
+
start: Math.min(...shadowLines.map((x) => x.start)),
|
|
2716
|
+
end: Math.max(...shadowLines.map((x) => x.end))
|
|
2717
|
+
};
|
|
2718
|
+
const originalText = lines === null ? "" : tIdx.text.split("\n").slice(lines.start - 1, lines.end).join("\n");
|
|
2719
|
+
const fidelity = {
|
|
2720
|
+
userTurns: shadowEvents.filter((ev) => ev.type === "user/message" && sourceKind(ev) === "user").length,
|
|
2721
|
+
toolCalls: shadowEvents.filter((ev) => ev.type === "tool/call").length
|
|
2722
|
+
};
|
|
2587
2723
|
episodes.push({
|
|
2588
2724
|
label: e.seq,
|
|
2589
2725
|
checkpointSeq: e.seq + 1,
|
|
@@ -2594,7 +2730,10 @@ function createCtxHandler(deps) {
|
|
|
2594
2730
|
shadowedTokenCount: d.shadowedTokenCount,
|
|
2595
2731
|
replacesCheckpoint: prevCheckpoint !== null && d.shadowedSeqs.includes(prevCheckpoint) ? prevCheckpoint : null,
|
|
2596
2732
|
summaryText: d.summary.map((s) => s.text).join(""),
|
|
2597
|
-
originalText
|
|
2733
|
+
originalText,
|
|
2734
|
+
lines,
|
|
2735
|
+
fidelity,
|
|
2736
|
+
shadowEvents
|
|
2598
2737
|
});
|
|
2599
2738
|
prevCheckpoint = e.seq + 1;
|
|
2600
2739
|
}
|
|
@@ -2613,26 +2752,31 @@ function createCtxHandler(deps) {
|
|
|
2613
2752
|
const seg1 = segments[1] === void 0 ? void 0 : parseSeg(segments[1]);
|
|
2614
2753
|
if (seg0 === void 0 || seg0.name === "transcript") {
|
|
2615
2754
|
seg0?.name;
|
|
2616
|
-
const canonical =
|
|
2617
|
-
return applyFace(buildSnapshot(agent, events, episodes), canonical, sel, seg0?.name === "transcript" ? "canonical" : "prepared");
|
|
2755
|
+
const canonical = tIdx.text;
|
|
2756
|
+
return applyFace(buildSnapshot(agent, events, episodes, tIdx), canonical, sel, seg0?.name === "transcript" ? "canonical" : "prepared");
|
|
2618
2757
|
}
|
|
2619
2758
|
if (seg0.name === "compactions") {
|
|
2620
2759
|
if (seg0.bracket === void 0) {
|
|
2621
|
-
if (sel !== null && sel.kind !== "raw") throw new UrlSchemesError("CTX_BAD_SELECTOR", "ctx://session/compactions: only :raw (or no selector) is supported on the manifest");
|
|
2622
2760
|
const body = episodes.map((e) => [
|
|
2623
2761
|
`label=${e.label} (checkpoint_seq=${e.checkpointSeq})`,
|
|
2624
2762
|
`compactionId=${e.compactionId}`,
|
|
2625
2763
|
`at=${new Date(e.at ?? 0).toISOString()}`,
|
|
2626
|
-
`
|
|
2764
|
+
`seq=${e.shadowedRange.start}..${e.shadowedRange.end} (${e.shadowedSeqs.length} items, ${e.shadowedTokenCount} tokens)`,
|
|
2765
|
+
`lines=${e.lines === null ? "<none>" : `${e.lines.start}..${e.lines.end}`}`,
|
|
2766
|
+
`fidelity=${e.fidelity.userTurns}:${e.fidelity.toolCalls}`,
|
|
2627
2767
|
`replaces_checkpoint=${e.replacesCheckpoint ?? "null"}`,
|
|
2628
2768
|
`preview=${e.summaryText.slice(0, 120)}…`
|
|
2629
2769
|
].join(" | ")).join("\n");
|
|
2630
|
-
|
|
2770
|
+
const text = body === "" ? "(no compactions recorded)" : body;
|
|
2771
|
+
if (sel === null || sel.kind === "raw") return text;
|
|
2772
|
+
if (sel.kind === "lines") return applyLines(text, sel.ranges);
|
|
2773
|
+
return applySelector(text, sel);
|
|
2631
2774
|
}
|
|
2632
2775
|
const episode = pickEpisode(episodes, seg0.bracket);
|
|
2633
2776
|
if (seg1 === void 0) {
|
|
2634
2777
|
if (sel !== null && sel.kind === "raw" && episode.originalText.length > 65536) return episode.originalText + `\n\n[ctx:// note: original span is ${episode.originalText.length} chars — page with :N-M line windows or grep this URL]`;
|
|
2635
|
-
return
|
|
2778
|
+
if (sel !== null && sel.kind === "lines" && episode.lines !== null) return applyEpisodeLines(episode.originalText, episode.lines.start, episode.lines.end, sel.ranges);
|
|
2779
|
+
return applyFace(episodePrepared(episode, episodes, tIdx, toolNameOf), episode.originalText, sel, "prepared");
|
|
2636
2780
|
}
|
|
2637
2781
|
throw new UrlSchemesError("CTX_BAD_PATH", `ctx://${raw}: unknown sub-path "/${seg1.name}" ("/original" was superseded by the :raw / :raw:N-M selectors)`);
|
|
2638
2782
|
}
|
|
@@ -2671,13 +2815,19 @@ function createCtxHandler(deps) {
|
|
|
2671
2815
|
}));
|
|
2672
2816
|
throw new UrlSchemesError("CTX_BAD_PATH", `ctx://${raw}: unknown collection "${name$1}"`);
|
|
2673
2817
|
};
|
|
2674
|
-
if (seg0.
|
|
2818
|
+
if (seg0.name === "user_prompts" || seg0.name === "tool_calls" || seg0.name === "agent_responses") {
|
|
2675
2819
|
const items = collection(seg0.name);
|
|
2676
|
-
const
|
|
2677
|
-
|
|
2820
|
+
const canonical = items.map((it) => it.text).join("\n\n");
|
|
2821
|
+
if (seg0.bracket === void 0) {
|
|
2822
|
+
const bounded = seg0.name !== "user_prompts";
|
|
2823
|
+
const index = (bounded ? items.slice(0, 20) : items).map((it) => `seq=${it.seq} ${previewOf(it.text)}`).join("\n");
|
|
2824
|
+
const tail = bounded && items.length > 20 ? `\n… +${items.length - 20} more — use :raw for all, ?q= to filter, [n|seq] for one` : "";
|
|
2825
|
+
return applyFace(index === "" ? `(no ${seg0.name} items recorded)` : index + tail, canonical, sel, "prepared");
|
|
2826
|
+
}
|
|
2827
|
+
const bySeq = items.find((it) => it.seq === Number(seg0.bracket));
|
|
2678
2828
|
if (bySeq !== void 0) return applyFace(bySeq.text, bySeq.text, sel, "prepared");
|
|
2679
|
-
const ordinal = Number(bracket);
|
|
2680
|
-
if (
|
|
2829
|
+
const ordinal = Number(seg0.bracket);
|
|
2830
|
+
if (!Number.isInteger(ordinal) || ordinal < 0 || ordinal >= items.length) throw new UrlSchemesError("CTX_NO_SUCH_ELEMENT", `ctx://session/${seg0.name}[${seg0.bracket}]: no such element (collection "${seg0.name}" has ${items.length} items, 0-based; labels are event seqs)`);
|
|
2681
2831
|
return applyFace(items[ordinal].text, items[ordinal].text, sel, "prepared");
|
|
2682
2832
|
}
|
|
2683
2833
|
throw new UrlSchemesError("CTX_UNKNOWN_KEY", `ctx://${raw}: unknown path (known: session, session/transcript, session/compactions[...], session/user_prompts[...], session/tool_calls[...], session/agent_responses[...], session/thinking[...], session/system[...], session/injections[...]; bare ctx:// lists the full roster)`);
|
|
@@ -2688,7 +2838,7 @@ function createCtxHandler(deps) {
|
|
|
2688
2838
|
if (Number.isInteger(ordinal) && ordinal >= 0 && ordinal < list.length) return list[ordinal];
|
|
2689
2839
|
throw new UrlSchemesError("CTX_NO_SUCH_ELEMENT", `ctx://…[${bracket}]: no such compaction episode (labels: ${list.map((e) => e.label).join(", ") || "none"})`);
|
|
2690
2840
|
}
|
|
2691
|
-
function buildSnapshot(agent$1, all, list) {
|
|
2841
|
+
function buildSnapshot(agent$1, all, list, tIdx$1) {
|
|
2692
2842
|
const headers = all.filter((e) => e.type === "request/header");
|
|
2693
2843
|
const system = (headers[headers.length - 1]?.data)?.header?.system;
|
|
2694
2844
|
const counts = {};
|
|
@@ -2712,9 +2862,10 @@ function createCtxHandler(deps) {
|
|
|
2712
2862
|
"tool_results"
|
|
2713
2863
|
]) counts[key] = counts[key] ?? 0;
|
|
2714
2864
|
const header = agent$1.session.header;
|
|
2865
|
+
const lastSeq = all.reduce((m, e) => Math.max(m, e.seq), 0);
|
|
2715
2866
|
return JSON.stringify({
|
|
2716
2867
|
resource: "ctx://session",
|
|
2717
|
-
syntax: "/sub-path [<label|n>] [:N-M]; :raw = canonical full content; :raw:N-M ≡ :N-M",
|
|
2868
|
+
syntax: "/sub-path [<label|n>] [:N-M|:path/|?q=]; :raw = canonical full content; :raw:N-M ≡ :N-M",
|
|
2718
2869
|
session: {
|
|
2719
2870
|
id: header.id ?? agent$1.id,
|
|
2720
2871
|
createdAt: header.createdAt,
|
|
@@ -2728,19 +2879,17 @@ function createCtxHandler(deps) {
|
|
|
2728
2879
|
totals: counts,
|
|
2729
2880
|
segments: [...list.map((e) => ({
|
|
2730
2881
|
segment: `compaction:${e.label}`,
|
|
2731
|
-
|
|
2732
|
-
|
|
2882
|
+
seq_start: e.shadowedRange?.start ?? null,
|
|
2883
|
+
seq_end: e.shadowedRange?.end ?? null,
|
|
2884
|
+
lines: e.lines,
|
|
2733
2885
|
items: e.shadowedSeqs.length
|
|
2734
|
-
})),
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
items: all.filter((e) => e.seq >= start).length
|
|
2742
|
-
};
|
|
2743
|
-
})()],
|
|
2886
|
+
})), {
|
|
2887
|
+
segment: "live",
|
|
2888
|
+
seq_start: (list.at(-1)?.shadowedRange?.end ?? -1) + 1,
|
|
2889
|
+
seq_end: lastSeq,
|
|
2890
|
+
lines: null,
|
|
2891
|
+
items: all.filter((e) => e.seq >= (list.at(-1)?.shadowedRange?.end ?? -1) + 1).length
|
|
2892
|
+
}],
|
|
2744
2893
|
system_prompt: {
|
|
2745
2894
|
chars: system?.length ?? 0,
|
|
2746
2895
|
preview: system === void 0 ? "" : system.slice(0, 200)
|
|
@@ -2750,16 +2899,24 @@ function createCtxHandler(deps) {
|
|
|
2750
2899
|
checkpoint_seq: e.checkpointSeq,
|
|
2751
2900
|
compactionId: e.compactionId,
|
|
2752
2901
|
at: e.at,
|
|
2753
|
-
|
|
2902
|
+
seq_start: e.shadowedRange.start,
|
|
2903
|
+
seq_end: e.shadowedRange.end,
|
|
2904
|
+
lines: e.lines,
|
|
2754
2905
|
shadowed_items: e.shadowedSeqs.length,
|
|
2755
2906
|
shadowed_token_count: e.shadowedTokenCount,
|
|
2907
|
+
fidelity: e.fidelity,
|
|
2756
2908
|
replaces_checkpoint: e.replacesCheckpoint,
|
|
2757
2909
|
summary_preview: sectionPreviews(e.summaryText)
|
|
2758
2910
|
})),
|
|
2911
|
+
asOf: {
|
|
2912
|
+
seq: lastSeq,
|
|
2913
|
+
line: tIdx$1.totalLines
|
|
2914
|
+
},
|
|
2759
2915
|
hints: [
|
|
2760
2916
|
"ctx://session:raw → full transcript",
|
|
2761
2917
|
"ctx://session/compactions[<label>]:raw → original span",
|
|
2762
|
-
"ctx://session/thinking /
|
|
2918
|
+
"ctx://session/thinking / system / injections / user_prompts / tool_calls / agent_responses → index faces",
|
|
2919
|
+
":path/<dot-path> narrow-reads the snapshot · ?q=<text> filters an index"
|
|
2763
2920
|
]
|
|
2764
2921
|
}, void 0, 2);
|
|
2765
2922
|
}
|