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