opencode-sesh 2.1.0 → 2.1.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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.2] - 2026-10-27
9
+
10
+ ### Fixed
11
+
12
+ - **engines:** Require OpenCode >=2.2.0
13
+
14
+ ## [2.1.1] - 2026-10-22
15
+
16
+ ### Fixed
17
+
18
+ - Harden pruning, picker concurrency, and TUI lifecycle
19
+
8
20
  ## [2.1.0] - 2026-10-20
9
21
 
10
22
  ### Added
@@ -512,6 +524,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
512
524
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
513
525
  CI.
514
526
 
527
+ [2.1.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.1...v2.1.2
528
+ [2.1.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.0...v2.1.1
515
529
  [2.1.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.0.4...v2.1.0
516
530
  [2.0.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.0.3...v2.0.4
517
531
  [2.0.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.0.2...v2.0.3
package/README.md CHANGED
@@ -18,20 +18,13 @@ terminal or from inside the TUI.
18
18
  `/sessions`) are scoped to the current project. `sesh` shows all of them —
19
19
  grouped by directory, full-text searchable, and previewable from anywhere.
20
20
 
21
- ```text
22
- Sessions (8) ▾ hide
23
- search…
24
- ▾ …/Projects/portal (5)
25
- ├○ Retry the OAuth token refr… 2h
26
- ├○ Login rate limiting notes 3d
27
- ├○ Draft the release notes 7d
28
- ├○ Auth middleware cleanup 11d
29
- └○ Rotate the signing keys 12d
30
- ▾ ~/Projects/mobile (3)
31
- ├○ Author avatar upload retry 1d
32
- ├○ Fix session cookie on Safari 2d
33
- └○ Auth redirect loop after log… 6d
34
- ```
21
+ <p align="center">
22
+ <img src="assets/demo-tui.gif" alt="sesh: session sidebar, cross-project picker search and transcript preview inside OpenCode" width="800">
23
+ </p>
24
+
25
+ **Install:** `npm install -g opencode-sesh && sesh install` for the terminal
26
+ picker, or add `"opencode-sesh"` to `plugins` for the TUI panel
27
+ ([details](#install)).
35
28
 
36
29
  ## Why sesh
37
30
 
@@ -80,6 +73,10 @@ switching to avoid loading them twice.
80
73
 
81
74
  ### Terminal picker
82
75
 
76
+ <p align="center">
77
+ <img src="assets/demo.gif" alt="sesh: searching sessions across projects and previewing a transcript in the terminal picker" width="800">
78
+ </p>
79
+
83
80
  Install the CLI with npm:
84
81
 
85
82
  ```bash
@@ -337,6 +334,13 @@ covers the current project) and offer to resume one.
337
334
  extracted text under `${XDG_CACHE_HOME:-~/.cache}/sesh` (or `SESH_CACHE_DIR`).
338
335
  Nothing is uploaded anywhere.
339
336
 
337
+ **What if a lock stays busy after a crash?** Stop all sesh pickers before
338
+ manually removing the lock directory named in the error; for pin locks, also
339
+ fully quit OpenCode. Locks are never automatically reclaimed. Restart OpenCode
340
+ after upgrading so older plugin instances cannot reclaim locks. Interrupted
341
+ extraction temporary files may remain in the cache; remove them only after all
342
+ pickers have stopped.
343
+
340
344
  **Does it work on Windows?** It targets macOS and Linux. WSL should work; native
341
345
  Windows is untested.
342
346
 
@@ -344,6 +348,12 @@ Windows is untested.
344
348
  keystroke away while you work. The terminal picker is fullscreen and works from
345
349
  any shell, including outside opencode.
346
350
 
351
+ ## More OpenCode plugins
352
+
353
+ - [ghost](https://github.com/ozandogrultan/opencode-ghost) — suggests your next prompt as ghost text in the empty composer.
354
+ - [quill](https://github.com/ozandogrultan/opencode-quill) — local word completion and next-word prediction as you type.
355
+ - [ember](https://github.com/ozandogrultan/opencode-ember) — prices a cold prompt-cache rewrite before you send.
356
+
347
357
  ## Contributing
348
358
 
349
359
  Issues and PRs are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full
package/bin/sesh-costs.sh CHANGED
@@ -11,7 +11,6 @@
11
11
  # --json machine-readable rows
12
12
  set -euo pipefail
13
13
 
14
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
14
  OPENCODE_BIN=${SESH_OPENCODE:-opencode}
16
15
  days=1
17
16
  emit_json=0
@@ -33,18 +33,10 @@ release_lock() {
33
33
  rmdir "$lock" 2>/dev/null || true
34
34
  lock_owned=0
35
35
  }
36
- # Mirrors the acquirer in sesh-list.sh, including its stale-owner
37
- # recovery. Recovery is conservative -- an unreadable or non-numeric owner,
38
- # or one still alive, always loses the race.
36
+ # Only the owner removes a lock; stale reclamation can race a new owner.
39
37
  acquire_lock() {
40
38
  if mkdir "$lock" 2>/dev/null; then printf '%s\n' "$$" > "$lock/pid"; return 0; fi
41
- local owner=''; [ -f "$lock/pid" ] && owner=$(cat "$lock/pid" 2>/dev/null || true)
42
- case "$owner" in ''|*[!0-9]*) return 1;; esac
43
- kill -0 "$owner" 2>/dev/null && return 1
44
- rm -f "$lock/pid" 2>/dev/null || return 1
45
- rmdir "$lock" 2>/dev/null || return 1
46
- mkdir "$lock" 2>/dev/null || return 1
47
- printf '%s\n' "$$" > "$lock/pid"
39
+ return 1
48
40
  }
49
41
 
50
42
  # fzf passes an empty second field for headers and notices. They are never
@@ -77,9 +69,9 @@ fi
77
69
  # Serialize this action with the sole snapshot publisher. A short collision
78
70
  # is safer than deleting against a moving snapshot.
79
71
  if ! acquire_lock; then
80
- echo "Session list is refreshing; try deletion again in a moment."
72
+ printf 'sesh: refresh lock is busy: %s; if it persists, stop all sesh pickers before removing this lock directory\n' "$lock" >&2
81
73
  pause
82
- exit 0
74
+ exit 75
83
75
  fi
84
76
  lock_owned=1
85
77
  trap 'release_lock' EXIT
package/bin/sesh-list.sh CHANGED
@@ -23,6 +23,10 @@ SELECTED_ID=''
23
23
  SCOPE_CWD=''
24
24
  usage() { echo "usage: ${0##*/} [--refresh] [--header] [--wait-lock SECONDS] --state-dir DIR [--cwd] [--limit N] [--archived] [--toggle-scope] [--query TEXT]" >&2; }
25
25
  while [ "$#" -gt 0 ]; do
26
+ case "$1" in
27
+ --wait-lock|--state-dir|--limit|--query|--selected-id)
28
+ [ "$#" -ge 2 ] || { echo "$1 requires an argument" >&2; exit 2; } ;;
29
+ esac
26
30
  case "$1" in
27
31
  --refresh) REFRESH=1 ;;
28
32
  --header) HEADER=1 ;;
@@ -57,6 +61,12 @@ EXTRACTIONS="$CACHE_DIR/extractions"
57
61
  # so metadata always covers the whole database and cache pruning is safe.
58
62
  SCOPE_CWD_SETTING=''
59
63
  if [ -f "$SETTINGS" ]; then
64
+ "$JQ_BIN" -e '
65
+ type == "object" and (.scope | type) == "string"
66
+ and (.limit | type) == "number" and .limit >= 0 and .limit == (.limit | floor)
67
+ and ((.archived // 0) == 0 or .archived == 1)
68
+ and ((.cwd // "") | type) == "string"
69
+ ' "$SETTINGS" >/dev/null 2>&1 || { echo 'sesh: invalid settings' >&2; exit 2; }
60
70
  [ "$LIMIT_SET" = 1 ] || LIMIT=$("$JQ_BIN" -r '.limit' "$SETTINGS")
61
71
  [ "$SCOPE_SET" = 1 ] || SCOPE_CWD=$("$JQ_BIN" -r '.scope' "$SETTINGS")
62
72
  [ "$ARCHIVED_SET" = 1 ] || INCLUDE_ARCHIVED=$("$JQ_BIN" -r '.archived // 0' "$SETTINGS" 2>/dev/null || printf '0')
@@ -198,19 +208,16 @@ render() {
198
208
  release_lock() { rm -f "$LOCK/pid" 2>/dev/null || true; rmdir "$LOCK" 2>/dev/null || true; }
199
209
  acquire_lock() {
200
210
  if mkdir "$LOCK" 2>/dev/null; then printf '%s\n' "$$" > "$LOCK/pid"; return 0; fi
201
- local owner=''; [ -f "$LOCK/pid" ] && owner=$(cat "$LOCK/pid" 2>/dev/null || true)
202
- case "$owner" in ''|*[!0-9]*) return 1;; esac
203
- kill -0 "$owner" 2>/dev/null && return 1
204
- rm -f "$LOCK/pid" 2>/dev/null || return 1
205
- rmdir "$LOCK" 2>/dev/null || return 1
206
- mkdir "$LOCK" 2>/dev/null || return 1
207
- printf '%s\n' "$$" > "$LOCK/pid"
211
+ # Only the owner removes a lock; stale reclamation can race a new owner.
212
+ return 1
208
213
  }
209
214
 
215
+ lock_error() { printf 'sesh: refresh lock is busy: %s; if it persists, stop all sesh pickers before removing this lock directory\n' "$LOCK" >&2; }
216
+
210
217
  acquire_lock_wait() {
211
218
  local remaining=$WAIT_LOCK
212
219
  while ! acquire_lock; do
213
- [ "$remaining" -gt 0 ] || return 75
220
+ [ "$remaining" -gt 0 ] || { lock_error; return 75; }
214
221
  sleep 1
215
222
  remaining=$((remaining - 1))
216
223
  done
@@ -231,35 +238,39 @@ refresh() {
231
238
  private_dir "$EXTRACTIONS"
232
239
  # Migrate warm caches too, including temporary files left by older versions.
233
240
  # Do not follow symlinks or chmod unrelated files outside our extraction store.
234
- find "$EXTRACTIONS" -maxdepth 1 -type f -user "$(id -u)" \( -name 'ses_*.json' -o -name 'ses_*.json.tmp' \) -exec chmod 600 {} +
241
+ # SIGKILL orphans stay: the cache is shared, but refresh locks are picker-local,
242
+ # so removing unowned temp files could interrupt another picker's writer.
243
+ find "$EXTRACTIONS" -maxdepth 1 -type f -user "$(id -u)" \( -name 'ses_*.json' -o -name 'ses_*.json.tmp' -o -name 'ses_*.json.??????' \) -exec chmod 600 {} +
235
244
  if [ "${SESH_LOCK_HELD:-}" != "$STATE_DIR" ]; then
236
- if [ "$WAIT_LOCK" -gt 0 ]; then acquire_lock_wait || return $?; else acquire_lock || return 0; fi
245
+ if [ "$WAIT_LOCK" -gt 0 ]; then acquire_lock_wait || return $?; else acquire_lock || { lock_error; return 75; }; fi
237
246
  fi
247
+ refresh_scratch=''
248
+ refresh_cache_tmp=''
249
+ if [ "${SESH_LOCK_HELD:-}" = "$STATE_DIR" ]; then
250
+ trap '[ -z "$refresh_cache_tmp" ] || rm -f "$refresh_cache_tmp"; [ -z "$refresh_scratch" ] || rm -rf "$refresh_scratch"' EXIT
251
+ else
252
+ trap '[ -z "$refresh_cache_tmp" ] || rm -f "$refresh_cache_tmp"; [ -z "$refresh_scratch" ] || rm -rf "$refresh_scratch"; release_lock' EXIT
253
+ fi
254
+ trap 'exit 130' HUP INT TERM
238
255
  # Persist the effective scope (after any toggle) so renders and workers agree.
239
256
  if [ "$SCOPE_SET" = 1 ] || [ ! -f "$SETTINGS" ]; then
240
257
  atomic_text "$SETTINGS" "$("$JQ_BIN" -cn --arg scope "$SCOPE_CWD" --argjson limit "$LIMIT" --arg cwd "$SCOPE_CWD_SETTING" --argjson archived "$INCLUDE_ARCHIVED" '{scope:$scope,limit:$limit,cwd:$cwd,archived:$archived}')"
241
258
  fi
242
259
  DB_PATH=$(resolve_db) || {
243
260
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database is unavailable; showing last good snapshot'; else set_status 'unavailable: opencode database is unavailable'; fi
244
- return 0
261
+ return 1
245
262
  }
246
263
  [ -f "$DB_PATH" ] || {
247
264
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database is unavailable; showing last good snapshot'; else set_status 'unavailable: opencode database is unavailable'; fi
248
- return 0
265
+ return 1
249
266
  }
250
267
  if [ -z "$SQLITE_BIN" ]; then
251
268
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: sqlite3 is not available; showing last good snapshot'; else set_status 'unavailable: sqlite3 is not available'; fi
252
- return 0
269
+ return 1
253
270
  fi
254
271
  local scratch sessions parts cache_records cache_state historical needs_extract out
255
272
  scratch=$(mktemp -d "$STATE_DIR/.refresh.XXXXXX")
256
273
  refresh_scratch=$scratch
257
- if [ "${SESH_LOCK_HELD:-}" = "$STATE_DIR" ]; then
258
- trap 'rm -rf "$refresh_scratch"' EXIT
259
- else
260
- trap 'rm -rf "$refresh_scratch"; release_lock' EXIT
261
- fi
262
- trap 'exit 130' HUP INT TERM
263
274
  sessions="$scratch/sessions.json"; parts="$scratch/parts.json"
264
275
  cache_records="$scratch/caches.json"; cache_state="$scratch/cache-state.json"
265
276
  historical="$scratch/historical.jsonl"; needs_extract="$scratch/needs-extract.json"
@@ -267,14 +278,14 @@ refresh() {
267
278
 
268
279
  db_json "SELECT id, directory, title, time_created, time_updated, COALESCE(time_archived, 0) AS archived, COALESCE(parent_id, '') AS parent FROM session_v2 ORDER BY time_updated DESC;" > "$sessions" 2>/dev/null || {
269
280
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
270
- return 0
281
+ return 1
271
282
  }
272
283
  # sqlite3 emits no bytes for zero rows. Publish an empty snapshot so removal
273
284
  # of the final session also produces the non-actionable selection notice.
274
285
  [ -s "$sessions" ] || printf '[]\n' > "$sessions"
275
286
  "$JQ_BIN" -e 'type == "array"' "$sessions" >/dev/null 2>&1 || {
276
287
  if [ -f "$SNAPSHOT" ]; then set_status 'stale: opencode database query failed; showing last good snapshot'; else set_status 'unavailable: opencode database query failed'; fi
277
- return 0
288
+ return 1
278
289
  }
279
290
  db_json "SELECT session_id, COUNT(*) AS n, MAX(time_updated) AS m FROM session_message GROUP BY session_id;" > "$parts" 2>/dev/null || printf '[]\n' > "$parts"
280
291
  "$JQ_BIN" -e 'type == "array"' "$parts" >/dev/null 2>&1 || printf '[]\n' > "$parts"
@@ -296,7 +307,7 @@ refresh() {
296
307
  | ($cached | map({key: .sessionId, value:true}) | from_entries) as $cachedIds
297
308
  | {cached: $cached,
298
309
  stale: [$meta[] | select($cachedIds[.id] != true) | {id, directory, title, time_created, time_updated, archived, partCount, partMax}],
299
- prune: [$caches[0][]? | select(.sessionId? as $s | $byId[$s] == null) | .cachePath],
310
+ prune: [$caches[0][]? | select(if (.sessionId | type) != "string" then true else $byId[.sessionId] == null end) | .cachePath],
300
311
  meta: $meta}
301
312
  ' "$cache_records" > "$cache_state" || {
302
313
  set_status 'stale: snapshot construction failed; showing last good snapshot'
@@ -366,9 +377,10 @@ refresh() {
366
377
  if [ "$extract_failed" = 0 ]; then
367
378
  while IFS= read -r sid && IFS= read -r cache_path && IFS= read -r record; do
368
379
  [ -n "$sid" ] || continue
369
- cache_tmp=$(mktemp "$EXTRACTIONS/$sid.json.XXXXXX")
370
- printf '%s\n' "$record" > "$cache_tmp"
371
- mv "$cache_tmp" "$cache_path"
380
+ refresh_cache_tmp=$(mktemp "$EXTRACTIONS/$sid.json.XXXXXX")
381
+ printf '%s\n' "$record" > "$refresh_cache_tmp"
382
+ mv "$refresh_cache_tmp" "$cache_path"
383
+ refresh_cache_tmp=''
372
384
  printf '%s\n' "$record" >> "$historical"
373
385
  done < "$records"
374
386
  fi
package/bin/sesh-pins.sh CHANGED
@@ -12,8 +12,16 @@ value=${2:-}
12
12
  read_pins() {
13
13
  if [ -f "$PINS_FILE" ]; then
14
14
  local pins=''
15
- pins=$("$JQ_BIN" -c 'select(type == "object") | {sessions: ((.sessions // []) | map(select(type == "string"))), directories: ((.directories // []) | map(select(type == "string")))}' "$PINS_FILE" 2>/dev/null) || pins=''
16
- printf '%s\n' "${pins:-$empty}"
15
+ pins=$("$JQ_BIN" -ces '
16
+ if length != 1 then error("invalid pins") else .[0] end
17
+ | select(type == "object")
18
+ | {sessions: .sessions, directories: .directories}
19
+ | select(all(.[]; type == "array" and all(.[]; type == "string")))
20
+ ' "$PINS_FILE" 2>/dev/null) || { echo 'sesh: cannot read pins; leaving them unchanged' >&2; return 1; }
21
+ printf '%s\n' "$pins"
22
+ elif [ -e "$PINS_FILE" ] || [ -L "$PINS_FILE" ]; then
23
+ echo 'sesh: cannot read pins; leaving them unchanged' >&2
24
+ return 1
17
25
  else
18
26
  printf '%s\n' "$empty"
19
27
  fi
@@ -34,13 +42,8 @@ mkdir -p "$(dirname "$PINS_FILE")"
34
42
  lock="$PINS_FILE.lock"
35
43
  for ((attempt = 0; attempt < 50; attempt++)); do
36
44
  if mkdir "$lock" 2>/dev/null; then printf '%s\n' "$$" > "$lock/pid"; break; fi
37
- owner=''
38
- [ ! -f "$lock/pid" ] || owner=$(cat "$lock/pid" 2>/dev/null || true)
39
- if [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; then
40
- rm -f "$lock/pid" 2>/dev/null || true
41
- rmdir "$lock" 2>/dev/null || true
42
- fi
43
- [ "$attempt" -lt 49 ] || { echo 'sesh: pins are busy; try again' >&2; exit 1; }
45
+ # Only the owner removes a lock; stale reclamation can race a new owner.
46
+ [ "$attempt" -lt 49 ] || { printf 'sesh: pins lock is busy: %s; if it persists, stop all sesh pickers and OpenCode instances before removing this lock directory\n' "$lock" >&2; exit 1; }
44
47
  sleep 0.1
45
48
  done
46
49
  tmp=''
package/bin/sesh-prune.sh CHANGED
@@ -70,11 +70,14 @@ query() { "$SQLITE_BIN" -json -readonly "$DB_PATH" "$1"; }
70
70
  JQ_BIN=${SESH_JQ:-$(command -v jq || true)}
71
71
  [ -n "$JQ_BIN" ] || { echo 'sesh: jq is required' >&2; exit 1; }
72
72
 
73
- CANDIDATES=$(query "SELECT id, directory, title, time_updated AS updated FROM session_v2 WHERE time_updated < $CUTOFF AND COALESCE(time_archived, 0) = 0 AND parent_id IS NULL ORDER BY time_updated ASC;")
73
+ CANDIDATES=$(query "SELECT id, directory, title, time_updated AS updated FROM session_v2 WHERE time_updated < $CUTOFF AND COALESCE(time_archived, 0) = 0 AND COALESCE(parent_id, '') = '' ORDER BY time_updated ASC;")
74
74
  # sqlite3 -json prints nothing (not []) when no rows match.
75
75
  [ -n "$CANDIDATES" ] || CANDIDATES='[]'
76
- PINS=$("$SCRIPT_DIR/sesh-pins.sh" read 2>/dev/null || printf '{"sessions":[],"directories":[]}')
77
- WAITING_IDS=$(bash "$SCRIPT_DIR/sesh-waiting.sh" --json 2>/dev/null | "$JQ_BIN" -r '.[].id' || true)
76
+ PINS=$("$SCRIPT_DIR/sesh-pins.sh" read) || { echo 'sesh: refusing prune: pins protection unavailable' >&2; exit 1; }
77
+ WAITING_IDS=$(bash "$SCRIPT_DIR/sesh-waiting.sh" --json | "$JQ_BIN" -er '
78
+ if type == "array" and all(.[]; (.id | type) == "string")
79
+ then map(.id) | join("\n") else error("invalid waiting sessions") end
80
+ ') || { echo 'sesh: refusing prune: waiting-session protection unavailable' >&2; exit 1; }
78
81
  [ -n "$WAITING_IDS" ] || WAITING_IDS='__none__'
79
82
 
80
83
  ELIGIBLE=$(printf '%s' "$CANDIDATES" | "$JQ_BIN" -c --slurpfile _pins <(printf '%s' "$PINS") --arg waiting "$WAITING_IDS" '
@@ -130,7 +133,7 @@ fi
130
133
  command -v "$OPENCODE_BIN" >/dev/null 2>&1 || { echo 'sesh: opencode executable is unavailable for deletes.' >&2; exit 1; }
131
134
  failed=0
132
135
  done_count=0
133
- for id in $(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r '.[].id'); do
136
+ while IFS= read -r id; do
134
137
  [[ "$id" =~ ^ses_[A-Za-z0-9]+$ ]] || { echo "Refusing: unexpected session id shape." >&2; exit 1; }
135
138
  if "$OPENCODE_BIN" session delete "$id" >/dev/null 2>&1; then
136
139
  done_count=$((done_count + 1))
@@ -138,6 +141,6 @@ for id in $(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r '.[].id'); do
138
141
  echo "Could not delete $id." >&2
139
142
  failed=$((failed + 1))
140
143
  fi
141
- done
144
+ done < <(printf '%s' "$ELIGIBLE" | "$JQ_BIN" -r '.[].id')
142
145
  echo "Deleted $done_count session(s) older than $older_than."
143
146
  [ "$failed" = 0 ] || exit 1
@@ -73,7 +73,10 @@ case "$MODE" in
73
73
  complete=$(cat "$STATE_DIR/worker-complete" 2>/dev/null || printf 0)
74
74
  case "$complete" in ''|*[!0-9]*) complete=0;; esac
75
75
  if [ "$complete" -ge "$TARGET" ]; then
76
- [ "$(cat "$STATE_DIR/status" 2>/dev/null || true)" = fresh ] || exit 1
76
+ case "$(cat "$STATE_DIR/status" 2>/dev/null || true)" in
77
+ fresh|'stale: transcript extraction incomplete; refresh to retry') ;;
78
+ *) exit 1 ;;
79
+ esac
77
80
  exit 0
78
81
  fi
79
82
  worker=$(cat "$STATE_DIR/worker.pid" 2>/dev/null || true)
@@ -21,7 +21,6 @@
21
21
  # Keep NEEDS_INPUT_SQL in sync with the TUI sidebar copy in tui-plugins/sesh-panel/tui.tsx.
22
22
  set -euo pipefail
23
23
 
24
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
24
  OPENCODE_BIN=${SESH_OPENCODE:-opencode}
26
25
  JSON=0
27
26
  while [ "$#" -gt 0 ]; do
@@ -80,7 +79,7 @@ SELECT s.id AS id, s.directory AS directory, s.title AS title, s.time_updated AS
80
79
  ) THEN 'question' ELSE 'stuck' END AS reason
81
80
  FROM session_v2 s
82
81
  WHERE COALESCE(s.time_archived, 0) = 0
83
- AND s.parent_id IS NULL
82
+ AND COALESCE(s.parent_id, '') = ''
84
83
  AND (EXISTS (
85
84
  SELECT 1 FROM live
86
85
  WHERE live.session_id = s.id AND live.name = 'question'
package/bin/sesh.sh CHANGED
@@ -75,12 +75,12 @@ picker_query=''
75
75
  while [ "$#" -gt 0 ]; do
76
76
  case "$1" in
77
77
  --cwd) scope_value=$PWD ;;
78
- --limit) picker_limit=${2:-}; shift ;;
78
+ --limit) [ "$#" -ge 2 ] || { echo '--limit requires an argument' >&2; exit 2; }; picker_limit=$2; shift ;;
79
79
  --archived) picker_archived=1 ;;
80
80
  --print) picker_print=1 ;;
81
81
  --fork) picker_fork=1 ;;
82
82
  --json) picker_json=1 ;;
83
- --query) picker_query=${2:-}; shift ;;
83
+ --query) [ "$#" -ge 2 ] || { echo '--query requires an argument' >&2; exit 2; }; picker_query=$2; shift ;;
84
84
  *) echo "unknown option: $1" >&2; exit 2 ;;
85
85
  esac
86
86
  shift
@@ -129,7 +129,7 @@ cache_cmd="$q_worker render --state-dir $q_state --query {q} --selected-id {6}"
129
129
  header_cmd="$q_list --header --state-dir $q_state"
130
130
  preview_cmd="if [ -f $q_state/help ]; then $q_shortcuts; else $q_preview $q_state {2}; fi"
131
131
  help_bind="?:transform:[ -z {q} ] && ( [ -f $q_state/help ] && rm -f $q_state/help && echo hide-preview || ( : > $q_state/help && echo show-preview+refresh-preview ) ) || echo 'put(?)'"
132
- toggle_cmd="$q_list --toggle-scope --state-dir $q_state >/dev/null"
132
+ toggle_cmd="$q_list --toggle-scope --wait-lock 12 --state-dir $q_state >/dev/null"
133
133
  delete_cmd="$q_delete $q_state {2}"
134
134
  pin_session_cmd="$q_pins toggle-session {2}"
135
135
  pin_directory_cmd="$q_pins toggle-directory {5}"