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 +14 -0
- package/README.md +24 -14
- package/bin/sesh-costs.sh +0 -1
- package/bin/sesh-delete.sh +4 -12
- package/bin/sesh-list.sh +37 -25
- package/bin/sesh-pins.sh +12 -9
- package/bin/sesh-prune.sh +8 -5
- package/bin/sesh-refresh-worker.sh +4 -1
- package/bin/sesh-waiting.sh +1 -2
- package/bin/sesh.sh +3 -3
- package/dist/tui.js +695 -682
- package/install.sh +24 -19
- package/package.json +14 -14
- package/plugins/sesh-list.ts +2 -2
- package/tui-plugins/sesh-panel/tui.tsx +139 -136
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
package/bin/sesh-delete.sh
CHANGED
|
@@ -33,18 +33,10 @@ release_lock() {
|
|
|
33
33
|
rmdir "$lock" 2>/dev/null || true
|
|
34
34
|
lock_owned=0
|
|
35
35
|
}
|
|
36
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
370
|
-
printf '%s\n' "$record" > "$
|
|
371
|
-
mv "$
|
|
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" -
|
|
16
|
-
|
|
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
|
-
[
|
|
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
|
|
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
|
|
77
|
-
WAITING_IDS=$(bash "$SCRIPT_DIR/sesh-waiting.sh" --json
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/bin/sesh-waiting.sh
CHANGED
|
@@ -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
|
|
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)
|
|
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)
|
|
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}"
|