diff --git a/GLOSSARY.md b/GLOSSARY.md index c53d36fc..f24e5e33 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -64,7 +64,9 @@ idle observations. Neither path renews counters or resource allocation. **Agent handoff**: Passing an unfinished objective and its checkpoint to a different conversation, -without implicitly transferring identity, permissions or resource allowance. +continuing its already-authorized scope, cumulative spending, remaining allowance +and original deadline. It does not renew resources, change identity or widen +permissions, and does not require optional governor enrollment in manual mode. **Handoff checkpoint**: The outgoing agent's durable account of completed work, remaining work, evidence, diff --git a/docs/dev/active-lanes.yaml b/docs/dev/active-lanes.yaml index d6aedefc..303c6383 100644 --- a/docs/dev/active-lanes.yaml +++ b/docs/dev/active-lanes.yaml @@ -1,6 +1,19 @@ schema_version: 1 work_item_tracking: required lanes: + - id: P178 + owner: primary / ecochran76; handoff_ritual_diagnosis + objective: Remove handoff resource ritual without renewing resources or activating governance + work_items: [https://github.com/CochranResearchGroup/codex-wake/issues/365] + plan: docs/dev/plans/0178-2026-10-27-handoff-resource-continuity.md + branch: fix/handoff-automatic-allocation + target: origin/main + plan_state: OPEN + custody_state: ACTIVE_WORKTREE + dependencies: [] + overlaps: [] + updated_at: 2026-10-27 + disposition: Bounded automatic source-allocation selection and manual-mode guidance; native file-read linkage and deployed acceptance remain separate - id: P176 owner: primary / ecochran76 objective: Repair durable future scheduling and native MCP host wiring diff --git a/docs/dev/plans/0178-2026-10-27-handoff-resource-continuity.md b/docs/dev/plans/0178-2026-10-27-handoff-resource-continuity.md new file mode 100644 index 00000000..d52bfa73 --- /dev/null +++ b/docs/dev/plans/0178-2026-10-27-handoff-resource-continuity.md @@ -0,0 +1,56 @@ +# Handoff resource continuity / #365 + +State: OPEN — bounded source and instruction repair, pending primary reconciliation +Owner: primary / ecochran76; delegated implementation: handoff_ritual_diagnosis +Branch: fix/handoff-automatic-allocation +Baseline: origin/main96f0f01104e6f0297d3c1ea4c7c2477051270787 +Work item: https://github.com/CochranResearchGroup/codex-wake/issues/365 + +Authority: operator clarified that a new session reading a handoff continues already-authorized work without resource reset, repeated approval or enrollment ritual. Existing numeric ceilings and original deadlines are retained. Manual mode does not silently activate governance. A stale completed/blocked goal for unrelated work is not a global lock. + +Scope: reproduce optional supervision's forced allocation selection; derive the existing allocation only from qualified native lineage plus the private retained source receipt/current host grant; preserve explicit allocation compatibility, current governor setting, spending, deadline and genuine stops. Correct agent instructions for ordinary operator-directed manual handoff reading. No live goal, watcher, service, provider, native daemon or original wake mutations. + +Non-goals: infer native identity from arbitrary Markdown; fabricate response-level accounting; observe every file read; transfer arbitrary host privileges; reset or renew an objective; automatically enroll unsupervised sessions; repair native orphan-tool panic (#364, plan0177); infer why an original result went missing. + +| Requirement | Acceptance and current boundary | +| --- | --- | +| Read a handoff and continue authorized manual work | Agent skill/lifecycle guidance explicitly removes set/reset/enrollment/reapproval as a prerequisite; no governor activation. No runtime file-read hook is claimed. | +| Optional supervised successor needs no allocation-ID ritual | Public MCP regression joins qualified source allocation with omitted ID; same original deadline and cumulative80/100tokens. | +| Completed/blocked source goal does not obstruct existing authorized continuation | Public regression covers source statuses active/complete/blocked without native goal mutations. | +| Accounting basis follows existing objective | Omitted accounting basis inherits source fresh/cache-inclusive basis; explicit changes still use the existing rejection guard. | +| Governor choice follows existing objective | Deactivated source allocation remains inactive on successor attachment. | +| Real exhaustion does not replenish on handoff | Public regression retains stop_latched with80/80spent. | +| Old unrelated objective does not lock a new session | Public regression retains old exhausted/blocked allocation and allows independent new allocation under host-approved self-enrollment envelope. | +| Do not fabricate lineage from ordinary handoff prose | Qualified native parent plus private source receipt/current host envelope are required for automatic supervised selection. Missing source binding holds that optional adapter only; manual authority is separate. | + +Definition of done for this bounded slice: source/public-seam regressions pass; instruction and domain authorities agree; primary adjudicates limitations; integration/release remain separate. No source result is claimed installed. + +## Diagnosis and red/green + +Initial exact red: public CommandRunner.call(wake_goal_limits_set) with qualified successor, intact source allocation and20tokens remaining returns setup_unavailable / successor must select its existing objective allocation. Omitting only allocation_id causes failure; specifying it preserves original deadline/cumulative80tokens and succeeds. Private minimal loop /tmp/codex-wake-handoff-ritual/test_handoff_auto.py ran RED0.188s, explicit-ID control GREEN0.218s. Initial malformed child fixture returned descendant_session_identity_mismatch; corrected to native shared-session lineage and unique response ID before accepting the symptom red. + +Ranked hypotheses: (1) default selection chooses a fresh native-thread allocation despite qualified existing lineage; explicit allocation makes it green. Confirmed. (2) manual Markdown read lacks durable native linkage; no runtime file-read hook exists, so instruction correction must not claim automatic metering. (3) stale inactive native goal alone is a universal gate; existing inactive-status tests pass, so this is not the demonstrated source bug. New independent-session regression protects separation from a prior exhausted objective. + +Repository regression test_goal_supervision.GoalSupervisionTests.test_verified_successor_inherits_allocation_without_operator_selection RED0.298s before source patch, then GREEN with stale source statuses and inactive-governor case. Existing managed handoff tests for continuous-monitor activation and inherited deadline already passed2/1.974s, confirming the explicitly managed handoff path is separate from ordinary file reading. + +The fix changes only the established first-enrollment successor refusal: after qualified usage identifies a source, read its private receipt, resolve current host grant and check runtime identity, then let the existing allocation admission enforce original caps/deadline/accounting/stop/deactivation. Explicit allocation selections and missing-source refusal stay compatible. No opportunistic enrollment is added to inspection or file reads. A second red slice caught cache-inclusive source allocations being rejected when a successor omitted the accounting argument. That omission now inherits the source basis rather than defaulting to fresh; no explicit basis change is authorized. + +## Final selected validation + +- `PYTHONPATH=src:tests -m unittest test_goal_supervision test_governor_deactivation test_enforcement_permissions test_goal_engagement`:54PASS16.963s initially; after accounting-basis follow-up54PASS13.623s with full output /tmp/codex-wake-handoff-ritual/selection-final.log. This is an affected selection, not the full suite. +- Candidate repository regression copied unchanged to /tmp/codex-wake-handoff-ritual/redtests/test_goal_supervision_candidate.py and run against baseline source: one test/four subcases RED0.588s, all with successor must select its existing objective allocation. Same test command/module against candidate source GREEN0.841s. Receipts /tmp/codex-wake-handoff-ritual/repository-{red,green}.log. Baseline terminal checkout source src/ was verified byte-equivalent through git diff to origin/main96f0f01 before patch. +- Additional exhaustion scenario initially asserted MCP error=false for a correctly held result; fixed the test expectation to error=true with stop_latched, not the implementation. That setup/assertion failure is not counted as a product regression or passing test. +- Second slice: same public regression, cache-inclusive source subcase RED0.682s with allocation accounting basis cannot change during self enrollment, then GREEN0.696s after inheriting omitted basis. Logs /tmp/codex-wake-handoff-ritual/basis-{red,green}.log. +- Pinned Ruff0.14.2 undefined-name selection on changed Python files PASS; `python3 scripts/check_policy_wiring.py` PASS; `git diff --check` PASS. +- No new native/provider acceptance, installation, live controller or goal mutation. Tests own isolated temporary roots and fixture watchers/native sockets, with normal unittest cleanup. + +Primary review/integration, installed skill refresh and release validation remain pending. This plan does not claim the arbitrary Markdown/no-lineage case has an automatic metering protocol. Memory disposition: forbidden — task does not authorize persistent memory writes. + +Primary reconciliation: development identity advances to0.22.0.dev2 for future installed/resident catalog discrimination. No live activation is implied by the source version. Native source repair remains separate. + + +## Primary review and integration + +Frozen review base96f0f01; implementation95cc3e2, identityb2284ea, documentation remediation9bf8b24. Primary reviewed Standards and Spec independently. One blocking Spec finding: workflows.md and the skill description still mandated enrollment for a stated ceiling, contradicting the new manual-handoff contract. Remediated in9bf8b24; exact paragraphs rechecked. No remaining blocking finding on the source behavior or scope. Public regression and affected54-case evidence retained; no redundant full local suite requested. Code review is source qualification, not installed activation or native-panic repair. + +Integration candidate0.22.0.dev2 retains existing API shape, explicit grants/selections, exhaustion and deactivation. No native reload, watcher activation or allocation renewal is part of merge. Missing manual-file lineage is explicitly not represented as verified metering. A future installed/readback packet must distinguish CLI/skill identity from resident MCP identity. diff --git a/docs/releases/v0.22.0.dev2.md b/docs/releases/v0.22.0.dev2.md new file mode 100644 index 00000000..4cd2de9c --- /dev/null +++ b/docs/releases/v0.22.0.dev2.md @@ -0,0 +1,7 @@ +# 0.22.0.dev2 + +Qualified native successors can omit the existing objective allocation ID when requesting optional supervision. The adapter resolves the retained source allocation and inherits its accounting basis when omitted. Original spending, deadline, deactivation and exhaustion remain unchanged; explicit host grants and selection remain compatible. + +Agent handoff guidance now states that reading an operator-provided handoff continues already-authorized manual work without enrollment, reset or repeated approval. A prior native goal for unrelated work does not lock a new objective. No governor startup or file-read interception is added. Arbitrary Markdown is not verified native lineage or metering proof. + +Source qualification: issue365, plan0178. Installation, resident MCP refresh and native panic repair are separate evidence boundaries. This release note alone claims none of those effects. diff --git a/pyproject.toml b/pyproject.toml index 6828d0d9..5c6ed63a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "codex-wake" -version = "0.22.0.dev1" +version = "0.22.0.dev2" description = "Durable Codex follow-ups, tracked agent exchanges and MCP tools" readme = "README.md" requires-python = ">=3.11" diff --git a/skills/codex-wake/SKILL.md b/skills/codex-wake/SKILL.md index 29f2bc22..a23d59dd 100644 --- a/skills/codex-wake/SKILL.md +++ b/skills/codex-wake/SKILL.md @@ -1,6 +1,6 @@ --- name: codex-wake -description: Use Codex Wake when a user gives /goal resource limits (tokens, time, memory, swap, CPU, tasks): register token/time limits for the existing native goal through MCP; an independent watcher requests checkpoint then pauses the goal and interrupts its exact active turn. Dedicated host-approved executors provide separate memory/CPU controls. Also use it for durable agent-to-agent requests, correlated replies, exclusive work claims, suspended reply wakes, session/tab lookup, wake listing/cancellation and persistent emergency pause, host-approved worker spawn, self-refresh of MCP resources through an approved selected-thread reload, or skill/MCP resources through approved client replacement or shared reload after idle checks, fresh-context handoff, and scheduled follow-ups. Prefer its MCP tools for tracked exchanges when configured; use the CLI otherwise. +description: Use Codex Wake for resource allowances and optional existing-native-goal supervision. Honor numeric ceilings and the operator-selected manual or supervised mode; reading a handoff continues already-authorized work without reset, repeated approval or optional enrollment. When external supervision is selected, register token/time limits through MCP; an independent watcher requests checkpoint then pauses the goal and interrupts its exact active turn. Dedicated host-approved executors provide separate memory/CPU controls. Also use it for durable agent-to-agent requests, correlated replies, exclusive work claims, suspended reply wakes, session/tab lookup, wake listing/cancellation and persistent emergency pause, host-approved worker spawn, self-refresh of MCP resources through an approved selected-thread reload, or skill/MCP resources through approved client replacement or shared reload after idle checks, fresh-context handoff, and scheduled follow-ups. Prefer its MCP tools for tracked exchanges when configured; use the CLI otherwise. --- # Codex Wake @@ -30,6 +30,18 @@ limits; report the fallback and keep shared services and stopped campaigns uncha bounded setup diagnosis and checkpoint writing. Only explicit user renewal recorded through host authority creates a new allowance. +Reading an operator-provided handoff in a new session continues the already +authorized objective with its remaining allowance and original deadline. It does +not require resource reset, repeated approval or optional governor enrollment. +Continue in the operator-selected manual mode when applicable; do not enable a +watcher or governor just because a handoff was read. A completed or blocked native +goal for another objective is not a global session lock. Genuine exhaustion of +the continuing objective remains explicit and does not replenish on handoff. +When already-enabled supervision is requested through the host adapter, a +qualified native successor automatically selects its source's existing allocation. +An ordinary Markdown read alone is not native lineage or accounting proof; keep +manual continuation separate from claims of verified automatic enforcement. + For refresh, inspect the profile's scope and preserve one fixed intent. A thread_mcp_idle profile requires a successful read-only native scope probe and refreshes only the selected thread's MCP connections after that thread is idle. diff --git a/skills/codex-wake/references/existing-goal-limits.md b/skills/codex-wake/references/existing-goal-limits.md index 11cf307e..f27d0dd2 100644 --- a/skills/codex-wake/references/existing-goal-limits.md +++ b/skills/codex-wake/references/existing-goal-limits.md @@ -19,10 +19,35 @@ See [governor controls](../../../docs/governor-control.md) for offline authentic host access and notice-delivery limitations. Verify installed capabilities before claiming these source features are present. +## Handoff continuation without an operator ritual + +Reading an operator-provided handoff does not create an enrollment requirement. +Carry forward the same objective, cumulative spending, remaining allowance, +original deadline and operator-selected governor/manual mode, then continue work +within that authority. Do not ask the operator to reset resources or repeat +approval merely because conversation identity changed. Do not silently activate +the governor. An old completed or blocked goal for unrelated work does not lock +this session; do not reset or reuse its stopped allocation to admit a new objective. + +For optional supervised enrollment, a verified native successor with qualified +source lineage and a retained source allocation can omit `allocation_id`; the +adapter selects the existing allocation without renewing its window. Explicit +host grants and explicit allocation selection remain compatible. Genuine +exhaustion and deactivation are inherited. Missing source allocation or changed +runtime/envelope remains a supervised-admission limitation, not a mandate to +perform enrollment before operator-authorized manual work. + +Opening a new session and reading arbitrary Markdown does not itself establish +verified native lineage or response-level accounting. Do not invent an allocation +from prose or infer unlimited resources from a missing meter. Preserve stated +numeric ceilings and use available evidence for manual accounting; report an +actual limit or missing measurement plainly. This source change does not install +a startup hook that observes ordinary file reads. + ## Agent workflow -When the user supplies a token or time allowance, load this guide before heavy -work. Obtain your actual `CODEX_THREAD_ID` in a native shell, then call: +When the user requests external supervision for a token or time allowance, load +this guide before heavy work. For that opt-in mode, obtain your actual `CODEX_THREAD_ID` in a native shell, then call: ```json { diff --git a/skills/codex-wake/references/lifecycle.md b/skills/codex-wake/references/lifecycle.md index af40dbe3..e045d198 100644 --- a/skills/codex-wake/references/lifecycle.md +++ b/skills/codex-wake/references/lifecycle.md @@ -86,6 +86,17 @@ Refresh, handoff and rollback preserve spending and deadlines; only explicit operator authorization can extend the budget. Missing profiles, grants or instructions require operator setup; an agent cannot invent replacements. +## Manual handoff reading + +The commands above manage an explicit runtime transition. They are not required +when the operator starts a new session and asks it to read a handoff and continue. +Continue the already-authorized objective in its existing manual or supervised +mode, carrying forward remaining resources and the original deadline. Reading a +handoff does not authorize a reset, new enrollment, silent governor activation or +repeated operator approval. A stale native goal for another objective is not a +continuation gate. Ordinary Markdown does not supply verified runtime lineage; +do not claim automatic supervised attachment from that read alone. + ## Shared MCP refresh A host profile with `refresh_mode: "shared_mcp_idle"` uses the existing refresh diff --git a/skills/codex-wake/references/workflows.md b/skills/codex-wake/references/workflows.md index 6ee4ae7e..5048cf2d 100644 --- a/skills/codex-wake/references/workflows.md +++ b/skills/codex-wake/references/workflows.md @@ -39,18 +39,32 @@ See [lifecycle requests and setup](lifecycle.md#shared-mcp-refresh). ## When the user gives resource limits For "use at most 2M tokens" or "stop after four hours", read the -[existing-session workflow](existing-goal-limits.md) before work. -Use `wake_goal_limits_set` with your actual caller UUID and the user's allocation; -verify qualified accounting and a fresh independent watcher in the returned -state. `wake_goal_limits_show` reconciles status. A configured policy alone is -insufficient. Limits persist across replacement goals and resume; only an -explicit user reset recorded by the host can reset counters/allocation. Missing -setup, unknown accounting and unverified stops hold autonomous campaign work; -report the reason and next action. Operator replies, status checks, bounded setup -diagnosis and checkpoint writing remain available. A halted goal is not a disabled -chat. New enrollments count fresh input plus output by default; cache-inclusive -accounting requires explicit selection. Re-enrollment preserves its existing basis. -Token stops are sampled, with unknown maximum overshoot. +[existing-session workflow](existing-goal-limits.md) and preserve the user's actual +numeric ceiling, cumulative spending, remaining allowance and original deadline. +Honor the operator-selected manual or supervised mode. Reading an operator-provided +handoff continues already-authorized work without resource reset, repeated approval +or optional governor enrollment. Do not activate supervision merely because limits +or a handoff are present. A stale completed or blocked native goal for unrelated +work is not a global lock; do not reset or reuse its allocation to admit new work. + +When external supervision is selected, use `wake_goal_limits_set` with the actual +caller UUID and authorized allocation. Qualified native successors may omit the +internal allocation ID: the adapter selects their retained source allocation and +inherits the same accounting basis when omitted, preserving cumulative charges, +original deadline, genuine exhaustion and governor deactivation. Ordinary Markdown +does not establish verified native lineage or accounting. Do not infer unlimited +resources or a new allocation from absent measurements. + +Verify qualified accounting and a fresh independent watcher before claiming +external enforcement. `wake_goal_limits_show` reconciles status; configuration +alone is insufficient. Missing setup, unknown accounting and unverified stops +hold externally supervised execution, not operator-authorized manual continuation +within demonstrably remaining resources. Report the exact limitation and next +action without requiring an enrollment ritual for manual work. Handoff does not +renew a genuinely exhausted objective; only explicit user renewal through host +authority can change its allowance. New supervised allocations default to fresh +input plus output; explicit cache-inclusive selection and inherited accounting +basis remain supported. Token stops are sampled, with unknown maximum overshoot. For explicit memory, swap, CPU or task controls, use the separate [dedicated executor workflow](resource-limits.md). Native supervision diff --git a/src/codex_wake/__init__.py b/src/codex_wake/__init__.py index 6a9ef362..edcff870 100644 --- a/src/codex_wake/__init__.py +++ b/src/codex_wake/__init__.py @@ -2,4 +2,4 @@ __all__ = ["__version__"] -__version__ = "0.22.0.dev1" +__version__ = "0.22.0.dev2" diff --git a/src/codex_wake/goal_supervision.py b/src/codex_wake/goal_supervision.py index 2a53beb8..e0c64505 100644 --- a/src/codex_wake/goal_supervision.py +++ b/src/codex_wake/goal_supervision.py @@ -625,7 +625,28 @@ def enroll(root,policy_path,caller_thread_id,token_limit,time_limit_seconds, from .goal_usage import observe_usage first=observe_usage(grant['codex_home'],caller_thread_id,created,token_accounting=basis) if first.get('state')!='qualified':raise AccountingUnavailable('first enrollment requires qualified original history',first) - if first.get('parent_thread_id'):raise ValueError('successor must select its existing objective allocation') + parent=first.get('parent_thread_id') + if parent: + if allocation_id is not None: + raise ValueError('successor must continue its existing objective allocation, not select a fresh one') + source=directory/(_name(parent)+'.json') + if not source.exists(): + raise ValueError('successor allocation is unavailable; inspect the verified source continuation') + _private(source);predecessor=json.loads(source.read_text()) + if predecessor.get('thread_id')!=parent: + raise ValueError('successor source identity changed') + inherited=_grant(policy_path,caller_thread_id,predecessor['allocation_id'],root) + if any(inherited[key]!=grant[key] for key in ('app_server','codex_home','codex_path')): + raise ValueError('cross-native successor allocation is unsupported') + # This remains the caller's explicit supervision request. Select + # its qualified existing allocation without creating a new window + # or asking the operator to copy an internal allocation identity. + grant=inherited + if token_accounting is None: + basis=predecessor.get('token_accounting','cache_inclusive') + receipt['token_accounting']=basis + receipt.update(grant=grant,allocation_id=grant['allocation_id'], + continuation_source_thread_id=parent) if grant.get('host_delegated'):_allocation_admit(directory,receipt,old) if sync(receipt): _record_supervision_notice(path,receipt,'deactivation') diff --git a/tests/test_goal_supervision.py b/tests/test_goal_supervision.py index 6cbf38f6..fd402cf8 100644 --- a/tests/test_goal_supervision.py +++ b/tests/test_goal_supervision.py @@ -221,6 +221,118 @@ def test_native_successor_cannot_omit_existing_allocation_to_get_fresh_budget(se self.assertTrue(failed,result) self.assertIn('successor',result['error']['message']) + def test_verified_successor_inherits_allocation_without_operator_selection(self): + for source_status,enabled,accounting in (('active',True,'fresh'),('complete',True,'fresh'), + ('blocked',True,'fresh'),('active',False,'fresh'),('active',True,'cache_inclusive')): + from codex_wake.mcp_server import CommandRunner + with self.subTest(source_status=source_status,enabled=enabled,accounting=accounting),tempfile.TemporaryDirectory() as directory: + root,policy,log,native,_=self.fixture(directory) + native['goal']['status']=source_status + template=json.loads(policy.read_text())['callers'][THREAD];template.pop('allocation_id') + policy.write_text(json.dumps(dict(version=2,native_caller_binding=True,allocations={},self_enrollment=template))) + successor='00000000-0000-7000-8000-000000000290' + rows=json.loads(json.dumps([json.loads(line) for line in log.read_text().splitlines()]).replace(THREAD,successor)) + rows[0]['payload']['parent_thread_id']=THREAD + rows[0]['payload']['session_id']=THREAD + rows[0]['payload']['source']={'subagent':{'thread_spawn':{'parent_thread_id':THREAD}}} + rows[-1]['payload']['session_id']=THREAD + rows[-1]['payload']['response_id']='successor-response' + rows[1]['payload']['goal']['createdAt']=NOW+10 + for row in rows[1:]:row['timestamp']='2030-01-01T00:00:10Z' + (log.parent/'successor.jsonl').write_text(''.join(json.dumps(row)+'\n' for row in rows)) + native['siblings']={successor:dict(goal=dict(threadId=successor,createdAt=NOW+10,status='active'),active=['work'],calls=[])} + context=lambda thread:dict(threadId=thread,sessionId=thread,callId='call-'+thread, + **{'x-codex-turn-metadata':dict(thread_id=thread,session_id=thread,turn_id='work')}) + runner=CommandRunner(goal_limits_policy=policy,goal_limits_root=root) + with self.controller(root),patch('codex_wake.lifecycle_budget.trusted_upper',return_value=NOW+10): + first,failed=runner.call('wake_goal_limits_set',dict(caller_thread_id=THREAD,token_limit=100,time_limit_seconds=120,token_accounting=accounting),native_context=context(THREAD)) + self.assertFalse(failed,first) + if not enabled: + from codex_wake.goal_governance import change + from codex_wake.session_permissions import Principal + change(root,THREAD,enabled=False,principal=Principal(THREAD,'operator')) + second,failed=runner.call('wake_goal_limits_set',dict(caller_thread_id=successor,token_limit=100,time_limit_seconds=120),native_context=context(successor)) + self.assertFalse(failed,second) + self.assertEqual(second['allocation_id'],first['allocation_id']) + self.assertEqual(second['deadline'],first['deadline']) + self.assertEqual(second['used_tokens'],80) + self.assertEqual(second['token_accounting'],accounting) + self.assertEqual(second['continuation_source_thread_id'],THREAD) + self.assertEqual(second['state'],'armed' if enabled else 'inactive') + self.assertEqual(second['governor_enabled'],enabled) + self.assertFalse(any(method in ('thread/goal/set','turn/interrupt') for method,_ in native['calls'])) + + def test_successor_automatic_selection_preserves_exhaustion(self): + from codex_wake.mcp_server import CommandRunner + with tempfile.TemporaryDirectory() as directory: + root,policy,log,native,_=self.fixture(directory) + template=json.loads(policy.read_text())['callers'][THREAD];template.pop('allocation_id') + policy.write_text(json.dumps(dict(version=2,native_caller_binding=True,allocations={},self_enrollment=template))) + successor='00000000-0000-7000-8000-000000000290' + rows=json.loads(json.dumps([json.loads(line) for line in log.read_text().splitlines()]).replace(THREAD,successor)) + rows[0]['payload']['parent_thread_id']=THREAD + rows[0]['payload']['session_id']=THREAD + rows[0]['payload']['source']={'subagent':{'thread_spawn':{'parent_thread_id':THREAD}}} + rows[-1]['payload']['session_id']=THREAD + rows[-1]['payload']['response_id']='successor-response' + rows[1]['payload']['goal']['createdAt']=NOW+10 + for row in rows[1:]:row['timestamp']='2030-01-01T00:00:10Z' + (log.parent/'successor.jsonl').write_text(''.join(json.dumps(row)+'\n' for row in rows)) + native['siblings']={successor:dict(goal=dict(threadId=successor,createdAt=NOW+10,status='active'),active=['work'],calls=[])} + context=lambda thread:dict(threadId=thread,sessionId=thread,callId='call-'+thread, + **{'x-codex-turn-metadata':dict(thread_id=thread,session_id=thread,turn_id='work')}) + runner=CommandRunner(goal_limits_policy=policy,goal_limits_root=root) + with self.controller(root),patch('codex_wake.lifecycle_budget.trusted_upper',return_value=NOW+10): + first,failed=runner.call('wake_goal_limits_set',dict(caller_thread_id=THREAD,token_limit=80,time_limit_seconds=120),native_context=context(THREAD)) + self.assertTrue(failed,first) + second,failed=runner.call('wake_goal_limits_set',dict(caller_thread_id=successor,token_limit=80,time_limit_seconds=120),native_context=context(successor)) + self.assertTrue(failed,second) + self.assertEqual(second['allocation_id'],first['allocation_id']) + self.assertEqual(second['deadline'],first['deadline']) + self.assertEqual(second['used_tokens'],80) + self.assertEqual(second['continuation_source_thread_id'],THREAD) + self.assertEqual(second['state'],'held') + self.assertTrue(second['stop_latched']) + self.assertEqual(second['token_limit'],80) + self.assertFalse(any(method in ('thread/goal/set','turn/interrupt') for method,_ in native['calls'])) + + + def test_unrelated_new_session_is_not_locked_by_old_exhausted_goal(self): + from codex_wake.mcp_server import CommandRunner + with tempfile.TemporaryDirectory() as directory: + root,policy,log,native,_=self.fixture(directory) + native['goal']['status']='blocked' + template=json.loads(policy.read_text())['callers'][THREAD];template.pop('allocation_id') + policy.write_text(json.dumps(dict(version=2,native_caller_binding=True,allocations={},self_enrollment=template))) + successor='00000000-0000-7000-8000-000000000290' + rows=json.loads(json.dumps([json.loads(line) for line in log.read_text().splitlines()]).replace(THREAD,successor)) + rows[-1]['payload']['response_id']='successor-response' + rows[1]['payload']['goal']['createdAt']=NOW+10 + for row in rows[1:]:row['timestamp']='2030-01-01T00:00:10Z' + (log.parent/'successor.jsonl').write_text(''.join(json.dumps(row)+'\n' for row in rows)) + native['siblings']={successor:dict(goal=dict(threadId=successor,createdAt=NOW+10,status='active'),active=['work'],calls=[])} + context=lambda thread:dict(threadId=thread,sessionId=thread,callId='call-'+thread, + **{'x-codex-turn-metadata':dict(thread_id=thread,session_id=thread,turn_id='work')}) + runner=CommandRunner(goal_limits_policy=policy,goal_limits_root=root) + with self.controller(root),patch('codex_wake.lifecycle_budget.trusted_upper',return_value=NOW+10): + first,failed=runner.call('wake_goal_limits_set',dict(caller_thread_id=THREAD,token_limit=40,time_limit_seconds=120),native_context=context(THREAD)) + self.assertTrue(failed,first) + second,failed=runner.call('wake_goal_limits_set',dict(caller_thread_id=successor,token_limit=80,time_limit_seconds=120),native_context=context(successor)) + self.assertFalse(failed,second) + self.assertNotEqual(second['allocation_id'],first['allocation_id']) + self.assertEqual(first['deadline'],NOW+120) + self.assertEqual(second['deadline'],NOW+130) + self.assertEqual(second['used_tokens'],40) + self.assertNotIn('continuation_source_thread_id',second) + self.assertEqual(second['state'],'armed') + self.assertTrue(first['stop_latched']) + self.assertEqual(second['token_limit'],80) + retained,_=runner.call('wake_goal_limits_show',dict(caller_thread_id=THREAD),native_context=context(THREAD)) + self.assertEqual(retained['allocation_id'],first['allocation_id']) + self.assertTrue(retained['stop_latched']) + self.assertFalse(any(method in ('thread/goal/set','turn/interrupt') for method,_ in native['calls'])) + + def test_public_successor_joins_automatically_created_allocation(self): from codex_wake.mcp_server import CommandRunner with tempfile.TemporaryDirectory() as directory: