@kodelyth/codex 2026.5.42 → 2026.6.1

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 (138) hide show
  1. package/package.json +16 -1
  2. package/doctor-contract-api.test.ts +0 -44
  3. package/doctor-contract-api.ts +0 -68
  4. package/harness.ts +0 -72
  5. package/index.test.ts +0 -230
  6. package/index.ts +0 -66
  7. package/media-understanding-provider.test.ts +0 -486
  8. package/media-understanding-provider.ts +0 -521
  9. package/prompt-overlay-runtime-contract.test.ts +0 -48
  10. package/prompt-overlay.ts +0 -21
  11. package/provider-catalog.ts +0 -83
  12. package/provider-discovery.ts +0 -45
  13. package/provider.test.ts +0 -384
  14. package/provider.ts +0 -243
  15. package/src/app-server/app-inventory-cache.test.ts +0 -176
  16. package/src/app-server/app-inventory-cache.ts +0 -324
  17. package/src/app-server/approval-bridge.test.ts +0 -1471
  18. package/src/app-server/approval-bridge.ts +0 -1211
  19. package/src/app-server/auth-bridge.test.ts +0 -1449
  20. package/src/app-server/auth-bridge.ts +0 -614
  21. package/src/app-server/auth-profile-runtime-contract.test.ts +0 -239
  22. package/src/app-server/capabilities.ts +0 -27
  23. package/src/app-server/client-factory.ts +0 -24
  24. package/src/app-server/client.test.ts +0 -563
  25. package/src/app-server/client.ts +0 -715
  26. package/src/app-server/compact.test.ts +0 -710
  27. package/src/app-server/compact.ts +0 -500
  28. package/src/app-server/computer-use.test.ts +0 -788
  29. package/src/app-server/computer-use.ts +0 -683
  30. package/src/app-server/config.test.ts +0 -879
  31. package/src/app-server/config.ts +0 -1038
  32. package/src/app-server/context-engine-projection.test.ts +0 -252
  33. package/src/app-server/context-engine-projection.ts +0 -403
  34. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +0 -80
  35. package/src/app-server/dynamic-tool-diagnostics.ts +0 -73
  36. package/src/app-server/dynamic-tool-profile.ts +0 -69
  37. package/src/app-server/dynamic-tools.test.ts +0 -1302
  38. package/src/app-server/dynamic-tools.ts +0 -623
  39. package/src/app-server/elicitation-bridge.test.ts +0 -1056
  40. package/src/app-server/elicitation-bridge.ts +0 -783
  41. package/src/app-server/event-projector.test.ts +0 -2668
  42. package/src/app-server/event-projector.ts +0 -2057
  43. package/src/app-server/image-payload-sanitizer.test.ts +0 -49
  44. package/src/app-server/image-payload-sanitizer.ts +0 -167
  45. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +0 -456
  46. package/src/app-server/local-runtime-attribution.ts +0 -39
  47. package/src/app-server/managed-binary.test.ts +0 -139
  48. package/src/app-server/managed-binary.ts +0 -193
  49. package/src/app-server/models.test.ts +0 -246
  50. package/src/app-server/models.ts +0 -172
  51. package/src/app-server/native-hook-relay.test.ts +0 -271
  52. package/src/app-server/native-hook-relay.ts +0 -150
  53. package/src/app-server/native-subagent-task-mirror.test.ts +0 -573
  54. package/src/app-server/native-subagent-task-mirror.ts +0 -497
  55. package/src/app-server/outcome-fallback-runtime-contract.test.ts +0 -404
  56. package/src/app-server/plugin-activation.test.ts +0 -336
  57. package/src/app-server/plugin-activation.ts +0 -283
  58. package/src/app-server/plugin-app-cache-key.ts +0 -74
  59. package/src/app-server/plugin-approval-roundtrip.ts +0 -122
  60. package/src/app-server/plugin-inventory.test.ts +0 -355
  61. package/src/app-server/plugin-inventory.ts +0 -357
  62. package/src/app-server/plugin-thread-config.test.ts +0 -865
  63. package/src/app-server/plugin-thread-config.ts +0 -455
  64. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +0 -33
  65. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +0 -199
  66. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +0 -102
  67. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +0 -227
  68. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +0 -2630
  69. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +0 -2630
  70. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +0 -1659
  71. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +0 -1655
  72. package/src/app-server/protocol-validators.test.ts +0 -75
  73. package/src/app-server/protocol-validators.ts +0 -203
  74. package/src/app-server/protocol.ts +0 -520
  75. package/src/app-server/rate-limit-cache.ts +0 -48
  76. package/src/app-server/rate-limits.test.ts +0 -202
  77. package/src/app-server/rate-limits.ts +0 -583
  78. package/src/app-server/request.ts +0 -73
  79. package/src/app-server/run-attempt.context-engine.test.ts +0 -1004
  80. package/src/app-server/run-attempt.test.ts +0 -9477
  81. package/src/app-server/run-attempt.ts +0 -4683
  82. package/src/app-server/run-attempt.vision-tools.test.ts +0 -35
  83. package/src/app-server/schema-normalization-runtime-contract.test.ts +0 -206
  84. package/src/app-server/session-binding.test.ts +0 -303
  85. package/src/app-server/session-binding.ts +0 -398
  86. package/src/app-server/session-history.ts +0 -44
  87. package/src/app-server/shared-client.test.ts +0 -589
  88. package/src/app-server/shared-client.ts +0 -289
  89. package/src/app-server/side-question.test.ts +0 -1175
  90. package/src/app-server/side-question.ts +0 -1007
  91. package/src/app-server/test-support.ts +0 -48
  92. package/src/app-server/thread-lifecycle.test.ts +0 -447
  93. package/src/app-server/thread-lifecycle.ts +0 -939
  94. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +0 -442
  95. package/src/app-server/timeout.ts +0 -9
  96. package/src/app-server/tool-progress-normalization.ts +0 -77
  97. package/src/app-server/trajectory.test.ts +0 -205
  98. package/src/app-server/trajectory.ts +0 -365
  99. package/src/app-server/transcript-mirror.test.ts +0 -524
  100. package/src/app-server/transcript-mirror.ts +0 -208
  101. package/src/app-server/transcript-repair-runtime-contract.test.ts +0 -44
  102. package/src/app-server/transport-stdio.test.ts +0 -171
  103. package/src/app-server/transport-stdio.ts +0 -107
  104. package/src/app-server/transport-websocket.test.ts +0 -69
  105. package/src/app-server/transport-websocket.ts +0 -90
  106. package/src/app-server/transport.ts +0 -117
  107. package/src/app-server/user-input-bridge.test.ts +0 -249
  108. package/src/app-server/user-input-bridge.ts +0 -316
  109. package/src/app-server/version.ts +0 -4
  110. package/src/app-server/vision-tools.ts +0 -12
  111. package/src/command-account.ts +0 -544
  112. package/src/command-formatters.ts +0 -425
  113. package/src/command-handlers.ts +0 -2004
  114. package/src/command-rpc.test.ts +0 -16
  115. package/src/command-rpc.ts +0 -142
  116. package/src/commands.test.ts +0 -3312
  117. package/src/commands.ts +0 -65
  118. package/src/conversation-binding-data.ts +0 -124
  119. package/src/conversation-binding.test.ts +0 -599
  120. package/src/conversation-binding.ts +0 -561
  121. package/src/conversation-control.test.ts +0 -126
  122. package/src/conversation-control.ts +0 -303
  123. package/src/conversation-turn-collector.test.ts +0 -191
  124. package/src/conversation-turn-collector.ts +0 -186
  125. package/src/conversation-turn-input.test.ts +0 -141
  126. package/src/conversation-turn-input.ts +0 -106
  127. package/src/manifest.test.ts +0 -20
  128. package/src/migration/apply.ts +0 -501
  129. package/src/migration/helpers.ts +0 -55
  130. package/src/migration/plan.ts +0 -461
  131. package/src/migration/provider.test.ts +0 -1741
  132. package/src/migration/provider.ts +0 -41
  133. package/src/migration/source.ts +0 -643
  134. package/src/migration/targets.ts +0 -25
  135. package/src/node-cli-sessions.test.ts +0 -180
  136. package/src/node-cli-sessions.ts +0 -711
  137. package/test-api.ts +0 -82
  138. package/tsconfig.json +0 -16
@@ -1,1004 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
5
- import { SessionManager } from "@earendil-works/pi-coding-agent";
6
- import type { EmbeddedRunAttemptParams } from "klaw/plugin-sdk/agent-harness";
7
- import {
8
- embeddedAgentLog,
9
- type HarnessContextEngine as ContextEngine,
10
- } from "klaw/plugin-sdk/agent-harness-runtime";
11
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
- import type { CodexAppServerClientFactory } from "./client-factory.js";
13
- import type { CodexServerNotification } from "./protocol.js";
14
- import { runCodexAppServerAttempt as runCodexAppServerAttemptImpl } from "./run-attempt.js";
15
- import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
16
- import { createCodexTestModel } from "./test-support.js";
17
-
18
- let tempDir: string;
19
- let codexAppServerClientFactoryForTest: CodexAppServerClientFactory | undefined;
20
-
21
- type RunCodexAppServerAttemptOptions = NonNullable<
22
- Parameters<typeof runCodexAppServerAttemptImpl>[1]
23
- >;
24
-
25
- function setCodexAppServerClientFactoryForTest(factory: CodexAppServerClientFactory): void {
26
- codexAppServerClientFactoryForTest = factory;
27
- }
28
-
29
- function resetCodexAppServerClientFactoryForTest(): void {
30
- codexAppServerClientFactoryForTest = undefined;
31
- }
32
-
33
- function runCodexAppServerAttempt(
34
- params: EmbeddedRunAttemptParams,
35
- options: RunCodexAppServerAttemptOptions = {},
36
- ) {
37
- const clientFactory = options.clientFactory ?? codexAppServerClientFactoryForTest;
38
- return runCodexAppServerAttemptImpl(
39
- params,
40
- clientFactory ? { ...options, clientFactory } : options,
41
- );
42
- }
43
-
44
- function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAttemptParams {
45
- return {
46
- prompt: "hello",
47
- sessionId: "session-1",
48
- sessionKey: "agent:main:session-1",
49
- sessionFile,
50
- workspaceDir,
51
- runId: "run-1",
52
- provider: "codex",
53
- modelId: "gpt-5.4-codex",
54
- model: createCodexTestModel("codex"),
55
- thinkLevel: "medium",
56
- disableTools: true,
57
- timeoutMs: 5_000,
58
- authStorage: {} as never,
59
- authProfileStore: { version: 1, profiles: {} },
60
- modelRegistry: {} as never,
61
- } as EmbeddedRunAttemptParams;
62
- }
63
-
64
- function assistantMessage(text: string, timestamp: number): AgentMessage {
65
- return {
66
- role: "assistant",
67
- content: [{ type: "text", text }],
68
- api: "openai-codex-responses",
69
- provider: "openai-codex",
70
- model: "gpt-5.4-codex",
71
- usage: {
72
- input: 0,
73
- output: 0,
74
- cacheRead: 0,
75
- cacheWrite: 0,
76
- totalTokens: 0,
77
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
78
- },
79
- stopReason: "stop",
80
- timestamp,
81
- };
82
- }
83
-
84
- function userMessage(text: string, timestamp: number): AgentMessage {
85
- return {
86
- role: "user",
87
- content: [{ type: "text", text }],
88
- timestamp,
89
- } as AgentMessage;
90
- }
91
-
92
- function threadStartResult(threadId = "thread-1") {
93
- return {
94
- thread: {
95
- id: threadId,
96
- sessionId: "session-1",
97
- forkedFromId: null,
98
- preview: "",
99
- ephemeral: false,
100
- modelProvider: "openai",
101
- createdAt: 1,
102
- updatedAt: 1,
103
- status: { type: "idle" },
104
- path: null,
105
- cwd: tempDir || "/tmp/klaw-codex-test",
106
- cliVersion: "0.125.0",
107
- source: "unknown",
108
- agentNickname: null,
109
- agentRole: null,
110
- gitInfo: null,
111
- name: null,
112
- turns: [],
113
- },
114
- model: "gpt-5.4-codex",
115
- modelProvider: "openai",
116
- serviceTier: null,
117
- cwd: tempDir || "/tmp/klaw-codex-test",
118
- instructionSources: [],
119
- approvalPolicy: "never",
120
- approvalsReviewer: "user",
121
- sandbox: { type: "dangerFullAccess" },
122
- permissionProfile: null,
123
- reasoningEffort: null,
124
- };
125
- }
126
-
127
- function turnStartResult(turnId = "turn-1", status = "inProgress") {
128
- return {
129
- turn: {
130
- id: turnId,
131
- status,
132
- items: [],
133
- error: null,
134
- startedAt: null,
135
- completedAt: null,
136
- durationMs: null,
137
- },
138
- };
139
- }
140
-
141
- function createStartedThreadHarness(
142
- requestImpl: (method: string, params: unknown) => Promise<unknown> = async () => undefined,
143
- ) {
144
- const requests: Array<{ method: string; params: unknown }> = [];
145
- let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
146
- const request = vi.fn(async (method: string, params?: unknown) => {
147
- requests.push({ method, params });
148
- const override = await requestImpl(method, params);
149
- if (override !== undefined) {
150
- return override;
151
- }
152
- if (method === "thread/start") {
153
- return threadStartResult();
154
- }
155
- if (method === "turn/start") {
156
- return turnStartResult();
157
- }
158
- return {};
159
- });
160
-
161
- setCodexAppServerClientFactoryForTest(
162
- async () =>
163
- ({
164
- request,
165
- addNotificationHandler: (handler: typeof notify) => {
166
- notify = handler;
167
- return () => undefined;
168
- },
169
- addRequestHandler: () => () => undefined,
170
- }) as never,
171
- );
172
-
173
- return {
174
- requests,
175
- async waitForMethod(method: string) {
176
- await vi.waitFor(() => expect(requests.map((entry) => entry.method)).toContain(method), {
177
- interval: 1,
178
- });
179
- },
180
- async notify(notification: CodexServerNotification) {
181
- await notify(notification);
182
- },
183
- async completeTurn(status: "completed" | "failed" = "completed") {
184
- await notify({
185
- method: "turn/completed",
186
- params: {
187
- threadId: "thread-1",
188
- turnId: "turn-1",
189
- turn: {
190
- id: "turn-1",
191
- status,
192
- ...(status === "failed" ? { error: { message: "codex failed" } } : {}),
193
- items: [{ type: "agentMessage", id: "msg-1", text: "final answer" }],
194
- },
195
- },
196
- });
197
- },
198
- };
199
- }
200
-
201
- function createContextEngine(overrides: Partial<ContextEngine> = {}): ContextEngine {
202
- const engine: ContextEngine = {
203
- info: {
204
- id: "lossless-claw",
205
- name: "Lossless Claw",
206
- ownsCompaction: true,
207
- },
208
- bootstrap: vi.fn(async () => ({ bootstrapped: true })),
209
- assemble: vi.fn(async ({ messages, prompt }) => ({
210
- messages: [...messages, userMessage(prompt ?? "", 10)],
211
- estimatedTokens: 42,
212
- systemPromptAddition: "context-engine system",
213
- })),
214
- ingest: vi.fn(async () => ({ ingested: true })),
215
- maintain: vi.fn(async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 })),
216
- compact: vi.fn(async () => ({
217
- ok: true,
218
- compacted: true,
219
- result: { summary: "summary", firstKeptEntryId: "entry-1", tokensBefore: 10 },
220
- })),
221
- ...overrides,
222
- };
223
- return engine;
224
- }
225
-
226
- type MockCallReader = { mock: { calls: unknown[][] } };
227
-
228
- function requireRecord(value: unknown, label: string): Record<string, unknown> {
229
- if (!value || typeof value !== "object" || Array.isArray(value)) {
230
- throw new Error(`expected ${label} to be an object`);
231
- }
232
- return value as Record<string, unknown>;
233
- }
234
-
235
- function optionalString(value: unknown): string {
236
- return typeof value === "string" ? value : "";
237
- }
238
-
239
- function requireFirstCallArg(mock: unknown, label: string): unknown {
240
- const call = (mock as MockCallReader).mock.calls[0];
241
- if (!call) {
242
- throw new Error(`expected ${label} to be called`);
243
- }
244
- return call[0];
245
- }
246
-
247
- function requireRequestParams(
248
- harness: ReturnType<typeof createStartedThreadHarness>,
249
- method: string,
250
- ): Record<string, unknown> {
251
- const request = harness.requests.find((entry) => entry.method === method);
252
- return requireRecord(request?.params, `${method} params`);
253
- }
254
-
255
- function requireArray(value: unknown, label: string): unknown[] {
256
- if (!Array.isArray(value)) {
257
- throw new Error(`expected ${label} to be an array`);
258
- }
259
- return value;
260
- }
261
-
262
- function expectRequestInputTextContains(
263
- harness: ReturnType<typeof createStartedThreadHarness>,
264
- expected: string,
265
- ): void {
266
- expect(getRequestInputText(harness)).toContain(expected);
267
- }
268
-
269
- function getRequestInputText(harness: ReturnType<typeof createStartedThreadHarness>): string {
270
- return getRequestInputTextAt(harness, 0);
271
- }
272
-
273
- function getRequestInputTextAt(
274
- harness: ReturnType<typeof createStartedThreadHarness>,
275
- index: number,
276
- ): string {
277
- const request = harness.requests.filter((entry) => entry.method === "turn/start").at(index);
278
- const params = requireRecord(request?.params, "turn/start params");
279
- const input = requireArray(params.input, "turn/start input");
280
- return input
281
- .map((entry) => {
282
- const item = requireRecord(entry, "turn/start input entry");
283
- return item.type === "text" ? optionalString(item.text) : "";
284
- })
285
- .join("\n");
286
- }
287
-
288
- describe("runCodexAppServerAttempt context-engine lifecycle", () => {
289
- beforeEach(async () => {
290
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "klaw-codex-context-engine-"));
291
- });
292
-
293
- afterEach(async () => {
294
- resetCodexAppServerClientFactoryForTest();
295
- vi.restoreAllMocks();
296
- await fs.rm(tempDir, { recursive: true, force: true });
297
- });
298
-
299
- it("bootstraps and assembles non-legacy context before the Codex turn starts", async () => {
300
- const sessionFile = path.join(tempDir, "session.jsonl");
301
- const workspaceDir = path.join(tempDir, "workspace");
302
- SessionManager.open(sessionFile).appendMessage(
303
- assistantMessage("existing context", Date.now()) as never,
304
- );
305
- const openSpy = vi.spyOn(SessionManager, "open");
306
- const contextEngine = createContextEngine();
307
- const harness = createStartedThreadHarness();
308
- const params = createParams(sessionFile, workspaceDir);
309
- params.contextEngine = contextEngine;
310
- params.contextTokenBudget = 321;
311
- params.config = { memory: { citations: "on" } } as EmbeddedRunAttemptParams["config"];
312
-
313
- const run = runCodexAppServerAttempt(params);
314
- await harness.waitForMethod("turn/start");
315
-
316
- if (!contextEngine.bootstrap) {
317
- throw new Error("expected bootstrap hook");
318
- }
319
- expect(contextEngine.bootstrap).toHaveBeenCalledTimes(1);
320
- const bootstrapParams = requireFirstCallArg(contextEngine.bootstrap, "bootstrap") as Parameters<
321
- NonNullable<ContextEngine["bootstrap"]>
322
- >[0];
323
- expect(bootstrapParams.sessionId).toBe("session-1");
324
- expect(bootstrapParams.sessionKey).toBe("agent:main:session-1");
325
- expect(bootstrapParams.sessionFile).toBe(sessionFile);
326
-
327
- expect(contextEngine.assemble).toHaveBeenCalledTimes(1);
328
- const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
329
- ContextEngine["assemble"]
330
- >[0];
331
- expect(assembleParams.sessionId).toBe("session-1");
332
- expect(assembleParams.sessionKey).toBe("agent:main:session-1");
333
- expect(assembleParams.tokenBudget).toBe(321);
334
- expect(assembleParams.citationsMode).toBe("on");
335
- expect(assembleParams.model).toBe("gpt-5.4-codex");
336
- expect(assembleParams.prompt).toBe("hello");
337
- expect(assembleParams.messages.map((message) => message.role)).toEqual(["assistant"]);
338
- expect(assembleParams.availableTools).toEqual(new Set());
339
-
340
- const threadStartParams = requireRequestParams(harness, "thread/start");
341
- expect(optionalString(threadStartParams.developerInstructions)).toContain(
342
- "context-engine system",
343
- );
344
- expectRequestInputTextContains(harness, "Klaw assembled context for this turn:");
345
-
346
- await harness.completeTurn();
347
- await run;
348
- expect(openSpy).not.toHaveBeenCalled();
349
- });
350
-
351
- it("uses the runtime token budget for large Codex context-engine projections", async () => {
352
- const sessionFile = path.join(tempDir, "session.jsonl");
353
- const workspaceDir = path.join(tempDir, "workspace");
354
- const longContext = `large LCM context start ${"x".repeat(30_000)} LARGE_CONTEXT_END`;
355
- const contextEngine = createContextEngine({
356
- assemble: vi.fn(async () => ({
357
- messages: [assistantMessage(longContext, 10)],
358
- estimatedTokens: 10_000,
359
- systemPromptAddition: "context-engine system",
360
- })),
361
- });
362
- const harness = createStartedThreadHarness();
363
- const params = createParams(sessionFile, workspaceDir);
364
- params.contextEngine = contextEngine;
365
- params.contextTokenBudget = 80_000;
366
-
367
- const run = runCodexAppServerAttempt(params);
368
- await harness.waitForMethod("turn/start");
369
-
370
- const inputText = getRequestInputText(harness);
371
- expect(inputText.length).toBeGreaterThan(30_000);
372
- expect(inputText).toContain("LARGE_CONTEXT_END");
373
- expect(inputText).not.toContain("[truncated ");
374
-
375
- await harness.completeTurn();
376
- await run;
377
- });
378
-
379
- it("uses configured compaction reserve when sizing Codex context-engine projections", async () => {
380
- const sessionFile = path.join(tempDir, "session.jsonl");
381
- const workspaceDir = path.join(tempDir, "workspace");
382
- const longContext = `configured reserve context start ${"x".repeat(30_000)} CONFIG_END`;
383
- const contextEngine = createContextEngine({
384
- assemble: vi.fn(async () => ({
385
- messages: [assistantMessage(longContext, 10)],
386
- estimatedTokens: 10_000,
387
- systemPromptAddition: "context-engine system",
388
- })),
389
- });
390
- const harness = createStartedThreadHarness();
391
- const params = createParams(sessionFile, workspaceDir);
392
- params.contextEngine = contextEngine;
393
- params.contextTokenBudget = 80_000;
394
- params.config = {
395
- agents: { defaults: { compaction: { reserveTokens: 60_000, reserveTokensFloor: 0 } } },
396
- } as EmbeddedRunAttemptParams["config"];
397
-
398
- const run = runCodexAppServerAttempt(params);
399
- await harness.waitForMethod("turn/start");
400
-
401
- const inputText = getRequestInputText(harness);
402
- expect(inputText).toContain("configured reserve context start");
403
- expect(inputText).toContain("[truncated ");
404
- expect(inputText).not.toContain("CONFIG_END");
405
-
406
- await harness.completeTurn();
407
- await run;
408
- });
409
-
410
- it("projects thread-bootstrap context only once for a matching context-engine epoch", async () => {
411
- const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined);
412
- const sessionFile = path.join(tempDir, "session.jsonl");
413
- const workspaceDir = path.join(tempDir, "workspace");
414
- SessionManager.open(sessionFile).appendMessage(
415
- assistantMessage("bootstrap-only context", Date.now()) as never,
416
- );
417
- const contextEngine = createContextEngine({
418
- assemble: vi.fn(async ({ messages, prompt }) => ({
419
- messages: [...messages, userMessage(prompt ?? "", 10)],
420
- estimatedTokens: 42,
421
- systemPromptAddition: "context-engine system",
422
- contextProjection: { mode: "thread_bootstrap" as const, epoch: "epoch-1" },
423
- })),
424
- });
425
- const firstHarness = createStartedThreadHarness();
426
- const firstParams = createParams(sessionFile, workspaceDir);
427
- firstParams.contextEngine = contextEngine;
428
-
429
- const firstRun = runCodexAppServerAttempt(firstParams);
430
- await firstHarness.waitForMethod("turn/start");
431
- expectRequestInputTextContains(firstHarness, "Klaw assembled context for this turn:");
432
- expectRequestInputTextContains(firstHarness, "bootstrap-only context");
433
- await firstHarness.completeTurn();
434
- await firstRun;
435
-
436
- const savedBinding = await readCodexAppServerBinding(sessionFile);
437
- expect(savedBinding?.contextEngine?.projection).toEqual({
438
- schemaVersion: 1,
439
- mode: "thread_bootstrap",
440
- epoch: "epoch-1",
441
- fingerprint: undefined,
442
- });
443
-
444
- const secondHarness = createStartedThreadHarness(async (method) => {
445
- if (method === "thread/resume") {
446
- return threadStartResult("thread-1");
447
- }
448
- return undefined;
449
- });
450
- const secondRun = runCodexAppServerAttempt(firstParams);
451
- await secondHarness.waitForMethod("turn/start");
452
-
453
- expect(secondHarness.requests.map((request) => request.method)).toEqual([
454
- "thread/resume",
455
- "turn/start",
456
- ]);
457
- const secondInputText = getRequestInputText(secondHarness);
458
- expect(secondInputText).not.toContain("Klaw assembled context for this turn:");
459
- expect(secondInputText).not.toContain("bootstrap-only context");
460
- expect(secondInputText).toBe("hello");
461
- const projectionLogs = info.mock.calls.filter(
462
- ([message]) => message === "codex app-server context-engine projection decision",
463
- );
464
- expect(projectionLogs).toEqual([
465
- [
466
- "codex app-server context-engine projection decision",
467
- expect.objectContaining({
468
- sessionId: "session-1",
469
- sessionKey: "agent:main:session-1",
470
- engineId: "lossless-claw",
471
- mode: "thread_bootstrap",
472
- epoch: "epoch-1",
473
- projected: true,
474
- reason: "missing-thread-binding",
475
- }),
476
- ],
477
- [
478
- "codex app-server context-engine projection decision",
479
- expect.objectContaining({
480
- sessionId: "session-1",
481
- sessionKey: "agent:main:session-1",
482
- engineId: "lossless-claw",
483
- mode: "thread_bootstrap",
484
- epoch: "epoch-1",
485
- previousThreadId: "thread-1",
486
- previousEpoch: "epoch-1",
487
- projected: false,
488
- reason: "matching-thread-bootstrap-binding",
489
- }),
490
- ],
491
- ]);
492
-
493
- await secondHarness.completeTurn();
494
- await secondRun;
495
- });
496
-
497
- it("starts a fresh Codex thread and reprojects when context-engine epoch changes", async () => {
498
- const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined);
499
- const sessionFile = path.join(tempDir, "session.jsonl");
500
- const workspaceDir = path.join(tempDir, "workspace");
501
- await writeCodexAppServerBinding(sessionFile, {
502
- threadId: "thread-old",
503
- cwd: workspaceDir,
504
- dynamicToolsFingerprint: "[]",
505
- contextEngine: {
506
- schemaVersion: 1,
507
- engineId: "lossless-claw",
508
- policyFingerprint:
509
- '{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
510
- projection: {
511
- schemaVersion: 1,
512
- mode: "thread_bootstrap",
513
- epoch: "epoch-old",
514
- },
515
- },
516
- });
517
- const contextEngine = createContextEngine({
518
- assemble: vi.fn(async ({ prompt }) => ({
519
- messages: [assistantMessage("new epoch context", 10), userMessage(prompt ?? "", 11)],
520
- estimatedTokens: 42,
521
- systemPromptAddition: "context-engine system",
522
- contextProjection: { mode: "thread_bootstrap" as const, epoch: "epoch-new" },
523
- })),
524
- });
525
- const harness = createStartedThreadHarness(async (method) => {
526
- if (method === "thread/start") {
527
- return threadStartResult("thread-new");
528
- }
529
- return undefined;
530
- });
531
- const params = createParams(sessionFile, workspaceDir);
532
- params.contextEngine = contextEngine;
533
-
534
- const run = runCodexAppServerAttempt(params);
535
- await harness.waitForMethod("turn/start");
536
-
537
- expect(harness.requests.map((request) => request.method)).toEqual([
538
- "thread/start",
539
- "turn/start",
540
- ]);
541
- expectRequestInputTextContains(harness, "Klaw assembled context for this turn:");
542
- expectRequestInputTextContains(harness, "new epoch context");
543
-
544
- await harness.notify({
545
- method: "turn/completed",
546
- params: {
547
- threadId: "thread-new",
548
- turnId: "turn-1",
549
- turn: {
550
- id: "turn-1",
551
- status: "completed",
552
- items: [{ type: "agentMessage", id: "msg-1", text: "fresh answer" }],
553
- },
554
- },
555
- });
556
- await run;
557
-
558
- const savedBinding = await readCodexAppServerBinding(sessionFile);
559
- expect(savedBinding?.threadId).toBe("thread-new");
560
- expect(savedBinding?.contextEngine?.projection?.epoch).toBe("epoch-new");
561
- expect(info).toHaveBeenCalledWith(
562
- "codex app-server context-engine projection decision",
563
- expect.objectContaining({
564
- sessionId: "session-1",
565
- engineId: "lossless-claw",
566
- epoch: "epoch-new",
567
- previousThreadId: "thread-old",
568
- previousEpoch: "epoch-old",
569
- projected: true,
570
- reason: "context-engine-binding-mismatch",
571
- }),
572
- );
573
- expect(info).toHaveBeenCalledWith(
574
- "codex app-server wrote context-engine thread binding",
575
- expect.objectContaining({
576
- sessionId: "session-1",
577
- threadId: "thread-new",
578
- engineId: "lossless-claw",
579
- epoch: "epoch-new",
580
- action: "rotated",
581
- }),
582
- );
583
- });
584
-
585
- it("reprojects thread-bootstrap context when context-engine policy changes", async () => {
586
- const sessionFile = path.join(tempDir, "session.jsonl");
587
- const workspaceDir = path.join(tempDir, "workspace");
588
- await writeCodexAppServerBinding(sessionFile, {
589
- threadId: "thread-old",
590
- cwd: workspaceDir,
591
- dynamicToolsFingerprint: "[]",
592
- contextEngine: {
593
- schemaVersion: 1,
594
- engineId: "lossless-claw",
595
- policyFingerprint:
596
- '{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
597
- projection: {
598
- schemaVersion: 1,
599
- mode: "thread_bootstrap",
600
- epoch: "epoch-1",
601
- },
602
- },
603
- });
604
- const contextEngine = createContextEngine({
605
- assemble: vi.fn(async ({ prompt }) => ({
606
- messages: [assistantMessage("policy changed context", 10), userMessage(prompt ?? "", 11)],
607
- estimatedTokens: 42,
608
- systemPromptAddition: "context-engine system",
609
- contextProjection: { mode: "thread_bootstrap" as const, epoch: "epoch-1" },
610
- })),
611
- });
612
- const harness = createStartedThreadHarness(async (method) => {
613
- if (method === "thread/start") {
614
- return threadStartResult("thread-new");
615
- }
616
- return undefined;
617
- });
618
- const params = createParams(sessionFile, workspaceDir);
619
- params.contextEngine = contextEngine;
620
- params.contextTokenBudget = 80_000;
621
-
622
- const run = runCodexAppServerAttempt(params);
623
- await harness.waitForMethod("turn/start");
624
-
625
- expect(harness.requests.map((request) => request.method)).toEqual([
626
- "thread/start",
627
- "turn/start",
628
- ]);
629
- expectRequestInputTextContains(harness, "Klaw assembled context for this turn:");
630
- expectRequestInputTextContains(harness, "policy changed context");
631
-
632
- await harness.notify({
633
- method: "turn/completed",
634
- params: {
635
- threadId: "thread-new",
636
- turnId: "turn-1",
637
- turn: {
638
- id: "turn-1",
639
- status: "completed",
640
- items: [{ type: "agentMessage", id: "msg-1", text: "fresh answer" }],
641
- },
642
- },
643
- });
644
- await run;
645
- });
646
-
647
- it("starts a fresh Codex thread when thread-bootstrap projection falls back to per-turn projection", async () => {
648
- const sessionFile = path.join(tempDir, "session.jsonl");
649
- const workspaceDir = path.join(tempDir, "workspace");
650
- await writeCodexAppServerBinding(sessionFile, {
651
- threadId: "thread-old",
652
- cwd: workspaceDir,
653
- dynamicToolsFingerprint: "[]",
654
- contextEngine: {
655
- schemaVersion: 1,
656
- engineId: "lossless-claw",
657
- policyFingerprint:
658
- '{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"projectionMaxChars":24000}',
659
- projection: {
660
- schemaVersion: 1,
661
- mode: "thread_bootstrap",
662
- epoch: "epoch-1",
663
- },
664
- },
665
- });
666
- const contextEngine = createContextEngine({
667
- assemble: vi.fn(async ({ prompt }) => ({
668
- messages: [assistantMessage("per-turn context", 10), userMessage(prompt ?? "", 11)],
669
- estimatedTokens: 42,
670
- systemPromptAddition: "context-engine system",
671
- })),
672
- });
673
- const harness = createStartedThreadHarness(async (method) => {
674
- if (method === "thread/resume") {
675
- return threadStartResult("thread-old");
676
- }
677
- if (method === "thread/start") {
678
- return threadStartResult("thread-new");
679
- }
680
- return undefined;
681
- });
682
- const params = createParams(sessionFile, workspaceDir);
683
- params.contextEngine = contextEngine;
684
-
685
- const run = runCodexAppServerAttempt(params);
686
- await harness.waitForMethod("turn/start");
687
-
688
- expect(harness.requests.map((request) => request.method)).toEqual([
689
- "thread/start",
690
- "turn/start",
691
- ]);
692
- expectRequestInputTextContains(harness, "Klaw assembled context for this turn:");
693
- expectRequestInputTextContains(harness, "per-turn context");
694
-
695
- await harness.notify({
696
- method: "turn/completed",
697
- params: {
698
- threadId: "thread-new",
699
- turnId: "turn-1",
700
- turn: {
701
- id: "turn-1",
702
- status: "completed",
703
- items: [{ type: "agentMessage", id: "msg-1", text: "fresh answer" }],
704
- },
705
- },
706
- });
707
- await run;
708
-
709
- const savedBinding = await readCodexAppServerBinding(sessionFile);
710
- expect(savedBinding?.threadId).toBe("thread-new");
711
- expect(savedBinding?.contextEngine?.projection).toBeUndefined();
712
- });
713
-
714
- it("retries a resumed context-engine thread on a fresh Codex thread after early context overflow", async () => {
715
- const sessionFile = path.join(tempDir, "session.jsonl");
716
- const successorFile = path.join(tempDir, "session.compacted.jsonl");
717
- const workspaceDir = path.join(tempDir, "workspace");
718
- SessionManager.open(sessionFile).appendMessage(
719
- assistantMessage("pre-compaction context", Date.now()) as never,
720
- );
721
- await writeCodexAppServerBinding(sessionFile, {
722
- threadId: "thread-old",
723
- cwd: workspaceDir,
724
- dynamicToolsFingerprint: "[]",
725
- contextEngine: {
726
- schemaVersion: 1,
727
- engineId: "lossless-claw",
728
- policyFingerprint:
729
- '{"schemaVersion":1,"engineId":"lossless-claw","ownsCompaction":true,"contextTokenBudget":400000,"projectionMaxChars":1000000}',
730
- projection: {
731
- schemaVersion: 1,
732
- mode: "thread_bootstrap",
733
- epoch: "epoch-before",
734
- },
735
- },
736
- });
737
- let epoch = "epoch-before";
738
- const compact = vi.fn(async () => {
739
- epoch = "epoch-after";
740
- SessionManager.open(successorFile).appendMessage(
741
- assistantMessage("successor compacted context", Date.now()) as never,
742
- );
743
- return {
744
- ok: true,
745
- compacted: true,
746
- result: {
747
- summary: "summary",
748
- firstKeptEntryId: "entry-1",
749
- tokensBefore: 10,
750
- sessionId: "session-1-compacted",
751
- sessionFile: successorFile,
752
- },
753
- };
754
- });
755
- const assemble = vi.fn(
756
- async ({ messages, prompt }: Parameters<ContextEngine["assemble"]>[0]) => ({
757
- messages: [
758
- ...messages,
759
- assistantMessage(`context ${epoch}`, 10),
760
- userMessage(prompt ?? "", 11),
761
- ],
762
- estimatedTokens: 42,
763
- systemPromptAddition: "context-engine system",
764
- contextProjection: { mode: "thread_bootstrap" as const, epoch },
765
- }),
766
- );
767
- const contextEngine = createContextEngine({ assemble, compact });
768
- const harness = createStartedThreadHarness(async (method, requestParams) => {
769
- const request = requireRecord(requestParams, `${method} params`);
770
- if (method === "thread/resume") {
771
- return threadStartResult("thread-old");
772
- }
773
- if (method === "turn/start" && request.threadId === "thread-old") {
774
- throw new Error("Codex ran out of room in the model's context window");
775
- }
776
- if (method === "thread/start") {
777
- return threadStartResult("thread-fresh");
778
- }
779
- if (method === "turn/start" && request.threadId === "thread-fresh") {
780
- return turnStartResult("turn-fresh");
781
- }
782
- return undefined;
783
- });
784
- const params = createParams(sessionFile, workspaceDir);
785
- params.contextEngine = contextEngine;
786
- params.contextTokenBudget = 400_000;
787
-
788
- const run = runCodexAppServerAttempt(params);
789
- await vi.waitFor(() =>
790
- expect(harness.requests.map((request) => request.method)).toEqual([
791
- "thread/resume",
792
- "turn/start",
793
- "thread/start",
794
- "turn/start",
795
- ]),
796
- );
797
- await harness.notify({
798
- method: "turn/completed",
799
- params: {
800
- threadId: "thread-fresh",
801
- turnId: "turn-fresh",
802
- turn: {
803
- id: "turn-fresh",
804
- status: "completed",
805
- items: [{ type: "agentMessage", id: "msg-1", text: "fresh answer" }],
806
- },
807
- },
808
- });
809
- const result = await run;
810
-
811
- expect(result.assistantTexts).toContain("fresh answer");
812
- expect(compact).toHaveBeenCalledWith(
813
- expect.objectContaining({
814
- sessionId: "session-1",
815
- sessionKey: "agent:main:session-1",
816
- sessionFile,
817
- tokenBudget: 400_000,
818
- currentTokenCount: 400_000,
819
- compactionTarget: "threshold",
820
- force: true,
821
- }),
822
- );
823
- expect(assemble).toHaveBeenCalledTimes(2);
824
- const retryAssembleParams = assemble.mock.calls[1]?.[0];
825
- expect(retryAssembleParams?.messages.map((message) => message.role)).toEqual(["assistant"]);
826
- const retryAssembleMessageTexts = retryAssembleParams?.messages.map((message) => {
827
- if (!("content" in message) || !Array.isArray(message.content)) {
828
- return "";
829
- }
830
- const firstContent = message.content[0];
831
- return typeof firstContent === "object" && firstContent !== null && "text" in firstContent
832
- ? firstContent.text
833
- : "";
834
- });
835
- expect(retryAssembleMessageTexts).toEqual(["successor compacted context"]);
836
- const retryInputText = getRequestInputTextAt(harness, -1);
837
- expect(retryInputText).toContain("successor compacted context");
838
- expect(retryInputText).not.toContain("pre-compaction context");
839
- const savedBinding = await readCodexAppServerBinding(successorFile);
840
- expect(savedBinding?.threadId).toBe("thread-fresh");
841
- expect(savedBinding?.contextEngine?.engineId).toBe("lossless-claw");
842
- expect(savedBinding?.contextEngine?.projection?.epoch).toBe("epoch-after");
843
- });
844
-
845
- it("keeps current inbound context at the front of the Codex context-engine prompt", async () => {
846
- const sessionFile = path.join(tempDir, "session.jsonl");
847
- const workspaceDir = path.join(tempDir, "workspace");
848
- SessionManager.open(sessionFile).appendMessage(
849
- assistantMessage("older context", Date.now()) as never,
850
- );
851
- const contextEngine = createContextEngine();
852
- const harness = createStartedThreadHarness();
853
- const params = createParams(sessionFile, workspaceDir);
854
- params.contextEngine = contextEngine;
855
- params.currentInboundContext = {
856
- text: [
857
- "Conversation context (untrusted, chronological, selected for current message):",
858
- "#6474 Sun 2026-05-10 22:22 GMT+5:30 [reply target] Klaw: anchor REPLYCTX this is the old message",
859
- "#6498 Sun 2026-05-10 22:22 GMT+5:30 Klaw: filler REPLYCTX 23",
860
- ].join("\n"),
861
- };
862
-
863
- const run = runCodexAppServerAttempt(params);
864
- await harness.waitForMethod("turn/start");
865
-
866
- const inputText = getRequestInputText(harness);
867
- expect(inputText).toContain("Klaw assembled context for this turn:");
868
- expect(inputText).toContain("Current user request:\nhello");
869
- expect(inputText).toContain("[reply target] Klaw: anchor REPLYCTX");
870
- expect(inputText.trim().startsWith("Conversation context (untrusted")).toBe(true);
871
-
872
- await harness.completeTurn();
873
- await run;
874
- });
875
-
876
- it("calls afterTurn with the mirrored transcript and runs turn maintenance", async () => {
877
- const sessionFile = path.join(tempDir, "session.jsonl");
878
- const workspaceDir = path.join(tempDir, "workspace");
879
- const afterTurn = vi.fn(
880
- async (_params: Parameters<NonNullable<ContextEngine["afterTurn"]>>[0]) => undefined,
881
- );
882
- const maintain = vi.fn(async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 }));
883
- const contextEngine = createContextEngine({ afterTurn, maintain, bootstrap: undefined });
884
- const harness = createStartedThreadHarness();
885
- const params = createParams(sessionFile, workspaceDir);
886
- params.contextEngine = contextEngine;
887
- params.contextTokenBudget = 111;
888
-
889
- const run = runCodexAppServerAttempt(params);
890
- await harness.waitForMethod("turn/start");
891
- await harness.completeTurn();
892
- await run;
893
-
894
- expect(afterTurn).toHaveBeenCalledTimes(1);
895
- const afterTurnCall = requireFirstCallArg(afterTurn, "afterTurn") as Parameters<
896
- NonNullable<ContextEngine["afterTurn"]>
897
- >[0];
898
- expect(afterTurnCall.sessionId).toBe("session-1");
899
- expect(afterTurnCall.sessionKey).toBe("agent:main:session-1");
900
- expect(afterTurnCall.prePromptMessageCount).toBe(0);
901
- expect(afterTurnCall.tokenBudget).toBe(111);
902
- expect(afterTurnCall.messages.some((message) => message.role === "user")).toBe(true);
903
- expect(afterTurnCall.messages.some((message) => message.role === "assistant")).toBe(true);
904
- expect(maintain).toHaveBeenCalledTimes(1);
905
- });
906
-
907
- it("reloads mirrored history after bootstrap mutates the session transcript", async () => {
908
- const sessionFile = path.join(tempDir, "session.jsonl");
909
- const workspaceDir = path.join(tempDir, "workspace");
910
- SessionManager.open(sessionFile).appendMessage(
911
- assistantMessage("existing context", Date.now()) as never,
912
- );
913
- const afterTurn = vi.fn(
914
- async (_params: Parameters<NonNullable<ContextEngine["afterTurn"]>>[0]) => undefined,
915
- );
916
- const bootstrap = vi.fn(
917
- async ({ sessionFile: file }: Parameters<NonNullable<ContextEngine["bootstrap"]>>[0]) => {
918
- SessionManager.open(file).appendMessage(
919
- assistantMessage("bootstrap context", Date.now() + 1) as never,
920
- );
921
- return { bootstrapped: true };
922
- },
923
- );
924
- const contextEngine = createContextEngine({
925
- bootstrap,
926
- afterTurn,
927
- maintain: undefined,
928
- });
929
- const harness = createStartedThreadHarness();
930
- const params = createParams(sessionFile, workspaceDir);
931
- params.contextEngine = contextEngine;
932
-
933
- const run = runCodexAppServerAttempt(params);
934
- await harness.waitForMethod("turn/start");
935
- await harness.completeTurn();
936
- await run;
937
-
938
- const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
939
- ContextEngine["assemble"]
940
- >[0];
941
- expect(assembleParams.messages.map((message) => message.role)).toEqual([
942
- "assistant",
943
- "assistant",
944
- ]);
945
- const afterTurnParams = requireFirstCallArg(afterTurn, "afterTurn") as Parameters<
946
- NonNullable<ContextEngine["afterTurn"]>
947
- >[0];
948
- expect(afterTurnParams.prePromptMessageCount).toBe(2);
949
- expectRequestInputTextContains(harness, "bootstrap context");
950
- });
951
-
952
- it("logs assemble failures as a formatted message instead of the raw error object", async () => {
953
- const sessionFile = path.join(tempDir, "session.jsonl");
954
- const workspaceDir = path.join(tempDir, "workspace");
955
- const rawError = new Error("Authorization: Bearer sk-abcdefghijklmnopqrstuv");
956
- const contextEngine = createContextEngine({
957
- assemble: vi.fn(async () => {
958
- throw rawError;
959
- }),
960
- bootstrap: undefined,
961
- });
962
- const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
963
- const harness = createStartedThreadHarness();
964
- const params = createParams(sessionFile, workspaceDir);
965
- params.contextEngine = contextEngine;
966
-
967
- const run = runCodexAppServerAttempt(params);
968
- await harness.waitForMethod("turn/start");
969
- await harness.completeTurn();
970
- await run;
971
-
972
- const warning = warn.mock.calls.find(
973
- ([message]) => message === "context engine assemble failed; using Codex baseline prompt",
974
- );
975
- const details = requireRecord(warning?.[1], "assemble warning details");
976
- expect(typeof details.error).toBe("string");
977
- expect(warning?.[1]).not.toEqual({ error: rawError });
978
- expect(String(details.error)).not.toContain("sk-abcdefghijklmnopqrstuv");
979
- });
980
-
981
- it("falls back to ingestBatch and skips turn maintenance on prompt failure", async () => {
982
- const sessionFile = path.join(tempDir, "session.jsonl");
983
- const workspaceDir = path.join(tempDir, "workspace");
984
- const ingestBatch = vi.fn(async () => ({ ingestedCount: 2 }));
985
- const maintain = vi.fn(async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 }));
986
- const contextEngine = createContextEngine({
987
- afterTurn: undefined,
988
- ingestBatch,
989
- maintain,
990
- bootstrap: undefined,
991
- });
992
- const harness = createStartedThreadHarness();
993
- const params = createParams(sessionFile, workspaceDir);
994
- params.contextEngine = contextEngine;
995
-
996
- const run = runCodexAppServerAttempt(params);
997
- await harness.waitForMethod("turn/start");
998
- await harness.completeTurn("failed");
999
- await run;
1000
-
1001
- expect(ingestBatch).toHaveBeenCalledTimes(1);
1002
- expect(maintain).not.toHaveBeenCalled();
1003
- });
1004
- });