Skip to content

refactor(integrations): run the Claude Code and Codex SessionStart flow from the lifecycle package - #2550

Open
Fengzdadi wants to merge 8 commits into
oceanbase:masterfrom
Fengzdadi:refactor/integration-lifecycle-session-start
Open

Fengzdadi wants to merge 8 commits into
oceanbase:masterfrom
Fengzdadi:refactor/integration-lifecycle-session-start

Conversation

@Fengzdadi

@Fengzdadi Fengzdadi commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Part of #2250 (plan step 3: move the Claude Code and Codex hook flows onto the lifecycle module). Builds on #2318, which added src/powercontext_integration_lifecycle/ and the copy generator, and on #2317, whose SessionStart classification moves here with the flow. The prompt hooks follow in a separate PR.

Rationale for this change

The Claude Code SessionStart hook (hooks/session_start.py) and the Codex one (hooks/session_binding.py) run the same flow: resolve the Scope, request the bootstrap package, record its delivery, keep the receipt for the first prompt, and report failures on stderr. Each plugin also keeps its own diagnostic throttle, bootstrap receipt state, bounded HTTP helpers, and Scope binding errors. The copies differ only in a few host values, so every fix has to be made twice; #2317 is the latest example.

This PR moves that code into the lifecycle package once and gives each plugin a small host declaration for what differs. The hooks keep only what is native to their host.

What changes are included in this PR?

Four commits, each with the details in its message:

  1. Plugins ship the package. Claude Code and Codex now ship the whole package as powercontext_lifecycle/ in their plugin directories, generated from src/powercontext_integration_lifecycle/ by make integration-lifecycle. Their single-file copies of the four helpers are gone; WorkBuddy and Hermes keep theirs. The generator deletes a generated module that the source no longer has, reports other extra files, rejects a source subpackage, and still reports a hand-made copy elsewhere in a plugin.

  2. Diagnostics and receipts. diagnostics.py (DiagnosticThrottle) and bootstrap_state.py (BootstrapState) replace each plugin's hooks/diagnostics.py and bootstrap_state.py. The host differences (the state file name and the two variables that locate the receipt directory) are declared once per plugin in the new claude_code_host.py and codex_host.py.

  3. SessionStart flow. session_start.py (run_session_start), transport.py (open_bounded, bind_response_deadline, complete_body), scope_binding_errors.py, and host_profile.py. A HostProfile names the integration, User-Agent, diagnostic component, throttle, and receipt state. The hooks validate the payload, accept their own lifecycles (Codex still binds the session on a missing or unknown lifecycle), convert their settings to a plain BootstrapConnection, and write the package in their native output. fix(integrations): report Scope resolution failures from prompt hooks #2317's SessionStart behavior is carried in the package as the hooks have it today:

    • HTTPException and short bodies are server_unavailable; undecodable or oversized bodies are invalid_response.
    • A 404 is version_mismatch only without an error code, and the error code goes into the event.
    • SessionStart failures stay out of the diagnostic throttle.
    • The bootstrap response, and the error code of a failed request, are read under the deadline their request started.
    • The deadline raises socket.timeout, which Python 3.9 needs for Claude Code.

    The prompt hooks, the Codex Stop hook, and the Claude Code status line import the helpers and errors from the package.

  4. Shell tests from recorded payloads. tests/test_session_start_shells.py runs each hook as a subprocess with payloads recorded from real hosts, in tests/fixtures/recorded_host_payloads/: Claude Code 2.1.277 (startup, resume, compact, fork) and Codex 0.165.0-alpha.1.2 (startup, resume, fork). Each recorded payload replaces the payload of the characterized bootstrap case and must lead to the same requests, output, and state. A broken-pipe case per host checks that the hook exits 0, reports the failed delivery once, and keeps no receipt. The tests also pass on the tree before the move.

Layout changes in both plugins: new powercontext_lifecycle/ and claude_code_host.py / codex_host.py; removed powercontext_client_config.py, hooks/{bootstrap_context,prepared_context,prompt_privacy,diagnostics}.py, bootstrap_state.py, and Claude Code's scope_binding_errors.py. The #2250 comment that promised no plugin layout changes covered #2318 only.

pyproject.toml: ty no longer checks the Codex package copy. It resolves powercontext_lifecycle to the identical Claude Code copy and cannot resolve the Codex copy's relative imports. The source and the Claude Code copy are still checked.

Are there any user-facing changes?

No behavior changes. Requests, receipts, diagnostics, and hook output are unchanged, and the Claude Code and Codex characterization fixtures from #2255 pass as recorded.

  • Receipt and diagnostic state keep their paths, formats, and locking, so state written before an upgrade still works. Files left behind by an install over an older plugin are no longer imported.
  • When scripts/workspace_scope.py or scripts/scope_binding.py fails, the last traceback line names powercontext_lifecycle.scope_binding_errors.ScopeBindingRejectedError (and so on) instead of the plugin-root module. Exit status and stdout are unchanged.

For contributors, the SessionStart flow is edited in src/powercontext_integration_lifecycle/, and make integration-lifecycle updates both plugins.

How was this change tested?

  • uv run pytest -n 8 --require-claude-code-python-floor tests/test_hook_characterization.py tests/claude_code_plugin tests/codex_plugin tests/test_session_start_shells.py: 601 passed, 4 skipped (by design in test_client_transport.py). The Python 3.9 tests ran on the macOS system Python 3.9.6. With tests/test_integration_lifecycle.py, tests/test_hook_diagnostics.py, and tests/test_integration_manifest.py added: 675 passed at the tip. The same set passes at each of the four commits, and the Claude Code and Codex characterization fixtures, feat(codex): add a setting that turns prompt-time recall off #2453's Codex cases included, are unchanged at every commit.
  • make integration-lifecycle leaves no diff at any commit; ruff check --no-fix and ruff format --check pass at every commit.
  • On Python 3.9.6 with -I -S, the Claude Code package copy, claude_code_host, claude_code_settings, and the scripts import.
  • The two fix(integrations): report Scope resolution failures from prompt hooks #2317 tests for the bootstrap response deadline now drive the package flow. With the shared deadline reverted in the package, both fail.
  • tests/test_session_start_shells.py: 9 passed (7 recorded payloads and a broken-pipe case per host). They also pass on the tree before the move.
  • make check: passes, ty check included.
  • make unit-test: 8405 passed, 419 skipped, 5 failed. Four are in tests/test_ci_installation_roles.py, which installs wheels and fails on macOS. The fifth, tests/test_handoff_report.py::test_handoff_report_statement_count_does_not_grow_with_scope_count, passed in three reruns on this branch and on master; this PR changes neither file.
  • Real hosts, with the plugins exported by git archive and a local SQLite Server with enforced authentication. Claude Code ran this branch's tip. Codex ran the tip of the follow-up prompt-hook branch, which contains this branch unchanged; its trusted plugin install there is byte-identical to that tip's plugin.
    • Claude Code 2.1.277, headless claude -p, hooks on /usr/bin/python3 3.9.6: startup, resume, fork (--fork-session), and compact (/compact on --resume) each injected the bootstrap package, the Server recorded prepare_bootstrap_context and record_bootstrap_delivery, and the model answered the marker that only the package carries. The first prompt took the receipt. With an explicit Scope that does not exist, SessionStart reported invalid_response with http_status 404 and error_code scope_not_found and injected nothing, and the prompt hook showed the scope_resolve notice. With the Server stopped, it reported server_unavailable and wrote nothing to the diagnostic throttle. Not run: clear, which needs interactive mode.
    • Codex 0.167.0-alpha.3.2 (the CLI in Codex.app), in an isolated CODEX_HOME with the plugin installed from the exported marketplace and its hooks trusted: codex exec --json for startup, then resume and fork of that session. Each ran its own SessionStart (startup, resume, fork), and the Server recorded set_scope_binding, prepare_bootstrap_context, and record_bootstrap_delivery. The fork's rollout carries the bootstrap package that its own hook injected, and the model answered the bootstrap marker after resume and fork. The first prompt of each session took the receipt, so only lock files were left. With an explicit Scope that does not exist, or with the Server stopped, the hook requested no bootstrap package. Codex does not show SessionStart's stderr diagnostic, so the visible check for these cases is the prompt hook's, in the prompt-hook PR. Not run: clear and compact.
    • The recorded SessionStart payloads from both hosts have the same keys as the fixtures in tests/fixtures/recorded_host_payloads/.

AI usage statement

This PR was prepared with Claude Code (Claude Opus 5.5). It wrote the change and the tests, ran the checks and the real-host runs, and reviewed the change.

… Codex as a package

Claude Code and Codex now ship the whole integration lifecycle package as
powercontext_lifecycle/ in their plugin directories, generated from
src/powercontext_integration_lifecycle/. Their hooks and settings import
the client configuration, prepared-context, prompt-privacy, and
bootstrap-context helpers from it, and their single-file copies are gone.
WorkBuddy and Hermes keep their single-file copies where their hooks and
installers expect them.

A module added to the source now reaches both plugins without a new entry
in the generator, which prepares moving their hook flows into the module.
The copies use another name than the source, so a process that has already
imported the source, such as the test suite, still loads the plugin's own
copy. Ruff checks the Codex package copy against Python 3.9, like the
single-file copies it replaces.

`make integration-lifecycle` deletes the generated modules in a package
copy that the source does not have, and names each one; it leaves any other
file in a package copy for `make check` to report. `make check` also
rejects a subpackage in the source, which the copies would leave out.
Elsewhere in the plugins it reports a module named like a single-module
copy, as before, a module whose generated-file comment marks it as a copy,
such as a single-module copy left behind, and a module identical to a
source module, such as one copied straight from the source. A host module
that only shares a package module's name is not a copy.

Both plugins install their whole directory, so installation is unchanged,
and the Claude Code and Codex characterization fixtures pass as recorded.
The plugin tests that copy or purge plugin modules now handle the package.
…into the lifecycle package

Claude Code and Codex each kept their own diagnostic throttling
(hooks/diagnostics.py) and bootstrap receipt state (bootstrap_state.py).
The copies differed only in the name of the diagnostics state file and in
the two environment variables that locate the receipt directory.

Both now live once in src/powercontext_integration_lifecycle/ and reach
the plugins through the generated powercontext_lifecycle package. The
modules take the host differences as values: DiagnosticThrottle names the
state file, and BootstrapState names the two variables. Each plugin
declares its values once, in claude_code_host.py and codex_host.py, and
its hooks use those instances.

State files, environment variables, locking, and failure handling are
unchanged, and the Claude Code and Codex characterization fixtures pass as
recorded. The plugin tests reach the receipt state through the hooks'
declarations. They also set the host's plugin data directory, so a
declaration that swaps the two variables fails them. The lock-contention
test runs against the source module, which the copies must match.
…nto the lifecycle package

Claude Code's hooks/session_start.py and Codex's hooks/session_binding.py
ran the same SessionStart flow: resolve the Scope, request the bootstrap
package, record its delivery, keep the receipt for the first prompt, and
report failures on stderr. Their Scope clients carried the same bounded
HTTP helpers, open_bounded, bind_response_deadline, and complete_body, and
both plugins defined the same typed Scope binding errors. The response
deadline raised socket.timeout in Claude Code, which Python 3.9 needs, and
TimeoutError in Codex, the same class on the Python 3.11 and later that
Codex runs; the shared helper raises socket.timeout. Both hooks read the
bootstrap response, and the error code of a failed request, under the
deadline that the request started, so slow headers leave the body only
what remains; the flow keeps that deadline.

The flow now lives once in src/powercontext_integration_lifecycle/, in
session_start.py, with the HTTP helpers in transport.py and the errors in
scope_binding_errors.py, which replaces Claude Code's module of that name
and Codex's classes in scope_binding.py. A HostProfile in
claude_code_host.py and codex_host.py names what differs between the
hosts: the integration name, the User-Agent, the diagnostic component, the
diagnostic throttle, and the receipt state, which the plugins declared
separately before. The hooks keep what is native to their host: they
validate the payload, accept their own lifecycles, pass their settings to
the flow as plain values, write the package in their output format, and,
as before, catch what the flow does not handle. Along with the Scope
resolver, they pass the flow their Scope client's reader for the error
code of a failed bootstrap request, since the clients stay in the plugins.
Codex still binds the session when the lifecycle is missing or unknown.
Everything else that used the helpers or the errors, from the prompt hooks
to the Claude Code status line, imports them from the package.

These are the first package modules that import their siblings. They do so
relatively, because the copies use another name than the source, and only
modules that WorkBuddy and Hermes do not ship may do it; the source
package test enforces both. ty no longer checks the Codex package copy: it
resolves powercontext_lifecycle to the identical Claude Code copy, so it
cannot resolve the relative imports of the Codex copy. It still checks the
source and the Claude Code copy.

Requests, receipts, diagnostics, and output are unchanged, and the Claude
Code and Codex characterization fixtures pass as recorded. The hooks now
read the event identifier before the previous receipt and convert their
settings, handoff included, before the Scope is resolved; after a broken
pipe they point stdout at the null device before the receipt is cleared.
None of these readings can fail, and the null device affects only stdout,
so the new order is not observable. The bootstrap tests intercept the
Server requests in the shared flow instead of the hooks, and the tests
that keep or check a receipt reach it through the hook's HostProfile, with
their assertions unchanged. A new test for each host checks that the hook
passes its configured credential, time limits, size limit, and handoff to
the flow; the characterization fixtures do not cover all of them.
…loads

The characterization fixtures give the Claude Code and Codex SessionStart
hooks payloads written by hand. The hosts send more: Claude Code adds
context and cache fields on resume and fork, and the model and prompt ID
after compaction; Codex adds the permission mode. The payloads in
tests/fixtures/recorded_host_payloads were recorded from Claude Code
2.1.277 (startup, resume, compact, fork) and Codex 0.165.0-alpha.1.2
(startup, resume, fork), one for each lifecycle; clear was not recorded,
nor compact in Codex. The payloads keep every key and value except the
session ID, working directory, transcript path, and prompt ID, which
follow the characterization fixtures.

Each recorded payload replaces the payload of the characterized case in
which SessionStart injects the package and the first prompt reports its
receipt. It must lead to the same requests, output, and state, apart from
the lifecycle that the bootstrap request names.

A second test runs each host's hook with a stdout pipe that has no
reader, as when the host stops reading. The hook must exit with status 0,
report the failed delivery once, and keep no receipt. No other test
covers the hooks' redirection of stdout to the null device after a broken
pipe: without it, Python reports the broken pipe again at exit and the
process exits with status 120.

Both tests also pass before the flow moved into the lifecycle package.
Copilot AI balanced review requested due to automatic review settings November 8, 2026 20:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@hireview001 hireview001 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot AI balanced review requested due to automatic review settings November 10, 2026 06:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed d3ec37d. No blocking regression found. The shared lifecycle flow preserves host-specific behavior and existing receipt/diagnostic state without adding database tables or public APIs.

CI still needs to clear: the OpenDAL type-check failure is also present on base 63f021d (base job). This review exercised installed hook subprocesses and Python 3.9 compatibility; it did not rerun interactive host acceptance.

@hireview001 hireview001 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hidb4ai

hidb4ai commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

CI follow-up for unchanged head d3ec37d7: both acceptance jobs fail before any plugin hook runs. Every agent/acp.txt in the SQLite artifact and OceanBase artifact reports:

/installed-agent/acp_runner.py:16
ImportError: cannot import name 'text_block' from 'acp'

The harness runtime and dependency installer are unchanged by this PR; its only e2e/bub change updates a test fixture for the plugin layout. Please restore the shared ACP runner/SDK compatibility and rerun acceptance before merging. The OpenDAL type-check failure also remains the previously identified base failure. My code approval stands; CI is still blocking.

Copilot AI balanced review requested due to automatic review settings November 11, 2026 20:40
@Fengzdadi

Copy link
Copy Markdown
Contributor Author

Merged current master (923e3d4, which includes #2537's ACP pin) into the branch; acceptance is rerunning.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 229831c. The merge preserves the previously reviewed PR patch, with no new database tables or public APIs and no blocking regression found.

The earlier ACP import failure and OpenDAL type-check diagnostic no longer reproduce in local checks after the master updates. Full CI acceptance is still queued; this approval does not establish that those jobs have passed.

Copilot AI balanced review requested due to automatic review settings November 13, 2026 06:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@Fengzdadi

Copy link
Copy Markdown
Contributor Author

The red checks on 993b682 come from one flaky e2e test. tests (3.11, e2e) failed tests/e2e/test_dream_usage.py::test_memory_dream_approval_embedding_usage_survives_replay_and_restart at line 332: the captured log shows Model usage write failed; record will not be retried, and the embedding totals read before approval were zero (requests 0 instead of 1). #2518 failed the same assertion with the same warning on 611c249 (job). This PR changes nothing under src/powercontext/ or tests/e2e/, and every tests job passed on master at 195be9b.

The other tests jobs were cancelled by fail-fast, and the jobs that need tests, including the upgrade acceptance, were skipped, so Upgrade release gate and CI result failed as well. Re-running the failed jobs should clear them.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.6.0 v1.6.0 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants