@jskit-ai/assistant-core 0.2.7 → 0.2.9
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/package.json +5 -5
- package/package.json +18 -7
- package/src/client/browser.js +21 -0
- package/src/client/conversation/AssistantAttachmentQueue.vue +25 -108
- package/src/client/conversation/AssistantComposerActions.vue +1 -1
- package/src/client/conversation/AssistantConversationElement.vue +14 -52
- package/src/client/conversation/AssistantGoalControl.vue +13 -10
- package/src/client/conversation/AssistantPromptInput.vue +6 -91
- package/src/client/conversation/AssistantTranscript.css +505 -0
- package/src/client/conversation/AssistantTranscript.vue +24 -1197
- package/src/client/conversation/attachmentFiles.js +158 -0
- package/src/client/conversation/attachmentQueueItems.js +121 -0
- package/src/client/conversation/attachmentUploadRows.js +265 -0
- package/src/client/conversation/conversationDisplay.js +54 -0
- package/src/client/conversation/messageDelivery.js +13 -9
- package/src/client/conversation/retainedConversation.js +14 -3
- package/src/client/conversation/submitText.js +13 -2
- package/src/client/conversation/transcriptEntries.js +225 -0
- package/src/client/conversation/useAssistantAttachments.js +86 -388
- package/src/client/conversation/useAssistantQuestions.js +16 -3
- package/src/client/conversation/useAssistantSuggestions.js +16 -2
- package/src/client/conversation/useScrollToBottom.js +17 -19
- package/src/client/conversation/useTextareaAutoGrow.js +115 -0
- package/src/client/conversation/useTranscriptScrollFollow.js +525 -0
- package/src/client/lib/assistantApi.js +192 -161
- package/src/client/types.js +110 -0
- package/src/server/conversation/attachments.js +21 -6
- package/src/server/conversation/boundedToolLoop.js +1 -0
- package/src/server/conversation/claudeClient.js +71 -20
- package/src/server/conversation/claudeCommandRun.js +191 -0
- package/src/server/conversation/claudeHistory.js +7 -0
- package/src/server/conversation/claudeNativeTurn.js +708 -0
- package/src/server/conversation/claudeProcess.js +186 -112
- package/src/server/conversation/claudeProcessStart.js +214 -0
- package/src/server/conversation/claudePromptDelivery.js +183 -0
- package/src/server/conversation/claudeSessionCleanup.js +248 -0
- package/src/server/conversation/claudeTools.js +15 -3
- package/src/server/conversation/claudeTurn.js +212 -1388
- package/src/server/conversation/codexAccountReader.js +168 -0
- package/src/server/conversation/codexAuthPreflight.js +5 -4
- package/src/server/conversation/codexClient.js +18 -6
- package/src/server/conversation/codexConfiguration.js +8 -844
- package/src/server/conversation/codexConversationStops.js +377 -0
- package/src/server/conversation/codexConversationStore.js +119 -85
- package/src/server/conversation/codexConversationTurns.js +378 -0
- package/src/server/conversation/codexConversations.js +134 -866
- package/src/server/conversation/codexDetachedConversation.js +203 -0
- package/src/server/conversation/codexDetachedTurn.js +145 -104
- package/src/server/conversation/codexEvents.js +5 -200
- package/src/server/conversation/codexGoalCommands.js +14 -4
- package/src/server/conversation/codexHelperExecution.js +57 -57
- package/src/server/conversation/codexHelperThreadLedger.js +30 -288
- package/src/server/conversation/codexHelperThreadRecord.js +304 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +54 -76
- package/src/server/conversation/codexHelperThreads.js +94 -93
- package/src/server/conversation/codexHistoryAdapter.js +13 -8
- package/src/server/conversation/codexIsolation.js +672 -0
- package/src/server/conversation/codexLiveItems.js +379 -0
- package/src/server/conversation/codexMessageCommands.js +68 -286
- package/src/server/conversation/codexMessageDelivery.js +46 -37
- package/src/server/conversation/codexModelCatalog.js +86 -7
- package/src/server/conversation/codexNotificationObserver.js +81 -88
- package/src/server/conversation/codexNotificationQueue.js +21 -10
- package/src/server/conversation/codexObservationControl.js +105 -89
- package/src/server/conversation/codexOutput.js +84 -419
- package/src/server/conversation/codexProcess.js +29 -1053
- package/src/server/conversation/codexProcessLeader.js +1 -0
- package/src/server/conversation/codexPromptDispatch.js +241 -0
- package/src/server/conversation/codexProvider.js +147 -1807
- package/src/server/conversation/codexProviderAuth.js +348 -0
- package/src/server/conversation/codexProviderInvalidation.js +371 -0
- package/src/server/conversation/codexProviderInventories.js +437 -0
- package/src/server/conversation/codexProviderOwner.js +149 -957
- package/src/server/conversation/codexProviderRelease.js +465 -0
- package/src/server/conversation/codexProviderSelection.js +26 -15
- package/src/server/conversation/codexProviderThreads.js +698 -0
- package/src/server/conversation/codexProviderTurns.js +453 -0
- package/src/server/conversation/codexReasoning.js +32 -23
- package/src/server/conversation/codexRenewalCommands.js +18 -6
- package/src/server/conversation/codexRenewalThreads.js +7 -22
- package/src/server/conversation/codexResultSettlement.js +70 -88
- package/src/server/conversation/codexRunJournal.js +91 -97
- package/src/server/conversation/codexRuntime.js +426 -0
- package/src/server/conversation/codexRuntimeInventory.js +113 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +39 -13
- package/src/server/conversation/codexRuntimeStart.js +431 -0
- package/src/server/conversation/codexRuntimeStop.js +324 -0
- package/src/server/conversation/codexSessionCleanup.js +25 -10
- package/src/server/conversation/codexSessionReconciliation.js +21 -7
- package/src/server/conversation/codexThreadReadiness.js +49 -52
- package/src/server/conversation/codexThreadSnapshot.js +210 -0
- package/src/server/conversation/codexThreadStatus.js +48 -62
- package/src/server/conversation/codexTurn.js +171 -55
- package/src/server/conversation/codexTurnActivity.js +210 -0
- package/src/server/conversation/codexTurnControl.js +44 -48
- package/src/server/conversation/codexTurnEndings.js +610 -0
- package/src/server/conversation/codexTurnRecovery.js +97 -848
- package/src/server/conversation/codexTurnState.js +138 -111
- package/src/server/conversation/completedResponses.js +227 -0
- package/src/server/conversation/configuration.js +2 -2
- package/src/server/conversation/continuity.js +43 -6
- package/src/server/conversation/conversationDelivery.js +394 -0
- package/src/server/conversation/conversationEntries.js +365 -0
- package/src/server/conversation/conversationEvents.js +192 -0
- package/src/server/conversation/conversationGoals.js +187 -0
- package/src/server/conversation/conversationHandles.js +237 -0
- package/src/server/conversation/conversationReads.js +206 -0
- package/src/server/conversation/conversationRefusals.js +58 -0
- package/src/server/conversation/conversationReplacement.js +254 -0
- package/src/server/conversation/conversationSending.js +273 -0
- package/src/server/conversation/conversationStopping.js +121 -0
- package/src/server/conversation/conversationTurnDelivery.js +413 -0
- package/src/server/conversation/conversationTurns.js +511 -0
- package/src/server/conversation/deliveryTiming.js +25 -3
- package/src/server/conversation/fileStorage.js +11 -6
- package/src/server/conversation/hookBridge.js +14 -3
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/localExecution.js +82 -625
- package/src/server/conversation/nativeCommandCallbacks.js +10 -0
- package/src/server/conversation/nativeConversationLifecycle.js +379 -0
- package/src/server/conversation/nativeHistoryExport.js +27 -18
- package/src/server/conversation/normalize.js +9 -0
- package/src/server/conversation/openCodeClient.js +295 -366
- package/src/server/conversation/openCodeNativeHistory.js +141 -0
- package/src/server/conversation/openCodeNativeSession.js +79 -0
- package/src/server/conversation/openCodeProcess.js +109 -53
- package/src/server/conversation/openCodeProcessOwner.js +565 -0
- package/src/server/conversation/openCodeRuntime.js +111 -1814
- package/src/server/conversation/openCodeSessionTurns.js +547 -0
- package/src/server/conversation/openCodeTemporaryConversations.js +394 -0
- package/src/server/conversation/openCodeTools.js +28 -14
- package/src/server/conversation/openCodeTurn.js +80 -74
- package/src/server/conversation/openCodeTurnMonitor.js +388 -0
- package/src/server/conversation/processIdentity.js +582 -0
- package/src/server/conversation/provider.js +21 -6
- package/src/server/conversation/providers/api.js +268 -107
- package/src/server/conversation/providers/claude.js +29 -6
- package/src/server/conversation/providers/claudeDriver.js +325 -250
- package/src/server/conversation/providers/codex.js +69 -33
- package/src/server/conversation/providers/codexDriver.js +503 -1081
- package/src/server/conversation/providers/codexDriverConnection.js +331 -0
- package/src/server/conversation/providers/codexDriverTurns.js +464 -0
- package/src/server/conversation/providers/opencode.js +30 -4
- package/src/server/conversation/providers/opencodeDriver.js +321 -659
- package/src/server/conversation/providers/opencodeStandalone.js +475 -0
- package/src/server/conversation/providers/scoped.js +64 -19
- package/src/server/conversation/runtime.js +171 -2863
- package/src/server/conversation/storage.js +91 -50
- package/src/server/conversation/streams.js +15 -2
- package/src/server/conversation/structuredOutput.js +7 -11
- package/src/server/conversation/systemPrompt.js +13 -1
- package/src/server/conversation/tools.js +47 -15
- package/src/server/conversation/transcript.js +49 -20
- package/src/server/hosts/claudeTurn.js +2 -4
- package/src/server/hosts/codexConfiguration.js +6 -8
- package/src/server/hosts/codexEvents.js +4 -2
- package/src/server/hosts/codexProcess.js +1 -1
- package/src/server/hosts/codexProvider.js +11 -9
- package/src/server/hosts/codexTurn.js +3 -3
- package/src/server/hosts/openCodeClient.js +4 -2
- package/src/server/index.js +0 -2
- package/src/server/lib/aiConnectionClient.js +35 -25
- package/src/server/lib/ndjson.js +1 -0
- package/src/server/lib/providers/anthropicClient.js +15 -6
- package/src/server/lib/providers/common.js +10 -6
- package/src/server/lib/providers/openAiCompatibleClient.js +40 -23
- package/src/server/lib/serviceToolCatalog.js +29 -283
- package/src/server/lib/serviceToolEntries.js +270 -0
- package/src/server/types.js +78 -0
- package/src/shared/assistantResource.js +0 -23
- package/src/shared/conversation/answerChoiceSugar.js +2 -14
- package/src/shared/conversation/messageInputSugar.js +18 -0
- package/src/shared/conversation/numberedQuestionSugar.js +6 -16
- package/src/shared/conversation/pagination.js +1 -1
- package/src/shared/conversation/questions.js +1 -0
- package/src/shared/conversation/streaming.js +1 -0
- package/src/shared/index.js +0 -1
- package/src/testing/conversationStorageContract.js +20 -0
- package/test/aiConnectionClient.test.js +42 -1
- package/test/apiConversationDriver.test.js +217 -38
- package/test/assistantBoundedToolLoop.test.js +1 -1
- package/test/claudeConversationSession.test.js +15 -11
- package/test/claudeInstructionResume.test.js +1 -1
- package/test/claudeProcess.test.js +9 -9
- package/test/claudeTurn.test.js +21 -8
- package/test/codexAccountChange.test.js +5 -3
- package/test/codexConversationSession.test.js +22 -21
- package/test/codexEvents.test.js +3 -1
- package/test/codexHelperDeletion.test.js +3 -7
- package/test/codexIsolation.test.js +6 -6
- package/test/codexModelCatalog.test.js +6 -7
- package/test/codexProcess.test.js +3 -3
- package/test/codexThreadSelection.test.js +4 -1
- package/test/codexTurn.test.js +29 -20
- package/test/conversationApiFacilities.test.js +11 -9
- package/test/conversationAttachments.test.js +3 -3
- package/test/conversationConfiguration.test.js +4 -4
- package/test/conversationNativeStorage.test.js +11 -10
- package/test/conversationReplacement.test.js +13 -13
- package/test/conversationRuntime.test.js +9 -5
- package/test/conversationSession.test.js +78 -22
- package/test/conversationTools.test.js +47 -26
- package/test/fileConversationStorage.test.js +10 -3
- package/test/fixtures/codexHelperThreadLedger.js +3 -3
- package/test/frameworkActions.test.js +33 -18
- package/test/localConversationExecution.test.js +4 -4
- package/test/localExecution.test.js +1 -1
- package/test/openCodeConnectionChange.test.js +3 -2
- package/test/openCodeRuntime.test.js +8 -7
- package/test/openCodeTools.test.js +1 -1
- package/test/opencodeConversationSession.test.js +4 -3
- package/test/serviceToolCatalog.test.js +3 -8
- package/src/server/lib/assistantCompletion.js +0 -190
- package/src/server/lib/assistantToolLoop.js +0 -259
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { CODEX_APP_SERVER_PROVIDER_ID, normalizeCodexAppServerTerminalEnv } from "./codexProcess.js";
|
|
2
|
+
import { CodexAppServerJsonRpcClient } from "./codexClient.js";
|
|
3
|
+
import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { exportCodexNativeHistory } from "./codexNativeHistoryExport.js";
|
|
6
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
8
|
+
import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { prepareCodexHistory } from "./codexHistoryAdapter.js";
|
|
11
|
+
import { readStoppedNativeDatabase, retireNativeConversation } from "./nativeHistoryExport.js";
|
|
12
|
+
import { releaseSupersededCodexRuntime } from "./codexRuntimeInventory.js";
|
|
13
|
+
import { codexAppServerProviderThreadTurns, codexAppServerProviderTurnClientIds, codexAppServerProviderTurnItems, codexAppServerThreadRawValue } from "./codexThreadSnapshot.js";
|
|
14
|
+
|
|
15
|
+
/** @typedef {import("./codexProvider.js").CodexAppServerAgentProvider} CodexAppServerAgentProvider */
|
|
16
|
+
|
|
17
|
+
export const CODEX_APP_SERVER_INVALID_REQUEST_CODE = -32600;
|
|
18
|
+
|
|
19
|
+
export function codexAppServerRequestIsInvalid(error = null, method = "") {
|
|
20
|
+
const expectedMethod = normalizeAgentText(method);
|
|
21
|
+
const actualMethod = normalizeAgentText(error?.method);
|
|
22
|
+
return Number(error?.code) === CODEX_APP_SERVER_INVALID_REQUEST_CODE &&
|
|
23
|
+
(!expectedMethod || !actualMethod || actualMethod === expectedMethod);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function codexAppServerThreadIsMissing(error = null, threadId = "") {
|
|
27
|
+
const normalizedThreadId = normalizeAgentText(threadId || "").toLowerCase();
|
|
28
|
+
if (
|
|
29
|
+
!normalizedThreadId ||
|
|
30
|
+
!codexAppServerRequestIsInvalid(error, "thread/read")
|
|
31
|
+
) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Codex also uses -32600 for unrelated invalid requests, so recovery must
|
|
35
|
+
// match both the missing-thread wording and the exact durable thread id.
|
|
36
|
+
const message = normalizeAgentText(error?.message || "").toLowerCase();
|
|
37
|
+
return [
|
|
38
|
+
`thread not loaded: ${normalizedThreadId}`,
|
|
39
|
+
`thread ${normalizedThreadId} not found`,
|
|
40
|
+
`no rollout found for thread id ${normalizedThreadId}`
|
|
41
|
+
].includes(message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function retireCodexConversationHistory(provider, binding, {
|
|
45
|
+
toolHomeSource, beforeDelete, signal, errorPrefix = ""
|
|
46
|
+
} = {}) {
|
|
47
|
+
const inspect = async () => {
|
|
48
|
+
const ids = [...new Set([binding.conversationId, ...await provider.listThreadDescendants(binding.conversationId)])].sort();
|
|
49
|
+
const records = [];
|
|
50
|
+
for (const conversationId of ids) {
|
|
51
|
+
let thread;
|
|
52
|
+
try { thread = codexAppServerThreadRawValue(await provider.readThreadStatus(conversationId)); }
|
|
53
|
+
catch (error) {
|
|
54
|
+
// Native thread/read also says "not loaded" after deletion. Only
|
|
55
|
+
// an exhaustive native inventory can distinguish absence from an
|
|
56
|
+
// existing unloaded thread, including one moved to another cwd.
|
|
57
|
+
if (codexAppServerThreadIsMissing(error, conversationId) &&
|
|
58
|
+
!await provider.nativeThreadExists(conversationId, { signal })) continue;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
if (thread.id !== conversationId || thread.cwd !== binding.workdir ||
|
|
62
|
+
!["idle", "notLoaded"].includes(thread.status?.type)) {
|
|
63
|
+
throw new Error("Codex retirement requires an idle native family in the exact saved directory.");
|
|
64
|
+
}
|
|
65
|
+
if (thread.historyMode !== "paginated") {
|
|
66
|
+
throw Object.assign(new Error("Codex native retirement requires paginated history. This unsupported conversation needs operator cleanup."),
|
|
67
|
+
{ code: `${errorPrefix}codex_paginated_history_required` });
|
|
68
|
+
}
|
|
69
|
+
const relative = path.isAbsolute(thread.path || "") && toolHomeSource
|
|
70
|
+
? path.relative(path.join(toolHomeSource, ".codex"), thread.path) : "";
|
|
71
|
+
if (thread.path && (!/^(?:sessions|archived_sessions)\//u.test(relative) || !/\.jsonl(?:\.zst)?$/u.test(thread.path))) {
|
|
72
|
+
throw new Error("Codex returned an unsafe or unknown rollout path.");
|
|
73
|
+
}
|
|
74
|
+
const files = [];
|
|
75
|
+
const plainPath = thread.path?.replace(/\.zst$/u, "");
|
|
76
|
+
for (const file of plainPath ? [plainPath, `${plainPath}.zst`] : []) {
|
|
77
|
+
try {
|
|
78
|
+
const info = await lstat(file);
|
|
79
|
+
if (!info.isFile() || info.isSymbolicLink() || await realpath(file) !== path.resolve(file)) {
|
|
80
|
+
throw new Error("Codex rollout is not a regular file at its exact native path.");
|
|
81
|
+
}
|
|
82
|
+
files.push({ path: file, inode: info.ino, size: info.size, modified: info.mtimeMs });
|
|
83
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
84
|
+
}
|
|
85
|
+
records.push({ conversationId, workdir: thread.cwd, historyMode: thread.historyMode, ...(files[0] || {}), files,
|
|
86
|
+
nativePath: thread.path ?? null, status: thread.status.type,
|
|
87
|
+
...(Number.isFinite(thread.createdAt) ? { createdAt: new Date(thread.createdAt * 1000).toISOString() } : {}),
|
|
88
|
+
...(Number.isFinite(thread.updatedAt) ? { updatedAt: new Date(thread.updatedAt * 1000).toISOString() } : {}) });
|
|
89
|
+
}
|
|
90
|
+
return records;
|
|
91
|
+
};
|
|
92
|
+
return retireNativeConversation({ binding, inspect, beforeDelete,
|
|
93
|
+
exportConversation: (id, onRecord) => provider.exportThreadHistory(id, onRecord, { signal }),
|
|
94
|
+
remove: () => provider.deleteThread(binding.conversationId) });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Native Codex0.159.1 keeps goals in its separate goals_1.sqlite store.
|
|
98
|
+
// Returns its SQL fields as stored; the host owns stopped conversion eligibility,
|
|
99
|
+
// including rejecting active/blocked/limited goals and resolving SQLite home.
|
|
100
|
+
export async function readCodexNativeGoal(databasePath, { threadId, signal } = {}) {
|
|
101
|
+
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(threadId || "") ||
|
|
102
|
+
path.basename(databasePath || "") !== "goals_1.sqlite") {
|
|
103
|
+
throw new TypeError("Codex goal inspection requires its exact thread ID and supported goals_1.sqlite path.");
|
|
104
|
+
}
|
|
105
|
+
return readStoppedNativeDatabase(databasePath, database => {
|
|
106
|
+
const columns = database.prepare("PRAGMA table_info(thread_goals)").all();
|
|
107
|
+
const text = ["thread_id", "goal_id", "objective", "status"];
|
|
108
|
+
const integers = ["token_budget", "tokens_used", "time_used_seconds", "created_at_ms", "updated_at_ms"];
|
|
109
|
+
if (!columns.some(column => column.name === "thread_id" && column.pk === 1) ||
|
|
110
|
+
text.some(name => !columns.some(column => column.name === name && column.type === "TEXT")) ||
|
|
111
|
+
integers.some(name => !columns.some(column => column.name === name && column.type === "INTEGER"))) {
|
|
112
|
+
throw new Error("Codex's saved native goal schema is unsupported; no history was changed.");
|
|
113
|
+
}
|
|
114
|
+
const rows = database.prepare(`SELECT thread_id, goal_id, objective, status, token_budget,
|
|
115
|
+
tokens_used, time_used_seconds, created_at_ms, updated_at_ms
|
|
116
|
+
FROM thread_goals WHERE thread_id = ? LIMIT 2`).all(threadId);
|
|
117
|
+
if (!rows.length) return null;
|
|
118
|
+
const row = rows[0];
|
|
119
|
+
if (rows.length !== 1 || row.thread_id !== threadId ||
|
|
120
|
+
!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(row.goal_id || "") ||
|
|
121
|
+
typeof row.objective !== "string" ||
|
|
122
|
+
!["active", "paused", "blocked", "usage_limited", "budget_limited", "complete"].includes(row.status) ||
|
|
123
|
+
row.token_budget !== null && (!Number.isSafeInteger(row.token_budget) || row.token_budget < 0) ||
|
|
124
|
+
["tokens_used", "time_used_seconds", "created_at_ms", "updated_at_ms"].some(key =>
|
|
125
|
+
!Number.isSafeInteger(row[key]) || row[key] < 0)) {
|
|
126
|
+
throw new Error("Codex's saved native goal is unsupported or ambiguous; no history was changed.");
|
|
127
|
+
}
|
|
128
|
+
return row;
|
|
129
|
+
}, { signal });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @param {{ readThread: (threadId: string) => Promise<unknown> }} provider */
|
|
133
|
+
export async function codexAppServerThreadHasReadableHistory(provider, threadId = "") {
|
|
134
|
+
const normalizedThreadId = normalizeAgentText(threadId);
|
|
135
|
+
if (!normalizedThreadId) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
await provider.readThread(normalizedThreadId);
|
|
140
|
+
return true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (codexAppServerRequestIsInvalid(error, "thread/read")) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The caller has already checked the requested thread against its current
|
|
150
|
+
// conversation. History can confirm this exact authored input, never infer it
|
|
151
|
+
// from an assistant item or from a failed read.
|
|
152
|
+
export async function inspectCodexAppServerMessageAdmission({ provider, threadId, messageId } = {}) {
|
|
153
|
+
try {
|
|
154
|
+
const thread = await provider.readThread(threadId);
|
|
155
|
+
const accepted = codexAppServerProviderThreadTurns(thread).find((turn) =>
|
|
156
|
+
codexAppServerProviderTurnItems(turn).some((item) => item.type === "userMessage" &&
|
|
157
|
+
codexAppServerProviderTurnClientIds({ items: [item] }).includes(messageId)));
|
|
158
|
+
return { ok: true, admission: accepted ? "accepted" : "unknown", messageId, threadId,
|
|
159
|
+
turnId: normalizeAgentText(accepted?.id) };
|
|
160
|
+
} catch {
|
|
161
|
+
return { ok: true, admission: "unknown", messageId, threadId };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Native result validation only. The caller owns any interruption, absence
|
|
166
|
+
// proof and durable cleanup receipt.
|
|
167
|
+
export async function deleteCodexAppServerThread({ provider, threadId } = {}) {
|
|
168
|
+
const result = await provider.deleteThread(threadId);
|
|
169
|
+
if (!isRecord(result)) {
|
|
170
|
+
const error = new Error("Codex app-server returned an invalid thread deletion result.");
|
|
171
|
+
error.code = "codex_helper_thread_delete_unconfirmed";
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function codexHelperDeletionSupport(provider) {
|
|
178
|
+
// readable-check: allow method-probe - input validation: an application's provider may lack interruption or deletion, and helper deletion then reports which one instead of failing inside the native call
|
|
179
|
+
return { interrupts: typeof provider.interruptTurn === "function", deletes: typeof provider.deleteThread === "function" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The caller persists cleanup ownership before this operation and removes that
|
|
183
|
+
// receipt only after success. Native failure details remain local to that owner.
|
|
184
|
+
export async function deleteCodexAppServerHelperThread({ provider, threadId, turnId = "" } = {}) {
|
|
185
|
+
const support = codexHelperDeletionSupport(provider);
|
|
186
|
+
let interruptError = null;
|
|
187
|
+
if (turnId) {
|
|
188
|
+
if (support.interrupts) {
|
|
189
|
+
try {
|
|
190
|
+
await provider.interruptTurn(threadId, turnId);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
interruptError = error;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
interruptError = new Error("Codex provider cannot interrupt an active helper turn.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!support.deletes) {
|
|
199
|
+
return { ok: false, error: new Error("Codex provider cannot delete a helper thread."), interruptError };
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const result = await deleteCodexAppServerThread({ provider, threadId });
|
|
203
|
+
return {
|
|
204
|
+
deleted: true,
|
|
205
|
+
interrupted: Boolean(turnId) && !interruptError,
|
|
206
|
+
ok: true,
|
|
207
|
+
result,
|
|
208
|
+
status: "deleted",
|
|
209
|
+
threadId,
|
|
210
|
+
turnId,
|
|
211
|
+
interruptError
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
let absent = false;
|
|
215
|
+
if (codexAppServerRequestIsInvalid(error, "thread/delete")) {
|
|
216
|
+
try {
|
|
217
|
+
absent = !await codexAppServerThreadHasReadableHistory(provider, threadId);
|
|
218
|
+
} catch {
|
|
219
|
+
absent = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (absent) {
|
|
223
|
+
return {
|
|
224
|
+
deleted: false,
|
|
225
|
+
interrupted: Boolean(turnId) && !interruptError,
|
|
226
|
+
ok: true,
|
|
227
|
+
status: "notFound",
|
|
228
|
+
threadId,
|
|
229
|
+
turnId,
|
|
230
|
+
interruptError
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { ok: false, error, interruptError };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function codexApplicationToolConfiguration(tools) {
|
|
238
|
+
const dynamicTools = (tools?.schemas || []).map(({ function: tool }) => ({
|
|
239
|
+
type: "function", name: tool.name, description: tool.description, inputSchema: tool.parameters
|
|
240
|
+
}));
|
|
241
|
+
return { dynamicTools, toolSchemaIdentity: createHash("sha256").update(JSON.stringify(dynamicTools)).digest("hex") };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function assertCodexApplicationToolSchemaIdentity(threadId, savedIdentity, toolSchemaIdentity) {
|
|
245
|
+
if (threadId && (savedIdentity || codexApplicationToolConfiguration().toolSchemaIdentity) !== toolSchemaIdentity) {
|
|
246
|
+
throw new Error("This Codex conversation was created with different application tool entry points. Start a new conversation to enable or remove application tools.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// input validation: a caller's thread preparation observes main threads before they resume, and bound application
|
|
251
|
+
// tools need the schema identity the thread was created with.
|
|
252
|
+
function assertCodexThreadPreparation({ observeThread, identity, applicationTools }) {
|
|
253
|
+
if (typeof observeThread !== "function") {
|
|
254
|
+
throw new TypeError("Main Codex threads require an observer before they can resume.");
|
|
255
|
+
}
|
|
256
|
+
if (applicationTools && !identity.readToolSchemaIdentity) {
|
|
257
|
+
throw new TypeError("Bound Codex application tools require the original schema identity reader and writer.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Retained thread selection keeps observation ahead of a resume that can start work. */
|
|
262
|
+
export async function ensureCodexAppServerThread({
|
|
263
|
+
observeThread,
|
|
264
|
+
provider,
|
|
265
|
+
settings,
|
|
266
|
+
projectHooks = false,
|
|
267
|
+
identity,
|
|
268
|
+
applicationTools,
|
|
269
|
+
providerReady,
|
|
270
|
+
workdir = "",
|
|
271
|
+
onStage = () => {}
|
|
272
|
+
} = {}) {
|
|
273
|
+
assertCodexThreadPreparation({ observeThread, identity, applicationTools });
|
|
274
|
+
const normalizedWorkdir = normalizeAgentText(workdir);
|
|
275
|
+
let stageStartedAt = Date.now();
|
|
276
|
+
const availability = await provider.ensureAvailable();
|
|
277
|
+
const appServerRuntime = availability.runtime || await provider.ensureRuntime();
|
|
278
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: "runtime" });
|
|
279
|
+
const existingThreadId = identity.read(normalizedWorkdir);
|
|
280
|
+
const toolConfiguration = applicationTools ? codexApplicationToolConfiguration(applicationTools) : null;
|
|
281
|
+
if (toolConfiguration) {
|
|
282
|
+
assertCodexApplicationToolSchemaIdentity(existingThreadId, await identity.readToolSchemaIdentity(normalizedWorkdir), toolConfiguration.toolSchemaIdentity);
|
|
283
|
+
}
|
|
284
|
+
let config = null;
|
|
285
|
+
if (projectHooks) {
|
|
286
|
+
stageStartedAt = Date.now();
|
|
287
|
+
config = await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir);
|
|
288
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: "hook-config" });
|
|
289
|
+
}
|
|
290
|
+
let { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
|
|
291
|
+
if (toolConfiguration) {
|
|
292
|
+
threadSettings = { ...threadSettings, dynamicTools: toolConfiguration.dynamicTools };
|
|
293
|
+
threadStartSettings = { ...threadStartSettings, dynamicTools: toolConfiguration.dynamicTools };
|
|
294
|
+
}
|
|
295
|
+
if (providerReady) await providerReady({ provider, threadId: existingThreadId });
|
|
296
|
+
let thread = null;
|
|
297
|
+
stageStartedAt = Date.now();
|
|
298
|
+
if (existingThreadId) {
|
|
299
|
+
if (identity.pauseGoal) {
|
|
300
|
+
const { goal } = await provider.readGoal(existingThreadId);
|
|
301
|
+
if (goal?.status === "active") await provider.setGoalStatus(existingThreadId, "paused");
|
|
302
|
+
}
|
|
303
|
+
// Resuming can immediately start an active goal before the RPC returns.
|
|
304
|
+
await observeThread(existingThreadId);
|
|
305
|
+
thread = await provider.resumeThread(existingThreadId, threadSettings);
|
|
306
|
+
if (identity.pauseGoal) {
|
|
307
|
+
await identity.clearPause();
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
thread = await provider.startThread(threadStartSettings);
|
|
311
|
+
}
|
|
312
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: existingThreadId ? "resume" : "start" });
|
|
313
|
+
const threadId = normalizeAgentText(thread.id || existingThreadId);
|
|
314
|
+
if (!threadId) {
|
|
315
|
+
throw new Error("Codex app-server did not return a thread id.");
|
|
316
|
+
}
|
|
317
|
+
stageStartedAt = Date.now();
|
|
318
|
+
const recorded = await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir,
|
|
319
|
+
...(toolConfiguration ? { toolSchemaIdentity: toolConfiguration.toolSchemaIdentity } : {}) });
|
|
320
|
+
// The session no longer uses the runtime directory it recorded before (an
|
|
321
|
+
// earlier sign-in's). Releasing it is cleanup: a failure keeps the directory.
|
|
322
|
+
const superseded = normalizeAgentText(recorded?.supersededRuntimeDir);
|
|
323
|
+
if (superseded) await releaseSupersededCodexRuntime(superseded).catch(() => null);
|
|
324
|
+
if (providerReady) await providerReady({ provider, threadId });
|
|
325
|
+
onStage({ durationMs: Date.now() - stageStartedAt, stage: "identity-metadata" });
|
|
326
|
+
return {
|
|
327
|
+
appServerRuntime,
|
|
328
|
+
thread,
|
|
329
|
+
threadId
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function codexAppServerThreadRequestParams(params = {}, threadEnv = {}) {
|
|
334
|
+
const source = isRecord(params) ? params : {};
|
|
335
|
+
const config = isRecord(source.config) ? source.config : {};
|
|
336
|
+
if (Object.hasOwn(config, "shell_environment_policy")) {
|
|
337
|
+
return source;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
...source,
|
|
341
|
+
config: {
|
|
342
|
+
...config,
|
|
343
|
+
shell_environment_policy: {
|
|
344
|
+
inherit: "none",
|
|
345
|
+
set: normalizeCodexAppServerTerminalEnv(threadEnv)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Without an application adjustment, thread parameters stay as they are.
|
|
352
|
+
export function keepCodexThreadParams(params) {
|
|
353
|
+
return params;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Without an application check, a thread resumes as it is.
|
|
357
|
+
export async function resumeCodexThreadAsIs() {}
|
|
358
|
+
|
|
359
|
+
function normalizeAgentThread(value = {}) {
|
|
360
|
+
const thread = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
361
|
+
return {
|
|
362
|
+
id: normalizeAgentText(thread.id || thread.threadId),
|
|
363
|
+
provider: normalizeAgentText(thread.provider),
|
|
364
|
+
// readable-check: allow no-dual-shape - data: a thread record arrives with Codex's raw thread or is that raw thread
|
|
365
|
+
raw: thread.raw || thread
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* The provider's thread operations: start, resume, settings, goals, reads and deletion.
|
|
370
|
+
* @param {object} dependencies
|
|
371
|
+
* @param {CodexAppServerAgentProvider} dependencies.provider
|
|
372
|
+
*/
|
|
373
|
+
export function createCodexProviderThreads({ provider }) {
|
|
374
|
+
const { prepareThreadParams = keepCodexThreadParams, beforeResumeThread = resumeCodexThreadAsIs } = provider.options;
|
|
375
|
+
async function startThread({ hostContext, ...params } = {}) {
|
|
376
|
+
params = await provider.conversationRuntime.prepareInstructions({ ...params, hostContext });
|
|
377
|
+
delete params.hostContext;
|
|
378
|
+
params = await prepareThreadParams(params) || params;
|
|
379
|
+
const client = await provider.activeClient();
|
|
380
|
+
params = await withHistoryAdapter(params, client);
|
|
381
|
+
const environment = provider.options.prepareThreadEnvironment
|
|
382
|
+
? await provider.options.prepareThreadEnvironment(provider.options.threadEnv || {})
|
|
383
|
+
: provider.options.threadEnv;
|
|
384
|
+
const requestParams = provider.conversationRuntime.threadParameters(params, environment);
|
|
385
|
+
if (params.ephemeral !== true) requestParams.historyMode = "paginated";
|
|
386
|
+
const response = await provider.runRequest(
|
|
387
|
+
() => client.request("thread/start", requestParams),
|
|
388
|
+
"codex-app-server-thread-start"
|
|
389
|
+
);
|
|
390
|
+
if (hostContext) {
|
|
391
|
+
await provider.options.bindThreadContext(response?.thread?.id, hostContext, params);
|
|
392
|
+
}
|
|
393
|
+
// Native ephemeral helpers have no stored history and report "legacy".
|
|
394
|
+
// Paginated history is required only for persistent conversations.
|
|
395
|
+
if (params.ephemeral !== true && response?.thread?.historyMode === "legacy") {
|
|
396
|
+
throw Object.assign(new Error("Codex did not create the required paginated conversation history. Update Codex before starting a conversation."), {
|
|
397
|
+
code: `${provider.errorPrefix}codex_history_unsupported`
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (response?.thread?.historyMode === "paginated" && response.thread.ephemeral !== true) {
|
|
401
|
+
const threadId = normalizeAgentText(response.thread.id);
|
|
402
|
+
// A new paginated thread needs its metadata and empty rollout persisted
|
|
403
|
+
// before it can be resumed. This initializes only the just-created,
|
|
404
|
+
// empty thread; it never hydrates an existing conversation.
|
|
405
|
+
await provider.runRequest(async () => {
|
|
406
|
+
await client.request("thread/name/set", { threadId, name: threadId });
|
|
407
|
+
await client.request("thread/read", { threadId, includeTurns: true });
|
|
408
|
+
}, "codex-app-server-thread-initialize-history");
|
|
409
|
+
}
|
|
410
|
+
if (provider.options.prepareThreadEnvironment && response?.thread?.id) {
|
|
411
|
+
provider.conversationRuntime.registerThread(response.thread.id, {
|
|
412
|
+
client, executionId: normalizeAgentText(provider.runtime?.executionId),
|
|
413
|
+
params: requestParams, managed: !Object.hasOwn(params.config || {}, "shell_environment_policy")
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...normalizeAgentThread({
|
|
418
|
+
id: response?.thread?.id,
|
|
419
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
420
|
+
raw: response?.thread
|
|
421
|
+
}),
|
|
422
|
+
response
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function resumeThread(threadId = "", { hostContext, ...params } = {}) {
|
|
427
|
+
if (hostContext) await provider.options.bindThreadContext(threadId, hostContext, params);
|
|
428
|
+
params = await provider.conversationRuntime.prepareInstructions({ ...params, hostContext }, threadId);
|
|
429
|
+
delete params.hostContext;
|
|
430
|
+
params = await prepareThreadParams(params) || params;
|
|
431
|
+
await beforeResumeThread(threadId);
|
|
432
|
+
const client = await provider.activeClient();
|
|
433
|
+
let response;
|
|
434
|
+
if (provider.options.prepareThreadEnvironment) {
|
|
435
|
+
response = await withThreadEnvironment(threadId, params, (requestParams, resumed) =>
|
|
436
|
+
resumed || client.request("thread/resume", { excludeTurns: true, ...requestParams, threadId }));
|
|
437
|
+
} else {
|
|
438
|
+
params = await withHistoryAdapter(params, client, { threadId });
|
|
439
|
+
const requestParams = codexAppServerThreadRequestParams(params, provider.options.threadEnv);
|
|
440
|
+
response = await provider.runRequest(
|
|
441
|
+
() => client.request("thread/resume", {
|
|
442
|
+
excludeTurns: true,
|
|
443
|
+
...requestParams,
|
|
444
|
+
threadId: normalizeAgentText(threadId || requestParams.threadId)
|
|
445
|
+
}),
|
|
446
|
+
"codex-app-server-thread-resume"
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
...normalizeAgentThread({
|
|
451
|
+
id: response?.thread?.id,
|
|
452
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
453
|
+
raw: response?.thread
|
|
454
|
+
}),
|
|
455
|
+
response
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function withHistoryAdapter(params, client, options = {}) {
|
|
460
|
+
return prepareCodexHistory(params, client, { ...options,
|
|
461
|
+
baseUrl: provider.runtime?.historyAdapterBaseUrl, modelProviderId: provider.options.modelProviderId });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function withThreadEnvironment(threadId, params, operation) {
|
|
465
|
+
try {
|
|
466
|
+
return await provider.conversationRuntime.withThreadContext(threadId, params, operation);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (typeof error.code === "string" && error.code.startsWith("assistant_")) error.code = `${provider.errorPrefix}${error.code.slice(10)}`;
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function ensureThreadControls(threadId) {
|
|
474
|
+
if (!provider.options.prepareThreadEnvironment) return;
|
|
475
|
+
return withThreadEnvironment(threadId, {}, async (_params, resumed) => ({ ok: true, recovered: Boolean(resumed) }));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function readGoal(threadId = "", { signal } = {}) {
|
|
479
|
+
const client = await provider.activeClient();
|
|
480
|
+
const result = await provider.runRequest(
|
|
481
|
+
() => client.request("thread/goal/get", { threadId: normalizeAgentText(threadId) }, { signal }),
|
|
482
|
+
"codex-app-server-goal-read"
|
|
483
|
+
);
|
|
484
|
+
return { ...result, goal: provider.conversationRuntime.projectGoal(threadId, result.goal) };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function updateThreadSettings(threadId, { model, effort } = {}, { signal } = {}) {
|
|
488
|
+
if (!normalizeAgentText(threadId) || !normalizeAgentText(model)) {
|
|
489
|
+
throw new TypeError("Codex settings require an existing thread and a model.");
|
|
490
|
+
}
|
|
491
|
+
const client = await provider.activeClient();
|
|
492
|
+
threadId = normalizeAgentText(threadId);
|
|
493
|
+
const requested = { model, effort, ...(effort ? { config: { model_reasoning_effort: effort } } : {}) };
|
|
494
|
+
const params = await prepareThreadParams(requested) || requested;
|
|
495
|
+
const update = async selected => {
|
|
496
|
+
const current = await provider.activeClient();
|
|
497
|
+
const applied = Promise.withResolvers();
|
|
498
|
+
// A successful RPC only queues the update. Observe its applied snapshot
|
|
499
|
+
// before resuming a goal or accepting a dependent settings operation.
|
|
500
|
+
const deadline = AbortSignal.any([AbortSignal.timeout(provider.options.requestTimeoutMs || 10_000), ...(signal ? [signal] : [])]);
|
|
501
|
+
const abort = () => applied.reject(deadline.reason);
|
|
502
|
+
const unsubscribe = current.subscribe(notification => {
|
|
503
|
+
const settings = notification.params?.threadSettings;
|
|
504
|
+
if (notification.method === "thread/settings/updated" && notification.params?.threadId === threadId &&
|
|
505
|
+
settings?.model === selected.model && (!selected.effort || settings.effort === selected.effort)) applied.resolve();
|
|
506
|
+
});
|
|
507
|
+
provider.settingsWaiters.add(applied.reject);
|
|
508
|
+
deadline.addEventListener("abort", abort, { once: true });
|
|
509
|
+
void applied.promise.catch(() => {});
|
|
510
|
+
try {
|
|
511
|
+
deadline.throwIfAborted();
|
|
512
|
+
const { thread } = await provider.runRequest(() => current.request("thread/read", {
|
|
513
|
+
threadId, includeTurns: false
|
|
514
|
+
}, { signal: deadline }), "codex-app-server-settings-read");
|
|
515
|
+
// Native no-op updates may emit no settings event. A current snapshot
|
|
516
|
+
// also confirms values already applied by the provider reload.
|
|
517
|
+
if (thread?.model === selected.model && (!selected.effort || thread.reasoningEffort === selected.effort) &&
|
|
518
|
+
(!selected.modelProvider || thread.modelProvider === selected.modelProvider)) return {};
|
|
519
|
+
const result = await provider.runRequest(() => current.request("thread/settings/update", {
|
|
520
|
+
threadId, model: selected.model, ...(selected.effort ? { effort: selected.effort } : {})
|
|
521
|
+
}, { signal: deadline }), "codex-app-server-settings-update");
|
|
522
|
+
await applied.promise;
|
|
523
|
+
return result;
|
|
524
|
+
} finally {
|
|
525
|
+
unsubscribe();
|
|
526
|
+
deadline.removeEventListener("abort", abort);
|
|
527
|
+
provider.settingsWaiters.delete(applied.reject);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
if (params.modelProvider || provider.conversationRuntime.hasPendingSettings(threadId)) {
|
|
531
|
+
const { thread } = await client.request("thread/read", { threadId, includeTurns: false }, { signal });
|
|
532
|
+
if (params.modelProvider !== thread?.modelProvider || provider.conversationRuntime.hasPendingSettings(threadId)) {
|
|
533
|
+
return provider.conversationRuntime.scheduleSettings(threadId, params, update);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return update(params);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function setGoal(threadId, { objective, tokenBudget } = {}, { signal } = {}) {
|
|
540
|
+
if (typeof objective !== "string" || !objective.trim() ||
|
|
541
|
+
(tokenBudget !== undefined && (!Number.isSafeInteger(tokenBudget) || tokenBudget <= 0))) {
|
|
542
|
+
throw new TypeError("A Codex goal requires an objective and an optional positive token budget.");
|
|
543
|
+
}
|
|
544
|
+
provider.conversationRuntime.goalChanged(threadId);
|
|
545
|
+
const operation = async () => {
|
|
546
|
+
const client = await provider.activeClient();
|
|
547
|
+
return client.request("thread/goal/set", {
|
|
548
|
+
threadId: normalizeAgentText(threadId), objective: objective.trim(), status: "active",
|
|
549
|
+
...(tokenBudget === undefined ? {} : { tokenBudget })
|
|
550
|
+
}, { signal });
|
|
551
|
+
};
|
|
552
|
+
return provider.options.prepareThreadEnvironment
|
|
553
|
+
? withThreadEnvironment(threadId, {}, operation)
|
|
554
|
+
: provider.runRequest(operation, "codex-app-server-goal-set");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function setGoalStatus(threadId = "", status = "", { signal } = {}) {
|
|
558
|
+
if (!["active", "paused"].includes(status)) {
|
|
559
|
+
throw new TypeError("Invalid Codex goal status.");
|
|
560
|
+
}
|
|
561
|
+
provider.conversationRuntime.goalChanged(threadId);
|
|
562
|
+
const operation = async () => {
|
|
563
|
+
const client = await provider.activeClient();
|
|
564
|
+
return client.request("thread/goal/set", { threadId: normalizeAgentText(threadId), status }, { signal });
|
|
565
|
+
};
|
|
566
|
+
return status === "active" && provider.options.prepareThreadEnvironment
|
|
567
|
+
? withThreadEnvironment(threadId, {}, operation)
|
|
568
|
+
: provider.runRequest(operation, "codex-app-server-goal-status");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function clearGoal(threadId = "", { signal } = {}) {
|
|
572
|
+
provider.conversationRuntime.goalChanged(threadId);
|
|
573
|
+
const client = await provider.activeClient();
|
|
574
|
+
return provider.runRequest(
|
|
575
|
+
() => client.request("thread/goal/clear", { threadId: normalizeAgentText(threadId) }, { signal }),
|
|
576
|
+
"codex-app-server-goal-clear"
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function readThread(threadId = "") {
|
|
581
|
+
const client = await provider.activeClient();
|
|
582
|
+
const response = await provider.runRequest(
|
|
583
|
+
() => client.request("thread/read", {
|
|
584
|
+
includeTurns: true,
|
|
585
|
+
threadId: normalizeAgentText(threadId)
|
|
586
|
+
}),
|
|
587
|
+
"codex-app-server-thread-read"
|
|
588
|
+
);
|
|
589
|
+
return {
|
|
590
|
+
...normalizeAgentThread({
|
|
591
|
+
id: response?.thread?.id || threadId,
|
|
592
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
593
|
+
// readable-check: allow no-dual-shape - data: Codex replies to thread/read with the thread wrapped or bare
|
|
594
|
+
raw: response?.thread || response
|
|
595
|
+
}),
|
|
596
|
+
response
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function readThreadStatus(threadId = "") {
|
|
601
|
+
const client = await provider.activeClient();
|
|
602
|
+
const response = await provider.runRequest(
|
|
603
|
+
() => client.request("thread/read", {
|
|
604
|
+
includeTurns: false,
|
|
605
|
+
threadId: normalizeAgentText(threadId)
|
|
606
|
+
}),
|
|
607
|
+
"codex-app-server-thread-status"
|
|
608
|
+
);
|
|
609
|
+
return {
|
|
610
|
+
...normalizeAgentThread({
|
|
611
|
+
id: response?.thread?.id || threadId,
|
|
612
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
613
|
+
// readable-check: allow no-dual-shape - data: Codex replies to thread/read with the thread wrapped or bare
|
|
614
|
+
raw: response?.thread || response
|
|
615
|
+
}),
|
|
616
|
+
response
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function listThreadTurns(threadId = "", params = {}) {
|
|
621
|
+
const client = await provider.activeClient();
|
|
622
|
+
return provider.runRequest(
|
|
623
|
+
() => client.request("thread/turns/list", {
|
|
624
|
+
...params,
|
|
625
|
+
threadId: normalizeAgentText(threadId || params.threadId)
|
|
626
|
+
}),
|
|
627
|
+
"codex-app-server-thread-turns-list"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function exportThreadHistory(threadId, onRecord, { signal } = {}) {
|
|
632
|
+
const active = await provider.activeClient();
|
|
633
|
+
// Oversized native items must fail preservation without disconnecting the
|
|
634
|
+
// shared conversation observer. Reuse JSKIT's transport on a separate socket.
|
|
635
|
+
const client = new CodexAppServerJsonRpcClient({
|
|
636
|
+
endpoint: active.endpoint,
|
|
637
|
+
maxMessageBytes: 64 * 1024 ** 2,
|
|
638
|
+
requestTimeoutMs: 30_000,
|
|
639
|
+
WebSocketImpl: provider.options.WebSocketImpl
|
|
640
|
+
});
|
|
641
|
+
const deadline = AbortSignal.timeout(300_000);
|
|
642
|
+
const boundedSignal = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
643
|
+
boundedSignal.throwIfAborted();
|
|
644
|
+
const abort = () => client.close();
|
|
645
|
+
boundedSignal.addEventListener("abort", abort, { once: true });
|
|
646
|
+
try {
|
|
647
|
+
await client.connect();
|
|
648
|
+
boundedSignal.throwIfAborted();
|
|
649
|
+
await client.initialize({ clientInfo: provider.configuration.historyClientInfo });
|
|
650
|
+
boundedSignal.throwIfAborted();
|
|
651
|
+
return await exportCodexNativeHistory(client, normalizeAgentText(threadId), onRecord, { signal: boundedSignal, errorPrefix: provider.errorPrefix });
|
|
652
|
+
} finally {
|
|
653
|
+
boundedSignal.removeEventListener("abort", abort);
|
|
654
|
+
client.close();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function unsubscribeThread(threadId = "") {
|
|
659
|
+
const client = await provider.activeClient();
|
|
660
|
+
return provider.runRequest(
|
|
661
|
+
() => client.request("thread/unsubscribe", {
|
|
662
|
+
threadId: normalizeAgentText(threadId)
|
|
663
|
+
}),
|
|
664
|
+
"codex-app-server-thread-unsubscribe"
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function deleteThread(threadId = "") {
|
|
669
|
+
const client = await provider.activeClient();
|
|
670
|
+
return provider.runRequest(
|
|
671
|
+
() => client.request("thread/delete", {
|
|
672
|
+
threadId: normalizeAgentText(threadId)
|
|
673
|
+
}),
|
|
674
|
+
"codex-app-server-thread-delete"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return Object.freeze({
|
|
679
|
+
startThread,
|
|
680
|
+
resumeThread,
|
|
681
|
+
withHistoryAdapter,
|
|
682
|
+
withThreadEnvironment,
|
|
683
|
+
ensureThreadControls,
|
|
684
|
+
readGoal,
|
|
685
|
+
updateThreadSettings,
|
|
686
|
+
// readable-check: allow setter-seam - not a slot: setGoal sets a thread's native goal
|
|
687
|
+
setGoal,
|
|
688
|
+
// readable-check: allow setter-seam - not a slot: setGoalStatus pauses or resumes a thread's native goal
|
|
689
|
+
setGoalStatus,
|
|
690
|
+
clearGoal,
|
|
691
|
+
readThread,
|
|
692
|
+
readThreadStatus,
|
|
693
|
+
listThreadTurns,
|
|
694
|
+
exportThreadHistory,
|
|
695
|
+
unsubscribeThread,
|
|
696
|
+
deleteThread
|
|
697
|
+
});
|
|
698
|
+
}
|