@arbiterforge/ca-pi 0.17.4 → 0.17.6

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/README.md CHANGED
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 2.25.3" src="https://img.shields.io/badge/version-2.25.3-2b7489">
13
+ <img alt="version 2.25.5" src="https://img.shields.io/badge/version-2.25.5-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.17.4",
3
+ "version": "0.17.6",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,27 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.6] - 2026-10-10
8
+
9
+ ### Fixed
10
+
11
+ - Preserve the shared engine's Codex native V2 review bindings and bounded
12
+ verification context storage. Pi verification and review authority remain
13
+ unsupported; this shared payload update does not enable those host seams.
14
+
15
+ ## [0.17.5] - 2026-10-10
16
+
17
+ ### Fixed
18
+
19
+ - Preserve the shared adapter's bounded native completion-review context
20
+ references and complete review-result prompt contract (#921).
21
+ - Fingerprint complete completion-review workspaces up to 1 GiB independently
22
+ of the 256 MiB selected verification-input budget. Preserve full content
23
+ hashing, freshness checks, and the 32 MiB member and 50,000 entry limits.
24
+ - Preserve the shared engine's distinct fresh-verification completion profile
25
+ and historical receipt validation. Pi verification and review authority remain
26
+ unsupported; this shared payload change does not enable those host seams.
27
+
7
28
  ## [0.17.4] - 2026-10-09
8
29
 
9
30
  ### Changed
@@ -6,43 +6,43 @@
6
6
  "binaries": {
7
7
  "darwin/amd64": {
8
8
  "file": "ca-artifact-darwin-amd64",
9
- "sha256": "433780fa59913512346069f9e1891e306a8bec41ed6fcc7cd26b50748d03ae06",
9
+ "sha256": "7b9e95dba78bda2962d1b879c373a835466fe59aa082a3afb6efbc1fb3de454d",
10
10
  "native_tested": true
11
11
  },
12
12
  "darwin/arm64": {
13
13
  "file": "ca-artifact-darwin-arm64",
14
- "sha256": "b68192c7a1da2cd2f9031f71ab4c7e015673f060d6bfdfa3aa4b57d9f090bf34",
14
+ "sha256": "2469bd38df4e241f19bb443c4388283aaadc0638951a77331b25be81929d4f4c",
15
15
  "native_tested": true
16
16
  },
17
17
  "linux/amd64": {
18
18
  "file": "ca-artifact-linux-amd64",
19
- "sha256": "09582eabdef3f828a12de21b85ccc7b52a188651c2a4f69f3f776a2e62c86036",
19
+ "sha256": "cf4116c08afa66426c3b9d78853dfc09b608e05412054b6fe18817a65db96a76",
20
20
  "native_tested": true
21
21
  },
22
22
  "linux/arm64": {
23
23
  "file": "ca-artifact-linux-arm64",
24
- "sha256": "f7699ce8f2f07200ea8508f21a6d369506443392aa8897d84e9d5e7dd6469770",
24
+ "sha256": "b1e92f0d279fe04b61dd287a4b20fc1db4abc3722c0325f5294a55bb1a972895",
25
25
  "native_tested": true
26
26
  },
27
27
  "windows/amd64": {
28
28
  "file": "ca-artifact-windows-amd64.exe",
29
- "sha256": "9a0f186e2a1bcfd92c8f9db5cb0aa2f6dfb128dd6f0dad2708a78596f0364e16",
29
+ "sha256": "d8fa783dc8d30c5d48a8c336bca8ee5fd85be4c72629d7ac7246842b89c5135c",
30
30
  "native_tested": true
31
31
  },
32
32
  "windows/arm64": {
33
33
  "file": "ca-artifact-windows-arm64.exe",
34
- "sha256": "3f1bc7d00f9488b4a4a9f21e5ca414fd5c97031ad7b1c2b52c02f5d7f2956e3a",
34
+ "sha256": "2df30c81eb58d502aa9a146563b9dd725176cac9f907ebf36b73c157e5de2124",
35
35
  "native_tested": true
36
36
  }
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "6a243348a0e74b0a50c37eccf4d61520129a5c1c",
40
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "37938394077",
43
- "binary_sha256": "433780fa59913512346069f9e1891e306a8bec41ed6fcc7cd26b50748d03ae06",
44
- "observation_sha256": "017903dc1173484279e8029e5edee028cd81d1eb71c60e706b0476d7ea1d0e42",
45
- "module_sha256": "421b4db5fa1afc9a8620c5538161d8b23f193d6a1042ad1c5aa004facdd77844",
42
+ "run_id": "38042731230",
43
+ "binary_sha256": "7b9e95dba78bda2962d1b879c373a835466fe59aa082a3afb6efbc1fb3de454d",
44
+ "observation_sha256": "c359781d36118baf2ebe6b73bb0ecefb59f4ca500b31a6f7e5baf52474e1127c",
45
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
46
46
  "native_cases": [
47
47
  "TestContextBoundedRender",
48
48
  "TestContextDocumentContract",
@@ -53,12 +53,12 @@
53
53
  ]
54
54
  },
55
55
  "darwin/arm64": {
56
- "source_commit": "6a243348a0e74b0a50c37eccf4d61520129a5c1c",
56
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "37938394077",
59
- "binary_sha256": "b68192c7a1da2cd2f9031f71ab4c7e015673f060d6bfdfa3aa4b57d9f090bf34",
60
- "observation_sha256": "28682b5ece3e695125587ff8578d22ce5ccd75f6643f626b64246bc7dd664716",
61
- "module_sha256": "421b4db5fa1afc9a8620c5538161d8b23f193d6a1042ad1c5aa004facdd77844",
58
+ "run_id": "38042731230",
59
+ "binary_sha256": "2469bd38df4e241f19bb443c4388283aaadc0638951a77331b25be81929d4f4c",
60
+ "observation_sha256": "2d4c75ec50210c0bbf4b8c9a754a0fd89ee81dc60c9607066cf17c4e1af2050a",
61
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
62
62
  "native_cases": [
63
63
  "TestContextBoundedRender",
64
64
  "TestContextDocumentContract",
@@ -69,12 +69,12 @@
69
69
  ]
70
70
  },
71
71
  "linux/amd64": {
72
- "source_commit": "6a243348a0e74b0a50c37eccf4d61520129a5c1c",
72
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "37938394077",
75
- "binary_sha256": "09582eabdef3f828a12de21b85ccc7b52a188651c2a4f69f3f776a2e62c86036",
76
- "observation_sha256": "0782dc40b8b03584b6f048204234fbf3896e0ee33ebfb52da4b72237d18679a6",
77
- "module_sha256": "421b4db5fa1afc9a8620c5538161d8b23f193d6a1042ad1c5aa004facdd77844",
74
+ "run_id": "38042731230",
75
+ "binary_sha256": "cf4116c08afa66426c3b9d78853dfc09b608e05412054b6fe18817a65db96a76",
76
+ "observation_sha256": "0a57cde8e4f251ee5faf2cd2dc256638622464030cda79988c11295dec6dfec9",
77
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
78
78
  "native_cases": [
79
79
  "TestContextBoundedRender",
80
80
  "TestContextDocumentContract",
@@ -85,12 +85,12 @@
85
85
  ]
86
86
  },
87
87
  "linux/arm64": {
88
- "source_commit": "6a243348a0e74b0a50c37eccf4d61520129a5c1c",
88
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "37938394077",
91
- "binary_sha256": "f7699ce8f2f07200ea8508f21a6d369506443392aa8897d84e9d5e7dd6469770",
92
- "observation_sha256": "451808d71946bed78727672d0c8ed94903eb9179a3907dd86248c1885eb7b094",
93
- "module_sha256": "421b4db5fa1afc9a8620c5538161d8b23f193d6a1042ad1c5aa004facdd77844",
90
+ "run_id": "38042731230",
91
+ "binary_sha256": "b1e92f0d279fe04b61dd287a4b20fc1db4abc3722c0325f5294a55bb1a972895",
92
+ "observation_sha256": "926d97ef85d2cd9caa658db620c7f135ea5a07873a188f289181a37ae636921d",
93
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
94
94
  "native_cases": [
95
95
  "TestContextBoundedRender",
96
96
  "TestContextDocumentContract",
@@ -101,12 +101,12 @@
101
101
  ]
102
102
  },
103
103
  "windows/amd64": {
104
- "source_commit": "6a243348a0e74b0a50c37eccf4d61520129a5c1c",
104
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "37938394077",
107
- "binary_sha256": "9a0f186e2a1bcfd92c8f9db5cb0aa2f6dfb128dd6f0dad2708a78596f0364e16",
108
- "observation_sha256": "336b9ea6f4b53b2aaa98cf5ee47f7d2792e2ece09eaafef8f37b0ae51c222275",
109
- "module_sha256": "421b4db5fa1afc9a8620c5538161d8b23f193d6a1042ad1c5aa004facdd77844",
106
+ "run_id": "38042731230",
107
+ "binary_sha256": "d8fa783dc8d30c5d48a8c336bca8ee5fd85be4c72629d7ac7246842b89c5135c",
108
+ "observation_sha256": "70827706a7d231df640f9d4b27b4250f81311bfd89bee11b65ff48b985c1085c",
109
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
110
110
  "native_cases": [
111
111
  "TestContextBoundedRender",
112
112
  "TestContextDocumentContract",
@@ -117,12 +117,12 @@
117
117
  ]
118
118
  },
119
119
  "windows/arm64": {
120
- "source_commit": "6a243348a0e74b0a50c37eccf4d61520129a5c1c",
120
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "37938394077",
123
- "binary_sha256": "3f1bc7d00f9488b4a4a9f21e5ca414fd5c97031ad7b1c2b52c02f5d7f2956e3a",
124
- "observation_sha256": "817414c35d85c544d9554544af86fc14aa11f866f0df0f6fe28f64adda5bfe1a",
125
- "module_sha256": "421b4db5fa1afc9a8620c5538161d8b23f193d6a1042ad1c5aa004facdd77844",
122
+ "run_id": "38042731230",
123
+ "binary_sha256": "2df30c81eb58d502aa9a146563b9dd725176cac9f907ebf36b73c157e5de2124",
124
+ "observation_sha256": "f15d9d9894d0ae845e14c3bfba1cf1528f1debe166850e9fb0b46aee27d99dbd",
125
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
126
126
  "native_cases": [
127
127
  "TestContextBoundedRender",
128
128
  "TestContextDocumentContract",
@@ -82,6 +82,18 @@ REQUEST_LOCK_WAIT_SECONDS = 5.0
82
82
  CLAUDE_JOIN_LOCK_WAIT_SECONDS = 30.0
83
83
  MAX_OUTPUT = 8 << 20
84
84
  MAX_DECISION = 65536
85
+ # Verification and native completion reviews retain the complete immutable context by
86
+ # exact reference instead of duplicating it in every mutable request state.
87
+ CONTEXT_STORAGE = "frozen-reference/0.1.0"
88
+ CONTEXT_REFERENCE = {"format": "codearbiter.evidence-context-reference/0.1.0"}
89
+ # The native canonical decoder already bounds each complete context at 8 MiB.
90
+ MAX_EVIDENCE_CONTEXT = 8 << 20
91
+ # Frozen evidence reads can outlast the ordinary client's 30-second budget.
92
+ EVIDENCE_ENGINE_TIMEOUT_SECONDS = 120
93
+ # ASCII canonical JSON can expand each input UTF-8 byte to at most six bytes.
94
+ # Reserve that closed decision budget plus 32 bounded 256-byte lifecycle slots
95
+ # (host identities, content-addressed locators, their keys and delimiters).
96
+ REVIEW_RESULT_RESERVE = 6 * MAX_DECISION + 32 * 256
85
97
  MAX_COMPLETION_FILES = 16
86
98
  MAX_COMPLETION_FILE_BYTES = 64 << 10
87
99
  MAX_COMPLETION_TOTAL_BYTES = 256 << 10
@@ -104,9 +116,13 @@ CLAUDE_REVIEWER_MODELS = frozenset({"opus", "sonnet", "haiku"})
104
116
  CLAUDE_REVIEW_PROFILE = "claude-review/0.1.0"
105
117
  CODEX_REVIEW_PROFILE = "codex-review/0.1.0"
106
118
  CODEX_NATIVE_V1 = "codex-native-v1/0.145.0"
119
+ CODEX_NATIVE_V2 = "codex-native-v2/0.162.0-alpha.2"
120
+ CODEX_NATIVE_PROFILES = frozenset({CODEX_NATIVE_V1, CODEX_NATIVE_V2})
107
121
  CODEX_UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
122
+ CODEX_AGENT_PATH_RE = re.compile(r"/root(?:/[a-z0-9_]+)*")
108
123
  CODEX_STEERING_TOOLS = frozenset({
109
124
  "multi_agent_v1send_input", "multi_agent_v1resume_agent", "multi_agent_v1close_agent",
125
+ "collaborationsend_message", "collaborationfollowup_task", "collaborationinterrupt_agent",
110
126
  })
111
127
  CODEX_REVIEW_TOOLS = frozenset({"spawn_agent", "collaborationspawn_agent"}) | CODEX_STEERING_TOOLS
112
128
  CODEX_CHILD_FORMAT = "codearbiter.codex-native-child/0.1.0"
@@ -404,9 +420,129 @@ def _request_lock(spool: Path, relative: Path, wait: float | None = None):
404
420
  # Do not unlink: a peer may already be waiting on this same inode.
405
421
 
406
422
 
423
+ def _referenced_review_state(value: dict[str, Any]) -> dict[str, Any]:
424
+ if (value.get("context_storage") != CONTEXT_STORAGE
425
+ or value.get("codex_review_profile") not in CODEX_NATIVE_PROFILES
426
+ or value.get("host", "codex") != "codex"
427
+ or value.get("activity") not in REVIEW_ACTIVITIES
428
+ or "completion" not in value["context"] or "handback" in value):
429
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "context reference is not a native completion review")
430
+ base = dict(value, state="ARMED", launch=None, observation_ref=None,
431
+ observation_sha256=None, receipt=None, recovery=None)
432
+ for field in ("payload", "authority_source"):
433
+ base.pop(field, None)
434
+ base["integrity_sha256"] = "0" * 64
435
+ stored_base = dict(base, context=CONTEXT_REFERENCE)
436
+ context = value["context"]
437
+ # Quality review additionally copies its exact task hashes into the result.
438
+ static_payload = {"input_sha256": context["input_sha256"],
439
+ "spec_sha256": context["spec_sha256"],
440
+ "completion_sha256": context["completion_sha256"]}
441
+ if value["activity"] == "spec_review":
442
+ static_payload["task_sha256"] = context["task_sha256"]
443
+ else:
444
+ static_payload.update(base_input_sha256=context["base_input_sha256"],
445
+ task_hashes=context["task_hashes"])
446
+ if len(_canonical(stored_base)) + len(_canonical(static_payload)) + REVIEW_RESULT_RESERVE > MAX_STATE:
447
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "review result has no bounded lifecycle reserve")
448
+ return dict(value, context=CONTEXT_REFERENCE)
449
+
450
+
451
+ def _valid_verification_binding_values(binding: dict[str, Any]) -> bool:
452
+ def path(value: Any) -> bool:
453
+ return isinstance(value, str) and bool(value) and "\0" not in value and Path(value).is_absolute()
454
+
455
+ def identity(value: Any) -> bool:
456
+ return isinstance(value, str) and re.fullmatch(r"[0-9]+:[0-9]+", value) is not None
457
+
458
+ def digest(value: Any) -> bool:
459
+ return isinstance(value, str) and SHA256_RE.fullmatch(value) is not None
460
+
461
+ argv, files, collector = binding["argv"], binding["launch_files"], binding["collector_profile"]
462
+ if (not isinstance(argv, list) or not argv
463
+ or any(not isinstance(item, str) or not item or "\0" in item for item in argv)
464
+ or not path(argv[0])
465
+ or not isinstance(collector, str) or collector not in COLLECTORS
466
+ or any(not path(binding[field]) for field in ("cwd", "workspace_root", "git_common_dir"))
467
+ or any(not identity(binding[field]) for field in (
468
+ "cwd_filesystem_id", "workspace_filesystem_id", "git_common_filesystem_id"))
469
+ or any(not digest(binding[field]) for field in ("definition_sha256", "executable_sha256"))
470
+ or not isinstance(files, list) or not files):
471
+ return False
472
+ # These are the roles emitted by _native_launch, _bind_commands and its
473
+ # nested npm qualifier. Validate serialization without replacing rebinding.
474
+ roles = {"declared-executable", "node-runtime", "npm-cli", "runner-entrypoint",
475
+ "npm-manifest", "npm-workspace-manifest", "npm-prefix"}
476
+ return all(isinstance(item, dict) and set(item) == {"path", "sha256", "filesystem_id", "role"}
477
+ and path(item["path"]) and digest(item["sha256"]) and identity(item["filesystem_id"])
478
+ and isinstance(item["role"], str) and item["role"] in roles for item in files)
479
+
480
+
481
+ def _referenced_verification_state(value: dict[str, Any]) -> dict[str, Any]:
482
+ context = value["context"]
483
+ subject = context.get("subject", {})
484
+ _validate_context(context, subject.get("artifact_id"), subject.get("record_id"), "verification")
485
+ binding_fields = {"definition_sha256", "argv", "collector_profile", "launch_files", "cwd",
486
+ "cwd_filesystem_id", "workspace_root", "workspace_filesystem_id", "git_common_dir",
487
+ "git_common_filesystem_id", "executable_sha256"}
488
+ if (value.get("context_storage") != CONTEXT_STORAGE
489
+ or value.get("activity") != "verification"
490
+ or value.get("host", "codex") not in HOSTS
491
+ or value.get("state") not in STATES
492
+ or value.get("launch") is not None
493
+ or any(field in value for field in (
494
+ "codex_review_profile", "handback", "dispatch_prompt", "review_contract_sha256",
495
+ "required_coverage", "launch_envelope"))
496
+ or not isinstance(value.get("command_bindings"), list)
497
+ or len(value["command_bindings"]) != len(context.get("commands", []))
498
+ or any(not isinstance(binding, dict)
499
+ or set(binding) != binding_fields
500
+ or not _valid_verification_binding_values(binding)
501
+ or binding.get("definition_sha256") != command["definition_sha256"]
502
+ for binding, command in zip(value["command_bindings"], context.get("commands", [])))):
503
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "context reference is not closed verification")
504
+ # Reserve the exact closed result (including every required test name), a
505
+ # wrapper with the bound commands/workspaces, and bounded host/lifecycle IDs.
506
+ # Large manifests stay immutable; large mutable results must still refuse.
507
+ commands = [{
508
+ "definition_sha256": command["definition_sha256"], "exit": command["definition"]["expected_exit"],
509
+ "tests": [{"name": name, "status": "pass"} for name in command["definition"]["required_tests"]],
510
+ "stdout_sha256": "0" * 64, "stderr_sha256": "0" * 64,
511
+ } for command in context["commands"]]
512
+ workspaces = {binding["workspace_root"]: {
513
+ "root": binding["workspace_root"], "filesystem_id": binding["workspace_filesystem_id"],
514
+ "git_common_dir": binding["git_common_dir"], "git_common_filesystem_id": binding["git_common_filesystem_id"],
515
+ "head": "0" * 64, "status_sha256": "0" * 64, "content_sha256": "0" * 64,
516
+ } for binding in value["command_bindings"]}
517
+ base = dict(value, context=CONTEXT_REFERENCE, state="CAPTURED", attempt="0" * 32,
518
+ observation_ref=None, observation_sha256=None, receipt=None, recovery=None)
519
+ base.pop("authority_source", None)
520
+ base["integrity_sha256"] = "0" * 64
521
+ base["payload"] = {field: context[field] for field in ("input_sha256", "spec_sha256", "task_sha256")}
522
+ base["payload"]["commands"] = commands
523
+ base["wrapper"] = {
524
+ "state": "CORROBORATED", "session_id": "s" * 256, "tool_use_id": "t" * 256,
525
+ "prompt_id" if value.get("host", "codex") == "claude" else "turn_id": "p" * 256,
526
+ "command_bindings": value["command_bindings"], "workspace_before": list(workspaces.values()),
527
+ }
528
+ if len(_canonical(base)) + 32 * 256 > MAX_STATE:
529
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "verification result has no bounded lifecycle reserve")
530
+ return dict(value, context=CONTEXT_REFERENCE)
531
+
532
+
533
+ def _referenced_state(value: dict[str, Any]) -> dict[str, Any]:
534
+ if value.get("activity") == "verification":
535
+ return _referenced_verification_state(value)
536
+ return _referenced_review_state(value)
537
+
538
+
407
539
  def _save(root: Path, value: dict[str, Any]) -> None:
408
540
  expected = value.get("integrity_sha256")
409
541
  updated = dict(value)
542
+ if "context_storage" in updated:
543
+ if _read_context(root, updated["context_ref"], updated["context_sha256"]) != updated["context"]:
544
+ raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "stored context changed")
545
+ updated = _referenced_state(updated)
410
546
  updated["integrity_sha256"] = _integrity(updated)
411
547
  legacy_abandonment = (
412
548
  value["state"] == "ABANDONED"
@@ -468,7 +604,7 @@ def _load(
468
604
  "authority_source", "dispatch_prompt", "review_contract_sha256",
469
605
  "required_coverage", "wrapper", "command_bindings", "recovery",
470
606
  "launch_envelope", "workspace_roots", "host", "codex_review_profile",
471
- "handback",
607
+ "handback", "context_storage", "codex_native_trace",
472
608
  }
473
609
  if (
474
610
  not isinstance(value, dict)
@@ -491,11 +627,21 @@ def _load(
491
627
  ):
492
628
  raise AuthorityError("INVALID_AUTHORITY_STATE", "request state failed validation")
493
629
  if "codex_review_profile" in value and (
494
- value["codex_review_profile"] != CODEX_NATIVE_V1
630
+ value["codex_review_profile"] not in CODEX_NATIVE_PROFILES
495
631
  or value.get("host", "codex") != "codex"
496
632
  or value["activity"] not in REVIEW_ACTIVITIES
497
633
  ):
498
634
  raise AuthorityError("INVALID_AUTHORITY_STATE", "Codex review profile is unsupported")
635
+ if "context_storage" in value:
636
+ if value["context"] != CONTEXT_REFERENCE:
637
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "stored context reference is malformed")
638
+ value["context"] = _read_context(root, value["context_ref"], value["context_sha256"])
639
+ subject = value["context"].get("subject", {})
640
+ _validate_context(value["context"], subject.get("artifact_id"),
641
+ subject.get("record_id"), value["activity"])
642
+ _referenced_state(value)
643
+ elif value["context"] == CONTEXT_REFERENCE:
644
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "context reference lacks its closed storage profile")
499
645
  if len(raw) > MAX_STATE:
500
646
  if recover_armed_review and value["state"] == "ABANDONED":
501
647
  # Only an abandonment can have retained this; recovering it again is
@@ -526,15 +672,25 @@ def _validate_hash(value: Any, field: str) -> str:
526
672
  return value
527
673
 
528
674
 
529
- def _read_context(root: Path, context_ref: str, context_hash: str) -> dict[str, Any]:
675
+ def _read_context(
676
+ root: Path, context_ref: str, context_hash: str
677
+ ) -> dict[str, Any]:
530
678
  _validate_hash(context_hash, "context_sha256")
531
679
  expected_ref = f".codearbiter/.artifacts/evidence-contexts/{context_hash}.json"
532
680
  if context_ref != expected_ref:
533
681
  raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context locator is not content addressed")
534
682
  try:
535
- raw = (root / expected_ref).read_bytes()
683
+ path = root / expected_ref
684
+ _safe_directory(root, Path(expected_ref).parent, create=False)
685
+ _path_identity(path)
686
+ if not stat.S_ISREG(path.lstat().st_mode):
687
+ raise ValueError("context is not a regular file")
688
+ with path.open("rb") as stream:
689
+ raw = stream.read(MAX_EVIDENCE_CONTEXT + 1)
690
+ if len(raw) > MAX_EVIDENCE_CONTEXT:
691
+ raise ValueError("context exceeds the native canonical budget")
536
692
  context = json.loads(raw.decode("utf-8"))
537
- except (OSError, UnicodeError, ValueError) as exc:
693
+ except (OSError, UnicodeError, ValueError, AuthorityError) as exc:
538
694
  raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes are unavailable") from exc
539
695
  if not isinstance(context, dict) or _digest(raw) != context_hash or raw != _native_context_canonical(context):
540
696
  raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes changed or are not canonical")
@@ -567,7 +723,10 @@ def _completion_client(root: Path) -> Any:
567
723
  """Use the native engine shipped with this adapter, never PATH or a caller."""
568
724
  import _artifactlib
569
725
 
570
- return _artifactlib.ArtifactClient(root, _artifactlib.helper_installation(__file__))
726
+ return _artifactlib.ArtifactClient(
727
+ root, _artifactlib.helper_installation(__file__),
728
+ timeout=EVIDENCE_ENGINE_TIMEOUT_SECONDS,
729
+ )
571
730
 
572
731
 
573
732
  def _context(
@@ -1020,6 +1179,7 @@ def _reject_overlapping_authority(session_id: str, turn_id: str) -> None:
1020
1179
  launch = request.get("launch") or {}
1021
1180
  if (
1022
1181
  request["state"] in {"LAUNCHING", "RUNNING"}
1182
+ and request.get("codex_review_profile") != CODEX_NATIVE_V2
1023
1183
  and launch.get("parent_session_id") == session_id
1024
1184
  and launch.get("parent_turn_id") == turn_id
1025
1185
  ):
@@ -1340,7 +1500,12 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1340
1500
  "these settings do not provide an OS sandbox.\n"
1341
1501
  if request.get("codex_review_profile") == CODEX_NATIVE_V1 else ""
1342
1502
  )
1503
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
1504
+ profile = (f"Native review profile: {CODEX_NATIVE_V2}. This review was dispatched through "
1505
+ "collaboration.spawn_agent with default role and fork_turns=none. Read-only conduct "
1506
+ "is cooperative; these settings do not provide an OS sandbox.\n")
1343
1507
  completion = ""
1508
+ completion_fields = ""
1344
1509
  if "completion" in context:
1345
1510
  completion = (
1346
1511
  "This explicit completion review assesses performed work against every selected task's "
@@ -1366,11 +1531,28 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1366
1531
  "done_when:<1-based index>. Each object has task_id, obligation, status (substantiated or "
1367
1532
  "missing), assessment (nonempty reasons with concrete source locations, including file paths "
1368
1533
  "and lines), and evidence_refs (nonempty unique exact selected receipt/source/event/observation/"
1369
- "context refs, mapped workspace_root, or material source_path). Reference the selected facts "
1534
+ f"context refs, {'mapped workspace_root' if legacy_inline else 'raw workspace_root path'}, "
1535
+ "or material source_path). Reference the selected facts "
1370
1536
  "that support each conclusion. Missing or unreadable facts require status=missing and "
1371
1537
  "decision=changes_requested. A passing decision requires every completion obligation "
1372
1538
  "substantiated; a claimed coverage list is insufficient.\n"
1539
+ "Separate required task/obligation pairs: "
1540
+ + json.dumps(sorted(_completion_obligations(context)), ensure_ascii=True) + ".\n"
1373
1541
  )
1542
+ completion_fields = ", completion_sha256 (exact selected packet hash), completion_assessment (separate obligation rows)"
1543
+ if not legacy_inline:
1544
+ references = {task_id: sorted(refs) for task_id, refs in _completion_references(context).items()}
1545
+ completion += (
1546
+ "Exact allowed evidence_refs by task_id (JSON):\n"
1547
+ + _canonical(references).decode("ascii") + "\n"
1548
+ "Copy decoded JSON string values exactly into evidence_refs; choose only entries for "
1549
+ "that row's task_id, never another task's references. Do not add labels or prefixes, "
1550
+ "line numbers, descriptions, or alter path spelling. A raw workspace_root path is the "
1551
+ "listed path itself, without a mapped workspace_root: prefix. The verification-context "
1552
+ "refs listed here belong to selected published verifications; the current review container "
1553
+ "shown as Frozen context above is not an allowed evidence ref unless listed for that task. "
1554
+ "Put concrete source locations and lines in assessment, not in evidence_refs.\n"
1555
+ )
1374
1556
  return (
1375
1557
  f"[CODEARBITER_AUTHORITY_REQUEST:{request['request_id']}]\n"
1376
1558
  f"Target repository: {request['repository']['path']}\n"
@@ -1384,7 +1566,8 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1384
1566
  + json.dumps(request["required_coverage"], ensure_ascii=True)
1385
1567
  + ". Fields: format, request_id, target_sha256, contract_sha256, decision "
1386
1568
  "(pass or changes_requested), coverage (unique strings), findings (objects with "
1387
- "severity, code, message), assessment (non-empty string). Reply with the JSON "
1569
+ "severity, code, message), assessment (non-empty string)"
1570
+ + completion_fields + ". Reply with the JSON "
1388
1571
  "object only: no code fence and no other text."
1389
1572
  )
1390
1573
 
@@ -1417,7 +1600,7 @@ def arm_request(
1417
1600
  if host == "claude" and reviewer_model not in CLAUDE_REVIEWER_MODELS:
1418
1601
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "reviewer model is unsupported")
1419
1602
  if codex_review_profile is not None and (
1420
- codex_review_profile != "native-v1" or host != "codex" or activity not in REVIEW_ACTIVITIES
1603
+ codex_review_profile not in {"native-v1", "native-v2"} or host != "codex" or activity not in REVIEW_ACTIVITIES
1421
1604
  ):
1422
1605
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "Codex review profile is unsupported")
1423
1606
  completion_selection = None
@@ -1466,8 +1649,12 @@ def arm_request(
1466
1649
  }
1467
1650
  if host != "codex":
1468
1651
  seed_value["host"] = host
1652
+ if activity == "verification":
1653
+ seed_value["context_storage"] = CONTEXT_STORAGE
1469
1654
  if codex_review_profile is not None:
1470
- seed_value["codex_review_profile"] = CODEX_NATIVE_V1
1655
+ seed_value["codex_review_profile"] = CODEX_NATIVE_V1 if codex_review_profile == "native-v1" else CODEX_NATIVE_V2
1656
+ if completion_selection is not None:
1657
+ seed_value["context_storage"] = CONTEXT_STORAGE
1471
1658
  seed = _canonical(seed_value)
1472
1659
  request_id = _digest(seed)
1473
1660
  request = {
@@ -1491,8 +1678,12 @@ def arm_request(
1491
1678
  }
1492
1679
  if host != "codex":
1493
1680
  request["host"] = host
1681
+ if activity == "verification":
1682
+ request["context_storage"] = CONTEXT_STORAGE
1494
1683
  if codex_review_profile is not None:
1495
- request["codex_review_profile"] = CODEX_NATIVE_V1
1684
+ request["codex_review_profile"] = seed_value["codex_review_profile"]
1685
+ if completion_selection is not None:
1686
+ request["context_storage"] = CONTEXT_STORAGE
1496
1687
  if activity in REVIEW_ACTIVITIES:
1497
1688
  request["review_contract_sha256"], request["required_coverage"] = _review_binding(context)
1498
1689
  request["dispatch_prompt"] = _dispatch_prompt(request)
@@ -1503,7 +1694,7 @@ def arm_request(
1503
1694
  "subagent_type": CLAUDE_REVIEWER,
1504
1695
  "model": reviewer_model,
1505
1696
  }
1506
- elif codex_review_profile is not None:
1697
+ elif codex_review_profile == "native-v1":
1507
1698
  request["launch_envelope"] = {
1508
1699
  "message": request["dispatch_prompt"], "fork_context": False,
1509
1700
  }
@@ -1526,7 +1717,7 @@ def arm_request(
1526
1717
  result["required_coverage"] = request["required_coverage"]
1527
1718
  result["launch_envelope"] = request["launch_envelope"]
1528
1719
  if codex_review_profile is not None:
1529
- result["codex_review_profile"] = CODEX_NATIVE_V1
1720
+ result["codex_review_profile"] = request["codex_review_profile"]
1530
1721
  if completion_selection is not None:
1531
1722
  result["completion_sha256"] = context["completion_sha256"]
1532
1723
  return result
@@ -2388,6 +2579,24 @@ def run_verification(
2388
2579
  request["context"], request["context_ref"], request["context_sha256"],
2389
2580
  ):
2390
2581
  raise AuthorityError("WORKSPACE_DRIFT", "repository content changed after arm")
2582
+ def normalized_workspace():
2583
+ result = client.call("evidence-context", {
2584
+ "artifact_id": request["context"]["subject"]["artifact_id"],
2585
+ "record_id": request["context"]["subject"]["record_id"],
2586
+ "activity": "verification",
2587
+ "verification_command_bindings": request["command_bindings"],
2588
+ })
2589
+ if (not isinstance(result, dict)
2590
+ or result.get("context_ref") != request["context_ref"]
2591
+ or result.get("context_sha256") != request["context_sha256"]
2592
+ or not isinstance(result.get("completion_workspace_after"), list)
2593
+ or not result["completion_workspace_after"]):
2594
+ raise AuthorityError("WORKSPACE_DRIFT", "engine completion closure is absent or changed context")
2595
+ return result["completion_workspace_after"]
2596
+
2597
+ # The engine alone normalizes validated native bookkeeping. Raw snapshots
2598
+ # still freeze every byte across the actual command execution interval.
2599
+ completion_workspace_before = normalized_workspace()
2391
2600
  workspace_before = _workspace_snapshots(request["command_bindings"])
2392
2601
  if workspace_before != wrapper.get("workspace_before"):
2393
2602
  raise AuthorityError("WORKSPACE_DRIFT", "workspace changed after wrapper authorization")
@@ -2422,6 +2631,9 @@ def run_verification(
2422
2631
  workspace_after = _workspace_snapshots(request["command_bindings"])
2423
2632
  if workspace_after != workspace_before:
2424
2633
  raise AuthorityError("WORKSPACE_DRIFT", "verification changed the frozen workspace")
2634
+ completion_workspace_after = normalized_workspace()
2635
+ if completion_workspace_after != completion_workspace_before:
2636
+ raise AuthorityError("WORKSPACE_DRIFT", "verification changed the normalized completion workspace")
2425
2637
  final_context, final_ref, final_hash = _context(
2426
2638
  root, client, request["context"]["subject"]["artifact_id"],
2427
2639
  request["context"]["subject"]["record_id"], "verification",
@@ -2435,6 +2647,7 @@ def run_verification(
2435
2647
  "command_bindings": request["command_bindings"],
2436
2648
  "workspace_before": workspace_before,
2437
2649
  "workspace_after": workspace_after,
2650
+ "completion_workspace_after": completion_workspace_after,
2438
2651
  "commands": commands,
2439
2652
  }
2440
2653
  payload = {
@@ -2444,7 +2657,7 @@ def run_verification(
2444
2657
  "commands": commands,
2445
2658
  }
2446
2659
  observation = _closed_observation(
2447
- request, payload, "declared-command/0.2.0", request["attempt"],
2660
+ request, payload, "declared-command/0.3.0", request["attempt"],
2448
2661
  producer_result,
2449
2662
  )
2450
2663
  _observation(root, request, observation)
@@ -2730,6 +2943,145 @@ def _codex_uuid(value: Any) -> str:
2730
2943
  return value
2731
2944
 
2732
2945
 
2946
+ @contextmanager
2947
+ def _codex_transcript(path: Any):
2948
+ """Open the hook-owned native file, never a caller-selected linked path."""
2949
+ if not isinstance(path, str) or not Path(path).is_absolute():
2950
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native transcript path must be absolute")
2951
+ target = Path(path)
2952
+ try:
2953
+ for component in (*reversed(target.parents), target):
2954
+ info = component.lstat()
2955
+ if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400:
2956
+ raise OSError("linked transcript component")
2957
+ if not stat.S_ISREG(info.st_mode):
2958
+ raise OSError("nonregular transcript")
2959
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
2960
+ with os.fdopen(os.open(target, flags), "rb") as stream:
2961
+ opened = os.fstat(stream.fileno())
2962
+ if (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino):
2963
+ raise OSError("changed transcript")
2964
+ yield stream, opened
2965
+ except OSError as exc:
2966
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native transcript is unavailable or unsafe") from exc
2967
+
2968
+
2969
+ def _codex_metadata(stream) -> tuple[bytes, dict[str, Any]]:
2970
+ stream.seek(0)
2971
+ raw = stream.readline(MAX_DECISION + 1)
2972
+ try:
2973
+ value = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_json_pairs,
2974
+ parse_constant=_invalid_json_constant)
2975
+ if (len(raw) > MAX_DECISION or not raw.endswith(b"\n") or not isinstance(value, dict)
2976
+ or value.get("type") != "session_meta" or not isinstance(value.get("payload"), dict)):
2977
+ raise ValueError("unsupported metadata")
2978
+ return raw, value["payload"]
2979
+ except (ValueError, UnicodeError, RecursionError) as exc:
2980
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native session metadata is malformed or oversized") from exc
2981
+
2982
+
2983
+ def _codex_v2_trace_anchor(event: dict[str, Any], task_name: str) -> dict[str, Any]:
2984
+ with _codex_transcript(event.get("transcript_path")) as (stream, info):
2985
+ raw, meta = _codex_metadata(stream)
2986
+ thread = _codex_uuid(meta.get("id"))
2987
+ session = meta.get("session_id", thread)
2988
+ parent_path = meta.get("agent_path") or "/root"
2989
+ if (session != event.get("session_id") or not isinstance(parent_path, str)
2990
+ or CODEX_AGENT_PATH_RE.fullmatch(parent_path) is None
2991
+ or not re.fullmatch(r"authority_[0-9a-f]{12}", task_name)):
2992
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent session or task path differs")
2993
+ offset = info.st_size
2994
+ tail_start = max(0, offset - MAX_DECISION)
2995
+ stream.seek(tail_start)
2996
+ tail = stream.read(offset - tail_start)
2997
+ if not tail.endswith(b"\n"):
2998
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript is not materialized")
2999
+ return {"path": event["transcript_path"], "device": info.st_dev, "inode": info.st_ino,
3000
+ "offset": offset, "tail_start": tail_start, "tail_sha256": _digest(tail),
3001
+ "metadata_sha256": _digest(raw), "parent_thread_id": thread,
3002
+ "parent_agent_path": parent_path, "agent_path": parent_path + "/" + task_name}
3003
+
3004
+
3005
+ def _codex_v2_activity(event: dict[str, Any], request: dict[str, Any]) -> dict[str, Any]:
3006
+ anchor = request.get("codex_native_trace")
3007
+ launch = request["launch"]
3008
+ if not isinstance(anchor, dict) or event.get("transcript_path") != anchor.get("path"):
3009
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript locator changed")
3010
+ with _codex_transcript(anchor["path"]) as (stream, info):
3011
+ raw, _ = _codex_metadata(stream)
3012
+ if ((info.st_dev, info.st_ino) != (anchor["device"], anchor["inode"])
3013
+ or info.st_size < anchor["offset"] or _digest(raw) != anchor["metadata_sha256"]):
3014
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript identity changed")
3015
+ stream.seek(anchor["tail_start"])
3016
+ if _digest(stream.read(anchor["offset"] - anchor["tail_start"])) != anchor["tail_sha256"]:
3017
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript prefix changed")
3018
+ stream.seek(anchor["offset"])
3019
+ suffix = stream.read(MAX_OUTPUT + 1)
3020
+ if len(suffix) > MAX_OUTPUT or not suffix.endswith(b"\n"):
3021
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity suffix is absent, incomplete or oversized")
3022
+ matches = []
3023
+ try:
3024
+ # JSONL records end at LF; Unicode line separators are valid string data.
3025
+ for line in suffix.decode("utf-8").split("\n")[:-1]:
3026
+ value = json.loads(line, object_pairs_hook=_unique_json_pairs, parse_constant=_invalid_json_constant)
3027
+ payload = value.get("payload") if isinstance(value, dict) else None
3028
+ item = payload.get("item") if isinstance(payload, dict) else None
3029
+ if (value.get("type") == "event_msg" and isinstance(item, dict)
3030
+ and item.get("type") == "SubAgentActivity" and payload.get("type") == "item_completed"
3031
+ and item.get("id") == launch["tool_use_id"]):
3032
+ matches.append(payload)
3033
+ except (ValueError, UnicodeError, AttributeError, RecursionError) as exc:
3034
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity suffix is not strict JSONL") from exc
3035
+ if len(matches) != 1:
3036
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity for exact call is missing or ambiguous")
3037
+ activity = matches[0]
3038
+ item = activity["item"]
3039
+ if (set(activity) != {"type", "thread_id", "turn_id", "item", "started_at_ms", "completed_at_ms"}
3040
+ or set(item) != {"type", "id", "kind", "agent_thread_id", "agent_path"}
3041
+ or activity["thread_id"] != anchor["parent_thread_id"] or activity["turn_id"] != launch["parent_turn_id"]
3042
+ or item["kind"] != "started" or item["agent_path"] != anchor["agent_path"]
3043
+ or any(type(activity[key]) is not int or activity[key] < 0 for key in ("started_at_ms", "completed_at_ms"))
3044
+ or activity["completed_at_ms"] < activity["started_at_ms"]):
3045
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity binding differs from the exact launch")
3046
+ _codex_uuid(item["agent_thread_id"])
3047
+ return activity
3048
+
3049
+
3050
+ def _codex_v2_child(event: dict[str, Any]) -> dict[str, Any] | None:
3051
+ # Historical V1 lifecycle fixtures/hosts need no native rollout metadata.
3052
+ if event.get("transcript_path") is None:
3053
+ return None
3054
+ try:
3055
+ with _codex_transcript(event["transcript_path"]) as (stream, _):
3056
+ raw, meta = _codex_metadata(stream)
3057
+ except AuthorityError:
3058
+ # A V1 Start did not promise native metadata. V2 binding below requires
3059
+ # qualified metadata; missing/unsafe bytes still cannot pass V2.
3060
+ return None
3061
+ if meta.get("cli_version") != "0.162.0-alpha.2":
3062
+ return None
3063
+ identity = {"cli_version": meta["cli_version"], "thread_id": _codex_uuid(meta.get("id")),
3064
+ "parent_thread_id": _codex_uuid(meta.get("parent_thread_id")),
3065
+ "session_id": _host_id(meta.get("session_id"), "session_id"),
3066
+ "agent_path": meta.get("agent_path"), "metadata_sha256": _digest(raw)}
3067
+ if (identity["thread_id"] != event["agent_id"] or identity["session_id"] != event.get("session_id")
3068
+ or not isinstance(identity["agent_path"], str)
3069
+ or CODEX_AGENT_PATH_RE.fullmatch(identity["agent_path"]) is None):
3070
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native child metadata differs from its lifecycle")
3071
+ return identity
3072
+
3073
+
3074
+ def _codex_v2_bind_child(request: dict[str, Any], child: dict[str, Any]) -> None:
3075
+ identity = child.get("native_v2")
3076
+ launch = request["launch"]
3077
+ if (not isinstance(identity, dict) or identity.get("parent_thread_id") != launch["parent_thread_id"]
3078
+ or identity.get("thread_id") != launch["agent_id"]
3079
+ or identity.get("session_id") != launch["parent_session_id"]
3080
+ or identity.get("agent_path") != launch["task_name"]):
3081
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native V2 child runtime or parent association is unqualified")
3082
+ launch["native_child"] = identity
3083
+
3084
+
2733
3085
  def _codex_native_marker(agent_id: str) -> Path:
2734
3086
  # UUID, not arrival order or session, is the direct host-observed join key.
2735
3087
  # Retaining this marker also prevents reuse across requests and sessions.
@@ -2794,8 +3146,10 @@ def _read_codex_child(path: Path, agent_id: str, session_id: str) -> dict[str, A
2794
3146
  start = value.get("start") if isinstance(value, dict) else None
2795
3147
  if (
2796
3148
  len(raw) > MAX_CHILD_MARKER or not isinstance(value, dict)
2797
- or set(value) != {"format", "agent_id", "session_id", "request_id", "start", "stop_seen",
2798
- "rejected", "integrity_sha256"}
3149
+ or set(value) - {"format", "agent_id", "session_id", "request_id", "start", "stop_seen",
3150
+ "rejected", "integrity_sha256", "native_v2"}
3151
+ or not {"format", "agent_id", "session_id", "request_id", "start", "stop_seen",
3152
+ "rejected", "integrity_sha256"} <= set(value)
2799
3153
  or value.get("format") != CODEX_CHILD_FORMAT or value.get("agent_id") != agent_id
2800
3154
  or not isinstance(value.get("session_id"), str) or not HOST_ID_RE.fullmatch(value["session_id"])
2801
3155
  or (value.get("request_id") is not None and (
@@ -2808,6 +3162,19 @@ def _read_codex_child(path: Path, agent_id: str, session_id: str) -> dict[str, A
2808
3162
  or not CODEX_UUID_RE.fullmatch(start["turn_id"])))
2809
3163
  ):
2810
3164
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child marker failed validation")
3165
+ if "native_v2" in value:
3166
+ identity = value["native_v2"]
3167
+ if (not isinstance(identity, dict) or set(identity) != {
3168
+ "cli_version", "thread_id", "parent_thread_id", "session_id", "agent_path", "metadata_sha256"}
3169
+ or identity.get("cli_version") != "0.162.0-alpha.2"
3170
+ or identity.get("thread_id") != agent_id or identity.get("session_id") != value["session_id"]
3171
+ or not isinstance(identity.get("parent_thread_id"), str)
3172
+ or CODEX_UUID_RE.fullmatch(identity["parent_thread_id"]) is None
3173
+ or not isinstance(identity.get("agent_path"), str)
3174
+ or CODEX_AGENT_PATH_RE.fullmatch(identity["agent_path"]) is None
3175
+ or not isinstance(identity.get("metadata_sha256"), str)
3176
+ or SHA256_RE.fullmatch(identity["metadata_sha256"]) is None):
3177
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native child runtime metadata failed validation")
2811
3178
  return value
2812
3179
 
2813
3180
 
@@ -2846,10 +3213,13 @@ def _codex_native_post(event: dict[str, Any], root: Path, request: dict[str, Any
2846
3213
  _reject(root, current, "invalid-first-native-review-post")
2847
3214
  if isinstance(exc, AuthorityError):
2848
3215
  raise
2849
- raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result is malformed") from exc
3216
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native spawn result is malformed") from exc
2850
3217
 
2851
3218
 
2852
3219
  def _codex_native_post_join(event: dict[str, Any], root: Path, request: dict[str, Any]) -> dict[str, Any]:
3220
+ expected_tool = "collaborationspawn_agent" if request.get("codex_review_profile") == CODEX_NATIVE_V2 else "spawn_agent"
3221
+ if event.get("tool_name") != expected_tool:
3222
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native review PostToolUse tool differs from its launch profile")
2853
3223
  if _canonical(event.get("tool_input")) != _canonical(request["launch_envelope"]):
2854
3224
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native review PostToolUse envelope was changed")
2855
3225
  _require_frozen_context(root, request)
@@ -2860,11 +3230,20 @@ def _codex_native_post_join(event: dict[str, Any], root: Path, request: dict[str
2860
3230
  response = json.loads(raw, object_pairs_hook=_unique_json_pairs, parse_constant=_invalid_json_constant)
2861
3231
  except ValueError as exc:
2862
3232
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result is not strict JSON") from exc
2863
- if (not isinstance(response, dict) or set(response) != {"agent_id", "nickname"}
3233
+ activity = None
3234
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
3235
+ if (not isinstance(response, dict) or set(response) not in ({"task_name"}, {"task_name", "nickname"})
3236
+ or response.get("task_name") != request["codex_native_trace"]["agent_path"]
3237
+ or ("nickname" in response and (not isinstance(response["nickname"], str) or len(response["nickname"]) > 256))):
3238
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v2 spawn result fields or canonical task path differ")
3239
+ activity = _codex_v2_activity(event, request)
3240
+ agent_id = _codex_uuid(activity["item"]["agent_thread_id"])
3241
+ elif (not isinstance(response, dict) or set(response) != {"agent_id", "nickname"}
2864
3242
  or (response["nickname"] is not None and (
2865
3243
  not isinstance(response["nickname"], str) or len(response["nickname"]) > 256))):
2866
3244
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result fields are unsupported")
2867
- agent_id = _codex_uuid(response.get("agent_id"))
3245
+ else:
3246
+ agent_id = _codex_uuid(response.get("agent_id"))
2868
3247
  session_id = request["launch"]["parent_session_id"]
2869
3248
  with _codex_child_lock(agent_id, session_id) as (path, child):
2870
3249
  request = _load(root, request["request_id"])
@@ -2880,7 +3259,11 @@ def _codex_native_post_join(event: dict[str, Any], root: Path, request: dict[str
2880
3259
  _codex_native_refuse(root, request, "native reviewer stopped or conflicted before association")
2881
3260
  launch = request["launch"]
2882
3261
  launch.update(agent_id=agent_id, post_confirmed=True)
3262
+ if activity is not None:
3263
+ launch.update(native_activity=activity, native_activity_sha256=_engine_digest(activity))
2883
3264
  if child["start"] is not None:
3265
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
3266
+ _codex_v2_bind_child(request, child)
2884
3267
  launch.update(child_turn_id=child["start"]["turn_id"], agent_type="default")
2885
3268
  request["state"] = "RUNNING"
2886
3269
  _save(root, request)
@@ -2921,7 +3304,7 @@ def _codex_native_lifecycle(event: dict[str, Any]) -> dict[str, Any] | None:
2921
3304
  except (AuthorityError, OSError) as exc:
2922
3305
  _claim_codex_event(agent_id, "failed")
2923
3306
  for root, request in _registered_requests():
2924
- if (request.get("codex_review_profile") == CODEX_NATIVE_V1
3307
+ if (request.get("codex_review_profile") in CODEX_NATIVE_PROFILES
2925
3308
  and request["state"] in {"LAUNCHING", "RUNNING"}
2926
3309
  and (request.get("launch") or {}).get("agent_id") == agent_id):
2927
3310
  _reject(root, request, "failed-native-review-lifecycle")
@@ -2949,6 +3332,9 @@ def _codex_native_lifecycle_join(event: dict[str, Any]) -> dict[str, Any] | None
2949
3332
  if name == "SubagentStart":
2950
3333
  valid = valid and start is None
2951
3334
  if valid:
3335
+ identity = _codex_v2_child(event)
3336
+ if identity is not None:
3337
+ child["native_v2"] = identity
2952
3338
  child["start"] = {"turn_id": event["turn_id"], "agent_type": "default"}
2953
3339
  else:
2954
3340
  valid = (valid and start is not None and start["turn_id"] == event.get("turn_id")
@@ -2962,7 +3348,7 @@ def _codex_native_lifecycle_join(event: dict[str, Any]) -> dict[str, Any] | None
2962
3348
  return None
2963
3349
  root, request = _registered_request(child["request_id"])
2964
3350
  launch = request.get("launch") or {}
2965
- if (not valid or request.get("codex_review_profile") != CODEX_NATIVE_V1
3351
+ if (not valid or request.get("codex_review_profile") not in CODEX_NATIVE_PROFILES
2966
3352
  or launch.get("parent_session_id") != child["session_id"]
2967
3353
  or launch.get("agent_id") != agent_id or launch.get("post_confirmed") is not True
2968
3354
  or event.get("turn_id") == launch.get("parent_turn_id")):
@@ -2970,6 +3356,8 @@ def _codex_native_lifecycle_join(event: dict[str, Any]) -> dict[str, Any] | None
2970
3356
  if name == "SubagentStart":
2971
3357
  if request["state"] != "LAUNCHING":
2972
3358
  _codex_native_refuse(root, request, "native reviewer start was repeated")
3359
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
3360
+ _codex_v2_bind_child(request, child)
2973
3361
  launch.update(child_turn_id=event["turn_id"], agent_type="default")
2974
3362
  request["state"] = "RUNNING"
2975
3363
  _save(root, request)
@@ -3010,6 +3398,43 @@ def _codex_native_steering(event: dict[str, Any]) -> None:
3010
3398
  if not isinstance(tool_input, dict):
3011
3399
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native reviewer steering input is malformed")
3012
3400
  agent_id = _codex_steering_uuid(tool_input.get(field))
3401
+ if event["tool_name"].startswith("collaboration"):
3402
+ target = tool_input.get("target")
3403
+ canonical_path = None
3404
+ if isinstance(target, str) and CODEX_AGENT_PATH_RE.fullmatch(target):
3405
+ canonical_path = target
3406
+ child_identity = None
3407
+ if agent_id is not None:
3408
+ _claim_codex_event(agent_id, "failed")
3409
+ with _codex_child_lock(agent_id, _host_id(event.get("session_id"), "session_id")) as (path, child):
3410
+ child_identity = child.get("native_v2")
3411
+ child["rejected"] = True
3412
+ _write_codex_child(path, child)
3413
+ for root, request in _registered_requests(strict=True):
3414
+ launch = request.get("launch") or {}
3415
+ if (request.get("codex_review_profile") != CODEX_NATIVE_V2
3416
+ or request["state"] not in {"LAUNCHING", "RUNNING"}
3417
+ or launch.get("parent_session_id") != event.get("session_id")):
3418
+ continue
3419
+ # Caller-relative identity matters only to an active review here.
3420
+ if canonical_path is None and isinstance(target, str) and re.fullmatch(r"[a-z0-9_]+(?:/[a-z0-9_]+)*", target):
3421
+ with _codex_transcript(event.get("transcript_path")) as (stream, _):
3422
+ _, meta = _codex_metadata(stream)
3423
+ caller_path = meta.get("agent_path") or "/root"
3424
+ if isinstance(caller_path, str) and CODEX_AGENT_PATH_RE.fullmatch(caller_path):
3425
+ canonical_path = caller_path + "/" + target
3426
+ matches = canonical_path == launch.get("task_name") or (
3427
+ agent_id is not None and (launch.get("agent_id") == agent_id or
3428
+ isinstance(child_identity, dict) and child_identity.get("agent_path") == launch.get("task_name")
3429
+ and child_identity.get("parent_thread_id") == launch.get("parent_thread_id")))
3430
+ if not matches and agent_id is not None and launch.get("agent_id") is None:
3431
+ try:
3432
+ matches = _codex_v2_activity(event, request)["item"]["agent_thread_id"] == agent_id
3433
+ except AuthorityError:
3434
+ pass # Missing direct evidence never supplies an association.
3435
+ if matches:
3436
+ _codex_native_refuse(root, request, "native reviewer cannot be steered or interrupted")
3437
+ return None
3013
3438
  if agent_id is None:
3014
3439
  return None
3015
3440
  _claim_codex_event(agent_id, "failed")
@@ -3033,25 +3458,28 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3033
3458
  return native
3034
3459
  session_id = _host_id(event.get("session_id"), "session_id")
3035
3460
 
3036
- if name in {"PreToolUse", "PostToolUse"} and event.get("tool_name") == "collaborationspawn_agent":
3461
+ if name == "PreToolUse" and event.get("tool_name") == "collaborationspawn_agent":
3037
3462
  tool_input = event.get("tool_input")
3038
3463
  message = tool_input.get("message") if isinstance(tool_input, dict) else None
3039
3464
  if isinstance(message, str) and message.startswith("[CODEARBITER_AUTHORITY_REQUEST:"):
3040
- raise AuthorityError(
3041
- "UNSUPPORTED_HOST_SEAM",
3042
- "default Codex v2 has no direct child UUID binding; qualify the native-v1 interface and arm a fresh request",
3043
- )
3044
- if name == "PreToolUse":
3465
+ _, candidate = _registered_request(_request_id_from_prompt(message))
3466
+ if candidate.get("codex_review_profile") != CODEX_NATIVE_V2:
3467
+ raise AuthorityError(
3468
+ "UNSUPPORTED_HOST_SEAM",
3469
+ "Codex v2 requires a fresh qualified native-v2 request with direct activity UUID binding; "
3470
+ "otherwise qualify native-v1 and arm a fresh request",
3471
+ )
3472
+ else:
3045
3473
  turn_id = _host_id(event.get("turn_id"), "turn_id")
3046
3474
  _record_ordinary_spawn(session_id, turn_id)
3047
3475
  _reject_overlapping_authority(session_id, turn_id)
3048
- return None
3476
+ return None
3049
3477
 
3050
3478
  if name in {"PreToolUse", "PostToolUse"} and event.get("tool_name") in CODEX_STEERING_TOOLS:
3051
3479
  return _codex_native_steering(event)
3052
3480
 
3053
3481
  if name == "PreToolUse":
3054
- if event.get("tool_name") != "spawn_agent":
3482
+ if event.get("tool_name") not in {"spawn_agent", "collaborationspawn_agent"}:
3055
3483
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "only spawn_agent can launch a reviewer")
3056
3484
  tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
3057
3485
  turn_id = _host_id(event.get("turn_id"), "turn_id")
@@ -3073,8 +3501,11 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3073
3501
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review request is not launchable")
3074
3502
  if _canonical(tool_input) != _canonical(request.get("launch_envelope")):
3075
3503
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch envelope was changed")
3504
+ is_v2 = request.get("codex_review_profile") == CODEX_NATIVE_V2
3505
+ if is_v2 != (event["tool_name"] == "collaborationspawn_agent"):
3506
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native review profile differs from launch tool")
3076
3507
  _require_frozen_context(root, request)
3077
- if _ordinary_spawn_marker(session_id, turn_id).exists():
3508
+ if not is_v2 and _ordinary_spawn_marker(session_id, turn_id).exists():
3078
3509
  request["state"] = "REJECTED"
3079
3510
  request["recovery"] = {
3080
3511
  "mode": "overlapping-subagent-launch",
@@ -3095,6 +3526,11 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3095
3526
  codex_review_profile=CODEX_NATIVE_V1, child_turn_id=None,
3096
3527
  fork_context=False,
3097
3528
  )
3529
+ elif is_v2:
3530
+ anchor = _codex_v2_trace_anchor(event, tool_input["task_name"])
3531
+ request["codex_native_trace"] = anchor
3532
+ request["launch"].update(codex_review_profile=CODEX_NATIVE_V2, child_turn_id=None,
3533
+ parent_thread_id=anchor["parent_thread_id"], task_name=anchor["agent_path"], fork_turns="none")
3098
3534
  else:
3099
3535
  request["launch"].update(task_name=tool_input["task_name"], fork_turns=tool_input["fork_turns"])
3100
3536
  request["state"] = "LAUNCHING"
@@ -3102,7 +3538,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3102
3538
  return {"request_id": request_id, "state": "LAUNCHING"}
3103
3539
 
3104
3540
  if name == "PostToolUse":
3105
- if event.get("tool_name") != "spawn_agent":
3541
+ if event.get("tool_name") not in {"spawn_agent", "collaborationspawn_agent"}:
3106
3542
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "post-tool event is not reviewer launch")
3107
3543
  tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
3108
3544
  turn_id = _host_id(event.get("turn_id"), "turn_id")
@@ -3121,7 +3557,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3121
3557
  if len(matches) != 1:
3122
3558
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
3123
3559
  root, request = matches[0]
3124
- if request.get("codex_review_profile") == CODEX_NATIVE_V1:
3560
+ if request.get("codex_review_profile") in CODEX_NATIVE_PROFILES:
3125
3561
  return _codex_native_post(event, root, request)
3126
3562
  response = event.get("tool_response")
3127
3563
  if not isinstance(response, dict):
@@ -3139,7 +3575,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3139
3575
  matches = []
3140
3576
  same_turn = []
3141
3577
  for candidate_root, candidate in _registered_requests():
3142
- if candidate.get("codex_review_profile") == CODEX_NATIVE_V1:
3578
+ if candidate.get("codex_review_profile") in CODEX_NATIVE_PROFILES:
3143
3579
  continue
3144
3580
  launch = candidate.get("launch") or {}
3145
3581
  if (
@@ -447,7 +447,8 @@ def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
447
447
  tools = ("Bash", "Agent") if host == "claude" else (
448
448
  "Bash", "shell_command", "exec_command", "unified_exec", "spawn_agent",
449
449
  "collaborationspawn_agent", "multi_agent_v1send_input", "multi_agent_v1resume_agent",
450
- "multi_agent_v1close_agent")
450
+ "multi_agent_v1close_agent", "collaborationsend_message", "collaborationfollowup_task",
451
+ "collaborationinterrupt_agent")
451
452
  required += [(event, tool, "artifact-authority-hook.py")
452
453
  for event in ("PreToolUse", "PostToolUse") for tool in tools]
453
454
  required += [(event, None, "artifact-authority-hook.py")
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.17.4"
14
+ adapter_version = "0.17.6"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -52,8 +52,8 @@ def main(argv=None) -> int:
52
52
  default="opus", help="Claude reviewer model pinned into the launch envelope",
53
53
  )
54
54
  arm.add_argument(
55
- "--codex-review-profile", choices=("native-v1",), default=None,
56
- help="select only after qualifying the actual registered native V1 UUID interface",
55
+ "--codex-review-profile", choices=("native-v1", "native-v2"), default=None,
56
+ help="select only after qualifying the actual registered native interface and lifecycle",
57
57
  )
58
58
  context_arm = sub.add_parser("arm-context")
59
59
  context_arm.add_argument("--root", required=True)
@@ -80,7 +80,8 @@ def main(argv=None) -> int:
80
80
  print(json.dumps(_artifactauthoritylib.cancel_context_preview(root, args.document_id), sort_keys=True))
81
81
  return 0
82
82
  client = _artifactlib.ArtifactClient(
83
- root, _artifactlib.helper_installation(__file__)
83
+ root, _artifactlib.helper_installation(__file__),
84
+ timeout=_artifactauthoritylib.EVIDENCE_ENGINE_TIMEOUT_SECONDS,
84
85
  )
85
86
  if args.command == "arm":
86
87
  workspaces = _workspace_map(args.workspace)
@@ -93,7 +94,7 @@ def main(argv=None) -> int:
93
94
  and args.codex_review_profile is None):
94
95
  raise _artifactauthoritylib.AuthorityError(
95
96
  "UNSUPPORTED_HOST_SEAM",
96
- "Codex review requires explicit --codex-review-profile native-v1 after live interface qualification",
97
+ "Codex review requires an explicit native-v1 or native-v2 profile after live interface qualification",
97
98
  )
98
99
  result = _artifactauthoritylib.arm_request(
99
100
  root, client, args.artifact_id, args.record_id, args.activity,
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.25.3"
316
+ adapter_version = "2.25.5"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -287,9 +287,11 @@ options. Local executables shadowing nested npm are rejected.
287
287
  Direct Node script entrypoints are also pinned. These launch-file bindings do
288
288
  not establish transitive dependency closure for deliberately excluded inputs.
289
289
 
290
- New observations use the closed `declared-command/0.2.0` producer profile to
291
- retain collector and launch-file identities. Historical `0.1.0` bindings remain
292
- separate and cannot be relabelled as qualified launch evidence.
290
+ New observations use the closed `declared-command/0.3.0` producer profile to
291
+ retain collector and launch-file identities plus an engine-produced normalized
292
+ completion closure. Raw workspace fingerprints must still match before and
293
+ after command execution. Historical `0.1.0` and `0.2.0` bindings retain their
294
+ original contracts and cannot be relabelled as newer evidence.
293
295
 
294
296
  For `spec_review` or `quality_review`, arm the corresponding activity without
295
297
  workspace mappings and dispatch the returned `launch_envelope` unchanged to one
@@ -332,10 +334,13 @@ acceptance. Scope review uses validated task completion bindings. If verificatio
332
334
  is rerun, obtain and record its completed task review before selecting that run
333
335
  for scope review; do not infer equivalence between receipts.
334
336
 
335
- Creating the first completion context recomputes the current raw worktree
336
- fingerprint and requires it to match the selected verifier's recorded fingerprint.
337
- Later freshness checks allow native task-status and execution bookkeeping updates
338
- in validated canonical artifacts while retaining their normative content hashes.
337
+ Creating a completion context for a `0.3.0` verification recomputes the normalized
338
+ worktree closure bound during that verification. Only validated native outputs
339
+ and execution bookkeeping are normalized; their publication must not invalidate
340
+ unchanged source. Historical `0.2.0` verification retains its initial exact raw
341
+ fingerprint comparison. Later freshness checks allow native task-status and
342
+ execution bookkeeping updates in validated canonical artifacts while retaining
343
+ their normative content hashes.
339
344
  Changes to task definitions, other HTML or other governance files still affect
340
345
  freshness under the normal snapshot policy. The closure covers
341
346
  tracked working-tree bytes and non-ignored untracked files, not ignored
@@ -349,6 +354,16 @@ the scope reaches `REVIEW`, reverify against one current source snapshot, run th
349
354
  combined quality review, and use `accept-scope` once. Do not accept tasks
350
355
  individually.
351
356
 
357
+ Current Codex native V1 completion reviews retain the full frozen context in
358
+ its immutable, content-addressed file and keep only its closed reference in
359
+ mutable request state. Loading validates the exact context locator, hash,
360
+ canonical bytes and review kind. The native context budget remains 8 MiB;
361
+ mutable request state remains bounded at 1 MiB. Before dispatch, the adapter
362
+ reserves the exact fixed result identities, six times the 64 KiB decision
363
+ budget for canonical JSON escaping, and 32 bounded 256-byte lifecycle slots.
364
+ A request without that capacity is refused before child launch. Historical
365
+ inline requests retain their existing limits and abandonment-only recovery.
366
+
352
367
  An ARMED review that never launched can be abandoned through the adapter:
353
368
 
354
369
  ```sh
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.17.4",
3
+ "version": "0.17.6",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",