omnilane 0.42.7 → 0.42.8

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "One routing table, native-first or CLI delegation, supervised completion workflows.",
9
- "version": "0.42.7"
9
+ "version": "0.42.8"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "omnilane",
14
14
  "source": "./",
15
15
  "description": "Route each subtask to the model that fits, then delegate through a compatible caller-owned native agent or vendor CLI.",
16
- "version": "0.42.7",
16
+ "version": "0.42.8",
17
17
  "category": "development",
18
18
  "keywords": [
19
19
  "routing",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.42.7",
3
+ "version": "0.42.8",
4
4
  "description": "One routing table, every harness: classify subtasks into lanes and delegate through compatible caller-owned native agents or vendor CLIs with exact-AA downward policy and supervised jobs.",
5
5
  "author": {
6
6
  "name": "Seraphim0916"
package/CHANGELOG.md CHANGED
@@ -6,6 +6,48 @@ semantic version tags.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.42.8] - 2026-09-12
10
+
11
+ ### Fixed
12
+
13
+ - When ancestor inspection fails under `CODEX_SANDBOX=seatbelt`, `whoami` now explains that Codex blocked process inspection and that dispatch must be rerun outside the sandbox because omnilane also needs `~/.omnilane` writes and network access; non-sandbox refusals are unchanged.
14
+
15
+ - Codex `app-server` caller identity always comes from the latest host-written
16
+ `turn_context`, never its startup defaults. Other Codex launches use rollout
17
+ evidence only when no explicit model was selected (profiles are not selectors).
18
+ - Bind `CODEX_THREAD_ID` in the current process to the codex direct child's
19
+ initial environment. macOS reads argv and environment separately through
20
+ `KERN_PROCARGS2`; Linux reads `/proc`. Look the rollout up as
21
+ `rollout-*-<thread>.jsonl` and, for a resumed thread,
22
+ `rollout-*-<thread>_<session>.jsonl`, read the most recently written match, and
23
+ require a matching `session_meta.id`, non-empty model/effort/turn id, and no
24
+ later `task_complete`, its `turn_complete` read alias, or `turn_aborted`
25
+ carrying that same turn id. A refusal names both turn ids and how long ago the
26
+ rollout was last written. Missing, malformed or stale evidence refuses.
27
+ - Stream JSONL records and retain only identity metadata and event types;
28
+ diagnostics never include message content. `whoami` reports thread and turn ids.
29
+ - Accept Codex `-c model=...` / `--config` overrides, including attached/equals
30
+ forms and TOML strings (Python 3.11+); explicit `-m` / `--model` still wins
31
+ outside app-server. Other vendors and caller-context/inherited/human priority
32
+ remain unchanged. `OMNILANE_AA_CALLER_FROM_PROCESS=0` disables both readers.
33
+ - The test suite no longer inherits the dispatcher's AA environment. Run inside
34
+ an omnilane worker it kept the exported `OMNILANE_AA_*`, so the authorizer
35
+ identity and the transport overlay hash pin outranked each fixture's own human
36
+ exemption and six tests failed for that reason alone.
37
+
38
+ ### Verification
39
+
40
+ - Accepted on real Codex desktop threads on 2026-09-12: `whoami` and
41
+ `dispatch.sh --dry-run` both exited 0 in a fresh thread and in a resumed thread
42
+ whose original rollout had stopped the previous day, reporting
43
+ `codex/gpt-6-astra-xhigh (score 54)` and `"allowed":true` with no job created.
44
+ - Source contract verified earlier in five real turns with bundled app-server
45
+ 0.153.4. 較新版本若延後寫檔,過期保護會讓它拒絕,而不是讀到舊的一輪。
46
+ - Metadata-only check of the two 0.154.0 worker rollouts: thread
47
+ `01a08f9e-6450-7f82-85e9-af534954b4ac` was absent from both active and archived
48
+ directories; `01a08fb4-fd1a-7811-a457-c21a4a6ff4d8` existed in active sessions,
49
+ created 2026-09-11 17:03:18 Asia/Taipei. Neither file's contents were opened.
50
+
9
51
  ## [0.42.7] - 2026-09-10
10
52
 
11
53
  ### Added
@@ -1016,7 +1058,8 @@ work to the wrong model, and records the evidence behind the shipped defaults.
1016
1058
  - Initial shared routing table, cross-vendor dispatcher, runners, installer,
1017
1059
  and baseline lint fixes.
1018
1060
 
1019
- [Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.42.7...HEAD
1061
+ [Unreleased]: https://github.com/Seraphim0916/omnilane/compare/v0.42.8...HEAD
1062
+ [0.42.8]: https://github.com/Seraphim0916/omnilane/compare/v0.42.7...v0.42.8
1020
1063
  [0.42.7]: https://github.com/Seraphim0916/omnilane/compare/v0.42.6...v0.42.7
1021
1064
  [0.42.6]: https://github.com/Seraphim0916/omnilane/compare/v0.42.5...v0.42.6
1022
1065
  [0.42.5]: https://github.com/Seraphim0916/omnilane/compare/v0.42.4...v0.42.5
package/README.ja.md CHANGED
@@ -577,6 +577,35 @@ work の別名ではありません。サービス管理など、work の境界
577
577
 
578
578
  ## 📜 リリース履歴
579
579
 
580
+ ## v0.42.8 の新機能
581
+
582
+ - **Codex の現在ターンの身元。** `app-server` は起動時のモデル・強度を常に無視します。
583
+ それ以外の Codex は明示的な `-m` / `--model` / `-c model=...` / `--config` を維持し、
584
+ モデル指定がない場合(プロファイルのみを含む)は現在ターンの記録を使います。
585
+ TOML のモデル指定には Python 3.11 以降が必要です。
586
+ - **曖昧なら拒否。** 現在のプロセス環境と Codex 直下の子プロセスの初期環境に、同じ UUID 形式の
587
+ `CODEX_THREAD_ID` が必要です。記録は `$CODEX_HOME/sessions`(既定 `~/.codex`)から
588
+ `rollout-*-<スレッド>.jsonl`、再開後は `rollout-*-<スレッド>_<セッション>.jsonl` として探し、
589
+ 最後に書き込まれたものを読み、`session_meta.id` が一致する必要があります。最後の `turn_context`
590
+ にはモデル・強度・ターン ID が必要です。その後に**同じターン ID** の `task_complete` /
591
+ `turn_complete`(読み取り時の別名)/ `turn_aborted` があれば古い身元として拒否し、
592
+ 両方のターン ID と、その記録が最後に書かれてからの経過時間を示します。
593
+ - **Codex サンドボックスでの拒否。** 祖先検索は従来どおり先に実行します。`CODEX_SANDBOX=seatbelt`
594
+ で失敗した場合、`whoami` はプロセス検査、`~/.omnilane` への書き込み、ネットワーク接続に
595
+ サンドボックス外での再実行が必要だと案内します。
596
+ - **追跡可能性と互換性。** JSONL は逐次読み込み、メッセージ本文を診断に含めません。
597
+ `whoami` はスレッドとターン ID を表示します。設定既定値・モデル一覧・アーカイブ・他の会話は
598
+ 代用しません。他社 CLI、明示・継承した身元、人間の宣言の優先順位は変更しません。
599
+ `OMNILANE_AA_CALLER_FROM_PROCESS=0` は両方の読み取りを無効にします。
600
+ - **テストの環境分離。** テストはディスパッチ元のワーカーから `OMNILANE_AA_*` を
601
+ 引き継がなくなりました。ワーカー内で実行すると、承認者の身元と転送オーバーレイの
602
+ ハッシュ固定が各フィクスチャ自身の人間宣言を上書きし、6 件が失敗していました。
603
+ - **実機での受け入れ。** 2026-09-12 に Codex デスクトップで実測:新規スレッドと、
604
+ 元の記録が前日で止まっていた再開スレッドの両方で `whoami` と
605
+ `dispatch.sh --dry-run` が終了コード 0、`codex/gpt-6-astra-xhigh (score 54)` と
606
+ `"allowed":true` を返し、ジョブは作成されませんでした。
607
+ 更新は `npm i -g omnilane@0.42.8`。
608
+
580
609
  ## v0.42.7 の新機能
581
610
 
582
611
  - **モデルのセッションは識別ファイルなしでディスパッチできます。** `--caller-context` が無い場合、dispatch はプロセスツリーを遡って最も近いベンダー CLI を見つけ、その起動時のモデルと effort を読み取ります。これまで omnilane のチェックアウト外のセッションは `missing-caller-context` で止まり、判断をオペレーターに差し戻していました(2026-09-08 と 2026-09-10 に 3 件)。
package/README.ko.md CHANGED
@@ -562,6 +562,34 @@ doctor가 파일과 벤더를 지목하며, 재서명 절차는 디스패치 스
562
562
 
563
563
  ## 📜 릴리스 기록
564
564
 
565
+ ## v0.42.8 새 기능
566
+
567
+ - **Codex 현재 턴 신원.** `app-server`는 시작 시 모델·강도 기본값을 항상 무시합니다.
568
+ 다른 Codex 실행은 명시적인 `-m` / `--model` / `-c model=...` / `--config`를 유지하며,
569
+ 모델 지정이 없으면(프로필만 지정한 경우 포함) 현재 턴 기록을 사용합니다.
570
+ TOML 모델 지정에는 Python 3.11 이상이 필요합니다.
571
+ - **불확실하면 거부.** 현재 프로세스 환경과 Codex의 직계 자식 프로세스 초기 환경에 같은 UUID 형식의
572
+ `CODEX_THREAD_ID`가 있어야 합니다. 기록은 `$CODEX_HOME/sessions`(기본 `~/.codex`)에서
573
+ `rollout-*-<스레드>.jsonl`로, 재개한 뒤에는 `rollout-*-<스레드>_<세션>.jsonl`로 찾으며,
574
+ 가장 최근에 기록된 파일을 읽고 `session_meta.id`가 일치해야 합니다. 마지막 `turn_context`에는
575
+ 모델·강도·턴 ID가 필요하며, 그 뒤에 **같은 턴 ID**의 `task_complete` / `turn_complete`(읽기 별칭) /
576
+ `turn_aborted`가 있으면 오래된 신원으로 거부하고, 두 턴 ID와 그 기록이 마지막으로 기록된 뒤
577
+ 지난 시간을 함께 알려 줍니다.
578
+ - **Codex 샌드박스 거부.** 조상 조회는 기존처럼 먼저 실행합니다. `CODEX_SANDBOX=seatbelt`에서
579
+ 실패하면 `whoami`는 프로세스 조회, `~/.omnilane` 쓰기 및 네트워크 연결을 위해 샌드박스
580
+ 밖에서 명령을 다시 실행해야 한다고 안내합니다.
581
+ - **추적성과 호환성.** JSONL을 순차적으로 읽고 메시지 본문은 진단에 포함하지 않습니다.
582
+ `whoami`는 스레드와 턴 ID를 표시합니다. 설정 기본값·모델 목록·보관 기록·다른 대화로
583
+ 추정하지 않습니다. 다른 공급자, 명시적·상속된 신원과 사람 선언의 우선순위는 유지합니다.
584
+ `OMNILANE_AA_CALLER_FROM_PROCESS=0`은 두 읽기 경로를 모두 끕니다.
585
+ - **테스트 환경 격리.** 테스트는 더 이상 디스패치 워커가 내보낸 `OMNILANE_AA_*`를
586
+ 물려받지 않습니다. 워커 안에서 실행하면 승인자 신원과 전송 오버레이 해시 고정이 각
587
+ 픽스처의 사람 선언을 덮어써 6개가 실패했습니다.
588
+ - **실제 환경 검증.** 2026-09-12 Codex 데스크톱에서 확인: 새 스레드와, 원래 기록이 전날
589
+ 멈춰 있던 재개 스레드 모두에서 `whoami`와 `dispatch.sh --dry-run`이 종료 코드 0으로
590
+ `codex/gpt-6-astra-xhigh (score 54)`와 `"allowed":true`를 보고했고 작업은 생성되지
591
+ 않았습니다. 업데이트: `npm i -g omnilane@0.42.8`.
592
+
565
593
  ## v0.42.7 새 기능
566
594
 
567
595
  - **모델 세션은 신원 파일 없이 디스패치할 수 있습니다.** `--caller-context` 가 없으면 dispatch 가 프로세스 트리를 거슬러 올라가 가장 가까운 벤더 CLI 를 찾고, 그 CLI 가 실행될 때의 모델과 effort 를 읽습니다. 지금까지 omnilane 체크아웃 밖의 세션은 `missing-caller-context` 에서 멈추고 판단을 운영자에게 되돌렸습니다(2026-09-08, 2026-09-10 에 3건).
package/README.md CHANGED
@@ -71,8 +71,9 @@ omnilane route hardest-coding "fix the flaky auth token refresh"
71
71
  > capability score, so a dispatch has to say who is asking. A human at a terminal
72
72
  > asserts that once with `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1`, or per call with
73
73
  > `--operator-asserted-human`. A model driving omnilane cannot assert it for
74
- > itself. Its identity is read from the CLI that launched it — the model and
75
- > effort flags that CLI was started with — so an ordinary session passes nothing,
74
+ > itself. Its identity is read from the nearest launching CLI: explicit model and
75
+ > effort flags, or for Codex app-server/no-model launches, a bound current-turn
76
+ > rollout (never app-server startup defaults). An ordinary session passes nothing,
76
77
  > and `omnilane whoami` prints that identity as a `--caller-context FILE`. With
77
78
  > neither an assertion nor a readable identity, the dispatch is refused with
78
79
  > `missing-caller-context` before any job is created.
@@ -629,8 +630,9 @@ machine's configuration or your request.
629
630
 
630
631
  `missing-caller-context` — no identity reached the gate. A human adds
631
632
  `OMNILANE_AA_OPERATOR_ASSERTED_HUMAN=1` or `--operator-asserted-human`. A model
632
- normally needs nothing: dispatch reads its identity from the CLI that launched
633
- it. When that fails, run `omnilane whoami` — it either prints a
633
+ normally needs nothing: dispatch reads its identity from the nearest launching
634
+ CLI, using a bound current-turn rollout for Codex app-server/no-model launches.
635
+ When that fails, run `omnilane whoami` — it either prints a
634
636
  `--caller-context FILE` to pass, or says exactly why it cannot (a missing
635
637
  `--effort`, a model alias, no scored row). A model must not assert the human
636
638
  exemption for itself.
@@ -704,6 +706,38 @@ working notes, including per-benchmark caveats, live in
704
706
 
705
707
  ## 📜 Release history
706
708
 
709
+ ## What's new in v0.42.8
710
+
711
+ - **Codex current-turn identity.** `app-server` always ignores startup model and
712
+ effort defaults. Other Codex launches keep explicit `-m` / `--model` or
713
+ `-c model=...` / `--config` selectors (TOML model overrides require Python 3.11+);
714
+ no model, including profile-only launches, uses the rollout path.
715
+ - **Bound, fail-closed evidence.** The current process environment and the codex
716
+ direct-child initial environment must carry the same UUID-shaped
717
+ `CODEX_THREAD_ID`. The rollout is looked up under `$CODEX_HOME/sessions`
718
+ (default `~/.codex`) as `rollout-*-<thread>.jsonl` and, after a resume,
719
+ `rollout-*-<thread>_<session>.jsonl`; the most recently written match is read
720
+ and its `session_meta.id` must match. Its latest `turn_context` needs model,
721
+ effort and turn id; a later `task_complete`, `turn_complete` (read alias) or
722
+ `turn_aborted` carrying that same turn id refuses stale identity, and the
723
+ refusal names both turn ids and how long ago the rollout was written. No config,
724
+ model-list, archive or other-session fallback; JSONL is streamed and message
725
+ content is never included in diagnostics. `whoami` reports thread and turn ids.
726
+ - **Codex sandbox refusal.** Ancestor lookup still runs first. If it fails under
727
+ `CODEX_SANDBOX=seatbelt`, `whoami` explains that process inspection,
728
+ `~/.omnilane` writes and networking require rerunning outside the sandbox.
729
+ - **Hermetic tests.** The suite no longer inherits `OMNILANE_AA_*` from a
730
+ dispatching worker, where the authorizer identity and the overlay hash pin used
731
+ to outrank each fixture's own human exemption and fail six tests.
732
+ - **Compatibility.** Other vendors, explicit/inherited caller identity and human
733
+ assertion precedence are unchanged. `OMNILANE_AA_CALLER_FROM_PROCESS=0` disables
734
+ both argv and rollout reading.
735
+ - **Accepted on a real desktop thread.** On 2026-09-12 `whoami` and
736
+ `dispatch.sh --dry-run` both exited 0 from the Codex desktop app — in a fresh
737
+ thread and in a resumed one whose original rollout had stopped the day before,
738
+ reporting `codex/gpt-6-astra-xhigh (score 54)` and `"allowed":true`.
739
+ - **Upgrade.** Run `npm i -g omnilane@0.42.8`.
740
+
707
741
  ## What's new in v0.42.7
708
742
 
709
743
  - **A model session no longer needs an identity file to dispatch.** When no `--caller-context` is given, dispatch walks up the process tree to the nearest vendor CLI and reads the model and effort it was launched with. Sessions outside the omnilane checkout used to stop on `missing-caller-context` and hand the question back to the operator; three did so on 2026-09-08 and 2026-09-10.
package/README.zh-CN.md CHANGED
@@ -556,6 +556,29 @@ codex 记在 session rollout,agy 写进 `cli.log`。这是 CLI 自己抄的订
556
556
 
557
557
  ## 📜 版本历程
558
558
 
559
+ ## v0.42.8 新功能
560
+
561
+ - **Codex 当前轮次身份。** `app-server` 始终忽略启动参数中的模型和强度默认值。
562
+ 其他 Codex 启动保留明确的 `-m` / `--model` / `-c model=...` / `--config`;
563
+ 未指定模型(包括只指定 profile)时读取当前轮次记录。TOML 模型覆盖需要 Python 3.11+。
564
+ - **证据不明就拒绝。** 本进程的当前环境与 Codex 直属子进程的初始环境必须有相同、符合 UUID 格式的
565
+ `CODEX_THREAD_ID`。记录文件在 `$CODEX_HOME/sessions`(默认 `~/.codex`)下按
566
+ `rollout-*-<对话串>.jsonl` 查找,对话串续用后还会有 `rollout-*-<对话串>_<会话>.jsonl`;
567
+ 取最后写入的那一个,`session_meta.id` 必须一致。最后一个 `turn_context` 必须包含模型、强度和轮次 ID;
568
+ 后面若出现**同一个轮次 ID** 的 `task_complete` / `turn_complete`(读取别名)/ `turn_aborted`,
569
+ 就拒绝使用过期身份,并在消息里写出两个轮次 ID,以及该记录多久没有被写入。
570
+ - **Codex 沙箱拒绝。** 仍会先查询祖先进程;若在 `CODEX_SANDBOX=seatbelt` 下查询失败,
571
+ `whoami` 会说明进程查询、写入 `~/.omnilane` 和联网都需要在沙箱外重新执行命令。
572
+ - **隐私与兼容。** 逐行读取 JSONL,诊断不含消息正文;`whoami` 会报告对话串和轮次 ID。
573
+ 不从配置默认值、模型列表、归档或其他对话推断。其他供应商、明确或继承的身份、人类声明
574
+ 的优先级不变;`OMNILANE_AA_CALLER_FROM_PROCESS=0` 同时关闭两种读取路径。
575
+ - **测试自己清环境。** 测试不再沿用派工工人带进来的 `OMNILANE_AA_*`;在工人里运行时,
576
+ 授权者身份与传输对照表的哈希锁定会盖掉各夹具自己的人类豁免,导致 6 个测试失败。
577
+ - **实机验收。** 2026-09-12 在 Codex 桌面版实测:新建对话串与续用对话串(原始记录文件
578
+ 前一天就停止写入)各跑一次,`whoami` 与 `dispatch.sh --dry-run` 都退出 0,读出
579
+ `codex/gpt-6-astra-xhigh (score 54)`、`"allowed":true`,且没有创建任何任务。
580
+ 升级:`npm i -g omnilane@0.42.8`。
581
+
559
582
  ## v0.42.7 新功能
560
583
 
561
584
  - **模型 session 派工不再需要身份文件。** 没给 `--caller-context` 时,dispatch 会沿进程树往上找到最近的厂商 CLI,读取它启动时带的模型与强度。以前不在 omnilane 目录下的 session 会卡在 `missing-caller-context`,把问题丢回给操作者——2026-09-08 与 2026-09-10 就有三个 session 这样停下来。
package/README.zh-TW.md CHANGED
@@ -597,6 +597,29 @@ codex 記在 session rollout,agy 寫進 `cli.log`。這是 CLI 自己抄的訂
597
597
 
598
598
  ## 📜 版本歷程
599
599
 
600
+ ## v0.42.8 新功能
601
+
602
+ - **Codex 本輪身分。** `app-server` 一律忽略啟動參數的模型與強度預設值。
603
+ 其他 Codex 啟動保留明確的 `-m` / `--model` / `-c model=...` / `--config`;
604
+ 沒有指定模型(包括只指定 profile)時,改讀本輪記錄。TOML 模型覆寫需要 Python 3.11+。
605
+ - **證據不明就拒絕。** 本程序的現行環境與 Codex 直屬子程序的初始環境,必須有相同且符合 UUID 形狀的
606
+ `CODEX_THREAD_ID`。記錄檔在 `$CODEX_HOME/sessions`(預設 `~/.codex`)底下找
607
+ `rollout-*-<對話串>.jsonl`,對話串續用之後還會有 `rollout-*-<對話串>_<工作階段>.jsonl`;
608
+ 取最後寫入的那一份,`session_meta.id` 必須一致。最後一筆 `turn_context` 必須有模型、強度與回合 ID;
609
+ 後面若出現**同一個回合 ID** 的 `task_complete` / `turn_complete`(讀取別名)/ `turn_aborted`,
610
+ 就拒絕沿用過期身分,並在訊息裡寫出兩個回合 ID,以及那份記錄多久沒有被寫入。
611
+ - **Codex 沙箱拒絕。** 仍會先查詢祖先程序;若在 `CODEX_SANDBOX=seatbelt` 下查詢失敗,
612
+ `whoami` 會說明程序查詢、寫入 `~/.omnilane` 與連網都必須把指令移到沙箱外重新執行。
613
+ - **隱私與相容。** 逐行讀取 JSONL,不把訊息內容放進診斷;`whoami` 會回報對話串與回合 ID。
614
+ 不從設定預設值、模型清單、封存或其他對話推斷。其他廠商、明確或繼承的身分、真人聲明
615
+ 的優先順序不變;`OMNILANE_AA_CALLER_FROM_PROCESS=0` 同時關閉兩條讀取路徑。
616
+ - **測試自己清環境。** 測試不再沿用派工工人帶進來的 `OMNILANE_AA_*`;在工人裡跑時,
617
+ 授權者身分與傳輸對照表的雜湊釘選會蓋掉各夾具自己的真人豁免,害 6 個測試失敗。
618
+ - **實機驗收。** 2026-09-12 在 Codex 桌面版實測:新開的對話串與續用的對話串(原始記錄檔
619
+ 前一天就停筆)各跑一次,`whoami` 與 `dispatch.sh --dry-run` 都退出 0,讀出
620
+ `codex/gpt-6-astra-xhigh (score 54)`、`"allowed":true`,而且沒有建立任何工作。
621
+ 升級:`npm i -g omnilane@0.42.8`。
622
+
600
623
  ## v0.42.7 新功能
601
624
 
602
625
  - **模型 session 派工不再需要身分檔。** 沒給 `--caller-context` 時,dispatch 會沿行程樹往上找到最近的廠商 CLI,讀它啟動時帶的模型與強度。以前不在 omnilane 目錄下的 session 會卡在 `missing-caller-context`,把問題丟回給操作者——2026-09-08 與 2026-09-10 就有三個 session 這樣停下來。
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.42.7
1
+ 0.42.8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnilane",
3
- "version": "0.42.7",
3
+ "version": "0.42.8",
4
4
  "description": "One routing table, every harness — classify subtasks into lanes and delegate each lane through a compatible caller-owned native agent or vendor CLI.",
5
5
  "bin": {
6
6
  "omnilane": "bin/omnilane"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
3
  "name": "omnilane",
4
- "version": "0.42.7",
4
+ "version": "0.42.8",
5
5
  "description": "One routing table, every harness: classify subtasks into lanes and delegate through compatible caller-owned native agents or vendor CLIs with exact-AA downward policy and supervised jobs."
6
6
  }
@@ -1,35 +1,56 @@
1
1
  #!/usr/bin/env python3
2
2
  """Read the exact AA caller identity of the CLI this process runs under.
3
3
 
4
- Walks up the process tree to the nearest vendor CLI, reads the model and effort
5
- it was launched with, and maps them onto the one scored configuration they
6
- select. Launch flags are the harness's request selector: the same class of
7
- evidence the transport overlay carries, and unlike a hand-written caller-context
8
- file, not something the model can edit.
4
+ Walks up the process tree to the nearest vendor CLI and maps its selector onto
5
+ one scored configuration. Codex app-server always uses the host-written current
6
+ turn; other Codex launches do so only without an explicit model. The rollout is
7
+ bound to the codex direct child's initial environment, never another session.
9
8
 
10
9
  Prints the path of a caller-context file for that identity. Exits 3 with the
11
- reason on stderr when it cannot decide. It never guesses: a missing flag, an
12
- alias, or an identity the frozen registry does not score exactly once is a
10
+ reason on stderr when it cannot decide. It never guesses: missing selector
11
+ evidence, an alias, or an identity not scored exactly once is a
13
12
  refusal, not a fallback.
14
13
  """
15
14
  from __future__ import annotations
16
15
 
17
16
  import argparse
17
+ import ctypes
18
18
  import json
19
19
  import os
20
+ import re
21
+ import struct
20
22
  import subprocess
21
23
  import sys
24
+ from datetime import datetime, timezone
22
25
  from pathlib import Path
23
- from typing import Callable, Optional
26
+ from typing import Callable, Mapping, Optional
24
27
 
25
28
  sys.path.insert(0, str(Path(__file__).resolve().parent))
26
29
  import aa_policy # noqa: E402
27
30
 
31
+ try:
32
+ import tomllib
33
+ except ImportError: # Explicit-model callers still work on Python 3.9/3.10.
34
+ tomllib = None
35
+
28
36
  REPO = Path(__file__).resolve().parents[2]
29
37
  MAX_DEPTH = 64
30
38
  ENCODED_EFFORTS = ("xhigh", "high", "medium", "low")
31
39
  Selector = tuple[str, Optional[str], Optional[str]]
32
40
  Lookup = Callable[[int], Optional[tuple[int, list[str]]]]
41
+ EnvironmentLookup = Callable[[int], dict[str, str]]
42
+ UUID_PATTERN = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\Z")
43
+ TURN_END_EVENTS = frozenset(("task_complete", "turn_complete", "turn_aborted"))
44
+ CODEX_SANDBOX_REFUSAL = (
45
+ "Codex sandbox (CODEX_SANDBOX=seatbelt) blocked process inspection; omnilane also "
46
+ "needs to write ~/.omnilane and access the network, which this sandbox does not allow; "
47
+ "rerun this command outside the sandbox (approve running outside the sandbox when Codex "
48
+ "asks, or use a conversation with full access)"
49
+ )
50
+
51
+
52
+ class _ProcessQueryUnavailable(Exception):
53
+ """Keep a swallowed platform process-query failure available to read_caller()."""
33
54
 
34
55
 
35
56
  def _vendor(executable: str) -> str | None:
@@ -58,16 +79,52 @@ def _flag(argv: list[str], *names: str) -> str | None:
58
79
  return value
59
80
 
60
81
 
61
- def _codex_effort(argv: list[str]) -> str | None:
62
- effort = None
63
- for index, token in enumerate(argv[:-1]):
82
+ def _codex_config(argv: list[str], name: str) -> str | None:
83
+ selected = None
84
+ tokens = iter(argv)
85
+ for token in tokens:
86
+ if token == "--":
87
+ break
64
88
  if token in ("-c", "--config"):
65
- key, _, value = argv[index + 1].partition("=")
66
- if key.strip() == "model_reasoning_effort":
67
- effort = value.strip().strip("'\"")
68
- return effort
89
+ override = next(tokens, "")
90
+ elif token.startswith("--config="):
91
+ override = token.partition("=")[2]
92
+ elif token.startswith("-c"):
93
+ override = token[2:].removeprefix("=")
94
+ else:
95
+ continue
96
+ key, sep, value = override.partition("=")
97
+ if sep and key.strip() == name:
98
+ selected = value.strip()
99
+ if selected is None:
100
+ return None
101
+ if tomllib is None:
102
+ if name == "model_reasoning_effort":
103
+ return selected.strip("'\"")
104
+ raise ValueError("model config decoding requires Python 3.11+ (tomllib)")
105
+ try:
106
+ value = tomllib.loads("value = " + selected)["value"]
107
+ except ValueError:
108
+ value = selected # Codex treats invalid TOML as a literal string.
109
+ if not isinstance(value, str) or not value.strip():
110
+ raise ValueError(f"{name} config must be a non-empty string")
111
+ return value
69
112
 
70
113
 
114
+ def _codex_subcommand(argv: list[str]) -> str | None:
115
+ takes_value = {"-c", "--config", "-m", "--model", "-p", "--profile", "-C", "--cd",
116
+ "-s", "--sandbox", "-a", "--ask-for-approval", "-i", "--image",
117
+ "--enable", "--disable", "--add-dir", "--local-provider"}
118
+ tokens = iter(argv[1:])
119
+ for token in tokens:
120
+ if token == "--":
121
+ return None
122
+ if token in takes_value:
123
+ next(tokens, None)
124
+ elif not token.startswith("-"):
125
+ return token
126
+ return None
127
+
71
128
  def read_selector(argv: list[str]) -> Selector | None:
72
129
  """(vendor, model, effort) when argv launches a vendor CLI, otherwise None."""
73
130
  if not argv:
@@ -77,7 +134,14 @@ def read_selector(argv: list[str]) -> Selector | None:
77
134
  if vendor == "claude":
78
135
  return vendor, _flag(rest, "--model"), _flag(rest, "--effort")
79
136
  if vendor == "codex":
80
- return vendor, _flag(rest, "-m", "--model"), _codex_effort(rest)
137
+ if _codex_subcommand(argv) == "app-server":
138
+ return vendor, None, None
139
+ model = _flag(rest, "-m", "--model")
140
+ if model is None:
141
+ model = _codex_config(rest, "model")
142
+ if model is None:
143
+ return vendor, None, None
144
+ return vendor, model, _codex_config(rest, "model_reasoning_effort")
81
145
  if vendor == "grok":
82
146
  return vendor, _flag(rest, "-m", "--model"), _flag(rest, "--reasoning-effort")
83
147
  if vendor == "gemini":
@@ -120,6 +184,61 @@ def resolve(registry: dict, vendor: str, model: str | None,
120
184
  return None, f"no scored configuration for {vendor} {model} at effort {effort}"
121
185
 
122
186
 
187
+ def _thread_environment(entries: list[bytes]) -> dict[str, str]:
188
+ values = [entry.partition(b"=")[2] for entry in entries
189
+ if entry.partition(b"=")[0] == b"CODEX_THREAD_ID"]
190
+ if len(values) > 1:
191
+ raise ValueError("duplicate CODEX_THREAD_ID in initial environment")
192
+ return {"CODEX_THREAD_ID": values[0].decode(errors="replace")} if values else {}
193
+
194
+
195
+ def _parse_procargs2(data: bytes) -> tuple[list[str], dict[str, str]]:
196
+ if len(data) < 4:
197
+ raise ValueError("truncated KERN_PROCARGS2 header")
198
+ argc = struct.unpack_from("=i", data)[0]
199
+ offset = data.find(b"\0", 4)
200
+ if argc <= 0 or offset < 0:
201
+ raise ValueError("invalid KERN_PROCARGS2 header")
202
+ while offset < len(data) and data[offset] == 0:
203
+ offset += 1
204
+ argv = []
205
+ for _ in range(argc):
206
+ end = data.find(b"\0", offset)
207
+ if end < 0:
208
+ raise ValueError("truncated KERN_PROCARGS2 argv")
209
+ argv.append(data[offset:end].decode(errors="replace"))
210
+ offset = end + 1
211
+ # argc, not a KEY=value search, separates argv from the initial environment.
212
+ entries = []
213
+ while offset < len(data) and data[offset] != 0:
214
+ end = data.find(b"\0", offset)
215
+ if end < 0:
216
+ raise ValueError("truncated KERN_PROCARGS2 environment")
217
+ entries.append(data[offset:end])
218
+ offset = end + 1
219
+ return argv, _thread_environment(entries)
220
+
221
+
222
+ def _darwin_procargs(pid: int) -> tuple[list[str], dict[str, str]]:
223
+ libc = ctypes.CDLL(None, use_errno=True)
224
+ argmax = ctypes.c_int()
225
+ size = ctypes.c_size_t(ctypes.sizeof(argmax))
226
+ if libc.sysctlbyname(b"kern.argmax", ctypes.byref(argmax), ctypes.byref(size), None, 0):
227
+ raise OSError(ctypes.get_errno(), "cannot read kern.argmax")
228
+ buffer = ctypes.create_string_buffer(argmax.value)
229
+ size = ctypes.c_size_t(len(buffer))
230
+ mib = (ctypes.c_int * 3)(1, 49, pid) # CTL_KERN, KERN_PROCARGS2.
231
+ if libc.sysctl(mib, 3, buffer, ctypes.byref(size), None, 0):
232
+ raise OSError(ctypes.get_errno(), "cannot read KERN_PROCARGS2")
233
+ return _parse_procargs2(buffer.raw[:size.value])
234
+
235
+
236
+ def _initial_environment(pid: int) -> dict[str, str]:
237
+ if sys.platform == "darwin":
238
+ return _darwin_procargs(pid)[1]
239
+ return _thread_environment(Path(f"/proc/{pid}/environ").read_bytes().split(b"\0"))
240
+
241
+
123
242
  def _process(pid: int) -> tuple[int, list[str]] | None:
124
243
  """(ppid, argv) for pid. /proc keeps argv exact; ps joins it with spaces, so
125
244
  argv[0] comes from `comm`, which keeps a path like `Application Support` whole."""
@@ -133,26 +252,34 @@ def _process(pid: int) -> tuple[int, list[str]] | None:
133
252
  return None
134
253
  return ppid, argv
135
254
  try:
136
- head = subprocess.run(["ps", "-o", "ppid=", "-o", "comm=", "-p", str(pid)],
137
- capture_output=True, text=True, timeout=5).stdout.strip()
138
- args = subprocess.run(["ps", "-o", "args=", "-p", str(pid)],
139
- capture_output=True, text=True, timeout=5).stdout.strip()
255
+ head_result = subprocess.run(
256
+ ["ps", "-o", "ppid=", "-o", "comm=", "-p", str(pid)],
257
+ capture_output=True, text=True, timeout=5)
258
+ args_result = subprocess.run(
259
+ ["ps", "-o", "args=", "-p", str(pid)],
260
+ capture_output=True, text=True, timeout=5)
140
261
  except (OSError, subprocess.SubprocessError):
141
- return None
262
+ raise _ProcessQueryUnavailable from None
263
+ head = head_result.stdout.strip()
264
+ args = args_result.stdout.strip()
142
265
  parts = head.split(None, 1)
143
266
  if len(parts) != 2 or not parts[0].isdigit():
144
267
  return None
145
268
  comm = parts[1].strip()
269
+ if sys.platform == "darwin" and _vendor(comm) == "codex":
270
+ argv, _ = _darwin_procargs(pid)
271
+ return int(parts[0]), argv
146
272
  rest = args[len(comm):] if args.startswith(comm) else args.partition(" ")[2]
147
273
  return int(parts[0]), [comm, *rest.split()]
148
274
 
149
275
 
150
- def find_launcher(pid: int, lookup: Lookup = _process) -> tuple[int, Selector] | None:
276
+ def _launcher(pid: int, lookup: Lookup) -> tuple[int, Selector, int | None] | None:
151
277
  """The nearest process at or above pid that is a vendor CLI, with its selector.
152
278
 
153
279
  Nearest wins: a codex worker started by a Claude session is a codex caller.
154
280
  """
155
281
  seen: set[int] = set()
282
+ child = None
156
283
  for _ in range(MAX_DEPTH):
157
284
  if pid <= 0 or pid in seen:
158
285
  return None
@@ -163,11 +290,165 @@ def find_launcher(pid: int, lookup: Lookup = _process) -> tuple[int, Selector] |
163
290
  ppid, argv = entry
164
291
  selector = read_selector(argv)
165
292
  if selector is not None:
166
- return pid, selector
293
+ return pid, selector, child
294
+ child = pid
167
295
  pid = ppid
168
296
  return None
169
297
 
170
298
 
299
+ def find_launcher(pid: int, lookup: Lookup = _process) -> tuple[int, Selector] | None:
300
+ try:
301
+ found = _launcher(pid, lookup)
302
+ except _ProcessQueryUnavailable:
303
+ return None
304
+ return found[:2] if found else None
305
+
306
+
307
+ def _rollout_age_hint(timestamp: object, now: Callable[[], datetime] | None) -> str:
308
+ """Describe the last record without letting unusable time data change a refusal."""
309
+ if not isinstance(timestamp, str):
310
+ return ""
311
+ try:
312
+ recorded = datetime.fromisoformat(timestamp[:-1] + "+00:00" if timestamp.endswith("Z") else timestamp)
313
+ current = now() if now is not None else datetime.now(timezone.utc)
314
+ if recorded.tzinfo is None or current.tzinfo is None:
315
+ return ""
316
+ seconds = (current - recorded).total_seconds()
317
+ except (AttributeError, TypeError, ValueError, OverflowError):
318
+ return ""
319
+ if seconds < 0:
320
+ return ""
321
+ # Floor display units; compare the unrounded duration against the hint threshold.
322
+ if seconds < 3600:
323
+ age = f"{int(seconds // 60)}m"
324
+ elif seconds < 86400:
325
+ age = f"{int(seconds // 3600)}h"
326
+ else:
327
+ age = f"{int(seconds // 86400)}d"
328
+ hint = f"; that rollout's last record is {timestamp}, {age} before now"
329
+ if seconds > 600:
330
+ hint += ", so CODEX_THREAD_ID may name an earlier conversation than the one running this command"
331
+ return hint
332
+
333
+
334
+ def _rollout_candidates(home: Path, thread: str) -> list[Path]:
335
+ """Resuming a thread opens a second rollout named <thread>_<session>, so match both forms."""
336
+ sessions = home / "sessions"
337
+ found = set(sessions.glob(f"*/*/*/rollout-*-{thread}.jsonl"))
338
+ found |= set(sessions.glob(f"*/*/*/rollout-*-{thread}_*.jsonl"))
339
+ return sorted(found, key=lambda path: (path.stat().st_mtime, path.name))
340
+
341
+
342
+ def _rollout_selector(thread: str,
343
+ current_environment: Mapping[str, str] = os.environ,
344
+ now: Callable[[], datetime] | None = None) -> tuple[Selector, str]:
345
+ home = Path(current_environment.get("CODEX_HOME") or Path.home() / ".codex")
346
+ try:
347
+ paths = _rollout_candidates(home, thread)
348
+ except OSError as error:
349
+ raise ValueError(f"cannot read rollout (errno {error.errno})") from None
350
+ if not paths:
351
+ raise ValueError("expected an active rollout for this thread; found 0")
352
+ # The most recently written file is the live one; older files belong to earlier sessions.
353
+ path = paths[-1]
354
+ metadata = 0
355
+ latest = None
356
+ ended = None
357
+ last_timestamp = None
358
+ try:
359
+ with path.open(encoding="utf-8") as stream:
360
+ for line in stream:
361
+ record = json.loads(line)
362
+ if not isinstance(record, dict):
363
+ raise ValueError("invalid rollout record")
364
+ last_timestamp = record.get("timestamp")
365
+ kind = record.get("type")
366
+ if kind not in ("session_meta", "turn_context", "event_msg"):
367
+ continue
368
+ payload = record.get("payload")
369
+ if not isinstance(payload, dict):
370
+ raise ValueError("invalid rollout payload")
371
+ if kind == "session_meta":
372
+ metadata += 1
373
+ if payload.get("id") != thread:
374
+ raise ValueError("session_meta id does not match CODEX_THREAD_ID")
375
+ elif kind == "turn_context":
376
+ latest = {key: payload.get(key) for key in ("turn_id", "model", "effort")}
377
+ ended = None
378
+ elif kind == "event_msg":
379
+ event = payload.get("type")
380
+ if not isinstance(event, str):
381
+ raise ValueError("invalid rollout event type")
382
+ if latest is not None and event in TURN_END_EVENTS:
383
+ event_turn = payload.get("turn_id")
384
+ if not isinstance(event_turn, str) or not event_turn.strip():
385
+ event_turn = None
386
+ context_turn = latest["turn_id"]
387
+ if not isinstance(context_turn, str) or not context_turn.strip():
388
+ context_turn = None
389
+ if event_turn is None or context_turn is None or event_turn == context_turn:
390
+ ended = (event, context_turn or "unknown", event_turn or "unknown")
391
+ except (json.JSONDecodeError, UnicodeError):
392
+ raise ValueError("rollout contains incomplete or invalid JSON") from None
393
+ except OSError as error:
394
+ raise ValueError(f"cannot read rollout (errno {error.errno})") from None
395
+ if metadata != 1:
396
+ raise ValueError("expected exactly one matching session_meta")
397
+ if latest is None:
398
+ raise ValueError("rollout has no turn_context")
399
+ if ended:
400
+ event, context_turn, event_turn = ended
401
+ raise ValueError(f"latest turn_context turn {context_turn} has already ended "
402
+ f"({event}, turn {event_turn})" + _rollout_age_hint(last_timestamp, now))
403
+ for key, value in latest.items():
404
+ if not isinstance(value, str) or not value.strip():
405
+ detail = " (turn unknown)" if key == "turn_id" else ""
406
+ raise ValueError(f"latest turn_context {key} must be a non-empty string{detail}")
407
+ return ("codex", latest["model"], latest["effort"]), f"thread {thread}, turn {latest['turn_id']}"
408
+
409
+
410
+ def read_caller(pid: int, lookup: Lookup = _process,
411
+ environment: EnvironmentLookup = _initial_environment,
412
+ current_environment: Mapping[str, str] = os.environ,
413
+ now: Callable[[], datetime] | None = None) -> tuple[int, Selector, str]:
414
+ if current_environment.get("OMNILANE_AA_CALLER_FROM_PROCESS") == "0":
415
+ raise ValueError("caller identity from process is disabled")
416
+ try:
417
+ found = _launcher(pid, lookup)
418
+ except _ProcessQueryUnavailable:
419
+ if current_environment.get("CODEX_SANDBOX") == "seatbelt":
420
+ raise ValueError(CODEX_SANDBOX_REFUSAL) from None
421
+ found = None
422
+ except OSError as error:
423
+ if current_environment.get("CODEX_SANDBOX") == "seatbelt":
424
+ raise ValueError(CODEX_SANDBOX_REFUSAL) from None
425
+ raise ValueError(f"cannot query caller process (errno {error.errno})") from None
426
+ if found is None:
427
+ if current_environment.get("CODEX_SANDBOX") == "seatbelt":
428
+ raise ValueError(CODEX_SANDBOX_REFUSAL)
429
+ raise ValueError("no vendor CLI among this process's ancestors; a model caller passes "
430
+ "--caller-context FILE and a human operator --operator-asserted-human")
431
+ launcher, selector, child = found
432
+ if selector[0] != "codex" or selector[1] is not None:
433
+ return launcher, selector, ""
434
+ thread = current_environment.get("CODEX_THREAD_ID")
435
+ if not thread or not UUID_PATTERN.fullmatch(thread):
436
+ raise ValueError("current process CODEX_THREAD_ID is missing or not a UUID")
437
+ if child is None:
438
+ raise ValueError("cannot find the codex direct child in the ancestor chain")
439
+ try:
440
+ inherited = environment(child).get("CODEX_THREAD_ID")
441
+ except (OSError, ValueError) as error:
442
+ detail = f"errno {error.errno}" if isinstance(error, OSError) else "invalid environment block"
443
+ raise ValueError(f"cannot read codex direct child initial environment ({detail})") from None
444
+ if not inherited or not UUID_PATTERN.fullmatch(inherited):
445
+ raise ValueError("codex direct child CODEX_THREAD_ID is missing or not a UUID")
446
+ if inherited != thread:
447
+ raise ValueError("CODEX_THREAD_ID mismatch between current process and codex direct child")
448
+ selector, source = _rollout_selector(thread, current_environment, now)
449
+ return launcher, selector, source
450
+
451
+
171
452
  def load_registry(path: str | Path) -> tuple[dict, str]:
172
453
  """The approved registry without the transport overlay, which says nothing
173
454
  about the caller and must not stop one from learning who it is."""
@@ -203,7 +484,9 @@ def write_context(row: dict, registry: dict, home: Path) -> Path:
203
484
  return path
204
485
 
205
486
 
206
- def main(argv: list[str] | None = None) -> int:
487
+ def main(argv: list[str] | None = None, environment: Mapping[str, str] = os.environ,
488
+ lookup: Lookup = _process,
489
+ process_environment: EnvironmentLookup = _initial_environment) -> int:
207
490
  parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
208
491
  parser.add_argument("--registry",
209
492
  default=os.environ.get("OMNILANE_AA_POLICY_FILE")
@@ -215,20 +498,20 @@ def main(argv: list[str] | None = None) -> int:
215
498
  except (aa_policy.PolicyError, OSError, ValueError) as error:
216
499
  print(f"omnilane: cannot read the AA registry: {error}", file=sys.stderr)
217
500
  return 3
218
- found = find_launcher(os.getpid())
219
- if found is None:
220
- print("omnilane: no vendor CLI among this process's ancestors; a model caller passes "
221
- "--caller-context FILE and a human operator --operator-asserted-human",
222
- file=sys.stderr)
501
+ try:
502
+ pid, (vendor, model, effort), source = read_caller(
503
+ os.getpid(), lookup, process_environment, environment)
504
+ except ValueError as error:
505
+ print(f"omnilane: cannot read the caller identity: {error}", file=sys.stderr)
223
506
  return 3
224
- pid, (vendor, model, effort) = found
225
507
  row, reason = resolve(registry, vendor, model, effort)
226
508
  if row is None:
227
509
  print(f"omnilane: cannot read the caller identity from pid {pid}: {reason}", file=sys.stderr)
228
510
  return 3
229
511
  home = Path(os.environ.get("OMNILANE_HOME") or Path.home() / ".omnilane")
230
512
  path = write_context(row, registry, home)
231
- print(f"omnilane: caller is {row['id']} (score {row['score']}), read from pid {pid}",
513
+ provenance = f", {source}" if source else ""
514
+ print(f"omnilane: caller is {row['id']} (score {row['score']}), read from pid {pid}{provenance}",
232
515
  file=sys.stderr)
233
516
  print(path)
234
517
  return 0
@@ -142,7 +142,13 @@ if [[ "$RC" -eq 142 || "$RC" -eq 124 ]]; then
142
142
  thread_id="$(head -1 "${OUTPUT_FILE}.progress.log" 2>/dev/null |
143
143
  sed -n 's/.*"thread_id":"\([^"]*\)".*/\1/p')"
144
144
  if [[ -n "$thread_id" ]]; then
145
- rollout="$(find "${CODEX_HOME:-$HOME/.codex}/sessions" -name "rollout-*-${thread_id}.jsonl" 2>/dev/null | head -1)"
145
+ # A resumed thread writes rollout-*-<thread>_<session>.jsonl, so match both names and keep the newest.
146
+ rollout=""
147
+ while IFS= read -r candidate; do
148
+ [[ -z "$candidate" ]] && continue
149
+ if [[ -z "$rollout" || "$candidate" -nt "$rollout" ]]; then rollout="$candidate"; fi
150
+ done < <(find "${CODEX_HOME:-$HOME/.codex}/sessions" \
151
+ \( -name "rollout-*-${thread_id}.jsonl" -o -name "rollout-*-${thread_id}_*.jsonl" \) 2>/dev/null)
146
152
  echo "omnilane: rollout: ${rollout:-not found (thread ${thread_id})}"
147
153
  else
148
154
  echo "omnilane: rollout: unknown — no thread_id in the progress log, so codex died before its first event"
@@ -19,13 +19,14 @@ You (the main loop) may be Claude, GPT, Grok, or Gemini. The procedure is identi
19
19
  Read-only work uses advise; edits require `--mode work --workdir <repo>`.
20
20
  `<repo>/scripts/dispatch.sh --caller-context FILE [--executor auto|native|cli] [--native-context FILE] [--vendor V] [--mode work] [--workdir DIR] <lane> "<task>"`
21
21
 
22
- A model caller needs a caller identity. Dispatch reads it from the CLI that
23
- launched you when that CLI names its model and effort, so an ordinary session
24
- passes nothing. When it cannot, dispatch is refused with
25
- `missing-caller-context` before a job exists: run `omnilane whoami` (or
26
- `<repo>/bin/omnilane whoami`) and pass the file it prints as
27
- `--caller-context`. See **Frozen exact-AA downward gate** for the schema and
28
- what to do when your effort is genuinely unverifiable.
22
+ A model caller needs a verifiable caller identity. Codex app-server reads the
23
+ current turn from its rollout; other launches read the model and effort from
24
+ the CLI that launched you, so an ordinary session passes nothing. When that
25
+ identity cannot be read, dispatch is refused with `missing-caller-context`
26
+ before a job exists: run `omnilane whoami` (or `<repo>/bin/omnilane whoami`)
27
+ and pass the file it prints as `--caller-context`. See **Frozen exact-AA
28
+ downward gate** for the schema and what to do when effort is genuinely
29
+ unverifiable.
29
30
 
30
31
  Add `--background` for long tasks; poll with `scripts/jobs.sh status|result <id>`.
31
32
  Use `--thread NAME` when later claude, codex, grok or gemini dispatches
@@ -358,21 +359,35 @@ and `snapshot_id` must equal the registry's own `snapshot.id`.
358
359
  Set `inherited_ceiling` to your own row's score when you are the root caller, or
359
360
  to the ceiling you were handed when you are a child.
360
361
 
361
- **Your identity is read from the CLI that launched you.** When no
362
- `--caller-context` is given, dispatch walks up the process tree to the nearest
363
- vendor CLI and reads the selector it was started with: `--model` / `--effort`
364
- for claude, `-m` and `-c model_reasoning_effort` for codex, `--reasoning-effort`
365
- for grok, the effort-encoded model id for agy. `omnilane whoami` runs the same
366
- walk and prints the resulting caller-context file, for a retry or to see what
367
- you will be held to. Nearest wins, so a codex worker started by a Claude session
368
- is a codex caller, and another session of the same CLI elsewhere on the host is
369
- never consulted. It does not guess: a missing flag, a model alias, or a Claude
370
- effort whose only scored row is non-reasoning is refused with the reason. This
371
- is request-selector evidence, the same class the transport overlay carries; it
372
- does not certify upstream identity, but unlike a hand-written file the model
373
- cannot edit it. An explicit `--caller-context` or `--operator-asserted-human`
374
- still wins, and `OMNILANE_AA_CALLER_FROM_PROCESS=0` restores the file-only
375
- contract.
362
+ **Your identity is read from the CLI that launched you.** Without explicit or
363
+ inherited caller-context, dispatch finds the nearest vendor CLI. Claude, Grok
364
+ and Agy retain their launch selectors. Codex outside app-server retains explicit
365
+ `-m` / `--model` or `-c model=...` / `--config` selectors; TOML model overrides
366
+ require Python 3.11+. A profile is not an explicit model selector.
367
+
368
+ Codex app-server always ignores startup selectors, even explicit model flags;
369
+ other Codex launches without a model use the same current-turn rollout reader.
370
+ It requires matching UUID-shaped `CODEX_THREAD_ID` values in this process and the
371
+ codex direct child's initial environment (not text embedded in argv). Exactly
372
+ one active rollout under `$CODEX_HOME/sessions` (default `~/.codex`) must have
373
+ matching `session_meta.id`. The last `turn_context` must provide non-empty model,
374
+ effort and turn id, with no later `task_complete`, `turn_complete` or
375
+ `turn_aborted` event.
376
+ Missing, ambiguous, malformed or stale evidence refuses: no config defaults,
377
+ model-list, archive or other-thread fallback. JSONL is streamed; only identity
378
+ metadata and event types reach diagnostics, never message content.
379
+
380
+ `omnilane whoami` prints the resulting caller-context path and reports thread
381
+ and turn ids for rollout evidence. The existing scored-row resolver still makes
382
+ the decision; this is host request-selector evidence, not proof of upstream
383
+ provider identity. Nearest wins, including workers launched by another vendor.
384
+ Explicit `--caller-context`, inherited identity and `--operator-asserted-human`
385
+ retain their precedence; `OMNILANE_AA_CALLER_FROM_PROCESS=0` disables both readers.
386
+
387
+ Ancestor lookup still runs first. If it fails under `CODEX_SANDBOX=seatbelt`,
388
+ `whoami` explains that process inspection, `~/.omnilane` writes and networking
389
+ require rerunning the command outside the Codex sandbox; do not retry with a
390
+ caller-context inside that sandbox.
376
391
 
377
392
  Only when `omnilane whoami` refuses: ask the operator, or declare the
378
393
  lowest-scoring row of your model and say so in your report. Understating only