opencode-sesh 2.0.4 → 2.1.1

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
@@ -159,7 +159,8 @@ when opencode exits, the picker reopens with the query preserved.
159
159
  ## TUI panel (`tui-plugins/sesh-panel/tui.tsx`)
160
160
 
161
161
  - **Sidebar & Home list:** `api.slots.register` on `sidebar_content` (`sidebar.content`
162
- prepend mode — native sidebar content stays) and `home_bottom` (`home.footer`
162
+ append mode by default — native sidebar content stays above Sessions;
163
+ `sidebarPlacement: "prepend"` puts Sessions first) and `home_bottom` (`home.footer`
163
164
  prepend mode). In the sidebar, pinned directories sort first, then directory groups
164
165
  by recency; sessions within each group sort by last update regardless of pins.
165
166
  In the home list, pinned sessions sort first.
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.1] - 2026-10-22
9
+
10
+ ### Fixed
11
+
12
+ - Harden pruning, picker concurrency, and TUI lifecycle
13
+
14
+ ## [2.1.0] - 2026-10-20
15
+
16
+ ### Added
17
+
18
+ - **tui:** Configure sessions sidebar placement
19
+
8
20
  ## [2.0.4] - 2026-10-20
9
21
 
10
22
  ### Added
@@ -506,6 +518,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
506
518
  - Fixture-database regression suite, TypeScript typechecking, and ShellCheck in
507
519
  CI.
508
520
 
521
+ [2.1.1]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.1.0...v2.1.1
522
+ [2.1.0]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.0.4...v2.1.0
509
523
  [2.0.4]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.0.3...v2.0.4
510
524
  [2.0.3]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.0.2...v2.0.3
511
525
  [2.0.2]: https://github.com/ozandogrultan/opencode-sesh/compare/v2.0.1...v2.0.2
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
+ # sesh
2
+
3
+ [![CI](https://github.com/ozandogrultan/opencode-sesh/actions/workflows/ci.yml/badge.svg)](https://github.com/ozandogrultan/opencode-sesh/actions/workflows/ci.yml)
4
+
1
5
  <h1 align="center">
2
6
  <img src="assets/logo.svg" alt="sesh" height="96">
3
7
  </h1>
4
8
 
5
9
  <p align="center">
6
- <a href="https://github.com/ozandogrultan/opencode-sesh/actions/workflows/ci.yml"><img src="https://github.com/ozandogrultan/opencode-sesh/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
7
10
  <a href="https://www.npmjs.com/package/opencode-sesh"><img src="https://img.shields.io/npm/v/opencode-sesh" alt="npm"></a>
8
11
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
9
12
  <a href="#contributing"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome"></a>
@@ -226,6 +229,10 @@ workspace at its directory; outside cmux it shows a toast instead.
226
229
 
227
230
  ### TUI sidebar and home list
228
231
 
232
+ `sidebarPlacement` (`"append"` or `"prepend"`, default: `"append"`) places the
233
+ **Sessions** section below or above OpenCode's native sidebar sections. With
234
+ `"append"`, native todos appear above Sessions when available.
235
+
229
236
  `showSidebar` (boolean, default: `true`) controls whether the **Sessions** section
230
237
  appears in the sidebar. Set it to `false` to hide the section while keeping the
231
238
  home list, session picker, and other commands available. OpenCode's native sidebar
@@ -238,7 +245,7 @@ the `opencode.json` entry unchanged:
238
245
  "plugins": [
239
246
  {
240
247
  "package": "opencode-sesh",
241
- "options": { "showSidebar": false }
248
+ "options": { "showSidebar": true, "sidebarPlacement": "append" }
242
249
  }
243
250
  ]
244
251
  }
@@ -333,6 +340,13 @@ covers the current project) and offer to resume one.
333
340
  extracted text under `${XDG_CACHE_HOME:-~/.cache}/sesh` (or `SESH_CACHE_DIR`).
334
341
  Nothing is uploaded anywhere.
335
342
 
343
+ **What if a lock stays busy after a crash?** Stop all sesh pickers before
344
+ manually removing the lock directory named in the error; for pin locks, also
345
+ fully quit OpenCode. Locks are never automatically reclaimed. Restart OpenCode
346
+ after upgrading so older plugin instances cannot reclaim locks. Interrupted
347
+ extraction temporary files may remain in the cache; remove them only after all
348
+ pickers have stopped.
349
+
336
350
  **Does it work on Windows?** It targets macOS and Linux. WSL should work; native
337
351
  Windows is untested.
338
352
 
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}"