diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json
index 83616a9..2a0a82d 100644
--- a/.codex-plugin/plugin.json
+++ b/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "codex-context-window",
- "version": "0.2.0",
+ "version": "0.2.1",
"description": "Inject Codex context-window limits and usage through lifecycle hooks.",
"author": {
"name": "Nikolai Leon"
diff --git a/.github/release-notes/v0.2.1.md b/.github/release-notes/v0.2.1.md
new file mode 100644
index 0000000..6f14235
--- /dev/null
+++ b/.github/release-notes/v0.2.1.md
@@ -0,0 +1,29 @@
+Codex Context Window v0.2.1 fixes context-window reporting for subagents.
+
+Subagents now receive their own effective context-window limit through `SubagentStart`, including subagents created without inherited turns. The hook reads only the `transcript_path` supplied for the current hook invocation, so it can no longer mistake a parent session for the active subagent.
+
+## What changed
+
+- **Subagent startup coverage** — `SubagentStart` reports the effective context-window limit from the subagent's own `task_started` event.
+- **Correct transcript selection** — the hook relies exclusively on the hook-provided `transcript_path`.
+- **No unsafe session fallback** — the recursive `session_id` lookup was removed because parent and subagent identifiers are not interchangeable at hook time.
+- **Updated documentation** — the English and Russian READMEs describe the new lifecycle coverage and transcript behavior.
+
+Existing `SessionStart`, usage-update, and post-compaction signals are unchanged.
+
+## Upgrade
+
+Refresh the marketplace and reinstall the plugin:
+
+```bash
+codex plugin marketplace upgrade codex-context-window
+codex plugin add codex-context-window@codex-context-window
+```
+
+Then open a new chat or restart Codex so the updated hook registration is loaded.
+
+## Compatibility
+
+This release has no breaking configuration changes and supports the same macOS, Linux, and Windows targets as v0.2.0.
+
+**Full Changelog**: https://github.com/nleononline/codex-context-window/compare/v0.2.0...v0.2.1
diff --git a/Cargo.lock b/Cargo.lock
index f9fff67..eb16441 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "codex-context-window"
-version = "0.2.0"
+version = "0.2.1"
dependencies = [
"memchr",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 979034e..0b54bbf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "codex-context-window"
-version = "0.2.0"
+version = "0.2.1"
edition = "2021"
description = "Native Codex hook for reporting context-window limits and usage"
license = "MIT"
diff --git a/README.md b/README.md
index 27440d6..2eb25dd 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ The plugin includes prebuilt binaries for macOS, Linux, and Windows on Arm64 and
### Hook coverage
- `SessionStart` with source `startup`: provides the effective context-window limit and warns that automatic compaction may happen before reported usage reaches it.
+- `SubagentStart`: provides the effective context-window limit for the new subagent from its own session file.
- `UserPromptSubmit` and `PostToolUse`: provide the latest available context-window usage.
- `SessionStart` with source `compact`: reminds the model to verify that the task goal, requirements, decisions, and current progress were not lost.
diff --git a/docs/ru/README.md b/docs/ru/README.md
index 36d9546..e435eea 100644
--- a/docs/ru/README.md
+++ b/docs/ru/README.md
@@ -50,6 +50,7 @@ codex plugin add codex-context-window@codex-context-window
### Покрытие хуков
- `SessionStart` с источником `startup`: передает эффективный лимит контекстного окна и предупреждает, что автоматическое сжатие может произойти раньше, чем отображаемая заполненность достигнет лимита.
+- `SubagentStart`: передает эффективный лимит контекстного окна нового субагента из его собственного файла сессии.
- `UserPromptSubmit` и `PostToolUse`: передают последнюю доступную информацию о заполненности контекстного окна.
- `SessionStart` с источником `compact`: напоминает модели проверить, что цель задачи, требования, принятые решения и текущий прогресс не были потеряны.
diff --git a/hooks/hooks.json b/hooks/hooks.json
index e4d6567..e0e07c6 100644
--- a/hooks/hooks.json
+++ b/hooks/hooks.json
@@ -14,6 +14,18 @@
]
}
],
+ "SubagentStart": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "/bin/sh \"${PLUGIN_ROOT}/hooks/run-context-window.sh\"",
+ "commandWindows": "cmd.exe /d /c call \"%PLUGIN_ROOT%\\hooks\\run-context-window.cmd\"",
+ "timeout": 5
+ }
+ ]
+ }
+ ],
"UserPromptSubmit": [
{
"hooks": [
diff --git a/src/hook.rs b/src/hook.rs
index 566180e..9366a1c 100644
--- a/src/hook.rs
+++ b/src/hook.rs
@@ -1,8 +1,6 @@
-use crate::rollout::{
- find_session_file, read_context_window_limit, read_last_token_usage, TokenUsage,
-};
+use crate::rollout::{read_context_window_limit, read_last_token_usage, TokenUsage};
use serde::{Deserialize, Serialize};
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
const META_OPEN: &str = "";
const META_CLOSE: &str = "";
@@ -11,7 +9,6 @@ pub const CONTEXT_COMPACTED_MESSAGE: &str = "YOUR CONTEXT WAS JUST COMPACT
#[derive(Debug, Default, Deserialize)]
pub struct HookInput {
- pub session_id: Option,
pub transcript_path: Option,
pub hook_event_name: Option,
pub source: Option,
@@ -57,54 +54,41 @@ fn append_debug_hook(message: String, hook_event_name: &str) -> String {
format!("{content} (hook: {hook_event_name}){META_CLOSE}")
}
-fn session_file(input: &HookInput, codex_home: Option<&Path>) -> Option {
- find_session_file(
- input.transcript_path.as_deref(),
- input.session_id.as_deref(),
- codex_home,
- )
-}
+fn context_window_message(input: &HookInput) -> Option {
+ let session_file = input.transcript_path.as_deref()?;
-fn context_window_message(input: &HookInput, codex_home: Option<&Path>) -> Option {
- let session_file = session_file(input, codex_home)?;
-
- match read_last_token_usage(&session_file) {
+ match read_last_token_usage(session_file) {
Ok(Some(usage)) => Some(format_context_window(usage)),
Ok(None) | Err(_) => None,
}
}
-fn context_window_limit_message(input: &HookInput, codex_home: Option<&Path>) -> Option {
- let session_file = session_file(input, codex_home)?;
+fn context_window_limit_message(input: &HookInput) -> Option {
+ let session_file = input.transcript_path.as_deref()?;
- match read_context_window_limit(&session_file) {
+ match read_context_window_limit(session_file) {
Ok(Some(limit)) => Some(format_context_window_limit(limit)),
Ok(None) | Err(_) => None,
}
}
-pub fn message_for_hook(input: &HookInput, codex_home: Option<&Path>) -> Option {
+pub fn message_for_hook(input: &HookInput) -> Option {
match (input.hook_event_name.as_deref(), input.source.as_deref()) {
- (Some("SessionStart"), Some("startup")) => context_window_limit_message(input, codex_home),
+ (Some("SessionStart"), Some("startup")) => context_window_limit_message(input),
+ (Some("SubagentStart"), _) => context_window_limit_message(input),
(Some("SessionStart"), Some("compact")) => Some(CONTEXT_COMPACTED_MESSAGE.to_owned()),
- (Some("UserPromptSubmit"), _) | (Some("PostToolUse"), _) => {
- context_window_message(input, codex_home)
- }
+ (Some("UserPromptSubmit"), _) | (Some("PostToolUse"), _) => context_window_message(input),
_ => None,
}
}
-pub fn create_hook_output(input: &HookInput, codex_home: Option<&Path>) -> Option {
- create_hook_output_with_debug(input, codex_home, false)
+pub fn create_hook_output(input: &HookInput) -> Option {
+ create_hook_output_with_debug(input, false)
}
-pub fn create_hook_output_with_debug(
- input: &HookInput,
- codex_home: Option<&Path>,
- debug_enabled: bool,
-) -> Option {
+pub fn create_hook_output_with_debug(input: &HookInput, debug_enabled: bool) -> Option {
let hook_event_name = input.hook_event_name.as_deref()?;
- let mut message = message_for_hook(input, codex_home)?;
+ let mut message = message_for_hook(input)?;
if debug_enabled {
message = append_debug_hook(message, hook_event_name);
}
@@ -159,11 +143,10 @@ mod tests {
transcript_path: Some(rollout),
hook_event_name: Some("SessionStart".to_owned()),
source: Some("startup".to_owned()),
- ..HookInput::default()
};
assert_eq!(
- serde_json::to_value(create_hook_output(&startup, None).unwrap()).unwrap(),
+ serde_json::to_value(create_hook_output(&startup).unwrap()).unwrap(),
json!({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
@@ -174,6 +157,29 @@ mod tests {
);
}
+ #[test]
+ fn returns_subagent_context_limit() {
+ let temp = TempDirectory::new();
+ let rollout = temp.path().join("rollout-test-subagent.jsonl");
+ fs::write(&rollout, format!("{}\n", task_started(258_400))).unwrap();
+ let subagent_start = HookInput {
+ transcript_path: Some(rollout),
+ hook_event_name: Some("SubagentStart".to_owned()),
+ source: None,
+ };
+
+ assert_eq!(
+ serde_json::to_value(create_hook_output(&subagent_start).unwrap()).unwrap(),
+ json!({
+ "hookSpecificOutput": {
+ "hookEventName": "SubagentStart",
+ "additionalContext":
+ "YOUR CONTEXT WINDOW LIMIT IS 258400 TOKENS. CODEX MAY AUTO-COMPACT BEFORE REPORTED USAGE REACHES THE LIMIT, SO PRESERVE IMPORTANT TASK STATE IN ADVANCE."
+ }
+ })
+ );
+ }
+
#[test]
fn returns_compaction_output() {
let compact = HookInput {
@@ -182,7 +188,7 @@ mod tests {
..HookInput::default()
};
assert_eq!(
- serde_json::to_value(create_hook_output(&compact, None).unwrap()).unwrap(),
+ serde_json::to_value(create_hook_output(&compact).unwrap()).unwrap(),
json!({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
@@ -201,10 +207,8 @@ mod tests {
..HookInput::default()
};
assert_eq!(
- serde_json::to_value(
- create_hook_output_with_debug(&session_start, None, true).unwrap()
- )
- .unwrap(),
+ serde_json::to_value(create_hook_output_with_debug(&session_start, true).unwrap())
+ .unwrap(),
json!({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
@@ -224,26 +228,23 @@ mod tests {
..HookInput::default()
};
- assert_eq!(create_hook_output(&input, None), None);
+ assert_eq!(create_hook_output(&input), None);
}
}
#[test]
- fn injects_usage_for_model_visible_hook() {
+ fn injects_usage_from_hook_transcript() {
let temp = TempDirectory::new();
- let session_id = "session-123";
- let session_directory = temp.path().join("sessions/2026/07/27");
- fs::create_dir_all(&session_directory).unwrap();
- let rollout = session_directory.join(format!("rollout-current-{session_id}.jsonl"));
+ let rollout = temp.path().join("transcript.jsonl");
fs::write(&rollout, format!("{}\n", token_count(250, 1_000))).unwrap();
let input = HookInput {
- session_id: Some(session_id.to_owned()),
+ transcript_path: Some(rollout),
hook_event_name: Some("UserPromptSubmit".to_owned()),
..HookInput::default()
};
assert_eq!(
- serde_json::to_value(create_hook_output(&input, Some(temp.path())).unwrap()).unwrap(),
+ serde_json::to_value(create_hook_output(&input).unwrap()).unwrap(),
json!({
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
@@ -257,41 +258,33 @@ mod tests {
#[test]
fn emits_nothing_when_usage_is_unknown() {
let input = HookInput {
- session_id: Some("missing-session".to_owned()),
hook_event_name: Some("PostToolUse".to_owned()),
..HookInput::default()
};
- assert_eq!(
- create_hook_output(&input, Some(Path::new("/definitely/missing"))),
- None
- );
+ assert_eq!(create_hook_output(&input), None);
}
#[test]
fn emits_nothing_when_startup_limit_is_unknown() {
let input = HookInput {
- session_id: Some("missing-session".to_owned()),
hook_event_name: Some("SessionStart".to_owned()),
source: Some("startup".to_owned()),
..HookInput::default()
};
- assert_eq!(
- create_hook_output(&input, Some(Path::new("/definitely/missing"))),
- None
- );
+ assert_eq!(create_hook_output(&input), None);
}
#[test]
fn ignores_unconfigured_hook_events() {
- for hook_event_name in ["PreToolUse", "SubagentStart"] {
+ for hook_event_name in ["PreToolUse", "SessionEnd"] {
let input = HookInput {
hook_event_name: Some(hook_event_name.to_owned()),
..HookInput::default()
};
- assert_eq!(create_hook_output(&input, None), None);
+ assert_eq!(create_hook_output(&input), None);
}
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 165875a..4f58384 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,23 +9,10 @@ pub use hook::{
format_context_window_limit, message_for_hook, HookInput, HookOutput, HookSpecificOutput,
CONTEXT_COMPACTED_MESSAGE,
};
-pub use rollout::{
- find_session_file, read_context_window_limit, read_last_token_usage, TokenUsage,
-};
+pub use rollout::{read_context_window_limit, read_last_token_usage, TokenUsage};
use std::env;
use std::ffi::OsString;
-use std::path::PathBuf;
-
-pub(crate) fn non_empty_env_path(name: &str) -> Option {
- env::var_os(name)
- .filter(|value| !value.is_empty())
- .map(PathBuf::from)
-}
-
-pub fn environment_codex_home() -> Option {
- non_empty_env_path("CODEX_HOME")
-}
pub fn environment_debug_enabled() -> bool {
env::var_os("CODEX_CONTEXT_WINDOW_DEBUG")
diff --git a/src/main.rs b/src/main.rs
index ae55d68..b508413 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,4 @@
-use codex_context_window::{
- create_hook_output_with_debug, environment_codex_home, environment_debug_enabled, HookInput,
-};
+use codex_context_window::{create_hook_output_with_debug, environment_debug_enabled, HookInput};
use std::io::{self, Read, Write};
fn debug(message: &str) {
@@ -24,10 +22,7 @@ fn main() {
}
};
- let codex_home = environment_codex_home();
- let Some(output) =
- create_hook_output_with_debug(&input, codex_home.as_deref(), environment_debug_enabled())
- else {
+ let Some(output) = create_hook_output_with_debug(&input, environment_debug_enabled()) else {
return;
};
diff --git a/src/rollout.rs b/src/rollout.rs
index 5e69165..76058bf 100644
--- a/src/rollout.rs
+++ b/src/rollout.rs
@@ -1,9 +1,8 @@
-use crate::non_empty_env_path;
use memchr::memrchr_iter;
use serde_json::Value;
-use std::fs::{self, File};
+use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
-use std::path::{Path, PathBuf};
+use std::path::Path;
const READ_CHUNK_SIZE: usize = 64 * 1024;
@@ -112,88 +111,12 @@ pub fn read_context_window_limit(file_path: &Path) -> io::Result