Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/en/docs/integrations/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ a few Memory entries. Read [Memory and Handoff](../workflows/memory-and-handoff.
## Opt in to Session bootstrap context

Bootstrap context is off by default. To load a small, deterministic package before Codex's first prompt on
`startup`, `resume`, `clear`, or `compact`, start Codex with:
`startup`, `resume`, `clear`, `compact`, or `fork`, start Codex with:

```bash
export POWERCONTEXT_CODEX_BOOTSTRAP_CONTEXT=true
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/docs/integrations/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ Memory 用于长期保存可复用的决策、约束和状态;Handoff 用于

## 选择启用 Session 启动上下文

启动上下文默认关闭。若要在 Codex 的 `startup`、`resume`、`clear` 或 `compact` 事件中,于第一条 prompt 前加载一份
小而确定的上下文包,请用以下环境启动 Codex:
启动上下文默认关闭。若要在 Codex 的 `startup`、`resume`、`clear`、`compact` 或 `fork` 事件中,于第一条 prompt 前
加载一份小而确定的上下文包,请用以下环境启动 Codex:

```bash
export POWERCONTEXT_CODEX_BOOTSTRAP_CONTEXT=true
Expand Down
2 changes: 1 addition & 1 deletion integrations/codex/plugins/powercontext/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact",
"matcher": "startup|resume|clear|compact|fork",
"hooks": [
{
"type": "command",
Expand Down
10 changes: 9 additions & 1 deletion tests/codex_plugin/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import json
import re
import socket
import threading
import time
Expand Down Expand Up @@ -267,7 +268,6 @@ def test_codex_hooks_fix_session_and_data_plane_bindings() -> None:
configuration = json.loads((PLUGIN_ROOT / "hooks" / "hooks.json").read_text())

session_start = configuration["hooks"]["SessionStart"][0]
assert session_start["matcher"] == "startup|resume|clear|compact"
session_hook = session_start["hooks"][0]
assert "session_binding.py" in session_hook["command"]
assert session_hook["additionalContextLimit"] == 8192
Expand All @@ -276,6 +276,14 @@ def test_codex_hooks_fix_session_and_data_plane_bindings() -> None:
assert "bind_tools.py" in pre_tool_use["hooks"][0]["command"]


@pytest.mark.parametrize("source", ["startup", "resume", "clear", "compact", "fork"])
def test_codex_session_start_hook_runs_for_every_lifecycle_codex_sends(source: str) -> None:
configuration = json.loads((PLUGIN_ROOT / "hooks" / "hooks.json").read_text())

(session_start,) = configuration["hooks"]["SessionStart"]
assert re.fullmatch(session_start["matcher"], source)


def test_customer_artifact_workflow_does_not_parse_plugin_root_as_an_actions_expression() -> None:
workflow = (REPOSITORY_ROOT / ".github" / "workflows" / "build-artifacts.yml").read_text()

Expand Down
42 changes: 29 additions & 13 deletions tests/e2e/test_bootstrap_host_service_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,33 @@
"claude-code": PROJECT_ROOT / "integrations" / "claude-code" / "plugins" / "powercontext",
}
BOOTSTRAP_TEXT = "HOST-REAL-BOOTSTRAP: use the exact packaged startup context chain."
# Codex 0.165.0-alpha.1.2 sent this payload to the plugin's SessionStart hook for a forked session. The session ID,
# working directory, and transcript path take this test's values; every other key and value is as Codex sent it.
CODEX_RECORDED_FORK = {
"session_id": "",
"transcript_path": "",
"cwd": "",
"hook_event_name": "SessionStart",
"model": "gpt-6.1-sol",
"permission_mode": "bypassPermissions",
"source": "fork",
}


@pytest.mark.parametrize("host", ["codex", "claude-code"])
def test_packaged_host_hooks_complete_the_live_bootstrap_chain(tmp_path: Path, host: str) -> None:
@pytest.mark.parametrize(
("host", "session_start"),
[
("codex", {"hook_event_name": "SessionStart", "source": "startup"}),
("claude-code", {"hook_event_name": "SessionStart", "source": "startup"}),
("codex", CODEX_RECORDED_FORK),
],
ids=["codex", "claude-code", "codex-recorded-fork"],
)
def test_packaged_host_hooks_complete_the_live_bootstrap_chain(
tmp_path: Path,
host: str,
session_start: dict[str, str],
) -> None:
app = create_server_app(
settings=ServerSettings(
database=SQLiteConfig(url=f"sqlite+aiosqlite:///{tmp_path / f'{host}.db'}"),
Expand All @@ -70,17 +93,10 @@ def test_packaged_host_hooks_complete_the_live_bootstrap_chain(tmp_path: Path, h
environment = _hook_environment(tmp_path, host, base_url, scope_id)
session_id = f"{host}-bootstrap-session"

started = _run_hook(
plugin,
_session_start_script(host),
environment,
{
"hook_event_name": "SessionStart",
"source": "startup",
"session_id": session_id,
"cwd": str(PROJECT_ROOT),
},
)
payload = {**session_start, "session_id": session_id, "cwd": str(PROJECT_ROOT)}
if "transcript_path" in payload:
payload["transcript_path"] = str(tmp_path / f"{session_id}.jsonl")
started = _run_hook(plugin, _session_start_script(host), environment, payload)
bootstrap = json.loads(started.stdout)["hookSpecificOutput"]
assert bootstrap["hookEventName"] == "SessionStart"
assert BOOTSTRAP_TEXT in bootstrap["additionalContext"]
Expand Down
Loading