@makaio/client-codex 1.0.0-dev-1789404058608 → 1.0.0-dev-1789642522820
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/{codex-client-session-service-epcEX-w5.mjs → codex-client-session-service-BrCcbB06.mjs} +738 -110
- package/dist/definition.d.ts +1 -0
- package/dist/index.mjs +2 -2
- package/dist/runtime/codex-client-session-service.d.ts +27 -2
- package/dist/runtime/fork-sniff.d.ts +101 -0
- package/dist/runtime/hook-normalizer.d.ts +55 -26
- package/dist/runtime/hook-response-composer.d.ts +25 -1
- package/dist/runtime/hook-response-contracts.d.ts +22 -2
- package/dist/runtime/package.mjs +2 -2
- package/dist/runtime/schemas.d.ts +21 -1
- package/dist/runtime/wiring.d.ts +11 -0
- package/dist/server.mjs +1 -1
- package/dist/{src-BMTPLcPK.mjs → src-D25LSa6Y.mjs} +1 -1
- package/package.json +1 -1
package/dist/{codex-client-session-service-epcEX-w5.mjs → codex-client-session-service-BrCcbB06.mjs}
RENAMED
|
@@ -4,14 +4,14 @@ import { AbsolutePathSchema, BinaryNotFoundError, ClientSubjects, ClientWiringAp
|
|
|
4
4
|
import { MakaioBus, RequestError } from "@makaio/framework/bus";
|
|
5
5
|
import { BaseService } from "@makaio/framework/service-base";
|
|
6
6
|
import * as fs$1 from "node:fs/promises";
|
|
7
|
-
import fs from "node:fs/promises";
|
|
7
|
+
import fs, { open } from "node:fs/promises";
|
|
8
8
|
import * as path$1 from "node:path";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import * as os$1 from "node:os";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import { CANONICAL_HOOK_RESPONSE_CAPABILITIES, CLIENT_SESSION_COMPACTION_TRIGGERS, ClientConfigPrimeSchema, SessionConfigSetupRequestSchema, SessionConfigSetupResponseSchema, SessionConfigTeardownRequestSchema, SessionConfigTeardownResponseSchema } from "@makaio/framework/contracts/client";
|
|
13
14
|
import { isDeepStrictEqual } from "node:util";
|
|
14
|
-
import { ClientConfigPrimeSchema, SessionConfigSetupRequestSchema, SessionConfigSetupResponseSchema, SessionConfigTeardownRequestSchema, SessionConfigTeardownResponseSchema } from "@makaio/framework/contracts/client";
|
|
15
15
|
import { parse } from "smol-toml";
|
|
16
16
|
import { lock } from "proper-lockfile";
|
|
17
17
|
|
|
@@ -135,7 +135,22 @@ const clientDefinition = createClientDefinition({
|
|
|
135
135
|
name: "Stop",
|
|
136
136
|
frameworkSubject: "client.session.turn.completed",
|
|
137
137
|
responseCapabilities: [CODEX_HOOK_RESPONSE_CAPABILITIES.block]
|
|
138
|
-
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "SubagentStart",
|
|
141
|
+
frameworkSubject: "client.session.subagent.started",
|
|
142
|
+
responseCapabilities: ["context.append"]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "SubagentStop",
|
|
146
|
+
frameworkSubject: "client.session.subagent.completed"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "PreCompact",
|
|
150
|
+
frameworkSubject: "client.session.compaction.pre"
|
|
151
|
+
},
|
|
152
|
+
{ name: "PostCompact" },
|
|
153
|
+
{ name: "PermissionRequest" }
|
|
139
154
|
]
|
|
140
155
|
}
|
|
141
156
|
});
|
|
@@ -942,6 +957,191 @@ async function handleCodexConfigPrime(payload) {
|
|
|
942
957
|
return { primed: true };
|
|
943
958
|
}
|
|
944
959
|
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/runtime/fork-sniff.ts
|
|
962
|
+
/**
|
|
963
|
+
* Fork lineage sniff for Codex rollout files.
|
|
964
|
+
*
|
|
965
|
+
* Codex records every thread as a JSONL *rollout* file. The first line is the
|
|
966
|
+
* thread's own `session_meta` record; when the thread was created by forking
|
|
967
|
+
* another thread, that record carries `forked_from_id` — the parent thread id.
|
|
968
|
+
* Codex then copies the parent's persisted rollout items verbatim into the
|
|
969
|
+
* child file, so the parent's own `session_meta` line appears *later* in the
|
|
970
|
+
* same file.
|
|
971
|
+
*
|
|
972
|
+
* ## Why a sniff is needed
|
|
973
|
+
*
|
|
974
|
+
* The `SessionStart` hook payload has no lineage field at all: it carries
|
|
975
|
+
* `session_id`, `transcript_path`, `cwd`, `hook_event_name`, `model`,
|
|
976
|
+
* `permission_mode` and `source` — nothing else. A fork also reports
|
|
977
|
+
* `source: 'startup'`, exactly like a brand-new thread, so the hook alone
|
|
978
|
+
* cannot distinguish the two. `transcript_path` points at the rollout file
|
|
979
|
+
* the CLI has just materialized for the starting thread, which makes the
|
|
980
|
+
* rollout head the only lineage source available at session-start time.
|
|
981
|
+
*
|
|
982
|
+
* ## Detection invariant
|
|
983
|
+
*
|
|
984
|
+
* The **first** `session_meta` record in the file is the file's own. Any later
|
|
985
|
+
* `session_meta` record was copied from an ancestor and must be ignored, so the
|
|
986
|
+
* scan stops at the first one it can parse:
|
|
987
|
+
*
|
|
988
|
+
* - `forked_from_id` present and different from the starting thread's own id
|
|
989
|
+
* → fork child; that id is the direct parent.
|
|
990
|
+
* - `forked_from_id` absent (or equal to the own id, which would be a
|
|
991
|
+
* self-reference and is never a usable parent) → no fork signal.
|
|
992
|
+
* - No parseable `session_meta` inside the window → inconclusive; return
|
|
993
|
+
* `undefined`. Fork registration is fill-once on the ingestion side, so a
|
|
994
|
+
* guessed parent would be permanently wrong while deferring is safe.
|
|
995
|
+
*
|
|
996
|
+
* Anchoring the window at the start of the file is what makes the rule sound
|
|
997
|
+
* under a byte cap: the decisive record is the first one, so no earlier record
|
|
998
|
+
* can exist outside the window.
|
|
999
|
+
*
|
|
1000
|
+
* ## Design principles
|
|
1001
|
+
*
|
|
1002
|
+
* - **Pure detection core**: {@link sniffRolloutForkLineage} operates on raw
|
|
1003
|
+
* JSONL lines. No I/O, no bus, no dependencies beyond the language runtime.
|
|
1004
|
+
* - **Bounded I/O wrapper**: {@link sniffRolloutFork} reads at most
|
|
1005
|
+
* {@link SNIFF_MAX_BYTES} from the rollout head, so the hook path never
|
|
1006
|
+
* blocks on a large rollout file.
|
|
1007
|
+
* - **Fail-open**: any I/O or parse error yields `undefined` (no fork signal);
|
|
1008
|
+
* hook processing is never blocked by a sniff failure.
|
|
1009
|
+
* @packageDocumentation
|
|
1010
|
+
*/
|
|
1011
|
+
/**
|
|
1012
|
+
* Maximum number of bytes to read from the rollout head for fork detection.
|
|
1013
|
+
*
|
|
1014
|
+
* The decisive record is the first line, but that line embeds the thread's
|
|
1015
|
+
* `base_instructions`, which can run to several kilobytes. 128 KiB leaves ample
|
|
1016
|
+
* headroom for it while keeping the blocking window small. A `session_meta`
|
|
1017
|
+
* line longer than the window yields a truncated, unparseable JSON fragment and
|
|
1018
|
+
* therefore no signal — the fail-open outcome, not a wrong parent.
|
|
1019
|
+
*/
|
|
1020
|
+
const SNIFF_MAX_BYTES = 128 * 1024;
|
|
1021
|
+
/** Rollout item discriminator for the thread metadata record. */
|
|
1022
|
+
const SESSION_META_TYPE = "session_meta";
|
|
1023
|
+
/**
|
|
1024
|
+
* Read `forked_from_id` from a parsed rollout line when it is the file's own
|
|
1025
|
+
* `session_meta` record.
|
|
1026
|
+
*
|
|
1027
|
+
* Codex serializes its rollout items with an externally tagged representation,
|
|
1028
|
+
* so the record on disk has a `type` discriminator and a `payload` body; the
|
|
1029
|
+
* metadata record flattens the thread metadata into that `payload`.
|
|
1030
|
+
* @param parsed - Parsed JSONL record from the rollout head
|
|
1031
|
+
* @returns The parent thread id, `undefined` when the record names no parent,
|
|
1032
|
+
* or `null` when the record is not a `session_meta` record at all
|
|
1033
|
+
*/
|
|
1034
|
+
function readForkParent(parsed) {
|
|
1035
|
+
if (parsed["type"] !== SESSION_META_TYPE) return null;
|
|
1036
|
+
const payload = parsed["payload"];
|
|
1037
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
1038
|
+
const forkedFromId = payload["forked_from_id"];
|
|
1039
|
+
return typeof forkedFromId === "string" && forkedFromId.length > 0 ? forkedFromId : void 0;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Detect fork lineage from raw rollout JSONL lines.
|
|
1043
|
+
*
|
|
1044
|
+
* Scans the provided window **forward** from the first line. The window must be
|
|
1045
|
+
* anchored at the start of the rollout file (see {@link sniffRolloutFork});
|
|
1046
|
+
* under that anchoring, the first `session_meta` record found is the file's own
|
|
1047
|
+
* and decides the outcome. Later `session_meta` records belong to ancestors
|
|
1048
|
+
* copied into the fork and are never consulted.
|
|
1049
|
+
* @param lines - Raw JSONL lines from the rollout head (may include empty
|
|
1050
|
+
* strings, partial lines, or non-JSON data)
|
|
1051
|
+
* @param hookSessionId - Session id reported by the hook payload (the starting
|
|
1052
|
+
* thread's own id)
|
|
1053
|
+
* @returns Fork sniff result when the own `session_meta` names a foreign
|
|
1054
|
+
* parent, or `undefined` for a plain start / no signal / inconclusive window
|
|
1055
|
+
*/
|
|
1056
|
+
function sniffRolloutForkLineage(lines, hookSessionId) {
|
|
1057
|
+
for (const raw of lines) {
|
|
1058
|
+
const line = raw.trim();
|
|
1059
|
+
if (line.length === 0) continue;
|
|
1060
|
+
let parsed;
|
|
1061
|
+
try {
|
|
1062
|
+
parsed = JSON.parse(line);
|
|
1063
|
+
} catch {
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
1067
|
+
const forkParent = readForkParent(parsed);
|
|
1068
|
+
if (forkParent === null) continue;
|
|
1069
|
+
if (forkParent === void 0 || forkParent === hookSessionId) return void 0;
|
|
1070
|
+
return { parentAdapterSessionId: forkParent };
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Read at most {@link SNIFF_MAX_BYTES} from the head of the rollout file.
|
|
1075
|
+
*
|
|
1076
|
+
* The final element is dropped when the file is larger than the window: it is a
|
|
1077
|
+
* partial line whose parse would fail anyway, and dropping it keeps the caller
|
|
1078
|
+
* from treating truncated JSON as data.
|
|
1079
|
+
* @param rolloutPath - Absolute path to the rollout JSONL file
|
|
1080
|
+
* @returns Raw lines from the file head, or `undefined` on any I/O error
|
|
1081
|
+
*/
|
|
1082
|
+
async function readRolloutHead(rolloutPath) {
|
|
1083
|
+
let fh;
|
|
1084
|
+
try {
|
|
1085
|
+
fh = await open(rolloutPath, "r");
|
|
1086
|
+
const { size } = await fh.stat();
|
|
1087
|
+
const readLength = Math.min(size, SNIFF_MAX_BYTES);
|
|
1088
|
+
const buf = Buffer.alloc(readLength);
|
|
1089
|
+
const { bytesRead } = await fh.read(buf, 0, readLength, 0);
|
|
1090
|
+
const lines = buf.toString("utf8", 0, bytesRead).split("\n");
|
|
1091
|
+
if (size > 131072) lines.pop();
|
|
1092
|
+
return lines;
|
|
1093
|
+
} catch {
|
|
1094
|
+
return;
|
|
1095
|
+
} finally {
|
|
1096
|
+
await fh?.close();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Sniff the rollout file at session-start time to detect fork lineage.
|
|
1101
|
+
*
|
|
1102
|
+
* Combines the bounded head read with the pure forward-scanning detection core.
|
|
1103
|
+
* Because the window is anchored at the start of the file, a byte cap can never
|
|
1104
|
+
* produce a wrong parent: the decisive record — the file's own `session_meta` —
|
|
1105
|
+
* is either inside the window or the sniff returns `undefined`. Returns
|
|
1106
|
+
* `undefined` (no fork signal) on any error; hook processing must never be
|
|
1107
|
+
* blocked by a sniff failure.
|
|
1108
|
+
* @param rolloutPath - Absolute path to the rollout JSONL file, as reported by
|
|
1109
|
+
* the hook payload's `transcript_path`
|
|
1110
|
+
* @param hookSessionId - Session id reported by the hook payload
|
|
1111
|
+
* @returns Fork sniff result, or `undefined` when no fork is detected or the
|
|
1112
|
+
* sniff cannot be performed
|
|
1113
|
+
*/
|
|
1114
|
+
async function sniffRolloutFork(rolloutPath, hookSessionId) {
|
|
1115
|
+
try {
|
|
1116
|
+
const lines = await readRolloutHead(rolloutPath);
|
|
1117
|
+
if (lines === void 0) return void 0;
|
|
1118
|
+
return sniffRolloutForkLineage(lines, hookSessionId);
|
|
1119
|
+
} catch {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/runtime/schemas.ts
|
|
1126
|
+
/**
|
|
1127
|
+
* Hook events emitted by Codex that map to the v1 observed-semantics set.
|
|
1128
|
+
*
|
|
1129
|
+
* These are the events the normalizer translates into `client.session.*` bus
|
|
1130
|
+
* emissions. Any event NOT listed here is left as raw `client:codex`
|
|
1131
|
+
* namespace data only.
|
|
1132
|
+
*
|
|
1133
|
+
* Event names are verified against pinned source `rust-v0.144.1`
|
|
1134
|
+
* (`codex-rs/hooks/src/lib.rs`).
|
|
1135
|
+
*/
|
|
1136
|
+
const CODEX_HOOK_SESSION_START = "SessionStart";
|
|
1137
|
+
const CODEX_HOOK_USER_PROMPT_SUBMIT = "UserPromptSubmit";
|
|
1138
|
+
const CODEX_HOOK_PRE_TOOL_USE = "PreToolUse";
|
|
1139
|
+
const CODEX_HOOK_POST_TOOL_USE = "PostToolUse";
|
|
1140
|
+
const CODEX_HOOK_STOP = "Stop";
|
|
1141
|
+
const CODEX_HOOK_SUBAGENT_START = "SubagentStart";
|
|
1142
|
+
const CODEX_HOOK_SUBAGENT_STOP = "SubagentStop";
|
|
1143
|
+
const CODEX_HOOK_PRE_COMPACT = "PreCompact";
|
|
1144
|
+
|
|
945
1145
|
//#endregion
|
|
946
1146
|
//#region src/runtime/hook-normalizer.ts
|
|
947
1147
|
/**
|
|
@@ -951,37 +1151,88 @@ async function handleCodexConfigPrime(payload) {
|
|
|
951
1151
|
* `client:codex.hook.received` to their corresponding
|
|
952
1152
|
* `client.session.*` observed-semantics subjects.
|
|
953
1153
|
*
|
|
954
|
-
* **Mapping table** (Codex event → global subject):
|
|
1154
|
+
* **Mapping table** (Codex event → global subject(s)):
|
|
955
1155
|
*
|
|
956
|
-
* | Codex event name
|
|
957
|
-
*
|
|
958
|
-
* | `SessionStart`
|
|
959
|
-
* | `UserPromptSubmit`
|
|
960
|
-
* | `Stop`
|
|
961
|
-
* | `PreToolUse`
|
|
962
|
-
* | `PostToolUse`
|
|
1156
|
+
* | Codex event name | Global subject(s) |
|
|
1157
|
+
* |------------------|----------------------------------------------------------|
|
|
1158
|
+
* | `SessionStart` | `client.session.started` |
|
|
1159
|
+
* | `UserPromptSubmit` | `client.session.turn.started`, then `client.session.userPrompt.submitted` |
|
|
1160
|
+
* | `Stop` | `client.session.turn.completed` |
|
|
1161
|
+
* | `PreToolUse` | `client.session.tool.pre` |
|
|
1162
|
+
* | `PostToolUse` | `client.session.tool.post` |
|
|
1163
|
+
* | `SubagentStart` | `client.session.subagent.started` |
|
|
1164
|
+
* | `SubagentStop` | `client.session.subagent.completed` |
|
|
1165
|
+
* | `PreCompact` | `client.session.compaction.pre` |
|
|
1166
|
+
* | `PostCompact` | _(raw-only — no global subject)_ |
|
|
963
1167
|
*
|
|
964
|
-
* All other event names
|
|
1168
|
+
* All other event names return an empty array — they are kept raw only and
|
|
965
1169
|
* are never emitted into the global `client.*` namespace.
|
|
966
1170
|
*
|
|
967
|
-
* **Source notes:**
|
|
968
|
-
* Codex
|
|
969
|
-
*
|
|
1171
|
+
* **Source notes:** Event names are verified against the pinned
|
|
1172
|
+
* `rust-v0.144.1` Codex source (`codex-rs/hooks/src/lib.rs`). Update this
|
|
1173
|
+
* normalizer when a new binary version changes or adds hook names.
|
|
1174
|
+
*
|
|
1175
|
+
* **Subagent hooks:** On both `SubagentStart` and `SubagentStop`, the raw
|
|
1176
|
+
* `session_id` is the PARENT session id. `adapterSessionId` on the base carries
|
|
1177
|
+
* it directly (no stripping). `agentId` identifies the subagent. `turnId` is
|
|
1178
|
+
* populated from `turn_id` when the Codex CLI includes it.
|
|
970
1179
|
* @packageDocumentation
|
|
971
1180
|
*/
|
|
1181
|
+
/** Client ID used in all normalized payloads emitted by this normalizer. */
|
|
1182
|
+
const CLIENT_ID$1 = "codex";
|
|
1183
|
+
/** Source tag carried on all normalized observations. */
|
|
1184
|
+
const SOURCE = "native-hook";
|
|
972
1185
|
/**
|
|
973
|
-
*
|
|
1186
|
+
* Known compaction trigger values reported by Codex.
|
|
974
1187
|
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
1188
|
+
* Derived from the contracts constant so it stays in sync without a separate
|
|
1189
|
+
* local enumeration that could drift.
|
|
977
1190
|
*/
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1191
|
+
const COMPACTION_TRIGGERS = new Set(CLIENT_SESSION_COMPACTION_TRIGGERS);
|
|
1192
|
+
/**
|
|
1193
|
+
* Map from the Codex CLI `SessionStart.source` union to the
|
|
1194
|
+
* framework-level {@link ClientSessionStartMode}.
|
|
1195
|
+
*
|
|
1196
|
+
* - `'startup'` → `'fresh'` (brand-new thread — **and a fork child**, see
|
|
1197
|
+
* below; the owning service upgrades the fork case to `'fork'`)
|
|
1198
|
+
* - `'resume'` → `'resume'` (thread continued from its own rollout file)
|
|
1199
|
+
* - `'clear'` → `'clear'` (conversation cleared, new thread id)
|
|
1200
|
+
* - `'compact'` → `'compact'` (context compacted, same thread id)
|
|
1201
|
+
*
|
|
1202
|
+
* The vendor union has exactly these four values in the pinned `rust-v0.144.1`
|
|
1203
|
+
* source (`codex-rs/hooks/src/events/session_start.rs`, `SessionStartSource`);
|
|
1204
|
+
* there is no `'fork'` value. In `codex-rs/core/src/session/session.rs` a fork
|
|
1205
|
+
* is classified next to a brand-new thread — the match arm that maps
|
|
1206
|
+
* `InitialHistory::New` to `SessionStartSource::Startup` also covers
|
|
1207
|
+
* `InitialHistory::Forked` — which is why `'startup'`, not `'resume'`, is the
|
|
1208
|
+
* mode that may still turn out to be a fork.
|
|
1209
|
+
* Lineage is recovered from the rollout file instead; see the fork sniff in
|
|
1210
|
+
* `fork-sniff.ts` and its caller in `codex-client-session-service.ts`.
|
|
1211
|
+
*
|
|
1212
|
+
* Vendor values not in this map yield `undefined`, leaving `startMode`
|
|
1213
|
+
* absent from the normalized payload — safe for forward compatibility when
|
|
1214
|
+
* Codex adds new source values.
|
|
1215
|
+
*/
|
|
1216
|
+
const VENDOR_SOURCE_TO_START_MODE = {
|
|
1217
|
+
startup: "fresh",
|
|
1218
|
+
resume: "resume",
|
|
1219
|
+
clear: "clear",
|
|
1220
|
+
compact: "compact"
|
|
1221
|
+
};
|
|
1222
|
+
/**
|
|
1223
|
+
* Extract the `source` field from a `SessionStart` hook payload and map it
|
|
1224
|
+
* to a {@link ClientSessionStartMode}.
|
|
1225
|
+
*
|
|
1226
|
+
* Returns `undefined` when the field is absent, non-string, or not a
|
|
1227
|
+
* recognized value — keeping the normalizer tolerant of future CLI additions.
|
|
1228
|
+
* @param payload - Raw `SessionStart` hook payload
|
|
1229
|
+
* @returns Mapped start mode, or `undefined` when the source is unknown
|
|
1230
|
+
*/
|
|
1231
|
+
function resolveStartMode(payload) {
|
|
1232
|
+
const source = payload["source"];
|
|
1233
|
+
if (typeof source !== "string") return void 0;
|
|
1234
|
+
return VENDOR_SOURCE_TO_START_MODE[source];
|
|
1235
|
+
}
|
|
985
1236
|
/**
|
|
986
1237
|
* Extract optional session identifier from a raw Codex hook payload.
|
|
987
1238
|
*
|
|
@@ -1021,87 +1272,249 @@ function extractPrompt(payload) {
|
|
|
1021
1272
|
return pickNonEmptyString(payload, "prompt");
|
|
1022
1273
|
}
|
|
1023
1274
|
/**
|
|
1024
|
-
*
|
|
1275
|
+
* Extract the subagent identity from a subagent hook payload.
|
|
1276
|
+
*
|
|
1277
|
+
* Codex reports the agent identity under `agent_id` on both `SubagentStart`
|
|
1278
|
+
* and `SubagentStop` hook payloads.
|
|
1279
|
+
* @param payload - Raw subagent hook payload
|
|
1280
|
+
* @returns Agent ID string, or `undefined` when absent or empty
|
|
1281
|
+
*/
|
|
1282
|
+
function extractAgentId(payload) {
|
|
1283
|
+
return pickNonEmptyString(payload, "agent_id");
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Extract the subagent type label from a subagent hook payload.
|
|
1287
|
+
*
|
|
1288
|
+
* Codex reports the agent type under `agent_type` on subagent hooks.
|
|
1289
|
+
* @param payload - Raw subagent hook payload
|
|
1290
|
+
* @returns Agent type string, or `undefined` when absent or empty
|
|
1291
|
+
*/
|
|
1292
|
+
function extractAgentType(payload) {
|
|
1293
|
+
return pickNonEmptyString(payload, "agent_type");
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Extract the subagent transcript path from a `SubagentStop` payload.
|
|
1297
|
+
*
|
|
1298
|
+
* Codex may report the agent's own transcript path under
|
|
1299
|
+
* `agent_transcript_path` at subagent stop time.
|
|
1300
|
+
* @param payload - Raw `SubagentStop` hook payload
|
|
1301
|
+
* @returns Absolute transcript path, or `undefined` when absent or empty
|
|
1302
|
+
*/
|
|
1303
|
+
function extractAgentTranscriptPath(payload) {
|
|
1304
|
+
return pickNonEmptyString(payload, "agent_transcript_path");
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Extract the turn correlation id from a subagent hook payload.
|
|
1308
|
+
*
|
|
1309
|
+
* Codex subagent hooks carry `turn_id` to correlate the hook event with the
|
|
1310
|
+
* parent turn that spawned the subagent.
|
|
1311
|
+
* @param payload - Raw subagent hook payload
|
|
1312
|
+
* @returns Turn id string, or `undefined` when absent or empty
|
|
1313
|
+
*/
|
|
1314
|
+
function extractTurnId(payload) {
|
|
1315
|
+
return pickNonEmptyString(payload, "turn_id");
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Extract and map the compaction trigger from a `PreCompact` payload.
|
|
1319
|
+
*
|
|
1320
|
+
* Codex reports the trigger under `trigger` with values `'manual'` or
|
|
1321
|
+
* `'auto'`. Unknown values are dropped for forward compatibility.
|
|
1322
|
+
* @param payload - Raw `PreCompact` hook payload
|
|
1323
|
+
* @returns Mapped compaction trigger, or `undefined` when absent or unknown
|
|
1324
|
+
*/
|
|
1325
|
+
function extractCompactionTrigger(payload) {
|
|
1326
|
+
const trigger = payload["trigger"];
|
|
1327
|
+
if (typeof trigger !== "string") return void 0;
|
|
1328
|
+
return COMPACTION_TRIGGERS.has(trigger) ? trigger : void 0;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Extract the transcript path from a raw Codex hook payload.
|
|
1332
|
+
*
|
|
1333
|
+
* Codex includes `transcript_path` on `SessionStart` and on the compaction
|
|
1334
|
+
* hooks; it is the absolute path of the thread's rollout JSONL file and is
|
|
1335
|
+
* serialized as `null` when no rollout has been materialized.
|
|
1336
|
+
* @param payload - Raw hook payload object
|
|
1337
|
+
* @returns Transcript path string, or `undefined` when absent or empty
|
|
1338
|
+
*/
|
|
1339
|
+
function extractTranscriptPath(payload) {
|
|
1340
|
+
return pickNonEmptyString(payload, "transcript_path");
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Normalize a `SubagentStart` hook payload into the subagent-started event.
|
|
1025
1344
|
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1345
|
+
* SubagentStart/Stop: the raw `session_id` is the PARENT session id.
|
|
1346
|
+
* `adapterSessionId` on the base carries it directly — it is the parent
|
|
1347
|
+
* session id, not a subagent own session id. `agentId` identifies the
|
|
1348
|
+
* subagent. `turnId` is populated from `turn_id` when present.
|
|
1349
|
+
* @param base - Full hook base (includes `adapterSessionId` = parent session id)
|
|
1350
|
+
* @param payload - Raw `SubagentStart` hook payload body
|
|
1351
|
+
* @returns Normalized event array, empty when `agentId` is absent
|
|
1352
|
+
*/
|
|
1353
|
+
function normalizeSubagentStart(base, payload) {
|
|
1354
|
+
const agentId = extractAgentId(payload);
|
|
1355
|
+
if (agentId === void 0) return [];
|
|
1356
|
+
const agentType = extractAgentType(payload);
|
|
1357
|
+
const turnId = extractTurnId(payload);
|
|
1358
|
+
return [{
|
|
1359
|
+
subject: ClientSubjects.session.subagent.started,
|
|
1360
|
+
payload: {
|
|
1361
|
+
...base,
|
|
1362
|
+
agentId,
|
|
1363
|
+
...agentType !== void 0 && { agentType },
|
|
1364
|
+
...turnId !== void 0 && { turnId }
|
|
1365
|
+
}
|
|
1366
|
+
}];
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Normalize a `SubagentStop` hook payload into the subagent-completed event.
|
|
1370
|
+
*
|
|
1371
|
+
* See {@link normalizeSubagentStart} for the `adapterSessionId` = parent
|
|
1372
|
+
* session id convention and `turnId` extraction.
|
|
1373
|
+
* @param base - Full hook base (includes `adapterSessionId` = parent session id)
|
|
1374
|
+
* @param payload - Raw `SubagentStop` hook payload body
|
|
1375
|
+
* @returns Normalized event array, empty when `agentId` is absent
|
|
1376
|
+
*/
|
|
1377
|
+
function normalizeSubagentStop(base, payload) {
|
|
1378
|
+
const agentId = extractAgentId(payload);
|
|
1379
|
+
if (agentId === void 0) return [];
|
|
1380
|
+
const agentType = extractAgentType(payload);
|
|
1381
|
+
const turnId = extractTurnId(payload);
|
|
1382
|
+
const agentTranscriptPath = extractAgentTranscriptPath(payload);
|
|
1383
|
+
return [{
|
|
1384
|
+
subject: ClientSubjects.session.subagent.completed,
|
|
1385
|
+
payload: {
|
|
1386
|
+
...base,
|
|
1387
|
+
agentId,
|
|
1388
|
+
...agentType !== void 0 && { agentType },
|
|
1389
|
+
...turnId !== void 0 && { turnId },
|
|
1390
|
+
...agentTranscriptPath !== void 0 && { agentTranscriptPath }
|
|
1391
|
+
}
|
|
1392
|
+
}];
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Normalize a `PreCompact` hook payload into the compaction-pre event.
|
|
1396
|
+
* @param base - Full hook base (includes `adapterSessionId`)
|
|
1397
|
+
* @param payload - Raw `PreCompact` hook payload body
|
|
1398
|
+
* @returns Single-element normalized event array
|
|
1399
|
+
*/
|
|
1400
|
+
function normalizePreCompact(base, payload) {
|
|
1401
|
+
const trigger = extractCompactionTrigger(payload);
|
|
1402
|
+
const transcriptPath = extractTranscriptPath(payload);
|
|
1403
|
+
return [{
|
|
1404
|
+
subject: ClientSubjects.session.compaction.pre,
|
|
1405
|
+
payload: {
|
|
1406
|
+
...base,
|
|
1407
|
+
...trigger !== void 0 && { trigger },
|
|
1408
|
+
...transcriptPath !== void 0 && { transcriptPath }
|
|
1409
|
+
}
|
|
1410
|
+
}];
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Normalize a raw Codex hook payload into `client.session.*` events.
|
|
1414
|
+
*
|
|
1415
|
+
* Returns an empty array for unknown or not-yet-modeled event names so the
|
|
1416
|
+
* caller skips global emission and keeps the event raw-only in
|
|
1417
|
+
* `client:codex.*`. A single hook may map to more than one normalized event:
|
|
1418
|
+
* `UserPromptSubmit` yields `turn.started` followed by `userPrompt.submitted`.
|
|
1419
|
+
* Emission order within the array is significant and must be preserved by the
|
|
1420
|
+
* caller.
|
|
1421
|
+
*
|
|
1422
|
+
* The `receivedAt` timestamp from the raw hook payload is used as `observedAt`
|
|
1423
|
+
* to preserve the original wall-clock time of the observation.
|
|
1028
1424
|
* @param raw - Raw hook payload delivered on `client:codex.hook.received`
|
|
1029
1425
|
* @param machineId - Stable runtime identity of the observing machine,
|
|
1030
1426
|
* caller-supplied by the owning client runtime. Stamped onto
|
|
1031
1427
|
* `client.session.started` so downstream storage receives the owning
|
|
1032
1428
|
* machine's identity without deriving it from the writer process.
|
|
1033
|
-
* @returns Normalized
|
|
1034
|
-
* the event name is unknown
|
|
1429
|
+
* @returns Normalized events with subject and typed payload, in emission
|
|
1430
|
+
* order; empty when the event name is unknown (raw-only)
|
|
1035
1431
|
*/
|
|
1036
1432
|
function normalizeCodexHook(raw, machineId) {
|
|
1037
|
-
const subject = CODEX_EVENT_MAP.get(raw.eventName);
|
|
1038
|
-
if (subject === void 0) return null;
|
|
1039
1433
|
const base = {
|
|
1040
|
-
clientId:
|
|
1041
|
-
source:
|
|
1434
|
+
clientId: CLIENT_ID$1,
|
|
1435
|
+
source: SOURCE,
|
|
1042
1436
|
observedAt: raw.receivedAt,
|
|
1043
1437
|
adapterSessionId: extractAdapterSessionId(raw.payload),
|
|
1044
1438
|
metadata: raw.metadata
|
|
1045
1439
|
};
|
|
1046
|
-
switch (
|
|
1047
|
-
case
|
|
1048
|
-
|
|
1049
|
-
payload
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1440
|
+
switch (raw.eventName) {
|
|
1441
|
+
case CODEX_HOOK_SESSION_START: {
|
|
1442
|
+
const startMode = resolveStartMode(raw.payload);
|
|
1443
|
+
const transcriptPath = extractTranscriptPath(raw.payload);
|
|
1444
|
+
return [{
|
|
1445
|
+
subject: ClientSubjects.session.started,
|
|
1446
|
+
payload: {
|
|
1447
|
+
...base,
|
|
1448
|
+
...machineId !== void 0 && { machineId },
|
|
1449
|
+
...startMode !== void 0 && { startMode },
|
|
1450
|
+
...transcriptPath !== void 0 && { transcriptPath }
|
|
1451
|
+
}
|
|
1452
|
+
}];
|
|
1453
|
+
}
|
|
1454
|
+
case CODEX_HOOK_USER_PROMPT_SUBMIT: return [{
|
|
1455
|
+
subject: ClientSubjects.session.turn.started,
|
|
1456
|
+
payload: { ...base }
|
|
1457
|
+
}, {
|
|
1458
|
+
subject: ClientSubjects.session.userPrompt.submitted,
|
|
1056
1459
|
payload: {
|
|
1057
1460
|
...base,
|
|
1058
1461
|
prompt: extractPrompt(raw.payload)
|
|
1059
1462
|
}
|
|
1060
|
-
};
|
|
1061
|
-
case
|
|
1062
|
-
subject,
|
|
1063
|
-
payload: { ...base }
|
|
1064
|
-
};
|
|
1065
|
-
case ClientSubjects.session.tool.pre: return {
|
|
1066
|
-
subject,
|
|
1463
|
+
}];
|
|
1464
|
+
case CODEX_HOOK_PRE_TOOL_USE: return [{
|
|
1465
|
+
subject: ClientSubjects.session.tool.pre,
|
|
1067
1466
|
payload: {
|
|
1068
1467
|
...base,
|
|
1069
1468
|
toolName: extractToolName(raw.payload),
|
|
1070
1469
|
toolCallId: extractToolCallId(raw.payload)
|
|
1071
1470
|
}
|
|
1072
|
-
};
|
|
1073
|
-
case
|
|
1074
|
-
subject,
|
|
1471
|
+
}];
|
|
1472
|
+
case CODEX_HOOK_POST_TOOL_USE: return [{
|
|
1473
|
+
subject: ClientSubjects.session.tool.post,
|
|
1075
1474
|
payload: {
|
|
1076
1475
|
...base,
|
|
1077
1476
|
toolName: extractToolName(raw.payload),
|
|
1078
1477
|
toolCallId: extractToolCallId(raw.payload)
|
|
1079
1478
|
}
|
|
1080
|
-
};
|
|
1081
|
-
|
|
1479
|
+
}];
|
|
1480
|
+
case CODEX_HOOK_STOP: return [{
|
|
1481
|
+
subject: ClientSubjects.session.turn.completed,
|
|
1482
|
+
payload: { ...base }
|
|
1483
|
+
}];
|
|
1484
|
+
case CODEX_HOOK_SUBAGENT_START: return normalizeSubagentStart(base, raw.payload);
|
|
1485
|
+
case CODEX_HOOK_SUBAGENT_STOP: return normalizeSubagentStop(base, raw.payload);
|
|
1486
|
+
case CODEX_HOOK_PRE_COMPACT: return normalizePreCompact(base, raw.payload);
|
|
1487
|
+
default: return [];
|
|
1082
1488
|
}
|
|
1083
1489
|
}
|
|
1084
1490
|
|
|
1085
|
-
//#endregion
|
|
1086
|
-
//#region src/runtime/schemas.ts
|
|
1087
|
-
/**
|
|
1088
|
-
* Hook events emitted by Codex that map to the v1 observed-semantics set.
|
|
1089
|
-
*
|
|
1090
|
-
* These are the events the normalizer translates into `client.session.*` bus
|
|
1091
|
-
* emissions. Any event NOT listed here is left as raw `client:codex`
|
|
1092
|
-
* namespace data only.
|
|
1093
|
-
*/
|
|
1094
|
-
const CODEX_HOOK_SESSION_START = "SessionStart";
|
|
1095
|
-
const CODEX_HOOK_USER_PROMPT_SUBMIT = "UserPromptSubmit";
|
|
1096
|
-
const CODEX_HOOK_PRE_TOOL_USE = "PreToolUse";
|
|
1097
|
-
const CODEX_HOOK_POST_TOOL_USE = "PostToolUse";
|
|
1098
|
-
const CODEX_HOOK_STOP = "Stop";
|
|
1099
|
-
|
|
1100
1491
|
//#endregion
|
|
1101
1492
|
//#region src/runtime/hook-response-contracts.ts
|
|
1493
|
+
/** Codex 0.144.1 synchronous hook-response contract. @packageDocumentation */
|
|
1102
1494
|
const CODEX_CLIENT_ID = "codex";
|
|
1103
1495
|
const CODEX_CONTRACT_ID = "openai.codex-hook-response";
|
|
1104
|
-
|
|
1496
|
+
/**
|
|
1497
|
+
* Semantic version of the Codex hook-response contract.
|
|
1498
|
+
*
|
|
1499
|
+
* Pinned to the proven capabilities of Codex CLI 0.144.1. Bump this version
|
|
1500
|
+
* when a future CLI release expands the native response surface.
|
|
1501
|
+
*
|
|
1502
|
+
* `1.2.0` adds `SubagentStart` as a request-capable, non-blockable interaction
|
|
1503
|
+
* carrying canonical `context.append`. The appended context lands in the
|
|
1504
|
+
* *subagent's* context window, not the parent's — proven live, see
|
|
1505
|
+
* `runtime/__tests__/fixtures/hook-contracts/probe/subagent-start-context-append.json`.
|
|
1506
|
+
* Subagent creation cannot be refused (`continue: false` is parsed for
|
|
1507
|
+
* compatibility but does not stop the subagent), so the interaction is
|
|
1508
|
+
* non-blockable. Purely additive: every `1.1.0` contributor remains valid.
|
|
1509
|
+
*
|
|
1510
|
+
* `1.3.0` adds the canonical `session.token` interaction to the catalog. The
|
|
1511
|
+
* definition does not declare it on any Codex event yet (Codex hands its MCP
|
|
1512
|
+
* subprocesses no session id). When declared, the composer collects the token
|
|
1513
|
+
* and hands it to the in-process token sink of the client runtime; it is never
|
|
1514
|
+
* rendered to the client binary's stdout. Purely additive: every `1.2.0`
|
|
1515
|
+
* contributor remains valid.
|
|
1516
|
+
*/
|
|
1517
|
+
const CODEX_CONTRACT_VERSION = "1.3.0";
|
|
1105
1518
|
/**
|
|
1106
1519
|
* Build a frozen Codex provider envelope.
|
|
1107
1520
|
* @param effects - Provider-native effect record.
|
|
@@ -1225,6 +1638,7 @@ function createCodexStopBlockEffect(reason) {
|
|
|
1225
1638
|
}
|
|
1226
1639
|
const CODEX_RESPONSE_CAPABILITIES = Object.freeze([
|
|
1227
1640
|
"context.append",
|
|
1641
|
+
CANONICAL_HOOK_RESPONSE_CAPABILITIES.sessionToken,
|
|
1228
1642
|
CODEX_HOOK_RESPONSE_CAPABILITIES.block,
|
|
1229
1643
|
CODEX_HOOK_RESPONSE_CAPABILITIES.permissionDeny,
|
|
1230
1644
|
CODEX_HOOK_RESPONSE_CAPABILITIES.inputUpdate
|
|
@@ -1235,6 +1649,7 @@ const CODEX_SUPPORTED_INTERACTIONS = Object.freeze([
|
|
|
1235
1649
|
CODEX_HOOK_PRE_TOOL_USE,
|
|
1236
1650
|
CODEX_HOOK_POST_TOOL_USE,
|
|
1237
1651
|
CODEX_HOOK_STOP,
|
|
1652
|
+
CODEX_HOOK_SUBAGENT_START,
|
|
1238
1653
|
...CODEX_RESPONSE_CAPABILITIES
|
|
1239
1654
|
]);
|
|
1240
1655
|
const CODEX_INTERACTION_BLOCKABILITY = Object.freeze(CODEX_SUPPORTED_INTERACTIONS.map((interaction) => Object.freeze({
|
|
@@ -1267,7 +1682,8 @@ const EVENT_EFFECTS = Object.freeze({
|
|
|
1267
1682
|
"update"
|
|
1268
1683
|
]),
|
|
1269
1684
|
[CODEX_HOOK_POST_TOOL_USE]: new Set(["context", "block"]),
|
|
1270
|
-
[CODEX_HOOK_STOP]: new Set(["block"])
|
|
1685
|
+
[CODEX_HOOK_STOP]: new Set(["block"]),
|
|
1686
|
+
[CODEX_HOOK_SUBAGENT_START]: new Set(["context"])
|
|
1271
1687
|
});
|
|
1272
1688
|
/**
|
|
1273
1689
|
* Classify an exact provider-native Codex effects record.
|
|
@@ -1335,6 +1751,22 @@ const codexProviderContractCatalog = Object.freeze({
|
|
|
1335
1751
|
function capabilities(eventName) {
|
|
1336
1752
|
return clientDefinition.runtimeCapabilities.hookEvents.find((event) => event.name === eventName)?.responseCapabilities ?? [];
|
|
1337
1753
|
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Extract the winning `session.token` value from the ordered effects array.
|
|
1756
|
+
*
|
|
1757
|
+
* Effects arrive in priority-descending order (highest-priority contributor
|
|
1758
|
+
* first), so the first matching `session.token` effect is the winner. When the
|
|
1759
|
+
* event does not declare the `session.token` capability the effect is dropped
|
|
1760
|
+
* and `undefined` is returned — this ensures the token is only collected when
|
|
1761
|
+
* the contributor explicitly targeted an event that supports it.
|
|
1762
|
+
* @param eventName - Native Codex hook event name.
|
|
1763
|
+
* @param effects - Deterministically ordered effects (priority-desc).
|
|
1764
|
+
* @returns The winning token value, or `undefined` when absent or not supported.
|
|
1765
|
+
*/
|
|
1766
|
+
function extractSessionToken(eventName, effects) {
|
|
1767
|
+
if (!capabilities(eventName).includes(CANONICAL_HOOK_RESPONSE_CAPABILITIES.sessionToken)) return void 0;
|
|
1768
|
+
for (const effect of effects) if ("kind" in effect && effect.kind === "session.token") return effect.value;
|
|
1769
|
+
}
|
|
1338
1770
|
const FIRST_BLOCK_EVENTS = new Set([
|
|
1339
1771
|
CODEX_HOOK_SESSION_START,
|
|
1340
1772
|
CODEX_HOOK_USER_PROMPT_SUBMIT,
|
|
@@ -1462,10 +1894,44 @@ function renderCodexNativeResponse(eventName, effects) {
|
|
|
1462
1894
|
return serialize({ hookSpecificOutput });
|
|
1463
1895
|
}
|
|
1464
1896
|
/**
|
|
1897
|
+
* Derive the session-token storage scope for a Codex hook event.
|
|
1898
|
+
*
|
|
1899
|
+
* `SubagentStart` requires a non-empty `agent_id` in the payload. The hook
|
|
1900
|
+
* normalizers that feed this composer reject subagent events without an id,
|
|
1901
|
+
* so a missing id signals a malformed event. Forwarding without an `agentId`
|
|
1902
|
+
* would store the token under the parent session key and overwrite it; the
|
|
1903
|
+
* sink is skipped instead (returns `undefined`).
|
|
1904
|
+
*
|
|
1905
|
+
* `SessionStart` and every other event are always session-scoped. Even when
|
|
1906
|
+
* a stray `agent_id` appears in the payload it is never adopted — doing so
|
|
1907
|
+
* would create a spurious subagent-scoped entry.
|
|
1908
|
+
* @param eventName - Codex hook event name.
|
|
1909
|
+
* @param rawPayload - The inner `payload` object from the hook envelope.
|
|
1910
|
+
* @returns Scope to forward to the token sink, or `undefined` to skip it.
|
|
1911
|
+
*/
|
|
1912
|
+
function resolveSessionTokenScope(eventName, rawPayload) {
|
|
1913
|
+
const adapterSessionId = pickNonEmptyString(rawPayload, "session_id") ?? pickNonEmptyString(rawPayload, "thread_id");
|
|
1914
|
+
if (adapterSessionId === void 0) return void 0;
|
|
1915
|
+
if (eventName === "SubagentStart") {
|
|
1916
|
+
const agentId = pickNonEmptyString(rawPayload, "agent_id");
|
|
1917
|
+
if (agentId === void 0) return void 0;
|
|
1918
|
+
return {
|
|
1919
|
+
clientId: "codex",
|
|
1920
|
+
adapterSessionId,
|
|
1921
|
+
agentId
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
return {
|
|
1925
|
+
clientId: "codex",
|
|
1926
|
+
adapterSessionId
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1465
1930
|
* Compose one terminal Codex native hook response.
|
|
1466
1931
|
* @param registry - Active response contributor registry.
|
|
1467
1932
|
* @param payload - Normalized native hook payload.
|
|
1468
|
-
* @param options - Request deadline, cancellation,
|
|
1933
|
+
* @param options - Request deadline, cancellation, diagnostics hooks, and
|
|
1934
|
+
* optional session-token sink.
|
|
1469
1935
|
* @returns The composed native response envelope.
|
|
1470
1936
|
*/
|
|
1471
1937
|
async function composeCodexHookResponse(registry, payload, options) {
|
|
@@ -1481,7 +1947,13 @@ async function composeCodexHookResponse(registry, payload, options) {
|
|
|
1481
1947
|
reason: result.closedFailure.detail
|
|
1482
1948
|
}
|
|
1483
1949
|
}]);
|
|
1484
|
-
|
|
1950
|
+
const effects = result.outcomes.flatMap((outcome) => outcome.effects ?? []);
|
|
1951
|
+
const token = extractSessionToken(payload.eventName, effects);
|
|
1952
|
+
if (token !== void 0 && options?.onSessionToken !== void 0) {
|
|
1953
|
+
const scope = resolveSessionTokenScope(payload.eventName, payload.payload);
|
|
1954
|
+
if (scope !== void 0) await options.onSessionToken(token, scope);
|
|
1955
|
+
}
|
|
1956
|
+
return renderCodexNativeResponse(payload.eventName, effects);
|
|
1485
1957
|
}
|
|
1486
1958
|
|
|
1487
1959
|
//#endregion
|
|
@@ -2578,10 +3050,23 @@ const CODEX_HOOK_COMMAND_SENTINEL = "hook received codex";
|
|
|
2578
3050
|
/** Sentinel for synchronous Codex hook responses. */
|
|
2579
3051
|
const CODEX_HOOK_HANDLE_COMMAND_SENTINEL = "hook handle codex";
|
|
2580
3052
|
/**
|
|
2581
|
-
*
|
|
3053
|
+
* Timeout for request-mode hooks on context-only (non-blockable) interactions.
|
|
2582
3054
|
*
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
3055
|
+
* Request-mode hooks carry `--debounce-failure`, so a down server is detected
|
|
3056
|
+
* quickly and cool-down suppression kicks in for subsequent invocations.
|
|
3057
|
+
* Context-only hooks still fail fast (1 s) to bound the overhead on each prompt
|
|
3058
|
+
* or subagent spawn when the bus is genuinely unreachable.
|
|
3059
|
+
* Blockable interactions retain {@link DEFAULT_HOOK_HANDLE_TIMEOUT_MS} because
|
|
3060
|
+
* those must complete before the native client can proceed.
|
|
3061
|
+
*/
|
|
3062
|
+
const CONTEXT_ONLY_HOOK_HANDLE_TIMEOUT_MS = 1e3;
|
|
3063
|
+
/**
|
|
3064
|
+
* Descriptors for all hook events derived from the client definition.
|
|
3065
|
+
*
|
|
3066
|
+
* Includes every event declared in the definition's `hookEvents` array,
|
|
3067
|
+
* regardless of whether it carries a `frameworkSubject`. Events without a
|
|
3068
|
+
* framework mapping (e.g. `PostCompact`) are still wired so that the raw
|
|
3069
|
+
* ingress reaches the bus for Codex-specific consumers.
|
|
2585
3070
|
*/
|
|
2586
3071
|
const SESSION_EVENTS = deriveSessionEventDescriptors(clientDefinition);
|
|
2587
3072
|
/**
|
|
@@ -2684,19 +3169,28 @@ async function removeCodexWiring(settings, scope, projectDir) {
|
|
|
2684
3169
|
}
|
|
2685
3170
|
/**
|
|
2686
3171
|
* Build the managed command for one capability-derived hook mode.
|
|
3172
|
+
*
|
|
3173
|
+
* For request-mode hooks the timeout is derived from the event's blockability:
|
|
3174
|
+
* blockable interactions get {@link DEFAULT_HOOK_HANDLE_TIMEOUT_MS} (5 s);
|
|
3175
|
+
* non-blockable, context-only interactions get
|
|
3176
|
+
* {@link CONTEXT_ONLY_HOOK_HANDLE_TIMEOUT_MS} (1 s) so a down server does not
|
|
3177
|
+
* stall every prompt or subagent spawn for the full duration.
|
|
2687
3178
|
* @param makaioCommand - Makaio CLI executable.
|
|
2688
|
-
* @param eventName - Native Codex event name.
|
|
3179
|
+
* @param eventName - Native Codex event name (used to look up blockability).
|
|
2689
3180
|
* @param mode - Capability-derived transport mode.
|
|
2690
3181
|
* @returns Shell-safe managed hook command.
|
|
2691
3182
|
*/
|
|
2692
3183
|
function buildModeCommand(makaioCommand, eventName, mode) {
|
|
2693
|
-
|
|
3184
|
+
if (mode !== "request") return buildHookCommand(makaioCommand, CODEX_HOOK_COMMAND_SENTINEL, eventName, void 0, ["--debounce-failure"]);
|
|
3185
|
+
const timeoutMs = CODEX_INTERACTION_BLOCKABILITY.some((entry) => entry.interaction === eventName && entry.blockable) ? DEFAULT_HOOK_HANDLE_TIMEOUT_MS : CONTEXT_ONLY_HOOK_HANDLE_TIMEOUT_MS;
|
|
3186
|
+
return buildClientCommand(makaioCommand, [
|
|
2694
3187
|
"--no-launch",
|
|
3188
|
+
"--debounce-failure",
|
|
2695
3189
|
...CODEX_HOOK_HANDLE_COMMAND_SENTINEL.split(" "),
|
|
2696
3190
|
eventName,
|
|
2697
3191
|
"--timeout",
|
|
2698
|
-
String(
|
|
2699
|
-
])
|
|
3192
|
+
String(timeoutMs)
|
|
3193
|
+
]);
|
|
2700
3194
|
}
|
|
2701
3195
|
|
|
2702
3196
|
//#endregion
|
|
@@ -2714,6 +3208,37 @@ const CLIENT_ID = "codex";
|
|
|
2714
3208
|
*/
|
|
2715
3209
|
const MANAGED_SESSION_CAP = 1e4;
|
|
2716
3210
|
/**
|
|
3211
|
+
* Set of `client.session.*` subjects that the `codex-app-server` adapter
|
|
3212
|
+
* emits for adapter-managed sessions.
|
|
3213
|
+
*
|
|
3214
|
+
* Derived from
|
|
3215
|
+
* `adapters/implementations/codex-app-server/src/agent.ts`
|
|
3216
|
+
* (lines 178, 186, 195, 217, 338, 383, 411, 438, 473, 495).
|
|
3217
|
+
*
|
|
3218
|
+
* When both the native-hook ingress and the adapter path are active for the
|
|
3219
|
+
* same session, only these subjects are suppressed by
|
|
3220
|
+
* {@link CodexClientSessionService.handleHookReceived} — the adapter already
|
|
3221
|
+
* owns their canonical emission. Hook-only subjects that have no adapter
|
|
3222
|
+
* equivalent (`subagent.started`, `subagent.completed`, `compaction.pre`)
|
|
3223
|
+
* are NOT in this set and must always be forwarded even for managed sessions.
|
|
3224
|
+
*
|
|
3225
|
+
* `client.session.started` is in this set (the adapter emits it once at thread
|
|
3226
|
+
* start), but the gate exempts it when `startMode` is `'compact'` or `'clear'`.
|
|
3227
|
+
* The reason: the adapter emits `session.started` only once — at thread start,
|
|
3228
|
+
* without a `startMode` — and never again for compaction or clear restarts.
|
|
3229
|
+
* Those transitions happen inside the running thread, so the hook-derived
|
|
3230
|
+
* `session.started{startMode:'compact'|'clear'}` is the sole signal for them
|
|
3231
|
+
* and must always be forwarded even for adapter-managed sessions.
|
|
3232
|
+
*/
|
|
3233
|
+
const ADAPTER_EMITTED_SUBJECTS = new Set([
|
|
3234
|
+
ClientSubjects.session.started,
|
|
3235
|
+
ClientSubjects.session.turn.started,
|
|
3236
|
+
ClientSubjects.session.turn.completed,
|
|
3237
|
+
ClientSubjects.session.userPrompt.submitted,
|
|
3238
|
+
ClientSubjects.session.tool.pre,
|
|
3239
|
+
ClientSubjects.session.tool.post
|
|
3240
|
+
]);
|
|
3241
|
+
/**
|
|
2717
3242
|
* Service that normalizes raw Codex hook events into global
|
|
2718
3243
|
* `client.session.*` observed-semantics events and handles Codex config
|
|
2719
3244
|
* management requests on `client:codex.config.hooks.*`.
|
|
@@ -2769,6 +3294,15 @@ var CodexClientSessionService = class extends BaseService {
|
|
|
2769
3294
|
providerContractRegistry;
|
|
2770
3295
|
hookResponseRegistry;
|
|
2771
3296
|
/**
|
|
3297
|
+
* In-process sink for session correlation tokens.
|
|
3298
|
+
*
|
|
3299
|
+
* When non-undefined, the {@link onSessionToken} callback hands the token
|
|
3300
|
+
* directly to this sink so no bus payload (and no `MAKAIO_DEBUG` bus logger)
|
|
3301
|
+
* ever sees the token value. Codex declares no session-token capability, so
|
|
3302
|
+
* this will typically remain `undefined`.
|
|
3303
|
+
*/
|
|
3304
|
+
sessionTokens;
|
|
3305
|
+
/**
|
|
2772
3306
|
* Creates a new Codex client session service.
|
|
2773
3307
|
* @param bus - Bus instance used for subscribing and emitting events
|
|
2774
3308
|
* @param settings - Optional {@link CodexClientSettings} instance for tests
|
|
@@ -2782,14 +3316,20 @@ var CodexClientSessionService = class extends BaseService {
|
|
|
2782
3316
|
* from clients-core for registering the Codex hook response contract.
|
|
2783
3317
|
* Omit in tests that do not exercise the response pipeline.
|
|
2784
3318
|
* @param hookResponseRegistry - Optional contributor registry used by the terminal composer.
|
|
3319
|
+
* @param sessionTokens - In-process token sink from clients-core. When
|
|
3320
|
+
* supplied, the {@link onSessionToken} callback hands the token directly
|
|
3321
|
+
* to the sink — no bus payload (and no `MAKAIO_DEBUG` bus logger) ever
|
|
3322
|
+
* sees the token value. Codex declares no session-token capability, so
|
|
3323
|
+
* this will typically be `undefined`.
|
|
2785
3324
|
*/
|
|
2786
|
-
constructor(bus = MakaioBus, settings, machineId, sessionConfigHandler = new CodexSessionConfigHandler(), providerContractRegistry, hookResponseRegistry) {
|
|
3325
|
+
constructor(bus = MakaioBus, settings, machineId, sessionConfigHandler = new CodexSessionConfigHandler(), providerContractRegistry, hookResponseRegistry, sessionTokens) {
|
|
2787
3326
|
super(bus);
|
|
2788
3327
|
this.settingsOverride = settings;
|
|
2789
3328
|
this.machineId = machineId;
|
|
2790
3329
|
this.sessionConfigHandler = sessionConfigHandler;
|
|
2791
3330
|
this.providerContractRegistry = providerContractRegistry;
|
|
2792
3331
|
this.hookResponseRegistry = hookResponseRegistry;
|
|
3332
|
+
this.sessionTokens = sessionTokens;
|
|
2793
3333
|
}
|
|
2794
3334
|
/**
|
|
2795
3335
|
* Register the raw hook ingress handler, the `hook.handle` request handler,
|
|
@@ -2886,7 +3426,10 @@ var CodexClientSessionService = class extends BaseService {
|
|
|
2886
3426
|
return composeCodexHookResponse(this.hookResponseRegistry, ctx.payload, {
|
|
2887
3427
|
deadline: ctx.deadline,
|
|
2888
3428
|
signal: ctx.signal,
|
|
2889
|
-
onDiagnostics: (diagnostics) => diagnostics.forEach((diagnostic) => console.warn(`[CodexClientSessionService] Hook contributor '${diagnostic.contributorId}': ${diagnostic.message}`))
|
|
3429
|
+
onDiagnostics: (diagnostics) => diagnostics.forEach((diagnostic) => console.warn(`[CodexClientSessionService] Hook contributor '${diagnostic.contributorId}': ${diagnostic.message}`)),
|
|
3430
|
+
onSessionToken: (token, scope) => {
|
|
3431
|
+
this.sessionTokens?.record(scope, token);
|
|
3432
|
+
}
|
|
2890
3433
|
}).then((response) => ctx.setResult(response));
|
|
2891
3434
|
});
|
|
2892
3435
|
}
|
|
@@ -2963,35 +3506,68 @@ var CodexClientSessionService = class extends BaseService {
|
|
|
2963
3506
|
}
|
|
2964
3507
|
}
|
|
2965
3508
|
/**
|
|
2966
|
-
* Translate a raw Codex hook event into
|
|
3509
|
+
* Translate a raw Codex hook event into normalized `client.session.*` emissions.
|
|
2967
3510
|
*
|
|
2968
3511
|
* Unknown / Codex-specific events produce no emission and are silently
|
|
2969
3512
|
* ignored. The raw event remains observable on `client:codex.*` for
|
|
2970
3513
|
* consumers that need Codex-native detail.
|
|
3514
|
+
*
|
|
3515
|
+
* One raw hook may produce multiple normalized events (e.g. `UserPromptSubmit`
|
|
3516
|
+
* yields `turn.started` then `userPrompt.submitted`). Events are emitted in
|
|
3517
|
+
* the order returned by the normalizer.
|
|
2971
3518
|
* @param raw - Raw hook payload delivered on `client:codex.hook.received`
|
|
2972
3519
|
*/
|
|
2973
3520
|
async handleHookReceived(raw) {
|
|
2974
|
-
const
|
|
2975
|
-
if (
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3521
|
+
const events = normalizeCodexHook(raw, this.machineId);
|
|
3522
|
+
if (events.length === 0 && (raw.eventName === "SubagentStart" || raw.eventName === "SubagentStop")) console.warn(`[CodexClientSessionService] ${raw.eventName} hook produced no normalized events — agent_id is likely absent. The hook remains raw-only on client:codex.*.`);
|
|
3523
|
+
const sharedAdapterSessionId = events[0]?.payload.adapterSessionId;
|
|
3524
|
+
const isManagedSession = this.isAdapterManagedSession(sharedAdapterSessionId);
|
|
3525
|
+
let firstError;
|
|
3526
|
+
for (const normalized of events) {
|
|
3527
|
+
if (this.shouldSuppressForManagedSession(normalized, isManagedSession)) continue;
|
|
3528
|
+
try {
|
|
3529
|
+
switch (normalized.subject) {
|
|
3530
|
+
case ClientSubjects.session.started:
|
|
3531
|
+
await this.bus.emit(ClientSubjects.session.started, await this.enrichForkLineage(normalized.payload));
|
|
3532
|
+
break;
|
|
3533
|
+
case ClientSubjects.session.userPrompt.submitted:
|
|
3534
|
+
await this.bus.emit(ClientSubjects.session.userPrompt.submitted, normalized.payload);
|
|
3535
|
+
break;
|
|
3536
|
+
case ClientSubjects.session.turn.started:
|
|
3537
|
+
await this.bus.emit(ClientSubjects.session.turn.started, normalized.payload);
|
|
3538
|
+
break;
|
|
3539
|
+
case ClientSubjects.session.turn.completed:
|
|
3540
|
+
await this.bus.emit(ClientSubjects.session.turn.completed, normalized.payload);
|
|
3541
|
+
break;
|
|
3542
|
+
case ClientSubjects.session.tool.pre:
|
|
3543
|
+
await this.bus.emit(ClientSubjects.session.tool.pre, normalized.payload);
|
|
3544
|
+
break;
|
|
3545
|
+
case ClientSubjects.session.tool.post:
|
|
3546
|
+
await this.bus.emit(ClientSubjects.session.tool.post, normalized.payload);
|
|
3547
|
+
break;
|
|
3548
|
+
case ClientSubjects.session.subagent.started: {
|
|
3549
|
+
const { payload: subagentStartedPayload } = normalized;
|
|
3550
|
+
await this.bus.emit(ClientSubjects.session.subagent.started, subagentStartedPayload);
|
|
3551
|
+
break;
|
|
3552
|
+
}
|
|
3553
|
+
case ClientSubjects.session.subagent.completed: {
|
|
3554
|
+
const { payload: subagentCompletedPayload } = normalized;
|
|
3555
|
+
await this.bus.emit(ClientSubjects.session.subagent.completed, subagentCompletedPayload);
|
|
3556
|
+
break;
|
|
3557
|
+
}
|
|
3558
|
+
case ClientSubjects.session.compaction.pre: {
|
|
3559
|
+
const { payload: compactionPrePayload } = normalized;
|
|
3560
|
+
await this.bus.emit(ClientSubjects.session.compaction.pre, compactionPrePayload);
|
|
3561
|
+
break;
|
|
3562
|
+
}
|
|
3563
|
+
default: throwUnhandledNormalizedEvent(normalized);
|
|
3564
|
+
}
|
|
3565
|
+
} catch (error) {
|
|
3566
|
+
console.warn("[CodexClientSessionService] Subscriber threw during emission of", normalized.subject.subject, "— continuing with next event.", error);
|
|
3567
|
+
if (firstError === void 0) firstError = error;
|
|
3568
|
+
}
|
|
2994
3569
|
}
|
|
3570
|
+
if (firstError !== void 0) throw firstError;
|
|
2995
3571
|
}
|
|
2996
3572
|
/**
|
|
2997
3573
|
* Determine whether a normalized native hook belongs to an adapter-managed
|
|
@@ -3003,17 +3579,69 @@ var CodexClientSessionService = class extends BaseService {
|
|
|
3003
3579
|
isAdapterManagedSession(adapterSessionId) {
|
|
3004
3580
|
return adapterSessionId !== void 0 && this.managedAdapterSessionIds.has(adapterSessionId);
|
|
3005
3581
|
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Returns true when a normalized event should be suppressed by the
|
|
3584
|
+
* adapter-managed gate — that is, when the session is adapter-managed AND the
|
|
3585
|
+
* subject is one the adapter emits AND the event is not a compaction or clear
|
|
3586
|
+
* restart start (which have no adapter counterpart and must always be forwarded).
|
|
3587
|
+
* @param normalized - Normalized hook event to evaluate
|
|
3588
|
+
* @param isManagedSession - Whether the originating session is adapter-managed
|
|
3589
|
+
* @returns True when the event should be dropped from the native-hook path
|
|
3590
|
+
*/
|
|
3591
|
+
shouldSuppressForManagedSession(normalized, isManagedSession) {
|
|
3592
|
+
if (!isManagedSession || !ADAPTER_EMITTED_SUBJECTS.has(normalized.subject)) return false;
|
|
3593
|
+
if (normalized.subject !== ClientSubjects.session.started) return true;
|
|
3594
|
+
const { startMode } = normalized.payload;
|
|
3595
|
+
return startMode !== "compact" && startMode !== "clear";
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Enrich a `client.session.started` payload with fork lineage when the
|
|
3599
|
+
* normalizer reported `startMode: 'fresh'` and a rollout path is available.
|
|
3600
|
+
*
|
|
3601
|
+
* Codex classifies a fork child next to a brand-new thread: both fire
|
|
3602
|
+
* `SessionStart` with `source: 'startup'`, and the payload carries no
|
|
3603
|
+
* lineage field. The child's rollout file, however, opens with its own
|
|
3604
|
+
* `session_meta` record, and that record names `forked_from_id` — the parent
|
|
3605
|
+
* thread id. This method performs a bounded read of the rollout head to
|
|
3606
|
+
* recover it, upgrading `startMode` from `'fresh'` to `'fork'` and
|
|
3607
|
+
* populating `parentAdapterSessionId`.
|
|
3608
|
+
*
|
|
3609
|
+
* Only `'fresh'` is sniffed. A resume appends to the *existing* rollout file,
|
|
3610
|
+
* so a resumed fork child would still show its original `forked_from_id`;
|
|
3611
|
+
* upgrading it to `'fork'` would re-register an already known session instead
|
|
3612
|
+
* of letting ingestion rebind it by adapter session id.
|
|
3613
|
+
*
|
|
3614
|
+
* Runs **after** the managed-session suppression gate (so adapter-managed
|
|
3615
|
+
* sessions are already filtered out) and **before** bus emission.
|
|
3616
|
+
*
|
|
3617
|
+
* On any sniff error the payload is returned unchanged — hook processing must
|
|
3618
|
+
* never be blocked by a sniff failure.
|
|
3619
|
+
* @param payload - Normalized `client.session.started` payload
|
|
3620
|
+
* @returns The payload, potentially enriched with fork lineage fields
|
|
3621
|
+
*/
|
|
3622
|
+
async enrichForkLineage(payload) {
|
|
3623
|
+
if (payload.startMode !== "fresh" || payload.transcriptPath === void 0 || payload.adapterSessionId === void 0) return payload;
|
|
3624
|
+
const sniffResult = await sniffRolloutFork(payload.transcriptPath, payload.adapterSessionId);
|
|
3625
|
+
if (sniffResult === void 0) return payload;
|
|
3626
|
+
return {
|
|
3627
|
+
...payload,
|
|
3628
|
+
startMode: "fork",
|
|
3629
|
+
parentAdapterSessionId: sniffResult.parentAdapterSessionId
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3006
3632
|
};
|
|
3007
3633
|
/**
|
|
3008
3634
|
* Fail fast when the normalizer grows a new subject but service emission has
|
|
3009
3635
|
* not been updated to preserve the normalized-event contract.
|
|
3010
3636
|
*
|
|
3011
3637
|
* The broad parameter type is intentional — the switch operates on
|
|
3012
|
-
* `SubjectDefinition`
|
|
3013
|
-
* TypeScript cannot narrow `normalized` to `never` in the
|
|
3014
|
-
*
|
|
3015
|
-
*
|
|
3016
|
-
* @
|
|
3638
|
+
* `SubjectDefinition` object references rather than a discriminated string
|
|
3639
|
+
* literal union, so TypeScript cannot narrow `normalized` to `never` in the
|
|
3640
|
+
* default branch. Exhaustiveness is therefore enforced at runtime by this
|
|
3641
|
+
* throw, not at compile time. When adding a new arm to the switch in
|
|
3642
|
+
* {@link CodexClientSessionService.handleHookReceived}, verify that the new
|
|
3643
|
+
* subject is also covered here by running the test suite.
|
|
3644
|
+
* @param event - Normalized event whose subject is not handled above
|
|
3017
3645
|
*/
|
|
3018
3646
|
function throwUnhandledNormalizedEvent(event) {
|
|
3019
3647
|
const subject = event.subject.subject;
|