dsh-turn-outline 0.2.5

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/lib/index.js ADDED
@@ -0,0 +1,305 @@
1
+ // src/wire.ts
2
+ var TEXT_CAP = 2048;
3
+ var ARGS_CAP = 512;
4
+ var PAGE_SIZE = 2e3;
5
+ var MAX_BODY_BYTES = 64 * 1024;
6
+ function clipText(value, cap) {
7
+ return value.length > cap ? `${value.slice(0, cap)}\u2026` : value;
8
+ }
9
+ function record(value) {
10
+ return value !== null && typeof value === "object" ? value : void 0;
11
+ }
12
+ function projectContent(content, cap) {
13
+ if (typeof content === "string") return content.length > 0 ? clipText(content, cap) : void 0;
14
+ if (!Array.isArray(content)) return void 0;
15
+ const out = [];
16
+ for (const block of content) {
17
+ const b = record(block);
18
+ if (b === void 0 || b.type !== "text" || typeof b.text !== "string" || b.text.length === 0) continue;
19
+ out.push({ type: "text", text: clipText(b.text, cap) });
20
+ }
21
+ return out.length > 0 ? out : void 0;
22
+ }
23
+ function sourceOf(data) {
24
+ const message = record(data.message);
25
+ if (message !== void 0) {
26
+ const source = record(message.source);
27
+ if (source !== void 0) return source;
28
+ }
29
+ return record(data.source);
30
+ }
31
+ function projectSourceFields(source) {
32
+ const out = {};
33
+ if (typeof source.kind === "string") out.kind = source.kind;
34
+ if (typeof source.callId === "string" && source.callId.length > 0) out.callId = source.callId;
35
+ return Object.keys(out).length > 0 ? out : void 0;
36
+ }
37
+ function projectEvent(event) {
38
+ const base = {
39
+ type: event.type,
40
+ seq: event.seq,
41
+ ...typeof event.time === "number" ? { time: event.time } : {}
42
+ };
43
+ const data = record(event.data);
44
+ if (data === void 0) return base;
45
+ switch (event.type) {
46
+ case "turn/start":
47
+ case "turn/end":
48
+ return typeof data.turn === "number" ? { ...base, data: { turn: data.turn } } : base;
49
+ case "user/message": {
50
+ const out = {};
51
+ const content = projectContent(data.content, TEXT_CAP);
52
+ if (content !== void 0) out.content = content;
53
+ const message = record(data.message);
54
+ if (message !== void 0) {
55
+ const m = {};
56
+ if (typeof message.content === "string" && message.content.length > 0) {
57
+ m.content = clipText(message.content, TEXT_CAP);
58
+ }
59
+ const source2 = record(message.source);
60
+ if (source2 !== void 0) {
61
+ const s = projectSourceFields(source2);
62
+ if (s !== void 0) m.source = s;
63
+ }
64
+ if (Object.keys(m).length > 0) out.message = m;
65
+ }
66
+ const source = record(data.source);
67
+ if (source !== void 0) {
68
+ const s = projectSourceFields(source);
69
+ if (s !== void 0) out.source = s;
70
+ }
71
+ return Object.keys(out).length > 0 ? { ...base, data: out } : base;
72
+ }
73
+ case "assistant/message": {
74
+ const out = {};
75
+ const message = record(data.message);
76
+ if (message !== void 0) {
77
+ const content = projectContent(message.content, TEXT_CAP);
78
+ if (content !== void 0) out.message = { content };
79
+ }
80
+ const fallback = projectContent(data.content, TEXT_CAP);
81
+ if (fallback !== void 0) out.content = fallback;
82
+ return Object.keys(out).length > 0 ? { ...base, data: out } : base;
83
+ }
84
+ case "tool/call": {
85
+ const out = {};
86
+ if (typeof data.name === "string" && data.name.length > 0) out.name = data.name;
87
+ if (typeof data.arguments === "string" && data.arguments.length > 0) {
88
+ out.arguments = clipText(data.arguments, ARGS_CAP);
89
+ }
90
+ if (typeof data.callId === "string" && data.callId.length > 0) out.callId = data.callId;
91
+ const source = sourceOf(data);
92
+ if (source !== void 0) {
93
+ const s = projectSourceFields(source);
94
+ if (s !== void 0 && typeof s.callId === "string") out.message = { source: s };
95
+ }
96
+ return Object.keys(out).length > 0 ? { ...base, data: out } : base;
97
+ }
98
+ case "tool/result": {
99
+ const out = {};
100
+ const error = record(data.error);
101
+ if (error !== void 0) {
102
+ const e = {};
103
+ if (typeof error.name === "string" && error.name.length > 0) e.name = error.name;
104
+ if (typeof error.code === "string" && error.code.length > 0) e.code = error.code;
105
+ if (Object.keys(e).length > 0) out.error = e;
106
+ }
107
+ if (typeof data.callId === "string" && data.callId.length > 0) out.callId = data.callId;
108
+ const source = sourceOf(data);
109
+ if (source !== void 0) {
110
+ const s = projectSourceFields(source);
111
+ if (s !== void 0 && typeof s.callId === "string") out.message = { source: s };
112
+ }
113
+ return Object.keys(out).length > 0 ? { ...base, data: out } : base;
114
+ }
115
+ case "compaction/summary": {
116
+ const summary = projectContent(data.summary, TEXT_CAP);
117
+ return summary !== void 0 ? { ...base, data: { summary } } : base;
118
+ }
119
+ default:
120
+ return base;
121
+ }
122
+ }
123
+
124
+ // src/index.ts
125
+ var inject = ["webServer", "sessionQuery", "sessions"];
126
+ var trailCache = /* @__PURE__ */ new Map();
127
+ var CACHE_MAX_ENTRIES = 64;
128
+ var CACHE_MAX_BYTES = 32 * 1024 * 1024;
129
+ var cacheBytes = 0;
130
+ function header(headers, name) {
131
+ const value = headers[name];
132
+ return typeof value === "string" ? value : void 0;
133
+ }
134
+ function parseAuthority(authority) {
135
+ try {
136
+ return new URL(`http://${authority}`);
137
+ } catch {
138
+ return void 0;
139
+ }
140
+ }
141
+ function isLoopbackHostname(hostname) {
142
+ if (hostname === "localhost" || hostname === "[::1]") return true;
143
+ const parts = hostname.split(".");
144
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
145
+ }
146
+ function isTrustedApiRequest(request) {
147
+ const host = header(request.headers, "host");
148
+ if (host === void 0) return false;
149
+ const hostUrl = parseAuthority(host);
150
+ if (hostUrl === void 0 || !isLoopbackHostname(hostUrl.hostname)) return false;
151
+ if (header(request.headers, "sec-fetch-site") === "cross-site") return false;
152
+ const origin = header(request.headers, "origin");
153
+ if (origin === void 0) return true;
154
+ try {
155
+ return new URL(origin).hostname === hostUrl.hostname;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+ function sendJson(res, status, body) {
161
+ const text = JSON.stringify(body);
162
+ res.writeHead(status, {
163
+ "content-type": "application/json; charset=utf-8",
164
+ "content-length": String(Buffer.byteLength(text))
165
+ });
166
+ res.end(text);
167
+ }
168
+ var BODY_TOO_LARGE = Symbol("trail-body-too-large");
169
+ async function readJsonBody(req) {
170
+ const declared = header(req.headers, "content-length");
171
+ if (declared !== void 0) {
172
+ const declaredBytes = Number(declared);
173
+ if (Number.isFinite(declaredBytes) && declaredBytes > MAX_BODY_BYTES) return BODY_TOO_LARGE;
174
+ }
175
+ const chunks = [];
176
+ let total = 0;
177
+ for await (const chunk of req) {
178
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
179
+ total += buf.length;
180
+ if (total > MAX_BODY_BYTES) return BODY_TOO_LARGE;
181
+ chunks.push(buf);
182
+ }
183
+ const text = Buffer.concat(chunks).toString("utf8");
184
+ try {
185
+ const parsed = JSON.parse(text.length > 0 ? text : "{}");
186
+ return parsed !== null && typeof parsed === "object" ? parsed : {};
187
+ } catch {
188
+ return {};
189
+ }
190
+ }
191
+ function afterSeqOf(value) {
192
+ const n = Number(value);
193
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
194
+ }
195
+ function upperBound(events, afterSeq) {
196
+ let lo = 0;
197
+ let hi = events.length;
198
+ while (lo < hi) {
199
+ const mid = lo + hi >> 1;
200
+ const seq = events[mid].seq;
201
+ if (typeof seq === "number" && seq <= afterSeq) lo = mid + 1;
202
+ else hi = mid;
203
+ }
204
+ return lo;
205
+ }
206
+ function liveSessionOf(ctx, sessionId) {
207
+ const sessions = ctx.sessions;
208
+ if (sessions === void 0 || typeof sessions.get !== "function") return void 0;
209
+ try {
210
+ const session = sessions.get(sessionId);
211
+ if (session === null || session === void 0 || typeof session !== "object") return void 0;
212
+ return Array.isArray(session.events) ? session : void 0;
213
+ } catch {
214
+ return void 0;
215
+ }
216
+ }
217
+ function respondPage(res, events, afterSeq, project) {
218
+ const lo = upperBound(events, afterSeq);
219
+ const end = Math.min(events.length, lo + PAGE_SIZE);
220
+ const tail = [];
221
+ for (let i = lo; i < end; i += 1) tail.push(project ? projectEvent(events[i]) : events[i]);
222
+ sendJson(res, 200, { events: tail, hasMore: end < events.length });
223
+ }
224
+ function cacheStore(sessionId, lastSeq, events) {
225
+ const bytes = Buffer.byteLength(JSON.stringify(events));
226
+ const existing = trailCache.get(sessionId);
227
+ if (existing !== void 0) {
228
+ trailCache.delete(sessionId);
229
+ cacheBytes -= existing.bytes;
230
+ }
231
+ trailCache.set(sessionId, { lastSeq, events, bytes });
232
+ cacheBytes += bytes;
233
+ while (trailCache.size > CACHE_MAX_ENTRIES || cacheBytes > CACHE_MAX_BYTES && trailCache.size > 1) {
234
+ const oldest = trailCache.keys().next().value;
235
+ if (oldest === void 0) break;
236
+ const entry = trailCache.get(oldest);
237
+ trailCache.delete(oldest);
238
+ cacheBytes -= entry?.bytes ?? 0;
239
+ }
240
+ }
241
+ function apply(ctx) {
242
+ ctx.effect(() => ctx.webServer.register({
243
+ kind: "exact",
244
+ path: "/turntrail/api/events",
245
+ handler: async (rawReq, rawRes) => {
246
+ const req = rawReq;
247
+ const res = rawRes;
248
+ if (!isTrustedApiRequest({ headers: req.headers })) {
249
+ sendJson(res, 403, { error: "forbidden" });
250
+ return;
251
+ }
252
+ if (req.method !== "POST") {
253
+ sendJson(res, 405, { error: "method not allowed" });
254
+ return;
255
+ }
256
+ const body = await readJsonBody(req);
257
+ if (body === BODY_TOO_LARGE) {
258
+ sendJson(res, 413, { error: "payload too large" });
259
+ return;
260
+ }
261
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
262
+ if (sessionId.length === 0) {
263
+ sendJson(res, 400, { error: "sessionId is required" });
264
+ return;
265
+ }
266
+ const afterSeq = afterSeqOf(body.afterSeq);
267
+ try {
268
+ const live = liveSessionOf(ctx, sessionId);
269
+ if (live !== void 0) {
270
+ respondPage(res, live.events, afterSeq, true);
271
+ return;
272
+ }
273
+ let lastSeq = -1;
274
+ try {
275
+ const records = await ctx.sessionQuery.listEvents(sessionId);
276
+ const last = records[records.length - 1];
277
+ lastSeq = last !== void 0 && typeof last.seq === "number" ? last.seq : -1;
278
+ } catch {
279
+ lastSeq = -1;
280
+ }
281
+ const cached = trailCache.get(sessionId);
282
+ let base;
283
+ if (cached !== void 0 && lastSeq >= 0 && cached.lastSeq === lastSeq) {
284
+ base = cached.events;
285
+ trailCache.delete(sessionId);
286
+ trailCache.set(sessionId, cached);
287
+ } else {
288
+ const snapshot = await ctx.sessionQuery.readSession(sessionId);
289
+ base = (Array.isArray(snapshot.events) ? snapshot.events : []).map(projectEvent);
290
+ cacheStore(sessionId, lastSeq, base);
291
+ }
292
+ respondPage(res, base, afterSeq, false);
293
+ } catch {
294
+ sendJson(res, 404, { error: "session-not-found" });
295
+ }
296
+ }
297
+ }));
298
+ }
299
+ export {
300
+ apply,
301
+ inject,
302
+ isLoopbackHostname,
303
+ isTrustedApiRequest
304
+ };
305
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/wire.ts", "../src/index.ts"],
4
+ "sourcesContent": ["/**\n * wire.ts \u2014 host-side response shaping for POST /turntrail/api/events.\n *\n * The client fold needs only a small, typed subset of each event. Every\n * response event is projected to that subset BEFORE serialization, so the\n * wire (and the client's retained state) never carries full tool results,\n * reasoning blocks, or unbounded message text.\n *\n * Projection is idempotent and lossless for the fold's needs:\n * - `seq` / `type` / `time` are always preserved (the seq watermark and\n * turn identity ride on them).\n * - text fields are clipped to TEXT_CAP; tool arguments to ARGS_CAP.\n * - content block arrays keep ONLY non-empty text blocks, in order (the\n * fold reads exactly the first / last text block).\n * - `tool/result` message content (the bulky part of real logs) is dropped\n * entirely \u2014 the fold consumes only `error.name` / `error.code`.\n */\nimport type { SessionEventLike } from './context-types'\n\n/** Max characters carried per text field (input / output / summary). */\nexport const TEXT_CAP = 2048\n\n/** Max characters of tool-call arguments (the fold only previews 120). */\nexport const ARGS_CAP = 512\n\n/** Max events per response page; the client loops while `hasMore` is true. */\nexport const PAGE_SIZE = 2000\n\n/** Hard body cap for the route's request payload. */\nexport const MAX_BODY_BYTES = 64 * 1024\n\nfunction clipText(value: string, cap: number): string {\n return value.length > cap ? `${value.slice(0, cap)}\u2026` : value\n}\n\nfunction record(value: unknown): Record<string, unknown> | undefined {\n return value !== null && typeof value === 'object' ? (value as Record<string, unknown>) : undefined\n}\n\n/**\n * Project one content value (bare string or block array) onto the fold's\n * needs: clipped text, or a text-blocks-only array. Returns undefined when\n * nothing foldable survives (the fold then treats the field as absent,\n * which matches its behavior for empty/non-text content).\n */\nfunction projectContent(content: unknown, cap: number): string | Array<{ type: 'text'; text: string }> | undefined {\n if (typeof content === 'string') return content.length > 0 ? clipText(content, cap) : undefined\n if (!Array.isArray(content)) return undefined\n const out: Array<{ type: 'text'; text: string }> = []\n for (const block of content) {\n const b = record(block)\n if (b === undefined || b.type !== 'text' || typeof b.text !== 'string' || b.text.length === 0) continue\n out.push({ type: 'text', text: clipText(b.text, cap) })\n }\n return out.length > 0 ? out : undefined\n}\n\nfunction sourceOf(data: Record<string, unknown>): Record<string, unknown> | undefined {\n const message = record(data.message)\n if (message !== undefined) {\n const source = record(message.source)\n if (source !== undefined) return source\n }\n return record(data.source)\n}\n\n/** Copy only the source fields the fold reads (kind / callId). */\nfunction projectSourceFields(source: Record<string, unknown>): Record<string, unknown> | undefined {\n const out: Record<string, unknown> = {}\n if (typeof source.kind === 'string') out.kind = source.kind\n if (typeof source.callId === 'string' && source.callId.length > 0) out.callId = source.callId\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/**\n * Project one session event onto the fold-relevant wire subset.\n * See the module header for the field policy.\n */\nexport function projectEvent(event: SessionEventLike): SessionEventLike {\n const base: SessionEventLike = {\n type: event.type,\n seq: event.seq,\n ...(typeof event.time === 'number' ? { time: event.time } : {}),\n }\n const data = record(event.data)\n if (data === undefined) return base\n\n switch (event.type) {\n case 'turn/start':\n case 'turn/end':\n return typeof data.turn === 'number' ? { ...base, data: { turn: data.turn } } : base\n\n case 'user/message': {\n const out: Record<string, unknown> = {}\n const content = projectContent(data.content, TEXT_CAP)\n if (content !== undefined) out.content = content\n const message = record(data.message)\n if (message !== undefined) {\n const m: Record<string, unknown> = {}\n if (typeof message.content === 'string' && message.content.length > 0) {\n m.content = clipText(message.content, TEXT_CAP)\n }\n const source = record(message.source)\n if (source !== undefined) {\n const s = projectSourceFields(source)\n if (s !== undefined) m.source = s\n }\n if (Object.keys(m).length > 0) out.message = m\n }\n const source = record(data.source)\n if (source !== undefined) {\n const s = projectSourceFields(source)\n if (s !== undefined) out.source = s\n }\n return Object.keys(out).length > 0 ? { ...base, data: out } : base\n }\n\n case 'assistant/message': {\n const out: Record<string, unknown> = {}\n const message = record(data.message)\n if (message !== undefined) {\n const content = projectContent(message.content, TEXT_CAP)\n if (content !== undefined) out.message = { content }\n }\n // The fold falls back to data.content when message.content is absent.\n const fallback = projectContent(data.content, TEXT_CAP)\n if (fallback !== undefined) out.content = fallback\n return Object.keys(out).length > 0 ? { ...base, data: out } : base\n }\n\n case 'tool/call': {\n const out: Record<string, unknown> = {}\n if (typeof data.name === 'string' && data.name.length > 0) out.name = data.name\n if (typeof data.arguments === 'string' && data.arguments.length > 0) {\n out.arguments = clipText(data.arguments, ARGS_CAP)\n }\n if (typeof data.callId === 'string' && data.callId.length > 0) out.callId = data.callId\n const source = sourceOf(data)\n if (source !== undefined) {\n const s = projectSourceFields(source)\n if (s !== undefined && typeof s.callId === 'string') out.message = { source: s }\n }\n return Object.keys(out).length > 0 ? { ...base, data: out } : base\n }\n\n case 'tool/result': {\n // The fold reads only error.name / error.code (plus call ids);\n // message.content \u2014 the bulky payload \u2014 is intentionally dropped.\n const out: Record<string, unknown> = {}\n const error = record(data.error)\n if (error !== undefined) {\n const e: Record<string, unknown> = {}\n if (typeof error.name === 'string' && error.name.length > 0) e.name = error.name\n if (typeof error.code === 'string' && error.code.length > 0) e.code = error.code\n if (Object.keys(e).length > 0) out.error = e\n }\n if (typeof data.callId === 'string' && data.callId.length > 0) out.callId = data.callId\n const source = sourceOf(data)\n if (source !== undefined) {\n const s = projectSourceFields(source)\n if (s !== undefined && typeof s.callId === 'string') out.message = { source: s }\n }\n return Object.keys(out).length > 0 ? { ...base, data: out } : base\n }\n\n case 'compaction/summary': {\n const summary = projectContent(data.summary, TEXT_CAP)\n return summary !== undefined ? { ...base, data: { summary } } : base\n }\n\n default:\n // command/run and unknown types: the fold ignores their data.\n return base\n }\n}\n", "/**\n * Host half of dsh-turn-outline: one incremental, read-only route that serves\n * session events for the trail panel, behind a browser-trust fence.\n *\n * POST /turntrail/api/events { sessionId, afterSeq }\n * \u2192 200 { events, hasMore } (one PAGE_SIZE page, ascending seq)\n * \u2192 400 bad request / missing sessionId\n * \u2192 403 fence rejection\n * \u2192 404 session-not-found\n * \u2192 405 non-POST\n * \u2192 413 oversized request body\n *\n * Two read paths, hottest first:\n *\n * LIVE sessions (the trail's hot case \u2014 the session is generating):\n * borrow the live store's frozen `events` snapshot. The probe is O(1)\n * (`events.at(-1).seq`), the tail slice is O(delta), and the whole path\n * is synchronous \u2014 no await between borrowing the snapshot and writing\n * the response, so no interleaved mutation. Zero deep clones: the store\n * already deep-freezes its events, and `JSON.stringify` reads frozen\n * objects fine. (The sessionQuery equivalents each `structuredClone`\n * EVERY event \u2014 3\u00D7 per request through listEvents + readSession \u2014 which\n * hammered the very process running the agent.)\n *\n * COLD / persisted sessions (idle, pulled only on activation):\n * sessionQuery with a per-session lastSeq cache, exactly as before.\n *\n * inject = ['webServer', 'sessionQuery', 'sessions'] \u2014 all host services;\n * `sessions` is core DSH (sessionQuery itself depends on it), and the code\n * feature-detects it so foreign contexts degrade to the persisted path.\n */\nimport type {\n HostCtx,\n HttpHeadersLike,\n HttpRequestLike,\n HttpResponseLike,\n LiveSessionLike,\n SessionEventLike,\n TrailEventsRequest,\n} from './context-types'\nimport { MAX_BODY_BYTES, PAGE_SIZE, projectEvent } from './wire'\n\nexport const inject = ['webServer', 'sessionQuery', 'sessions']\n\n/** Per-session cache: { lastSeq, events } refreshed only on growth (LRU). */\ninterface CacheEntry {\n lastSeq: number\n events: SessionEventLike[]\n bytes: number\n}\nconst trailCache = new Map<string, CacheEntry>()\nconst CACHE_MAX_ENTRIES = 64\nconst CACHE_MAX_BYTES = 32 * 1024 * 1024\nlet cacheBytes = 0\n\n// \u2500\u2500 Browser-trust fence (DNS-rebinding / cross-site defense, not auth) \u2500\u2500\u2500\u2500\n\nfunction header(headers: HttpHeadersLike, name: string): string | undefined {\n const value = (headers as Record<string, unknown>)[name]\n return typeof value === 'string' ? value : undefined\n}\n\nfunction parseAuthority(authority: string): URL | undefined {\n try {\n return new URL(`http://${authority}`)\n } catch {\n return undefined\n }\n}\n\nexport function isLoopbackHostname(hostname: string): boolean {\n if (hostname === 'localhost' || hostname === '[::1]') return true\n const parts = hostname.split('.')\n return parts.length === 4\n && parts[0] === '127'\n && parts.every((part) => /^\\d{1,3}$/.test(part) && Number(part) <= 255)\n}\n\n/**\n * Decide whether one request may reach the trail routes. Behaviorally\n * identical to DSH's /api gateway fence (api-request-trust + loopback-hostname):\n * Host must resolve to loopback; cross-site browser markers refuse; an Origin\n * (when present) must name the same hostname.\n */\nexport function isTrustedApiRequest(request: { headers: HttpHeadersLike }): boolean {\n const host = header(request.headers, 'host')\n if (host === undefined) return false\n const hostUrl = parseAuthority(host)\n if (hostUrl === undefined || !isLoopbackHostname(hostUrl.hostname)) return false\n if (header(request.headers, 'sec-fetch-site') === 'cross-site') return false\n const origin = header(request.headers, 'origin')\n if (origin === undefined) return true\n try {\n return new URL(origin).hostname === hostUrl.hostname\n } catch {\n return false\n }\n}\n\n// \u2500\u2500 Route helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction sendJson(res: HttpResponseLike, status: number, body: unknown): void {\n const text = JSON.stringify(body)\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'content-length': String(Buffer.byteLength(text)),\n })\n res.end(text)\n}\n\n/** Sentinel for an oversized request body (mapped to 413). */\nconst BODY_TOO_LARGE = Symbol('trail-body-too-large')\n\nasync function readJsonBody(req: HttpRequestLike): Promise<TrailEventsRequest | typeof BODY_TOO_LARGE> {\n const declared = header(req.headers, 'content-length')\n if (declared !== undefined) {\n const declaredBytes = Number(declared)\n if (Number.isFinite(declaredBytes) && declaredBytes > MAX_BODY_BYTES) return BODY_TOO_LARGE\n }\n const chunks: Buffer[] = []\n let total = 0\n for await (const chunk of req) {\n const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), 'utf8')\n total += buf.length\n if (total > MAX_BODY_BYTES) return BODY_TOO_LARGE\n chunks.push(buf)\n }\n const text = Buffer.concat(chunks).toString('utf8')\n try {\n const parsed = JSON.parse(text.length > 0 ? text : '{}')\n return parsed !== null && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n}\n\nfunction afterSeqOf(value: unknown): number {\n const n = Number(value)\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0\n}\n\n/** First index whose event seq is strictly greater than afterSeq (ascending seqs). */\nfunction upperBound(events: ReadonlyArray<{ seq?: number }>, afterSeq: number): number {\n let lo = 0\n let hi = events.length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n const seq = events[mid].seq\n if (typeof seq === 'number' && seq <= afterSeq) lo = mid + 1\n else hi = mid\n }\n return lo\n}\n\n/** Borrow the live store's frozen snapshot \u2014 O(1) amortized, zero copies. */\nfunction liveSessionOf(ctx: HostCtx, sessionId: string): LiveSessionLike | undefined {\n const sessions = ctx.sessions\n if (sessions === undefined || typeof sessions.get !== 'function') return undefined\n try {\n const session = sessions.get(sessionId)\n if (session === null || session === undefined || typeof session !== 'object') return undefined\n return Array.isArray((session as { events?: unknown }).events)\n ? (session as LiveSessionLike)\n : undefined\n } catch {\n return undefined\n }\n}\n\n/** Slice one ascending page after afterSeq; `project` trims the wire fields. */\nfunction respondPage(\n res: HttpResponseLike,\n events: ReadonlyArray<SessionEventLike>,\n afterSeq: number,\n project: boolean,\n): void {\n const lo = upperBound(events, afterSeq)\n const end = Math.min(events.length, lo + PAGE_SIZE)\n const tail: SessionEventLike[] = []\n for (let i = lo; i < end; i += 1) tail.push(project ? projectEvent(events[i]) : events[i])\n sendJson(res, 200, { events: tail, hasMore: end < events.length })\n}\n\n/** Insert/refresh one cache entry, then evict past the entry/byte budgets. */\nfunction cacheStore(sessionId: string, lastSeq: number, events: SessionEventLike[]): void {\n const bytes = Buffer.byteLength(JSON.stringify(events))\n const existing = trailCache.get(sessionId)\n if (existing !== undefined) {\n trailCache.delete(sessionId)\n cacheBytes -= existing.bytes\n }\n trailCache.set(sessionId, { lastSeq, events, bytes })\n cacheBytes += bytes\n while (trailCache.size > CACHE_MAX_ENTRIES || (cacheBytes > CACHE_MAX_BYTES && trailCache.size > 1)) {\n const oldest = trailCache.keys().next().value\n if (oldest === undefined) break\n const entry = trailCache.get(oldest)\n trailCache.delete(oldest)\n cacheBytes -= entry?.bytes ?? 0\n }\n}\n\nexport function apply(ctx: HostCtx): void {\n ctx.effect(() => ctx.webServer.register({\n kind: 'exact',\n path: '/turntrail/api/events',\n handler: async (rawReq, rawRes) => {\n const req = rawReq as HttpRequestLike\n const res = rawRes as HttpResponseLike\n\n if (!isTrustedApiRequest({ headers: req.headers })) {\n sendJson(res, 403, { error: 'forbidden' })\n return\n }\n if (req.method !== 'POST') {\n sendJson(res, 405, { error: 'method not allowed' })\n return\n }\n const body = await readJsonBody(req)\n if (body === BODY_TOO_LARGE) {\n sendJson(res, 413, { error: 'payload too large' })\n return\n }\n const sessionId = typeof body.sessionId === 'string' ? body.sessionId : ''\n if (sessionId.length === 0) {\n sendJson(res, 400, { error: 'sessionId is required' })\n return\n }\n const afterSeq = afterSeqOf(body.afterSeq)\n\n try {\n // Live fast path: synchronous, O(1) probe, O(delta) projection.\n const live = liveSessionOf(ctx, sessionId)\n if (live !== undefined) {\n respondPage(res, live.events, afterSeq, true)\n return\n }\n\n // Persisted path: lastSeq probe first; re-read the log only when it grew.\n let lastSeq = -1\n try {\n const records = await ctx.sessionQuery.listEvents(sessionId)\n const last = records[records.length - 1]\n lastSeq = last !== undefined && typeof last.seq === 'number' ? last.seq : -1\n } catch {\n lastSeq = -1\n }\n const cached = trailCache.get(sessionId)\n let base: SessionEventLike[]\n if (cached !== undefined && lastSeq >= 0 && cached.lastSeq === lastSeq) {\n base = cached.events\n // LRU touch: re-insert at the fresh end.\n trailCache.delete(sessionId)\n trailCache.set(sessionId, cached)\n } else {\n const snapshot = await ctx.sessionQuery.readSession(sessionId)\n base = (Array.isArray(snapshot.events) ? snapshot.events : []).map(projectEvent)\n cacheStore(sessionId, lastSeq, base)\n }\n // base is already projected (idempotently); serve without re-projecting.\n respondPage(res, base, afterSeq, false)\n } catch {\n sendJson(res, 404, { error: 'session-not-found' })\n }\n },\n }))\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["source"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "dsh-turn-outline",
3
+ "version": "0.2.5",
4
+ "description": "Turn-outline sidebar tab for dsh-better-sidebar: fold a session into user turns (input + tool steps + output) with structured jump-back into the conversation | DSH 轮次轨迹侧边栏:按用户轮次折叠会话(输入+工具步骤+输出),一键定位回对话原位",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "cordis.patch.yml",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "node build.mjs",
20
+ "prepack": "node build.mjs",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "vitest run",
23
+ "pack": "pnpm pack"
24
+ },
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ },
29
+ "client": {
30
+ "platform": "web",
31
+ "inject": [
32
+ "@deepseek-ai/dsh-client-runtime",
33
+ "@deepseek-ai/dsh-client-ui-primitives"
34
+ ]
35
+ }
36
+ },
37
+ "peerDependencies": {
38
+ "dsh-better-sidebar": "^0.17.0",
39
+ "react": "^18.2.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "dsh-better-sidebar": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@testing-library/dom": "^10.4.0",
48
+ "@testing-library/react": "^16.1.0",
49
+ "@types/node": "^22.10.0",
50
+ "@types/react": "^18.3.1",
51
+ "@types/react-dom": "^18.3.0",
52
+ "esbuild": "^0.25.0",
53
+ "jsdom": "^26.0.0",
54
+ "react": "^18.2.0",
55
+ "react-dom": "^18.2.0",
56
+ "typescript": "^5.6.0",
57
+ "vitest": "^3.2.0"
58
+ },
59
+ "engines": {
60
+ "node": ">=18"
61
+ },
62
+ "packageManager": "pnpm@11.8.0",
63
+ "license": "MIT",
64
+ "author": "Andor <zyfsta@outlook.com>",
65
+ "homepage": "https://github.com/Andor-Z/dsh-turn-outline",
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/Andor-Z/dsh-turn-outline.git"
69
+ },
70
+ "bugs": {
71
+ "url": "https://github.com/Andor-Z/dsh-turn-outline/issues"
72
+ },
73
+ "keywords": [
74
+ "dsh",
75
+ "dsh-plugin",
76
+ "deepseek-harness",
77
+ "deepseek",
78
+ "cordis",
79
+ "webui",
80
+ "better-sidebar",
81
+ "turn-outline",
82
+ "trajectory",
83
+ "outline",
84
+ "sidebar"
85
+ ]
86
+ }