@jskit-ai/assistant-core 0.2.13 → 0.2.14

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 (48) hide show
  1. package/CHANGELOG.md +346 -0
  2. package/examples/conversation/package.json +5 -5
  3. package/locales/en.json +171 -6
  4. package/package.json +5 -5
  5. package/src/server/conversation/boundedToolLoop.js +4 -2
  6. package/src/server/conversation/claudeClient.js +15 -10
  7. package/src/server/conversation/claudeCommandRun.js +16 -6
  8. package/src/server/conversation/claudeNativeTurn.js +6 -5
  9. package/src/server/conversation/claudeProcess.js +20 -8
  10. package/src/server/conversation/claudeProcessStart.js +1 -1
  11. package/src/server/conversation/claudeSessionCleanup.js +6 -10
  12. package/src/server/conversation/claudeTurn.js +2 -3
  13. package/src/server/conversation/codexAccountReader.js +15 -6
  14. package/src/server/conversation/codexConversationStops.js +3 -3
  15. package/src/server/conversation/codexConversations.js +1 -1
  16. package/src/server/conversation/codexDetachedTurn.js +4 -2
  17. package/src/server/conversation/codexGoalCommands.js +3 -1
  18. package/src/server/conversation/codexHelperExecution.js +4 -4
  19. package/src/server/conversation/codexHelperThreadRecovery.js +2 -2
  20. package/src/server/conversation/codexHistoryAdapter.js +1 -1
  21. package/src/server/conversation/codexMessageCommands.js +4 -2
  22. package/src/server/conversation/codexModelCatalog.js +1 -1
  23. package/src/server/conversation/codexObservationControl.js +1 -1
  24. package/src/server/conversation/codexProcess.js +3 -1
  25. package/src/server/conversation/codexProvider.js +4 -2
  26. package/src/server/conversation/codexProviderAuth.js +17 -9
  27. package/src/server/conversation/codexProviderInvalidation.js +6 -2
  28. package/src/server/conversation/codexProviderThreads.js +4 -2
  29. package/src/server/conversation/codexProviderTurns.js +4 -2
  30. package/src/server/conversation/codexRenewalThreads.js +2 -2
  31. package/src/server/conversation/conversationTurnDelivery.js +3 -1
  32. package/src/server/conversation/conversationTurns.js +3 -1
  33. package/src/server/conversation/openCodeProcess.js +5 -9
  34. package/src/server/conversation/openCodeRuntime.js +3 -1
  35. package/src/server/conversation/openCodeTurn.js +3 -2
  36. package/src/server/conversation/providers/claudeDriver.js +12 -4
  37. package/src/server/conversation/providers/codex.js +8 -4
  38. package/src/server/conversation/providers/codexDriver.js +23 -10
  39. package/src/server/conversation/providers/codexDriverConnection.js +18 -6
  40. package/src/server/conversation/providers/codexDriverTurns.js +3 -1
  41. package/src/server/conversation/providers/opencode.js +3 -1
  42. package/src/server/conversation/providers/opencodeDriver.js +3 -1
  43. package/src/server/conversation/providers/opencodeStandalone.js +37 -13
  44. package/src/server/conversation/providers/scoped.js +3 -1
  45. package/src/server/conversation/systemPrompt.js +3 -1
  46. package/src/server/conversation/tools.js +6 -2
  47. package/src/shared/assistantSettingsResource.js +14 -2
  48. package/test/claudeLifecycle.test.js +17 -0
@@ -24,7 +24,8 @@ function codexAppServerHelperLoginParams(auth = {}, errorPrefix = "") {
24
24
  }
25
25
  throw codexAppServerHelperAuthError(
26
26
  `${errorPrefix}codex_helper_auth_invalid`,
27
- "The selected Codex authentication mode is unsupported. Reconnect Codex and retry."
27
+ "The selected Codex authentication mode is unsupported. Reconnect Codex and retry.",
28
+ { messageCode: "assistant.codex.authModeUnsupported" }
28
29
  );
29
30
  }
30
31
 
@@ -65,7 +66,7 @@ export function codexAppServerHelperRequestError(error = null, reason = "", erro
65
66
  ) {
66
67
  return error;
67
68
  }
68
- const failure = new Error("Codex isolated helper execution failed. Retry the task.");
69
+ const failure = Object.assign(new Error("Codex isolated helper execution failed. Retry the task."), { messageCode: "assistant.codex.helperExecutionFailed", messageParams: {} });
69
70
  failure.code = `${errorPrefix}codex_helper_provider_request_failed`;
70
71
  const providerCode = Number(error?.code);
71
72
  if (Number.isSafeInteger(providerCode)) {
@@ -109,7 +110,8 @@ export function createCodexProviderAuth({ provider }) {
109
110
  if (helperAuthBlocked) {
110
111
  throw provider.accountReader.error(
111
112
  `${provider.errorPrefix}codex_helper_auth_changed`,
112
- "The selected Codex account changed during helper work. Retry the task with the current account."
113
+ "The selected Codex account changed during helper work. Retry the task with the current account.",
114
+ { messageCode: "assistant.codex.helperAccountChanged" }
113
115
  );
114
116
  }
115
117
  const current = await selectedHelperAuth();
@@ -117,7 +119,8 @@ export function createCodexProviderAuth({ provider }) {
117
119
  helperAuthBlocked = true;
118
120
  throw provider.accountReader.error(
119
121
  `${provider.errorPrefix}codex_helper_auth_changed`,
120
- "The selected Codex account changed during helper work. Retry the task with the current account."
122
+ "The selected Codex account changed during helper work. Retry the task with the current account.",
123
+ { messageCode: "assistant.codex.helperAccountChanged" }
121
124
  );
122
125
  }
123
126
  }
@@ -133,7 +136,8 @@ export function createCodexProviderAuth({ provider }) {
133
136
  ) {
134
137
  throw provider.accountReader.error(
135
138
  `${provider.errorPrefix}codex_helper_auth_changed`,
136
- "The selected Codex account changed while helper execution was starting. Retry the task."
139
+ "The selected Codex account changed while helper execution was starting. Retry the task.",
140
+ { messageCode: "assistant.codex.helperAccountChangedStarting" }
137
141
  );
138
142
  }
139
143
  helperAuth = auth;
@@ -162,7 +166,8 @@ export function createCodexProviderAuth({ provider }) {
162
166
  helperAuthBlocked = true;
163
167
  throw provider.accountReader.error(
164
168
  `${provider.errorPrefix}codex_helper_auth_unavailable`,
165
- "Codex could not activate the selected account for isolated helper work. Reconnect Codex and retry."
169
+ "Codex could not activate the selected account for isolated helper work. Reconnect Codex and retry.",
170
+ { messageCode: "assistant.codex.helperAccountActivationFailed" }
166
171
  );
167
172
  }
168
173
  }
@@ -182,7 +187,8 @@ export function createCodexProviderAuth({ provider }) {
182
187
  helperAuthBlocked = true;
183
188
  throw provider.accountReader.error(
184
189
  `${provider.errorPrefix}codex_helper_auth_refresh_rejected`,
185
- "Codex helper authentication refresh was rejected. Reconnect Codex and retry."
190
+ "Codex helper authentication refresh was rejected. Reconnect Codex and retry.",
191
+ { messageCode: "assistant.codex.helperAuthRefreshRejected" }
186
192
  );
187
193
  }
188
194
  const refreshed = await selectedHelperAuth();
@@ -193,14 +199,16 @@ export function createCodexProviderAuth({ provider }) {
193
199
  helperAuthBlocked = true;
194
200
  throw provider.accountReader.error(
195
201
  `${provider.errorPrefix}codex_helper_auth_changed`,
196
- "The selected Codex account changed during helper work. Retry the task with the current account."
202
+ "The selected Codex account changed during helper work. Retry the task with the current account.",
203
+ { messageCode: "assistant.codex.helperAccountChanged" }
197
204
  );
198
205
  }
199
206
  if (refreshed.secretSignature === currentAuth.secretSignature) {
200
207
  helperAuthBlocked = true;
201
208
  throw provider.accountReader.error(
202
209
  `${provider.errorPrefix}codex_helper_auth_refresh_pending`,
203
- "The selected Codex account has not produced a newer access token yet. Reconnect Codex and retry."
210
+ "The selected Codex account has not produced a newer access token yet. Reconnect Codex and retry.",
211
+ { messageCode: "assistant.codex.helperAccessTokenStale" }
204
212
  );
205
213
  }
206
214
  helperAuth = refreshed;
@@ -60,7 +60,9 @@ export function createCodexProviderInvalidation({ inScope, withLifecycle, forget
60
60
  let cursor;
61
61
  do {
62
62
  const page = await request(inspector, "thread/loaded/list", { limit: 100, ...(cursor ? { cursor } : {}) });
63
- if (!Array.isArray(page?.data)) throw new Error("Codex returned an invalid loaded-thread inventory. Restore its connection before retrying.");
63
+ if (!Array.isArray(page?.data)) throw Object.assign(new Error("Codex returned an invalid loaded-thread inventory. Restore its connection before retrying."), {
64
+ messageCode: "assistant.codex.loadedThreadsInvalid", messageParams: {}
65
+ });
64
66
  for (const id of page.data) {
65
67
  if (typeof id !== "string" || !id) throw new Error("Codex returned an invalid loaded-thread identity.");
66
68
  candidates.add(id);
@@ -77,7 +79,9 @@ export function createCodexProviderInvalidation({ inScope, withLifecycle, forget
77
79
  const { thread } = await request(inspector, "thread/read", { threadId: id, includeTurns: false });
78
80
  if (!thread || thread.id !== id || typeof thread.modelProvider !== "string" || !thread.modelProvider ||
79
81
  !(typeof thread.status === "string" ? thread.status : thread.status?.type)) {
80
- throw new Error("Codex returned an untrustworthy thread/provider inventory. Restore its connection before changing credentials.");
82
+ throw Object.assign(new Error("Codex returned an untrustworthy thread/provider inventory. Restore its connection before changing credentials."), {
83
+ messageCode: "assistant.codex.threadInventoryUntrusted", messageParams: {}
84
+ });
81
85
  }
82
86
  if (dedicated && thread.modelProvider !== modelProviderId) {
83
87
  throw new Error("Codex's provider credential home retains another provider thread. Close its native terminal/helper and retry; unrelated threads remain running.");
@@ -243,7 +243,9 @@ export function codexApplicationToolConfiguration(tools) {
243
243
 
244
244
  export function assertCodexApplicationToolSchemaIdentity(threadId, savedIdentity, toolSchemaIdentity) {
245
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.");
246
+ throw Object.assign(new Error("This Codex conversation was created with different application tool entry points. Start a new conversation to enable or remove application tools."), {
247
+ messageCode: "assistant.codex.toolEntryPointsChanged", messageParams: {}
248
+ });
247
249
  }
248
250
  }
249
251
 
@@ -394,7 +396,7 @@ export function createCodexProviderThreads({ provider }) {
394
396
  // Paginated history is required only for persistent conversations.
395
397
  if (params.ephemeral !== true && response?.thread?.historyMode === "legacy") {
396
398
  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`
399
+ code: `${provider.errorPrefix}codex_history_unsupported`, messageCode: "assistant.codex.paginatedConversationHistoryMissing", messageParams: {}
398
400
  });
399
401
  }
400
402
  if (response?.thread?.historyMode === "paginated" && response.thread.ephemeral !== true) {
@@ -410,7 +410,7 @@ export function createCodexProviderTurns({ provider }) {
410
410
  if (goal?.status === "active") {
411
411
  const result = await request("thread/goal/set", { threadId, status: "paused" });
412
412
  if (result.goal?.status !== "paused" || result.goal?.createdAt !== goal.createdAt || result.goal?.objective !== goal.objective) {
413
- throw new Error("Codex did not confirm that the same goal was paused.");
413
+ throw Object.assign(new Error("Codex did not confirm that the same goal was paused."), { messageCode: "assistant.codex.goalPauseUnconfirmed", messageParams: {} });
414
414
  }
415
415
  goal = result.goal;
416
416
  }
@@ -425,7 +425,9 @@ export function createCodexProviderTurns({ provider }) {
425
425
  }
426
426
  if (!turnId) throw new Error("Codex's running turn could not be identified.");
427
427
  await interruptNativeTurn(client, threadId, turnId, signal);
428
- if (await readStatus() !== "idle") throw new Error("Codex did not confirm that its turn stopped.");
428
+ if (await readStatus() !== "idle") throw Object.assign(new Error("Codex did not confirm that its turn stopped."), {
429
+ messageCode: "assistant.codex.turnStopUnconfirmed", messageParams: {}
430
+ });
429
431
  }
430
432
  return { goal };
431
433
  }
@@ -194,14 +194,14 @@ export async function resumeExactCodexAppServerThread({
194
194
  if (error?.code === errorCode) {
195
195
  throw error;
196
196
  }
197
- throw codexRenewalThreadError(
197
+ throw Object.assign(codexRenewalThreadError(
198
198
  errorCode,
199
199
  "The old assistant thread cannot be read. Write or edit the handover manually instead.",
200
200
  {
201
201
  expectedThreadId: normalizedExpectedThreadId,
202
202
  providerError: normalizeAgentText(error?.message)
203
203
  }
204
- );
204
+ ), { messageCode: "assistant.codex.oldThreadUnreadable", messageParams: {} });
205
205
  }
206
206
  throw error;
207
207
  }
@@ -297,7 +297,9 @@ function createConversationTurnDelivery({ storage, streams, events, reads, deliv
297
297
  admitted.resolve(result.value);
298
298
  return;
299
299
  }
300
- if (!request.accepted) throw new Error("The engine did not acknowledge the message.");
300
+ if (!request.accepted) throw Object.assign(new Error("The engine did not acknowledge the message."), {
301
+ messageCode: "assistant.conversation.messageNotAcknowledged", messageParams: {}
302
+ });
301
303
  }).catch(async error => {
302
304
  if (!request.accepted && (error.delivery === "not-sent" || !entry.conversation && error.delivery !== "uncertain")) {
303
305
  try { await input.onPromptRejected(); }
@@ -494,7 +494,9 @@ function createConversationTurns({ storage, streams, events, entries, reads, del
494
494
  entry.storageFailure.cause = saveError;
495
495
  problem ||= saveError;
496
496
  }
497
- if (!initial.admissionSettled) initial.admission.reject(problem || new Error("The engine did not acknowledge the message."));
497
+ if (!initial.admissionSettled) initial.admission.reject(problem || Object.assign(new Error("The engine did not acknowledge the message."), {
498
+ messageCode: "assistant.conversation.messageNotAcknowledged", messageParams: {}
499
+ }));
498
500
  entry.active = null;
499
501
  await events.setPhase(entry, "", active.request.turnId);
500
502
  await events.emit(entry, { type: "settled", turnId: active.request.turnId, status: entry.storageFailure || entry.executionFailure ? "failed" : status,
@@ -130,10 +130,10 @@ async function waitForOpenCodeReady({
130
130
  signal?.throwIfAborted();
131
131
  if (health?.healthy === true) {
132
132
  if (expectedVersion && normalizeNullableText(health.version) !== expectedVersion) {
133
- const error = new Error(
134
- `OpenCode ${normalizeNullableText(health.version) || "(unknown)"} is installed; this host requires ${expectedVersion}.`
135
- );
133
+ const installed = normalizeNullableText(health.version) || "(unknown)";
134
+ const error = new Error(`OpenCode ${installed} is installed; this host requires ${expectedVersion}.`);
136
135
  error.code = "assistant_opencode_version_mismatch";
136
+ error.params = { installed, required: expectedVersion };
137
137
  throw error;
138
138
  }
139
139
  return health;
@@ -569,12 +569,8 @@ export async function verifyOpenCodeApiKey({
569
569
  Boolean(normalizeNullableText(result?.code)) ||
570
570
  (Number.isSafeInteger(exitCode) && exitCode !== 1);
571
571
  const error = unavailable
572
- ? new AppError(503, "OpenCode API-key verification could not run.", {
573
- code: "assistant_opencode_key_verification_unavailable"
574
- })
575
- : new AppError(422, "OpenCode rejected the API-key verification request.", {
576
- code: "assistant_opencode_key_verification_failed"
577
- });
572
+ ? new AppError(503, "OpenCode API-key verification could not run.", { code: "assistant_opencode_key_verification_unavailable" })
573
+ : new AppError(422, "OpenCode rejected the API-key verification request.", { code: "assistant_opencode_key_verification_failed" });
578
574
  error.retryable = unavailable;
579
575
  throw error;
580
576
  }
@@ -188,7 +188,9 @@ export function createOpenCodeSharedRuntime({ onStop = ignoreRuntimeStop, scope,
188
188
  function allowTerminalAttachments(sessionId, terminalSessionId, input) {
189
189
  const target = [...processes.values()].find((candidate) =>
190
190
  candidate.sessionId === sessionId && candidate.terminalSessionId === terminalSessionId);
191
- if (!target) throw new Error("Reopen the OpenCode terminal before attaching files.");
191
+ if (!target) throw Object.assign(new Error("Reopen the OpenCode terminal before attaching files."), {
192
+ messageCode: "assistant.opencode.reopenTerminalToAttach", messageParams: {}
193
+ });
192
194
  return target.server.client.allowConversationAttachments(target.upstreamSessionId, input.attachments);
193
195
  }
194
196
 
@@ -83,7 +83,7 @@ async function consumeOpenCodeEvents(client, conversationId, {
83
83
  ) {
84
84
  const failure = normalizeObject(event.data?.properties?.error);
85
85
  onError(Object.assign(new Error(readError({ error: failure }) || "OpenCode turn failed."), {
86
- name: normalizeNullableText(failure.name) || "Error"
86
+ name: normalizeNullableText(failure.name) || "Error", messageCode: "assistant.opencode.turnFailed", messageParams: {}
87
87
  }));
88
88
  }
89
89
  if (summary.type && current && typeof onEvent === "function") {
@@ -527,7 +527,8 @@ export async function runOpenCodeConversationTurn(client, conversationId, input
527
527
  createError = (reason, message, details) => new AppError(
528
528
  ["thread_mismatch", "fresh_thread_required", "unrelated_history"].includes(reason) ? 409 : 502,
529
529
  message || (reason === "unreadable" ? "The exact OpenCode turn did not produce a readable result." : "OpenCode did not finish the turn."),
530
- { code: `assistant_opencode_turn_${reason}`, details })
530
+ { code: `assistant_opencode_turn_${reason}`, details,
531
+ ...(message ? {} : reason === "unreadable" ? { messageCode: "assistant.opencode.turnUnreadable" } : { messageCode: "assistant.opencode.turnNotFinished" }) })
531
532
  } = input;
532
533
  if (
533
534
  (expectedThreadId && expectedThreadId !== conversationId) ||
@@ -124,7 +124,9 @@ export function createClaudeConversationDriver({
124
124
  credentialHome: { home: env.HOME || homedir() }, commandRunner: captureAccountStatus, signal });
125
125
  signal.throwIfAborted();
126
126
  if (!account.loggedIn || !account.email) {
127
- throw new Error(account.error || "Sign in with claude auth login before using this conversation.");
127
+ throw Object.assign(new Error(account.error || "Sign in with claude auth login before using this conversation."), {
128
+ messageCode: "assistant.claude.signInRequired", messageParams: {}
129
+ });
128
130
  }
129
131
  return hash([configRoot, account.authMethod, account.email.toLowerCase()]);
130
132
  }
@@ -221,9 +223,13 @@ export function createClaudeConversationDriver({
221
223
  }
222
224
  const provider = nativeAiProvider(providerId);
223
225
  const model = nativeAiModel(connection.model, providerId);
224
- if (providerId !== "anthropic" && (!provider || !model)) throw new Error("This exact provider/model is not supported by the Claude integration. Choose a supported model without renaming it.");
226
+ if (providerId !== "anthropic" && (!provider || !model)) throw Object.assign(new Error("This exact provider/model is not supported by the Claude integration. Choose a supported model without renaming it."), {
227
+ messageCode: "assistant.claude.modelUnsupported", messageParams: {}
228
+ });
225
229
  validateConnectionModel(configuration, connection);
226
- if (model && configuration.effort && !model.variants.includes(configuration.effort)) throw new Error("The selected Claude model does not support this reasoning effort.");
230
+ if (model && configuration.effort && !model.variants.includes(configuration.effort)) throw Object.assign(new Error("The selected Claude model does not support this reasoning effort."), {
231
+ messageCode: "assistant.claude.reasoningEffortUnsupported", messageParams: {}
232
+ });
227
233
  selected = claudeModelConfiguration({ providerId, model: connection.model }, {
228
234
  apiKey: connection.apiKey, baseUrl: provider?.claudeBaseUrl || connection.baseURL || "https://api.anthropic.com"
229
235
  });
@@ -390,7 +396,9 @@ export function createClaudeConversationDriver({
390
396
  ? await realpath(env.HOME || homedir()) : scopeWorkdir;
391
397
  if (binding.workdir !== nativeWorkdir || binding.configRoot !== configRoot ||
392
398
  (Object.hasOwn(binding, "scopeWorkdir") && binding.scopeWorkdir !== scopeWorkdir)) {
393
- throw new Error("This Claude conversation belongs to another working directory or credential home. Restore its original host configuration.");
399
+ throw Object.assign(new Error("This Claude conversation belongs to another working directory or credential home. Restore its original host configuration."), {
400
+ messageCode: "assistant.claude.otherHostConfiguration", messageParams: {}
401
+ });
394
402
  }
395
403
  owner = standaloneOwner();
396
404
  const context = { key: scopeWorkdir, workdir: binding.workdir, binding, reportFailure,
@@ -133,7 +133,9 @@ export function createCodexConversationAdapter(host) {
133
133
  (bound?.managed === false ? bound.params.config.shell_environment_policy : null);
134
134
  if (!settings && fixedPolicy && (params.developerInstructions == null || params.ephemeral || bound?.params.ephemeral)) {
135
135
  if (bound && params.developerInstructions != null && instructionPrompts.get(threadId) !== params.developerInstructions) {
136
- throw new Error("Start a new ephemeral conversation to change its instructions.");
136
+ throw Object.assign(new Error("Start a new ephemeral conversation to change its instructions."), {
137
+ messageCode: "assistant.codex.ephemeralInstructionsFixed", messageParams: {}
138
+ });
137
139
  }
138
140
  assertRecoveryOpen();
139
141
  return operation(threadParameters({ ...bound?.params, ...params }, {}));
@@ -162,7 +164,7 @@ export function createCodexConversationAdapter(host) {
162
164
  const { thread } = await request("thread/read", { threadId, includeTurns: false });
163
165
  if (thread?.historyMode !== "paginated") {
164
166
  throw Object.assign(new Error("This conversation uses an unsupported Codex history format. Renew the session to keep your files and saved chat and start a fresh conversation. You can write the handover yourself."), {
165
- code: "assistant_codex_history_unsupported"
167
+ code: "assistant_codex_history_unsupported", messageCode: "assistant.codex.historyFormatUnsupported", messageParams: {}
166
168
  });
167
169
  }
168
170
  nativeModelProvider = thread?.modelProvider || "";
@@ -331,10 +333,12 @@ export function createCodexConversationAdapter(host) {
331
333
  host.onRecoveryFailure(threadId);
332
334
  bindings.delete(threadId);
333
335
  log("recovery_failed", { code: cause?.code || "", reason: cause.message });
334
- throw Object.assign(new Error(["assistant_agent_control_binding_retained", "assistant_codex_provider_binding_retained"].includes(cause?.code)
336
+ const retained = ["assistant_agent_control_binding_retained", "assistant_codex_provider_binding_retained"].includes(cause?.code);
337
+ throw Object.assign(new Error(retained
335
338
  ? `${cause.message} Your conversation and work are preserved.`
336
339
  : "The assistant connection could not be verified. Your conversation and files are preserved. Retry the connection, or renew the session to continue in a fresh conversation.", { cause }), {
337
- code: "assistant_agent_control_recovery_failed", retryable: true
340
+ code: "assistant_agent_control_recovery_failed", retryable: true,
341
+ ...(retained ? {} : { messageCode: "assistant.codex.connectionUnverified", messageParams: {} })
338
342
  });
339
343
  }
340
344
  // A newer settings save supersedes the older choice before scheduling
@@ -50,6 +50,19 @@ function assertCodexApplicationEnvironment(host) {
50
50
  if (!host.env) throw new TypeError("createCodexConversationDriver requires the application's environment, such as process.env.");
51
51
  }
52
52
 
53
+ // Refusals Codex conversations send from several places; each names its sentence.
54
+ function codexOutputTooLarge() {
55
+ return Object.assign(new Error("Codex output exceeded the configured limit."), {
56
+ messageCode: "assistant.codex.outputTooLarge", messageParams: {}
57
+ });
58
+ }
59
+
60
+ function nativeGoalChanged() {
61
+ return Object.assign(new Error("The native goal changed. Read it again before changing it."), {
62
+ messageCode: "assistant.codex.nativeGoalChanged", messageParams: {}
63
+ });
64
+ }
65
+
53
66
  /**
54
67
  * Conversations own threads and observers on the account's shared native runtime.
55
68
  * @param {object} dependencies
@@ -95,7 +108,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
95
108
  } catch (error) {
96
109
  // Native keychain logins can have no auth file. Never expose malformed
97
110
  // credential contents in a parser's error message.
98
- if (error.code !== "ENOENT") throw new Error("Codex credential data could not be read. Restore its account configuration.");
111
+ if (error.code !== "ENOENT") throw Object.assign(new Error("Codex credential data could not be read. Restore its account configuration."), { messageCode: "assistant.codex.credentialsUnreadable", messageParams: {} });
99
112
  } finally { await file?.close(); }
100
113
  return auth;
101
114
  }
@@ -110,7 +123,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
110
123
  }
111
124
  const key = env.OPENAI_API_KEY || auth.OPENAI_API_KEY;
112
125
  if (account?.type === "apiKey" && typeof key === "string" && key) return hash([configRoot, account.type, key]);
113
- throw new Error("Sign in with codex login before using this conversation.");
126
+ throw Object.assign(new Error("Sign in with codex login before using this conversation."), { messageCode: "assistant.codex.signInRequired", messageParams: {} });
114
127
  }
115
128
 
116
129
  async function nativeHost(selected) {
@@ -137,7 +150,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
137
150
  identity: generation,
138
151
  generation,
139
152
  async assertCurrent(captured) {
140
- if (captured && captured !== await generation()) throw new Error("Codex authentication changed. Reopen the conversation with its original account.");
153
+ if (captured && captured !== await generation()) throw Object.assign(new Error("Codex authentication changed. Reopen the conversation with its original account."), { messageCode: "assistant.codex.authChangedReopen", messageParams: {} });
141
154
  },
142
155
  markInvalid() {},
143
156
  reconnectRequiredCode: "codex_reconnect_required",
@@ -297,7 +310,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
297
310
  }
298
311
  if (!supplied) assertCodexApplicationEnvironment(host);
299
312
  if (!supplied && (!binding || typeof binding.threadId !== "string" || binding.workdir !== await realpath(workdir) || binding.configRoot !== configRoot)) {
300
- throw new Error("This Codex conversation belongs to another working directory or credential home. Restore its original host configuration.");
313
+ throw Object.assign(new Error("This Codex conversation belongs to another working directory or credential home. Restore its original host configuration."), { messageCode: "assistant.codex.otherHostConfiguration", messageParams: {} });
301
314
  }
302
315
  const session = {
303
316
  binding,
@@ -325,7 +338,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
325
338
  : name.includes("AssistantMessage") ? Math.min(maximumOutput, maximumFinalReply) : maximumOutput;
326
339
  const length = input.text?.length || 0;
327
340
  if (length <= limit) return nativeStore[name](id, input);
328
- if (!name.includes("AssistantMessage") || length > maximumOutput) throw new Error("Codex output exceeded the configured limit.");
341
+ if (!name.includes("AssistantMessage") || length > maximumOutput) throw codexOutputTooLarge();
329
342
  return nativeStore.mutateSession(id, async transaction => {
330
343
  const { runtime: state } = await transaction.readMetadata();
331
344
  const identity = input.nativeIdentity;
@@ -344,7 +357,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
344
357
  break;
345
358
  }
346
359
  }
347
- throw new Error("Codex output exceeded the configured limit.");
360
+ throw codexOutputTooLarge();
348
361
  });
349
362
  };
350
363
  const runtime = supplied ? conversation.runtime : { ...conversation.runtime, store: { ...nativeStore,
@@ -353,9 +366,9 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
353
366
  writeConversationCommentaryMessage: boundedWriter("writeConversationCommentaryMessage"),
354
367
  writeConversationThinkingMessage: boundedWriter("writeConversationThinkingMessage"),
355
368
  async updateConversationStream(id, input) {
356
- if ((input.delta?.length || input.text?.length || 0) > maximumOutput) throw new Error("Codex output exceeded the configured limit.");
369
+ if ((input.delta?.length || input.text?.length || 0) > maximumOutput) throw codexOutputTooLarge();
357
370
  const stream = await nativeStore.updateConversationStream(id, input);
358
- if (stream?.messages.some(message => message.text.length > maximumOutput)) throw new Error("Codex output exceeded the configured limit.");
371
+ if (stream?.messages.some(message => message.text.length > maximumOutput)) throw codexOutputTooLarge();
359
372
  return stream;
360
373
  },
361
374
  writeConversationUserMessage(id, input) {
@@ -497,7 +510,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
497
510
  if (representation !== "native") {
498
511
  session.binding.threadId = nativeContext.threadId;
499
512
  const goal = session.binding.goal || null;
500
- if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
513
+ if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw nativeGoalChanged();
501
514
  command = { ...input, threadId: session.binding.threadId,
502
515
  createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective };
503
516
  }
@@ -523,7 +536,7 @@ export function createCodexConversationDriver({ connections, host = {}, limits =
523
536
  prepareThread: Boolean(session.binding.threadId && ["set", "resume"].includes(input.action)), tools
524
537
  });
525
538
  const goal = session.binding.threadId ? (await session.native.readGoal(session.binding.threadId, { signal })).goal : null;
526
- if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw new Error("The native goal changed. Read it again before changing it.");
539
+ if ((goalValue(goal)?.id || null) !== (input.expectedGoalId || null)) throw nativeGoalChanged();
527
540
  const result = await owner.updateGoal(sessionId, { ...input, threadId: session.binding.threadId,
528
541
  createdAt: goal?.createdAt, objective: input.action === "set" ? input.objective : goal?.objective }, {
529
542
  context: nativeContext, signal: context?.signal,
@@ -93,7 +93,9 @@ export function createCodexDriverConnection({
93
93
  prepareThreadEnvironment: environment => session.preparedThreadSettings?.config.shell_environment_policy.set || environment,
94
94
  prepareThreadResumeParams(threadId, params) {
95
95
  if (threadId !== session.binding.threadId || !session.preparedThreadSettings) {
96
- throw new Error("Codex requires current authorized settings before resuming this conversation.");
96
+ throw Object.assign(new Error("Codex requires current authorized settings before resuming this conversation."), {
97
+ messageCode: "assistant.codex.authorizedSettingsRequired", messageParams: {}
98
+ });
97
99
  }
98
100
  return { ...session.preparedThreadSettings, ...params,
99
101
  config: { ...session.preparedThreadSettings.config, ...params.config } };
@@ -197,9 +199,13 @@ export function createCodexDriverConnection({
197
199
  signal.throwIfAborted();
198
200
  const provider = nativeAiProvider(connection.providerId);
199
201
  const model = nativeAiModel(connection.model, connection.providerId);
200
- if (!provider || !model) throw new Error("This exact provider/model is not supported by the Codex integration. Choose a supported model without renaming it.");
202
+ if (!provider || !model) throw Object.assign(new Error("This exact provider/model is not supported by the Codex integration. Choose a supported model without renaming it."), {
203
+ messageCode: "assistant.codex.modelUnsupported", messageParams: {}
204
+ });
201
205
  validateConnectionModel(configuration, connection);
202
- if (configuration.effort && !model.variants.includes(configuration.effort)) throw new Error("The selected Codex model does not support this reasoning effort.");
206
+ if (configuration.effort && !model.variants.includes(configuration.effort)) throw Object.assign(new Error("The selected Codex model does not support this reasoning effort."), {
207
+ messageCode: "assistant.codex.reasoningEffortUnsupported", messageParams: {}
208
+ });
203
209
  const identity = hash([provider.id, provider.baseUrl, connection.apiKey]);
204
210
  const previous = session.binding.connectionIdentities?.[provider.id];
205
211
  const expected = receiptBinding?.connectionIdentities?.[provider.id];
@@ -207,7 +213,9 @@ export function createCodexDriverConnection({
207
213
  if (requireBound && (previous !== expected || identity !== expected)) {
208
214
  throw new Error("The completed Codex response belongs to a different provider account binding.");
209
215
  }
210
- if (previous && previous !== identity) throw new Error("This conversation belongs to another Codex provider account. Restore that connection or start a new conversation.");
216
+ if (previous && previous !== identity) throw Object.assign(new Error("This conversation belongs to another Codex provider account. Restore that connection or start a new conversation."), {
217
+ messageCode: "assistant.codex.otherProviderAccount", messageParams: {}
218
+ });
211
219
  if (!previous) await updateBinding({ connectionIdentities: { ...session.binding.connectionIdentities, [provider.id]: identity } });
212
220
  return { model: model.id, providerId: provider.id, accountIdentity: identity, config: codexProviderConfiguration(provider, connection.apiKey) };
213
221
  }
@@ -250,7 +258,9 @@ export function createCodexDriverConnection({
250
258
  const identity = await accountIdentity(session.native.client, signal);
251
259
  if (session.binding.accountIdentity && session.binding.accountIdentity !== identity) {
252
260
  providerOwner.closeProvider(providerKey);
253
- throw new Error("This conversation belongs to another Codex account. Restore that account or start a new conversation.");
261
+ throw Object.assign(new Error("This conversation belongs to another Codex account. Restore that account or start a new conversation."), {
262
+ messageCode: "assistant.codex.otherAccount", messageParams: {}
263
+ });
254
264
  }
255
265
  if (!session.binding.accountIdentity) await updateBinding({ accountIdentity: identity });
256
266
  }
@@ -315,7 +325,9 @@ export function createCodexDriverConnection({
315
325
  async write({ threadId }) { await updateBinding({ threadId, toolSchemaIdentity }); } }
316
326
  });
317
327
  if (!retainedThreadId && prepared.thread.raw?.historyMode !== "paginated") {
318
- throw new Error("Codex did not create the required paginated history. Update Codex before starting this conversation.");
328
+ throw Object.assign(new Error("Codex did not create the required paginated history. Update Codex before starting this conversation."), {
329
+ messageCode: "assistant.codex.paginatedHistoryMissing", messageParams: {}
330
+ });
319
331
  }
320
332
  owner.subscribeEvents(sessionId, session.native, prepared.threadId, { providerKey });
321
333
  return { ...selected, resumeOptions: params };
@@ -274,7 +274,9 @@ export function createCodexDriverTurns({
274
274
  const admission = Promise.withResolvers();
275
275
  completion.promise.catch(() => {});
276
276
  admission.promise.catch(() => {});
277
- if (input.goal) throw new Error("Codex goals are controls. Inspect any unresolved goal-message record before continuing.");
277
+ if (input.goal) throw Object.assign(new Error("Codex goals are controls. Inspect any unresolved goal-message record before continuing."), {
278
+ messageCode: "assistant.codex.goalsAreControls", messageParams: {}
279
+ });
278
280
  const active = session.current = { configuration, context, inputId: input.messageId,
279
281
  onEvent, onNativeTurn, signal, completion, admission, inputReady: admission.promise, finishIfCurrent,
280
282
  admitted: false, dispatched: false, turnId: "" };
@@ -39,7 +39,9 @@ export function createOpenCodeConversationAdapter({ resolveInstructions }) {
39
39
  });
40
40
  }
41
41
  const text = await current.text;
42
- if (contexts.get(id) !== current) throw new Error("Conversation context changed while reading instructions. Retry before sending.");
42
+ if (contexts.get(id) !== current) throw Object.assign(new Error("Conversation context changed while reading instructions. Retry before sending."), {
43
+ messageCode: "assistant.conversation.contextChangedReadingInstructions", messageParams: {}
44
+ });
43
45
  const previous = contributions.get(output.system);
44
46
  if (previous !== undefined) {
45
47
  const index = output.system.indexOf(previous);
@@ -272,7 +272,9 @@ export function createOpenCodeConversationDriver({
272
272
  return representation === "native" ? result : { accepted: result.admission === "accepted" };
273
273
  },
274
274
  async run(command) {
275
- if (disposed || current) throw new Error("This OpenCode conversation is closed or already working.");
275
+ if (disposed || current) throw Object.assign(new Error("This OpenCode conversation is closed or already working."), {
276
+ messageCode: "assistant.opencode.closedOrWorking", messageParams: {}
277
+ });
276
278
  const active = { context: command.context, failure: Promise.withResolvers(),
277
279
  ...(command.tools ? { tools: command.tools, messageIds: new Map() } : {}) };
278
280
  let failure;