@jskit-ai/assistant-core 0.1.199 → 0.1.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { appendFile, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { request } from "node:http";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { gzipSync, zstdCompressSync } from "node:zlib";
|
|
9
|
+
import { startCodexHistoryAdapter, translateCodexHistory, prepareCodexHistory } from "../src/server/conversation/codexHistoryAdapter.js";
|
|
10
|
+
import { nativeAiProvider } from "../src/shared/nativeProviders.js";
|
|
11
|
+
|
|
12
|
+
const foreign = { type: "reasoning", id: "foreign-id", encrypted_content: "foreign-opaque-state",
|
|
13
|
+
summary: [{ type: "summary_text", text: "Prior summary" }], content: [{ type: "reasoning_text", text: "Exact history\n kept intact." }] };
|
|
14
|
+
|
|
15
|
+
test("history translation preserves every ordinary item, plaintext and native history", () => {
|
|
16
|
+
const body = { model: "gpt-6-astra", input: [
|
|
17
|
+
{ type: "message", role: "user", content: [{ type: "input_text", text: "Implement it." }] },
|
|
18
|
+
foreign,
|
|
19
|
+
{ type: "function_call", call_id: "tool-1", name: "shell_command", arguments: "{}" },
|
|
20
|
+
{ type: "function_call_output", call_id: "tool-1", output: "test passed" },
|
|
21
|
+
{ type: "reasoning", summary: [], content: [], encrypted_content: "openai-state" }
|
|
22
|
+
] };
|
|
23
|
+
const original = structuredClone(body);
|
|
24
|
+
const translated = translateCodexHistory(body);
|
|
25
|
+
assert.deepEqual(body, original);
|
|
26
|
+
assert.equal(translated.input.length, body.input.length);
|
|
27
|
+
assert.equal(translated.input[1].role, "assistant");
|
|
28
|
+
assert.ok(translated.input[1].content[0].text.includes("Prior summary\nExact history\n kept intact."));
|
|
29
|
+
assert.ok(!JSON.stringify(translated).includes("foreign-opaque-state"));
|
|
30
|
+
for (const index of [0, 2, 3, 4]) assert.equal(translated.input[index], body.input[index]);
|
|
31
|
+
assert.throws(() => translateCodexHistory({ input: [{ ...foreign, content: [{ type: "unknown", text: "keep me" }] }] }), /not supported/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("DeepSeek receives unsupported exec history as context while keeping tools, images and native state intact", () => {
|
|
35
|
+
const call = { type: "custom_tool_call", name: "exec", call_id: "exec-1", input: "text(await tools.exec_command({cmd: 'pwd'}));" };
|
|
36
|
+
const image = { type: "input_image", image_url: "data:image/png;base64,fixture", detail: "original" };
|
|
37
|
+
const output = { type: "custom_tool_call_output", call_id: "exec-1", output: [
|
|
38
|
+
{ type: "input_text", text: "Exact output\n with whitespace" }, image
|
|
39
|
+
] };
|
|
40
|
+
const previousFailure = { type: "function_call", name: "exec", call_id: "exec-bad", arguments: '{"input":"text(42)"}' };
|
|
41
|
+
const ordinary = { type: "function_call", name: "exec_command", call_id: "shell-1", arguments: '{"cmd":"pwd"}' };
|
|
42
|
+
const patch = { type: "custom_tool_call", name: "apply_patch", call_id: "patch-1", input: "*** Begin Patch\n*** End Patch" };
|
|
43
|
+
const body = { tools: [{ type: "function", name: "exec_command" }], input: [foreign, call, output,
|
|
44
|
+
previousFailure, { type: "function_call_output", call_id: "exec-bad", output: "unsupported call: exec" }, ordinary, patch] };
|
|
45
|
+
const saved = structuredClone(body);
|
|
46
|
+
const translated = translateCodexHistory(body, "deepseek");
|
|
47
|
+
assert.deepEqual(body, saved);
|
|
48
|
+
assert.equal(translated.tools, body.tools);
|
|
49
|
+
for (const index of [0, 5, 6]) assert.equal(translated.input[index], body.input[index]);
|
|
50
|
+
assert.match(translated.input[1].content[0].text, /context only, not an available tool/);
|
|
51
|
+
assert.ok(translated.input[1].content[0].text.includes(JSON.stringify(call)));
|
|
52
|
+
assert.equal(translated.input[2].content[1], output.output[0]);
|
|
53
|
+
assert.equal(translated.input[2].content[2], image);
|
|
54
|
+
assert.ok(translated.input[3].content[0].text.includes(JSON.stringify(previousFailure)));
|
|
55
|
+
assert.equal(translated.input[4].content[1].text, "unsupported call: exec");
|
|
56
|
+
assert.equal(translateCodexHistory(body).input[1], call);
|
|
57
|
+
assert.throws(() => translateCodexHistory({ input: [call, { ...output, output: [{ type: "unknown" }] }] }, "deepseek"), /history is not supported/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("adapter forwards bounded compressed requests only to fixed provider routes and streams responses", async (t) => {
|
|
61
|
+
const calls = [];
|
|
62
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), fetchImpl: async (url, options) => {
|
|
63
|
+
calls.push({ url, ...options });
|
|
64
|
+
return new Response("data: one\n\ndata: two\n\n", { status: 200, headers: { "content-type": "text/event-stream", "x-request-id": "upstream-id" } });
|
|
65
|
+
} });
|
|
66
|
+
t.after(() => adapter.close());
|
|
67
|
+
for (const [encoding, encode] of [["identity", Buffer.from], ["gzip", gzipSync], ["zstd", zstdCompressSync]]) {
|
|
68
|
+
const result = await fetch(`${adapter.baseUrl}/chatgpt/responses`, { method: "POST",
|
|
69
|
+
headers: { "content-encoding": encoding, authorization: "Bearer native-auth" },
|
|
70
|
+
body: encode(JSON.stringify({ input: [foreign] })) });
|
|
71
|
+
assert.equal(result.status, 200);
|
|
72
|
+
assert.equal(result.headers.get("x-request-id"), "upstream-id");
|
|
73
|
+
assert.equal(await result.text(), "data: one\n\ndata: two\n\n");
|
|
74
|
+
const call = calls.at(-1);
|
|
75
|
+
assert.equal(call.url, "https://chatgpt.com/backend-api/codex/responses");
|
|
76
|
+
assert.equal(call.headers.get("authorization"), "Bearer native-auth");
|
|
77
|
+
assert.equal(call.headers.has("content-encoding"), false);
|
|
78
|
+
assert.equal(call.redirect, "error");
|
|
79
|
+
assert.equal(JSON.parse(call.body).input[0].type, "message");
|
|
80
|
+
}
|
|
81
|
+
const compact = await fetch(`${adapter.baseUrl}/apiKey/responses/compact`, { method: "POST", body: JSON.stringify({ input: [foreign] }) });
|
|
82
|
+
await compact.text();
|
|
83
|
+
assert.equal(calls.at(-1).url, "https://api.openai.com/v1/responses/compact");
|
|
84
|
+
const models = await fetch(`${adapter.baseUrl}/chatgpt/models?client_version=1`);
|
|
85
|
+
await models.text();
|
|
86
|
+
assert.equal(calls.at(-1).url, "https://chatgpt.com/backend-api/codex/models?client_version=1");
|
|
87
|
+
const deepseek = await fetch(`${adapter.baseUrl}/deepseek/responses`, { method: "POST",
|
|
88
|
+
headers: { authorization: "Bearer deepseek-key" }, body: JSON.stringify({ input: [foreign,
|
|
89
|
+
{ type: "custom_tool_call", name: "exec", call_id: "exec-1", input: "text(42)" },
|
|
90
|
+
{ type: "custom_tool_call_output", call_id: "exec-1", output: "42" }
|
|
91
|
+
] }) });
|
|
92
|
+
assert.equal(await deepseek.text(), "data: one\n\ndata: two\n\n");
|
|
93
|
+
assert.equal(calls.at(-1).url, "https://api.deepseek.com/responses");
|
|
94
|
+
assert.equal(calls.at(-1).headers.get("authorization"), "Bearer deepseek-key");
|
|
95
|
+
const deepseekHistory = JSON.parse(calls.at(-1).body).input;
|
|
96
|
+
assert.deepEqual(deepseekHistory[0], foreign);
|
|
97
|
+
assert.equal(deepseekHistory[1].type, "message");
|
|
98
|
+
assert.equal(deepseekHistory[2].content[1].text, "42");
|
|
99
|
+
for (const providerId of ["zai", "zai-coding-plan"]) {
|
|
100
|
+
const result = await fetch(`${adapter.baseUrl}/${providerId}/responses`, { method: "POST",
|
|
101
|
+
headers: { authorization: `Bearer ${providerId}-key` }, body: JSON.stringify({ model: "glm-5.3", input: [foreign] }) });
|
|
102
|
+
assert.equal(await result.text(), "data: one\n\ndata: two\n\n");
|
|
103
|
+
assert.equal(calls.at(-1).url, "https://api.z.ai/api/v1/responses");
|
|
104
|
+
assert.equal(calls.at(-1).headers.get("authorization"), `Bearer ${providerId}-key`);
|
|
105
|
+
assert.deepEqual(JSON.parse(calls.at(-1).body).input, [foreign]);
|
|
106
|
+
}
|
|
107
|
+
const count = calls.length;
|
|
108
|
+
for (const url of ["/chatgpt/arbitrary", "/https://example.com/responses", "/chatgpt/responses?redirect=https://example.com"]) {
|
|
109
|
+
const result = await fetch(`${adapter.baseUrl}${url}`);
|
|
110
|
+
assert.equal(result.status, 404);
|
|
111
|
+
}
|
|
112
|
+
assert.equal((await fetch(adapter.baseUrl.replace(/\/[0-9a-f-]{36}\//u, "/wrong-token/") + "/chatgpt/models")).status, 404);
|
|
113
|
+
assert.equal((await fetch(adapter.baseUrl.replace(/\/v2$/u, "") + "/chatgpt/models")).status, 404);
|
|
114
|
+
assert.equal(calls.length, count);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("bad or oversized history fails visibly before upstream admission", async (t) => {
|
|
118
|
+
let called = false;
|
|
119
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), maxRequestBytes: 512,
|
|
120
|
+
fetchImpl: async () => { called = true; throw new Error("must not send"); } });
|
|
121
|
+
t.after(() => adapter.close());
|
|
122
|
+
for (const [body, headers, status] of [
|
|
123
|
+
["not JSON", {}, 400],
|
|
124
|
+
["x".repeat(600), {}, 413],
|
|
125
|
+
[gzipSync("x".repeat(10000)), { "content-encoding": "gzip" }, 400],
|
|
126
|
+
[JSON.stringify({ input: [{ ...foreign, summary: [{ type: "unknown", text: "preserve" }] }] }), {}, 422]
|
|
127
|
+
]) {
|
|
128
|
+
const response = await fetch(`${adapter.baseUrl}/chatgpt/responses`, { method: "POST", body, headers });
|
|
129
|
+
assert.equal(response.status, status);
|
|
130
|
+
assert.match((await response.json()).error.message, /history/i);
|
|
131
|
+
}
|
|
132
|
+
assert.equal(called, false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the final translated request is bounded in both model-switch directions", async (t) => {
|
|
136
|
+
let attempts = 0;
|
|
137
|
+
const limit = 1024;
|
|
138
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), maxRequestBytes: limit,
|
|
139
|
+
fetchImpl: async () => { attempts += 1; return new Response("unexpected"); } });
|
|
140
|
+
t.after(() => adapter.close());
|
|
141
|
+
for (const [destination, input] of [
|
|
142
|
+
["chatgpt", [{ type: "reasoning", content: [{ type: "reasoning_text", text: "x".repeat(850) }] }]],
|
|
143
|
+
["deepseek", [{ type: "custom_tool_call", call_id: "exec", name: "exec", input: "x".repeat(830) }]]
|
|
144
|
+
]) {
|
|
145
|
+
const body = JSON.stringify({ input });
|
|
146
|
+
assert.ok(Buffer.byteLength(body) <= limit);
|
|
147
|
+
assert.ok(Buffer.byteLength(JSON.stringify(translateCodexHistory({ input }, destination))) > limit);
|
|
148
|
+
const response = await fetch(`${adapter.baseUrl}/${destination}/responses`, { method: "POST", body });
|
|
149
|
+
assert.equal(response.status, 413);
|
|
150
|
+
assert.match((await response.json()).error.message, /translated history exceeds/);
|
|
151
|
+
}
|
|
152
|
+
assert.equal(attempts, 0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("context rejection and ambiguous upstream failure never replay a request in either direction", async (t) => {
|
|
156
|
+
const attempts = [];
|
|
157
|
+
const rejection = JSON.stringify({ error: { code: "context_length_exceeded", message: "Context too large" } });
|
|
158
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), fetchImpl: async (url, options) => {
|
|
159
|
+
attempts.push({ url, body: options.body });
|
|
160
|
+
if (JSON.parse(options.body).model === "connection-failed") throw new Error("delivery unknown");
|
|
161
|
+
return new Response(rejection, { status: 400, headers: { "content-type": "application/json" } });
|
|
162
|
+
} });
|
|
163
|
+
t.after(() => adapter.close());
|
|
164
|
+
for (const destination of ["deepseek", "chatgpt"]) {
|
|
165
|
+
for (const model of ["context-rejected", "connection-failed"]) {
|
|
166
|
+
const before = attempts.length;
|
|
167
|
+
const response = await fetch(`${adapter.baseUrl}/${destination}/responses`, { method: "POST",
|
|
168
|
+
body: JSON.stringify({ model, input: [{ type: "message", role: "user", content: "Implement it once." }] }) });
|
|
169
|
+
assert.equal(response.status, model === "context-rejected" ? 400 : 502);
|
|
170
|
+
if (model === "context-rejected") assert.equal(await response.text(), rejection);
|
|
171
|
+
else await response.text();
|
|
172
|
+
assert.equal(attempts.length, before + 1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("native disconnection aborts upstream once without retrying or buffering the stream", { timeout: 5000 }, async (t) => {
|
|
178
|
+
const aborted = Promise.withResolvers();
|
|
179
|
+
let attempts = 0;
|
|
180
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), fetchImpl: async (_url, { signal }) => {
|
|
181
|
+
attempts += 1;
|
|
182
|
+
signal.addEventListener("abort", () => aborted.resolve(), { once: true });
|
|
183
|
+
return new Response(new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("data: first\n\n")); } }));
|
|
184
|
+
} });
|
|
185
|
+
t.after(() => adapter.close());
|
|
186
|
+
await new Promise((resolve, reject) => {
|
|
187
|
+
const req = request(`${adapter.baseUrl}/chatgpt/responses`, { method: "POST" }, (res) => {
|
|
188
|
+
res.once("data", () => { req.destroy(); resolve(); });
|
|
189
|
+
});
|
|
190
|
+
req.once("error", reject);
|
|
191
|
+
req.end('{"input":[]}');
|
|
192
|
+
});
|
|
193
|
+
await aborted.promise;
|
|
194
|
+
assert.equal(attempts, 1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("upstream stream failure closes native delivery and leaves the adapter usable", async (t) => {
|
|
198
|
+
let attempts = 0;
|
|
199
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), fetchImpl: async () => {
|
|
200
|
+
attempts += 1;
|
|
201
|
+
if (attempts > 1) return new Response("next request");
|
|
202
|
+
return new Response(new ReadableStream({ start(controller) { controller.error(new Error("private upstream detail")); } }));
|
|
203
|
+
} });
|
|
204
|
+
t.after(() => adapter.close());
|
|
205
|
+
await assert.rejects(async () => (await fetch(`${adapter.baseUrl}/chatgpt/responses`, { method: "POST", body: '{"input":[]}' })).text());
|
|
206
|
+
assert.equal(await (await fetch(`${adapter.baseUrl}/chatgpt/models`)).text(), "next request");
|
|
207
|
+
assert.equal(attempts, 2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("native OpenAI account selects its upstream without changing provider or external model credentials", async () => {
|
|
211
|
+
const baseUrl = `http://127.0.0.1:23456/${randomUUID()}/v2`;
|
|
212
|
+
for (const type of ["chatgpt", "apiKey"]) {
|
|
213
|
+
const params = await prepareCodexHistory({ modelProvider: "openai", config: { web_search: "disabled" } }, {
|
|
214
|
+
request: async (method) => { assert.equal(method, "account/read"); return { account: { type } }; }
|
|
215
|
+
}, { baseUrl });
|
|
216
|
+
assert.equal(params.modelProvider, "openai");
|
|
217
|
+
assert.equal(params.config.openai_base_url, `${baseUrl}/${type}`);
|
|
218
|
+
assert.equal(params.config.web_search, "disabled");
|
|
219
|
+
}
|
|
220
|
+
const external = { modelProvider: "deepseek", config: { "model_providers.deepseek": { experimental_bearer_token: "external-key" } } };
|
|
221
|
+
assert.equal(await prepareCodexHistory(external, { request() { throw new Error("must not read OpenAI auth"); } }, { baseUrl }), external);
|
|
222
|
+
const curated = { modelProvider: "deepseek", config: { "model_providers.deepseek": {
|
|
223
|
+
base_url: "https://api.deepseek.com/", experimental_bearer_token: "external-key", wire_api: "responses"
|
|
224
|
+
}, web_search: "disabled" } };
|
|
225
|
+
const translated = await prepareCodexHistory(curated, { request() { throw new Error("must not read OpenAI auth"); } }, { baseUrl });
|
|
226
|
+
assert.equal(translated.config["model_providers.deepseek"].base_url, `${baseUrl}/deepseek`);
|
|
227
|
+
assert.equal(translated.config["model_providers.deepseek"].experimental_bearer_token, "external-key");
|
|
228
|
+
assert.equal(translated.config.web_search, "disabled");
|
|
229
|
+
assert.equal(curated.config["model_providers.deepseek"].base_url, "https://api.deepseek.com/");
|
|
230
|
+
await assert.rejects(prepareCodexHistory({ modelProvider: "openai" }, { request: async () => ({ account: null }) }, { baseUrl }), /Reconnect/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async function compactedFixture(t, { maxRequestBytes } = {}) {
|
|
235
|
+
const home = await mkdtemp(path.join(os.tmpdir(), "jskit-compacted-history-"));
|
|
236
|
+
t.after(() => rm(home, { recursive: true, force: true }));
|
|
237
|
+
const id = randomUUID();
|
|
238
|
+
const directory = path.join(home, "sessions", "2026", "09", "25");
|
|
239
|
+
await mkdir(directory, { recursive: true });
|
|
240
|
+
const historyPath = path.join(directory, `rollout-2026-09-25T00-00-00-${id}.jsonl`);
|
|
241
|
+
const compacted = { type: "compaction", encrypted_content: "exact-native-compaction" };
|
|
242
|
+
const records = [
|
|
243
|
+
{ type: "session_meta", payload: { id } },
|
|
244
|
+
{ type: "response_item", payload: { type: "message", role: "developer", content: [{ type: "input_text", text: "OLD_DEVELOPER_INSTRUCTIONS" }] } },
|
|
245
|
+
{ type: "response_item", payload: { type: "message", role: "user", content: [{ type: "input_text", text: "Currency AUD.\nKeep this exact spacing: two." }] } },
|
|
246
|
+
{ type: "response_item", payload: { type: "custom_tool_call", call_id: "t1", name: "exec", input: "Read fixture.json" } },
|
|
247
|
+
{ type: "response_item", payload: { type: "custom_tool_call_output", call_id: "t1", output: "fixtureRevision: RANDOM_TOOL_FACT" } },
|
|
248
|
+
{ type: "response_item", payload: foreign },
|
|
249
|
+
{ type: "compacted", payload: { replacement_history: [compacted] } },
|
|
250
|
+
{ type: "response_item", payload: { type: "message", role: "user", content: [{ type: "input_text", text: "AFTER_BOUNDARY" }] } }
|
|
251
|
+
];
|
|
252
|
+
const save = (rows = records, tail = "") => writeFile(historyPath, rows.map((row) => JSON.stringify(row)).join("\n") + "\n" + tail);
|
|
253
|
+
await save();
|
|
254
|
+
const calls = [];
|
|
255
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), codexHome: home, maxRequestBytes, fetchImpl: async (url, options) => {
|
|
256
|
+
calls.push({ url, ...options, body: options.body && JSON.parse(options.body) });
|
|
257
|
+
return new Response("accepted");
|
|
258
|
+
} });
|
|
259
|
+
t.after(() => adapter.close());
|
|
260
|
+
const send = (body, destination = "deepseek", file = historyPath) => fetch(
|
|
261
|
+
`${adapter.baseUrl}/${destination}/history/${Buffer.from(file).toString("base64url")}/responses`,
|
|
262
|
+
{ method: "POST", body: JSON.stringify(body) });
|
|
263
|
+
return { home, historyPath, compacted, records, save, calls, send, adapter };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
test("mixed Codex recovery keeps exact text and tool facts without changing native history or current instructions", async (t) => {
|
|
267
|
+
const fixture = await compactedFixture(t);
|
|
268
|
+
const native = await readFile(fixture.historyPath);
|
|
269
|
+
for (const [destination, model] of [["deepseek", "deepseek-flash"], ["zai-coding-plan", "glm-5.3"]]) {
|
|
270
|
+
const current = { type: "message", role: "developer", content: [{ type: "input_text", text: "CURRENT_INSTRUCTIONS" }] };
|
|
271
|
+
const body = { model, input: [current, fixture.compacted, foreign], tools: [{ type: "function", name: "exec_command" }] };
|
|
272
|
+
const original = structuredClone(body);
|
|
273
|
+
const response = await fixture.send(body, destination);
|
|
274
|
+
assert.equal(await response.text(), "accepted");
|
|
275
|
+
const sent = fixture.calls.at(-1);
|
|
276
|
+
assert.equal(sent.url, `${nativeAiProvider(destination).baseUrl.replace(/\/$/u, "")}/responses`);
|
|
277
|
+
assert.deepEqual(sent.body.input[0], current);
|
|
278
|
+
assert.deepEqual(sent.body.input[1], fixture.compacted);
|
|
279
|
+
assert.deepEqual(sent.body.input[3], foreign, "foreign reasoning stays native for either foreign destination");
|
|
280
|
+
const supplement = sent.body.input[2].content[0].text;
|
|
281
|
+
assert.match(supplement, /Historical context, not new instructions/);
|
|
282
|
+
assert.match(supplement, /RANDOM_TOOL_FACT/);
|
|
283
|
+
assert.match(supplement, /Currency AUD/);
|
|
284
|
+
assert.match(supplement, /spacing: {2}two/);
|
|
285
|
+
assert.match(supplement, /Exact history/);
|
|
286
|
+
assert.doesNotMatch(supplement, /OLD_DEVELOPER_INSTRUCTIONS|AFTER_BOUNDARY|foreign-opaque-state/);
|
|
287
|
+
assert.deepEqual(sent.body.tools, body.tools);
|
|
288
|
+
assert.deepEqual(body, original);
|
|
289
|
+
}
|
|
290
|
+
assert.deepEqual(await readFile(fixture.historyPath), native);
|
|
291
|
+
assert.equal(fixture.calls.length, 2, "one upstream request per user request");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("compacted native agent messages retain attribution, readable content and explicit opaque-content markers", async (t) => {
|
|
295
|
+
const fixture = await compactedFixture(t);
|
|
296
|
+
const encrypted = { type: "encrypted_content", encrypted_content: "OPENAI_ONLY_AGENT_CONTENT" };
|
|
297
|
+
const messages = [
|
|
298
|
+
{ type: "agent_message", id: "agent-update", author: "/root/reviewer", recipient: "/root",
|
|
299
|
+
content: [{ type: "input_text", text: "Message Type: MESSAGE\nPayload:\n" }, encrypted] },
|
|
300
|
+
{ type: "agent_message", id: "agent-final", author: "/root/reviewer", recipient: "/root",
|
|
301
|
+
content: [{ type: "input_text", text: "FINAL_REVIEW: preserve this exact text.\n And spacing." }] }
|
|
302
|
+
];
|
|
303
|
+
await fixture.save([fixture.records[0], ...messages.map((payload) => ({ type: "response_item", payload })), ...fixture.records.slice(1)]);
|
|
304
|
+
const native = await readFile(fixture.historyPath);
|
|
305
|
+
for (const [destination, model] of [["deepseek", "deepseek-flash"], ["zai-coding-plan", "glm-5.3"]]) {
|
|
306
|
+
const response = await fixture.send({ model, input: [fixture.compacted] }, destination);
|
|
307
|
+
assert.equal(await response.text(), "accepted");
|
|
308
|
+
const supplement = fixture.calls.at(-1).body.input[1].content[0].text;
|
|
309
|
+
const recovered = JSON.parse(supplement.slice(supplement.indexOf("\n") + 1));
|
|
310
|
+
assert.equal(recovered[0].type, "agent_message");
|
|
311
|
+
assert.equal(recovered[0].author, "/root/reviewer");
|
|
312
|
+
assert.equal(recovered[0].recipient, "/root");
|
|
313
|
+
assert.deepEqual(recovered[0].content[0], messages[0].content[0]);
|
|
314
|
+
assert.match(recovered[0].content[1].text, /Encrypted agent-message content is unavailable/);
|
|
315
|
+
assert.deepEqual(recovered[1].content, messages[1].content);
|
|
316
|
+
assert.doesNotMatch(supplement, /OPENAI_ONLY_AGENT_CONTENT/);
|
|
317
|
+
}
|
|
318
|
+
assert.deepEqual(await readFile(fixture.historyPath), native);
|
|
319
|
+
for (const content of [[{ ...encrypted, encrypted_content: null }], [{ type: "unknown_agent_content" }]]) {
|
|
320
|
+
await fixture.save([fixture.records[0], { type: "response_item", payload: { ...messages[0], content } }, ...fixture.records.slice(1)]);
|
|
321
|
+
const response = await fixture.send({ model: "deepseek-flash", input: [fixture.compacted] });
|
|
322
|
+
assert.equal(response.status, 422);
|
|
323
|
+
assert.match((await response.json()).error.message, /not supported/);
|
|
324
|
+
}
|
|
325
|
+
assert.equal(fixture.calls.length, 2, "malformed or unknown content never reaches the provider");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("compacted screenshots stay visible and associated with their original messages and tool results", async (t) => {
|
|
329
|
+
const fixture = await compactedFixture(t);
|
|
330
|
+
const images = [
|
|
331
|
+
{ type: "input_image", image_url: "data:image/png;base64,first-image", detail: "original" },
|
|
332
|
+
{ type: "input_image", image_url: "data:image/png;base64,second-image", detail: "high" }
|
|
333
|
+
];
|
|
334
|
+
const before = [
|
|
335
|
+
{ type: "response_item", payload: { type: "message", role: "user", content: [
|
|
336
|
+
{ type: "input_text", text: "Compare this screenshot." }, images[0]
|
|
337
|
+
] } },
|
|
338
|
+
{ type: "response_item", payload: { type: "custom_tool_call_output", call_id: "screenshot-result", output: [
|
|
339
|
+
{ type: "input_text", text: "Screenshot after the change." }, images[1]
|
|
340
|
+
] } },
|
|
341
|
+
{ type: "response_item", payload: { type: "message", role: "assistant", content: [
|
|
342
|
+
{ type: "output_text", text: "Preserve a large, valid text history. ".repeat(30000) }
|
|
343
|
+
] } }
|
|
344
|
+
];
|
|
345
|
+
await fixture.save([fixture.records[0], ...before, ...fixture.records.slice(1)]);
|
|
346
|
+
const native = await readFile(fixture.historyPath);
|
|
347
|
+
const response = await fixture.send({ model: "deepseek-flash", input: [fixture.compacted] });
|
|
348
|
+
assert.equal(await response.text(), "accepted");
|
|
349
|
+
const content = fixture.calls[0].body.input[1].content;
|
|
350
|
+
assert.deepEqual(content.filter(({ type }) => type === "input_image"), images);
|
|
351
|
+
const archived = JSON.parse(content[0].text.slice(content[0].text.indexOf("\n") + 1));
|
|
352
|
+
assert.deepEqual(archived[0].content, [before[0].payload.content[0], { type: "input_image", archived_image: 1 }]);
|
|
353
|
+
assert.deepEqual(archived[1].output, [before[1].payload.output[0], { type: "input_image", archived_image: 2 }]);
|
|
354
|
+
assert.equal(archived[1].call_id, "screenshot-result");
|
|
355
|
+
assert.equal(archived[2].content[0].text, before[2].payload.content[0].text);
|
|
356
|
+
assert.match(content[1].text, /Archived image 1/);
|
|
357
|
+
assert.match(content[3].text, /Archived image 2/);
|
|
358
|
+
assert.equal(content.at(-1).text, "[/Archived conversation]");
|
|
359
|
+
assert.deepEqual(await readFile(fixture.historyPath), native);
|
|
360
|
+
const unsupported = await fixture.send({ model: "glm-5.3", input: [fixture.compacted] }, "zai-coding-plan");
|
|
361
|
+
assert.equal(unsupported.status, 422);
|
|
362
|
+
assert.match((await unsupported.json()).error.message, /does not support images/);
|
|
363
|
+
assert.equal(fixture.calls.length, 1);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("single-provider and already readable compacted requests do not read native history", async (t) => {
|
|
367
|
+
const fixture = await compactedFixture(t);
|
|
368
|
+
const missing = path.join(fixture.home, "does-not-exist");
|
|
369
|
+
for (const [destination, input] of [["chatgpt", [fixture.compacted]], ["apiKey", [fixture.compacted]],
|
|
370
|
+
["deepseek", [foreign]], ["zai-coding-plan", [foreign]], ["deepseek", [{ type: "message", role: "user", content: "Native text summary" }]]]) {
|
|
371
|
+
assert.equal(await (await fixture.send({ input }, destination, missing)).text(), "accepted");
|
|
372
|
+
assert.deepEqual(fixture.calls.at(-1).body.input, input);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("repeated compaction restores through the exact latest boundary and ignores an unfinished append", async (t) => {
|
|
377
|
+
const fixture = await compactedFixture(t);
|
|
378
|
+
const latest = { type: "compaction", encrypted_content: "later-native-compaction" };
|
|
379
|
+
await fixture.save([...fixture.records, { type: "compacted", payload: { replacement_history: [latest] } }], '{"type":');
|
|
380
|
+
const response = await fixture.send({ model: "deepseek-flash", input: [latest] });
|
|
381
|
+
assert.equal(await response.text(), "accepted");
|
|
382
|
+
const supplement = fixture.calls[0].body.input[1].content[0].text;
|
|
383
|
+
assert.match(supplement, /RANDOM_TOOL_FACT/);
|
|
384
|
+
assert.match(supplement, /AFTER_BOUNDARY/);
|
|
385
|
+
assert.doesNotMatch(supplement, /exact-native-compaction/);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("foreign recovery honors the latest readable native compaction before a later encrypted boundary", async (t) => {
|
|
389
|
+
const fixture = await compactedFixture(t, { maxRequestBytes: 4096 });
|
|
390
|
+
const message = (role, text) => ({ type: "message", role, content: [{ type: role === "assistant" ? "output_text" : "input_text", text }] });
|
|
391
|
+
const image = { type: "input_image", image_url: "data:image/png;base64,retained-image", detail: "original" };
|
|
392
|
+
const retained = message("user", "Keep the approved scope and screenshot.");
|
|
393
|
+
retained.content.push(image);
|
|
394
|
+
const summary = message("user", "Native summary: feature complete; finish outstanding checks.");
|
|
395
|
+
const recentTool = { type: "function_call_output", call_id: "recent-check", output: "RECENT_CHECK_RESULT" };
|
|
396
|
+
const intermediate = { type: "compaction", encrypted_content: "intermediate-opaque-state" };
|
|
397
|
+
const latest = { type: "compaction", encrypted_content: "latest-opaque-state" };
|
|
398
|
+
await fixture.save([
|
|
399
|
+
...fixture.records,
|
|
400
|
+
{ type: "response_item", payload: message("assistant", "SUPERSEDED_OUTPUT".repeat(10000)) },
|
|
401
|
+
{ type: "compacted", payload: { message: "Older native summary", replacement_history: [message("user", "Older native summary")] } },
|
|
402
|
+
{ type: "response_item", payload: message("assistant", "SUPERSEDED_BETWEEN_SUMMARIES") },
|
|
403
|
+
{ type: "compacted", payload: { message: summary.content[0].text, replacement_history: [
|
|
404
|
+
message("developer", "OLD_DEVELOPER_INSTRUCTIONS"), retained, summary
|
|
405
|
+
] } },
|
|
406
|
+
{ type: "response_item", payload: recentTool },
|
|
407
|
+
{ type: "compacted", payload: { replacement_history: [intermediate] } },
|
|
408
|
+
{ type: "response_item", payload: message("assistant", "AFTER_INTERMEDIATE_BOUNDARY") },
|
|
409
|
+
{ type: "compacted", payload: { replacement_history: [latest] } },
|
|
410
|
+
{ type: "response_item", payload: message("user", "AFTER_TARGET_BOUNDARY") }
|
|
411
|
+
]);
|
|
412
|
+
const native = await readFile(fixture.historyPath);
|
|
413
|
+
const current = message("developer", "CURRENT_INSTRUCTIONS");
|
|
414
|
+
const request = message("user", "Implement the plan I have approved.");
|
|
415
|
+
const response = await fixture.send({ model: "deepseek-flash", input: [current, latest, request] });
|
|
416
|
+
assert.equal(await response.text(), "accepted");
|
|
417
|
+
assert.equal(fixture.calls.length, 1);
|
|
418
|
+
const input = fixture.calls[0].body.input;
|
|
419
|
+
assert.deepEqual(input[0], current);
|
|
420
|
+
assert.deepEqual(input[1], latest);
|
|
421
|
+
assert.deepEqual(input.at(-1), request);
|
|
422
|
+
const content = input[2].content;
|
|
423
|
+
assert.match(content[0].text, /Native summary: feature complete; finish outstanding checks/);
|
|
424
|
+
assert.match(content[0].text, /RECENT_CHECK_RESULT/);
|
|
425
|
+
assert.match(content[0].text, /AFTER_INTERMEDIATE_BOUNDARY/);
|
|
426
|
+
assert.doesNotMatch(content[0].text, /SUPERSEDED|Older native summary|RANDOM_TOOL_FACT|OLD_DEVELOPER_INSTRUCTIONS|AFTER_TARGET_BOUNDARY|opaque-state/);
|
|
427
|
+
assert.deepEqual(content.filter(({ type }) => type === "input_image"), [image]);
|
|
428
|
+
assert.deepEqual(await readFile(fixture.historyPath), native);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("a lifetime archive over 32 MiB recovers a small latest summary without modifying saved history", async (t) => {
|
|
432
|
+
const fixture = await compactedFixture(t, { maxRequestBytes: 4096 });
|
|
433
|
+
await fixture.save([fixture.records[0]]);
|
|
434
|
+
const old = JSON.stringify({ type: "response_item", payload: { type: "message", role: "assistant",
|
|
435
|
+
content: [{ type: "output_text", text: "SUPERSEDED_".repeat(105000) }] } }) + "\n";
|
|
436
|
+
for (let index = 0; index < 34; index += 1) await appendFile(fixture.historyPath, old);
|
|
437
|
+
const summary = { type: "message", role: "user", content: [{ type: "input_text", text: "LATEST_SUMMARY_🦘: finish the approved checks." }] };
|
|
438
|
+
await appendFile(fixture.historyPath, [
|
|
439
|
+
{ type: "compacted", payload: { message: "Readable summary", replacement_history: [summary] } },
|
|
440
|
+
{ type: "response_item", payload: { type: "function_call_output", call_id: "check", output: "RECENT_CHECK" } },
|
|
441
|
+
{ type: "compacted", payload: { replacement_history: [fixture.compacted] } }
|
|
442
|
+
].map((row) => JSON.stringify(row)).join("\n") + "\n");
|
|
443
|
+
const before = await stat(fixture.historyPath);
|
|
444
|
+
assert.ok(before.size > 32 * 1024 * 1024);
|
|
445
|
+
const response = await fixture.send({ model: "deepseek-flash", input: [fixture.compacted] });
|
|
446
|
+
assert.equal(await response.text(), "accepted");
|
|
447
|
+
const history = fixture.calls[0].body.input[1].content[0].text;
|
|
448
|
+
assert.match(history, /LATEST_SUMMARY_🦘/);
|
|
449
|
+
assert.match(history, /RECENT_CHECK/);
|
|
450
|
+
assert.doesNotMatch(history, /SUPERSEDED/);
|
|
451
|
+
const after = await stat(fixture.historyPath);
|
|
452
|
+
assert.equal(after.size, before.size);
|
|
453
|
+
assert.equal(after.mtimeMs, before.mtimeMs);
|
|
454
|
+
// Validation still covers records after the requested boundary.
|
|
455
|
+
await appendFile(fixture.historyPath, JSON.stringify(fixture.records[0]) + "\n");
|
|
456
|
+
assert.equal((await fixture.send({ model: "deepseek-flash", input: [fixture.compacted] })).status, 422);
|
|
457
|
+
assert.equal(fixture.calls.length, 1);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("unsafe, unsupported or oversized compacted histories fail before any provider request", async (t) => {
|
|
461
|
+
const fixture = await compactedFixture(t, { maxRequestBytes: 4096 });
|
|
462
|
+
const body = { model: "deepseek-flash", input: [fixture.compacted] };
|
|
463
|
+
const check = async (promise, pattern, status = 422) => {
|
|
464
|
+
const response = await promise;
|
|
465
|
+
assert.equal(response.status, status);
|
|
466
|
+
const message = (await response.json()).error.message;
|
|
467
|
+
assert.match(message, pattern);
|
|
468
|
+
assert.match(message, /Saved history has not been changed/);
|
|
469
|
+
};
|
|
470
|
+
await check(fixture.send(body, "deepseek", path.join(fixture.home, "missing")), /could not be read/);
|
|
471
|
+
await check(fixture.send({ ...body, input: [{ ...fixture.compacted, encrypted_content: "wrong-thread" }] }), /exact saved compaction/);
|
|
472
|
+
await check(fixture.send({ ...body, model: "unqualified-model" }), /not been qualified/);
|
|
473
|
+
await fixture.save([...fixture.records, { type: "event_msg", payload: { type: "thread_rolled_back", num_turns: 1 } }]);
|
|
474
|
+
await check(fixture.send(body), /after Undo/);
|
|
475
|
+
await fixture.save([{ ...fixture.records[0], payload: { id: "another-thread" } }, ...fixture.records.slice(1)]);
|
|
476
|
+
await check(fixture.send(body), /identify one supported conversation/);
|
|
477
|
+
for (const payload of [{ type: "unknown_tool" }, { type: "message", role: "user", content: [{ type: "input_audio", data: "audio" }] },
|
|
478
|
+
{ type: "message", role: "user", content: [{ type: "input_image", file_id: "foreign-provider-file" }] }]) {
|
|
479
|
+
await fixture.save([fixture.records[0], { type: "response_item", payload }, ...fixture.records.slice(1)]);
|
|
480
|
+
await check(fixture.send(body), /unsupported|non-text/);
|
|
481
|
+
}
|
|
482
|
+
await fixture.save(fixture.records, 'malformed saved record\n');
|
|
483
|
+
await check(fixture.send(body), /could not be read completely/);
|
|
484
|
+
await fixture.save([...fixture.records, fixture.records.find((row) => row.type === "compacted")]);
|
|
485
|
+
await check(fixture.send(body), /exact saved compaction/);
|
|
486
|
+
await fixture.save([{ ...fixture.records[0], payload: { ...fixture.records[0].payload, forked_from_id: randomUUID() } }, ...fixture.records.slice(1)]);
|
|
487
|
+
await check(fixture.send(body), /identify one supported conversation/);
|
|
488
|
+
await fixture.save();
|
|
489
|
+
await check(fixture.send({ ...body, input: Array(8).fill(fixture.compacted) }), /request size limit/, 413);
|
|
490
|
+
await fixture.save([fixture.records[0], { type: "response_item", payload: { type: "message", role: "user", content: "x".repeat(800_000) } }, ...fixture.records.slice(1)]);
|
|
491
|
+
await check(fixture.send(body), /request size limit/, 413);
|
|
492
|
+
await fixture.save();
|
|
493
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "jskit-outside-history-"));
|
|
494
|
+
t.after(() => rm(outside, { recursive: true, force: true }));
|
|
495
|
+
const outsidePath = path.join(outside, path.basename(fixture.historyPath));
|
|
496
|
+
await writeFile(outsidePath, await readFile(fixture.historyPath));
|
|
497
|
+
await rm(fixture.historyPath);
|
|
498
|
+
await symlink(outsidePath, fixture.historyPath);
|
|
499
|
+
await check(fixture.send(body), /outside this Codex runtime/);
|
|
500
|
+
assert.equal(fixture.calls.length, 0);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("curated foreign thread bindings reuse native metadata and recover after adapter restart", async () => {
|
|
504
|
+
const historyPath = "/home/native/sessions/2026/09/25/rollout-fixture.jsonl";
|
|
505
|
+
for (const destination of ["deepseek", "zai-coding-plan"]) {
|
|
506
|
+
const params = { modelProvider: destination, config: { [`model_providers.${destination}`]: {
|
|
507
|
+
base_url: nativeAiProvider(destination).baseUrl, experimental_bearer_token: "native-owned"
|
|
508
|
+
} } };
|
|
509
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
510
|
+
const baseUrl = `http://127.0.0.1:23456/${randomUUID()}/v2`;
|
|
511
|
+
const configured = await prepareCodexHistory(params, { request() { throw new Error("metadata already read"); } }, { baseUrl, historyPath });
|
|
512
|
+
const url = configured.config[`model_providers.${destination}`].base_url;
|
|
513
|
+
assert.equal(url, `${baseUrl}/${destination}/history/${Buffer.from(historyPath).toString("base64url")}`);
|
|
514
|
+
const restored = await prepareCodexHistory(params, { request: async (method, input) => {
|
|
515
|
+
assert.equal(method, "thread/read"); assert.deepEqual(input, { threadId: "native-thread", includeTurns: false });
|
|
516
|
+
return { thread: { path: historyPath } };
|
|
517
|
+
} }, { baseUrl, threadId: "native-thread" });
|
|
518
|
+
assert.deepEqual(restored, configured);
|
|
519
|
+
assert.equal(configured.config[`model_providers.${destination}`].experimental_bearer_token, "native-owned");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("an owned conversation resolves its native history only when foreign compaction needs it", async t => {
|
|
525
|
+
const f = await compactedFixture(t);
|
|
526
|
+
let reads = 0;
|
|
527
|
+
const calls = [];
|
|
528
|
+
const adapter = await startCodexHistoryAdapter({ token: randomUUID(), codexHome: f.home,
|
|
529
|
+
readHistoryPath: () => { reads += 1; return f.historyPath; },
|
|
530
|
+
fetchImpl: async (_url, options) => { calls.push(JSON.parse(options.body)); return new Response("accepted"); }
|
|
531
|
+
});
|
|
532
|
+
t.after(() => adapter.close());
|
|
533
|
+
for (const [destination, input] of [["chatgpt", [f.compacted]], ["deepseek", [foreign]], ["deepseek", [f.compacted]]]) {
|
|
534
|
+
const response = await fetch(`${adapter.baseUrl}/${destination}/responses`, {
|
|
535
|
+
method: "POST", body: JSON.stringify({ model: "deepseek-flash", input })
|
|
536
|
+
});
|
|
537
|
+
assert.equal(await response.text(), "accepted");
|
|
538
|
+
}
|
|
539
|
+
assert.equal(reads, 1);
|
|
540
|
+
assert.match(calls.at(-1).input[1].content[0].text, /RANDOM_TOOL_FACT/);
|
|
541
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { codexInteractiveArguments } from "../src/server/conversation/codexConfiguration.js";
|
|
4
|
+
|
|
5
|
+
const options = Object.freeze({
|
|
6
|
+
command: "codex", model: "selected-model", effort: "high",
|
|
7
|
+
disableStartupUpdates: true, bypassApprovalsAndSandbox: true, bypassHookTrust: true
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("remote Codex resume keeps server-owned permissions and the selected native thread", () => {
|
|
11
|
+
assert.deepEqual(codexInteractiveArguments({
|
|
12
|
+
...options, remoteEndpoint: "unix:///runtime/codex.sock", threadId: "retained-thread"
|
|
13
|
+
}), [
|
|
14
|
+
"codex", "-c", "check_for_update_on_startup=false",
|
|
15
|
+
"--remote", "unix:///runtime/codex.sock", "--model", "selected-model",
|
|
16
|
+
"-c", 'model_reasoning_effort="high"',
|
|
17
|
+
"--dangerously-bypass-hook-trust", "resume", "retained-thread"
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("local Codex and new remote threads retain their explicit permission selection", () => {
|
|
22
|
+
for (const input of [{}, { threadId: "local-thread" }, { remoteEndpoint: "unix:///runtime/codex.sock" }]) {
|
|
23
|
+
const args = codexInteractiveArguments({ ...options, ...input });
|
|
24
|
+
assert.ok(args.includes("--dangerously-bypass-approvals-and-sandbox"));
|
|
25
|
+
assert.ok(args.includes("--dangerously-bypass-hook-trust"));
|
|
26
|
+
assert.equal(args.includes("resume"), Boolean(input.threadId));
|
|
27
|
+
}
|
|
28
|
+
assert.equal(codexInteractiveArguments({ ...options, bypassApprovalsAndSandbox: false })
|
|
29
|
+
.includes("--dangerously-bypass-approvals-and-sandbox"), false);
|
|
30
|
+
});
|