dsh-thoughtdag 0.5.5 → 0.5.6
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/dist-app/assets/{canonical-DyO4cfs8.js → canonical-CSoZNuPg.js} +2 -2
- package/dist-app/assets/{canvas-record-B106EgJV.js → canvas-record-wnrN3Xiq.js} +1 -1
- package/dist-app/assets/{claude-code-session-COqNVArs.js → claude-code-session-Cjh9ZQd-.js} +1 -1
- package/dist-app/assets/{codex-session-CVhXEocv.js → codex-session-Dgq2Nsm-.js} +1 -1
- package/dist-app/assets/{dsh-session-y6pdDHlg.js → dsh-session-B9EsawUx.js} +1 -1
- package/dist-app/assets/{experiment-loop-Bj2wGvJp.js → experiment-loop-CW1DebVY.js} +1 -1
- package/dist-app/assets/{http-bridge-TYkQm1R3.js → http-bridge-DG_nZVjI.js} +1 -1
- package/dist-app/assets/index-B1IC85BS.js +914 -0
- package/dist-app/assets/index-CgnmeNGy.css +1 -0
- package/dist-app/assets/{index-BndAg25p.js → index-D7EuGCbC.js} +1 -1
- package/dist-app/assets/{index-D2RofBbL.js → index-Fb-Q079K.js} +1 -1
- package/dist-app/assets/{index-BZP0zxsA.js → index-rGP3NmkV.js} +3 -3
- package/dist-app/assets/local-ladder.worker-Dj3KyWai.js +1 -0
- package/dist-app/assets/{pi-session-fSCIsceL.js → pi-session-1yVM3dMC.js} +1 -1
- package/dist-app/assets/{sensitive-scan-_-YPfT-P.js → sensitive-scan-CROt_JlT.js} +1 -1
- package/dist-app/assets/{session-handoff-rVZFB7ll.js → session-handoff-r3h38IOI.js} +2 -2
- package/dist-app/assets/{shared-svBDp9_R.js → shared-ABEucpDy.js} +1 -1
- package/dist-app/assets/{turndown-plugin-gfm.cjs-DBiLx1SX.js → turndown-plugin-gfm.cjs-C6ig148J.js} +1 -1
- package/dist-app/assets/update-check-Bo9R7uYE.js +1 -0
- package/dist-app/index.html +2 -2
- package/lib/client.js +33 -8
- package/lib/index.js +183 -11
- package/lib/why.mjs +56 -0
- package/package.json +1 -1
- package/dist-app/assets/index-CqebPxzE.css +0 -1
- package/dist-app/assets/index-DR0C_9n-.js +0 -912
- package/dist-app/assets/local-ladder.worker-BkmzXrqq.js +0 -1
- package/dist-app/assets/update-check-n2hvSdhA.js +0 -1
package/lib/index.js
CHANGED
|
@@ -293,7 +293,11 @@ function decompressZstdToText(raw) {
|
|
|
293
293
|
* `session.jsonl.zstd`, while session format V3 (0.1.5-rc.2 and later) uses
|
|
294
294
|
* `session.v3.jsonl.zstd`. Probe the known names, newest first, so a listing
|
|
295
295
|
* keeps working across both. */
|
|
296
|
-
|
|
296
|
+
// `session.jsonl.zstd` (≤ 0.1.4), `session.v3.jsonl.zstd` (0.1.5), `session.v4.jsonl.zstd` (the 0.2
|
|
297
|
+
// desktop) …: the session directory is listed and the highest version wins, so the next format
|
|
298
|
+
// version is found before this file learns its name (the 0.2 desktop's own sessions were invisible
|
|
299
|
+
// while only v3 was probed)
|
|
300
|
+
const SESSION_LOG_RE = /^session(?:\.v(\d+))?\.jsonl\.zstd?$/
|
|
297
301
|
async function findSessionFiles(dshHome) {
|
|
298
302
|
const root = join(dshHome, 'sessions')
|
|
299
303
|
const out = []
|
|
@@ -305,13 +309,13 @@ async function findSessionFiles(dshHome) {
|
|
|
305
309
|
try { sessions = await readdir(join(root, ws.name), { withFileTypes: true }) } catch { continue }
|
|
306
310
|
for (const s of sessions) {
|
|
307
311
|
if (!s.isDirectory()) continue
|
|
308
|
-
let
|
|
312
|
+
let names
|
|
313
|
+
try { names = await readdir(join(root, ws.name, s.name)) } catch { continue }
|
|
314
|
+
const best = names.map(n => ({ n, m: SESSION_LOG_RE.exec(n) })).filter(x => x.m !== null).sort((a, b) => Number(b.m[1] ?? 0) - Number(a.m[1] ?? 0))[0]
|
|
315
|
+
if (best === undefined) continue
|
|
316
|
+
const log = join(root, ws.name, s.name, best.n)
|
|
309
317
|
let st
|
|
310
|
-
|
|
311
|
-
const candidate = join(root, ws.name, s.name, name)
|
|
312
|
-
try { st = await stat(candidate); log = candidate; break } catch { /* try the next format */ }
|
|
313
|
-
}
|
|
314
|
-
if (log === undefined || st === undefined) continue
|
|
318
|
+
try { st = await stat(log) } catch { continue }
|
|
315
319
|
out.push({ dir: ws.name, sessionDir: s.name, log, size: st.size, mtime: st.mtimeMs })
|
|
316
320
|
}
|
|
317
321
|
}
|
|
@@ -534,7 +538,7 @@ async function visionIdsOf(ctx, providerId, catalogModels) {
|
|
|
534
538
|
return out
|
|
535
539
|
}
|
|
536
540
|
|
|
537
|
-
async function modelsPayload(ctx) {
|
|
541
|
+
async function modelsPayload(ctx, ownProviders = []) {
|
|
538
542
|
const cat = await ctx.sessionController.modelCatalog()
|
|
539
543
|
// the harness itself, as an entry: the agent loop with tools, not a bare model
|
|
540
544
|
// the harness's agent loop with tools, once per model it can run on —
|
|
@@ -549,15 +553,137 @@ async function modelsPayload(ctx) {
|
|
|
549
553
|
}
|
|
550
554
|
}
|
|
551
555
|
models.push(...agents)
|
|
556
|
+
// the person's own interfaces follow, under their own names; a harness id wins a collision
|
|
557
|
+
const own = ownModelEntries(ownProviders, new Set(models.map(m => m.id)))
|
|
558
|
+
models.push(...own)
|
|
552
559
|
const def = cat.default ? `${cat.default.provider}/${cat.default.model}` : null
|
|
553
560
|
return {
|
|
554
561
|
models,
|
|
555
562
|
default: def && models.some(m => m.id === def) ? def : (models[0]?.id ?? null),
|
|
556
|
-
capabilities: { webSearch:
|
|
563
|
+
capabilities: { webSearch: ownProviders.some(p => isOpenRouterURL(p.baseURL)), searchEngine: 'none', scholarSearch: false, vision: models.some(m => m.vision) },
|
|
557
564
|
harness: { routableProviders: cat.routableProviders ?? [], failures: (cat.failures ?? []).map(f => ({ provider: f.id, message: f.message })) },
|
|
558
565
|
}
|
|
559
566
|
}
|
|
560
567
|
|
|
568
|
+
// ── the person's own interfaces ────────────────────────────────────────
|
|
569
|
+
// The canvas keeps API interfaces in the browser (the same list the desktop
|
|
570
|
+
// app has) and sends them along with every call and with a registration
|
|
571
|
+
// at boot (POST /runtime-providers). Here they are spoken to directly over
|
|
572
|
+
// the OpenAI-compatible protocol, so a key added inside the harness works
|
|
573
|
+
// the way it does in the app; the harness's own models are untouched.
|
|
574
|
+
const OWN_PROVIDER_CAP = 12
|
|
575
|
+
const OWN_MODEL_CAP = 60
|
|
576
|
+
const isOpenRouterURL = (u) => /openrouter\.ai/i.test(String(u ?? ''))
|
|
577
|
+
|
|
578
|
+
/** The providers a request or a registration carries, sanitised. */
|
|
579
|
+
function ownProvidersOf(list) {
|
|
580
|
+
const out = []
|
|
581
|
+
for (const p of (Array.isArray(list) ? list : []).slice(0, OWN_PROVIDER_CAP)) {
|
|
582
|
+
const baseURL = String(p?.baseURL ?? '').trim().replace(/\/+$/, '')
|
|
583
|
+
if (!/^https?:\/\//i.test(baseURL)) continue
|
|
584
|
+
const name = String(p?.name || 'Custom').slice(0, 40)
|
|
585
|
+
const models = (Array.isArray(p?.models) ? p.models : [])
|
|
586
|
+
.map(m => (typeof m === 'string' ? { id: m } : m))
|
|
587
|
+
.filter(m => m && typeof m.id === 'string' && m.id)
|
|
588
|
+
.slice(0, OWN_MODEL_CAP)
|
|
589
|
+
.map(m => ({ id: m.id, ...(typeof m.vision === 'boolean' ? { vision: m.vision } : {}) }))
|
|
590
|
+
if (models.length === 0) continue
|
|
591
|
+
out.push({ name, baseURL, apiKey: typeof p?.apiKey === 'string' ? p.apiKey : '', models })
|
|
592
|
+
}
|
|
593
|
+
return out
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Model entries for the picker: the model's short id and its interface's name, like the app shows them. */
|
|
597
|
+
function ownModelEntries(providers, taken) {
|
|
598
|
+
const entries = []
|
|
599
|
+
for (const p of providers) {
|
|
600
|
+
for (const m of p.models) {
|
|
601
|
+
if (taken.has(m.id)) continue
|
|
602
|
+
taken.add(m.id)
|
|
603
|
+
const shortId = m.id.includes('/') ? m.id.split('/').slice(1).join('/') : m.id
|
|
604
|
+
entries.push({ id: m.id, name: `${shortId} (${p.name})`, provider: p.name, vision: m.vision })
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return entries
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** The interface a requested model belongs to, from the request's own list first, then the registered list. */
|
|
611
|
+
function ownTargetOf(requested, fromBody, registered) {
|
|
612
|
+
if (typeof requested !== 'string' || !requested) return null
|
|
613
|
+
for (const p of [...fromBody, ...registered]) if (p.models.some(m => m.id === requested)) return { provider: p, model: requested }
|
|
614
|
+
return null
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The canvas's { role, content } messages and images as OpenAI-compatible messages. */
|
|
618
|
+
function ownMessagesOf(body) {
|
|
619
|
+
const messages = []
|
|
620
|
+
for (const m of Array.isArray(body?.messages) ? body.messages : []) {
|
|
621
|
+
if (!m || typeof m.content !== 'string') continue
|
|
622
|
+
messages.push({ role: m.role === 'system' ? 'system' : m.role === 'assistant' ? 'assistant' : 'user', content: m.content })
|
|
623
|
+
}
|
|
624
|
+
if (messages.length === 0) throw new HttpError(400, 'messages: nothing to send')
|
|
625
|
+
const images = Array.isArray(body?.images) ? body.images.filter(i => i && typeof i.data === 'string' && IMAGE_MEDIA.has(i.mimeType)) : []
|
|
626
|
+
if (images.length > 0) {
|
|
627
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
628
|
+
if (messages[i].role !== 'user') continue
|
|
629
|
+
messages[i] = { role: 'user', content: [{ type: 'text', text: messages[i].content }, ...images.map(img => ({ type: 'image_url', image_url: { url: `data:${img.mimeType};base64,${img.data}` } }))] }
|
|
630
|
+
break
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return messages
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** One chat-completions request to the interface; the response as fetched (the caller reads it). */
|
|
637
|
+
async function ownRequest(target, body, stream) {
|
|
638
|
+
const { provider, model } = target
|
|
639
|
+
// OpenRouter searches through its `:online` variant and takes a reasoning switch; a fast call never thinks out loud
|
|
640
|
+
const openrouter = isOpenRouterURL(provider.baseURL)
|
|
641
|
+
const id = openrouter && body?.webSearch && !model.endsWith(':online') ? `${model}:online` : model
|
|
642
|
+
const payload = { model: id, messages: ownMessagesOf(body), stream, ...(openrouter ? { reasoning: { enabled: !body?.fast } } : {}) }
|
|
643
|
+
const headers = { 'content-type': 'application/json', ...(provider.apiKey ? { authorization: `Bearer ${provider.apiKey}` } : {}), ...(openrouter ? { 'HTTP-Referer': 'https://github.com/chenxiachan/thoughtdag', 'X-Title': 'ThoughtDAG' } : {}) }
|
|
644
|
+
const res = await fetch(`${provider.baseURL}/chat/completions`, { method: 'POST', headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(10 * 60 * 1000) })
|
|
645
|
+
if (!res.ok) {
|
|
646
|
+
const text = await res.text().catch(() => '')
|
|
647
|
+
let detail = text.slice(0, 300)
|
|
648
|
+
try { const j = JSON.parse(text); detail = j?.error?.message ?? j?.message ?? detail } catch { /* not json */ }
|
|
649
|
+
throw new HttpError(res.status === 401 || res.status === 403 ? 401 : 502, `${provider.name}: HTTP ${res.status}${detail ? ' · ' + detail : ''}`)
|
|
650
|
+
}
|
|
651
|
+
return res
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Stream one answer from the interface as the canvas's frames: { text }, { reasoning }, [DONE]. */
|
|
655
|
+
async function ownStream(res, target, body, isClosed) {
|
|
656
|
+
const upstream = await ownRequest(target, body, true)
|
|
657
|
+
const decoder = new TextDecoder()
|
|
658
|
+
let buffer = ''
|
|
659
|
+
for await (const chunk of upstream.body) {
|
|
660
|
+
if (isClosed()) break
|
|
661
|
+
buffer += decoder.decode(chunk, { stream: true })
|
|
662
|
+
let nl
|
|
663
|
+
while ((nl = buffer.indexOf('\n')) >= 0) {
|
|
664
|
+
const line = buffer.slice(0, nl).trim(); buffer = buffer.slice(nl + 1)
|
|
665
|
+
if (!line.startsWith('data:')) continue
|
|
666
|
+
const data = line.slice(5).trim()
|
|
667
|
+
if (data === '[DONE]') return
|
|
668
|
+
let parsed
|
|
669
|
+
try { parsed = JSON.parse(data) } catch { continue }
|
|
670
|
+
if (parsed?.error) { res.write(`data: ${JSON.stringify({ error: parsed.error.message ?? String(parsed.error) })}\n\n`); return }
|
|
671
|
+
const delta = parsed?.choices?.[0]?.delta ?? {}
|
|
672
|
+
const reasoning = delta.reasoning_content ?? delta.reasoning
|
|
673
|
+
if (typeof reasoning === 'string' && reasoning) res.write(`data: ${JSON.stringify({ reasoning })}\n\n`)
|
|
674
|
+
if (typeof delta.content === 'string' && delta.content) res.write(`data: ${JSON.stringify({ text: delta.content })}\n\n`)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** One whole answer from the interface (the canvas's background calls). */
|
|
680
|
+
async function ownCall(target, body) {
|
|
681
|
+
const upstream = await ownRequest(target, body, false)
|
|
682
|
+
const j = await upstream.json()
|
|
683
|
+
const c = j?.choices?.[0]?.message?.content
|
|
684
|
+
return typeof c === 'string' ? c : Array.isArray(c) ? c.map(x => (typeof x?.text === 'string' ? x.text : '')).join('') : ''
|
|
685
|
+
}
|
|
686
|
+
|
|
561
687
|
const IMAGE_MEDIA = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
|
|
562
688
|
|
|
563
689
|
/** The SPA's { role, content } messages as harness Messages; system lines
|
|
@@ -834,7 +960,7 @@ async function visionModel(ctx) {
|
|
|
834
960
|
|
|
835
961
|
/** Resolve the requested model against the catalog, falling back to its default. */
|
|
836
962
|
async function targetOf(ctx, requested) {
|
|
837
|
-
const payload = await modelsPayload(ctx)
|
|
963
|
+
const payload = await modelsPayload(ctx) // the harness's own; the person's interfaces are resolved before this is asked
|
|
838
964
|
const id = requested && payload.models.some(m => m.id === requested) ? requested : payload.default
|
|
839
965
|
const t = splitModelId(id)
|
|
840
966
|
if (!t) throw new HttpError(503, 'no model is routable in this harness')
|
|
@@ -943,6 +1069,8 @@ export async function apply(ctx, config) {
|
|
|
943
1069
|
: '/thoughtdag'
|
|
944
1070
|
const trustedHosts = new Set(['localhost', '127.0.0.1', ...[...(config?.trustedHosts ?? [])].map(h => String(h).trim().toLowerCase()).filter(Boolean)])
|
|
945
1071
|
|
|
1072
|
+
// the interfaces the canvas registered (memory only: a restart forgets them, the canvas registers again at boot)
|
|
1073
|
+
let registeredProviders = []
|
|
946
1074
|
const api = async (req, res) => {
|
|
947
1075
|
try {
|
|
948
1076
|
const hostname = (typeof req.headers.host === 'string' ? req.headers.host : '').replace(/:\d+$/, '').toLowerCase()
|
|
@@ -1010,6 +1138,7 @@ export async function apply(ctx, config) {
|
|
|
1010
1138
|
const cwd = url.searchParams.get('cwd') ?? undefined
|
|
1011
1139
|
return sendJson(res, 200, await whyLib.findJson(phrase, { scope, limit, ...(cwd ? { cwd } : {}) }))
|
|
1012
1140
|
}
|
|
1141
|
+
if (path === '/why/turns') return sendJson(res, 200, await whyLib.turnsJson({ offset: Number(url.searchParams.get('offset') ?? 0) || 0, ...(url.searchParams.get('limit') === null ? {} : { limit: Number(url.searchParams.get('limit')) || 0 }), head: Number(url.searchParams.get('head') ?? 300) || 300 }))
|
|
1013
1142
|
if (path === '/why/recall') {
|
|
1014
1143
|
try { return sendJson(res, 200, await whyLib.recallJson(url.searchParams.get('session') ?? '', Number(url.searchParams.get('turn') ?? 0))) }
|
|
1015
1144
|
catch (e) { return sendJson(res, 404, { error: e instanceof Error ? e.message : String(e) }) }
|
|
@@ -1089,7 +1218,34 @@ export async function apply(ctx, config) {
|
|
|
1089
1218
|
if (await agentsHttp().handle(req, res, path, body)) return
|
|
1090
1219
|
}
|
|
1091
1220
|
// ── model connection (the SPA's proxy protocol, on the harness's providers) ──
|
|
1092
|
-
if (path === '/models' && req.method === 'GET') return sendJson(res, 200, await modelsPayload(ctx))
|
|
1221
|
+
if (path === '/models' && req.method === 'GET') return sendJson(res, 200, await modelsPayload(ctx, registeredProviders))
|
|
1222
|
+
// the canvas registers its browser-stored interfaces at boot (and after a change): the list answers with them in it
|
|
1223
|
+
// the interface dialog asks an endpoint what it serves (the /models protocol standard), through this host
|
|
1224
|
+
if (path === '/probe-models' && req.method === 'POST') {
|
|
1225
|
+
const body = await readJson(req, MAX_WRITE_BODY_BYTES)
|
|
1226
|
+
const baseURL = String(body?.baseURL ?? '').trim().replace(/\/+$/, '')
|
|
1227
|
+
if (!/^https?:\/\//i.test(baseURL)) return sendJson(res, 400, { error: 'baseURL required' })
|
|
1228
|
+
try {
|
|
1229
|
+
const r = await fetch(`${baseURL}/models`, { headers: body?.apiKey ? { authorization: `Bearer ${body.apiKey}` } : {}, signal: AbortSignal.timeout(15000) })
|
|
1230
|
+
if (!r.ok) return sendJson(res, r.status, { error: `endpoint answered HTTP ${r.status}` })
|
|
1231
|
+
const j = await r.json()
|
|
1232
|
+
const list = Array.isArray(j?.data) ? j.data : Array.isArray(j?.models) ? j.models : []
|
|
1233
|
+
const models = list.map(m => ({
|
|
1234
|
+
id: typeof (m?.id ?? m?.name) === 'string' ? (m.id ?? m.name).replace(/^models\//, '') : '',
|
|
1235
|
+
...(typeof m?.created === 'number' ? { created: m.created } : {}),
|
|
1236
|
+
...(Array.isArray(m?.architecture?.input_modalities) ? { vision: m.architecture.input_modalities.includes('image') } : {}),
|
|
1237
|
+
...(typeof m?.context_length === 'number' ? { contextLength: m.context_length } : {}),
|
|
1238
|
+
})).filter(m => m.id)
|
|
1239
|
+
return sendJson(res, 200, { models })
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
return sendJson(res, 502, { error: 'could not reach the endpoint: ' + (error instanceof Error ? error.message : String(error)) })
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (path === '/runtime-providers' && req.method === 'POST') {
|
|
1245
|
+
const body = await readJson(req, MAX_CALL_BODY_BYTES)
|
|
1246
|
+
registeredProviders = ownProvidersOf(body?.providers)
|
|
1247
|
+
return sendJson(res, 200, await modelsPayload(ctx, registeredProviders))
|
|
1248
|
+
}
|
|
1093
1249
|
const approvalRoute = /^\/approvals\/([^/]+)$/.exec(path)
|
|
1094
1250
|
if (approvalRoute !== null && req.method === 'POST') {
|
|
1095
1251
|
const id = decodeURIComponent(approvalRoute[1])
|
|
@@ -1119,6 +1275,22 @@ export async function apply(ctx, config) {
|
|
|
1119
1275
|
res.end()
|
|
1120
1276
|
return
|
|
1121
1277
|
}
|
|
1278
|
+
// the person's own interface, when the model is one of theirs
|
|
1279
|
+
const own = ownTargetOf(body?.model, ownProvidersOf(body?.providers), registeredProviders)
|
|
1280
|
+
if (own) {
|
|
1281
|
+
if (path === '/claude') return sendJson(res, 200, { text: await ownCall(own, body), model: own.model })
|
|
1282
|
+
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' })
|
|
1283
|
+
let closed = false
|
|
1284
|
+
req.on('close', () => { closed = true })
|
|
1285
|
+
try {
|
|
1286
|
+
await ownStream(res, own, body, () => closed)
|
|
1287
|
+
if (!closed) res.write('data: [DONE]\n\n')
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
if (!closed) res.write(`data: ${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n\n`)
|
|
1290
|
+
}
|
|
1291
|
+
res.end()
|
|
1292
|
+
return
|
|
1293
|
+
}
|
|
1122
1294
|
const target = await targetOf(ctx, body?.model)
|
|
1123
1295
|
const options = await callOf(ctx, body, target)
|
|
1124
1296
|
if (path === '/claude') {
|
package/lib/why.mjs
CHANGED
|
@@ -3093,6 +3093,61 @@ async function findJson(phrase, opts = {}) {
|
|
|
3093
3093
|
hits: shown.map((h) => ({ kind: h.kind, session: h.session.id, runner: h.session.runner, title: h.session.title, cwd: h.turn.cwd ?? h.session.cwd, file: h.session.file, turn: turnNumber(facts, h), at: h.turn.at ?? null, where: h.where, snippet: h.snippet, open: openLink(h) }))
|
|
3094
3094
|
};
|
|
3095
3095
|
}
|
|
3096
|
+
async function turnsJson(opts = {}) {
|
|
3097
|
+
if (opts.limit === 0) {
|
|
3098
|
+
const text = await loadText();
|
|
3099
|
+
return { total: Object.values(text.sessions).reduce((a, b) => a + b, 0), turns: [] };
|
|
3100
|
+
}
|
|
3101
|
+
const facts = await ensureFresh();
|
|
3102
|
+
const head = Math.max(40, Math.min(1e3, opts.head ?? 300));
|
|
3103
|
+
const turnsOf = /* @__PURE__ */ new Map();
|
|
3104
|
+
const lookup = (k, i) => {
|
|
3105
|
+
let m = turnsOf.get(k);
|
|
3106
|
+
if (!m) {
|
|
3107
|
+
m = new Map(facts.sessions[k]?.turns.map((t) => [t.i, t]) ?? []);
|
|
3108
|
+
turnsOf.set(k, m);
|
|
3109
|
+
}
|
|
3110
|
+
return m.get(i);
|
|
3111
|
+
};
|
|
3112
|
+
const clipHead = (text) => (text ?? "").replace(/\s+/g, " ").trim().slice(0, head);
|
|
3113
|
+
const raw = [];
|
|
3114
|
+
let lines = null;
|
|
3115
|
+
try {
|
|
3116
|
+
await fsp.access(TEXT_LINES);
|
|
3117
|
+
lines = createInterface({ input: createReadStream(TEXT_LINES, { encoding: "utf8", highWaterMark: 1 << 20 }) });
|
|
3118
|
+
} catch {
|
|
3119
|
+
return { total: 0, turns: [] };
|
|
3120
|
+
}
|
|
3121
|
+
for await (const line of lines) {
|
|
3122
|
+
let t;
|
|
3123
|
+
try {
|
|
3124
|
+
t = JSON.parse(line);
|
|
3125
|
+
} catch {
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
const session = facts.sessions[t.k];
|
|
3129
|
+
const turn = lookup(t.k, t.i);
|
|
3130
|
+
if (!session || !turn) continue;
|
|
3131
|
+
const memory = session.kind === "memory";
|
|
3132
|
+
raw.push({ session, sourceKey: t.k, turn, q: memory ? "" : clipHead(t.q), a: clipHead(memory ? t.m : t.a), kind: memory ? "memory" : "turn" });
|
|
3133
|
+
}
|
|
3134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3135
|
+
const all = [];
|
|
3136
|
+
for (const h of raw.sort((a, b) => a.session.mtime - b.session.mtime)) {
|
|
3137
|
+
if (h.turn.item) {
|
|
3138
|
+
if (seen.has(h.turn.item)) continue;
|
|
3139
|
+
seen.add(h.turn.item);
|
|
3140
|
+
}
|
|
3141
|
+
all.push(h);
|
|
3142
|
+
}
|
|
3143
|
+
all.sort((a, b) => (b.turn.at ?? "").localeCompare(a.turn.at ?? ""));
|
|
3144
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
3145
|
+
const limit = Math.max(0, opts.limit ?? all.length);
|
|
3146
|
+
return {
|
|
3147
|
+
total: all.length,
|
|
3148
|
+
turns: all.slice(offset, offset + limit).map((h) => ({ kind: h.kind, session: h.session.id, runner: h.session.runner, title: h.session.title, cwd: h.turn.cwd ?? h.session.cwd, file: h.session.file, turn: turnNumber(facts, h), at: h.turn.at ?? null, q: h.q, a: h.a, open: openLink(h) }))
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3096
3151
|
async function recallJson(session, n) {
|
|
3097
3152
|
return recallTurn(await ensureFresh(), session, n);
|
|
3098
3153
|
}
|
|
@@ -3534,6 +3589,7 @@ export {
|
|
|
3534
3589
|
suggestJson,
|
|
3535
3590
|
summarize,
|
|
3536
3591
|
topicsJson,
|
|
3592
|
+
turnsJson,
|
|
3537
3593
|
when,
|
|
3538
3594
|
zstdSkipped
|
|
3539
3595
|
};
|
package/package.json
CHANGED