@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21

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 (243) hide show
  1. package/CONTRIBUTING.md +170 -0
  2. package/LICENSE +63 -136
  3. package/README.md +83 -66
  4. package/docs/README.md +44 -0
  5. package/docs/acknowledgements.md +14 -0
  6. package/docs/architecture.md +146 -0
  7. package/docs/assets/README.md +27 -0
  8. package/docs/assets/sevro-logo.png +0 -0
  9. package/docs/assets/sevro-symbol.svg +1 -0
  10. package/docs/assets/sevro-wordmark.svg +1 -0
  11. package/docs/builtin-graders-v1.md +41 -13
  12. package/docs/codex-host.md +184 -52
  13. package/docs/contracts.md +19 -0
  14. package/docs/creating-cases.md +139 -0
  15. package/docs/development-cli.md +190 -30
  16. package/docs/documentation-quality.md +61 -0
  17. package/docs/evaluation-workflow.md +110 -0
  18. package/docs/evidence/code-mode-controls-71.md +64 -0
  19. package/docs/evidence/code-mode-programs-85.md +64 -0
  20. package/docs/evidence/codex-state-73/README.md +204 -0
  21. package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
  22. package/docs/evidence/codex-state-73/results.json +661 -0
  23. package/docs/evidence/local-http-74.md +104 -0
  24. package/docs/evidence/runtime-startup-30/README.md +185 -0
  25. package/docs/evidence/runtime-startup-30/after.json +319 -0
  26. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
  27. package/docs/evidence/runtime-startup-30/before.json +403 -0
  28. package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
  29. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
  30. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
  31. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
  32. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
  33. package/docs/evidence/skill-reads-70/results.json +823 -0
  34. package/docs/evidence.md +21 -0
  35. package/docs/extension-protocol-v1.md +50 -16
  36. package/docs/fixture-cleanup-validation.md +3 -0
  37. package/docs/getting-started.md +102 -0
  38. package/docs/guide-evaluation.md +89 -0
  39. package/docs/guide-verification.json +239 -0
  40. package/docs/identity-v1.md +35 -1
  41. package/docs/installing.md +88 -0
  42. package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
  43. package/docs/licensing.md +42 -0
  44. package/docs/native-controls.md +76 -5
  45. package/docs/native-hosts.md +228 -0
  46. package/docs/reading-results.md +141 -0
  47. package/docs/recipes.md +489 -0
  48. package/docs/releases.md +81 -40
  49. package/docs/report-v1.md +29 -1
  50. package/docs/results-v1.md +180 -0
  51. package/docs/run-settings-v1.md +147 -0
  52. package/docs/running-evaluations.md +31 -0
  53. package/docs/runtime-v1.md +614 -0
  54. package/docs/specs/repository-guide.md +58 -0
  55. package/docs/troubleshooting.md +30 -0
  56. package/docs/typescript-quality.md +234 -0
  57. package/docs/usability-study.md +114 -0
  58. package/examples/basic/host.ts +2 -2
  59. package/examples/recipes/candidate.ts +26 -0
  60. package/examples/recipes/cases/coding-change.json +21 -0
  61. package/examples/recipes/cases/host-tools.json +21 -0
  62. package/examples/recipes/cases/mise.json +21 -0
  63. package/examples/recipes/cases/node.json +21 -0
  64. package/examples/recipes/cases/python-uv.json +21 -0
  65. package/examples/recipes/cases/semantic.json +17 -0
  66. package/examples/recipes/coding-change/check.ts +23 -0
  67. package/examples/recipes/extension-artifacts.ts +65 -0
  68. package/examples/recipes/extension-methods.ts +174 -0
  69. package/examples/recipes/extension.ts +37 -0
  70. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  71. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  72. package/examples/recipes/repository-skill/README.md +3 -0
  73. package/examples/recipes/run-inputs.ts +188 -0
  74. package/examples/recipes/run-options.ts +131 -0
  75. package/examples/recipes/run.ts +63 -0
  76. package/examples/recipes/runtime/empty.json +1 -0
  77. package/examples/recipes/runtime/goals.json +1 -0
  78. package/examples/recipes/runtime/mise.json +6 -0
  79. package/examples/recipes/runtime/path.json +1 -0
  80. package/examples/recipes/runtime/plugins.json +4 -0
  81. package/examples/recipes/runtime/python-uv.json +10 -0
  82. package/examples/recipes/semantic-judge.ts +29 -0
  83. package/examples/templates/coding/adapter.ts +17 -0
  84. package/examples/templates/coding/case.json +15 -0
  85. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  86. package/examples/templates/repository-skill/extension-data.ts +64 -0
  87. package/examples/templates/repository-skill/extension.ts +31 -0
  88. package/examples/templates/response/adapter.ts +12 -0
  89. package/examples/templates/response/case.json +13 -0
  90. package/package.json +39 -6
  91. package/schemas/report-v1.schema.json +9 -0
  92. package/schemas/run-evidence-v1.schema.json +2 -1
  93. package/schemas/runtime-v1.schema.json +193 -0
  94. package/src/advisory.ts +10 -5
  95. package/src/candidate-artifacts.ts +208 -0
  96. package/src/candidate-codex-state.ts +185 -0
  97. package/src/case-validation.ts +105 -0
  98. package/src/cli/cancellation.ts +20 -0
  99. package/src/cli/command.ts +81 -0
  100. package/src/cli/completion.ts +15 -0
  101. package/src/cli/demo.ts +59 -0
  102. package/src/cli/evaluation.ts +55 -0
  103. package/src/cli/extension.ts +79 -0
  104. package/src/cli/help.ts +222 -0
  105. package/src/cli/hosts.ts +59 -0
  106. package/src/cli/init-settings.ts +30 -0
  107. package/src/cli/init.ts +111 -0
  108. package/src/cli/invocation.ts +627 -0
  109. package/src/cli/loading.ts +151 -0
  110. package/src/cli/preparation.ts +163 -0
  111. package/src/cli/presentation.ts +91 -0
  112. package/src/cli/progress.ts +6 -0
  113. package/src/cli/run-manifest.ts +133 -0
  114. package/src/cli/run-options.ts +101 -0
  115. package/src/cli/run-overrides.ts +83 -0
  116. package/src/cli/settings.ts +160 -0
  117. package/src/cli/summary.ts +43 -0
  118. package/src/cli.ts +13 -843
  119. package/src/contract-types.ts +94 -0
  120. package/src/engine.ts +19 -2373
  121. package/src/evaluation/advisory.ts +150 -0
  122. package/src/evaluation/check-plan.ts +510 -0
  123. package/src/evaluation/collection.ts +88 -0
  124. package/src/evaluation/context.ts +10 -0
  125. package/src/evaluation/grading-host.ts +39 -0
  126. package/src/evaluation/grading.ts +258 -0
  127. package/src/evaluation/invocation-plan.ts +548 -0
  128. package/src/evaluation/plan.ts +736 -0
  129. package/src/evaluation/progress.ts +19 -0
  130. package/src/evaluation/resources.ts +241 -0
  131. package/src/evaluation/run.ts +206 -0
  132. package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
  133. package/src/evaluation/semantic.ts +312 -0
  134. package/src/evaluation/trial.ts +866 -0
  135. package/src/evaluation/types.ts +207 -0
  136. package/src/evidence/host-results.ts +92 -0
  137. package/src/evidence/retained-artifacts.ts +18 -0
  138. package/src/evidence/trial-writer.ts +203 -0
  139. package/src/evidence/usage.ts +17 -0
  140. package/src/extension-client.ts +228 -148
  141. package/src/extension-session.ts +326 -195
  142. package/src/fixtures/advisory.ts +338 -0
  143. package/src/fixtures/bin.ts +57 -0
  144. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  145. package/src/fixtures/create.ts +187 -0
  146. package/src/fixtures/generated.ts +300 -0
  147. package/src/fixtures/git-hooks.ts +24 -0
  148. package/src/fixtures/preparation.ts +270 -0
  149. package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
  150. package/src/fixtures/setup.ts +224 -0
  151. package/src/fixtures/tools.ts +88 -0
  152. package/src/fixtures/workspace-state.ts +212 -0
  153. package/src/generated/cli-result.cjs +2 -0
  154. package/src/generated/cli-result.d.cts +3 -0
  155. package/src/generated/extension.cjs +2 -0
  156. package/src/generated/extension.d.cts +3 -0
  157. package/src/generated/report.cjs +2 -0
  158. package/src/generated/report.d.cts +3 -0
  159. package/src/generated/run-evidence.cjs +2 -0
  160. package/src/generated/run-evidence.d.cts +3 -0
  161. package/src/generated/runtime.cjs +2 -0
  162. package/src/generated/runtime.d.cts +3 -0
  163. package/src/graders/artifact.ts +56 -23
  164. package/src/graders/git-head.ts +79 -75
  165. package/src/graders/output.ts +222 -140
  166. package/src/graders/semantic.ts +102 -43
  167. package/src/graders/shell.ts +379 -207
  168. package/src/host-diagnostic-redaction.ts +52 -0
  169. package/src/host-diagnostics.ts +319 -0
  170. package/src/hosts/claude-continuation.ts +63 -18
  171. package/src/hosts/claude-credential.ts +43 -62
  172. package/src/hosts/claude-events.ts +86 -51
  173. package/src/hosts/claude-keychain.ts +59 -0
  174. package/src/hosts/claude-native-goal.ts +57 -33
  175. package/src/hosts/claude-process.ts +111 -0
  176. package/src/hosts/claude-repository-invocation.ts +208 -117
  177. package/src/hosts/claude-settings.ts +240 -21
  178. package/src/hosts/claude-skill-activations.ts +166 -0
  179. package/src/hosts/claude-state.ts +403 -0
  180. package/src/hosts/claude-tool-calls.ts +171 -106
  181. package/src/hosts/claude.ts +561 -451
  182. package/src/hosts/codex-app-server.ts +326 -178
  183. package/src/hosts/codex-configuration.ts +44 -18
  184. package/src/hosts/codex-events.ts +97 -60
  185. package/src/hosts/codex-exec-controls.ts +516 -0
  186. package/src/hosts/codex-invocation.ts +70 -0
  187. package/src/hosts/codex-marketplace.ts +283 -0
  188. package/src/hosts/codex-native-calls.ts +546 -500
  189. package/src/hosts/codex-process.ts +202 -0
  190. package/src/hosts/codex-profile.ts +257 -28
  191. package/src/hosts/codex-session-files.ts +106 -0
  192. package/src/hosts/codex-state.ts +492 -0
  193. package/src/hosts/codex.ts +783 -832
  194. package/src/hosts/isolation-common.ts +94 -0
  195. package/src/hosts/isolation-roots.ts +54 -27
  196. package/src/hosts/isolation.ts +10 -0
  197. package/src/hosts/linux-sandbox.ts +159 -0
  198. package/src/hosts/mac-sandbox.ts +187 -61
  199. package/src/hosts/native-controls.ts +152 -62
  200. package/src/hosts/process-lifecycle.ts +37 -0
  201. package/src/hosts/protected-worktrees.ts +51 -19
  202. package/src/hosts/skill-hook-catalog.ts +131 -0
  203. package/src/hosts/skill-hook-handler.ts +374 -0
  204. package/src/hosts/skill-hooks.ts +580 -0
  205. package/src/hosts/workspace-fingerprint.ts +55 -35
  206. package/src/identity.ts +102 -48
  207. package/src/instrumentation.ts +109 -48
  208. package/src/native-transcript-state.ts +131 -0
  209. package/src/native-transcript-view.ts +90 -0
  210. package/src/native-transcripts.ts +286 -0
  211. package/src/process-lifecycle.ts +150 -0
  212. package/src/provenance.ts +161 -81
  213. package/src/prune.ts +215 -0
  214. package/src/report.ts +172 -140
  215. package/src/resolved-case.ts +33 -0
  216. package/src/results-lock.ts +29 -0
  217. package/src/results.ts +133 -86
  218. package/src/run-owner.ts +154 -63
  219. package/src/runtime/config.ts +456 -0
  220. package/src/runtime/discovery-apple.ts +90 -0
  221. package/src/runtime/discovery-providers.ts +63 -0
  222. package/src/runtime/discovery.ts +70 -0
  223. package/src/runtime/hook-processes.ts +97 -0
  224. package/src/runtime/hooks.ts +476 -0
  225. package/src/runtime/paths.ts +313 -0
  226. package/src/runtime/query.ts +85 -0
  227. package/src/runtime/seeds.ts +134 -0
  228. package/src/runtime/shared-cache.ts +143 -0
  229. package/src/runtime/state.ts +143 -0
  230. package/src/schema-types.ts +198 -0
  231. package/src/schema.ts +15 -19
  232. package/src/transcript-retention.ts +146 -0
  233. package/src/value-guards.ts +20 -0
  234. package/src/advisory-fixture.ts +0 -282
  235. package/src/fixture-bin.ts +0 -87
  236. package/src/fixture-setup.ts +0 -163
  237. package/src/generated-fixture.ts +0 -220
  238. package/src/git-hooks.ts +0 -63
  239. package/src/hosts/claude-nested-skills.ts +0 -218
  240. package/src/hosts/codex-command-output.ts +0 -399
  241. package/src/hosts/codex-executor-command.ts +0 -195
  242. package/src/hosts/codex-skill-reads.ts +0 -391
  243. package/src/preparation.ts +0 -211
@@ -1,18 +1,16 @@
1
- import { nativeCommandOutputs } from "./codex-command-output";
2
- import type { Dirent } from "node:fs";
3
- import { lstat, readFile, readdir, stat } from "node:fs/promises";
4
- import { join } from "node:path";
1
+ import { isRecord, isUnknownArray } from "../value-guards";
5
2
  import {
6
- codexNativeReadDiagnostic,
7
- type NativeReadDiagnostic,
8
- } from "./codex-skill-reads";
3
+ codexExecCallReceipts,
4
+ type ExecCallReceipt,
5
+ } from "./codex-exec-controls";
6
+ import {
7
+ readNativeSession,
8
+ locateNativeSession as sessionPath,
9
+ } from "./codex-session-files";
9
10
 
10
- const MAX_SESSION_BYTES = 8 * 1024 * 1024;
11
11
  const MAX_SESSION_ENTRIES = 10_000;
12
- const MAX_SESSION_FILES = 1024;
13
12
  const MAX_RETAINED_CALLS = 128;
14
13
  const MAX_CHILD_SESSIONS = 8;
15
- const THREAD_ID = /^[A-Za-z0-9._-]{1,128}$/;
16
14
  const GOAL_CONTROLS = new Set(["create_goal", "get_goal", "update_goal"]);
17
15
  const COLLABORATION_CONTROLS = new Set([
18
16
  "spawn_agent",
@@ -63,7 +61,6 @@ interface ChildSession {
63
61
  threadId: string;
64
62
  status: "available" | "unavailable" | "ambiguous" | "partial";
65
63
  resultStatus?: "completed" | "unavailable";
66
- readDiagnostics?: NativeReadDiagnostic;
67
64
  nestedSpawns?: NestedSpawn[];
68
65
  requestsTruncated?: boolean;
69
66
  }
@@ -81,9 +78,7 @@ interface NestedSpawn {
81
78
  readerResultStatus: "completed" | "unavailable";
82
79
  }
83
80
 
84
- interface NativeSkillContext {
85
- workspace: string;
86
- installedPluginRoots: string[];
81
+ interface NativeCallContext {
87
82
  followUpPrompt?: string;
88
83
  }
89
84
 
@@ -95,18 +90,14 @@ interface NativeCallObservation {
95
90
  calls: NativeCall[];
96
91
  toolCalls: NativeToolCall[];
97
92
  submittedExecCalls: number;
93
+ execCalls?: ExecCallReceipt[];
98
94
  acceptedSpawns: AcceptedSpawn[];
99
95
  feedbackCalls: NativeFeedbackCall[];
100
- parentReadDiagnostics?: NativeReadDiagnostic;
101
96
  childSessions: ChildSession[];
102
97
  childrenTruncated: boolean;
103
98
  };
104
99
  }
105
100
 
106
- function record(value: unknown): value is Record<string, unknown> {
107
- return value !== null && typeof value === "object" && !Array.isArray(value);
108
- }
109
-
110
101
  function unavailable(
111
102
  completeness: NativeCallObservation["completeness"],
112
103
  ): NativeCallObservation {
@@ -126,76 +117,31 @@ function unavailable(
126
117
  };
127
118
  }
128
119
 
129
- async function sessionPath(home: string, threadId: string) {
130
- if (!THREAD_ID.test(threadId)) return { status: "partial" as const };
131
- const sessionsRoot = join(home, "sessions");
132
- try {
133
- const root = await lstat(sessionsRoot);
134
- if (!root.isDirectory() || root.isSymbolicLink())
135
- return { status: "partial" as const };
136
- } catch (error) {
137
- return {
138
- status:
139
- error instanceof Error && "code" in error && error.code === "ENOENT"
140
- ? ("unavailable" as const)
141
- : ("partial" as const),
142
- };
143
- }
144
- const pending = [sessionsRoot];
145
- const suffix = `-${threadId}.jsonl`;
146
- const matches: string[] = [];
147
- let visited = 0;
148
- while (pending.length) {
149
- const directory = pending.pop()!;
150
- let entries: Dirent<string>[];
151
- try {
152
- entries = await readdir(directory, { withFileTypes: true });
153
- } catch {
154
- return { status: "partial" as const };
155
- }
156
- for (const entry of entries) {
157
- visited++;
158
- if (visited > MAX_SESSION_FILES || entry.isSymbolicLink())
159
- return { status: "partial" as const };
160
- const path = join(directory, entry.name);
161
- if (entry.isDirectory()) pending.push(path);
162
- else if (entry.isFile() && entry.name.endsWith(suffix))
163
- matches.push(path);
164
- if (matches.length > 1) return { status: "ambiguous" as const };
165
- }
166
- }
167
- return matches.length === 1
168
- ? { status: "found" as const, path: matches[0]! }
169
- : { status: "unavailable" as const };
120
+ function nativeControlNamespace(
121
+ payload: Record<string, unknown> & { name: string },
122
+ ): NativeCall["namespace"] | null {
123
+ if (goalNamespace(payload.namespace) && GOAL_CONTROLS.has(payload.name))
124
+ return "functions";
125
+ if (
126
+ payload.namespace === "collaboration" &&
127
+ COLLABORATION_CONTROLS.has(payload.name)
128
+ )
129
+ return "collaboration";
130
+ return null;
131
+ }
132
+ function goalNamespace(value: unknown): boolean {
133
+ return value === "functions" || value === undefined;
170
134
  }
171
-
172
135
  function directCall(
173
136
  ordinal: number,
174
137
  payload: Record<string, unknown>,
175
138
  ): NativeCall | null {
176
139
  if (payload.type !== "function_call" || typeof payload.name !== "string")
177
140
  return null;
178
- if (
179
- (payload.namespace === "functions" || payload.namespace === undefined) &&
180
- GOAL_CONTROLS.has(payload.name)
181
- )
182
- return {
183
- ordinal,
184
- namespace: "functions",
185
- name: payload.name,
186
- evidence: "invocation_attempt",
187
- };
188
- if (
189
- payload.namespace === "collaboration" &&
190
- COLLABORATION_CONTROLS.has(payload.name)
191
- )
192
- return {
193
- ordinal,
194
- namespace: "collaboration",
195
- name: payload.name,
196
- evidence: "invocation_attempt",
197
- };
198
- return null;
141
+ const namespace = nativeControlNamespace({ ...payload, name: payload.name });
142
+ return namespace
143
+ ? { ordinal, namespace, name: payload.name, evidence: "invocation_attempt" }
144
+ : null;
199
145
  }
200
146
 
201
147
  const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
@@ -208,144 +154,191 @@ function bounded(value: unknown, pattern: RegExp): string | undefined {
208
154
  : undefined;
209
155
  }
210
156
 
157
+ function toolNamespace(value: unknown): NativeToolCall["namespace"] {
158
+ return value === "functions" || value === "collaboration" || value === "clock"
159
+ ? value
160
+ : "other";
161
+ }
162
+ function parsedObject(text: unknown): Record<string, unknown> | null {
163
+ if (typeof text !== "string") return null;
164
+ try {
165
+ const value: unknown = JSON.parse(text);
166
+ return isRecord(value) ? value : null;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+ function feedbackTarget(
172
+ args: Record<string, unknown> | null,
173
+ ): string | undefined {
174
+ if (!args) return undefined;
175
+ return (
176
+ bounded(args.target, AGENT_REF) ??
177
+ bounded(args.target, /^[a-z0-9][a-z0-9_]{0,63}$/)
178
+ );
179
+ }
180
+ function nativeToolTarget(
181
+ payload: Record<string, unknown>,
182
+ namespace: NativeToolCall["namespace"],
183
+ name: string,
184
+ ): string | undefined {
185
+ if (
186
+ namespace !== "collaboration" ||
187
+ !["followup_task", "send_message", "interrupt_agent"].includes(name)
188
+ )
189
+ return undefined;
190
+ return feedbackTarget(parsedObject(payload.arguments));
191
+ }
192
+ function toolCallType(value: unknown): boolean {
193
+ return value === "function_call" || value === "custom_tool_call";
194
+ }
211
195
  function nativeToolCall(
212
196
  ordinal: number,
213
197
  payload: Record<string, unknown>,
214
198
  ): NativeToolCall | null {
215
- if (payload.type !== "function_call" && payload.type !== "custom_tool_call")
216
- return null;
217
- const namespace =
218
- payload.namespace === "functions" ||
219
- payload.namespace === "collaboration" ||
220
- payload.namespace === "clock"
221
- ? payload.namespace
222
- : "other";
199
+ if (!toolCallType(payload.type)) return null;
200
+ const namespace = toolNamespace(payload.namespace);
223
201
  const name =
224
202
  bounded(payload.name, /^[A-Za-z_][A-Za-z0-9_]{0,63}$/) ?? "other";
225
- let target: string | undefined;
226
- if (
227
- namespace === "collaboration" &&
228
- ["followup_task", "send_message", "interrupt_agent"].includes(name) &&
229
- typeof payload.arguments === "string"
230
- ) {
231
- try {
232
- const args: unknown = JSON.parse(payload.arguments);
233
- if (record(args))
234
- target =
235
- bounded(args.target, AGENT_REF) ??
236
- bounded(args.target, /^[a-z0-9][a-z0-9_]{0,63}$/);
237
- } catch {
238
- // An unreadable target stays unknown without exposing arguments.
239
- }
240
- }
203
+ const target = nativeToolTarget(payload, namespace, name);
241
204
  return { ordinal, namespace, name, ...(target ? { target } : {}) };
242
205
  }
243
206
 
244
- function acceptedSpawns(
245
- entries: Array<{ ordinal: number; payload: Record<string, unknown> }>,
246
- ): AcceptedSpawn[] {
247
- const requests = entries.filter(
248
- ({ payload }) =>
249
- payload.type === "function_call" &&
250
- payload.namespace === "collaboration" &&
251
- payload.name === "spawn_agent",
207
+ type NativeEntry = { ordinal: number; payload: Record<string, unknown> };
208
+ function spawnRequest(payload: Record<string, unknown>): boolean {
209
+ return (
210
+ payload.type === "function_call" &&
211
+ payload.namespace === "collaboration" &&
212
+ payload.name === "spawn_agent"
252
213
  );
214
+ }
215
+ function acceptedSpawns(entries: NativeEntry[]): AcceptedSpawn[] {
216
+ const requests = entries.filter(({ payload }) => spawnRequest(payload));
253
217
  const accepted: AcceptedSpawn[] = [];
254
218
  for (const request of requests) {
255
- const callId = bounded(request.payload.call_id, IDENTIFIER);
256
- if (
257
- !callId ||
258
- requests.filter(({ payload }) => payload.call_id === callId).length !==
259
- 1 ||
260
- typeof request.payload.arguments !== "string"
261
- )
262
- continue;
263
- let args: unknown;
264
- try {
265
- args = JSON.parse(request.payload.arguments);
266
- } catch {
267
- continue;
268
- }
269
- if (!record(args)) continue;
270
- const starts = entries.filter(({ payload }) => {
271
- const item = record(payload.item) ? payload.item : undefined;
272
- return (
273
- payload.type === "item_completed" &&
274
- item?.type === "SubAgentActivity" &&
275
- item.id === callId &&
276
- item.kind === "started"
277
- );
278
- });
279
- const outputs = entries.filter(
280
- ({ payload }) =>
281
- payload.type === "function_call_output" && payload.call_id === callId,
282
- );
283
- if (starts.length !== 1 || outputs.length !== 1) continue;
284
- const start = starts[0]!;
285
- const result = outputs[0]!;
286
- const item = start.payload.item as Record<string, unknown>;
287
- const agentRef = bounded(item.agent_path, AGENT_REF);
288
- const threadId = bounded(item.agent_thread_id, IDENTIFIER);
289
- if (
290
- !agentRef ||
291
- !threadId ||
292
- !(request.ordinal < start.ordinal && start.ordinal < result.ordinal) ||
293
- typeof result.payload.output !== "string"
294
- )
295
- continue;
296
- let output: unknown;
297
- try {
298
- output = JSON.parse(result.payload.output);
299
- } catch {
300
- continue;
301
- }
302
- if (!record(output) || output.task_name !== agentRef) continue;
303
- accepted.push({
304
- callId,
305
- agentRef,
306
- threadId,
307
- requestedOrdinal: request.ordinal,
308
- startedOrdinal: start.ordinal,
309
- acceptedOrdinal: result.ordinal,
310
- ...(bounded(args.task_name, /^[a-z0-9][a-z0-9_]{0,63}$/)
311
- ? { taskName: args.task_name as string }
312
- : {}),
313
- ...(bounded(args.model, IDENTIFIER)
314
- ? { model: args.model as string }
315
- : {}),
316
- ...(bounded(
317
- args.reasoning_effort,
318
- /^(?:low|medium|high|xhigh|max|ultra)$/,
319
- )
320
- ? { reasoningEffort: args.reasoning_effort as string }
321
- : {}),
322
- ...(bounded(args.fork_turns, /^(?:none|all|[1-9][0-9]*)$/)
323
- ? { forkTurns: args.fork_turns as string }
324
- : {}),
325
- });
219
+ const spawn = acceptedSpawn(request, requests, entries);
220
+ if (spawn) accepted.push(spawn);
326
221
  }
327
222
  return accepted;
328
223
  }
224
+ function spawnArguments(request: NativeEntry, requests: NativeEntry[]) {
225
+ const callId = bounded(request.payload.call_id, IDENTIFIER);
226
+ if (
227
+ !callId ||
228
+ requests.filter(({ payload }) => payload.call_id === callId).length !== 1
229
+ )
230
+ return null;
231
+ const args = parsedObject(request.payload.arguments);
232
+ return args ? { callId, args } : null;
233
+ }
234
+ function startedSpawn(
235
+ payload: Record<string, unknown>,
236
+ callId: string,
237
+ ): boolean {
238
+ const item = isRecord(payload.item) ? payload.item : {};
239
+ return (
240
+ payload.type === "item_completed" &&
241
+ item.type === "SubAgentActivity" &&
242
+ item.id === callId &&
243
+ item.kind === "started"
244
+ );
245
+ }
246
+ function spawnBoundaries(entries: NativeEntry[], callId: string) {
247
+ const starts = entries.filter(({ payload }) => startedSpawn(payload, callId));
248
+ const outputs = entries.filter(
249
+ ({ payload }) =>
250
+ payload.type === "function_call_output" && payload.call_id === callId,
251
+ );
252
+ const [start] = starts,
253
+ [result] = outputs;
254
+ if (
255
+ starts.length !== 1 ||
256
+ outputs.length !== 1 ||
257
+ start === undefined ||
258
+ result === undefined
259
+ )
260
+ return null;
261
+ return { start, result };
262
+ }
263
+ function spawnIdentity(
264
+ request: NativeEntry,
265
+ start: NativeEntry,
266
+ result: NativeEntry,
267
+ ) {
268
+ const item = isRecord(start.payload.item) ? start.payload.item : {};
269
+ const agentRef = bounded(item.agent_path, AGENT_REF),
270
+ threadId = bounded(item.agent_thread_id, IDENTIFIER);
271
+ if (!agentRef || !threadId || !spawnOrdered(request, start, result))
272
+ return null;
273
+ return { agentRef, threadId };
274
+ }
275
+ function spawnOrdered(
276
+ request: NativeEntry,
277
+ start: NativeEntry,
278
+ result: NativeEntry,
279
+ ): boolean {
280
+ return request.ordinal < start.ordinal && start.ordinal < result.ordinal;
281
+ }
282
+ function acceptedSpawn(
283
+ request: NativeEntry,
284
+ requests: NativeEntry[],
285
+ entries: NativeEntry[],
286
+ ): AcceptedSpawn | null {
287
+ const declaration = spawnArguments(request, requests);
288
+ if (!declaration) return null;
289
+ const boundaries = spawnBoundaries(entries, declaration.callId);
290
+ if (!boundaries) return null;
291
+ return correlatedSpawn(request, declaration, boundaries);
292
+ }
293
+ function correlatedSpawn(
294
+ request: NativeEntry,
295
+ declaration: { callId: string; args: Record<string, unknown> },
296
+ boundaries: { start: NativeEntry; result: NativeEntry },
297
+ ): AcceptedSpawn | null {
298
+ const { start, result } = boundaries;
299
+ const identity = spawnIdentity(request, start, result);
300
+ if (!identity) return null;
301
+ const output = parsedObject(result.payload.output);
302
+ if (!output || output.task_name !== identity.agentRef) return null;
303
+ return {
304
+ callId: declaration.callId,
305
+ ...identity,
306
+ requestedOrdinal: request.ordinal,
307
+ startedOrdinal: start.ordinal,
308
+ acceptedOrdinal: result.ordinal,
309
+ ...requestFields(declaration.args),
310
+ };
311
+ }
312
+ function requestFields(args: Record<string, unknown>) {
313
+ const taskName = bounded(args.task_name, /^[a-z0-9][a-z0-9_]{0,63}$/);
314
+ const model = bounded(args.model, IDENTIFIER);
315
+ const reasoningEffort = bounded(
316
+ args.reasoning_effort,
317
+ /^(?:low|medium|high|xhigh|max|ultra)$/,
318
+ );
319
+ const forkTurns = bounded(args.fork_turns, /^(?:none|all|[1-9][0-9]*)$/);
320
+ return {
321
+ ...(taskName ? { taskName } : {}),
322
+ ...(model ? { model } : {}),
323
+ ...(reasoningEffort ? { reasoningEffort } : {}),
324
+ ...(forkTurns ? { forkTurns } : {}),
325
+ };
326
+ }
329
327
 
328
+ function feedbackRepresentation(
329
+ message: unknown,
330
+ ): NativeFeedbackCall["messageRepresentation"] {
331
+ if (typeof message !== "string") return "unavailable";
332
+ return /^gAAAAA[A-Za-z0-9_-]+={0,2}$/.test(message)
333
+ ? "encrypted"
334
+ : "plaintext";
335
+ }
330
336
  function feedbackMessageEvidence(
331
337
  payload: Record<string, unknown>,
332
338
  followUpPrompt?: string,
333
339
  ) {
334
- let message: unknown;
335
- if (typeof payload.arguments === "string") {
336
- try {
337
- const args: unknown = JSON.parse(payload.arguments);
338
- if (record(args)) message = args.message;
339
- } catch {
340
- // An unreadable message remains unavailable.
341
- }
342
- }
343
- const messageRepresentation =
344
- typeof message !== "string"
345
- ? ("unavailable" as const)
346
- : /^gAAAAA[A-Za-z0-9_-]+={0,2}$/.test(message)
347
- ? ("encrypted" as const)
348
- : ("plaintext" as const);
340
+ const message = parsedObject(payload.arguments)?.message;
341
+ const messageRepresentation = feedbackRepresentation(message);
349
342
  return {
350
343
  messageRepresentation,
351
344
  messageMatchesFollowUpPrompt:
@@ -355,190 +348,278 @@ function feedbackMessageEvidence(
355
348
  };
356
349
  }
357
350
 
351
+ function feedbackPayload(
352
+ payload: Record<string, unknown>,
353
+ ): payload is Record<string, unknown> & { name: NativeFeedbackCall["tool"] } {
354
+ return (
355
+ payload.type === "function_call" &&
356
+ payload.namespace === "collaboration" &&
357
+ typeof payload.name === "string" &&
358
+ ["followup_task", "send_message", "interrupt_agent"].includes(payload.name)
359
+ );
360
+ }
361
+ function feedbackResponses(
362
+ entries: NativeEntry[],
363
+ callId: string | undefined,
364
+ ordinal: number,
365
+ ) {
366
+ return callId
367
+ ? entries.filter(
368
+ (entry) =>
369
+ entry.ordinal > ordinal &&
370
+ entry.payload.type === "function_call_output" &&
371
+ entry.payload.call_id === callId,
372
+ )
373
+ : [];
374
+ }
375
+ function responseObserved(outputs: NativeEntry[]): boolean {
376
+ return outputs.length === 1 && typeof outputs[0]?.payload.output === "string";
377
+ }
358
378
  function nativeFeedbackCalls(
359
- entries: Array<{ ordinal: number; payload: Record<string, unknown> }>,
379
+ entries: NativeEntry[],
360
380
  followUpPrompt?: string,
361
381
  ): NativeFeedbackCall[] {
362
- return entries.flatMap(({ ordinal, payload }) => {
363
- if (
364
- payload.type !== "function_call" ||
365
- payload.namespace !== "collaboration" ||
366
- !["followup_task", "send_message", "interrupt_agent"].includes(
367
- String(payload.name),
368
- )
369
- )
370
- return [];
371
- const callId = bounded(payload.call_id, IDENTIFIER);
372
- const outputs = callId
373
- ? entries.filter(
374
- (entry) =>
375
- entry.ordinal > ordinal &&
376
- entry.payload.type === "function_call_output" &&
377
- entry.payload.call_id === callId,
378
- )
379
- : [];
380
- return [
381
- {
382
- ordinal,
383
- tool: payload.name as NativeFeedbackCall["tool"],
384
- target: nativeToolCall(ordinal, payload)?.target ?? null,
385
- responseObserved:
386
- outputs.length === 1 &&
387
- typeof outputs[0]?.payload.output === "string",
388
- ...feedbackMessageEvidence(payload, followUpPrompt),
389
- },
390
- ];
391
- });
382
+ return entries.flatMap((entry) =>
383
+ feedbackCall(entry, entries, followUpPrompt),
384
+ );
385
+ }
386
+ function feedbackCall(
387
+ { ordinal, payload }: NativeEntry,
388
+ entries: NativeEntry[],
389
+ followUpPrompt: string | undefined,
390
+ ): NativeFeedbackCall[] {
391
+ if (!feedbackPayload(payload)) return [];
392
+ const outputs = feedbackResponses(
393
+ entries,
394
+ bounded(payload.call_id, IDENTIFIER),
395
+ ordinal,
396
+ );
397
+ return [
398
+ {
399
+ ordinal,
400
+ tool: payload.name,
401
+ target: nativeToolCall(ordinal, payload)?.target ?? null,
402
+ responseObserved: responseObserved(outputs),
403
+ ...feedbackMessageEvidence(payload, followUpPrompt),
404
+ },
405
+ ];
392
406
  }
393
407
 
408
+ interface ParseState {
409
+ calls: NativeCall[];
410
+ toolCalls: NativeToolCall[];
411
+ entries: NativeEntry[];
412
+ submittedExecCalls: number;
413
+ lastOrdinal: number;
414
+ malformed: boolean;
415
+ entryCount: number;
416
+ }
394
417
  function parseSession(
395
418
  text: string,
396
419
  followUpPrompt?: string,
397
- ): {
398
- observation: NativeCallObservation;
399
- entries: Array<{ ordinal: number; payload: Record<string, unknown> }>;
400
- } {
401
- const calls: NativeCall[] = [];
402
- const toolCalls: NativeToolCall[] = [];
403
- const entries: Array<{ ordinal: number; payload: Record<string, unknown> }> =
404
- [];
405
- let submittedExecCalls = 0;
406
- let lastOrdinal = -1;
407
- let malformed = false;
408
- let entryCount = 0;
420
+ ): { observation: NativeCallObservation; entries: NativeEntry[] } {
421
+ const state: ParseState = {
422
+ calls: [],
423
+ toolCalls: [],
424
+ entries: [],
425
+ submittedExecCalls: 0,
426
+ lastOrdinal: -1,
427
+ malformed: false,
428
+ entryCount: 0,
429
+ };
409
430
  for (const line of text.split("\n")) {
410
431
  if (!line.trim()) continue;
411
- entryCount++;
412
- if (entryCount > MAX_SESSION_ENTRIES) {
413
- malformed = true;
414
- break;
415
- }
416
- let entry: unknown;
417
- try {
418
- entry = JSON.parse(line);
419
- } catch {
420
- malformed = true;
421
- continue;
422
- }
423
- if (
424
- !record(entry) ||
425
- !Number.isSafeInteger(entry.ordinal) ||
426
- (entry.ordinal as number) <= lastOrdinal ||
427
- !record(entry.payload)
428
- ) {
429
- malformed = true;
430
- continue;
431
- }
432
- lastOrdinal = entry.ordinal as number;
433
- entries.push({ ordinal: lastOrdinal, payload: entry.payload });
434
- if (
435
- entry.payload.type === "custom_tool_call" &&
436
- entry.payload.name === "exec"
437
- )
438
- submittedExecCalls++;
439
- const call = directCall(lastOrdinal, entry.payload);
440
- if (call) calls.push(call);
441
- const toolCall = nativeToolCall(lastOrdinal, entry.payload);
442
- if (toolCall) toolCalls.push(toolCall);
443
- if (
444
- calls.length > MAX_RETAINED_CALLS ||
445
- toolCalls.length > MAX_RETAINED_CALLS
446
- ) {
447
- calls.length = MAX_RETAINED_CALLS;
448
- toolCalls.length = MAX_RETAINED_CALLS;
449
- malformed = true;
450
- break;
451
- }
432
+ if (!consumeSessionLine(line, state)) break;
452
433
  }
453
434
  return {
454
- entries,
455
- observation: {
456
- id: "sevro.codex.native-calls",
457
- completeness: malformed || entryCount === 0 ? "partial" : "complete",
458
- data: {
459
- method: "native_session",
460
- calls,
461
- toolCalls,
462
- submittedExecCalls,
463
- acceptedSpawns:
464
- malformed || entries.length === 0 ? [] : acceptedSpawns(entries),
465
- feedbackCalls:
466
- malformed || entries.length === 0
467
- ? []
468
- : nativeFeedbackCalls(entries, followUpPrompt),
469
- childSessions: [],
470
- childrenTruncated: false,
471
- },
435
+ entries: state.entries,
436
+ observation: sessionObservation(state, followUpPrompt),
437
+ };
438
+ }
439
+ function consumeSessionLine(line: string, state: ParseState): boolean {
440
+ state.entryCount++;
441
+ if (state.entryCount > MAX_SESSION_ENTRIES) {
442
+ state.malformed = true;
443
+ return false;
444
+ }
445
+ const entry = parsedSessionEntry(line, state);
446
+ if (!entry) return true;
447
+ state.lastOrdinal = entry.ordinal;
448
+ state.entries.push(entry);
449
+ retainSessionCalls(entry, state);
450
+ return !truncateSessionCalls(state);
451
+ }
452
+ function sessionOrdinal(
453
+ entry: Record<string, unknown>,
454
+ lastOrdinal: number,
455
+ ): entry is Record<string, unknown> & { ordinal: number } {
456
+ return (
457
+ typeof entry.ordinal === "number" &&
458
+ Number.isSafeInteger(entry.ordinal) &&
459
+ entry.ordinal > lastOrdinal
460
+ );
461
+ }
462
+ function parsedSessionEntry(
463
+ line: string,
464
+ state: ParseState,
465
+ ): NativeEntry | null {
466
+ let entry: unknown;
467
+ try {
468
+ entry = JSON.parse(line);
469
+ } catch {
470
+ state.malformed = true;
471
+ return null;
472
+ }
473
+ if (
474
+ !isRecord(entry) ||
475
+ !sessionOrdinal(entry, state.lastOrdinal) ||
476
+ !isRecord(entry.payload)
477
+ ) {
478
+ state.malformed = true;
479
+ return null;
480
+ }
481
+ return { ordinal: entry.ordinal, payload: entry.payload };
482
+ }
483
+ function retainSessionCalls(entry: NativeEntry, state: ParseState): void {
484
+ if (
485
+ entry.payload.type === "custom_tool_call" &&
486
+ entry.payload.name === "exec"
487
+ )
488
+ state.submittedExecCalls++;
489
+ const call = directCall(entry.ordinal, entry.payload);
490
+ if (call) state.calls.push(call);
491
+ const tool = nativeToolCall(entry.ordinal, entry.payload);
492
+ if (tool) state.toolCalls.push(tool);
493
+ }
494
+ function truncateSessionCalls(state: ParseState): boolean {
495
+ if (
496
+ state.calls.length <= MAX_RETAINED_CALLS &&
497
+ state.toolCalls.length <= MAX_RETAINED_CALLS
498
+ )
499
+ return false;
500
+ state.calls.length = MAX_RETAINED_CALLS;
501
+ state.toolCalls.length = MAX_RETAINED_CALLS;
502
+ state.malformed = true;
503
+ return true;
504
+ }
505
+ function sessionReceipts(
506
+ state: ParseState,
507
+ followUpPrompt: string | undefined,
508
+ ) {
509
+ if (state.malformed || state.entries.length === 0)
510
+ return { acceptedSpawns: [], feedbackCalls: [] };
511
+ return {
512
+ acceptedSpawns: acceptedSpawns(state.entries),
513
+ feedbackCalls: nativeFeedbackCalls(state.entries, followUpPrompt),
514
+ };
515
+ }
516
+ function sessionObservation(
517
+ state: ParseState,
518
+ followUpPrompt: string | undefined,
519
+ ): NativeCallObservation {
520
+ return {
521
+ id: "sevro.codex.native-calls",
522
+ completeness:
523
+ state.malformed || state.entryCount === 0 ? "partial" : "complete",
524
+ data: {
525
+ method: "native_session",
526
+ calls: state.calls,
527
+ toolCalls: state.toolCalls,
528
+ submittedExecCalls: state.submittedExecCalls,
529
+ ...(state.submittedExecCalls > 0
530
+ ? { execCalls: codexExecCallReceipts(state.entries) }
531
+ : {}),
532
+ ...sessionReceipts(state, followUpPrompt),
533
+ childSessions: [],
534
+ childrenTruncated: false,
472
535
  },
473
536
  };
474
537
  }
475
538
 
476
539
  /** Bind a private final answer to the same turn's later native completion. */
477
- function childResultCompleted(
478
- entries: Array<{ ordinal: number; payload: Record<string, unknown> }>,
479
- ): boolean {
480
- const finals = entries.filter(({ payload }) => {
481
- const item = record(payload.item) ? payload.item : null;
482
- return (
483
- payload.type === "item_completed" &&
484
- item?.type === "AgentMessage" &&
485
- item.phase === "final_answer"
486
- );
487
- });
540
+
541
+ function finalAgentMessage(payload: Record<string, unknown>): boolean {
542
+ const item = isRecord(payload.item) ? payload.item : {};
543
+ return (
544
+ payload.type === "item_completed" &&
545
+ item.type === "AgentMessage" &&
546
+ item.phase === "final_answer"
547
+ );
548
+ }
549
+ function childResultCompleted(entries: NativeEntry[]): boolean {
550
+ const finals = entries.filter(({ payload }) => finalAgentMessage(payload));
488
551
  const completions = entries.filter(
489
552
  ({ payload }) => payload.type === "task_complete",
490
553
  );
491
- if (
492
- finals.length !== 1 ||
493
- completions.length !== 1 ||
494
- entries.some(
554
+ const boundaries = childResultBoundaries(finals, completions, entries);
555
+ return (
556
+ boundaries !== null &&
557
+ childFinalMatched(boundaries.final, boundaries.complete)
558
+ );
559
+ }
560
+ function childResultBoundaries(
561
+ finals: NativeEntry[],
562
+ completions: NativeEntry[],
563
+ entries: NativeEntry[],
564
+ ) {
565
+ if (!uniqueChildCompletion(finals, completions, entries)) return null;
566
+ const [final] = finals,
567
+ [complete] = completions;
568
+ return final && complete ? { final, complete } : null;
569
+ }
570
+
571
+ function uniqueChildCompletion(
572
+ finals: NativeEntry[],
573
+ completions: NativeEntry[],
574
+ entries: NativeEntry[],
575
+ ): boolean {
576
+ return (
577
+ finals.length === 1 &&
578
+ completions.length === 1 &&
579
+ !entries.some(
495
580
  ({ payload }) =>
496
581
  payload.type === "turn_aborted" || payload.type === "turn_failed",
497
582
  )
498
- )
499
- return false;
500
- const final = finals[0]!;
501
- const complete = completions[0]!;
502
- const item = final.payload.item as Record<string, unknown>;
503
- const content = item.content;
583
+ );
584
+ }
585
+ function childFinalMatched(final: NativeEntry, complete: NativeEntry): boolean {
586
+ if (!childCompletionOrdered(final, complete)) return false;
587
+ const item = isRecord(final.payload.item) ? final.payload.item : {};
588
+ const message = childMessage(item.content);
589
+ return (
590
+ message !== null &&
591
+ message.trim().length > 0 &&
592
+ complete.payload.last_agent_message === message
593
+ );
594
+ }
595
+ function childCompletionOrdered(
596
+ final: NativeEntry,
597
+ complete: NativeEntry,
598
+ ): boolean {
599
+ return (
600
+ !!bounded(final.payload.turn_id, IDENTIFIER) &&
601
+ complete.payload.turn_id === final.payload.turn_id &&
602
+ final.ordinal < complete.ordinal
603
+ );
604
+ }
605
+ function childTextBlock(part: unknown): part is { type: "Text"; text: string } {
606
+ return (
607
+ isRecord(part) && part.type === "Text" && typeof part.text === "string"
608
+ );
609
+ }
610
+ function childMessage(content: unknown): string | null {
504
611
  if (
505
- !bounded(final.payload.turn_id, IDENTIFIER) ||
506
- complete.payload.turn_id !== final.payload.turn_id ||
507
- final.ordinal >= complete.ordinal ||
508
- !Array.isArray(content) ||
612
+ !isUnknownArray(content) ||
509
613
  content.length === 0 ||
510
- !content.every(
511
- (part) =>
512
- record(part) && part.type === "Text" && typeof part.text === "string",
513
- )
614
+ !content.every(childTextBlock)
514
615
  )
515
- return false;
516
- const message = content.map((part) => part.text as string).join("");
517
- return (
518
- message.trim().length > 0 && complete.payload.last_agent_message === message
519
- );
616
+ return null;
617
+ return content.map((part) => part.text).join("");
520
618
  }
521
619
 
522
620
  function nestedRequestFields(payload: Record<string, unknown>) {
523
- let args: unknown;
524
- try {
525
- args = JSON.parse(payload.arguments as string);
526
- } catch {
527
- return {};
528
- }
529
- if (!record(args)) return {};
530
- return {
531
- ...(bounded(args.task_name, /^[a-z0-9][a-z0-9_]{0,63}$/)
532
- ? { taskName: args.task_name as string }
533
- : {}),
534
- ...(bounded(args.model, IDENTIFIER) ? { model: args.model as string } : {}),
535
- ...(bounded(args.reasoning_effort, /^(?:low|medium|high|xhigh|max|ultra)$/)
536
- ? { reasoningEffort: args.reasoning_effort as string }
537
- : {}),
538
- ...(bounded(args.fork_turns, /^(?:none|all|[1-9][0-9]*)$/)
539
- ? { forkTurns: args.fork_turns as string }
540
- : {}),
541
- };
621
+ const args = parsedObject(payload.arguments);
622
+ return args ? requestFields(args) : {};
542
623
  }
543
624
 
544
625
  async function nestedSpawnReceipts(
@@ -555,26 +636,11 @@ async function nestedSpawnReceipts(
555
636
  );
556
637
  const accepted = acceptedSpawns(entries);
557
638
  const nestedSpawns: NestedSpawn[] = await Promise.all(
558
- requests.slice(0, MAX_CHILD_SESSIONS).map(async ({ ordinal, payload }) => {
559
- const receipt = accepted.find(
560
- (spawn) => spawn.requestedOrdinal === ordinal,
561
- );
562
- const session: ChildSession | null = receipt
563
- ? receipt.threadId === rootThread
564
- ? { threadId: receipt.threadId, status: "partial" }
565
- : await childSessionStatus(home, parentThread, receipt.threadId)
566
- : null;
567
- return {
568
- requestedOrdinal: ordinal,
569
- status: receipt ? ("accepted" as const) : ("unaccepted" as const),
570
- ...nestedRequestFields(payload),
571
- ...(receipt
572
- ? { agentRef: receipt.agentRef, threadId: receipt.threadId }
573
- : {}),
574
- sessionStatus: session?.status ?? "unavailable",
575
- readerResultStatus: session?.resultStatus ?? "unavailable",
576
- };
577
- }),
639
+ requests
640
+ .slice(0, MAX_CHILD_SESSIONS)
641
+ .map((request) =>
642
+ nestedSpawnReceipt(home, rootThread, parentThread, accepted, request),
643
+ ),
578
644
  );
579
645
  return {
580
646
  nestedSpawns,
@@ -582,51 +648,88 @@ async function nestedSpawnReceipts(
582
648
  };
583
649
  }
584
650
 
651
+ async function nestedSpawnReceipt(
652
+ home: string,
653
+ rootThread: string,
654
+ parentThread: string,
655
+ accepted: AcceptedSpawn[],
656
+ { ordinal, payload }: NativeEntry,
657
+ ): Promise<NestedSpawn> {
658
+ const receipt = accepted.find((spawn) => spawn.requestedOrdinal === ordinal);
659
+ const session = await nestedReaderSession(
660
+ home,
661
+ rootThread,
662
+ parentThread,
663
+ receipt,
664
+ );
665
+ return {
666
+ requestedOrdinal: ordinal,
667
+ status: receipt ? "accepted" : "unaccepted",
668
+ ...nestedRequestFields(payload),
669
+ ...(receipt
670
+ ? { agentRef: receipt.agentRef, threadId: receipt.threadId }
671
+ : {}),
672
+ ...nestedReaderStatus(session),
673
+ };
674
+ }
675
+ function nestedReaderStatus(session: ChildSession | null) {
676
+ return {
677
+ sessionStatus: session?.status ?? "unavailable",
678
+ readerResultStatus: session?.resultStatus ?? "unavailable",
679
+ };
680
+ }
681
+ async function nestedReaderSession(
682
+ home: string,
683
+ rootThread: string,
684
+ parentThread: string,
685
+ receipt: AcceptedSpawn | undefined,
686
+ ): Promise<ChildSession | null> {
687
+ if (!receipt) return null;
688
+ if (receipt.threadId === rootThread)
689
+ return { threadId: receipt.threadId, status: "partial" };
690
+ return childSessionStatus(home, parentThread, receipt.threadId);
691
+ }
692
+
585
693
  async function childSessionStatus(
586
694
  home: string,
587
695
  parentThread: string,
588
696
  threadId: string,
589
- skillContext?: NativeSkillContext,
590
697
  rootThread?: string,
591
698
  ): Promise<ChildSession> {
592
699
  if (threadId === parentThread) return { threadId, status: "partial" };
593
700
  const located = await sessionPath(home, threadId);
594
701
  if (located.status !== "found") return { threadId, status: located.status };
595
702
  try {
596
- const size = (await stat(located.path)).size;
597
- if (size > MAX_SESSION_BYTES) return { threadId, status: "partial" };
598
- const bytes = await readFile(located.path);
599
- if (bytes.byteLength > MAX_SESSION_BYTES)
600
- return { threadId, status: "partial" };
601
- const parsed = parseSession(
602
- new TextDecoder("utf-8", { fatal: true }).decode(bytes),
603
- );
604
- if (parsed.observation.completeness !== "complete")
605
- return { threadId, status: "partial" };
606
- const readDiagnostics = skillContext
607
- ? await codexNativeReadDiagnostic(
608
- parsed.entries,
609
- skillContext.workspace,
610
- skillContext.installedPluginRoots,
611
- )
612
- : undefined;
613
- const nested = rootThread
614
- ? await nestedSpawnReceipts(home, rootThread, threadId, parsed.entries)
615
- : undefined;
616
- return {
617
- threadId,
618
- status: "available",
619
- resultStatus: childResultCompleted(parsed.entries)
620
- ? "completed"
621
- : "unavailable",
622
- ...(readDiagnostics ? { readDiagnostics } : {}),
623
- ...nested,
624
- };
703
+ const text = await readNativeSession(located.path);
704
+ if (text === null) return { threadId, status: "partial" };
705
+ return await availableChildSession(home, threadId, text, rootThread);
625
706
  } catch {
626
707
  return { threadId, status: "partial" };
627
708
  }
628
709
  }
629
710
 
711
+ async function availableChildSession(
712
+ home: string,
713
+ threadId: string,
714
+ text: string,
715
+ rootThread: string | undefined,
716
+ ): Promise<ChildSession> {
717
+ const parsed = parseSession(text);
718
+ if (parsed.observation.completeness !== "complete")
719
+ return { threadId, status: "partial" };
720
+ const nested = rootThread
721
+ ? await nestedSpawnReceipts(home, rootThread, threadId, parsed.entries)
722
+ : undefined;
723
+ return {
724
+ threadId,
725
+ status: "available",
726
+ resultStatus: childResultCompleted(parsed.entries)
727
+ ? "completed"
728
+ : "unavailable",
729
+ ...nested,
730
+ };
731
+ }
732
+
630
733
  /** Last validated native event before a follow-up prompt enters this thread. */
631
734
  export async function codexNativeSessionLastOrdinal(
632
735
  home: string,
@@ -635,24 +738,24 @@ export async function codexNativeSessionLastOrdinal(
635
738
  const located = await sessionPath(home, threadId);
636
739
  if (located.status !== "found") return null;
637
740
  try {
638
- if ((await stat(located.path)).size > MAX_SESSION_BYTES) return null;
639
- const bytes = await readFile(located.path);
640
- if (bytes.byteLength > MAX_SESSION_BYTES) return null;
641
- const parsed = parseSession(
642
- new TextDecoder("utf-8", { fatal: true }).decode(bytes),
643
- );
644
- if (parsed.observation.completeness !== "complete") return null;
645
- return parsed.entries.at(-1)?.ordinal ?? null;
741
+ const text = await readNativeSession(located.path);
742
+ return text === null ? null : lastSessionOrdinal(text);
646
743
  } catch {
647
744
  return null;
648
745
  }
649
746
  }
650
747
 
748
+ function lastSessionOrdinal(text: string): number | null {
749
+ const parsed = parseSession(text);
750
+ if (parsed.observation.completeness !== "complete") return null;
751
+ return parsed.entries.at(-1)?.ordinal ?? null;
752
+ }
753
+
651
754
  /** Bind one private native session to the completed public thread. */
652
755
  export async function codexNativeCallObservation(
653
756
  home: string,
654
757
  threadId: string,
655
- skillContext?: NativeSkillContext,
758
+ context?: NativeCallContext,
656
759
  ): Promise<NativeCallObservation> {
657
760
  const located = await sessionPath(home, threadId);
658
761
  if (located.status !== "found")
@@ -660,98 +763,41 @@ export async function codexNativeCallObservation(
660
763
  located.status === "ambiguous" ? "partial" : located.status,
661
764
  );
662
765
  try {
663
- const size = (await stat(located.path)).size;
664
- if (size > MAX_SESSION_BYTES) return unavailable("partial");
665
- const bytes = await readFile(located.path);
666
- if (bytes.byteLength > MAX_SESSION_BYTES) return unavailable("partial");
667
- const { observation, entries } = parseSession(
668
- new TextDecoder("utf-8", { fatal: true }).decode(bytes),
669
- skillContext?.followUpPrompt,
670
- );
671
- if (observation.completeness !== "complete") return observation;
672
- const parentReadDiagnostics = skillContext
673
- ? await codexNativeReadDiagnostic(
674
- entries,
675
- skillContext.workspace,
676
- skillContext.installedPluginRoots,
677
- )
678
- : undefined;
679
- const children = [
680
- ...new Set(
681
- observation.data.acceptedSpawns.map((spawn) => spawn.threadId),
682
- ),
683
- ];
684
- const childSessions = await Promise.all(
685
- children
686
- .slice(0, MAX_CHILD_SESSIONS)
687
- .map((child) =>
688
- childSessionStatus(home, threadId, child, skillContext, threadId),
689
- ),
766
+ return await nativeSessionObservation(
767
+ home,
768
+ threadId,
769
+ located.path,
770
+ context,
690
771
  );
691
- return {
692
- ...observation,
693
- data: {
694
- ...observation.data,
695
- ...(parentReadDiagnostics ? { parentReadDiagnostics } : {}),
696
- childSessions,
697
- childrenTruncated: children.length > MAX_CHILD_SESSIONS,
698
- },
699
- };
700
772
  } catch {
701
773
  return unavailable("partial");
702
774
  }
703
775
  }
704
776
 
705
- /** Actor-local recovery stays in host memory and never enters public observations. */
706
- export async function codexNativeSkillReadRecovery(
777
+ async function nativeSessionObservation(
707
778
  home: string,
708
779
  threadId: string,
709
- ): Promise<Map<string, { command: string; output: string }>> {
710
- const found = await sessionPath(home, threadId);
711
- if (found.status !== "found") return new Map();
712
- try {
713
- if ((await stat(found.path)).size > MAX_SESSION_BYTES) return new Map();
714
- const text = await readFile(found.path, "utf8");
715
- if (Buffer.byteLength(text) > MAX_SESSION_BYTES) return new Map();
716
- const parsed = parseSession(text);
717
- if (parsed.observation.completeness !== "complete") return new Map();
718
- const output = nativeCommandOutputs(parsed.entries);
719
- const recovered = new Map<string, { command: string; output: string }>();
720
- const counts = new Map<string, number>();
721
- for (const { payload } of parsed.entries) {
722
- const item = record(payload.item) ? payload.item : null;
723
- if (
724
- payload.type === "item_completed" &&
725
- item?.type === "CommandExecution" &&
726
- typeof item.id === "string"
727
- )
728
- counts.set(item.id, (counts.get(item.id) ?? 0) + 1);
729
- }
730
- for (const { ordinal, payload } of parsed.entries) {
731
- const item = record(payload.item) ? payload.item : null;
732
- if (
733
- payload.type !== "item_completed" ||
734
- item?.type !== "CommandExecution" ||
735
- typeof item.id !== "string" ||
736
- counts.get(item.id) !== 1
737
- )
738
- continue;
739
- const recoveredOutput = output.get(ordinal)?.output;
740
- if (
741
- recoveredOutput === undefined ||
742
- !Array.isArray(item.command) ||
743
- item.command.length !== 3 ||
744
- !["-c", "-lc"].includes(String(item.command[1])) ||
745
- typeof item.command[2] !== "string"
746
- )
747
- continue;
748
- recovered.set(item.id, {
749
- command: item.command[2],
750
- output: recoveredOutput,
751
- });
752
- }
753
- return recovered;
754
- } catch {
755
- return new Map();
756
- }
780
+ path: string,
781
+ context: NativeCallContext | undefined,
782
+ ): Promise<NativeCallObservation> {
783
+ const text = await readNativeSession(path);
784
+ if (text === null) return unavailable("partial");
785
+ const { observation } = parseSession(text, context?.followUpPrompt);
786
+ if (observation.completeness !== "complete") return observation;
787
+ const children = [
788
+ ...new Set(observation.data.acceptedSpawns.map((spawn) => spawn.threadId)),
789
+ ];
790
+ const childSessions = await Promise.all(
791
+ children
792
+ .slice(0, MAX_CHILD_SESSIONS)
793
+ .map((child) => childSessionStatus(home, threadId, child, threadId)),
794
+ );
795
+ return {
796
+ ...observation,
797
+ data: {
798
+ ...observation.data,
799
+ childSessions,
800
+ childrenTruncated: children.length > MAX_CHILD_SESSIONS,
801
+ },
802
+ };
757
803
  }