omnilane 0.32.0 → 0.33.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -1
- package/README.ja.md +15 -1
- package/README.ko.md +15 -1
- package/README.md +16 -2
- package/README.zh-CN.md +14 -1
- package/README.zh-TW.md +14 -1
- package/VERSION +1 -1
- package/completions/_omnilane +2 -1
- package/completions/omnilane.bash +2 -2
- package/completions/omnilane.fish +2 -0
- package/hooks/report-completions.sh +36 -5
- package/hooks/routing-instruction.md +25 -10
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/dispatch.sh +336 -48
- package/scripts/doctor.sh +1 -1
- package/scripts/jobs.sh +74 -30
- package/scripts/lib/common.sh +88 -0
- package/scripts/lib/job-worker.sh +0 -28
- package/scripts/provider-probe.sh +1 -1
- package/scripts/release-audit.sh +1 -1
- package/scripts/runners/run-claude.sh +63 -4
- package/scripts/runners/run-codex.sh +31 -4
- package/scripts/runners/run-gemini.sh +49 -1
- package/scripts/runners/run-grok.sh +18 -2
- package/skills/omnilane/SKILL.md +43 -7
package/scripts/dispatch.sh
CHANGED
|
@@ -3,7 +3,7 @@ set -euo pipefail
|
|
|
3
3
|
# omnilane dispatch — one routing table, any harness.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# dispatch.sh [--background] [--live|--single-shot] [--dry-run]
|
|
6
|
+
# dispatch.sh [--background] [--live|--single-shot] [--dry-run] [--thread NAME]
|
|
7
7
|
# [--mode advise|work|sysops] [--workdir DIR]
|
|
8
8
|
# [--vendor V] [--model M] [--effort E] [--timeout SECONDS]
|
|
9
9
|
# [--job-timeout SECONDS] [--idle-timeout SECONDS] LANE "TASK TEXT"
|
|
@@ -34,9 +34,10 @@ source "$OMNILANE_REPO/scripts/lib/live-protocol.sh"
|
|
|
34
34
|
MODE="advise"; WORKDIR="$PWD"; BACKGROUND=0; DRY_RUN=0
|
|
35
35
|
OVERRIDE_VENDOR=""; OVERRIDE_MODEL=""; OVERRIDE_EFFORT=""; OVERRIDE_TIMEOUT=""
|
|
36
36
|
OVERRIDE_JOB_TIMEOUT=""; OVERRIDE_IDLE_TIMEOUT=""; SESSION_REQUEST="auto"
|
|
37
|
+
THREAD_NAME=""; THREAD_MODE=""; THREAD_ID=""; THREAD_TURN=""; THREAD_CREATED=""
|
|
37
38
|
|
|
38
39
|
usage_error() {
|
|
39
|
-
echo 'usage: dispatch.sh [--background] [--dry-run] [flags] LANE "TASK" | [--json] --list|--validate [--json] | [--json] --explain LANE [--json] | --help' >&2
|
|
40
|
+
echo 'usage: dispatch.sh [--background] [--dry-run] [--thread NAME] [flags] LANE "TASK" | [--json] --list|--validate [--json] | [--json] --explain LANE [--json] | --help' >&2
|
|
40
41
|
exit 2
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -50,6 +51,7 @@ Dispatch one task to the first available vendor CLI in LANE's fallback chain.
|
|
|
50
51
|
A TASK of "-" reads the task text from stdin.
|
|
51
52
|
|
|
52
53
|
flags:
|
|
54
|
+
--thread NAME continue a named thread (claude/codex/grok/gemini; single-shot)
|
|
53
55
|
--live require a resident session (background only)
|
|
54
56
|
--single-shot force one-shot dispatch (background only)
|
|
55
57
|
--idle-timeout SECONDS close an idle live mailbox (default 900; 0 disables)
|
|
@@ -76,33 +78,6 @@ read-only queries (no provider call, no job state; --json for one envelope):
|
|
|
76
78
|
EOF
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
json_escape() {
|
|
80
|
-
local s="$1" out="" ch escaped code i
|
|
81
|
-
for ((i = 0; i < ${#s}; i++)); do
|
|
82
|
-
ch="${s:i:1}"
|
|
83
|
-
case "$ch" in
|
|
84
|
-
'"') out="$out\\\"" ;;
|
|
85
|
-
'\\') out="$out\\\\" ;;
|
|
86
|
-
$'\b') out="$out\\b" ;;
|
|
87
|
-
$'\f') out="$out\\f" ;;
|
|
88
|
-
$'\n') out="$out\\n" ;;
|
|
89
|
-
$'\r') out="$out\\r" ;;
|
|
90
|
-
$'\t') out="$out\\t" ;;
|
|
91
|
-
*)
|
|
92
|
-
LC_CTYPE=C printf -v code '%d' "'$ch"
|
|
93
|
-
# Bash 3.2 on macOS reports UTF-8 bytes above 0x7f as signed values.
|
|
94
|
-
# Only non-negative C0 bytes are JSON control characters.
|
|
95
|
-
if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
|
|
96
|
-
printf -v escaped '\\u%04x' "$code"
|
|
97
|
-
out="$out$escaped"
|
|
98
|
-
else
|
|
99
|
-
out="$out$ch"
|
|
100
|
-
fi
|
|
101
|
-
;;
|
|
102
|
-
esac
|
|
103
|
-
done
|
|
104
|
-
printf '%s' "$out"
|
|
105
|
-
}
|
|
106
81
|
|
|
107
82
|
# Run one read-only inspection command once, preserving its human output and
|
|
108
83
|
# exit status inside a stable JSON envelope. This avoids a second routing
|
|
@@ -125,6 +100,172 @@ emit_json_inspection() {
|
|
|
125
100
|
exit "$rc"
|
|
126
101
|
}
|
|
127
102
|
|
|
103
|
+
thread_refuse() {
|
|
104
|
+
local notice="$1"
|
|
105
|
+
printf '%s\n' "$notice"
|
|
106
|
+
printf '%s\n' "$notice" >&2
|
|
107
|
+
exit 2
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
generate_thread_id() {
|
|
111
|
+
local value=""
|
|
112
|
+
if command -v uuidgen >/dev/null 2>&1; then
|
|
113
|
+
value="$(uuidgen)" || return 1
|
|
114
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
115
|
+
value="$(python3 -c 'import uuid; print(uuid.uuid4())')" || return 1
|
|
116
|
+
else
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
[[ "$value" =~ ^[A-Za-z0-9._:-]+$ && "${#value}" -le 256 ]] || return 1
|
|
120
|
+
printf '%s\n' "$value"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
extract_claude_result_session_id() {
|
|
124
|
+
local events_path="$1"
|
|
125
|
+
[[ -f "$events_path" && ! -L "$events_path" ]] || return 1
|
|
126
|
+
perl -MJSON::PP -e '
|
|
127
|
+
use strict;
|
|
128
|
+
use warnings;
|
|
129
|
+
my ($path) = @ARGV;
|
|
130
|
+
open my $fh, "<", $path or die $!;
|
|
131
|
+
my $last = "";
|
|
132
|
+
while (my $line = <$fh>) {
|
|
133
|
+
my $event = eval { decode_json($line) };
|
|
134
|
+
next unless ref($event) eq "HASH" && ($event->{type} // "") eq "result";
|
|
135
|
+
my $id = $event->{session_id};
|
|
136
|
+
next if !defined($id) || ref($id) || $id !~ /\A[A-Za-z0-9._:-]{1,256}\z/;
|
|
137
|
+
$last = $id;
|
|
138
|
+
}
|
|
139
|
+
exit 1 unless length($last);
|
|
140
|
+
print $last;
|
|
141
|
+
' "$events_path" 2>/dev/null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
extract_codex_result_session_id() {
|
|
145
|
+
local events_path="$1"
|
|
146
|
+
[[ -f "$events_path" && ! -L "$events_path" ]] || return 1
|
|
147
|
+
perl -MJSON::PP -e '
|
|
148
|
+
use strict;
|
|
149
|
+
use warnings;
|
|
150
|
+
my ($path) = @ARGV;
|
|
151
|
+
open my $fh, "<", $path or die $!;
|
|
152
|
+
my $last = "";
|
|
153
|
+
while (my $line = <$fh>) {
|
|
154
|
+
my $event = eval { decode_json($line) };
|
|
155
|
+
next unless ref($event) eq "HASH";
|
|
156
|
+
my $id = $event->{thread_id};
|
|
157
|
+
next if !defined($id) || ref($id) || $id !~ /\A[A-Za-z0-9._:-]{1,256}\z/;
|
|
158
|
+
$last = $id;
|
|
159
|
+
}
|
|
160
|
+
exit 1 unless length($last);
|
|
161
|
+
print $last;
|
|
162
|
+
' "$events_path" 2>/dev/null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
extract_grok_result_session_id() {
|
|
166
|
+
local stderr_path="$1" fallback="$2" forked_id=""
|
|
167
|
+
if [[ -f "$stderr_path" && ! -L "$stderr_path" ]] && grep -Eiq 'fork' "$stderr_path"; then
|
|
168
|
+
forked_id="$(perl -ne '
|
|
169
|
+
while (/([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})/g) {
|
|
170
|
+
$last = $1;
|
|
171
|
+
}
|
|
172
|
+
END { exit 1 unless defined($last); print $last }
|
|
173
|
+
' "$stderr_path" 2>/dev/null)" || return 1
|
|
174
|
+
printf '%s\n' "$forked_id"
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
[[ "$fallback" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]] || return 1
|
|
178
|
+
printf '%s\n' "$fallback"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
extract_gemini_result_session_id() {
|
|
182
|
+
local result_path="$1"
|
|
183
|
+
[[ -f "$result_path" && ! -L "$result_path" ]] || return 1
|
|
184
|
+
perl -MJSON::PP -e '
|
|
185
|
+
use strict;
|
|
186
|
+
use warnings;
|
|
187
|
+
my ($path) = @ARGV;
|
|
188
|
+
open my $fh, "<", $path or die $!;
|
|
189
|
+
local $/;
|
|
190
|
+
my $result = decode_json(<$fh>);
|
|
191
|
+
my $id = ref($result) eq "HASH" ? $result->{conversation_id} : undef;
|
|
192
|
+
exit 1 if !defined($id) || ref($id) || $id !~ /\A[A-Za-z0-9._:-]{1,256}\z/;
|
|
193
|
+
print $id;
|
|
194
|
+
' "$result_path" 2>/dev/null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
append_thread_notice() {
|
|
198
|
+
local notice="$1"
|
|
199
|
+
printf '%s\n' "$notice" >&2
|
|
200
|
+
printf '\n%s\n' "$notice" >> "$JOB_DIR/out.txt"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
write_thread_state() {
|
|
204
|
+
local returned_id="$1" final="$OMNILANE_HOME/threads/$THREAD_NAME.json"
|
|
205
|
+
local tmp="$OMNILANE_HOME/threads/.$THREAD_NAME.tmp.$$-$RANDOM"
|
|
206
|
+
local updated old_umask write_rc=0
|
|
207
|
+
prepare_threads_store || return 1
|
|
208
|
+
updated="$(date -u +%FT%TZ)" || return 1
|
|
209
|
+
[[ -n "$THREAD_CREATED" ]] || THREAD_CREATED="$updated"
|
|
210
|
+
old_umask="$(umask)"
|
|
211
|
+
umask 077
|
|
212
|
+
printf '{"name":"%s","vendor":"%s","model":"%s","effort":"%s","workdir":"%s","session_id":"%s","turns":%s,"last_job_id":"%s","created":"%s","updated":"%s"}\n' \
|
|
213
|
+
"$(json_escape "$THREAD_NAME")" "$(json_escape "$VENDOR")" \
|
|
214
|
+
"$(json_escape "$MODEL")" "$(json_escape "$EFFORT")" \
|
|
215
|
+
"$(json_escape "$WORKDIR")" "$(json_escape "$returned_id")" \
|
|
216
|
+
"$THREAD_TURN" "$(json_escape "$JOB_ID")" \
|
|
217
|
+
"$(json_escape "$THREAD_CREATED")" "$(json_escape "$updated")" \
|
|
218
|
+
> "$tmp" || write_rc=$?
|
|
219
|
+
if [[ "$write_rc" -eq 0 ]]; then
|
|
220
|
+
chmod 600 "$tmp" || write_rc=$?
|
|
221
|
+
fi
|
|
222
|
+
if [[ "$write_rc" -eq 0 ]]; then
|
|
223
|
+
mv "$tmp" "$final" || write_rc=$?
|
|
224
|
+
fi
|
|
225
|
+
umask "$old_umask"
|
|
226
|
+
rm "$tmp" 2>/dev/null || true
|
|
227
|
+
return "$write_rc"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
update_thread_state() {
|
|
231
|
+
local returned_id notice original_id="$THREAD_ID"
|
|
232
|
+
case "$VENDOR" in
|
|
233
|
+
claude)
|
|
234
|
+
returned_id="$(extract_claude_result_session_id "$JOB_DIR/out.txt.events.jsonl")"
|
|
235
|
+
;;
|
|
236
|
+
codex)
|
|
237
|
+
returned_id="$(extract_codex_result_session_id "$JOB_DIR/out.txt.progress.log")"
|
|
238
|
+
;;
|
|
239
|
+
grok)
|
|
240
|
+
returned_id="$(extract_grok_result_session_id "$JOB_DIR/out.txt.stderr.log" "$THREAD_ID")"
|
|
241
|
+
;;
|
|
242
|
+
gemini)
|
|
243
|
+
returned_id="$(extract_gemini_result_session_id "$JOB_DIR/out.txt.result.json")"
|
|
244
|
+
;;
|
|
245
|
+
*)
|
|
246
|
+
return 1
|
|
247
|
+
;;
|
|
248
|
+
esac || {
|
|
249
|
+
append_thread_notice "omnilane: thread $THREAD_NAME did not return a resumable $VENDOR session id"
|
|
250
|
+
return 1
|
|
251
|
+
}
|
|
252
|
+
if [[ "$returned_id" != "$THREAD_ID" && "$THREAD_ID" != "pending" ]]; then
|
|
253
|
+
notice="omnilane: thread $THREAD_NAME: $VENDOR returned a different session id ($THREAD_ID -> $returned_id)"
|
|
254
|
+
append_thread_notice "$notice"
|
|
255
|
+
fi
|
|
256
|
+
write_thread_state "$returned_id" || {
|
|
257
|
+
append_thread_notice "omnilane: thread $THREAD_NAME state could not be written"
|
|
258
|
+
return 1
|
|
259
|
+
}
|
|
260
|
+
THREAD_ID="$returned_id"
|
|
261
|
+
# Completes the pre-run "session pending" line on stdout; it is visibility,
|
|
262
|
+
# not a notice, so it stays out of out.txt (which foreground mode cats).
|
|
263
|
+
if [[ "$original_id" == "pending" ]]; then
|
|
264
|
+
printf 'omnilane: thread %s turn %s (%s session %s, %s)\n' \
|
|
265
|
+
"$THREAD_NAME" "$THREAD_TURN" "$VENDOR" "$THREAD_ID" "$THREAD_MODE"
|
|
266
|
+
fi
|
|
267
|
+
}
|
|
268
|
+
|
|
128
269
|
print_dry_run_value() {
|
|
129
270
|
printf '%s=' "$1"
|
|
130
271
|
printf '%q\n' "$2"
|
|
@@ -148,6 +289,12 @@ print_dry_run_plan() {
|
|
|
148
289
|
printf 'idle_timeout=%s\n' "$IDLE_TIMEOUT"
|
|
149
290
|
print_dry_run_value session_mode "$SESSION_MODE"
|
|
150
291
|
printf 'candidate=%s/%s\n' "$RESOLVED_IDX" "$RESOLVED_TOTAL"
|
|
292
|
+
if [[ -n "$THREAD_NAME" ]]; then
|
|
293
|
+
print_dry_run_value thread "$THREAD_NAME"
|
|
294
|
+
print_dry_run_value thread_mode "$THREAD_MODE"
|
|
295
|
+
print_dry_run_value thread_session "$THREAD_ID"
|
|
296
|
+
print_dry_run_value thread_turn "$THREAD_TURN"
|
|
297
|
+
fi
|
|
151
298
|
printf 'background=%s\n' "$background"
|
|
152
299
|
printf 'task_source=%s\n' "$task_source"
|
|
153
300
|
printf 'provider_invoked=no\n'
|
|
@@ -480,7 +627,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
480
627
|
}
|
|
481
628
|
SESSION_REQUEST="single-shot"; shift ;;
|
|
482
629
|
--dry-run) DRY_RUN=1; shift ;;
|
|
483
|
-
--mode|--workdir|--vendor|--model|--effort|--timeout|--job-timeout|--idle-timeout)
|
|
630
|
+
--mode|--workdir|--vendor|--model|--effort|--timeout|--job-timeout|--idle-timeout|--thread)
|
|
484
631
|
# Value-taking flags: a missing value must be a clean usage error (exit 2),
|
|
485
632
|
# not a `set -u` "unbound variable" crash on $2.
|
|
486
633
|
[[ $# -ge 2 ]] || { echo "omnilane: $1 needs a value" >&2; exit 2; }
|
|
@@ -493,6 +640,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
493
640
|
--timeout) OVERRIDE_TIMEOUT="$2" ;;
|
|
494
641
|
--job-timeout) OVERRIDE_JOB_TIMEOUT="$2" ;;
|
|
495
642
|
--idle-timeout) OVERRIDE_IDLE_TIMEOUT="$2" ;;
|
|
643
|
+
--thread) THREAD_NAME="$2" ;;
|
|
496
644
|
esac
|
|
497
645
|
shift 2 ;;
|
|
498
646
|
-*) echo "omnilane: unknown flag" >&2; exit 2 ;;
|
|
@@ -506,7 +654,22 @@ done
|
|
|
506
654
|
echo 'omnilane: unexpected extra arguments; quote a multiword task' >&2
|
|
507
655
|
exit 2
|
|
508
656
|
}
|
|
509
|
-
if [[ "$
|
|
657
|
+
if [[ -n "$THREAD_NAME" ]] && ! omnilane_valid_thread_name "$THREAD_NAME"; then
|
|
658
|
+
thread_refuse "omnilane: invalid --thread name '$THREAD_NAME' (want 1-64 characters: A-Z, a-z, 0-9, dot, underscore, hyphen)"
|
|
659
|
+
fi
|
|
660
|
+
if [[ -n "$THREAD_NAME" && "$SESSION_REQUEST" == "live" ]]; then
|
|
661
|
+
thread_refuse "omnilane: --thread cannot be combined with --live; threads run single-shot and a live session is already a conversation"
|
|
662
|
+
fi
|
|
663
|
+
if [[ -n "$THREAD_NAME" && -n "$OVERRIDE_VENDOR" ]]; then
|
|
664
|
+
case "$OVERRIDE_VENDOR" in
|
|
665
|
+
claude|codex|grok|gemini) ;;
|
|
666
|
+
*)
|
|
667
|
+
thread_refuse "omnilane: --thread is supported for claude, codex, grok and gemini only; resolved vendor '$OVERRIDE_VENDOR' cannot continue a thread"
|
|
668
|
+
;;
|
|
669
|
+
esac
|
|
670
|
+
fi
|
|
671
|
+
if [[ "$SESSION_REQUEST" != "auto" && "$BACKGROUND" -ne 1 &&
|
|
672
|
+
! ( -n "$THREAD_NAME" && "$SESSION_REQUEST" == "single-shot" ) ]]; then
|
|
510
673
|
echo "omnilane: --${SESSION_REQUEST} requires --background" >&2
|
|
511
674
|
exit 2
|
|
512
675
|
fi
|
|
@@ -559,6 +722,63 @@ VENDOR="${FIELDS[0]}"; MODEL="${FIELDS[1]:-}"; EFFORT="${FIELDS[2]:-}"
|
|
|
559
722
|
[[ -n "$OVERRIDE_MODEL" ]] && MODEL="$OVERRIDE_MODEL"
|
|
560
723
|
[[ -n "$OVERRIDE_EFFORT" ]] && EFFORT="$OVERRIDE_EFFORT"
|
|
561
724
|
|
|
725
|
+
if [[ -n "$THREAD_NAME" ]]; then
|
|
726
|
+
case "$VENDOR" in
|
|
727
|
+
claude|codex|grok|gemini) ;;
|
|
728
|
+
*)
|
|
729
|
+
thread_refuse "omnilane: --thread is supported for claude, codex, grok and gemini only; resolved vendor '$VENDOR' cannot continue a thread"
|
|
730
|
+
;;
|
|
731
|
+
esac
|
|
732
|
+
THREAD_WORKDIR="$(cd -- "$WORKDIR" 2>/dev/null && pwd -P)" || {
|
|
733
|
+
thread_refuse "omnilane: thread $THREAD_NAME workdir is not accessible: $WORKDIR"
|
|
734
|
+
}
|
|
735
|
+
WORKDIR="$THREAD_WORKDIR"
|
|
736
|
+
THREADS_ROOT="$OMNILANE_HOME/threads"
|
|
737
|
+
if [[ -L "$THREADS_ROOT" || ( -e "$THREADS_ROOT" && ! -d "$THREADS_ROOT" ) ]]; then
|
|
738
|
+
thread_refuse "omnilane: unsafe thread store path (want real directory): $THREADS_ROOT"
|
|
739
|
+
fi
|
|
740
|
+
if [[ "$DRY_RUN" -ne 1 ]]; then
|
|
741
|
+
prepare_threads_store || thread_refuse "omnilane: thread store could not be prepared"
|
|
742
|
+
fi
|
|
743
|
+
THREAD_STATE_FILE="$THREADS_ROOT/$THREAD_NAME.json"
|
|
744
|
+
if [[ -e "$THREAD_STATE_FILE" || -L "$THREAD_STATE_FILE" ]]; then
|
|
745
|
+
read_thread_state "$THREAD_STATE_FILE" "$THREAD_NAME" || {
|
|
746
|
+
thread_refuse "omnilane: thread $THREAD_NAME state is not safely readable"
|
|
747
|
+
}
|
|
748
|
+
if [[ "$THREAD_STATE_VENDOR" != "$VENDOR" ]]; then
|
|
749
|
+
thread_refuse "omnilane: thread $THREAD_NAME pinned vendor '$THREAD_STATE_VENDOR' but resolved vendor is '$VENDOR'"
|
|
750
|
+
fi
|
|
751
|
+
if [[ "$THREAD_STATE_MODEL" != "$MODEL" ]]; then
|
|
752
|
+
thread_refuse "omnilane: thread $THREAD_NAME pinned model '$THREAD_STATE_MODEL' but resolved model is '$MODEL'"
|
|
753
|
+
fi
|
|
754
|
+
if [[ "$THREAD_STATE_EFFORT" != "$EFFORT" ]]; then
|
|
755
|
+
thread_refuse "omnilane: thread $THREAD_NAME pinned effort '$THREAD_STATE_EFFORT' but resolved effort is '$EFFORT'"
|
|
756
|
+
fi
|
|
757
|
+
if [[ "$THREAD_STATE_WORKDIR" != "$WORKDIR" ]]; then
|
|
758
|
+
thread_refuse "omnilane: thread $THREAD_NAME pinned workdir '$THREAD_STATE_WORKDIR' but effective workdir is '$WORKDIR'"
|
|
759
|
+
fi
|
|
760
|
+
THREAD_MODE="resume"
|
|
761
|
+
THREAD_ID="$THREAD_STATE_SESSION_ID"
|
|
762
|
+
THREAD_TURN=$((THREAD_STATE_TURNS + 1))
|
|
763
|
+
THREAD_CREATED="$THREAD_STATE_CREATED"
|
|
764
|
+
else
|
|
765
|
+
THREAD_MODE="new"
|
|
766
|
+
case "$VENDOR" in
|
|
767
|
+
codex|gemini) THREAD_ID="pending" ;;
|
|
768
|
+
*)
|
|
769
|
+
THREAD_ID="$(generate_thread_id)" || {
|
|
770
|
+
thread_refuse "omnilane: thread $THREAD_NAME could not generate a session id"
|
|
771
|
+
}
|
|
772
|
+
;;
|
|
773
|
+
esac
|
|
774
|
+
THREAD_TURN=1
|
|
775
|
+
THREAD_CREATED=""
|
|
776
|
+
fi
|
|
777
|
+
export OMNILANE_THREAD_MODE="$THREAD_MODE"
|
|
778
|
+
export OMNILANE_THREAD_ID="$THREAD_ID"
|
|
779
|
+
export OMNILANE_THREAD_NAME="$THREAD_NAME"
|
|
780
|
+
fi
|
|
781
|
+
|
|
562
782
|
if [[ "$VENDOR" == "off" ]]; then
|
|
563
783
|
echo "omnilane: lane '$LANE' is disabled in routing config" >&2; exit 3
|
|
564
784
|
fi
|
|
@@ -566,7 +786,7 @@ RUNNER="$OMNILANE_REPO/scripts/runners/run-$VENDOR.sh"
|
|
|
566
786
|
[[ -x "$RUNNER" ]] || { echo "omnilane: no runner for vendor '$VENDOR'" >&2; exit 2; }
|
|
567
787
|
|
|
568
788
|
SESSION_MODE="single-shot"
|
|
569
|
-
if [[ "$SESSION_REQUEST" != "single-shot" ]] && live_vendor_capable "$VENDOR"; then
|
|
789
|
+
if [[ -z "$THREAD_NAME" && "$SESSION_REQUEST" != "single-shot" ]] && live_vendor_capable "$VENDOR"; then
|
|
570
790
|
SESSION_MODE="live"
|
|
571
791
|
fi
|
|
572
792
|
if [[ "$SESSION_REQUEST" == "live" ]] && ! live_vendor_capable "$VENDOR"; then
|
|
@@ -685,7 +905,12 @@ JOB_ID="$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
|
|
|
685
905
|
JOB_DIR="$JOBS_ROOT/$JOB_ID"
|
|
686
906
|
mkdir -m 700 "$JOB_DIR"
|
|
687
907
|
FOREMAN_SESSION=""
|
|
688
|
-
if
|
|
908
|
+
if [[ -n "${CLAUDE_CODE_SESSION_ID+x}" ]]; then
|
|
909
|
+
if [[ "$CLAUDE_CODE_SESSION_ID" =~ ^[A-Za-z0-9._:-]+$ &&
|
|
910
|
+
"${#CLAUDE_CODE_SESSION_ID}" -le 256 ]]; then
|
|
911
|
+
FOREMAN_SESSION="$CLAUDE_CODE_SESSION_ID"
|
|
912
|
+
fi
|
|
913
|
+
elif resolved_foreman_session="$(find_foreman_session "$$" 2>/dev/null)"; then
|
|
689
914
|
FOREMAN_SESSION="$resolved_foreman_session"
|
|
690
915
|
fi
|
|
691
916
|
|
|
@@ -696,18 +921,56 @@ else
|
|
|
696
921
|
fi
|
|
697
922
|
|
|
698
923
|
# meta "timeout" is the resolved per-CLI-call watchdog cap, not a whole-job total.
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
"
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
924
|
+
THREAD_TURN_JSON="${THREAD_TURN:-null}"
|
|
925
|
+
if [[ -n "$THREAD_NAME" ]]; then
|
|
926
|
+
(umask 077; printf '{"lane":"%s","vendor":"%s","session_mode":"%s","idle_timeout":%s,"model":"%s","effort":"%s","timeout":%s,"job_timeout":%s,"mode":"%s","workdir":"%s","foreman_session":"%s","thread":"%s","thread_turn":%s,"candidate":"%s/%s","started":"%s"}\n' \
|
|
927
|
+
"$(json_escape "$LANE")" "$(json_escape "$VENDOR")" "$(json_escape "$SESSION_MODE")" \
|
|
928
|
+
"$IDLE_TIMEOUT" "$(json_escape "$MODEL")" "$(json_escape "$EFFORT")" \
|
|
929
|
+
"$TIMEOUT" "$JOB_TIMEOUT_JSON" "$(json_escape "$MODE")" \
|
|
930
|
+
"$(json_escape "$WORKDIR")" "$(json_escape "$FOREMAN_SESSION")" \
|
|
931
|
+
"$(json_escape "$THREAD_NAME")" "$THREAD_TURN_JSON" \
|
|
932
|
+
"$RESOLVED_IDX" "$RESOLVED_TOTAL" \
|
|
933
|
+
"$(date -u +%FT%TZ)" > "$JOB_DIR/meta.json")
|
|
934
|
+
else
|
|
935
|
+
(umask 077; printf '{"lane":"%s","vendor":"%s","session_mode":"%s","idle_timeout":%s,"model":"%s","effort":"%s","timeout":%s,"job_timeout":%s,"mode":"%s","workdir":"%s","foreman_session":"%s","candidate":"%s/%s","started":"%s"}\n' \
|
|
936
|
+
"$(json_escape "$LANE")" "$(json_escape "$VENDOR")" "$(json_escape "$SESSION_MODE")" \
|
|
937
|
+
"$IDLE_TIMEOUT" "$(json_escape "$MODEL")" "$(json_escape "$EFFORT")" \
|
|
938
|
+
"$TIMEOUT" "$JOB_TIMEOUT_JSON" "$(json_escape "$MODE")" \
|
|
939
|
+
"$(json_escape "$WORKDIR")" "$(json_escape "$FOREMAN_SESSION")" \
|
|
940
|
+
"$RESOLVED_IDX" "$RESOLVED_TOTAL" \
|
|
941
|
+
"$(date -u +%FT%TZ)" > "$JOB_DIR/meta.json")
|
|
942
|
+
fi
|
|
943
|
+
|
|
944
|
+
if [[ -n "$THREAD_NAME" ]]; then
|
|
945
|
+
printf 'omnilane: thread %s turn %s (%s session %s, %s)\n' \
|
|
946
|
+
"$THREAD_NAME" "$THREAD_TURN" "$VENDOR" "$THREAD_ID" "$THREAD_MODE"
|
|
947
|
+
fi
|
|
706
948
|
|
|
707
949
|
secure_job_files() {
|
|
708
950
|
find "$JOB_DIR" -type f -exec chmod 600 {} +
|
|
709
951
|
}
|
|
710
952
|
|
|
953
|
+
sanitize_utf8() {
|
|
954
|
+
local max="$1" drop_leading="${2:-0}"
|
|
955
|
+
perl -MEncode -e '
|
|
956
|
+
use strict;
|
|
957
|
+
use warnings;
|
|
958
|
+
binmode STDIN, ":raw";
|
|
959
|
+
binmode STDOUT, ":raw";
|
|
960
|
+
local $/;
|
|
961
|
+
my $bytes = <STDIN> // "";
|
|
962
|
+
my ($max, $drop_leading) = @ARGV;
|
|
963
|
+
$bytes =~ s/\A[\x80-\xBF]+// if $drop_leading;
|
|
964
|
+
my $text = Encode::decode("UTF-8", $bytes, Encode::FB_DEFAULT);
|
|
965
|
+
my $encoded = Encode::encode("UTF-8", $text);
|
|
966
|
+
while (length($encoded) > $max && length($text)) {
|
|
967
|
+
$text =~ s/\A.//s;
|
|
968
|
+
$encoded = Encode::encode("UTF-8", $text);
|
|
969
|
+
}
|
|
970
|
+
print $encoded;
|
|
971
|
+
' "$max" "$drop_leading"
|
|
972
|
+
}
|
|
973
|
+
|
|
711
974
|
completion_tail() {
|
|
712
975
|
local path="$1" size=0 keep value="" sentinel=$'\001'
|
|
713
976
|
local note=$'[truncated: leading output omitted]\n'
|
|
@@ -718,9 +981,16 @@ completion_tail() {
|
|
|
718
981
|
[[ "$size" =~ ^[0-9]+$ ]] || return 1
|
|
719
982
|
if [[ "$size" -gt 2000 ]]; then
|
|
720
983
|
keep=$((2000 - ${#note}))
|
|
721
|
-
value="$(
|
|
984
|
+
value="$({
|
|
985
|
+
printf '%s' "$note"
|
|
986
|
+
tail -c "$keep" "$path" | sanitize_utf8 "$keep" 1
|
|
987
|
+
printf '%s' "$sentinel"
|
|
988
|
+
})" || return 1
|
|
722
989
|
else
|
|
723
|
-
value="$(
|
|
990
|
+
value="$({
|
|
991
|
+
sanitize_utf8 2000 0 < "$path"
|
|
992
|
+
printf '%s' "$sentinel"
|
|
993
|
+
})" || return 1
|
|
724
994
|
fi
|
|
725
995
|
value="${value%"$sentinel"}"
|
|
726
996
|
fi
|
|
@@ -738,11 +1008,21 @@ write_completion_record() {
|
|
|
738
1008
|
finished="$(date -u +%FT%TZ)" || return 1
|
|
739
1009
|
old_umask="$(umask)"
|
|
740
1010
|
umask 077
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1011
|
+
if [[ -n "$THREAD_NAME" ]]; then
|
|
1012
|
+
printf '{"job_id":"%s","lane":"%s","vendor":"%s","model":"%s","mode":"%s","workdir":"%s","foreman_session":"%s","thread":"%s","thread_turn":%s,"exit":%s,"finished":"%s","tail":"%s"}\n' \
|
|
1013
|
+
"$(json_escape "$JOB_ID")" "$(json_escape "$LANE")" "$(json_escape "$VENDOR")" \
|
|
1014
|
+
"$(json_escape "$MODEL")" "$(json_escape "$MODE")" "$(json_escape "$WORKDIR")" \
|
|
1015
|
+
"$(json_escape "$FOREMAN_SESSION")" "$(json_escape "$THREAD_NAME")" \
|
|
1016
|
+
"$THREAD_TURN_JSON" "$rc" "$(json_escape "$finished")" \
|
|
1017
|
+
"$(json_escape "$tail_value")" > "$tmp" || write_rc=$?
|
|
1018
|
+
else
|
|
1019
|
+
printf '{"job_id":"%s","lane":"%s","vendor":"%s","model":"%s","mode":"%s","workdir":"%s","foreman_session":"%s","exit":%s,"finished":"%s","tail":"%s"}\n' \
|
|
1020
|
+
"$(json_escape "$JOB_ID")" "$(json_escape "$LANE")" "$(json_escape "$VENDOR")" \
|
|
1021
|
+
"$(json_escape "$MODEL")" "$(json_escape "$MODE")" "$(json_escape "$WORKDIR")" \
|
|
1022
|
+
"$(json_escape "$FOREMAN_SESSION")" \
|
|
1023
|
+
"$rc" "$(json_escape "$finished")" "$(json_escape "$tail_value")" \
|
|
1024
|
+
> "$tmp" || write_rc=$?
|
|
1025
|
+
fi
|
|
746
1026
|
if [[ "$write_rc" -eq 0 ]]; then
|
|
747
1027
|
chmod 600 "$tmp" || write_rc=$?
|
|
748
1028
|
fi
|
|
@@ -757,11 +1037,19 @@ write_completion_record() {
|
|
|
757
1037
|
|
|
758
1038
|
finish_job() {
|
|
759
1039
|
local rc="$1"
|
|
1040
|
+
if [[ -n "$THREAD_NAME" ]]; then
|
|
1041
|
+
if [[ "$rc" -eq 0 ]]; then
|
|
1042
|
+
update_thread_state || rc=1
|
|
1043
|
+
elif [[ "$THREAD_MODE" == "resume" ]]; then
|
|
1044
|
+
append_thread_notice "omnilane: thread $THREAD_NAME could not be continued; stored $VENDOR session $THREAD_ID was preserved"
|
|
1045
|
+
fi
|
|
1046
|
+
fi
|
|
760
1047
|
secure_job_files
|
|
761
1048
|
(umask 077; printf '%s\n' "$rc" > "$JOB_DIR/exit")
|
|
762
1049
|
if [[ "${OMNILANE_INBOX:-1}" != "0" ]]; then
|
|
763
1050
|
write_completion_record "$rc" >/dev/null 2>&1 || true
|
|
764
1051
|
fi
|
|
1052
|
+
FINISHED_RC="$rc"
|
|
765
1053
|
}
|
|
766
1054
|
|
|
767
1055
|
run_job() {
|
|
@@ -787,7 +1075,7 @@ run_job() {
|
|
|
787
1075
|
rc=124
|
|
788
1076
|
fi
|
|
789
1077
|
finish_job "$rc"
|
|
790
|
-
return "$
|
|
1078
|
+
return "$FINISHED_RC"
|
|
791
1079
|
}
|
|
792
1080
|
|
|
793
1081
|
if [[ "$BACKGROUND" == "1" ]]; then
|
package/scripts/doctor.sh
CHANGED
package/scripts/jobs.sh
CHANGED
|
@@ -13,38 +13,11 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
|
|
|
13
13
|
# shellcheck disable=SC1091
|
|
14
14
|
source "$OMNILANE_REPO/scripts/lib/live-protocol.sh"
|
|
15
15
|
JOBS="$OMNILANE_HOME/jobs"
|
|
16
|
+
THREADS="$OMNILANE_HOME/threads"
|
|
16
17
|
JOB_ID_PATTERN='^[0-9]{8}-[0-9]{6}-[0-9]+-[0-9]+$'
|
|
17
18
|
JSON_MODE=0
|
|
18
19
|
COMMAND="unknown"
|
|
19
20
|
|
|
20
|
-
# The backslash case pattern is intentional.
|
|
21
|
-
# shellcheck disable=SC1003
|
|
22
|
-
json_escape() {
|
|
23
|
-
local s="$1" out="" ch escaped code i
|
|
24
|
-
for ((i = 0; i < ${#s}; i++)); do
|
|
25
|
-
ch="${s:i:1}"
|
|
26
|
-
case "$ch" in
|
|
27
|
-
'"') out="$out\\\"" ;;
|
|
28
|
-
'\\') out="$out\\\\" ;;
|
|
29
|
-
$'\b') out="$out\\b" ;;
|
|
30
|
-
$'\f') out="$out\\f" ;;
|
|
31
|
-
$'\n') out="$out\\n" ;;
|
|
32
|
-
$'\r') out="$out\\r" ;;
|
|
33
|
-
$'\t') out="$out\\t" ;;
|
|
34
|
-
*)
|
|
35
|
-
LC_CTYPE=C printf -v code '%d' "'$ch"
|
|
36
|
-
if [[ "$code" -ge 0 && "$code" -lt 32 ]]; then
|
|
37
|
-
printf -v escaped '\\u%04x' "$code"
|
|
38
|
-
out="$out$escaped"
|
|
39
|
-
else
|
|
40
|
-
out="$out$ch"
|
|
41
|
-
fi
|
|
42
|
-
;;
|
|
43
|
-
esac
|
|
44
|
-
done
|
|
45
|
-
printf '%s' "$out"
|
|
46
|
-
}
|
|
47
|
-
|
|
48
21
|
die() {
|
|
49
22
|
local rc="$1" message="$2"
|
|
50
23
|
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
@@ -56,7 +29,7 @@ die() {
|
|
|
56
29
|
exit "$rc"
|
|
57
30
|
}
|
|
58
31
|
|
|
59
|
-
USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done]|status ID|result ID|tail ID [--lines N]|send ID TEXT|watch ID|close ID|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|recommend [--last N] [--lane L] [--min-samples N]|wait ID [--timeout N]|cancel ID|rm ID|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
32
|
+
USAGE_TEXT="usage: jobs.sh [--json] list [--lane L] [--vendor V] [--status running|done]|status ID|result ID|tail ID [--lines N]|send ID TEXT|watch ID|close ID|retry ID [--background]|stats [--last N] [--lane L] [--vendor V]|recommend [--last N] [--lane L] [--min-samples N]|wait ID [--timeout N]|cancel ID|rm ID|threads [list] [--json]|threads show NAME [--json]|threads rm NAME|audit [--last N]|prune [--keep N] [--older-than DAYS] [--apply]|help"
|
|
60
33
|
|
|
61
34
|
usage() {
|
|
62
35
|
die 2 "$USAGE_TEXT"
|
|
@@ -347,7 +320,7 @@ set -- ${args[@]+"${args[@]}"}
|
|
|
347
320
|
COMMAND="${1:-unknown}"
|
|
348
321
|
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
349
322
|
case "$COMMAND" in
|
|
350
|
-
list|status|result|stats|recommend|audit) ;;
|
|
323
|
+
list|status|result|stats|recommend|audit|threads) ;;
|
|
351
324
|
*) usage ;;
|
|
352
325
|
esac
|
|
353
326
|
fi
|
|
@@ -358,6 +331,77 @@ case "${1:-}" in
|
|
|
358
331
|
help|--help|-h)
|
|
359
332
|
[[ "$JSON_MODE" -eq 0 && $# -eq 1 ]] || usage
|
|
360
333
|
echo "$USAGE_TEXT" ;;
|
|
334
|
+
threads)
|
|
335
|
+
thread_action="${2:-list}"
|
|
336
|
+
case "$thread_action" in
|
|
337
|
+
list)
|
|
338
|
+
[[ $# -eq 1 || ( $# -eq 2 && "$2" == "list" ) ]] || usage
|
|
339
|
+
if [[ -L "$THREADS" || ( -e "$THREADS" && ! -d "$THREADS" ) ]]; then
|
|
340
|
+
die 1 "unsafe thread store path"
|
|
341
|
+
fi
|
|
342
|
+
thread_names=()
|
|
343
|
+
if [[ -d "$THREADS" ]]; then
|
|
344
|
+
for thread_path in "$THREADS"/*.json; do
|
|
345
|
+
[[ -e "$thread_path" || -L "$thread_path" ]] || continue
|
|
346
|
+
thread_name="${thread_path##*/}"
|
|
347
|
+
thread_name="${thread_name%.json}"
|
|
348
|
+
omnilane_valid_thread_name "$thread_name" || die 1 "invalid thread state filename"
|
|
349
|
+
read_thread_state "$thread_path" "$thread_name" || die 1 "thread $thread_name state is not safely readable"
|
|
350
|
+
thread_names+=("$thread_name")
|
|
351
|
+
done
|
|
352
|
+
fi
|
|
353
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
354
|
+
printf '{"schema_version":1,"command":"threads","ok":true,"threads":['
|
|
355
|
+
thread_first=1
|
|
356
|
+
if [[ "${#thread_names[@]}" -gt 0 ]]; then
|
|
357
|
+
while IFS= read -r thread_name; do
|
|
358
|
+
read_thread_state "$THREADS/$thread_name.json" "$thread_name" \
|
|
359
|
+
|| die 1 "thread $thread_name state is not safely readable"
|
|
360
|
+
[[ "$thread_first" -eq 1 ]] || printf ','
|
|
361
|
+
thread_first=0
|
|
362
|
+
cat "$THREADS/$thread_name.json"
|
|
363
|
+
done < <(printf '%s\n' "${thread_names[@]}" | LC_ALL=C sort)
|
|
364
|
+
fi
|
|
365
|
+
printf ']}\n'
|
|
366
|
+
else
|
|
367
|
+
printf 'name\tvendor\tmodel\tturns\tupdated\tlast_job\n'
|
|
368
|
+
if [[ "${#thread_names[@]}" -gt 0 ]]; then
|
|
369
|
+
while IFS= read -r thread_name; do
|
|
370
|
+
read_thread_state "$THREADS/$thread_name.json" "$thread_name" \
|
|
371
|
+
|| die 1 "thread $thread_name state is not safely readable"
|
|
372
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
373
|
+
"$THREAD_STATE_NAME" "$THREAD_STATE_VENDOR" "$THREAD_STATE_MODEL" \
|
|
374
|
+
"$THREAD_STATE_TURNS" "$THREAD_STATE_UPDATED" "$THREAD_STATE_LAST_JOB_ID"
|
|
375
|
+
done < <(printf '%s\n' "${thread_names[@]}" | LC_ALL=C sort)
|
|
376
|
+
fi
|
|
377
|
+
fi
|
|
378
|
+
;;
|
|
379
|
+
show)
|
|
380
|
+
[[ $# -eq 3 ]] || usage
|
|
381
|
+
thread_name="$3"
|
|
382
|
+
omnilane_valid_thread_name "$thread_name" || die 2 "invalid thread name"
|
|
383
|
+
thread_path="$THREADS/$thread_name.json"
|
|
384
|
+
read_thread_state "$thread_path" "$thread_name" || die 1 "no safely readable thread '$thread_name'"
|
|
385
|
+
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
386
|
+
printf '{"schema_version":1,"command":"threads show","ok":true,"thread":'
|
|
387
|
+
cat "$thread_path"
|
|
388
|
+
printf '}\n'
|
|
389
|
+
else
|
|
390
|
+
cat "$thread_path"
|
|
391
|
+
fi
|
|
392
|
+
;;
|
|
393
|
+
rm)
|
|
394
|
+
[[ "$JSON_MODE" -eq 0 && $# -eq 3 ]] || usage
|
|
395
|
+
thread_name="$3"
|
|
396
|
+
omnilane_valid_thread_name "$thread_name" || die 2 "invalid thread name"
|
|
397
|
+
thread_path="$THREADS/$thread_name.json"
|
|
398
|
+
read_thread_state "$thread_path" "$thread_name" || die 1 "no safely readable thread '$thread_name'"
|
|
399
|
+
rm "$thread_path"
|
|
400
|
+
echo "removed thread $thread_name (vendor session preserved)"
|
|
401
|
+
;;
|
|
402
|
+
*) usage ;;
|
|
403
|
+
esac
|
|
404
|
+
;;
|
|
361
405
|
send)
|
|
362
406
|
[[ "$JSON_MODE" -eq 0 && $# -eq 3 ]] || usage
|
|
363
407
|
select_job "$2"
|