@gakr-gakr/codex 0.1.0

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.
Files changed (86) hide show
  1. package/autobot.plugin.json +374 -0
  2. package/doctor-contract-api.ts +68 -0
  3. package/harness.ts +72 -0
  4. package/index.ts +124 -0
  5. package/media-understanding-provider.ts +521 -0
  6. package/package.json +40 -0
  7. package/prompt-overlay.ts +21 -0
  8. package/provider-catalog.ts +83 -0
  9. package/provider-discovery.ts +45 -0
  10. package/provider.ts +243 -0
  11. package/src/app-server/app-inventory-cache.ts +324 -0
  12. package/src/app-server/approval-bridge.ts +1211 -0
  13. package/src/app-server/auth-bridge.ts +614 -0
  14. package/src/app-server/capabilities.ts +27 -0
  15. package/src/app-server/client-factory.ts +24 -0
  16. package/src/app-server/client.ts +715 -0
  17. package/src/app-server/compact.ts +512 -0
  18. package/src/app-server/computer-use.ts +683 -0
  19. package/src/app-server/config.ts +1038 -0
  20. package/src/app-server/context-engine-projection.ts +403 -0
  21. package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
  22. package/src/app-server/dynamic-tool-profile.ts +70 -0
  23. package/src/app-server/dynamic-tools.ts +623 -0
  24. package/src/app-server/elicitation-bridge.ts +783 -0
  25. package/src/app-server/event-projector.ts +2065 -0
  26. package/src/app-server/image-payload-sanitizer.ts +167 -0
  27. package/src/app-server/local-runtime-attribution.ts +39 -0
  28. package/src/app-server/managed-binary.ts +193 -0
  29. package/src/app-server/models.ts +172 -0
  30. package/src/app-server/native-hook-relay.ts +150 -0
  31. package/src/app-server/native-subagent-task-mirror.ts +497 -0
  32. package/src/app-server/plugin-activation.ts +283 -0
  33. package/src/app-server/plugin-app-cache-key.ts +74 -0
  34. package/src/app-server/plugin-approval-roundtrip.ts +122 -0
  35. package/src/app-server/plugin-inventory.ts +357 -0
  36. package/src/app-server/plugin-thread-config.ts +455 -0
  37. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
  38. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
  39. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
  40. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
  41. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
  42. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
  43. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
  44. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
  45. package/src/app-server/protocol-validators.ts +203 -0
  46. package/src/app-server/protocol.ts +520 -0
  47. package/src/app-server/rate-limit-cache.ts +48 -0
  48. package/src/app-server/rate-limits.ts +583 -0
  49. package/src/app-server/request.ts +73 -0
  50. package/src/app-server/run-attempt.ts +4862 -0
  51. package/src/app-server/session-binding.ts +398 -0
  52. package/src/app-server/session-history.ts +44 -0
  53. package/src/app-server/shared-client.ts +289 -0
  54. package/src/app-server/side-question.ts +1009 -0
  55. package/src/app-server/test-support.ts +48 -0
  56. package/src/app-server/thread-lifecycle.ts +959 -0
  57. package/src/app-server/timeout.ts +9 -0
  58. package/src/app-server/tool-progress-normalization.ts +77 -0
  59. package/src/app-server/trajectory.ts +368 -0
  60. package/src/app-server/transcript-mirror.ts +208 -0
  61. package/src/app-server/transport-stdio.ts +107 -0
  62. package/src/app-server/transport-websocket.ts +90 -0
  63. package/src/app-server/transport.ts +117 -0
  64. package/src/app-server/user-input-bridge.ts +316 -0
  65. package/src/app-server/version.ts +4 -0
  66. package/src/app-server/vision-tools.ts +12 -0
  67. package/src/command-account.ts +544 -0
  68. package/src/command-formatters.ts +426 -0
  69. package/src/command-handlers.ts +2021 -0
  70. package/src/command-plugins-management.ts +137 -0
  71. package/src/command-rpc.ts +142 -0
  72. package/src/commands.ts +65 -0
  73. package/src/conversation-binding-data.ts +124 -0
  74. package/src/conversation-binding.ts +561 -0
  75. package/src/conversation-control.ts +303 -0
  76. package/src/conversation-turn-collector.ts +186 -0
  77. package/src/conversation-turn-input.ts +106 -0
  78. package/src/migration/apply.ts +501 -0
  79. package/src/migration/helpers.ts +55 -0
  80. package/src/migration/plan.ts +461 -0
  81. package/src/migration/provider.ts +41 -0
  82. package/src/migration/source.ts +643 -0
  83. package/src/migration/targets.ts +25 -0
  84. package/src/node-cli-sessions.ts +711 -0
  85. package/test-api.ts +95 -0
  86. package/tsconfig.json +16 -0
@@ -0,0 +1,150 @@
1
+ import { createHash } from "node:crypto";
2
+ import type {
3
+ NativeHookRelayEvent,
4
+ NativeHookRelayRegistrationHandle,
5
+ } from "autobot/plugin-sdk/agent-harness-runtime";
6
+ import type { JsonObject, JsonValue } from "./protocol.js";
7
+
8
+ export const CODEX_NATIVE_HOOK_RELAY_EVENTS: readonly NativeHookRelayEvent[] = [
9
+ "pre_tool_use",
10
+ "post_tool_use",
11
+ "permission_request",
12
+ "before_agent_finalize",
13
+ ] as const;
14
+
15
+ type CodexHookEventName = "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop";
16
+
17
+ const CODEX_HOOK_EVENT_BY_NATIVE_EVENT: Record<NativeHookRelayEvent, CodexHookEventName> = {
18
+ pre_tool_use: "PreToolUse",
19
+ post_tool_use: "PostToolUse",
20
+ permission_request: "PermissionRequest",
21
+ before_agent_finalize: "Stop",
22
+ };
23
+
24
+ const CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT: Record<NativeHookRelayEvent, string> = {
25
+ pre_tool_use: "pre_tool_use",
26
+ post_tool_use: "post_tool_use",
27
+ permission_request: "permission_request",
28
+ before_agent_finalize: "stop",
29
+ };
30
+
31
+ const CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS = [
32
+ "/<session-flags>/config.toml",
33
+ "<session-flags>/config.toml",
34
+ ] as const;
35
+
36
+ export function buildCodexNativeHookRelayConfig(params: {
37
+ relay: NativeHookRelayRegistrationHandle;
38
+ events?: readonly NativeHookRelayEvent[];
39
+ hookTimeoutSec?: number;
40
+ clearOmittedEvents?: boolean;
41
+ }): JsonObject {
42
+ const events = params.events?.length ? params.events : CODEX_NATIVE_HOOK_RELAY_EVENTS;
43
+ const selectedEvents = new Set<NativeHookRelayEvent>(events);
44
+ const config: JsonObject = {
45
+ "features.hooks": true,
46
+ };
47
+ const hookState: JsonObject = {};
48
+ for (const event of CODEX_NATIVE_HOOK_RELAY_EVENTS) {
49
+ const codexEvent = CODEX_HOOK_EVENT_BY_NATIVE_EVENT[event];
50
+ const selected = selectedEvents.has(event);
51
+ if (!selected || !params.relay.shouldRelayEvent(event)) {
52
+ if (selected || params.clearOmittedEvents) {
53
+ config[`hooks.${codexEvent}`] = [] satisfies JsonValue;
54
+ }
55
+ if (params.clearOmittedEvents) {
56
+ for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) {
57
+ hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] = {
58
+ enabled: false,
59
+ } satisfies JsonValue;
60
+ }
61
+ }
62
+ continue;
63
+ }
64
+ const command = params.relay.commandForEvent(event);
65
+ const timeout = normalizeHookTimeoutSec(params.hookTimeoutSec);
66
+ config[`hooks.${codexEvent}`] = [
67
+ {
68
+ hooks: [
69
+ {
70
+ type: "command",
71
+ command,
72
+ timeout,
73
+ async: false,
74
+ statusMessage: "AutoBot native hook relay",
75
+ },
76
+ ],
77
+ },
78
+ ] satisfies JsonValue;
79
+ const state = {
80
+ enabled: true,
81
+ trusted_hash: codexCommandHookTrustedHash({
82
+ event,
83
+ command,
84
+ timeout,
85
+ statusMessage: "AutoBot native hook relay",
86
+ }),
87
+ };
88
+ for (const sourcePath of CODEX_SESSION_FLAGS_HOOK_SOURCE_PATHS) {
89
+ hookState[`${sourcePath}:${CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[event]}:0:0`] =
90
+ state satisfies JsonValue;
91
+ }
92
+ }
93
+ config["hooks.state"] = hookState;
94
+ return config;
95
+ }
96
+
97
+ export function buildCodexNativeHookRelayDisabledConfig(): JsonObject {
98
+ return {
99
+ "features.hooks": false,
100
+ "hooks.PreToolUse": [],
101
+ "hooks.PostToolUse": [],
102
+ "hooks.PermissionRequest": [],
103
+ "hooks.Stop": [],
104
+ };
105
+ }
106
+
107
+ function normalizeHookTimeoutSec(value: number | undefined): number {
108
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.ceil(value) : 5;
109
+ }
110
+
111
+ function codexCommandHookTrustedHash(params: {
112
+ event: NativeHookRelayEvent;
113
+ command: string;
114
+ timeout: number;
115
+ statusMessage: string;
116
+ }): string {
117
+ // Keep the match-all matcher omitted rather than null. Codex app-server
118
+ // converts JSON null to an empty TOML string before hashing, which changes the
119
+ // trust identity even though both forms match all tools.
120
+ const identity = {
121
+ event_name: CODEX_HOOK_KEY_LABEL_BY_NATIVE_EVENT[params.event],
122
+ hooks: [
123
+ {
124
+ async: false,
125
+ command: params.command,
126
+ statusMessage: params.statusMessage,
127
+ timeout: params.timeout,
128
+ type: "command",
129
+ },
130
+ ],
131
+ };
132
+ const hash = createHash("sha256")
133
+ .update(JSON.stringify(sortJsonValue(identity)))
134
+ .digest("hex");
135
+ return `sha256:${hash}`;
136
+ }
137
+
138
+ function sortJsonValue(value: JsonValue): JsonValue {
139
+ if (!value || typeof value !== "object") {
140
+ return value;
141
+ }
142
+ if (Array.isArray(value)) {
143
+ return value.map(sortJsonValue);
144
+ }
145
+ const sorted: JsonObject = {};
146
+ for (const key of Object.keys(value).toSorted()) {
147
+ sorted[key] = sortJsonValue(value[key]);
148
+ }
149
+ return sorted;
150
+ }
@@ -0,0 +1,497 @@
1
+ import {
2
+ CODEX_NATIVE_SUBAGENT_RUN_ID_PREFIX,
3
+ CODEX_NATIVE_SUBAGENT_RUNTIME,
4
+ CODEX_NATIVE_SUBAGENT_TASK_KIND,
5
+ createRunningTaskRun,
6
+ finalizeTaskRunByRunId,
7
+ recordTaskRunProgressByRunId,
8
+ } from "autobot/plugin-sdk/codex-native-task-runtime";
9
+ import type {
10
+ CodexServerNotification,
11
+ CodexSessionSource,
12
+ CodexSubAgentThreadSpawnSource,
13
+ CodexThread,
14
+ CodexThreadStartedNotification,
15
+ CodexThreadStatus,
16
+ CodexThreadStatusChangedNotification,
17
+ JsonObject,
18
+ JsonValue,
19
+ } from "./protocol.js";
20
+ import { isJsonObject } from "./protocol.js";
21
+
22
+ export type TaskLifecycleRuntime = {
23
+ createRunningTaskRun: typeof createRunningTaskRun;
24
+ recordTaskRunProgressByRunId: typeof recordTaskRunProgressByRunId;
25
+ finalizeTaskRunByRunId: typeof finalizeTaskRunByRunId;
26
+ };
27
+
28
+ export type CodexNativeSubagentTaskMirrorParams = {
29
+ parentThreadId: string;
30
+ requesterSessionKey?: string;
31
+ agentId?: string;
32
+ now?: () => number;
33
+ };
34
+
35
+ const defaultRuntime: TaskLifecycleRuntime = {
36
+ createRunningTaskRun,
37
+ recordTaskRunProgressByRunId,
38
+ finalizeTaskRunByRunId,
39
+ };
40
+
41
+ export class CodexNativeSubagentTaskMirror {
42
+ private readonly mirroredThreadIds = new Set<string>();
43
+ private readonly terminalRunIds = new Set<string>();
44
+ private readonly now: () => number;
45
+
46
+ constructor(
47
+ private readonly params: CodexNativeSubagentTaskMirrorParams,
48
+ private readonly runtime: TaskLifecycleRuntime = defaultRuntime,
49
+ ) {
50
+ this.now = params.now ?? Date.now;
51
+ }
52
+
53
+ handleNotification(notification: CodexServerNotification): void {
54
+ const params = isJsonObject(notification.params) ? notification.params : undefined;
55
+ if (!params) {
56
+ return;
57
+ }
58
+ if (notification.method === "thread/started") {
59
+ this.handleThreadStarted(params);
60
+ return;
61
+ }
62
+ if (notification.method === "thread/status/changed") {
63
+ this.handleThreadStatusChanged(params);
64
+ return;
65
+ }
66
+ if (notification.method === "item/started" || notification.method === "item/completed") {
67
+ this.handleCollabAgentItem(params);
68
+ }
69
+ }
70
+
71
+ private handleThreadStarted(params: JsonObject): void {
72
+ const notification = readThreadStartedNotification(params);
73
+ if (!notification) {
74
+ return;
75
+ }
76
+ const thread = notification.thread;
77
+ const spawn = readSubagentThreadSpawnSource(thread.source, this.params.parentThreadId);
78
+ if (!spawn) {
79
+ return;
80
+ }
81
+ const threadId = thread.id.trim();
82
+ if (!threadId || this.mirroredThreadIds.has(threadId)) {
83
+ return;
84
+ }
85
+ this.mirroredThreadIds.add(threadId);
86
+ const runId = codexNativeSubagentRunId(threadId);
87
+ const label =
88
+ trimOptional(spawn.agent_nickname) ??
89
+ trimOptional(thread.agentNickname) ??
90
+ trimOptional(spawn.agent_role) ??
91
+ trimOptional(thread.agentRole) ??
92
+ "Codex subagent";
93
+ const task =
94
+ trimOptional(thread.preview) ??
95
+ `Codex native subagent${label === "Codex subagent" ? "" : ` ${label}`}`;
96
+ const createdAt = secondsToMillis(thread.createdAt) ?? this.now();
97
+ this.runtime.createRunningTaskRun({
98
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
99
+ taskKind: CODEX_NATIVE_SUBAGENT_TASK_KIND,
100
+ sourceId: runId,
101
+ requesterSessionKey: this.params.requesterSessionKey,
102
+ ...(this.params.requesterSessionKey
103
+ ? {
104
+ ownerKey: this.params.requesterSessionKey,
105
+ scopeKind: "session" as const,
106
+ }
107
+ : {}),
108
+ agentId: this.params.agentId,
109
+ runId,
110
+ label,
111
+ task,
112
+ notifyPolicy: "silent",
113
+ deliveryStatus: "not_applicable",
114
+ preferMetadata: true,
115
+ startedAt: createdAt,
116
+ lastEventAt: this.now(),
117
+ progressSummary: "Codex native subagent started.",
118
+ });
119
+ this.applyStatus(threadId, thread.status);
120
+ }
121
+
122
+ private handleThreadStatusChanged(params: JsonObject): void {
123
+ const notification = readThreadStatusChangedNotification(params);
124
+ if (!notification) {
125
+ return;
126
+ }
127
+ this.applyStatus(notification.threadId, notification.status);
128
+ }
129
+
130
+ private applyStatus(threadId: string, status: CodexThreadStatus | null | undefined): void {
131
+ const statusType = status?.type;
132
+ if (!statusType) {
133
+ return;
134
+ }
135
+ const runId = codexNativeSubagentRunId(threadId);
136
+ if (this.terminalRunIds.has(runId) && statusType !== "systemError") {
137
+ return;
138
+ }
139
+ const eventAt = this.now();
140
+ if (statusType === "active") {
141
+ this.runtime.recordTaskRunProgressByRunId({
142
+ runId,
143
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
144
+ lastEventAt: eventAt,
145
+ progressSummary: "Codex native subagent is active.",
146
+ });
147
+ return;
148
+ }
149
+ if (statusType === "idle") {
150
+ this.terminalRunIds.add(runId);
151
+ this.runtime.finalizeTaskRunByRunId({
152
+ runId,
153
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
154
+ status: "succeeded",
155
+ endedAt: eventAt,
156
+ lastEventAt: eventAt,
157
+ progressSummary: "Codex native subagent is idle.",
158
+ terminalSummary: "Codex native subagent finished.",
159
+ });
160
+ return;
161
+ }
162
+ if (statusType === "systemError") {
163
+ this.terminalRunIds.add(runId);
164
+ this.runtime.finalizeTaskRunByRunId({
165
+ runId,
166
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
167
+ status: "failed",
168
+ endedAt: eventAt,
169
+ lastEventAt: eventAt,
170
+ error: "Codex app-server reported a system error for the native subagent thread.",
171
+ progressSummary: "Codex native subagent hit a system error.",
172
+ terminalSummary: "Codex native subagent failed.",
173
+ });
174
+ return;
175
+ }
176
+ if (statusType === "notLoaded") {
177
+ this.runtime.recordTaskRunProgressByRunId({
178
+ runId,
179
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
180
+ lastEventAt: eventAt,
181
+ progressSummary: "Codex native subagent is not loaded.",
182
+ });
183
+ }
184
+ }
185
+
186
+ private handleCollabAgentItem(params: JsonObject): void {
187
+ const item = isJsonObject(params.item) ? params.item : undefined;
188
+ if (!item || readString(item, "type") !== "collabAgentToolCall") {
189
+ return;
190
+ }
191
+ if (readString(item, "senderThreadId") !== this.params.parentThreadId) {
192
+ return;
193
+ }
194
+ const receiverThreadIds = readStringArray(item.receiverThreadIds);
195
+ const isSpawnAgentTool = normalizeToolName(readString(item, "tool")) === "spawnagent";
196
+ if (isSpawnAgentTool) {
197
+ for (const receiverThreadId of receiverThreadIds) {
198
+ this.createTaskFromCollabSpawnItem(receiverThreadId, item);
199
+ }
200
+ }
201
+ const agentsStates = readAgentsStates(item.agentsStates);
202
+ const toolCallStatus = normalizeCollabToolCallStatus(readString(item, "status"));
203
+ const terminalToolCallThreadIds = new Set<string>();
204
+ if (isSpawnAgentTool && isBlockedOrFailedCollabToolCallStatus(toolCallStatus)) {
205
+ for (const threadId of receiverThreadIds) {
206
+ terminalToolCallThreadIds.add(threadId);
207
+ }
208
+ for (const threadId of agentsStates.keys()) {
209
+ terminalToolCallThreadIds.add(threadId);
210
+ }
211
+ }
212
+ const terminalAgentStateThreadIds = new Set<string>();
213
+ for (const [threadId, state] of agentsStates) {
214
+ const normalizedStatus = normalizeAgentStateStatus(state.status);
215
+ if (
216
+ terminalToolCallThreadIds.has(threadId) &&
217
+ isNonTerminalAgentStateStatus(normalizedStatus)
218
+ ) {
219
+ continue;
220
+ }
221
+ this.applyCollabAgentStatus(threadId, normalizedStatus, state.message);
222
+ if (isTerminalAgentStateStatus(normalizedStatus)) {
223
+ terminalAgentStateThreadIds.add(threadId);
224
+ }
225
+ }
226
+ if (isBlockedOrFailedCollabToolCallStatus(toolCallStatus)) {
227
+ for (const threadId of terminalToolCallThreadIds) {
228
+ if (terminalAgentStateThreadIds.has(threadId)) {
229
+ continue;
230
+ }
231
+ const state = agentsStates.get(threadId);
232
+ this.applyCollabAgentStatus(threadId, toolCallStatus, state?.message);
233
+ }
234
+ }
235
+ }
236
+
237
+ private createTaskFromCollabSpawnItem(threadId: string, item: JsonObject): void {
238
+ const normalizedThreadId = threadId.trim();
239
+ if (!normalizedThreadId || this.mirroredThreadIds.has(normalizedThreadId)) {
240
+ return;
241
+ }
242
+ this.mirroredThreadIds.add(normalizedThreadId);
243
+ const prompt = trimOptional(readString(item, "prompt"));
244
+ const runId = codexNativeSubagentRunId(normalizedThreadId);
245
+ const createdAt = this.now();
246
+ this.runtime.createRunningTaskRun({
247
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
248
+ taskKind: CODEX_NATIVE_SUBAGENT_TASK_KIND,
249
+ sourceId: runId,
250
+ requesterSessionKey: this.params.requesterSessionKey,
251
+ ...(this.params.requesterSessionKey
252
+ ? {
253
+ ownerKey: this.params.requesterSessionKey,
254
+ scopeKind: "session" as const,
255
+ }
256
+ : {}),
257
+ agentId: this.params.agentId,
258
+ runId,
259
+ label: "Codex subagent",
260
+ task: prompt ?? "Codex native subagent",
261
+ notifyPolicy: "silent",
262
+ deliveryStatus: "not_applicable",
263
+ preferMetadata: true,
264
+ startedAt: createdAt,
265
+ lastEventAt: createdAt,
266
+ progressSummary: "Codex native subagent spawned.",
267
+ });
268
+ }
269
+
270
+ private applyCollabAgentStatus(
271
+ threadId: string,
272
+ status: string | undefined,
273
+ message: string | null | undefined,
274
+ ): void {
275
+ const normalizedStatus = normalizeAgentStateStatus(status);
276
+ if (!normalizedStatus) {
277
+ return;
278
+ }
279
+ const runId = codexNativeSubagentRunId(threadId);
280
+ if (this.terminalRunIds.has(runId) && isNonTerminalAgentStateStatus(normalizedStatus)) {
281
+ return;
282
+ }
283
+ const eventAt = this.now();
284
+ if (normalizedStatus === "pendingInit" || normalizedStatus === "running") {
285
+ this.runtime.recordTaskRunProgressByRunId({
286
+ runId,
287
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
288
+ lastEventAt: eventAt,
289
+ progressSummary:
290
+ trimOptional(message) ??
291
+ (normalizedStatus === "pendingInit"
292
+ ? "Codex native subagent is initializing."
293
+ : "Codex native subagent is running."),
294
+ });
295
+ return;
296
+ }
297
+ if (normalizedStatus === "completed") {
298
+ this.terminalRunIds.add(runId);
299
+ this.runtime.finalizeTaskRunByRunId({
300
+ runId,
301
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
302
+ status: "succeeded",
303
+ endedAt: eventAt,
304
+ lastEventAt: eventAt,
305
+ progressSummary: trimOptional(message) ?? "Codex native subagent completed.",
306
+ terminalSummary: trimOptional(message) ?? "Codex native subagent finished.",
307
+ });
308
+ return;
309
+ }
310
+ if (normalizedStatus === "blocked") {
311
+ this.terminalRunIds.add(runId);
312
+ this.runtime.finalizeTaskRunByRunId({
313
+ runId,
314
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
315
+ status: "succeeded",
316
+ endedAt: eventAt,
317
+ lastEventAt: eventAt,
318
+ progressSummary: trimOptional(message) ?? "Codex native subagent blocked.",
319
+ terminalSummary: trimOptional(message) ?? "Codex native subagent blocked.",
320
+ terminalOutcome: "blocked",
321
+ });
322
+ return;
323
+ }
324
+ this.terminalRunIds.add(runId);
325
+ this.runtime.finalizeTaskRunByRunId({
326
+ runId,
327
+ runtime: CODEX_NATIVE_SUBAGENT_RUNTIME,
328
+ status:
329
+ normalizedStatus === "interrupted" || normalizedStatus === "shutdown"
330
+ ? "cancelled"
331
+ : "failed",
332
+ endedAt: eventAt,
333
+ lastEventAt: eventAt,
334
+ error: trimOptional(message) ?? `Codex native subagent status: ${normalizedStatus}`,
335
+ progressSummary: trimOptional(message) ?? `Codex native subagent ${normalizedStatus}.`,
336
+ terminalSummary: trimOptional(message) ?? "Codex native subagent did not complete.",
337
+ });
338
+ }
339
+ }
340
+
341
+ export function codexNativeSubagentRunId(threadId: string): string {
342
+ return `${CODEX_NATIVE_SUBAGENT_RUN_ID_PREFIX}${threadId.trim()}`;
343
+ }
344
+
345
+ export function readSubagentThreadSpawnSource(
346
+ source: CodexSessionSource | null | undefined,
347
+ parentThreadId: string,
348
+ ): CodexSubAgentThreadSpawnSource | undefined {
349
+ if (!source || typeof source !== "object" || !("subAgent" in source)) {
350
+ return undefined;
351
+ }
352
+ const subAgent = source.subAgent;
353
+ if (!subAgent || typeof subAgent !== "object" || !("thread_spawn" in subAgent)) {
354
+ return undefined;
355
+ }
356
+ const spawn = subAgent.thread_spawn;
357
+ if (!spawn || typeof spawn !== "object") {
358
+ return undefined;
359
+ }
360
+ return spawn.parent_thread_id === parentThreadId ? spawn : undefined;
361
+ }
362
+
363
+ function readThreadStartedNotification(
364
+ params: JsonObject,
365
+ ): CodexThreadStartedNotification | undefined {
366
+ const thread = params.thread;
367
+ if (!isJsonObject(thread) || typeof thread.id !== "string") {
368
+ return undefined;
369
+ }
370
+ return { thread: thread as CodexThread };
371
+ }
372
+
373
+ function readThreadStatusChangedNotification(
374
+ params: JsonObject,
375
+ ): CodexThreadStatusChangedNotification | undefined {
376
+ if (typeof params.threadId !== "string") {
377
+ return undefined;
378
+ }
379
+ const status = params.status;
380
+ if (!isJsonObject(status) || !isCodexThreadStatusType(status.type)) {
381
+ return undefined;
382
+ }
383
+ return {
384
+ threadId: params.threadId,
385
+ status: status as CodexThreadStatus,
386
+ };
387
+ }
388
+
389
+ function isCodexThreadStatusType(value: unknown): value is CodexThreadStatus["type"] {
390
+ return value === "notLoaded" || value === "idle" || value === "systemError" || value === "active";
391
+ }
392
+
393
+ function readAgentsStates(
394
+ value: JsonValue | undefined,
395
+ ): Map<string, { status?: string; message?: string | null }> {
396
+ const states = new Map<string, { status?: string; message?: string | null }>();
397
+ if (!isJsonObject(value)) {
398
+ return states;
399
+ }
400
+ for (const [threadId, rawState] of Object.entries(value)) {
401
+ if (!isJsonObject(rawState)) {
402
+ continue;
403
+ }
404
+ const status = readString(rawState, "status");
405
+ const message = readNullableString(rawState, "message");
406
+ states.set(threadId, { status, message });
407
+ }
408
+ return states;
409
+ }
410
+
411
+ function readStringArray(value: JsonValue | undefined): string[] {
412
+ if (!Array.isArray(value)) {
413
+ return [];
414
+ }
415
+ return value.filter((entry): entry is string => typeof entry === "string" && entry.trim() !== "");
416
+ }
417
+
418
+ function readString(value: JsonObject, key: string): string | undefined {
419
+ const entry = value[key];
420
+ return typeof entry === "string" ? entry : undefined;
421
+ }
422
+
423
+ function readNullableString(value: JsonObject, key: string): string | null | undefined {
424
+ const entry = value[key];
425
+ return typeof entry === "string" || entry === null ? entry : undefined;
426
+ }
427
+
428
+ function normalizeToolName(value: string | undefined): string | undefined {
429
+ return value?.replace(/[^a-z0-9]/giu, "").toLowerCase();
430
+ }
431
+
432
+ function normalizeCollabToolCallStatus(value: string | undefined): string | undefined {
433
+ const key = value?.replace(/[^a-z0-9]/giu, "").toLowerCase();
434
+ if (key === "completed" || key === "succeeded" || key === "success") {
435
+ return "completed";
436
+ }
437
+ if (key === "failed" || key === "error" || key === "errored") {
438
+ return "failed";
439
+ }
440
+ if (key === "blocked" || key === "declined") {
441
+ return "blocked";
442
+ }
443
+ if (key === "inprogress" || key === "running") {
444
+ return "running";
445
+ }
446
+ return value?.trim();
447
+ }
448
+
449
+ function isBlockedOrFailedCollabToolCallStatus(value: string | undefined): boolean {
450
+ return value === "failed" || value === "blocked";
451
+ }
452
+
453
+ function isNonTerminalAgentStateStatus(value: string | undefined): boolean {
454
+ return value === "pendingInit" || value === "running";
455
+ }
456
+
457
+ function isTerminalAgentStateStatus(value: string | undefined): boolean {
458
+ return value !== undefined && !isNonTerminalAgentStateStatus(value);
459
+ }
460
+
461
+ function normalizeAgentStateStatus(value: string | undefined): string | undefined {
462
+ const key = value?.replace(/[^a-z0-9]/giu, "").toLowerCase();
463
+ if (!key) {
464
+ return undefined;
465
+ }
466
+ if (key === "pendinginit") {
467
+ return "pendingInit";
468
+ }
469
+ if (key === "inprogress" || key === "running") {
470
+ return "running";
471
+ }
472
+ if (key === "completed" || key === "succeeded" || key === "success") {
473
+ return "completed";
474
+ }
475
+ if (key === "interrupted" || key === "cancelled" || key === "canceled" || key === "shutdown") {
476
+ return key === "shutdown" ? "shutdown" : "interrupted";
477
+ }
478
+ if (key === "failed" || key === "error" || key === "systemerror") {
479
+ return "failed";
480
+ }
481
+ if (key === "blocked" || key === "declined") {
482
+ return "blocked";
483
+ }
484
+ return value?.trim();
485
+ }
486
+
487
+ function secondsToMillis(value: number | null | undefined): number | undefined {
488
+ if (typeof value !== "number" || !Number.isFinite(value)) {
489
+ return undefined;
490
+ }
491
+ return value * 1000;
492
+ }
493
+
494
+ function trimOptional(value: string | null | undefined): string | undefined {
495
+ const trimmed = value?.trim();
496
+ return trimmed ? trimmed : undefined;
497
+ }