oh-my-opencode 4.9.0 → 4.9.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.
Files changed (37) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +1 -0
  2. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-branches.mjs +39 -0
  3. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-events.mjs +106 -0
  4. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-server.mjs +117 -0
  5. package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +716 -0
  6. package/dist/cli/doctor/checks/dependencies.d.ts +2 -2
  7. package/dist/cli/index.js +31742 -31476
  8. package/dist/cli-node/index.js +31742 -31476
  9. package/dist/config/schema/experimental.d.ts +1 -0
  10. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
  11. package/dist/index.js +3067 -1344
  12. package/dist/oh-my-opencode.schema.json +3 -0
  13. package/dist/shared/internal-initiator-marker.d.ts +7 -0
  14. package/dist/shared/live-server-route.d.ts +24 -0
  15. package/dist/shared/module-resolution-failure.d.ts +7 -0
  16. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +1 -0
  17. package/dist/testing/create-plugin-module.d.ts +4 -0
  18. package/package.json +12 -12
  19. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  20. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  23. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  25. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  27. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  29. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  33. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  35. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  36. package/packages/omo-codex/plugin/hooks/hooks.json +16 -16
  37. package/packages/omo-codex/plugin/package.json +1 -1
@@ -0,0 +1,716 @@
1
+ #!/usr/bin/env bash
2
+ # serve-wake-split-probe.sh
3
+ # Serve-topology wake runner-split QA harness.
4
+ #
5
+ # Proves whether omo's plugin-origin promptAsync (parent-wake bg notifications)
6
+ # forks a second concurrent LLM runner in opencode serve topology (REPRODUCED)
7
+ # or routes correctly through the live listener (FIXED).
8
+ #
9
+ # Two assertion modes:
10
+ # --expect reproduced exit 0 if stops>1 OR children>1 OR mechanism arm true
11
+ # --expect fixed exit 0 if children==1 AND stops==1
12
+ #
13
+ # Usage:
14
+ # serve-wake-split-probe.sh [--expect reproduced|fixed] [--evidence-dir DIR]
15
+ # [--self-test] [--help]
16
+ #
17
+ # Env:
18
+ # OMO_SANDBOX_OMO_CONFIG JSON string; when set, deep-merged over the base
19
+ # agent overrides (env keys win) and written to
20
+ # $XDG_CONFIG_HOME/opencode/oh-my-openagent.json
21
+ # before the server starts (flag-disabled control).
22
+ # FAKE_OPENAI_PORT Force the fake-LLM to bind a specific port
23
+ # (default: random). Port 1 triggers a startup
24
+ # failure test used by the self-test failure path.
25
+ #
26
+ # Exit codes:
27
+ # 0 expectation met (or --self-test OK)
28
+ # 1 expectation NOT met, or internal harness error
29
+ # 2 usage / bad arguments
30
+
31
+ set -uo pipefail
32
+
33
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+ . "$SCRIPT_DIR/lib/common.sh"
35
+
36
+ # ---- defaults ----------------------------------------------------------------
37
+ EXPECT_MODE="" # reproduced | fixed
38
+ EVIDENCE_DIR=""
39
+ SELF_TEST=0
40
+ FAKE_SERVER_PID=""
41
+ FAKE_SERVER_PORT=""
42
+ FAKE_LLM_LOG="" # set after evidence dir is known
43
+
44
+ # ---- argument parsing --------------------------------------------------------
45
+ while [ $# -gt 0 ]; do
46
+ case "$1" in
47
+ --expect)
48
+ EXPECT_MODE="$2"
49
+ shift 2
50
+ ;;
51
+ --evidence-dir)
52
+ EVIDENCE_DIR="$2"
53
+ shift 2
54
+ ;;
55
+ --self-test)
56
+ SELF_TEST=1
57
+ shift
58
+ ;;
59
+ -h|--help)
60
+ sed -n '2,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
61
+ exit 0
62
+ ;;
63
+ *)
64
+ printf 'unknown option: %s\n' "$1" >&2
65
+ exit 2
66
+ ;;
67
+ esac
68
+ done
69
+
70
+ if [ "$SELF_TEST" -eq 0 ] && [ -z "$EXPECT_MODE" ]; then
71
+ EXPECT_MODE="reproduced"
72
+ fi
73
+
74
+ if [ -n "$EXPECT_MODE" ] && [ "$EXPECT_MODE" != "reproduced" ] && [ "$EXPECT_MODE" != "fixed" ]; then
75
+ printf 'error: --expect must be reproduced or fixed\n' >&2
76
+ exit 2
77
+ fi
78
+
79
+ # ---- helpers -----------------------------------------------------------------
80
+
81
+ swsp_log() { printf '%s\n' "$*" >&2; }
82
+ swsp_info() { printf '[swsp] %s\n' "$*" >&2; }
83
+
84
+ # Start the fake-LLM server; sets FAKE_SERVER_PID + FAKE_SERVER_PORT.
85
+ swsp_start_fake_llm() {
86
+ local log_file="$1"
87
+ local port_file
88
+ port_file="$(mktemp -t swsp-port.XXXXXX)"
89
+ OQA_TMPDIRS+=("$port_file")
90
+
91
+ FAKE_LLM_LOG="$log_file" FAKE_OPENAI_PORT="${FAKE_OPENAI_PORT:-0}" \
92
+ bun run --bun "$SCRIPT_DIR/lib/fake-openai-server.mjs" >"$port_file.stdout" 2>&1 &
93
+ FAKE_SERVER_PID=$!
94
+ disown "$FAKE_SERVER_PID" 2>/dev/null || true
95
+
96
+ # Poll for the port line (fake-openai listening on <port>)
97
+ local deadline
98
+ deadline=$(( $(date +%s) + 10 ))
99
+ while [ "$(date +%s)" -lt "$deadline" ]; do
100
+ if grep -q "^fake-openai listening on " "$port_file.stdout" 2>/dev/null; then
101
+ FAKE_SERVER_PORT="$(grep "^fake-openai listening on " "$port_file.stdout" | head -1 | awk '{print $NF}')"
102
+ break
103
+ fi
104
+ if ! kill -0 "$FAKE_SERVER_PID" 2>/dev/null; then
105
+ swsp_log "FAIL: fake-openai server process died immediately"
106
+ cat "$port_file.stdout" >&2 2>/dev/null || true
107
+ return 1
108
+ fi
109
+ sleep 0.3
110
+ done
111
+ OQA_TMPDIRS+=("$port_file.stdout")
112
+
113
+ if [ -z "$FAKE_SERVER_PORT" ]; then
114
+ swsp_log "FAIL: fake-openai server did not report port within 10s"
115
+ cat "$port_file.stdout" >&2 2>/dev/null || true
116
+ kill "$FAKE_SERVER_PID" 2>/dev/null || true
117
+ return 1
118
+ fi
119
+
120
+ # Verify it's up
121
+ local hdeadline=0
122
+ hdeadline=$(( $(date +%s) + 5 ))
123
+ while [ "$(date +%s)" -lt "$hdeadline" ]; do
124
+ if curl -sf "http://127.0.0.1:${FAKE_SERVER_PORT}/health" >/dev/null 2>&1; then
125
+ swsp_info "fake-openai listening on port $FAKE_SERVER_PORT"
126
+ return 0
127
+ fi
128
+ sleep 0.2
129
+ done
130
+ swsp_log "FAIL: fake-openai /health did not respond within 5s on port $FAKE_SERVER_PORT"
131
+ kill "$FAKE_SERVER_PID" 2>/dev/null || true
132
+ return 1
133
+ }
134
+
135
+ swsp_stop_fake_llm() {
136
+ if [ -n "$FAKE_SERVER_PID" ]; then
137
+ kill "$FAKE_SERVER_PID" 2>/dev/null || true
138
+ sleep 0.3
139
+ kill -0 "$FAKE_SERVER_PID" 2>/dev/null && kill -9 "$FAKE_SERVER_PID" 2>/dev/null || true
140
+ FAKE_SERVER_PID=""
141
+ fi
142
+ }
143
+
144
+ # Write the sandbox omo config: base agent overrides (explore/librarian -> the
145
+ # fake provider, required for child model resolution) deep-merged with
146
+ # OMO_SANDBOX_OMO_CONFIG when set (jq '.[0] * .[1]'; env keys win).
147
+ # Args: sandbox_config_dir
148
+ swsp_write_omo_config() {
149
+ local cfg_dir="$1"
150
+ local omo_cfg="$cfg_dir/opencode/oh-my-openagent.json"
151
+ local base='{"agents":{"explore":{"model":"openai/gpt-fake"},"librarian":{"model":"openai/gpt-fake"}}}'
152
+
153
+ mkdir -p "$cfg_dir/opencode"
154
+ if [ -n "${OMO_SANDBOX_OMO_CONFIG:-}" ]; then
155
+ if ! printf '%s\n%s\n' "$base" "$OMO_SANDBOX_OMO_CONFIG" | jq -s '.[0] * .[1]' >"$omo_cfg" 2>/dev/null; then
156
+ swsp_log "FAIL: OMO_SANDBOX_OMO_CONFIG is not valid JSON"
157
+ return 1
158
+ fi
159
+ swsp_info "wrote merged OMO_SANDBOX_OMO_CONFIG to $omo_cfg"
160
+ else
161
+ printf '%s\n' "$base" >"$omo_cfg"
162
+ swsp_info "wrote agent overrides to $omo_cfg"
163
+ fi
164
+ }
165
+
166
+ # Write the sandbox opencode.jsonc with the fake provider + local plugin.
167
+ # Args: sandbox_config_dir fake_port
168
+ swsp_write_opencode_config() {
169
+ local cfg_dir="$1"
170
+ local fake_port="$2"
171
+ local repo_root
172
+ repo_root="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
173
+
174
+ mkdir -p "$cfg_dir/opencode"
175
+ cat >"$cfg_dir/opencode/opencode.jsonc" <<JSONC
176
+ {
177
+ "plugin": ["file://${repo_root}/packages/omo-opencode/src/index.ts"],
178
+ "model": "openai/gpt-fake",
179
+ "provider": {
180
+ "openai": {
181
+ "options": {
182
+ "apiKey": "fake-key",
183
+ "baseURL": "http://127.0.0.1:${fake_port}/v1",
184
+ "timeout": 30000
185
+ },
186
+ "models": {
187
+ "gpt-fake": {
188
+ "tool_call": true,
189
+ "limit": {
190
+ "context": 200000,
191
+ "output": 8192
192
+ }
193
+ }
194
+ }
195
+ }
196
+ },
197
+ "permission": {
198
+ "bash": "allow",
199
+ "call_omo_agent": "allow"
200
+ }
201
+ }
202
+ JSONC
203
+ swsp_info "opencode.jsonc written to $cfg_dir/opencode/opencode.jsonc"
204
+ }
205
+
206
+ # Poll the sandbox DB for children/stops on a message matching a LIKE pattern.
207
+ # Args: db_path like_pattern timeout_s
208
+ # Outputs: "<children> <stops>" on stdout
209
+ swsp_poll_db_metrics() {
210
+ local db="$1"
211
+ local like_pat="$2"
212
+ local timeout_s="${3:-90}"
213
+ local deadline
214
+ local metrics_query="
215
+ WITH target AS (
216
+ SELECT m.id AS user_id, m.session_id
217
+ FROM message m
218
+ JOIN part p ON p.message_id = m.id
219
+ WHERE json_extract(m.data, '\$.role') = 'user'
220
+ AND json_extract(p.data, '\$.type') = 'text'
221
+ AND json_extract(p.data, '\$.text') LIKE '${like_pat}'
222
+ ),
223
+ counts AS (
224
+ SELECT
225
+ count(a.id) AS children,
226
+ sum(CASE WHEN json_extract(a.data, '\$.finish') = 'stop' THEN 1 ELSE 0 END) AS stops
227
+ FROM target t
228
+ LEFT JOIN message a
229
+ ON a.session_id = t.session_id
230
+ AND json_extract(a.data, '\$.parentID') = t.user_id
231
+ GROUP BY t.user_id
232
+ )
233
+ SELECT printf('%d %d',
234
+ coalesce((SELECT max(children) FROM counts), 0),
235
+ coalesce((SELECT max(stops) FROM counts), 0)
236
+ );
237
+ "
238
+
239
+ deadline=$(( $(date +%s) + timeout_s ))
240
+ while [ "$(date +%s)" -lt "$deadline" ]; do
241
+ if [ ! -f "$db" ]; then
242
+ sleep 0.5
243
+ continue
244
+ fi
245
+ local result
246
+ result="$(sqlite3 "$db" "$metrics_query" 2>/dev/null)" || true
247
+
248
+ local children stops
249
+ children="$(printf '%s' "$result" | awk '{print $1}')"
250
+ stops="$(printf '%s' "$result" | awk '{print $2}')"
251
+
252
+ # Return once we have at least 1 stop (parent session finished)
253
+ if [ -n "$stops" ] && [ "${stops:-0}" -ge 1 ] 2>/dev/null; then
254
+ printf '%s %s' "$children" "$stops"
255
+ return 0
256
+ fi
257
+ sleep 0.5
258
+ done
259
+
260
+ # Return whatever we have on timeout
261
+ local result
262
+ result="$(sqlite3 "$db" "$metrics_query" 2>/dev/null)" || true
263
+ printf '%s' "${result:-0 0}"
264
+ }
265
+
266
+ # Wait until a session is no longer in the server's active status map.
267
+ # Args: server_url pass session_id timeout_s
268
+ swsp_wait_session_idle() {
269
+ local url="$1" pass="$2" ses_id="$3" timeout_s="${4:-120}"
270
+ local deadline
271
+ deadline=$(( $(date +%s) + timeout_s ))
272
+ while [ "$(date +%s)" -lt "$deadline" ]; do
273
+ local status_json
274
+ status_json="$(curl -sf -u "opencode:${pass}" "${url}/session/status" 2>/dev/null)" || true
275
+ if [ -z "$status_json" ] || ! printf '%s' "$status_json" | grep -q "$ses_id" 2>/dev/null; then
276
+ return 0
277
+ fi
278
+ sleep 0.5
279
+ done
280
+ swsp_log "WARNING: session $ses_id did not go idle within ${timeout_s}s; proceeding with current DB state"
281
+ return 0
282
+ }
283
+
284
+ # Count plugin_inits from the omo log since a byte offset, filtered to sandbox dir.
285
+ # Args: log_offset sandbox_dir
286
+ swsp_count_plugin_inits() {
287
+ local offset="$1"
288
+ local sandbox_dir="$2"
289
+ local log_path="${TMPDIR:-/tmp}/oh-my-opencode.log"
290
+ if [ ! -f "$log_path" ]; then
291
+ printf '0'
292
+ return 0
293
+ fi
294
+ # tail from byte offset
295
+ tail -c "+$((offset + 1))" "$log_path" 2>/dev/null \
296
+ | grep "ENTRY - plugin loading" \
297
+ | grep -c "$sandbox_dir" 2>/dev/null \
298
+ || printf '0'
299
+ }
300
+
301
+ # Detect WAKE_DISPATCHED_DURING_PARENT_TURN:
302
+ # true iff the omo log (since offset) contains a [prompt-async-gate] promptAsync dispatching
303
+ # line with source containing "parent-wake", AND that line's timestamp is within the
304
+ # parent-hold window (between branch=parent-hold line and next non-wake completion).
305
+ # Args: log_offset fake_llm_log sandbox_dir
306
+ swsp_detect_wake_during_parent() {
307
+ local offset="$1"
308
+ local fake_log="$2"
309
+ local sandbox_dir="$3"
310
+ local omo_log="${TMPDIR:-/tmp}/oh-my-opencode.log"
311
+
312
+ # Find parent-hold timestamp from fake-llm.log
313
+ local hold_ts
314
+ hold_ts="$(grep "branch=parent-hold" "$fake_log" 2>/dev/null | head -1 | grep -o '\[.*\]' | tr -d '[]')" || true
315
+
316
+ if [ -z "$hold_ts" ]; then
317
+ # parent-hold never fired — cannot determine
318
+ printf 'false'
319
+ return 0
320
+ fi
321
+
322
+ # Check for gate dispatch log line with parent-wake source since offset
323
+ local dispatch_line
324
+ dispatch_line="$(tail -c "+$((offset + 1))" "$omo_log" 2>/dev/null \
325
+ | grep "promptAsync dispatching" \
326
+ | grep -i "parent-wake\|background-agent-parent-wake" \
327
+ | head -1)" || true
328
+
329
+ if [ -z "$dispatch_line" ]; then
330
+ printf 'false'
331
+ return 0
332
+ fi
333
+
334
+ # Extract timestamp from dispatch line (ISO 8601 in brackets or as prefix)
335
+ local dispatch_ts
336
+ dispatch_ts="$(printf '%s' "$dispatch_line" | grep -o '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]' | head -1)" || true
337
+
338
+ if [ -z "$dispatch_ts" ]; then
339
+ # Can't compare timestamps — fall back to presence check
340
+ printf 'true'
341
+ return 0
342
+ fi
343
+
344
+ # Simple lexicographic timestamp compare (ISO 8601 sorts correctly)
345
+ # The hold_ts is the start of the hold window; if dispatch happened after it, signal is true
346
+ if [ "$dispatch_ts" \> "$hold_ts" ] || [ "$dispatch_ts" = "$hold_ts" ]; then
347
+ printf 'true'
348
+ else
349
+ printf 'false'
350
+ fi
351
+ }
352
+
353
+ # Verify branch-count guard: all required branches fired.
354
+ # Returns 0 if OK, 1 if any required branch missing (also sets RESULT=HARNESS_ERROR).
355
+ swsp_check_branch_counts() {
356
+ local fake_log="$1"
357
+ local ptc pc cc wc
358
+ ptc="$(grep -c "branch=parent-tool-call" "$fake_log" 2>/dev/null | tr -d '[:space:]')"; ptc="${ptc:-0}"
359
+ pc="$(grep -c "branch=parent-hold" "$fake_log" 2>/dev/null | tr -d '[:space:]')"; pc="${pc:-0}"
360
+ cc="$(grep -c "branch=child" "$fake_log" 2>/dev/null | tr -d '[:space:]')"; cc="${cc:-0}"
361
+ wc="$(grep -c "branch=wake" "$fake_log" 2>/dev/null | tr -d '[:space:]')"; wc="${wc:-0}"
362
+ ptc="${ptc%%[!0-9]*}"; pc="${pc%%[!0-9]*}"; cc="${cc%%[!0-9]*}"; wc="${wc%%[!0-9]*}"
363
+ ptc="${ptc:-0}"; pc="${pc:-0}"; cc="${cc:-0}"; wc="${wc:-0}"
364
+
365
+ swsp_info "branch counts: parent-tool-call=$ptc parent-hold=$pc child=$cc wake=$wc"
366
+
367
+ if [ "$ptc" -lt 1 ] || [ "$pc" -lt 1 ] || [ "$cc" -lt 1 ] || [ "$wc" -lt 1 ]; then
368
+ printf 'RESULT=HARNESS_ERROR branch_counts parent-tool-call=%s parent-hold=%s child=%s wake=%s\n' \
369
+ "$ptc" "$pc" "$cc" "$wc"
370
+ return 1
371
+ fi
372
+ return 0
373
+ }
374
+
375
+ # ---- self-test ---------------------------------------------------------------
376
+ swsp_self_test() {
377
+ swsp_info "running self-test..."
378
+ local fails=0
379
+
380
+ # Deps
381
+ oqa_require opencode sqlite3 curl jq bun || { swsp_log "FAIL: missing dependencies"; fails=$((fails+1)); }
382
+
383
+ # Start fake-LLM
384
+ local st_log
385
+ st_log="$(mktemp -t swsp-st-llm.XXXXXX)"
386
+ OQA_TMPDIRS+=("$st_log")
387
+
388
+ if ! swsp_start_fake_llm "$st_log"; then
389
+ swsp_log "FAIL: fake-LLM did not start (port=${FAKE_OPENAI_PORT:-dynamic})"
390
+ fails=$((fails+1))
391
+ else
392
+ swsp_info "fake-LLM started on port $FAKE_SERVER_PORT"
393
+
394
+ # Health check
395
+ if curl -sf "http://127.0.0.1:${FAKE_SERVER_PORT}/health" >/dev/null 2>&1; then
396
+ swsp_info "PASS: fake-LLM /health 200"
397
+ else
398
+ swsp_log "FAIL: fake-LLM /health did not return 200"
399
+ fails=$((fails+1))
400
+ fi
401
+ fi
402
+
403
+ # Sandbox + opencode serve
404
+ if ! oqa_start_server; then
405
+ swsp_log "FAIL: opencode serve did not start"
406
+ swsp_stop_fake_llm
407
+ fails=$((fails+1))
408
+ else
409
+ swsp_info "PASS: opencode serve started at $OQA_SERVER_URL"
410
+
411
+ # /global/health check
412
+ local health_code
413
+ health_code="$(curl -so /dev/null -w "%{http_code}" -u "opencode:${OQA_SERVER_PASS}" \
414
+ "${OQA_SERVER_URL}/global/health" 2>/dev/null)" || true
415
+ if [ "$health_code" = "200" ]; then
416
+ swsp_info "PASS: /global/health 200"
417
+ else
418
+ swsp_log "FAIL: /global/health returned $health_code"
419
+ fails=$((fails+1))
420
+ fi
421
+
422
+ # OMO_SANDBOX_OMO_CONFIG env contract assertion: merge keeps base overrides
423
+ local omo_cfg_path="$XDG_CONFIG_HOME/opencode/oh-my-openagent.json"
424
+ OMO_SANDBOX_OMO_CONFIG='{"_probe":true}' swsp_write_omo_config "$XDG_CONFIG_HOME"
425
+ local probe_val explore_model
426
+ probe_val="$(jq -r '._probe' "$omo_cfg_path" 2>/dev/null)"
427
+ explore_model="$(jq -r '.agents.explore.model' "$omo_cfg_path" 2>/dev/null)"
428
+ if [ "$probe_val" = "true" ] && [ "$explore_model" = "openai/gpt-fake" ]; then
429
+ swsp_info "PASS: OMO_SANDBOX_OMO_CONFIG merge assertion (env key + base overrides both present)"
430
+ else
431
+ swsp_log "FAIL: omo config merge wrong: _probe='$probe_val' explore_model='$explore_model'"
432
+ fails=$((fails+1))
433
+ fi
434
+ fi
435
+
436
+ swsp_stop_fake_llm
437
+
438
+ # Orphan check
439
+ local orphan_count
440
+ orphan_count="$(pgrep -f "fake-openai-server" 2>/dev/null | wc -l | tr -d ' ')" || orphan_count=0
441
+ if [ "${orphan_count:-0}" -eq 0 ]; then
442
+ swsp_info "PASS: no orphan fake-openai-server processes"
443
+ else
444
+ swsp_log "FAIL: $orphan_count orphan fake-openai-server process(es) remain"
445
+ pkill -f "fake-openai-server" 2>/dev/null || true
446
+ fails=$((fails+1))
447
+ fi
448
+
449
+ if [ "$fails" -eq 0 ]; then
450
+ printf 'SELF-TEST OK\n'
451
+ return 0
452
+ fi
453
+ printf 'SELF-TEST FAILED (%d failure(s))\n' "$fails" >&2
454
+ return 1
455
+ }
456
+
457
+ # ---- main probe run ----------------------------------------------------------
458
+ swsp_run_probe() {
459
+ local evidence_dir="${EVIDENCE_DIR:-$(mktemp -d -t swsp-evidence.XXXXXX)}"
460
+ mkdir -p "$evidence_dir"
461
+ OQA_TMPDIRS+=("$evidence_dir") 2>/dev/null || true # only auto-clean if we created it
462
+
463
+ # Override: if caller gave --evidence-dir, don't delete it
464
+ if [ -n "$EVIDENCE_DIR" ]; then
465
+ # Remove from cleanup list (last element we added)
466
+ unset 'OQA_TMPDIRS[${#OQA_TMPDIRS[@]}-1]' 2>/dev/null || true
467
+ fi
468
+
469
+ swsp_info "evidence dir: $evidence_dir"
470
+
471
+ local fake_llm_log="$evidence_dir/fake-llm.log"
472
+ local harness_log="$evidence_dir/harness.log"
473
+ local serve_stdout="$evidence_dir/opencode-serve.stdout"
474
+ local serve_stderr="$evidence_dir/opencode-serve.stderr"
475
+
476
+ # Step 1: Record real-DB session count (read-only)
477
+ local real_db_path real_db_count_before
478
+ real_db_path="$(opencode db path 2>/dev/null | head -1 || echo "")"
479
+ if [ -n "$real_db_path" ] && [ -f "$real_db_path" ]; then
480
+ real_db_count_before="$(sqlite3 "$real_db_path" 'SELECT count(*) FROM session' 2>/dev/null || echo "0")"
481
+ else
482
+ real_db_count_before="0"
483
+ real_db_path="(not found)"
484
+ fi
485
+ swsp_info "real DB session count before: $real_db_count_before"
486
+ printf 'real_db=%s before=%s\n' "$real_db_path" "$real_db_count_before" >"$evidence_dir/isolation-receipt.txt"
487
+
488
+ # Capture omo log byte offset
489
+ local omo_log="${TMPDIR:-/tmp}/oh-my-opencode.log"
490
+ local omo_log_offset
491
+ if [ -f "$omo_log" ]; then
492
+ omo_log_offset="$(wc -c <"$omo_log" 2>/dev/null | tr -d ' ')" || omo_log_offset=0
493
+ else
494
+ omo_log_offset=0
495
+ fi
496
+
497
+ # Step 2: Start fake-openai server
498
+ swsp_info "starting fake-openai server..."
499
+ if ! swsp_start_fake_llm "$fake_llm_log"; then
500
+ swsp_log "HARNESS_ERROR: fake-openai server failed to start"
501
+ printf 'RESULT=HARNESS_ERROR fake_llm_start_failed\n' | tee -a "$harness_log"
502
+ return 1
503
+ fi
504
+ swsp_info "fake-openai on port $FAKE_SERVER_PORT"
505
+ printf 'fake_llm_port=%s\n' "$FAKE_SERVER_PORT" >>"$evidence_dir/isolation-receipt.txt"
506
+
507
+ # Step 3: Create isolated sandbox and write opencode.jsonc
508
+ # oqa_mk_isolated_xdg sets XDG_CONFIG_HOME, XDG_DATA_HOME, etc.
509
+ oqa_mk_isolated_xdg
510
+ swsp_info "sandbox: $OQA_XDG_ROOT"
511
+
512
+ swsp_write_omo_config "$XDG_CONFIG_HOME"
513
+
514
+ swsp_write_opencode_config "$XDG_CONFIG_HOME" "$FAKE_SERVER_PORT"
515
+ local sandbox_db="$XDG_DATA_HOME/opencode/opencode.db"
516
+
517
+ # Step 4: Start opencode serve (using oqa_start_server internals but with our config)
518
+ # oqa_start_server includes another oqa_mk_isolated_xdg call which would reset XDG vars.
519
+ # Instead, start server directly using the already-set XDG vars.
520
+ swsp_info "starting opencode serve..."
521
+ local port pass
522
+ port="$(oqa_free_port)"
523
+ pass="oqa-${RANDOM}${RANDOM}"
524
+
525
+ OPENCODE_SERVER_PASSWORD="$pass" opencode serve --port "$port" --hostname 127.0.0.1 \
526
+ >"$serve_stdout" 2>"$serve_stderr" &
527
+ OQA_SERVER_PID=$!
528
+ disown "$OQA_SERVER_PID" 2>/dev/null || true
529
+ export OQA_SERVER_PORT="$port"
530
+ export OQA_SERVER_PASS="$pass"
531
+ export OQA_SERVER_URL="http://127.0.0.1:$port"
532
+
533
+ if ! oqa_wait_http "$OQA_SERVER_URL/global/health" "opencode:$pass" 30; then
534
+ swsp_log "HARNESS_ERROR: opencode serve failed to start"
535
+ cat "$serve_stderr" >&2 2>/dev/null || true
536
+ printf 'RESULT=HARNESS_ERROR opencode_serve_start_failed\n' | tee -a "$harness_log"
537
+ swsp_stop_fake_llm
538
+ return 1
539
+ fi
540
+ swsp_info "opencode serve ready at $OQA_SERVER_URL"
541
+
542
+ # Encode the working directory for use in URL
543
+ local enc_dir
544
+ enc_dir="$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=""))' "$OQA_PROJ" 2>/dev/null \
545
+ || printf '%s' "$OQA_PROJ" | sed 's|/|%2F|g')"
546
+
547
+ # Step 5: Create a session
548
+ swsp_info "creating session..."
549
+ local ses_response ses_id
550
+ ses_response="$(curl -sS -u "opencode:${pass}" \
551
+ -X POST "${OQA_SERVER_URL}/session?directory=${enc_dir}" \
552
+ -H 'content-type: application/json' \
553
+ -d '{"title":"wake split probe"}' 2>/dev/null)" || ses_response=""
554
+
555
+ ses_id="$(printf '%s' "$ses_response" | jq -r '.id // .sessionID // empty' 2>/dev/null)" || ses_id=""
556
+
557
+ if [ -z "$ses_id" ]; then
558
+ swsp_log "HARNESS_ERROR: could not create session (response: $ses_response)"
559
+ printf 'RESULT=HARNESS_ERROR session_create_failed\n' | tee -a "$harness_log"
560
+ swsp_stop_fake_llm
561
+ return 1
562
+ fi
563
+ swsp_info "session: $ses_id"
564
+ printf 'session_id=%s\n' "$ses_id" >>"$evidence_dir/isolation-receipt.txt"
565
+
566
+ # Step 6: Send the split probe prompt
567
+ swsp_info "sending split probe prompt..."
568
+ local prompt_response
569
+ prompt_response="$(curl -sS -u "opencode:${pass}" \
570
+ -X POST "${OQA_SERVER_URL}/session/${ses_id}/prompt_async?directory=${enc_dir}" \
571
+ -H 'content-type: application/json' \
572
+ -d '{"parts":[{"type":"text","text":"Run the split probe: call call_omo_agent exactly once as instructed, then run the bash hold command."}]}' \
573
+ 2>/dev/null)" || prompt_response=""
574
+ swsp_info "prompt_async response: $prompt_response"
575
+
576
+ # Step 7: Poll sandbox DB for children/stops; also wait for session idle
577
+ swsp_info "polling DB for wake-split metrics (up to 120s)..."
578
+ local metrics
579
+ metrics="$(swsp_poll_db_metrics "$sandbox_db" '%[BACKGROUND TASK%' 120)"
580
+ local children stops
581
+ children="$(printf '%s' "$metrics" | awk '{print $1}')"
582
+ stops="$(printf '%s' "$metrics" | awk '{print $2}')"
583
+ children="${children:-0}"
584
+ stops="${stops:-0}"
585
+ swsp_info "DB metrics: children=$children stops=$stops"
586
+
587
+ # Wait for parent session to go idle
588
+ swsp_info "waiting for parent session to go idle..."
589
+ swsp_wait_session_idle "$OQA_SERVER_URL" "$pass" "$ses_id" 60
590
+
591
+ # Re-read metrics after idle
592
+ metrics="$(swsp_poll_db_metrics "$sandbox_db" '%[BACKGROUND TASK%' 10)"
593
+ children="$(printf '%s' "$metrics" | awk '{print $1}')"
594
+ stops="$(printf '%s' "$metrics" | awk '{print $2}')"
595
+ children="${children:-0}"
596
+ stops="${stops:-0}"
597
+ swsp_info "final DB metrics: children=$children stops=$stops"
598
+ printf 'children=%s stops=%s\n' "$children" "$stops" >"$evidence_dir/marker-metrics.txt"
599
+
600
+ # Step 8: Plugin-init count
601
+ local plugin_inits
602
+ plugin_inits="$(swsp_count_plugin_inits "$omo_log_offset" "$OQA_PROJ")"
603
+ plugin_inits="${plugin_inits:-0}"
604
+ swsp_info "plugin_inits: $plugin_inits"
605
+ printf '%s\n' "$plugin_inits" >"$evidence_dir/plugin-init-count.txt"
606
+
607
+ # Step 9: Route provenance
608
+ local route_prov=""
609
+ if [ -f "$omo_log" ]; then
610
+ route_prov="$(tail -c "+$((omo_log_offset + 1))" "$omo_log" 2>/dev/null \
611
+ | grep -E "live-server-route" || true)"
612
+ fi
613
+ printf '%s\n' "$route_prov" >"$evidence_dir/route-provenance.log"
614
+ swsp_info "route-provenance lines: $(printf '%s' "$route_prov" | wc -l | tr -d ' ')"
615
+
616
+ # WAKE_DISPATCHED_DURING_PARENT_TURN mechanism signal
617
+ local wake_during_parent
618
+ wake_during_parent="$(swsp_detect_wake_during_parent "$omo_log_offset" "$fake_llm_log" "$OQA_PROJ")"
619
+ swsp_info "WAKE_DISPATCHED_DURING_PARENT_TURN=$wake_during_parent"
620
+
621
+ # Step 10: Branch-count guard
622
+ if ! swsp_check_branch_counts "$fake_llm_log" >&2; then
623
+ # Branch counts not met — HARNESS_ERROR
624
+ local ptc pc cc wc
625
+ ptc="$(grep -c "branch=parent-tool-call" "$fake_llm_log" 2>/dev/null || printf '0')"
626
+ pc="$(grep -c "branch=parent-hold" "$fake_llm_log" 2>/dev/null || printf '0')"
627
+ cc="$(grep -c "branch=child" "$fake_llm_log" 2>/dev/null || printf '0')"
628
+ wc="$(grep -c "branch=wake" "$fake_llm_log" 2>/dev/null || printf '0')"
629
+ local verdict_line
630
+ verdict_line="RESULT=HARNESS_ERROR children=${children} stops=${stops} plugin_inits=${plugin_inits} WAKE_DISPATCHED_DURING_PARENT_TURN=${wake_during_parent} branch_counts=parent-tool-call:${ptc},parent-hold:${pc},child:${cc},wake:${wc}"
631
+ printf '%s\n' "$verdict_line" | tee -a "$harness_log"
632
+ swsp_stop_fake_llm
633
+ return 1
634
+ fi
635
+
636
+ # Step 11: Determine verdict
637
+ local result="INCONCLUSIVE"
638
+ local exit_code=1
639
+
640
+ # Arm 1: SQLite evidence (stops>1 or children>1)
641
+ if [ "${stops:-0}" -gt 1 ] || [ "${children:-0}" -gt 1 ] 2>/dev/null; then
642
+ result="REPRODUCED"
643
+ fi
644
+
645
+ # Arm 2: Mechanism signal (wake dispatched during parent turn + in-process path)
646
+ # in-process path: no live-server-route dispatch line for this wake
647
+ local has_live_dispatch=false
648
+ if printf '%s' "$route_prov" | grep -q "dispatch via live listener" 2>/dev/null; then
649
+ has_live_dispatch=true
650
+ fi
651
+ if [ "$wake_during_parent" = "true" ] && [ "$has_live_dispatch" = "false" ]; then
652
+ result="REPRODUCED"
653
+ fi
654
+
655
+ # FIXED: exactly 1 child and 1 stop, and neither REPRODUCED arm held
656
+ if [ "$result" = "INCONCLUSIVE" ] && [ "${children:-0}" -eq 1 ] && [ "${stops:-0}" -eq 1 ] 2>/dev/null; then
657
+ result="FIXED"
658
+ fi
659
+
660
+ local verdict_line
661
+ verdict_line="RESULT=${result} children=${children} stops=${stops} plugin_inits=${plugin_inits} WAKE_DISPATCHED_DURING_PARENT_TURN=${wake_during_parent}"
662
+ printf '%s\n' "$verdict_line" | tee -a "$harness_log"
663
+
664
+ # Determine exit code based on expected mode
665
+ if [ -n "$EXPECT_MODE" ]; then
666
+ if [ "$EXPECT_MODE" = "reproduced" ] && [ "$result" = "REPRODUCED" ]; then
667
+ exit_code=0
668
+ elif [ "$EXPECT_MODE" = "fixed" ] && [ "$result" = "FIXED" ]; then
669
+ exit_code=0
670
+ else
671
+ exit_code=1
672
+ fi
673
+ else
674
+ exit_code=0 # no expectation: just report
675
+ fi
676
+
677
+ # Step 12: Isolation receipt — verify real DB count unchanged
678
+ local real_db_count_after=""
679
+ if [ -n "$real_db_path" ] && [ "$real_db_path" != "(not found)" ] && [ -f "$real_db_path" ]; then
680
+ real_db_count_after="$(sqlite3 "$real_db_path" 'SELECT count(*) FROM session' 2>/dev/null || echo "0")"
681
+ else
682
+ real_db_count_after="$real_db_count_before"
683
+ fi
684
+ printf 'after=%s unchanged=%s\n' \
685
+ "$real_db_count_after" \
686
+ "$([ "$real_db_count_after" = "$real_db_count_before" ] && echo yes || echo NO)" \
687
+ >>"$evidence_dir/isolation-receipt.txt"
688
+ swsp_info "isolation: real DB before=$real_db_count_before after=$real_db_count_after"
689
+
690
+ # Preserve the sandbox DB for post-hoc inspection before the trap removes it
691
+ sqlite3 "$sandbox_db" ".backup '$evidence_dir/sandbox-opencode.db'" 2>/dev/null || true
692
+
693
+ # Cleanup receipt
694
+ swsp_stop_fake_llm
695
+ printf 'fake_llm=stopped opencode_serve=stopping\n' >"$evidence_dir/cleanup-receipt.txt"
696
+
697
+ # Orphan check
698
+ local orphan_count
699
+ orphan_count="$(pgrep -f "fake-openai-server" 2>/dev/null | wc -l | tr -d ' ')" || orphan_count=0
700
+ if [ "${orphan_count:-0}" -gt 0 ]; then
701
+ swsp_log "WARNING: $orphan_count orphan fake-openai-server process(es); killing"
702
+ pkill -f "fake-openai-server" 2>/dev/null || true
703
+ fi
704
+ printf 'orphan_fake_llm=%s\n' "${orphan_count:-0}" >>"$evidence_dir/cleanup-receipt.txt"
705
+
706
+ return "$exit_code"
707
+ }
708
+
709
+ # ---- dispatch ----------------------------------------------------------------
710
+ if [ "$SELF_TEST" -eq 1 ]; then
711
+ swsp_self_test
712
+ exit $?
713
+ fi
714
+
715
+ swsp_run_probe
716
+ exit $?