opencode-sesh 2.1.4 → 2.1.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/AGENTS.md +9 -4
- package/CHANGELOG.md +7 -0
- package/dist/tui.js +114 -29
- package/package.json +3 -2
- package/tui-plugins/sesh-panel/tui.tsx +102 -29
package/AGENTS.md
CHANGED
|
@@ -31,6 +31,7 @@ bun run test # build + fixture-DB regression suite — must stay green
|
|
|
31
31
|
bun run test:changelog # release-tooling regressions (scripts/changelog.sh)
|
|
32
32
|
bun run test:picker # PTY picker suite
|
|
33
33
|
bun run test:tui-home # real OpenTUI home layout regressions (also in bun run test)
|
|
34
|
+
bun run test:tui-worker # real blob Worker + bun:sqlite regressions (also in bun run test)
|
|
34
35
|
bun run typecheck # tsc over tui-plugins/ and plugins/
|
|
35
36
|
bun run lint:sh # bash -n on every script
|
|
36
37
|
HOME=/tmp/fakehome bash install.sh # installer smoke test (never touches ~/.config)
|
|
@@ -181,10 +182,11 @@ when opencode exits, the picker reopens with the query preserved.
|
|
|
181
182
|
scrolling inside; the picker remains the place for transcript search.
|
|
182
183
|
Sessions waiting on the user (unanswered questions, stuck runs) pin a
|
|
183
184
|
**Needs input** group above the directories, with the count in the heading —
|
|
184
|
-
same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`. The query
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`. The query runs in
|
|
186
|
+
a blob Worker (`createDbClient`; failure means "local store unavailable"),
|
|
187
|
+
off the TUI thread but still costing worker CPU every poll, so the shared
|
|
188
|
+
poll runs it only while the sidebar is subscribed
|
|
189
|
+
(`poll(listener, { waiting: true })`); the home list never triggers it.
|
|
188
190
|
- **Picker:** a custom dialog (not `DialogSelect`) grouped by
|
|
189
191
|
project/directory, recency-ordered, resume via
|
|
190
192
|
`api.route.navigate("session", …)`. `option+o` and the command palette open it;
|
|
@@ -269,6 +271,9 @@ when opencode exits, the picker reopens with the query preserved.
|
|
|
269
271
|
and Python 3).
|
|
270
272
|
- `bun run test:tui-home` — real OpenTUI render regressions for the home list
|
|
271
273
|
(also run by `bun run test`).
|
|
274
|
+
- `bun run test:tui-worker` — starts the real blob Worker against a bun:sqlite
|
|
275
|
+
fixture; asserts results round-trip and the main thread keeps ticking during
|
|
276
|
+
a slow query (also run by `bun run test`).
|
|
272
277
|
- `bun run typecheck` — `tsc` over `tui-plugins/` and `plugins/`.
|
|
273
278
|
- `bun run lint:sh` — `bash -n` on every script.
|
|
274
279
|
- `HOME=/tmp/fakehome bash install.sh` — installer smoke test.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.5] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **tui:** Run needs-input SQLite reads in a worker (#34)
|
|
13
|
+
|
|
8
14
|
## [2.1.4] - 2026-11-02
|
|
9
15
|
|
|
10
16
|
### Fixed
|
|
@@ -536,6 +542,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
536
542
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
537
543
|
CI.
|
|
538
544
|
|
|
545
|
+
[2.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.4...v2.1.5
|
|
539
546
|
[2.1.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.3...v2.1.4
|
|
540
547
|
[2.1.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.2...v2.1.3
|
|
541
548
|
[2.1.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.1...v2.1.2
|
package/dist/tui.js
CHANGED
|
@@ -636,9 +636,106 @@ async function openTranscriptDb() {
|
|
|
636
636
|
return undefined;
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
|
+
|
|
640
|
+
// Runs in a blob Worker so slow queries never block the TUI thread. The
|
|
641
|
+
// connection opens on the first request and stays open; `loadSqlite` is
|
|
642
|
+
// injected only by tests.
|
|
643
|
+
const DB_WORKER_SOURCE = `
|
|
644
|
+
let opening
|
|
645
|
+
const load = typeof loadSqlite === "function" ? loadSqlite : () => import("bun:sqlite")
|
|
646
|
+
const open = (path) => opening ??= load().then((sqlite) => new sqlite.Database(path, { readonly: true }), (error) => { opening = undefined; throw error })
|
|
647
|
+
self.onmessage = async (event) => {
|
|
648
|
+
const { id, path, sql, params } = event.data
|
|
649
|
+
try {
|
|
650
|
+
const db = await open(path)
|
|
651
|
+
self.postMessage({ id, rows: db.query(sql).all(...(params ?? [])) })
|
|
652
|
+
} catch (error) {
|
|
653
|
+
self.postMessage({ id, error: String(error?.message ?? error) })
|
|
654
|
+
}
|
|
655
|
+
}`;
|
|
656
|
+
const DB_QUERY_TIMEOUT_MS = 30000;
|
|
657
|
+
function createDbWorker() {
|
|
658
|
+
return new globalThis.Worker(URL.createObjectURL(new Blob([DB_WORKER_SOURCE], {
|
|
659
|
+
type: "text/javascript"
|
|
660
|
+
})));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Read-only queries against the local store, served by one lazily started
|
|
664
|
+
// worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
|
|
665
|
+
// a timeout or worker error discards the worker so the next call starts afresh.
|
|
666
|
+
function createDbClient(createWorker = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
|
|
667
|
+
let worker;
|
|
668
|
+
let nextId = 0;
|
|
669
|
+
const pending = new Map();
|
|
670
|
+
const reset = error => {
|
|
671
|
+
const dead = worker;
|
|
672
|
+
worker = undefined;
|
|
673
|
+
try {
|
|
674
|
+
dead?.terminate();
|
|
675
|
+
} catch {}
|
|
676
|
+
const requests = [...pending.values()];
|
|
677
|
+
pending.clear();
|
|
678
|
+
for (const request of requests) {
|
|
679
|
+
clearTimeout(request.timer);
|
|
680
|
+
request.reject(error);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
const start = () => {
|
|
684
|
+
const created = createWorker();
|
|
685
|
+
created.onmessage = event => {
|
|
686
|
+
const {
|
|
687
|
+
id,
|
|
688
|
+
rows,
|
|
689
|
+
error
|
|
690
|
+
} = event?.data ?? {};
|
|
691
|
+
const request = pending.get(id);
|
|
692
|
+
if (!request) return;
|
|
693
|
+
pending.delete(id);
|
|
694
|
+
clearTimeout(request.timer);
|
|
695
|
+
if (typeof error === "string") request.reject(new Error(error));else request.resolve(Array.isArray(rows) ? rows : []);
|
|
696
|
+
};
|
|
697
|
+
created.onerror = event => {
|
|
698
|
+
event?.preventDefault?.();
|
|
699
|
+
if (worker === created) reset(new Error(String(event?.message ?? "local store worker failed")));
|
|
700
|
+
};
|
|
701
|
+
created.unref?.();
|
|
702
|
+
return created;
|
|
703
|
+
};
|
|
704
|
+
const run = (sql, params = []) => new Promise((resolve, reject) => {
|
|
705
|
+
try {
|
|
706
|
+
worker ??= start();
|
|
707
|
+
} catch (error) {
|
|
708
|
+
reject(error);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const id = ++nextId;
|
|
712
|
+
const timer = setTimeout(() => reset(new Error("local store query timed out")), timeoutMs);
|
|
713
|
+
timer.unref?.();
|
|
714
|
+
pending.set(id, {
|
|
715
|
+
resolve,
|
|
716
|
+
reject,
|
|
717
|
+
timer
|
|
718
|
+
});
|
|
719
|
+
try {
|
|
720
|
+
worker.postMessage({
|
|
721
|
+
id,
|
|
722
|
+
path: resolveDbPath(),
|
|
723
|
+
sql,
|
|
724
|
+
params
|
|
725
|
+
});
|
|
726
|
+
} catch (error) {
|
|
727
|
+
reset(error);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
return {
|
|
731
|
+
run,
|
|
732
|
+
close: () => reset(new Error("local store closed"))
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const dbClient = createDbClient();
|
|
639
736
|
function createSessionPoll(api, refreshPins) {
|
|
640
|
-
// Listener -> whether it reads `snapshot.waiting`. The needs-input query
|
|
641
|
-
//
|
|
737
|
+
// Listener -> whether it reads `snapshot.waiting`. The needs-input query
|
|
738
|
+
// costs worker CPU on every poll, so it only runs while such a listener exists.
|
|
642
739
|
const listeners = new Map();
|
|
643
740
|
let snapshot = {
|
|
644
741
|
entries: [],
|
|
@@ -677,20 +774,18 @@ function createSessionPoll(api, refreshPins) {
|
|
|
677
774
|
};
|
|
678
775
|
}
|
|
679
776
|
if (!active()) return;
|
|
680
|
-
|
|
681
|
-
if (db) {
|
|
777
|
+
if (wantsWaiting()) {
|
|
682
778
|
try {
|
|
779
|
+
const waiting = await queryWaitingIds(dbClient.run);
|
|
683
780
|
if (wantsWaiting()) {
|
|
684
781
|
snapshot = {
|
|
685
782
|
...snapshot,
|
|
686
|
-
waiting
|
|
783
|
+
waiting
|
|
687
784
|
};
|
|
688
785
|
waitingLoaded = true;
|
|
689
786
|
}
|
|
690
787
|
} catch {
|
|
691
788
|
// Keep the previous waiting set when the local store is unavailable.
|
|
692
|
-
} finally {
|
|
693
|
-
db.close();
|
|
694
789
|
}
|
|
695
790
|
} else if (!wantsWaiting()) {
|
|
696
791
|
snapshot = {
|
|
@@ -788,8 +883,8 @@ WHERE COALESCE(s.time_archived, 0) = 0
|
|
|
788
883
|
WHERE live.session_id = s.id AND live.status = 'running' AND live.created < ${stuckBefore})
|
|
789
884
|
AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
|
|
790
885
|
ORDER BY s.time_updated DESC`;
|
|
791
|
-
async function queryWaitingIds(
|
|
792
|
-
const rows =
|
|
886
|
+
async function queryWaitingIds(run) {
|
|
887
|
+
const rows = await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS));
|
|
793
888
|
const out = new Map();
|
|
794
889
|
for (const row of rows) {
|
|
795
890
|
if (typeof row?.id === "string" && SESSION_ID_PATTERN.test(row.id) && (row.reason === "question" || row.reason === "stuck")) {
|
|
@@ -801,8 +896,8 @@ async function queryWaitingIds(db) {
|
|
|
801
896
|
// The shared needs-input query decides *which* sessions are waiting. Read the
|
|
802
897
|
// oldest still-unanswered question or stuck tool for each result so triage can
|
|
803
898
|
// show how long it has waited, rather than its last (unrelated) update time.
|
|
804
|
-
async function queryWaitingDetails(
|
|
805
|
-
const reasons = await queryWaitingIds(
|
|
899
|
+
async function queryWaitingDetails(run) {
|
|
900
|
+
const reasons = await queryWaitingIds(run);
|
|
806
901
|
const details = new Map([...reasons].map(([id, reason]) => [id, {
|
|
807
902
|
reason: reason
|
|
808
903
|
}]));
|
|
@@ -810,12 +905,12 @@ async function queryWaitingDetails(db) {
|
|
|
810
905
|
const cutoff = Date.now() - STUCK_AFTER_MS;
|
|
811
906
|
for (let i = 0; i < ids.length; i += 200) {
|
|
812
907
|
const batch = ids.slice(i, i + 200);
|
|
813
|
-
const rows =
|
|
908
|
+
const rows = await run(`${LIVE_TOOLS_CTE(` AND m.session_id IN (${batch.map(() => "?").join(",")})`)}
|
|
814
909
|
SELECT live.session_id AS id,
|
|
815
910
|
MIN(CASE WHEN ${QUESTION_OPEN} THEN live.created END) AS question_since,
|
|
816
911
|
MIN(CASE WHEN live.status = 'running' AND live.created < ${cutoff} THEN live.created END) AS stuck_since
|
|
817
912
|
FROM live
|
|
818
|
-
GROUP BY live.session_id
|
|
913
|
+
GROUP BY live.session_id`, batch);
|
|
819
914
|
for (const row of rows) {
|
|
820
915
|
const detail = details.get(row.id);
|
|
821
916
|
if (!detail) continue;
|
|
@@ -2749,21 +2844,14 @@ const tui = async api => {
|
|
|
2749
2844
|
const [waitingCoverage, setWaitingCoverage] = createSignal("loading");
|
|
2750
2845
|
let alive = true;
|
|
2751
2846
|
void (async () => {
|
|
2752
|
-
const db = await openTranscriptDb();
|
|
2753
|
-
if (!db) {
|
|
2754
|
-
if (alive) setWaitingCoverage("unavailable");
|
|
2755
|
-
return;
|
|
2756
|
-
}
|
|
2757
2847
|
try {
|
|
2758
|
-
const ids = await queryWaitingIds(
|
|
2848
|
+
const ids = await queryWaitingIds(dbClient.run);
|
|
2759
2849
|
if (alive) {
|
|
2760
2850
|
setWaitingIds(new Set(ids.keys()));
|
|
2761
2851
|
setWaitingCoverage("ready");
|
|
2762
2852
|
}
|
|
2763
2853
|
} catch {
|
|
2764
2854
|
if (alive) setWaitingCoverage("unavailable");
|
|
2765
|
-
} finally {
|
|
2766
|
-
db.close();
|
|
2767
2855
|
}
|
|
2768
2856
|
})();
|
|
2769
2857
|
const [pendingDelete, setPendingDelete] = createSignal();
|
|
@@ -3924,20 +4012,16 @@ const tui = async api => {
|
|
|
3924
4012
|
const {
|
|
3925
4013
|
entries
|
|
3926
4014
|
} = await fetchEntries(api);
|
|
3927
|
-
|
|
3928
|
-
|
|
4015
|
+
let waiting;
|
|
4016
|
+
try {
|
|
4017
|
+
waiting = await queryWaitingDetails(dbClient.run);
|
|
4018
|
+
} catch {
|
|
3929
4019
|
api.ui.toast({
|
|
3930
4020
|
message: "Needs-input data unavailable",
|
|
3931
4021
|
variant: "error"
|
|
3932
4022
|
});
|
|
3933
4023
|
return;
|
|
3934
4024
|
}
|
|
3935
|
-
let waiting;
|
|
3936
|
-
try {
|
|
3937
|
-
waiting = await queryWaitingDetails(db);
|
|
3938
|
-
} finally {
|
|
3939
|
-
db.close();
|
|
3940
|
-
}
|
|
3941
4025
|
items = entries.filter(entry => waiting.has(entry.id)).map(entry => ({
|
|
3942
4026
|
entry,
|
|
3943
4027
|
reason: waiting.get(entry.id)?.reason === "question" ? "awaiting answer" : "run stuck",
|
|
@@ -4491,6 +4575,7 @@ export default Plugin.define({
|
|
|
4491
4575
|
if (api.ui.dialog.open) api.ui.dialog.clear();
|
|
4492
4576
|
disposeSlot();
|
|
4493
4577
|
removeRootMouse?.();
|
|
4578
|
+
dbClient.close();
|
|
4494
4579
|
};
|
|
4495
4580
|
}
|
|
4496
4581
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sesh",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "OpenCode session browser: search, preview, and resume every session across every project from the terminal or TUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,8 +57,9 @@
|
|
|
57
57
|
"build": "node scripts/build-tui.mjs",
|
|
58
58
|
"prepack": "node scripts/build-tui.mjs",
|
|
59
59
|
"test": "bun run test:unit && bash tests/changelog.sh",
|
|
60
|
-
"test:unit": "node scripts/build-tui.mjs && bun run test:tui-home && bash tests/sesh.sh && bash tests/install.sh",
|
|
60
|
+
"test:unit": "node scripts/build-tui.mjs && bun run test:tui-home && bun run test:tui-worker && bash tests/sesh.sh && bash tests/install.sh",
|
|
61
61
|
"test:tui-home": "bun --conditions=browser tests/tui-home.mjs",
|
|
62
|
+
"test:tui-worker": "bun tests/tui-worker.mjs",
|
|
62
63
|
"test:changelog": "bash tests/changelog.sh",
|
|
63
64
|
"test:picker": "python3 tests/picker.py",
|
|
64
65
|
"typecheck": "tsc --noEmit",
|
|
@@ -537,11 +537,96 @@ async function openTranscriptDb(): Promise<any | undefined> {
|
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
// Runs in a blob Worker so slow queries never block the TUI thread. The
|
|
541
|
+
// connection opens on the first request and stays open; `loadSqlite` is
|
|
542
|
+
// injected only by tests.
|
|
543
|
+
const DB_WORKER_SOURCE = `
|
|
544
|
+
let opening
|
|
545
|
+
const load = typeof loadSqlite === "function" ? loadSqlite : () => import("bun:sqlite")
|
|
546
|
+
const open = (path) => opening ??= load().then((sqlite) => new sqlite.Database(path, { readonly: true }), (error) => { opening = undefined; throw error })
|
|
547
|
+
self.onmessage = async (event) => {
|
|
548
|
+
const { id, path, sql, params } = event.data
|
|
549
|
+
try {
|
|
550
|
+
const db = await open(path)
|
|
551
|
+
self.postMessage({ id, rows: db.query(sql).all(...(params ?? [])) })
|
|
552
|
+
} catch (error) {
|
|
553
|
+
self.postMessage({ id, error: String(error?.message ?? error) })
|
|
554
|
+
}
|
|
555
|
+
}`
|
|
556
|
+
|
|
557
|
+
const DB_QUERY_TIMEOUT_MS = 30000
|
|
558
|
+
|
|
559
|
+
type DbRun = (sql: string, params?: unknown[]) => Promise<unknown[]>
|
|
560
|
+
|
|
561
|
+
function createDbWorker(): any {
|
|
562
|
+
return new (globalThis as any).Worker(URL.createObjectURL(new Blob([DB_WORKER_SOURCE], { type: "text/javascript" })))
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Read-only queries against the local store, served by one lazily started
|
|
566
|
+
// worker. Any failure (no worker, no bun:sqlite, query error, timeout) rejects;
|
|
567
|
+
// a timeout or worker error discards the worker so the next call starts afresh.
|
|
568
|
+
function createDbClient(createWorker: () => any = createDbWorker, timeoutMs = DB_QUERY_TIMEOUT_MS) {
|
|
569
|
+
type Pending = { resolve: (rows: unknown[]) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }
|
|
570
|
+
let worker: any
|
|
571
|
+
let nextId = 0
|
|
572
|
+
const pending = new Map<number, Pending>()
|
|
573
|
+
const reset = (error: Error) => {
|
|
574
|
+
const dead = worker
|
|
575
|
+
worker = undefined
|
|
576
|
+
try { dead?.terminate() } catch {}
|
|
577
|
+
const requests = [...pending.values()]
|
|
578
|
+
pending.clear()
|
|
579
|
+
for (const request of requests) {
|
|
580
|
+
clearTimeout(request.timer)
|
|
581
|
+
request.reject(error)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const start = () => {
|
|
585
|
+
const created = createWorker()
|
|
586
|
+
created.onmessage = (event: any) => {
|
|
587
|
+
const { id, rows, error } = event?.data ?? {}
|
|
588
|
+
const request = pending.get(id)
|
|
589
|
+
if (!request) return
|
|
590
|
+
pending.delete(id)
|
|
591
|
+
clearTimeout(request.timer)
|
|
592
|
+
if (typeof error === "string") request.reject(new Error(error))
|
|
593
|
+
else request.resolve(Array.isArray(rows) ? rows : [])
|
|
594
|
+
}
|
|
595
|
+
created.onerror = (event: any) => {
|
|
596
|
+
event?.preventDefault?.()
|
|
597
|
+
if (worker === created) reset(new Error(String(event?.message ?? "local store worker failed")))
|
|
598
|
+
}
|
|
599
|
+
created.unref?.()
|
|
600
|
+
return created
|
|
601
|
+
}
|
|
602
|
+
const run: DbRun = (sql, params = []) =>
|
|
603
|
+
new Promise((resolve, reject) => {
|
|
604
|
+
try {
|
|
605
|
+
worker ??= start()
|
|
606
|
+
} catch (error) {
|
|
607
|
+
reject(error)
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
const id = ++nextId
|
|
611
|
+
const timer = setTimeout(() => reset(new Error("local store query timed out")), timeoutMs)
|
|
612
|
+
;(timer as any).unref?.()
|
|
613
|
+
pending.set(id, { resolve, reject, timer })
|
|
614
|
+
try {
|
|
615
|
+
worker.postMessage({ id, path: resolveDbPath(), sql, params })
|
|
616
|
+
} catch (error) {
|
|
617
|
+
reset(error as Error)
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
return { run, close: () => reset(new Error("local store closed")) }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const dbClient = createDbClient()
|
|
624
|
+
|
|
540
625
|
type SessionSnapshot = { entries: Entry[]; waiting: Map<string, string>; failed: boolean }
|
|
541
626
|
|
|
542
627
|
function createSessionPoll(api: TuiPluginApi, refreshPins: () => void) {
|
|
543
|
-
// Listener -> whether it reads `snapshot.waiting`. The needs-input query
|
|
544
|
-
//
|
|
628
|
+
// Listener -> whether it reads `snapshot.waiting`. The needs-input query
|
|
629
|
+
// costs worker CPU on every poll, so it only runs while such a listener exists.
|
|
545
630
|
const listeners = new Map<(snapshot: SessionSnapshot) => void, boolean>()
|
|
546
631
|
let snapshot: SessionSnapshot = { entries: [], waiting: new Map(), failed: false }
|
|
547
632
|
let clock: ReturnType<typeof setInterval> | undefined
|
|
@@ -569,17 +654,15 @@ function createSessionPoll(api: TuiPluginApi, refreshPins: () => void) {
|
|
|
569
654
|
snapshot = { ...snapshot, failed: true }
|
|
570
655
|
}
|
|
571
656
|
if (!active()) return
|
|
572
|
-
|
|
573
|
-
if (db) {
|
|
657
|
+
if (wantsWaiting()) {
|
|
574
658
|
try {
|
|
659
|
+
const waiting = await queryWaitingIds(dbClient.run)
|
|
575
660
|
if (wantsWaiting()) {
|
|
576
|
-
snapshot = { ...snapshot, waiting
|
|
661
|
+
snapshot = { ...snapshot, waiting }
|
|
577
662
|
waitingLoaded = true
|
|
578
663
|
}
|
|
579
664
|
} catch {
|
|
580
665
|
// Keep the previous waiting set when the local store is unavailable.
|
|
581
|
-
} finally {
|
|
582
|
-
db.close()
|
|
583
666
|
}
|
|
584
667
|
} else if (!wantsWaiting()) {
|
|
585
668
|
snapshot = { ...snapshot, waiting: new Map() }
|
|
@@ -674,8 +757,8 @@ WHERE COALESCE(s.time_archived, 0) = 0
|
|
|
674
757
|
AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
|
|
675
758
|
ORDER BY s.time_updated DESC`
|
|
676
759
|
|
|
677
|
-
async function queryWaitingIds(
|
|
678
|
-
const rows = (
|
|
760
|
+
async function queryWaitingIds(run: DbRun): Promise<Map<string, string>> {
|
|
761
|
+
const rows = (await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS))) as {
|
|
679
762
|
id: unknown
|
|
680
763
|
reason: unknown
|
|
681
764
|
}[]
|
|
@@ -697,8 +780,8 @@ type WaitingDetail = { reason: "question" | "stuck"; since?: number }
|
|
|
697
780
|
// The shared needs-input query decides *which* sessions are waiting. Read the
|
|
698
781
|
// oldest still-unanswered question or stuck tool for each result so triage can
|
|
699
782
|
// show how long it has waited, rather than its last (unrelated) update time.
|
|
700
|
-
async function queryWaitingDetails(
|
|
701
|
-
const reasons = await queryWaitingIds(
|
|
783
|
+
async function queryWaitingDetails(run: DbRun): Promise<Map<string, WaitingDetail>> {
|
|
784
|
+
const reasons = await queryWaitingIds(run)
|
|
702
785
|
const details = new Map<string, WaitingDetail>(
|
|
703
786
|
[...reasons].map(([id, reason]) => [id, { reason: reason as WaitingDetail["reason"] }]),
|
|
704
787
|
)
|
|
@@ -706,12 +789,12 @@ async function queryWaitingDetails(db: any): Promise<Map<string, WaitingDetail>>
|
|
|
706
789
|
const cutoff = Date.now() - STUCK_AFTER_MS
|
|
707
790
|
for (let i = 0; i < ids.length; i += 200) {
|
|
708
791
|
const batch = ids.slice(i, i + 200)
|
|
709
|
-
const rows = (
|
|
792
|
+
const rows = (await run(`${LIVE_TOOLS_CTE(` AND m.session_id IN (${batch.map(() => "?").join(",")})`)}
|
|
710
793
|
SELECT live.session_id AS id,
|
|
711
794
|
MIN(CASE WHEN ${QUESTION_OPEN} THEN live.created END) AS question_since,
|
|
712
795
|
MIN(CASE WHEN live.status = 'running' AND live.created < ${cutoff} THEN live.created END) AS stuck_since
|
|
713
796
|
FROM live
|
|
714
|
-
GROUP BY live.session_id
|
|
797
|
+
GROUP BY live.session_id`, batch)) as { id: string; question_since: number | null; stuck_since: number | null }[]
|
|
715
798
|
for (const row of rows) {
|
|
716
799
|
const detail = details.get(row.id)
|
|
717
800
|
if (!detail) continue
|
|
@@ -2227,21 +2310,14 @@ const tui: TuiPlugin = async (api) => {
|
|
|
2227
2310
|
const [waitingCoverage, setWaitingCoverage] = createSignal<"loading" | "ready" | "unavailable">("loading")
|
|
2228
2311
|
let alive = true
|
|
2229
2312
|
void (async () => {
|
|
2230
|
-
const db = await openTranscriptDb()
|
|
2231
|
-
if (!db) {
|
|
2232
|
-
if (alive) setWaitingCoverage("unavailable")
|
|
2233
|
-
return
|
|
2234
|
-
}
|
|
2235
2313
|
try {
|
|
2236
|
-
const ids = await queryWaitingIds(
|
|
2314
|
+
const ids = await queryWaitingIds(dbClient.run)
|
|
2237
2315
|
if (alive) {
|
|
2238
2316
|
setWaitingIds(new Set(ids.keys()))
|
|
2239
2317
|
setWaitingCoverage("ready")
|
|
2240
2318
|
}
|
|
2241
2319
|
} catch {
|
|
2242
2320
|
if (alive) setWaitingCoverage("unavailable")
|
|
2243
|
-
} finally {
|
|
2244
|
-
db.close()
|
|
2245
2321
|
}
|
|
2246
2322
|
})()
|
|
2247
2323
|
const [pendingDelete, setPendingDelete] = createSignal<Entry>()
|
|
@@ -3059,16 +3135,12 @@ const tui: TuiPlugin = async (api) => {
|
|
|
3059
3135
|
let items: { entry: Entry; reason: string; since?: number }[] = []
|
|
3060
3136
|
try {
|
|
3061
3137
|
const { entries } = await fetchEntries(api)
|
|
3062
|
-
const db = await openTranscriptDb()
|
|
3063
|
-
if (!db) {
|
|
3064
|
-
api.ui.toast({ message: "Needs-input data unavailable", variant: "error" })
|
|
3065
|
-
return
|
|
3066
|
-
}
|
|
3067
3138
|
let waiting: Map<string, WaitingDetail>
|
|
3068
3139
|
try {
|
|
3069
|
-
waiting = await queryWaitingDetails(
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3140
|
+
waiting = await queryWaitingDetails(dbClient.run)
|
|
3141
|
+
} catch {
|
|
3142
|
+
api.ui.toast({ message: "Needs-input data unavailable", variant: "error" })
|
|
3143
|
+
return
|
|
3072
3144
|
}
|
|
3073
3145
|
items = entries
|
|
3074
3146
|
.filter((entry) => waiting.has(entry.id))
|
|
@@ -3521,6 +3593,7 @@ export default Plugin.define({
|
|
|
3521
3593
|
if (api.ui.dialog.open) api.ui.dialog.clear()
|
|
3522
3594
|
disposeSlot()
|
|
3523
3595
|
removeRootMouse?.()
|
|
3596
|
+
dbClient.close()
|
|
3524
3597
|
}
|
|
3525
3598
|
},
|
|
3526
3599
|
})
|