Skip to content

[AI-1459] SessionStart memory index: Codex CLI adapter - #395

Merged
realtonyyoung merged 3 commits into
mainfrom
tonyyoung/ai-1459-codex-sessionstart-memory
Jul 29, 2026
Merged

realtonyyoung merged 3 commits into
mainfrom
tonyyoung/ai-1459-codex-sessionstart-memory

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Wires the shared SessionStart memory subsystem into the Codex hook — the third harness onto that foundation after Claude and Cursor.

What the foundation already gave us

Worth stating up front, because it shrank this change a lot: the shared subsystem already had a SessionStartHarness.Codex output adapter rendering exactly the required envelope, and its orchestrator already implements the monotonic budget (a Remaining() recomputed before every await), the lease state machine, and the fail-open catch-all. So this PR is the call site, not a renderer or a budget mechanism.

Ordering

The fetch starts before the lifecycle POST so the two overlap, then is awaited under the budget remaining at that instant — HookBudget.Remaining(processStart, "session-start") - HookBudget.Safety, the headroom reserved for serialization plus the write — immediately before the stdout handshake.

Codex blocks on this hook's stdout, so:

  • an unreachable/slow memory server degrades to the minimal handshake rather than delaying the write;
  • the payload is fully serialized before the first byte, so a renderer fault can never emit a partial rich object followed by a second minimal one (Codex's parser accepts exactly one JSON value);
  • post-stdout work (watcher-ensure, spool drain) still runs strictly after the write, through the existing ordering seam.

Scope safety

Mirrors the Cursor adapter's guard: the git root discovered from the payload cwd is preferred, the payload cwd is the fallback, and a blank scope root skips injection entirely rather than letting the shared resolver fall back to the hook process's cwd and inject an unrelated repository's memories.

Eligibility comes for free from existing structure: nested KCAP_SKIP=1 invocations return before this path, and excluded/disabled repos short-circuit above it — neither reaches the memory subsystem. Codex's SessionStart payload carries no lifecycle source, so the reason is reported as New; re-injection on a resume of the same session id is prevented by the shared lease keyed on (harness, session id) rather than by a signal we cannot observe.

One deliberate asymmetry (please sanity-check this)

The no-fragment path keeps the pre-existing handshake constant instead of the shared adapter's rendering of the same envelope. Both encode {"continue":true}, but the adapter appends a trailing newline to every envelope it renders (json + "\n" — which Claude and Cursor already ship). Adopting it here would change the bytes Codex receives on every no-memory SessionStart (opt-out, exclusion, provider failure, budget exhaustion) for no gain, and byte-identity on that path is an acceptance criterion. So the constant wins for null; the adapter owns the fragment-bearing shape, which is the only genuinely new output. A test pins the asymmetry so it is a recorded decision, not an accident.

Tests

CodexSessionStartMemoryTests (8, new) pins:

  • byte-identical minimal handshake when no fragment exists (the load-bearing regression — it caught the trailing-newline difference above);
  • one combined object carrying continue + hookSpecificOutput.additionalContext;
  • a single JSON value with no trailing document;
  • quote / backslash / newline / tab / non-BMP / empty-string escaping round-trips;
  • the recorded newline asymmetry;
  • Stop never carrying memory context.

Verified locally: CodexSessionStartMemoryTests 8/8 · CodexStdoutContractTests 2/2 (ordering contract intact) · ClaudeHookCommandTests 35/35 · CursorHookCommandTests 38/38 · CodexHooksInstallerTests 8/8. check-linear-ids.sh clean. AOT publish generates native code with no IL/trim warnings.

Spec-vs-code note

The issue's rev-2 spec asserted that lifecycle POST and repo enrichment should be post-stdout. In the code as it stands they already run before the handshake write, and this PR did not restructure that — it inserts the memory fetch into the existing ordering rather than rewriting Codex's hook sequence, which would be a larger and riskier change than this issue's scope.

Closes AI-1459.

Wires the shared SessionStart memory subsystem into the Codex hook, the third
harness onto that foundation after Claude and Cursor.

The fetch is started BEFORE the lifecycle POST so the two overlap, then awaited
under the budget remaining at that instant (HookBudget.Remaining minus Safety,
the headroom reserved for serialization and the write) immediately before the
stdout handshake. Codex blocks on this hook's stdout, so an unreachable memory
server degrades to the minimal handshake instead of delaying the write; the
payload is fully serialized before the first byte, so a renderer fault can never
emit a partial rich object followed by a second minimal one.

Scope safety mirrors the Cursor adapter: the git root discovered from the payload
cwd is preferred, the payload cwd is the fallback, and a blank scope root skips
injection entirely rather than letting the shared resolver fall back to the hook
process's cwd and inject an unrelated repository's memories. Nested KCAP_SKIP=1
invocations already return before this path, and excluded/disabled repos are
short-circuited above it, so neither reaches the memory subsystem.

The no-fragment path deliberately keeps the existing handshake constant rather
than the shared adapter's rendering of the same envelope: both encode
{"continue":true}, but the adapter appends a trailing newline to every envelope
(as Claude and Cursor already ship), and adopting it would change the bytes Codex
receives on every no-memory SessionStart. Byte-identity there is an acceptance
criterion, so the constant wins; the adapter still owns the fragment-bearing
shape, which is the only genuinely new output.

Tests pin the byte-identical minimal handshake, the single-JSON-value contract
(no trailing document), quote/newline/control/non-BMP escaping round-trips, the
recorded newline asymmetry, and that Stop never carries memory context.

Closes AI-1459.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 29, 2026

Copy link
Copy Markdown

AI-1459

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Codex hook: inject SessionStart memory fragment with budgeted stdout contract

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Start SessionStart memory fetch before lifecycle POST; await under remaining hook budget.
• Emit rich SessionStart envelope only when a fragment exists; otherwise preserve byte-identical
 handshake.
• Add unit tests pinning Codex’s single-JSON stdout contract and newline asymmetry.
Diagram

graph TD
  A["CodexHookCommand.Handle"] --> B["StartMemoryIndexTask"] --> C{{"SessionStartMemoryOrchestrator"}}
  A --> D["PostOrSpool lifecycle"] --> E[("Hook spool")]
  C --> F["AwaitMemoryFragmentAsync"] --> G["WriteSessionStartOutput"] --> H{{"Codex stdout parser"}}

  subgraph Legend
    direction LR
    _proc["Process/step"] ~~~ _store[("Local store")] ~~~ _ext{{"External/other subsystem"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always use shared adapter rendering (even for null fragment)
  • ➕ One rendering path for all SessionStart outputs across harnesses
  • ➕ Centralizes formatting decisions (key order, newline) in one place
  • ➖ Changes Codex’s no-memory bytes globally (newline and/or shape drift risk)
  • ➖ Increases risk of protocol regressions where Codex expects historical handshake exactness
2. Extend adapter to support a 'no-trailing-newline' option
  • ➕ Keeps Codex no-memory bytes stable while still delegating all rendering to shared code
  • ➕ Makes newline behavior an explicit policy choice per harness
  • ➖ Adds API surface/complexity to the shared subsystem for a single harness quirk
  • ➖ Must ensure other harnesses don’t accidentally opt out and change their wire format
3. Standardize on newline-terminated minimal handshake for Codex too
  • ➕ Eliminates the asymmetry and makes envelopes consistent across harnesses
  • ➕ Lets Codex always use shared adapter output
  • ➖ Still a breaking-bytes change on the most common (no-fragment) path
  • ➖ Requires confidence Codex tolerates newline on minimal handshake across all clients/versions

Recommendation: Keep the PR’s current strategy: preserve byte-identical minimal handshake when fragment is absent, and delegate only the new fragment-bearing envelope to the shared adapter. Given Codex’s stdout parsing constraints and the stated acceptance criterion (no-memory path must be indistinguishable from pre-memory behavior), this minimizes regression risk while still reusing the shared subsystem where it matters. If future cleanup is desired, the most maintainable alternative is adding an explicit 'no-newline' rendering option to the shared adapter—only after confirming it won’t introduce cross-harness ambiguity.

Files changed (2) +245 / -4

Enhancement (1) +147 / -4
CodexHookCommand.csAdd SessionStart memory fetch + fragment-aware stdout writer for Codex +147/-4

Add SessionStart memory fetch + fragment-aware stdout writer for Codex

• Introduces a SessionStart-specific stdout writer that emits the shared memory envelope only when a fragment is present, otherwise preserving the historical minimal handshake bytes. Starts the memory fetch before the lifecycle POST, then awaits it under remaining hook budget immediately before the stdout write, with fail-open behavior and scope-root safety guards. Adds test seams (processStart, client/store factories) to make timing and lease-store behavior deterministic in unit tests.

src/Capacitor.Cli/Commands/CodexHookCommand.cs

Tests (1) +98 / -0
CodexSessionStartMemoryTests.csPin Codex SessionStart stdout JSON contract with/without memory fragment +98/-0

Pin Codex SessionStart stdout JSON contract with/without memory fragment

• Adds unit tests that verify byte-identical minimal handshake output when no fragment exists, and a single well-formed combined JSON object when a fragment is present (including newline behavior). Also asserts the output remains exactly one JSON value (no trailing document) and that Stop output never includes memory context.

test/Capacitor.Cli.Tests.Unit/CodexSessionStartMemoryTests.cs

@qodo-code-review

qodo-code-review Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Unauthenticated memory client ✓ Resolved 🐞 Bug ≡ Correctness
Description
StartMemoryIndexTask defaults to a bare new HttpClient() with no auth, so on authenticated servers
the memory index request will 401 and never yield a Ready fragment (degrading to the minimal
handshake every time). The shared provider’s 401-retry flow expects an Authorization header on the
first client to extract the rejected access token, which this default path cannot provide.
Code

src/Capacitor.Cli/Commands/CodexHookCommand.cs[R138-141]

+            var provider = new SessionStartMemoryContextProvider(
+                new SessionStartMemoryScopeResolver(),
+                memoryClientFactory ?? ((_, ct) => Task.FromResult(new HttpClient())),
+                disposeClients: true);
Evidence
Codex’s default client factory returns an unauthenticated HttpClient. The memory provider’s logic
demonstrates it expects an Authorization header (reads it to retry after 401), and the Claude
harness shows the intended pattern: create an auth-aware client with rejected-token recovery.

src/Capacitor.Cli/Commands/CodexHookCommand.cs[122-147]
src/Capacitor.Cli/SessionStartMemory/SessionStartMemoryContextProvider.cs[22-33]
src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[38-41]
src/Capacitor.Cli.Core/HttpClientExtensions.cs[44-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Codex’s SessionStart memory integration builds `SessionStartMemoryContextProvider` with a default `memoryClientFactory` that returns `new HttpClient()` (no bearer token). For servers where `/api/memories/index` is protected, this means the request will 401 and the memory fragment will never be injected.

### Issue Context
- `SessionStartMemoryContextProvider` explicitly handles `401 Unauthorized` by retrying with a refreshed client, using the rejected token read from `firstClient.DefaultRequestHeaders.Authorization`.
- With a default unauthenticated client, that header is absent and the retry cannot target the rejected token.
- Claude wires the memory client factory through `HttpClientExtensions.CreateClientWithAuthStatusAsync(..., rejectedAccessToken: ...)`, which creates an authenticated client when possible.

### Fix
- Change Codex’s default `memoryClientFactory` to reuse the existing auth-aware path:
 - `async (rejected, ct) => (await HttpClientExtensions.CreateClientWithAuthStatusAsync(baseUrl, ct, allowAutoRedirect: false, rejectedAccessToken: rejected)).Client`
- Add a fail-open guard before invoking auth discovery to avoid `EnsureAbsolute` hard-exiting on malformed URLs (similar to `PostBestEffortAsync`):
 - if `string.IsNullOrWhiteSpace(baseUrl) || !HttpClientExtensions.IsAcceptableUrl(baseUrl)` then skip memory injection (`return Task.FromResult<string?>(null)`)

### Fix Focus Areas
- src/Capacitor.Cli/Commands/CodexHookCommand.cs[122-151]
- src/Capacitor.Cli/SessionStartMemory/SessionStartMemoryContextProvider.cs[22-33]
- src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[38-41]
- src/Capacitor.Cli.Core/HttpClientExtensions.cs[44-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. README.md not updated for Codex ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The PR changes Codex SessionStart behavior to fetch and inject a SessionStart memory fragment into
the stdout envelope, but README.md still documents Codex as not wired for the SessionStart
team-memory index. This leaves user-facing documentation incorrect for Codex users after this
change.
Code

src/Capacitor.Cli/Commands/CodexHookCommand.cs[R361-372]

+        // Start the team-memory fetch BEFORE the lifecycle POST so the two overlap; it is awaited
+        // (budget-capped) immediately before the stdout write below, which is the only consumer.
+        // Deliberately started after the exclusion/disabled early-outs above so an excluded repo
+        // never reaches the memory subsystem at all.
+        var memoryTask = StartMemoryIndexTask(
+            baseUrl, sessionId,
+            // The git root discovered above (stamped onto the node) is preferred; the payload cwd is
+            // the fallback. Never a process-cwd fallback — see StartMemoryIndexTask's scope note.
+            TryGetString(enrichedNode, "workspace_root") ?? TryGetString(enrichedNode, "cwd"),
+            AppConfig.ResolvedProfile?.Profile?.DisableMemoryIndex is true,
+            HookBudget.Remaining(processStart, "session-start") - HookBudget.Safety,
+            memoryClientFactory, memoryStoreFactory);
Evidence
PR Compliance ID 6 requires updating README.md in the same PR when user-facing CLI behavior
changes. The new Codex SessionStart path starts the memory-index fetch (user-visible injected
context), while the README still states the SessionStart team-memory index is only for Claude/Cursor
and that Codex is not wired.

CLAUDE.md: Update README.md in the Same PR When User-Facing CLI Surface Changes
src/Capacitor.Cli/Commands/CodexHookCommand.cs[361-372]
README.md[201-213]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This PR wires SessionStart team-memory index injection for Codex, but `README.md` still claims Codex is not wired, making the docs inaccurate.

## Issue Context
Codex now starts a SessionStart memory fetch and may emit a fragment-bearing `hookSpecificOutput.additionalContext` envelope. The README currently describes SessionStart team-memory index as only applying to Claude and Cursor, and the capability matrix row for Codex says `Hook/extension wired: no`.

## Fix Focus Areas
- README.md[188-220]
- src/Capacitor.Cli/Commands/CodexHookCommand.cs[361-372]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Safety double-subtracted 🐞 Bug ≡ Correctness
Description
CodexHookCommand subtracts HookBudget.Safety from HookBudget.Remaining(), but Remaining() already
subtracts Safety internally, shrinking both the memory-fetch budget and the WaitAsync cap by an
extra 1.5s. This makes the new memory injection path time out/skip more often than intended even
when time remains in the hook ceiling.
Code

src/Capacitor.Cli/Commands/CodexHookCommand.cs[R162-169]

+    static async Task<string?> AwaitMemoryFragmentAsync(Task<string?> task, long processStart) {
+        try {
+            var budget = HookBudget.Remaining(processStart, "session-start") - HookBudget.Safety;
+
+            if (budget <= TimeSpan.Zero)
+                return task.IsCompletedSuccessfully ? task.Result : null;
+
+            return await task.WaitAsync(budget);
Evidence
HookBudget.Remaining() already subtracts Safety, but the new Codex code subtracts Safety
again, reducing the available budget twice. Claude’s equivalent helper uses Remaining() directly,
reinforcing that Codex’s extra subtraction is anomalous.

src/Capacitor.Cli/Commands/HookBudget.cs[10-21]
src/Capacitor.Cli/Commands/CodexHookCommand.cs[162-173]
src/Capacitor.Cli/Commands/CodexHookCommand.cs[365-372]
src/Capacitor.Cli/Commands/ClaudeHookCommand.cs[837-843]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`HookBudget.Remaining(processStart, ...)` already returns a safety-adjusted remaining budget (it subtracts `HookBudget.Safety` internally). The Codex SessionStart memory integration subtracts `HookBudget.Safety` *again* when (a) starting the memory task and (b) awaiting it, effectively reserving safety twice and prematurely expiring the memory fetch.

### Issue Context
- `HookBudget.Remaining()` is defined as `Ceiling - elapsed - Safety`.
- Codex’s memory path uses `HookBudget.Remaining(...) - HookBudget.Safety` in two places.
- Claude’s analogous logic awaits under `HookBudget.Remaining(...)` (no extra subtraction), suggesting Codex’s extra subtraction is unintended.

### Fix
- Use `HookBudget.Remaining(processStart, "session-start")` directly (no additional `- HookBudget.Safety`) for:
 - the `budget` passed into `StartMemoryIndexTask(...)`
 - the `budget` used in `AwaitMemoryFragmentAsync(...)`
- If you truly need a *second* reserve distinct from `HookBudget.Safety`, introduce a separate constant (e.g., `SerializationReserve`) instead of reusing `HookBudget.Safety`.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/CodexHookCommand.cs[162-173]
- src/Capacitor.Cli/Commands/CodexHookCommand.cs[365-372]
- src/Capacitor.Cli/Commands/HookBudget.cs[10-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/Capacitor.Cli/Commands/CodexHookCommand.cs
Comment on lines +162 to +169
static async Task<string?> AwaitMemoryFragmentAsync(Task<string?> task, long processStart) {
try {
var budget = HookBudget.Remaining(processStart, "session-start") - HookBudget.Safety;

if (budget <= TimeSpan.Zero)
return task.IsCompletedSuccessfully ? task.Result : null;

return await task.WaitAsync(budget);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Safety double-subtracted 🐞 Bug ≡ Correctness

CodexHookCommand subtracts HookBudget.Safety from HookBudget.Remaining(), but Remaining() already
subtracts Safety internally, shrinking both the memory-fetch budget and the WaitAsync cap by an
extra 1.5s. This makes the new memory injection path time out/skip more often than intended even
when time remains in the hook ceiling.
Agent Prompt
### Issue description
`HookBudget.Remaining(processStart, ...)` already returns a safety-adjusted remaining budget (it subtracts `HookBudget.Safety` internally). The Codex SessionStart memory integration subtracts `HookBudget.Safety` *again* when (a) starting the memory task and (b) awaiting it, effectively reserving safety twice and prematurely expiring the memory fetch.

### Issue Context
- `HookBudget.Remaining()` is defined as `Ceiling - elapsed - Safety`.
- Codex’s memory path uses `HookBudget.Remaining(...) - HookBudget.Safety` in two places.
- Claude’s analogous logic awaits under `HookBudget.Remaining(...)` (no extra subtraction), suggesting Codex’s extra subtraction is unintended.

### Fix
- Use `HookBudget.Remaining(processStart, "session-start")` directly (no additional `- HookBudget.Safety`) for:
  - the `budget` passed into `StartMemoryIndexTask(...)`
  - the `budget` used in `AwaitMemoryFragmentAsync(...)`
- If you truly need a *second* reserve distinct from `HookBudget.Safety`, introduce a separate constant (e.g., `SerializationReserve`) instead of reusing `HookBudget.Safety`.

### Fix Focus Areas
- src/Capacitor.Cli/Commands/CodexHookCommand.cs[162-173]
- src/Capacitor.Cli/Commands/CodexHookCommand.cs[365-372]
- src/Capacitor.Cli/Commands/HookBudget.cs[10-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/Capacitor.Cli/Commands/CodexHookCommand.cs Outdated
realtonyyoung and others added 2 commits July 29, 2026 14:00
/api/memories/index is bearer-authenticated, and the shared provider hands a
rejected bearer back to the client factory so it can mint a refreshed client. The
production fallback was a bare new HttpClient(), so BOTH the initial request and
the 401 refresh went out anonymous: the provider recorded a retryable failure and
Codex silently received no memory context on every authenticated deployment.

Replaced with the same auth-aware refresh factory ClaudeHookCommand uses, named
(DefaultMemoryClientFactory) so it is assertable, and disposeClients now only
disposes clients we created — an injected factory's client is its caller's, and
may be handed back again on the refresh call.

Guarded by a source-level test scoped to the factory body: a credential-attaching
assertion would need KCAP_CONFIG_DIR bound before PathHelpers' static init, which
a parallel shared assembly cannot guarantee. Mutation-tested — reintroducing the
bare client fails the guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ment Codex (Qodo)

Two Qodo findings.

1. Fail-open regression introduced by the auth fix. The authenticated-client
   helper funnels through EnsureAbsolute, which prints a hint and calls
   Environment.Exit(2) on a URL it cannot accept. From SessionStart that would
   kill the hook BEFORE the stdout handshake, so Codex would receive no output at
   all and reject the session — strictly worse than skipping an optional memory
   fragment. CanAttemptMemoryInjection now rejects a blank/unacceptable URL before
   any auth discovery, mirroring PostBestEffortAsync's guard. Tested via the
   predicate rather than the exit, since tripping Environment.Exit would take the
   test host down.

2. README documented Codex as not wired for the SessionStart team-memory index.
   Updated the capability-matrix row and the two prose mentions (harness list and
   the per-vendor envelope field), per the repo rule that user-facing CLI surface
   changes update README.md in the same PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

NO FINDINGS

@realtonyyoung

Copy link
Copy Markdown
Collaborator Author

Review close-out

Codex review flow: clean (2 rounds on the original + 2 on the delta). Qodo: both findings addressed.

Findings fixed

  1. [P1, found independently by both codex and Qodo] Unauthenticated memory client. The production fallback was a bare new HttpClient(). Since /api/memories/index is bearer-authed and the shared provider hands the rejected bearer back to the factory after a 401, both the initial call and the refresh went out anonymous → retryable failure → Codex silently never received memory context on any authenticated deployment. Fixed with the same auth-aware refresh factory ClaudeHookCommand uses, extracted as DefaultMemoryClientFactory so it is assertable; disposeClients now only disposes clients we created.

  2. [P1, found by Qodo] Fail-open regression that fix Auto-install Claude Code plugin during setup #1 introduced. The auth helper funnels through EnsureAbsolute, which calls Environment.Exit(2) on an unacceptable URL — from SessionStart that would kill the hook before the handshake, so Codex would receive no stdout and reject the session. CanAttemptMemoryInjection now gates auth discovery on the same IsAcceptableUrl predicate the validator uses.

  3. [Rule violation, Qodo] README. Capability-matrix row for Codex CLI plus two prose mentions updated.

Correction to this PR's original description

The description claimed fail-open as an end-to-end property. That is accurate only for the memory branch. A codex delta review correctly showed the SessionStart handshake can still be lost on an unacceptable base URL, because AgentHookPoster.PostOrSpoolAsync performs unguarded auth discovery before the write.

That hole is pre-existing — verified on origin/main, where HandleSessionStart already called PostOrSpoolAsync ahead of the handshake — so it is neither introduced nor widened here. It is not fixed in this PR because PostOrSpoolAsync is shared by all seven vendor hooks and the right disposition for an unpostable URL (skip-and-handshake vs. spool vs. exit) is a product decision affecting ShouldSpawnAfter/watcher sequencing. Tracked as AI-1586 with the mechanism, provenance, and test guidance. Codex reviewed and accepted that scope split.

Verification

New CodexSessionStartMemoryTests 16/16 · CodexStdoutContractTests 2/2 (ordering contract intact) · Claude 35/35 · Cursor 38/38 · installer 8/8. The auth guard is mutation-tested — reintroducing the bare client makes it fail. CI: both AOT publish checks, the linear-ID guard, and ubuntu build+test green; windows in flight.

@realtonyyoung
realtonyyoung merged commit 55dca3a into main Jul 29, 2026
6 checks passed
@realtonyyoung
realtonyyoung deleted the tonyyoung/ai-1459-codex-sessionstart-memory branch July 29, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant