opencode-sesh 2.1.7 → 2.1.8
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 +4 -1
- package/CHANGELOG.md +7 -0
- package/bin/sesh-waiting.sh +20 -1
- package/dist/tui.js +30 -5
- package/package.json +1 -1
- package/tui-plugins/sesh-panel/tui.tsx +36 -5
package/AGENTS.md
CHANGED
|
@@ -182,7 +182,10 @@ when opencode exits, the picker reopens with the query preserved.
|
|
|
182
182
|
scrolling inside; the picker remains the place for transcript search.
|
|
183
183
|
Sessions waiting on the user (unanswered questions, stuck runs) pin a
|
|
184
184
|
**Needs input** group above the directories, with the count in the heading —
|
|
185
|
-
same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`.
|
|
185
|
+
same `NEEDS_INPUT_SQL` heuristic as `bin/sesh-waiting.sh`. Message payloads are
|
|
186
|
+
scanned only for in-flight root sessions (`time_suspended` set, `time_busy` newer
|
|
187
|
+
than `time_idle`); databases lacking those columns (checked via
|
|
188
|
+
`pragma_table_info`) fall back to the full scan. The query runs in
|
|
186
189
|
a blob Worker (`createDbClient`; failure means "local store unavailable"),
|
|
187
190
|
off the TUI thread but still costing worker CPU every poll, so the shared
|
|
188
191
|
poll runs it only while the sidebar is subscribed
|
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.8] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **tui:** Prefilter needs-input candidates by in-flight session state (#38)
|
|
13
|
+
|
|
8
14
|
## [2.1.7] - 2026-11-02
|
|
9
15
|
|
|
10
16
|
### Changed
|
|
@@ -558,6 +564,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
558
564
|
- Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
|
|
559
565
|
CI.
|
|
560
566
|
|
|
567
|
+
[2.1.8]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.7...v2.1.8
|
|
561
568
|
[2.1.7]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.6...v2.1.7
|
|
562
569
|
[2.1.6]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.5...v2.1.6
|
|
563
570
|
[2.1.5]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.4...v2.1.5
|
package/bin/sesh-waiting.sh
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
# (a long-running subagent/shell tool with recent activity is not stuck;
|
|
16
16
|
# V2 resumes interrupted runs server-side, so `running` with no activity
|
|
17
17
|
# means orphaned or awaiting approval).
|
|
18
|
+
# Only a turn in flight can be waiting (time_suspended is set while a turn
|
|
19
|
+
# runs), so message payloads are scanned only for those candidate sessions;
|
|
20
|
+
# databases without the in-flight columns scan every assistant message.
|
|
18
21
|
#
|
|
19
22
|
# Usage: sesh-waiting.sh [--json]
|
|
20
23
|
# default prints a human table; --json prints [{id,reason,title,directory,updated}]
|
|
@@ -57,8 +60,23 @@ NOW_MS=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null ||
|
|
|
57
60
|
STUCK_BEFORE=$((NOW_MS - 600000))
|
|
58
61
|
|
|
59
62
|
# NEEDS_INPUT_SQL (shared heuristic — keep in sync with tui-plugins/sesh-panel/tui.tsx).
|
|
63
|
+
CANDIDATES=''
|
|
64
|
+
PREFILTER=''
|
|
65
|
+
HAS_COLUMNS=0
|
|
66
|
+
if [ -n "$SQLITE_BIN" ]; then
|
|
67
|
+
HAS_COLUMNS=$("$SQLITE_BIN" -readonly "$DB_PATH" "SELECT count(*) = 3 FROM pragma_table_info('session_v2') WHERE name IN ('time_busy', 'time_idle', 'time_suspended');" 2>/dev/null || echo 0)
|
|
68
|
+
fi
|
|
69
|
+
if [ "$HAS_COLUMNS" = 1 ]; then
|
|
70
|
+
CANDIDATES="cand AS (
|
|
71
|
+
SELECT id FROM session_v2
|
|
72
|
+
WHERE time_suspended IS NOT NULL AND COALESCE(parent_id, '') = '' AND COALESCE(time_archived, 0) = 0
|
|
73
|
+
AND time_busy > COALESCE(time_idle, 0)
|
|
74
|
+
), "
|
|
75
|
+
PREFILTER="AND m.session_id IN (SELECT id FROM cand)"
|
|
76
|
+
fi
|
|
77
|
+
|
|
60
78
|
read -r -d '' QUERY <<SQL || true
|
|
61
|
-
WITH live AS (
|
|
79
|
+
WITH ${CANDIDATES}live AS (
|
|
62
80
|
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
63
81
|
json_extract(c.value, '\$.name') AS name,
|
|
64
82
|
json_extract(c.value, '\$.state.status') AS status,
|
|
@@ -68,6 +86,7 @@ WITH live AS (
|
|
|
68
86
|
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
69
87
|
AND json_extract(c.value, '\$.type') = 'tool'
|
|
70
88
|
AND json_extract(c.value, '\$.state.status') IN ('pending', 'running')
|
|
89
|
+
$PREFILTER
|
|
71
90
|
)
|
|
72
91
|
SELECT s.id AS id, s.directory AS directory, s.title AS title, s.time_updated AS updated,
|
|
73
92
|
CASE WHEN EXISTS (
|
package/dist/tui.js
CHANGED
|
@@ -864,9 +864,19 @@ ORDER BY lifetime_cost DESC`;
|
|
|
864
864
|
// orphaned or awaiting approval). Server-independent on purpose: the TUI
|
|
865
865
|
// sync layer only sees permission/question state for sessions owned by the
|
|
866
866
|
// current server, which is invisible cross-project.
|
|
867
|
+
// Only a turn in flight can be waiting (time_suspended is set while a turn runs
|
|
868
|
+
// and a question/permission prompt keeps it running), so message payloads are
|
|
869
|
+
// scanned only for those candidate sessions. Databases without the in-flight
|
|
870
|
+
// columns scan every assistant message.
|
|
867
871
|
const STUCK_AFTER_MS = 600000;
|
|
868
|
-
const
|
|
869
|
-
|
|
872
|
+
const IN_FLIGHT_COLUMNS = ["time_busy", "time_idle", "time_suspended"];
|
|
873
|
+
const CANDIDATES_CTE = `cand AS (
|
|
874
|
+
SELECT id FROM session_v2
|
|
875
|
+
WHERE time_suspended IS NOT NULL AND parent_id IS NULL AND COALESCE(time_archived, 0) = 0
|
|
876
|
+
AND time_busy > COALESCE(time_idle, 0)
|
|
877
|
+
), `;
|
|
878
|
+
const LIVE_TOOLS_CTE = (sessionFilter, prefilter = false) => `
|
|
879
|
+
WITH ${prefilter ? CANDIDATES_CTE : ""}live AS (
|
|
870
880
|
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
871
881
|
json_extract(c.value, '$.name') AS name,
|
|
872
882
|
json_extract(c.value, '$.state.status') AS status,
|
|
@@ -875,7 +885,7 @@ WITH live AS (
|
|
|
875
885
|
WHERE m.type = 'assistant'
|
|
876
886
|
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
877
887
|
AND json_extract(c.value, '$.type') = 'tool'
|
|
878
|
-
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}
|
|
888
|
+
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}${prefilter ? "\n AND m.session_id IN (SELECT id FROM cand)" : ""}
|
|
879
889
|
)`;
|
|
880
890
|
const QUESTION_OPEN = `live.name = 'question'
|
|
881
891
|
AND NOT EXISTS (
|
|
@@ -884,7 +894,7 @@ const QUESTION_OPEN = `live.name = 'question'
|
|
|
884
894
|
const NO_RECENT_ACTIVITY = (sessionIdExpr, stuckBefore) => `NOT EXISTS (
|
|
885
895
|
SELECT 1 FROM session_message r JOIN session_v2 f ON f.id = r.session_id
|
|
886
896
|
WHERE (f.id = ${sessionIdExpr} OR f.parent_id = ${sessionIdExpr}) AND r.time_updated >= ${stuckBefore})`;
|
|
887
|
-
const NEEDS_INPUT_SQL = stuckBefore => `${LIVE_TOOLS_CTE("")}
|
|
897
|
+
const NEEDS_INPUT_SQL = (stuckBefore, prefilter = false) => `${LIVE_TOOLS_CTE("", prefilter)}
|
|
888
898
|
SELECT s.id AS id,
|
|
889
899
|
CASE WHEN EXISTS (SELECT 1 FROM live WHERE live.session_id = s.id AND ${QUESTION_OPEN})
|
|
890
900
|
THEN 'question' ELSE 'stuck' END AS reason
|
|
@@ -897,8 +907,23 @@ WHERE COALESCE(s.time_archived, 0) = 0
|
|
|
897
907
|
WHERE live.session_id = s.id AND live.status = 'running' AND live.created < ${stuckBefore})
|
|
898
908
|
AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
|
|
899
909
|
ORDER BY s.time_updated DESC`;
|
|
910
|
+
const inFlightSupport = new WeakMap();
|
|
911
|
+
function hasInFlightColumns(run) {
|
|
912
|
+
let known = inFlightSupport.get(run);
|
|
913
|
+
if (!known) {
|
|
914
|
+
known = run("SELECT name FROM pragma_table_info('session_v2')").then(rows => {
|
|
915
|
+
const names = new Set(rows.map(row => row?.name));
|
|
916
|
+
return IN_FLIGHT_COLUMNS.every(name => names.has(name));
|
|
917
|
+
}, () => {
|
|
918
|
+
inFlightSupport.delete(run);
|
|
919
|
+
return false;
|
|
920
|
+
});
|
|
921
|
+
inFlightSupport.set(run, known);
|
|
922
|
+
}
|
|
923
|
+
return known;
|
|
924
|
+
}
|
|
900
925
|
async function queryWaitingIds(run) {
|
|
901
|
-
const rows = await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS));
|
|
926
|
+
const rows = await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS, await hasInFlightColumns(run)));
|
|
902
927
|
const out = new Map();
|
|
903
928
|
for (const row of rows) {
|
|
904
929
|
if (typeof row?.id === "string" && SESSION_ID_PATTERN.test(row.id) && (row.reason === "question" || row.reason === "stuck")) {
|
package/package.json
CHANGED
|
@@ -733,9 +733,19 @@ ORDER BY lifetime_cost DESC`
|
|
|
733
733
|
// orphaned or awaiting approval). Server-independent on purpose: the TUI
|
|
734
734
|
// sync layer only sees permission/question state for sessions owned by the
|
|
735
735
|
// current server, which is invisible cross-project.
|
|
736
|
+
// Only a turn in flight can be waiting (time_suspended is set while a turn runs
|
|
737
|
+
// and a question/permission prompt keeps it running), so message payloads are
|
|
738
|
+
// scanned only for those candidate sessions. Databases without the in-flight
|
|
739
|
+
// columns scan every assistant message.
|
|
736
740
|
const STUCK_AFTER_MS = 600000
|
|
737
|
-
const
|
|
738
|
-
|
|
741
|
+
const IN_FLIGHT_COLUMNS = ["time_busy", "time_idle", "time_suspended"]
|
|
742
|
+
const CANDIDATES_CTE = `cand AS (
|
|
743
|
+
SELECT id FROM session_v2
|
|
744
|
+
WHERE time_suspended IS NOT NULL AND parent_id IS NULL AND COALESCE(time_archived, 0) = 0
|
|
745
|
+
AND time_busy > COALESCE(time_idle, 0)
|
|
746
|
+
), `
|
|
747
|
+
const LIVE_TOOLS_CTE = (sessionFilter: string, prefilter = false) => `
|
|
748
|
+
WITH ${prefilter ? CANDIDATES_CTE : ""}live AS (
|
|
739
749
|
SELECT m.session_id AS session_id, m.seq AS seq,
|
|
740
750
|
json_extract(c.value, '$.name') AS name,
|
|
741
751
|
json_extract(c.value, '$.state.status') AS status,
|
|
@@ -744,7 +754,9 @@ WITH live AS (
|
|
|
744
754
|
WHERE m.type = 'assistant'
|
|
745
755
|
AND (m.data LIKE '%"status":"running"%' OR m.data LIKE '%"status":"pending"%')
|
|
746
756
|
AND json_extract(c.value, '$.type') = 'tool'
|
|
747
|
-
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}
|
|
757
|
+
AND json_extract(c.value, '$.state.status') IN ('pending', 'running')${sessionFilter}${
|
|
758
|
+
prefilter ? "\n AND m.session_id IN (SELECT id FROM cand)" : ""
|
|
759
|
+
}
|
|
748
760
|
)`
|
|
749
761
|
const QUESTION_OPEN = `live.name = 'question'
|
|
750
762
|
AND NOT EXISTS (
|
|
@@ -753,7 +765,7 @@ const QUESTION_OPEN = `live.name = 'question'
|
|
|
753
765
|
const NO_RECENT_ACTIVITY = (sessionIdExpr: string, stuckBefore: number) => `NOT EXISTS (
|
|
754
766
|
SELECT 1 FROM session_message r JOIN session_v2 f ON f.id = r.session_id
|
|
755
767
|
WHERE (f.id = ${sessionIdExpr} OR f.parent_id = ${sessionIdExpr}) AND r.time_updated >= ${stuckBefore})`
|
|
756
|
-
const NEEDS_INPUT_SQL = (stuckBefore: number) => `${LIVE_TOOLS_CTE("")}
|
|
768
|
+
const NEEDS_INPUT_SQL = (stuckBefore: number, prefilter = false) => `${LIVE_TOOLS_CTE("", prefilter)}
|
|
757
769
|
SELECT s.id AS id,
|
|
758
770
|
CASE WHEN EXISTS (SELECT 1 FROM live WHERE live.session_id = s.id AND ${QUESTION_OPEN})
|
|
759
771
|
THEN 'question' ELSE 'stuck' END AS reason
|
|
@@ -767,8 +779,27 @@ WHERE COALESCE(s.time_archived, 0) = 0
|
|
|
767
779
|
AND ${NO_RECENT_ACTIVITY("s.id", stuckBefore)}))
|
|
768
780
|
ORDER BY s.time_updated DESC`
|
|
769
781
|
|
|
782
|
+
const inFlightSupport = new WeakMap<DbRun, Promise<boolean>>()
|
|
783
|
+
function hasInFlightColumns(run: DbRun): Promise<boolean> {
|
|
784
|
+
let known = inFlightSupport.get(run)
|
|
785
|
+
if (!known) {
|
|
786
|
+
known = run("SELECT name FROM pragma_table_info('session_v2')").then(
|
|
787
|
+
(rows) => {
|
|
788
|
+
const names = new Set((rows as { name?: unknown }[]).map((row) => row?.name))
|
|
789
|
+
return IN_FLIGHT_COLUMNS.every((name) => names.has(name))
|
|
790
|
+
},
|
|
791
|
+
() => {
|
|
792
|
+
inFlightSupport.delete(run)
|
|
793
|
+
return false
|
|
794
|
+
},
|
|
795
|
+
)
|
|
796
|
+
inFlightSupport.set(run, known)
|
|
797
|
+
}
|
|
798
|
+
return known
|
|
799
|
+
}
|
|
800
|
+
|
|
770
801
|
async function queryWaitingIds(run: DbRun): Promise<Map<string, string>> {
|
|
771
|
-
const rows = (await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS))) as {
|
|
802
|
+
const rows = (await run(NEEDS_INPUT_SQL(Date.now() - STUCK_AFTER_MS, await hasInFlightColumns(run)))) as {
|
|
772
803
|
id: unknown
|
|
773
804
|
reason: unknown
|
|
774
805
|
}[]
|