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 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`. The query runs in
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
@@ -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 LIVE_TOOLS_CTE = sessionFilter => `
869
- WITH live AS (
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sesh",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
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",
@@ -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 LIVE_TOOLS_CTE = (sessionFilter: string) => `
738
- WITH live AS (
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
  }[]