@jskit-ai/assistant-core 0.1.239 → 0.1.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/examples/conversation/package.json +5 -5
  2. package/package.json +5 -5
  3. package/src/client/conversation/AssistantTranscript.vue +26 -29
  4. package/src/client/conversation/LongTextPreviewBlocks.vue +11 -0
  5. package/src/client/conversation/messageDelivery.js +8 -2
  6. package/src/client/conversation/useAssistantAttachments.js +1 -1
  7. package/src/server/conversation/agentRun.js +12 -198
  8. package/src/server/conversation/attachments.js +6 -5
  9. package/src/server/conversation/boundedToolLoop.js +160 -0
  10. package/src/server/conversation/claudeHistory.js +20 -16
  11. package/src/server/conversation/claudeProcess.js +2 -2
  12. package/src/server/conversation/claudeTurn.js +51 -40
  13. package/src/server/conversation/codexAuthPreflight.js +104 -0
  14. package/src/server/conversation/codexClient.js +11 -16
  15. package/src/server/conversation/codexConfiguration.js +112 -98
  16. package/src/server/conversation/codexConversationStore.js +189 -0
  17. package/src/server/conversation/codexConversations.js +47 -44
  18. package/src/server/conversation/codexDetachedTurn.js +12 -11
  19. package/src/server/conversation/codexEvents.js +95 -99
  20. package/src/server/conversation/codexGoalCommands.js +1 -1
  21. package/src/server/conversation/codexHelperExecution.js +3 -13
  22. package/src/server/conversation/codexHelperThreadLedger.js +7 -13
  23. package/src/server/conversation/codexHelperThreadRecovery.js +2 -16
  24. package/src/server/conversation/codexHelperThreads.js +5 -6
  25. package/src/server/conversation/codexHistoryAdapter.js +32 -26
  26. package/src/server/conversation/codexMessageCommands.js +22 -22
  27. package/src/server/conversation/codexMessageDelivery.js +12 -7
  28. package/src/server/conversation/codexModelCatalog.js +97 -0
  29. package/src/server/conversation/codexNotificationObserver.js +24 -40
  30. package/src/server/conversation/codexNotificationQueue.js +2 -3
  31. package/src/server/conversation/codexOutput.js +20 -27
  32. package/src/server/conversation/codexProcess.js +123 -127
  33. package/src/server/conversation/codexProcessLeader.js +3 -3
  34. package/src/server/conversation/codexProvider.js +31 -614
  35. package/src/server/conversation/codexProviderOwner.js +2 -2
  36. package/src/server/conversation/codexProviderSelection.js +38 -40
  37. package/src/server/conversation/codexRenewalCommands.js +13 -10
  38. package/src/server/conversation/codexRenewalThreads.js +412 -0
  39. package/src/server/conversation/codexRunJournal.js +24 -42
  40. package/src/server/conversation/codexRuntimeLifecycle.js +19 -15
  41. package/src/server/conversation/codexSessionCleanup.js +11 -10
  42. package/src/server/conversation/codexSessionReconciliation.js +6 -6
  43. package/src/server/conversation/codexThreadReadiness.js +19 -33
  44. package/src/server/conversation/codexThreadStatus.js +3 -3
  45. package/src/server/conversation/codexTurn.js +124 -297
  46. package/src/server/conversation/codexTurnRecovery.js +49 -28
  47. package/src/server/conversation/codexTurnState.js +34 -13
  48. package/src/server/conversation/commandWrapper.js +3 -0
  49. package/src/server/conversation/deliveryTiming.js +14 -5
  50. package/src/server/conversation/index.js +1 -0
  51. package/src/server/conversation/localExecution.js +13 -17
  52. package/src/server/conversation/normalize.js +8 -3
  53. package/src/server/conversation/openCodeClient.js +9 -10
  54. package/src/server/conversation/openCodeProcess.js +9 -5
  55. package/src/server/conversation/openCodeRuntime.js +10 -11
  56. package/src/server/conversation/openCodeTurn.js +43 -29
  57. package/src/server/conversation/providers/claudeDriver.js +10 -16
  58. package/src/server/conversation/providers/codexDriver.js +27 -43
  59. package/src/server/conversation/providers/opencodeDriver.js +17 -42
  60. package/src/server/conversation/providers/scoped.js +48 -4
  61. package/src/server/conversation/runtime.js +50 -49
  62. package/src/server/conversation/runtimeStateUpgrade.js +34 -22
  63. package/src/server/conversation/transcript.js +10 -10
  64. package/src/server/hosts/codexConfiguration.js +1 -0
  65. package/src/server/hosts/codexProvider.js +8 -6
  66. package/src/server/index.js +2 -1
  67. package/src/server/lib/assistantToolLoop.js +1 -153
  68. package/src/server/lib/serviceToolCatalog.js +17 -42
  69. package/src/shared/assistantResource.js +4 -2
  70. package/src/shared/conversation/attachments.js +4 -0
  71. package/src/shared/conversation/longTextBlocks.js +54 -19
  72. package/src/shared/conversation/numberedQuestionSugar.js +17 -10
  73. package/src/shared/conversation/pagination.js +4 -6
  74. package/src/shared/conversation/patches.js +1 -4
  75. package/src/shared/conversation/streaming.js +3 -11
  76. package/src/shared/conversation/turns.js +11 -0
  77. package/src/shared/index.js +1 -0
  78. package/test/assistantBoundedToolLoop.test.js +2 -2
  79. package/test/assistantResource.test.js +4 -4
  80. package/test/assistantScroll.browser.test.js +29 -6
  81. package/test/assistantSettingsResource.test.js +2 -2
  82. package/test/claudeConversationSession.test.js +20 -3
  83. package/test/claudeTurn.test.js +27 -7
  84. package/test/codexClient.test.js +20 -0
  85. package/test/codexConversationSession.test.js +5 -3
  86. package/test/codexConversationsCheckpoint.test.js +51 -0
  87. package/test/codexModelCatalog.test.js +1 -1
  88. package/test/codexProviderHome.test.js +51 -0
  89. package/test/codexProviderSelection.test.js +71 -0
  90. package/test/codexTurn.test.js +222 -6
  91. package/test/conversationAgentRun.test.js +2 -1
  92. package/test/conversationAttachments.test.js +27 -2
  93. package/test/conversationDeliveryTiming.test.js +21 -0
  94. package/test/conversationNormalize.test.js +11 -0
  95. package/test/fixtures/nestedListQuestionMessage.md +37 -0
  96. package/test/frameworkActions.test.js +2 -2
  97. package/test/longTextBlocks.test.js +102 -0
  98. package/test/messageDelivery.test.js +7 -1
  99. package/test/openCodeClient.test.js +20 -0
  100. package/test/openCodeRuntime.test.js +1 -1
  101. package/test/openCodeTurn.test.js +90 -0
  102. package/test/opencodeConversationSession.test.js +1 -1
  103. package/test/serviceToolCatalog.test.js +27 -0
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { stat } from "node:fs/promises";
3
3
  import { CodexAppServerAgentProvider } from "./codexProvider.js";
4
4
  import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
5
+ import { normalizeCodexRunText as normalizeText } from "./codexTurnState.js";
5
6
 
6
7
  // Moved from Vibe64's codexTerminal provider owner. Caller-local provider keys
7
8
  // are grouped without duplicating the inventory. Recovery and sharing always
@@ -9,7 +10,6 @@ import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
9
10
  const codexAppServerProviders = new Map();
10
11
  const codexAppServerOwnedRuntimes = new Map();
11
12
  const codexAppServerRuntimeLifecycles = new Map();
12
- const normalizeText = value => String(value || "").trim();
13
13
 
14
14
  export function codexAppServerOwnedRuntimeKey(providerKey = "", providerOptions = {}) {
15
15
  const runtimeDir = normalizeText(providerOptions?.runtimeDir);
@@ -36,7 +36,7 @@ export function codexAppServerRuntimeStopWasVerified(result = {}) {
36
36
  result?.runtimeDirRemoved === true;
37
37
  }
38
38
 
39
- async function directoryExists(filePath = "") {
39
+ export async function directoryExists(filePath = "") {
40
40
  try {
41
41
  return (await stat(filePath)).isDirectory();
42
42
  } catch (error) {
@@ -1,9 +1,10 @@
1
1
  import { normalizeCodexRunText } from "./codexTurnState.js";
2
2
  import { codexAppServerProjectHookTrustConfig } from "./codexConfiguration.js";
3
- import { createCodexAppServerModelCatalogCache } from "./codexProvider.js";
3
+ import { createCodexAppServerModelCatalogCache } from "./codexModelCatalog.js";
4
4
  import { readCodexAppServerAccountIdentity } from "./codexHelperExecution.js";
5
5
 
6
- // Private composition of unchanged operations from the original run owner.
6
+ // Provider acquisition and per-operation provider contexts for a run owner's
7
+ // managed threads, with their model catalogue and account identity reads.
7
8
  export function createCodexProviderSelection({
8
9
  providerSessions,
9
10
  turnState,
@@ -16,6 +17,25 @@ export function createCodexProviderSelection({
16
17
  cacheMs: modelCatalogCacheMs,
17
18
  errorPrefix
18
19
  });
20
+ function sessionConnection(sessionId, providerOptions) {
21
+ return providerSessions.context(normalizeCodexRunText(sessionId), providerOptions);
22
+ }
23
+
24
+ function ensureSessionProvider(sessionId, providerOptions, threadId) {
25
+ return providerSessions.owner.ensureSession(sessionConnection(sessionId, providerOptions), threadId);
26
+ }
27
+
28
+ function rememberManagedSession(sessionId, providerOptions, { executionRoot, workdir, threadId } = {}) {
29
+ const connection = sessionConnection(sessionId, providerOptions);
30
+ connection.runOwner.runtimeLifecycle.rememberManagedSession(connection.providerKey, {
31
+ providerOptions,
32
+ sessionId: normalizeCodexRunText(sessionId),
33
+ executionRoot,
34
+ threadId,
35
+ workdir
36
+ });
37
+ }
38
+
19
39
  function managedProviderKeyForThread(sessionId, workdir, threadId) {
20
40
  for (const [providerKey, managed] of providerSessions.managed) {
21
41
  const provider = providerSessions.owner.providers.get(providerKey);
@@ -54,9 +74,7 @@ export function createCodexProviderSelection({
54
74
  const provider = providerSessions.owner.providers.get(providerKey);
55
75
  if (provider) {
56
76
  return {
57
- provider: await providerSessions.owner.ensureSession(
58
- providerSessions.context(normalizedSessionId, managed.providerOptions)
59
- ),
77
+ provider: await ensureSessionProvider(normalizedSessionId, managed.providerOptions),
60
78
  providerKey,
61
79
  providerOptions: managed.providerOptions
62
80
  };
@@ -76,22 +94,18 @@ export function createCodexProviderSelection({
76
94
  const activeProvider = await ensureCodexAppServerProviderForActiveTurn(session, managedIdentity);
77
95
  const provider = activeProvider?.provider;
78
96
  if (provider) return provider;
79
- const providerOptions = await prepareProviderOptions();
80
- return await providerSessions.owner.ensureSession(
81
- providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
82
- );
97
+ return await ensureSessionProvider(sessionId, await prepareProviderOptions());
83
98
  }
84
99
 
85
- async function codexAppServerRuntimeForVisibleTerminal(sessionId, session, options, managedIdentity, prepareProviderOptions) {
100
+ async function codexAppServerRuntimeForVisibleTerminal(rawSessionId, session, options, managedIdentity, prepareProviderOptions) {
101
+ const sessionId = normalizeCodexRunText(rawSessionId);
86
102
  const activeProvider = await ensureCodexAppServerProviderForActiveTurn(session, managedIdentity);
87
103
  const providerOptions = activeProvider?.providerOptions || await prepareProviderOptions();
88
- const provider = activeProvider?.provider || await providerSessions.owner.ensureSession(
89
- providerSessions.context(normalizeCodexRunText(sessionId), providerOptions)
90
- );
104
+ const provider = activeProvider?.provider || await ensureSessionProvider(sessionId, providerOptions);
91
105
  await codexAppServerProjectHookTrustConfig(provider, options.workdir, {
92
106
  persist: true
93
107
  });
94
- const providerKey = activeProvider?.providerKey || providerSessions.context(sessionId, providerOptions).providerKey;
108
+ const providerKey = activeProvider?.providerKey || sessionConnection(sessionId, providerOptions).providerKey;
95
109
  return providerSessions.owner.acquireRuntime({
96
110
  operation: () => provider.ensureRuntime(),
97
111
  provider,
@@ -112,9 +126,7 @@ export function createCodexProviderSelection({
112
126
  );
113
127
  if (activeProvider) return { provider: activeProvider.provider || null, reused: true };
114
128
  return {
115
- provider: await providerSessions.owner.ensureSession(providerSessions.context(
116
- normalizeCodexRunText(sessionId), await context.providerOptions(currentSession)
117
- )),
129
+ provider: await ensureSessionProvider(sessionId, await context.providerOptions(currentSession)),
118
130
  reused: false
119
131
  };
120
132
  }
@@ -130,9 +142,8 @@ export function createCodexProviderSelection({
130
142
  const activeProvider = await ensureCodexAppServerProviderForActiveTurn(
131
143
  currentSession, context.managedIdentity(currentSession)
132
144
  );
133
- return activeProvider?.provider || await providerSessions.owner.ensureSession(
134
- providerSessions.context(normalizeCodexRunText(sessionId), await context.providerOptions(currentSession))
135
- );
145
+ return activeProvider?.provider ||
146
+ await ensureSessionProvider(sessionId, await context.providerOptions(currentSession));
136
147
  },
137
148
  observationOwner(threadId) {
138
149
  for (const [key, managed] of providerSessions.managed) {
@@ -150,9 +161,7 @@ export function createCodexProviderSelection({
150
161
  return {
151
162
  threadId: context.threadId,
152
163
  async acquireProvider(current) {
153
- return providerSessions.owner.ensureSession(providerSessions.context(
154
- normalizeCodexRunText(sessionId), await context.providerOptions(current)
155
- ));
164
+ return ensureSessionProvider(sessionId, await context.providerOptions(current));
156
165
  }
157
166
  };
158
167
  }
@@ -160,22 +169,11 @@ export function createCodexProviderSelection({
160
169
  function codexAppServerSessionObserverOptions(sessionId, providerOptions) {
161
170
  return {
162
171
  get providerKey() {
163
- return providerSessions.context(normalizeCodexRunText(sessionId), providerOptions).providerKey;
172
+ return sessionConnection(sessionId, providerOptions).providerKey;
164
173
  }
165
174
  };
166
175
  }
167
176
 
168
- function rememberCodexAppServerPreparedThread(sessionId, providerOptions, context, thread) {
169
- const connection = providerSessions.context(sessionId, providerOptions);
170
- connection.runOwner.runtimeLifecycle.rememberManagedSession(connection.providerKey, {
171
- providerOptions,
172
- sessionId,
173
- executionRoot: context.executionRoot,
174
- threadId: thread.threadId,
175
- workdir: context.workdir
176
- });
177
- }
178
-
179
177
 
180
178
  async function readExecutionProfileModelCatalog(sessionId, preparation) {
181
179
  if (!preparation.assistantScope) {
@@ -184,9 +182,7 @@ export function createCodexProviderSelection({
184
182
  restoration.projectRuntimeRoot, restoration.context
185
183
  ));
186
184
  }
187
- const provider = preparation.provider || await providerSessions.owner.ensureSession(
188
- providerSessions.context(normalizeCodexRunText(sessionId), await preparation.providerOptions)
189
- );
185
+ const provider = preparation.provider || await ensureSessionProvider(sessionId, await preparation.providerOptions);
190
186
  return readCodexAppServerModelCatalog(provider, { signal: preparation.signal });
191
187
  }
192
188
 
@@ -196,7 +192,7 @@ export function createCodexProviderSelection({
196
192
  restoration.projectRuntimeRoot, restoration.context
197
193
  ));
198
194
  const providerOptions = await preparation.providerOptions;
199
- const context = providerSessions.context(sessionId, providerOptions);
195
+ const context = sessionConnection(sessionId, providerOptions);
200
196
  const provider = await providerSessions.owner.withLifecycle(
201
197
  () => providerSessions.owner.providerForSession(context),
202
198
  context.providerKey,
@@ -216,6 +212,8 @@ export function createCodexProviderSelection({
216
212
  codexAppServerControlContext,
217
213
  codexAppServerObservationRecoveryContext,
218
214
  codexAppServerSessionObserverOptions,
219
- rememberCodexAppServerPreparedThread
215
+ sessionConnection,
216
+ ensureSessionProvider,
217
+ rememberManagedSession
220
218
  };
221
219
  }
@@ -1,7 +1,8 @@
1
- import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, normalizeCodexRunText, codexAppServerErrorMessage } from "./codexTurnState.js";
1
+ import { codexAppServerTurnStatusIsSuccessfulComplete, codexAppServerTurnStatusIsProviderFailure, codexAppServerErrorMessage } from "./codexTurnState.js";
2
2
  import { createCodexAppServerDetachedTurnWatcher, waitForCodexAppServerTurn } from "./codexDetachedTurn.js";
3
- import { resumeExactCodexAppServerThread, startFreshCodexAppServerThread, sendCodexAppServerPrompt } from "./codexProvider.js";
4
- import { codexAppServerProviderTurnId, codexAppServerProviderTurnError, inspectCodexAppServerRenewalThread } from "./codexEvents.js";
3
+ import { codexAppServerDeliveredTurn, sendCodexAppServerPrompt } from "./codexProvider.js";
4
+ import { resumeExactCodexAppServerThread, startFreshCodexAppServerThread } from "./codexRenewalThreads.js";
5
+ import { codexAppServerProviderTurnId, inspectCodexAppServerRenewalThread } from "./codexEvents.js";
5
6
 
6
7
  // Private composition of unchanged operations from the original run owner.
7
8
  export function createCodexRenewalCommands({
@@ -41,7 +42,9 @@ export function createCodexRenewalCommands({
41
42
 
42
43
  observer.subscribeEvents(sessionId, provider, threadId,
43
44
  selection.codexAppServerSessionObserverOptions(sessionId, context.providerOptions));
44
- selection.rememberCodexAppServerPreparedThread(sessionId, context.providerOptions, context, resumed);
45
+ selection.rememberManagedSession(sessionId, context.providerOptions, {
46
+ executionRoot: context.executionRoot, workdir: context.workdir, threadId: resumed.threadId
47
+ });
45
48
 
46
49
  let result = null;
47
50
  let reconciled = Boolean(targetTurn);
@@ -112,8 +115,8 @@ export function createCodexRenewalCommands({
112
115
  provider,
113
116
  threadId
114
117
  }, context);
115
- turnId = normalizeCodexRunText(delivery.turn?.id);
116
- status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
118
+ const delivered = codexAppServerDeliveredTurn(delivery);
119
+ ({ turnId, status } = delivered);
117
120
  if (!turnId) {
118
121
  throw errors.create("handover_identity_missing");
119
122
  }
@@ -129,7 +132,7 @@ export function createCodexRenewalCommands({
129
132
  watcher.failAfterDetailGrace(errors.create(
130
133
  "failed",
131
134
  { status },
132
- codexAppServerProviderTurnError(delivery.turn || {})
135
+ delivered.error
133
136
  ));
134
137
  } else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
135
138
  await watcher.completeNow(status);
@@ -250,8 +253,8 @@ export function createCodexRenewalCommands({
250
253
  readOnly: true,
251
254
  threadId
252
255
  }, context);
253
- turnId = normalizeCodexRunText(delivery.turn?.id);
254
- status = normalizeCodexRunText(delivery.turn?.status || delivery.turn?.raw?.status);
256
+ const delivered = codexAppServerDeliveredTurn(delivery);
257
+ ({ turnId, status } = delivered);
255
258
  if (!turnId) {
256
259
  throw errors.create("seed_identity_missing");
257
260
  }
@@ -262,7 +265,7 @@ export function createCodexRenewalCommands({
262
265
  watcher.failAfterDetailGrace(errors.create(
263
266
  "failed",
264
267
  { status },
265
- codexAppServerProviderTurnError(delivery.turn || {})
268
+ delivered.error
266
269
  ));
267
270
  } else if (codexAppServerTurnStatusIsSuccessfulComplete(status)) {
268
271
  await watcher.completeNow(status);
@@ -0,0 +1,412 @@
1
+ import { normalizeNullableText as normalizeAgentText } from "./normalize.js";
2
+ import { codexAppServerProjectHookTrustConfig, codexAppServerTextHasControlCharacters } from "./codexConfiguration.js";
3
+ import {
4
+ codexAppServerRequestIsInvalid,
5
+ CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH,
6
+ CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT
7
+ } from "./codexProvider.js";
8
+
9
+ // Renewal thread operations: resume the exact retained thread, or prove a
10
+ // genuinely fresh successor thread for one renewal operation.
11
+ const CODEX_RENEWAL_UNMATERIALIZED_THREAD_SUFFIX =
12
+ "is not materialized yet; includeTurns is unavailable before first user message";
13
+
14
+ export function defineCodexRenewalThreadIds(value = [], {
15
+ errorCode = "codex_renewal_fresh_thread_required"
16
+ } = {}) {
17
+ if (
18
+ !Array.isArray(value) ||
19
+ value.length > CODEX_APP_SERVER_THREAD_INVENTORY_MAX_COUNT
20
+ ) {
21
+ throw codexRenewalThreadError(
22
+ errorCode,
23
+ "The successor assistant thread inventory is invalid."
24
+ );
25
+ }
26
+ const threadIds = value.map((threadId) => normalizeAgentText(threadId));
27
+ if (
28
+ threadIds.some((threadId) => (
29
+ !threadId ||
30
+ threadId.length > CODEX_APP_SERVER_THREAD_INVENTORY_ID_MAX_LENGTH ||
31
+ codexAppServerTextHasControlCharacters(threadId)
32
+ )) ||
33
+ new Set(threadIds).size !== threadIds.length
34
+ ) {
35
+ throw codexRenewalThreadError(
36
+ errorCode,
37
+ "The successor assistant thread inventory contains an invalid identity."
38
+ );
39
+ }
40
+ return Object.freeze([...threadIds].sort());
41
+ }
42
+
43
+ async function listCodexRenewalThreadIds(provider, workdir, errorCode) {
44
+ if (typeof provider?.listAppServerThreadsForCwd !== "function") {
45
+ throw codexRenewalThreadError(
46
+ errorCode,
47
+ "The assistant provider cannot inventory the successor's exact session threads."
48
+ );
49
+ }
50
+ const normalizedWorkdir = normalizeAgentText(workdir);
51
+ const inventory = await provider.listAppServerThreadsForCwd({
52
+ cwd: normalizedWorkdir
53
+ });
54
+ if (normalizeAgentText(inventory?.cwd) !== normalizedWorkdir) {
55
+ throw codexRenewalThreadError(
56
+ errorCode,
57
+ "The assistant provider returned a thread inventory for a different session source."
58
+ );
59
+ }
60
+ return defineCodexRenewalThreadIds(inventory?.threadIds, { errorCode });
61
+ }
62
+
63
+ function assertCodexRenewalThreadSnapshot(threadSnapshot = null, {
64
+ threadId = "",
65
+ workdir = "",
66
+ errorCode
67
+ } = {}) {
68
+ const expectedThreadId = normalizeAgentText(threadId);
69
+ const expectedWorkdir = normalizeAgentText(workdir);
70
+ const actualThreadId = codexAppServerThreadResponseId(threadSnapshot);
71
+ const actualWorkdir = normalizeAgentText(
72
+ threadSnapshot?.cwd ||
73
+ threadSnapshot?.raw?.cwd ||
74
+ threadSnapshot?.response?.thread?.cwd
75
+ );
76
+ if (actualThreadId !== expectedThreadId || actualWorkdir !== expectedWorkdir) {
77
+ throw codexRenewalThreadError(
78
+ errorCode,
79
+ "The assistant provider could not verify the exact successor thread and session source.",
80
+ {
81
+ actualThreadId,
82
+ actualWorkdir,
83
+ expectedThreadId,
84
+ expectedWorkdir
85
+ }
86
+ );
87
+ }
88
+ return threadSnapshot;
89
+ }
90
+
91
+ function codexRenewalThreadNeedsStatusRead(error = null, threadId = "") {
92
+ const normalizedThreadId = normalizeAgentText(threadId);
93
+ return Boolean(
94
+ normalizedThreadId &&
95
+ (
96
+ (
97
+ codexAppServerRequestIsInvalid(error, "thread/read") &&
98
+ normalizeAgentText(error?.message) ===
99
+ `thread ${normalizedThreadId} ${CODEX_RENEWAL_UNMATERIALIZED_THREAD_SUFFIX}`
100
+ ) ||
101
+ (
102
+ Number(error?.code) === -32601 &&
103
+ normalizeAgentText(error?.method) === "thread/read"
104
+ )
105
+ )
106
+ );
107
+ }
108
+
109
+ async function readCodexRenewalSuccessorThreadSnapshot({
110
+ provider,
111
+ threadId = "",
112
+ workdir = "",
113
+ errorCode
114
+ } = {}) {
115
+ try {
116
+ return assertCodexRenewalThreadSnapshot(
117
+ await provider.readThread(threadId),
118
+ { threadId, workdir, errorCode }
119
+ );
120
+ } catch (error) {
121
+ if (
122
+ !codexRenewalThreadNeedsStatusRead(error, threadId) ||
123
+ typeof provider?.readThreadStatus !== "function"
124
+ ) {
125
+ throw error;
126
+ }
127
+ return assertCodexRenewalThreadSnapshot(
128
+ await provider.readThreadStatus(threadId),
129
+ { threadId, workdir, errorCode }
130
+ );
131
+ }
132
+ }
133
+
134
+ export function codexRenewalThreadError(code, message, details = {}, {
135
+ retryable = false
136
+ } = {}) {
137
+ const error = new Error(message);
138
+ error.code = code;
139
+ error.details = {
140
+ ...details,
141
+ retryable
142
+ };
143
+ error.retryable = retryable;
144
+ return error;
145
+ }
146
+
147
+ function codexAppServerThreadResponseId(thread = null, fallback = "") {
148
+ return normalizeAgentText(
149
+ thread?.id ||
150
+ thread?.response?.thread?.id ||
151
+ fallback
152
+ );
153
+ }
154
+
155
+ /** Resume only the supplied retained thread; native read failures never replace it. */
156
+ export async function resumeExactCodexAppServerThread({
157
+ expectedThreadId = "",
158
+ provider,
159
+ settings,
160
+ projectHooks = false,
161
+ workdir = "",
162
+ errorCode = "codex_renewal_thread_unreadable"
163
+ } = {}) {
164
+ const normalizedWorkdir = normalizeAgentText(workdir);
165
+ const normalizedExpectedThreadId = normalizeAgentText(expectedThreadId);
166
+ if (
167
+ typeof provider?.ensureRuntime !== "function" ||
168
+ typeof provider?.resumeThread !== "function" ||
169
+ typeof provider?.readThread !== "function"
170
+ ) {
171
+ throw codexRenewalThreadError(
172
+ errorCode,
173
+ "The old assistant provider cannot read its exact main thread.",
174
+ { expectedThreadId: normalizedExpectedThreadId }
175
+ );
176
+ }
177
+ const appServerRuntime = await provider.ensureRuntime();
178
+ const config = projectHooks
179
+ ? await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir)
180
+ : null;
181
+ const { threadSettings } = await settings(normalizedWorkdir, config);
182
+ let thread = null;
183
+ let threadSnapshot = null;
184
+ try {
185
+ thread = await provider.resumeThread(normalizedExpectedThreadId, threadSettings);
186
+ const resumedThreadId = codexAppServerThreadResponseId(thread, normalizedExpectedThreadId);
187
+ if (resumedThreadId !== normalizedExpectedThreadId) {
188
+ throw codexRenewalThreadError(
189
+ errorCode,
190
+ "The old assistant provider resumed a different thread.",
191
+ {
192
+ expectedThreadId: normalizedExpectedThreadId,
193
+ resumedThreadId
194
+ }
195
+ );
196
+ }
197
+ threadSnapshot = await provider.readThread(normalizedExpectedThreadId);
198
+ } catch (error) {
199
+ if (
200
+ error?.code === errorCode ||
201
+ codexAppServerRequestIsInvalid(error, "thread/resume") ||
202
+ codexAppServerRequestIsInvalid(error, "thread/read")
203
+ ) {
204
+ if (error?.code === errorCode) {
205
+ throw error;
206
+ }
207
+ throw codexRenewalThreadError(
208
+ errorCode,
209
+ "The old assistant thread cannot be read. Write or edit the handover manually instead.",
210
+ {
211
+ expectedThreadId: normalizedExpectedThreadId,
212
+ providerError: normalizeAgentText(error?.message)
213
+ }
214
+ );
215
+ }
216
+ throw error;
217
+ }
218
+ return Object.freeze({
219
+ appServerRuntime,
220
+ thread,
221
+ threadId: normalizedExpectedThreadId,
222
+ threadSnapshot,
223
+ threadSettings
224
+ });
225
+ }
226
+
227
+ /** Preserve the original baseline → create/adopt → durable identity → first-read order. */
228
+ export async function startFreshCodexAppServerThread({
229
+ provider,
230
+ settings,
231
+ projectHooks = false,
232
+ identity,
233
+ resumableThreadId = "",
234
+ forbiddenThreadId = "",
235
+ operationId = "",
236
+ workdir = "",
237
+ errorCode = "codex_renewal_fresh_thread_required",
238
+ applicationName = "The application"
239
+ } = {}) {
240
+ const normalizedWorkdir = normalizeAgentText(workdir);
241
+ const normalizedForbiddenThreadId = normalizeAgentText(forbiddenThreadId);
242
+ const normalizedOperationId = normalizeAgentText(operationId);
243
+ if (resumableThreadId && resumableThreadId === normalizedForbiddenThreadId) {
244
+ throw codexRenewalThreadError(
245
+ errorCode,
246
+ "The renewed session cannot reuse the old session's assistant thread.",
247
+ { forbiddenThreadId: normalizedForbiddenThreadId }
248
+ );
249
+ }
250
+ if (
251
+ typeof provider?.ensureRuntime !== "function" ||
252
+ typeof provider?.resumeThread !== "function" ||
253
+ typeof provider?.readThread !== "function" ||
254
+ typeof provider?.startThread !== "function"
255
+ ) {
256
+ throw codexRenewalThreadError(
257
+ errorCode,
258
+ "The assistant provider cannot prove a genuinely fresh renewal thread."
259
+ );
260
+ }
261
+ const appServerRuntime = await provider.ensureRuntime();
262
+ const config = projectHooks
263
+ ? await codexAppServerProjectHookTrustConfig(provider, normalizedWorkdir)
264
+ : null;
265
+ const { threadSettings, threadStartSettings } = await settings(normalizedWorkdir, config);
266
+ let fresh = false;
267
+ let thread = null;
268
+ let threadSnapshot = null;
269
+ let baselineThreadIds = Object.freeze([]);
270
+ if (resumableThreadId) {
271
+ try {
272
+ thread = await provider.resumeThread(resumableThreadId, threadSettings);
273
+ const resumedThreadId = codexAppServerThreadResponseId(thread, resumableThreadId);
274
+ if (resumedThreadId !== resumableThreadId) {
275
+ throw codexRenewalThreadError(
276
+ errorCode,
277
+ "The assistant provider resumed a different renewal thread.",
278
+ {
279
+ expectedThreadId: resumableThreadId,
280
+ resumedThreadId
281
+ }
282
+ );
283
+ }
284
+ threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
285
+ provider,
286
+ threadId: resumableThreadId,
287
+ workdir: normalizedWorkdir,
288
+ errorCode
289
+ });
290
+ } catch (error) {
291
+ if (
292
+ error?.code === errorCode ||
293
+ codexAppServerRequestIsInvalid(error, "thread/resume") ||
294
+ codexAppServerRequestIsInvalid(error, "thread/read")
295
+ ) {
296
+ if (error?.code === errorCode) {
297
+ throw error;
298
+ }
299
+ throw codexRenewalThreadError(
300
+ errorCode,
301
+ `The previously started renewal thread is no longer readable; ${applicationName} will not replace it silently.`,
302
+ {
303
+ expectedThreadId: resumableThreadId,
304
+ providerError: normalizeAgentText(error?.message)
305
+ }
306
+ );
307
+ }
308
+ throw error;
309
+ }
310
+ } else {
311
+ if (!normalizedOperationId) {
312
+ throw codexRenewalThreadError(
313
+ errorCode,
314
+ "Starting a fresh successor thread requires its exact renewal operation id."
315
+ );
316
+ }
317
+ baselineThreadIds = await identity.readBaseline();
318
+ if (!baselineThreadIds) {
319
+ baselineThreadIds = await listCodexRenewalThreadIds(
320
+ provider,
321
+ normalizedWorkdir,
322
+ errorCode
323
+ );
324
+ await identity.writeBaseline(baselineThreadIds);
325
+ }
326
+ const currentThreadIds = await listCodexRenewalThreadIds(
327
+ provider,
328
+ normalizedWorkdir,
329
+ errorCode
330
+ );
331
+ const baseline = new Set(baselineThreadIds);
332
+ const candidateThreadIds = currentThreadIds.filter((threadId) => !baseline.has(threadId));
333
+ if (candidateThreadIds.length > 1) {
334
+ throw codexRenewalThreadError(
335
+ errorCode,
336
+ `More than one unclaimed assistant thread appeared for this renewal; ${applicationName} will not guess which one is authoritative.`,
337
+ { candidateThreadIds }
338
+ );
339
+ }
340
+ if (candidateThreadIds.length === 1) {
341
+ const [candidateThreadId] = candidateThreadIds;
342
+ thread = await provider.resumeThread(candidateThreadId, threadSettings);
343
+ const resumedThreadId = codexAppServerThreadResponseId(thread, candidateThreadId);
344
+ if (resumedThreadId !== candidateThreadId) {
345
+ throw codexRenewalThreadError(
346
+ errorCode,
347
+ "The assistant provider resumed a different recovered renewal thread.",
348
+ {
349
+ expectedThreadId: candidateThreadId,
350
+ resumedThreadId
351
+ }
352
+ );
353
+ }
354
+ threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
355
+ provider,
356
+ threadId: candidateThreadId,
357
+ workdir: normalizedWorkdir,
358
+ errorCode
359
+ });
360
+ } else {
361
+ thread = await provider.startThread(threadStartSettings);
362
+ fresh = true;
363
+ }
364
+ }
365
+ const threadId = codexAppServerThreadResponseId(thread, resumableThreadId);
366
+ if (
367
+ !threadId ||
368
+ threadId === normalizedForbiddenThreadId ||
369
+ (fresh && baselineThreadIds.includes(threadId))
370
+ ) {
371
+ throw codexRenewalThreadError(
372
+ errorCode,
373
+ threadId
374
+ ? "The assistant provider reused the old session's thread instead of starting a fresh one."
375
+ : "The assistant provider did not return a fresh renewal thread id.",
376
+ {
377
+ forbiddenThreadId: normalizedForbiddenThreadId,
378
+ threadId
379
+ }
380
+ );
381
+ }
382
+ await identity.write({ appServerRuntime, threadId, workdir: normalizedWorkdir });
383
+ if (fresh) {
384
+ try {
385
+ threadSnapshot = await readCodexRenewalSuccessorThreadSnapshot({
386
+ provider,
387
+ threadId,
388
+ workdir: normalizedWorkdir,
389
+ errorCode
390
+ });
391
+ } catch (error) {
392
+ throw codexRenewalThreadError(
393
+ errorCode,
394
+ `The newly started renewal thread cannot be read; ${applicationName} will not replace it silently.`,
395
+ {
396
+ providerError: normalizeAgentText(error?.message),
397
+ threadId
398
+ },
399
+ { retryable: true }
400
+ );
401
+ }
402
+ }
403
+ return Object.freeze({
404
+ appServerRuntime,
405
+ fresh,
406
+ thread,
407
+ threadId,
408
+ threadSnapshot,
409
+ threadStartSettings,
410
+ threadSettings
411
+ });
412
+ }