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
29 changes: 29 additions & 0 deletions devlog/_plan/261004_cache_identity/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Two independent cache-identity fixes

Public callers can provide a conversation marker without the session header consumed downstream, while Devin currently allocates a trajectory for every turn. This unit prepares separate replacements for #6521 and #6488, preserving their useful behavior and adding deterministic identity/lifetime coverage. Maintainers use this record to review coverage and decide future integration; publication is the terminal outcome, not merge or release.

## Scope and completion

Satisfy-spec HOTL, triggered by the next-release cache lane. Class C4 for identity and credential scoping. First lock this docs-only roadmap, then execute `010_caller.md`, then `020_devin.md` serially on independent branches from dev. The serial order avoids shared-checkout branch collisions; neither runtime patch depends on the other.

Allowed: scoped local commits, pushes, two ordinary dev PRs, focused verification, read-only GitHub evidence and gpt-6.1-sol leaf implementation/review through the available V1 tools. Excluded: merges, release/version edits, installed-runtime changes, live upstream/account writes, source closure, branch deletion, native stacks, unrelated edits, outbound peer messages. Security working notes stay in ignored scratch. No token/cost or wall-clock budget was imposed; use local fixtures and ordinary PR CI only, no paid/live provider probes.

DONE requires published attached replacement PRs, source coverage and author trailers, passing focused regressions/static gates, independent review and inspected current-head CI. Missing broad/native/live proof keeps a draft with explicit limits. A material scope conflict returns to main for decision; external blockage is reported truthfully under host rules. Evidence lives in ignored lane scratch and goalplan receipts; public records contain only source provenance and outcomes.

## Source and verification

Baseline: `0818ea1812a028e1c14cd0b0511b44863407bc52`.
- #6521: `011ab507fcf03ae86933098de1994b25ecfd0495`, mayigululu-hash.
- #6488: `a0b199fc6b96367fb174f6488a7a45eb58eccd54`, Hanqing Zhao (@HQ1995).

Original live-cache measurements are author evidence in the source PRs, not new measurements or promised savings. Both source diffs were inspected; neither is an inherited stack. Read-only native membership queries returned empty lists.

Focused baseline command: `bun test tests/providers/devin-prompt-cache.test.ts tests/providers/xai/grok-session-identity.test.ts tests/server/loopback-listener-admission.test.ts`: 59 pass, 0 fail. Dependencies installed with `bun install --frozen-lockfile` (exit 0). Each implementation runs its named behavior tests, typecheck, structure/privacy/layout/ratchet and docs build. Full local and changed suites are impractical during concurrent lanes and release; broader platform coverage belongs to hosted PR checks. CI triggers on every pull request; feature pushes alone do not trigger the main workflow. No manual release workflow is needed.

## Consultation

Architect 01a10493-64d4-76c2-b14e-d1307ad82a43 proposed CS-01..04 and DT-01..04. Main accepts caller principal scoping and promotion before admission, while preserving the original request for Bun timeout ownership. Main accepts the bounded provider-local trajectory store and whole-wire field chain. Own thread headers outrank a parent-derived `_clientThreadId`; direct internal callers without a parent may retain that field as fallback. No idle TTL/logout integration is claimed: fixed capacity bounds retention and credential rotation creates a different key. The executable documents carry the decisions; same-architect reflection and independent audit are required before implementation.

Architect reflection: ALIGNED, with three test refinements accepted: exercise actual combo recall separation; parent-only/direct fallback/session aliases; double release after reacquisition and named retry wire continuity. Baseline typecheck, structure, privacy, layout (18 tests) and ratchet passed. The gate commands are repository scripts/direct test paths and observe the planned target classes. No broad suite was run.

Roadmap D conclusion: independent reviewer 01a1049a-9a43-7ee1-9e7a-58073e8eb9e7 found no blockers (VERDICT: PASS). Fresh-reader check understood answer, evidence, and next action without revision. The docs-only cycle locks the two independent plans. Next: execute caller, then Devin. No cache savings or runtime improvement has been measured in this cycle.
25 changes: 25 additions & 0 deletions devlog/_plan/261004_cache_identity/010_caller.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Caller session header replacement

Depends on roadmap only. Branch `codex/next-release-261004-cache-caller`, independent dev PR. Implements source #6521 with CS-01..04 refinements. Runtime scope is caller helper and serve-options; no global recall redesign.

## File delta

NEW `src/server/caller-session-identity.ts`: retain source `callerSessionId(headers)` validation: any explicit session_id/session-id/thread-id presence suppresses promotion, trim x-session-id and accept /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/. `withCallerSessionIdentity(req, admission)` returns the same request on skip, raw validated ID for loopback, and a stable opaque SHA-256 digest of a structured tuple of trusted `contextPrincipalIdOf(admission)` and conversation for authenticated callers. Unknown principal declines promotion. No body read, no persistent map, no raw credential storage. Clone with `new Request(req, {headers})` only when promoting.

MODIFY `src/server/index/serve-options.ts`: import helper. Responses: after auth/origin checks form `sessionReq = withCallerSessionIdentity(withGrokSessionIdentity(req), admission)`; use sessionReq for runAdmittedHttpTurn and handleResponses. Messages: likewise form `sessionReq = withCallerSessionIdentity(req, admission)` for admission and handleClaudeMessages. Keep original req for timeout lookup, response CORS and cancellation callbacks. Grok and explicit identities win unchanged. Existing guards still run first.

NEW `tests/server/caller-session-identity.test.ts`: carry all source cases and mocked handleResponses upstream-wire test, adapting signature to explicit loopback admission. Add explicit-empty precedence, exact 128/129 boundary, whitespace normalization, conflicting Grok/caller values, two trusted principals with same conversation, credential rotation and absent principal. Verify body bytes/method/url/abort and no-op object identity. Add behavioral ingress coverage for both Responses and Messages and verify admission receives the same promoted identity; use existing local fixture patterns, no live requests. Public combo recall separation may be demonstrated by resulting session lane keys and existing recall APIs. Source pins remain secondary coverage.

MODIFY `tests/providers/xai/grok-session-identity.test.ts` and `tests/server/loopback-listener-admission.test.ts`: update changed source-wiring pins without dropping assertions. MODIFY `scripts/test-layout/layout.json` and `tests/fixtures/test-layout-expected.json`: add caller-session-identity to server. MODIFY `structure/runtime.md`, applicable inbound-compat/responses owner docs and `docs-site/src/content/docs/reference/proxy-formats.md`: describe headers, priority, trusted-principal namespace, loopback behavior, no cache-hit promise. Review mapped owner docs; update only affected contracts.

## Activation and coverage

Explicit or Grok header plus differing x-session-id -> explicit/Grok wire value. Invalid/absent x-session-id -> original request. Two different admitted principals with equal x-session-id -> distinct promoted lanes. Rotated trusted credential -> new lane. Abort original -> rewritten signal aborts. Responses/Messages fixture -> backend receives promoted header after ingress authorization. All original source behaviors are carried except authenticated raw-ID equality is deliberately replaced by principal-scoped identity, and unsupported cache-savings assertions are removed.

Field chain: x-session-id creation is external caller; ingress validates and serializes session_id on Request; existing parsers consume it for affinity/logging and passthrough forwards it. No new persisted schema or deserializer.

Verifier: `bun test tests/server/caller-session-identity.test.ts tests/providers/xai/grok-session-identity.test.ts tests/server/loopback-listener-admission.test.ts` plus typecheck, structure/privacy and both layout guards/file-size ratchet. These direct test paths observe this delta; source-only pins do not prove live behavior. Docs build observes the public page. No full/changed suite per resource contract. Independent security review required. Existing explicit/Grok caller-supplied identities retain existing semantics; this change makes no global authorization-isolation claim.

Reflection refinement: use real combo recall read/write APIs for equal caller markers under different principals; same principal retains continuity. Test through actual admitted ingress in addition to helper tests.

Implementation record: helper and both routes implemented; ingress regression moved into registered sibling `tests/server/caller-session-ingress.test.ts` to keep unit and endpoint concerns readable. The accidental compact-route edit found by main was removed before verification. Four behavior files passed 59 tests; layout/tooling/ratchet files passed 27 tests. Typecheck and docs build passed. Structure contracts consolidated into inbound-compat with links from capped runtime/Responses docs; their 600-line budgets are preserved. Publication waits for independent code review and current-head CI inspection.
23 changes: 23 additions & 0 deletions devlog/_plan/261004_cache_identity/020_devin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Devin conversation trajectory replacement

Depends on roadmap; serial execution follows caller publication solely for checkout ownership. Branch `codex/next-release-261004-cache-devin` starts independently at refreshed dev. Implements #6488 with DT-01..04 refinements.

## File delta

NEW `src/adapters/devin/trajectory.ts`: small provider-local claim/release API and bounded process-local store. Key = fixed digest of structured tuple [devinCacheIdentity(resolvedApiKey,resolvedHost), conversation], retaining no raw token/conversation. Map holds at most 256 entries {id,active}; insertion order is recency. Existing inactive key becomes active and is refreshed. Matching active key -> unretained fresh request trajectory. For a new key evict oldest inactive entry; if all 256 active, return unretained fresh trajectory. Never evict active entries. Release closure marks only its entry inactive, safely repeatable. Unnamed turns return no override and keep per-request wire UUID fallback. No timers, persistence or new dependency.

MODIFY `src/adapters/devin.ts`: import claim helper; derive conversation from own thread-id, then session_id/session-id/x-session-affinity, then parsed._clientThreadId only when no parent header identifies it as a shared parent. Preserve source direct internal _clientThreadId behavior without coalescing explicit siblings. Claim inside outer try after credential, tenant host, catalog and cancellation guards; pass trajectoryId into the existing chat request; release in finally after all returns/errors/retries. Claim remains live through reset/signature retry.

MODIFY `src/adapters/devin/cloud-direct/chat.ts`: add optional trajectoryId to BuildArgs and CloudChatRequest, forward in streamChatEvents to buildGetChatMessageRequest, replace #15.1 randomUUID with `args.trajectoryId ?? crypto.randomUUID()`. Creation: adapter claim; serialization: protobuf #15.1; deserialization: none (outbound-only); consumer: remote Cognition plus local wire-decoding tests. No external support or hit-rate claim beyond original author report.

MODIFY `tests/providers/devin-prompt-cache.test.ts`: preserve source wire override/default tests and named reuse/unnamed/overlap/HTTP400-release cases. NEW `tests/providers/devin-trajectory.test.ts`: pure store isolation/bounds plus adapter cases as appropriate. Cases: new request-scoped adapters reuse named ID; changed credential/host/conversation separates; own sibling thread IDs outrank shared parent; three overlapping turns distinct; later sequential turn reuses original; post-claim abort and failure release; >256 inactive names evict oldest; 256 active entries never evict, overflow unretained and distinct. Use deterministic barriers with bounded failure deadline and finally teardown. Public claim API is production API, no test-only reset export.

MODIFY both test layout registries for the new providers test. MODIFY `structure/providers-and-adapters.md` and `docs-site/src/content/docs/reference/adapters.md`: describe credential/host/conversation scope, overlap, bounded process-local lifetime and no guarantee of upstream cache savings. Review other owning structure docs for affected semantics.

## Verification and source coverage

Carry all source runtime and regression behavior, with deliberate replacement of raw delimiter key, uncapped live set, and parent-first identity. Preserve optional field fallback and all existing reset/signature handling. Scoped implementation leaf owns runtime/tests/layout; main owns docs and verification.

Run `bun test tests/providers/devin-*.test.ts tests/adapters/adapter-inner-send-budget-wiring.test.ts`, including new tests and existing originals, then typecheck, structure/privacy/layout/ratchet and docs build. Each conditional path above has a fixture that activates it and checks wire UUIDs or claim IDs. Actual native/live cache-hit measurements remain unrun. Publish draft pending broader hosted/native evidence, inspect exact-head checks and repair in-scope defects.

Reflection refinements: parent-only request with populated parsed._clientThreadId must remain unnamed; direct fallback without a parent and each alias are exercised. Double release after reacquisition must not deactivate the new claim (release closure has a released flag). Named reset/signature retry wire requests retain the same trajectory within the live claim.
18 changes: 18 additions & 0 deletions docs-site/src/content/docs/reference/proxy-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,24 @@ for final text and `session.updated` with `session.status="closed"`.
This is the native opencodex data-plane shape. The request body must be a JSON object with a
non-empty `model`. `input` may be a string or an array of Responses items.

### Caller conversation header

For HTTP `POST /v1/responses` and `POST /v1/messages`, a client can send a stable
`x-session-id` for each conversation. When no `session_id`, `session-id`, or `thread-id`
header is present, opencodex promotes the marker to `session_id`. Managed Grok identity
on Responses takes precedence too. Even an empty explicit header suppresses promotion.

Use 1–128 ASCII letters, digits, `.`, `_`, `:`, or `-`, starting with a letter or digit.
Surrounding whitespace is trimmed; missing or invalid markers are ignored. Loopback
admission preserves the marker. Authenticated admission derives an opaque identifier
from the marker and the admitted credential principal, so different credentials do not
share a newly promoted identifier. Rotation changes this identifier; authenticated
requests without a trusted principal are left unchanged.

Use distinct markers for distinct conversations. This preserves conversation continuity
for downstream consumers, but does not guarantee an upstream cache hit or measured savings.
Chat Completions, WebSocket frames, compact, and count_tokens do not use this promotion.

### Accepted request fields

| Area | Accepted shape |
Expand Down
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"provider-antigravity-family-catalog.test.ts": "providers",
"claude-subagent-model-force.test.ts": "claude-integration",
"subagent-model-force-api.test.ts": "server",
"caller-session-identity.test.ts": "server",
"caller-session-ingress.test.ts": "server",
"agent-subagent-force.test.ts": "cli",
"compaction-event-ownership.test.ts": "responses",
"zed-retained-budget.test.ts": "providers",
Expand Down
29 changes: 29 additions & 0 deletions src/server/caller-session-identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** Promote third-party conversation markers after ingress authentication and origin checks. */
import { contextPrincipalIdOf, type DataPlaneAdmission } from "./auth-cors";

const SESSION_HEADERS = ["session_id", "session-id", "thread-id"] as const;
const SAFE_CALLER_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;

export function callerSessionId(headers: Headers): string | undefined {
if (SESSION_HEADERS.some(name => headers.has(name))) return undefined;
const value = headers.get("x-session-id")?.trim();
if (!value || !SAFE_CALLER_SESSION_ID.test(value)) return undefined;
return value;
}

/** Authenticated callers share continuity only within their trusted credential principal. */
export function withCallerSessionIdentity(req: Request, admission: DataPlaneAdmission): Request {
const conversation = callerSessionId(req.headers);
if (!conversation) return req;
let sessionId = conversation;
if (admission.kind !== "loopback") {
const principal = contextPrincipalIdOf(admission);
if (!principal) return req;
sessionId = new Bun.CryptoHasher("sha256")
.update(JSON.stringify(["opencodex-caller-session-v1", principal, conversation])).digest("hex");
}
const headers = new Headers(req.headers);
headers.set("session_id", sessionId);
// Preserve the unread body stream and propagate the original request's abort signal.
return new Request(req, { headers });
}
11 changes: 7 additions & 4 deletions src/server/index/serve-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ import { readyProtocolMetadata } from "../../remote/protocol";
import { modelCapabilityFields } from "../models-capabilities";
import { createWebsocketHandler } from "./websocket-handler";
import { withGrokSessionIdentity } from "../../grok/session-identity";
import { withCallerSessionIdentity } from "../caller-session-identity";

export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept" | "hub-link";

Expand Down Expand Up @@ -1474,10 +1475,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
logged = true;
addFinalRequestLog(requestId, start, logCtx, status, meta);
};
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
const sessionReq = withCallerSessionIdentity(withGrokSessionIdentity(req), admission);
return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => {
let response: Response;
try {
response = await handleResponses(withGrokSessionIdentity(req), config, logCtx, {
response = await handleResponses(sessionReq, config, logCtx, {
turnAdmissionLease,
admission,
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
Expand Down Expand Up @@ -1554,8 +1556,9 @@ export function createServeOptions(ctx: ServeOptionsContext) {
// Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
// translated Anthropic stream here.
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
const sessionReq = withCallerSessionIdentity(req, admission);
return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => withCors(
await handleClaudeMessages(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
req,
policy,
), { requestId, start, logCtx });
Expand Down
Loading
Loading