@jskit-ai/assistant-core 0.1.244 → 0.2.0
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/CHANGELOG.md +539 -0
- package/examples/conversation/package.json +5 -5
- package/locales/en.json +383 -0
- package/package.json +7 -6
- package/src/client/components/AssistantSettingsFormCard.vue +4 -1
- package/src/client/conversation/AssistantAttachmentPreview.vue +6 -3
- package/src/client/conversation/AssistantAttachmentQueue.vue +36 -37
- package/src/client/conversation/AssistantComposerActions.vue +6 -3
- package/src/client/conversation/AssistantComposerSupport.vue +8 -5
- package/src/client/conversation/AssistantConversationElement.vue +16 -13
- package/src/client/conversation/AssistantConversationStatus.vue +5 -2
- package/src/client/conversation/AssistantGoalControl.vue +22 -16
- package/src/client/conversation/AssistantMessageAttachments.vue +7 -4
- package/src/client/conversation/AssistantModelControl.vue +23 -20
- package/src/client/conversation/AssistantProgress.vue +6 -3
- package/src/client/conversation/AssistantQuestionInputs.vue +9 -6
- package/src/client/conversation/AssistantTranscript.vue +39 -28
- package/src/client/conversation/LongTextPreviewBlocks.vue +10 -2
- package/src/client/conversation/attachmentSize.js +38 -0
- package/src/client/conversation/messageDelivery.js +14 -5
- package/src/client/conversation/submitText.js +11 -4
- package/src/client/conversation/useAssistantAttachments.js +40 -30
- package/src/client/conversation/useAssistantSuggestions.js +4 -1
- package/src/client/i18n.js +6 -0
- package/src/client/lib/assistantApi.js +14 -2
- package/src/server/conversation/attachments.js +24 -14
- package/src/server/conversation/claudeProcess.js +4 -2
- package/src/server/conversation/claudeTurn.js +14 -11
- package/src/server/conversation/codexConversations.js +4 -0
- package/src/server/conversation/codexGoalCommands.js +9 -9
- package/src/server/conversation/codexHelperExecution.js +4 -0
- package/src/server/conversation/codexHelperThreads.js +2 -0
- package/src/server/conversation/codexMessageCommands.js +7 -1
- package/src/server/conversation/codexTurnControl.js +2 -1
- package/src/server/conversation/codexTurnState.js +2 -0
- package/src/server/conversation/continuity.js +16 -6
- package/src/server/conversation/runtime.js +193 -141
- package/src/server/lib/ndjson.js +19 -1
- package/src/server/lib/providers/common.js +7 -2
- package/src/shared/conversation/numberedQuestionSugar.js +7 -1
- package/src/shared/conversation/turns.js +10 -2
- package/test/i18nCatalog.test.js +22 -0
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Client-made errors keep their English message; messageCode lets a caller show
|
|
2
|
+
// them in the active language through useJskitText().serverMessage(error).
|
|
3
|
+
const CANNOT_SEND_YET = "Your words are in the chat box, but this conversation cannot send yet.";
|
|
4
|
+
const submissionError = (message, { messageCode }) => Object.assign(new Error(message), { messageCode, messageParams: {} });
|
|
5
|
+
|
|
1
6
|
/** A companion's text enters the same draft and send action as keyboard input. */
|
|
2
7
|
export function createAssistantTextSubmission({ getState, setDraft, submit, afterDraftChange = async () => {} }) {
|
|
3
8
|
let revision = 0;
|
|
@@ -5,7 +10,9 @@ export function createAssistantTextSubmission({ getState, setDraft, submit, afte
|
|
|
5
10
|
const request = ++revision;
|
|
6
11
|
const value = String(text || "").trim();
|
|
7
12
|
const initial = getState();
|
|
8
|
-
if (!value || !initial.active || signal?.aborted)
|
|
13
|
+
if (!value || !initial.active || signal?.aborted) {
|
|
14
|
+
throw submissionError("The chat composer is not ready yet.", { messageCode: "assistant.composer.notReady" });
|
|
15
|
+
}
|
|
9
16
|
const hasDraft = Boolean(initial.draft?.trim());
|
|
10
17
|
const writtenValue = hasDraft ? `${initial.draft}\n${value}` : value;
|
|
11
18
|
setDraft(writtenValue);
|
|
@@ -15,14 +22,14 @@ export function createAssistantTextSubmission({ getState, setDraft, submit, afte
|
|
|
15
22
|
while (true) {
|
|
16
23
|
const state = getState();
|
|
17
24
|
if (signal?.aborted || request !== revision || !state.active || state.id !== initial.id || state.draft !== writtenValue) {
|
|
18
|
-
throw
|
|
25
|
+
throw submissionError("The chat changed before sending. Review the draft before sending.", { messageCode: "assistant.composer.chatChanged" });
|
|
19
26
|
}
|
|
20
27
|
if (state.canSend) {
|
|
21
28
|
const result = await submit();
|
|
22
|
-
if (result === false) throw
|
|
29
|
+
if (result === false) throw submissionError(CANNOT_SEND_YET, { messageCode: "assistant.composer.cannotSendYet" });
|
|
23
30
|
return state.turnActive ? "steer" : "send";
|
|
24
31
|
}
|
|
25
|
-
if (Date.now() >= deadline) throw
|
|
32
|
+
if (Date.now() >= deadline) throw submissionError(CANNOT_SEND_YET, { messageCode: "assistant.composer.cannotSendYet" });
|
|
26
33
|
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
27
34
|
}
|
|
28
35
|
};
|
|
@@ -8,6 +8,16 @@ import {
|
|
|
8
8
|
watch
|
|
9
9
|
} from "vue";
|
|
10
10
|
import { ASSISTANT_ATTACHMENT_MAX_ITEMS } from "../../shared/conversation/attachments.js";
|
|
11
|
+
import { useJskitText } from "../i18n.js";
|
|
12
|
+
import { attachmentLimitText } from "./attachmentSize.js";
|
|
13
|
+
|
|
14
|
+
const { serverMessage, t } = useJskitText();
|
|
15
|
+
|
|
16
|
+
// An upload or preparation failure in the active language; a thrown error keeps
|
|
17
|
+
// its own English when it has no translation.
|
|
18
|
+
function attachmentErrorText(error, fallback) {
|
|
19
|
+
return String((error && typeof error === "object" && serverMessage(error)) || error?.message || error || fallback);
|
|
20
|
+
}
|
|
11
21
|
|
|
12
22
|
function assistantAttachmentSessionId(sessionId) {
|
|
13
23
|
return String(toValue(sessionId) || "").trim();
|
|
@@ -217,9 +227,6 @@ function useAssistantAttachments({
|
|
|
217
227
|
if (![maxBytes, maxItems, uploadConcurrency].every(value => Number.isSafeInteger(value) && value > 0)) {
|
|
218
228
|
throw new TypeError("Attachment limits must be positive integers.");
|
|
219
229
|
}
|
|
220
|
-
const maxSizeLabel = maxBytes % (1024 * 1024) === 0
|
|
221
|
-
? `${maxBytes / (1024 * 1024)} MiB`
|
|
222
|
-
: `${maxBytes / 1_000_000} MB`;
|
|
223
230
|
const attachments = ref([]);
|
|
224
231
|
const queueItems = ref([]);
|
|
225
232
|
const dragDepth = ref(0);
|
|
@@ -266,10 +273,13 @@ function useAssistantAttachments({
|
|
|
266
273
|
};
|
|
267
274
|
});
|
|
268
275
|
|
|
269
|
-
|
|
276
|
+
const tooLarge = (row) => t("jskit.assistant.attachments.error.tooLarge", { fileName: row.fileName, limit: attachmentLimitText(maxBytes) });
|
|
277
|
+
const tooMany = () => t("jskit.assistant.attachments.error.tooMany", { count: maxItems });
|
|
278
|
+
|
|
279
|
+
function reportAttachmentError(error, fallbackMessage = t("jskit.assistant.attachments.error.uploadFailed"), {
|
|
270
280
|
notify = true
|
|
271
281
|
} = {}) {
|
|
272
|
-
const message =
|
|
282
|
+
const message = attachmentErrorText(error, fallbackMessage);
|
|
273
283
|
status.value = message;
|
|
274
284
|
if (notify && typeof onError === "function") {
|
|
275
285
|
try {
|
|
@@ -319,7 +329,7 @@ function useAssistantAttachments({
|
|
|
319
329
|
|
|
320
330
|
function trackCleanup(task) {
|
|
321
331
|
const cleanup = Promise.resolve(task).catch((error) => {
|
|
322
|
-
reportAttachmentError(error, "
|
|
332
|
+
reportAttachmentError(error, t("jskit.assistant.attachments.error.cleanupFailed"));
|
|
323
333
|
return false;
|
|
324
334
|
});
|
|
325
335
|
cleanupTasks.add(cleanup);
|
|
@@ -334,7 +344,7 @@ function useAssistantAttachments({
|
|
|
334
344
|
}
|
|
335
345
|
const response = await deleteAttachment(currentSessionId, attachmentId);
|
|
336
346
|
if (response?.ok === false) {
|
|
337
|
-
throw new Error(attachmentUploadError(response) || "
|
|
347
|
+
throw new Error(attachmentUploadError(response) || t("jskit.assistant.attachments.error.cleanupFailed"));
|
|
338
348
|
}
|
|
339
349
|
return true;
|
|
340
350
|
}
|
|
@@ -424,11 +434,11 @@ function useAssistantAttachments({
|
|
|
424
434
|
}
|
|
425
435
|
row.activeHandoffToken = null;
|
|
426
436
|
removeReadyReceipt(row);
|
|
427
|
-
row.error =
|
|
437
|
+
row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.unusable"));
|
|
428
438
|
row.failureStage = "handoff";
|
|
429
439
|
row.phase = "failed";
|
|
430
440
|
settleWaiter(row, null, waiter);
|
|
431
|
-
reportAttachmentError(error, "
|
|
441
|
+
reportAttachmentError(error, t("jskit.assistant.attachments.error.unusable"), { notify: false });
|
|
432
442
|
return null;
|
|
433
443
|
}
|
|
434
444
|
}
|
|
@@ -468,13 +478,13 @@ function useAssistantAttachments({
|
|
|
468
478
|
await handOffUploadedAttachment(row, receipt, attempt.waiter);
|
|
469
479
|
} else {
|
|
470
480
|
const error = uploadError || new Error(
|
|
471
|
-
attachmentUploadError(uploaded) || "
|
|
481
|
+
attachmentUploadError(uploaded) || t("jskit.assistant.attachments.error.uploadFailed")
|
|
472
482
|
);
|
|
473
|
-
row.error =
|
|
483
|
+
row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.uploadFailed"));
|
|
474
484
|
row.failureStage = "upload";
|
|
475
485
|
row.phase = "failed";
|
|
476
486
|
settleWaiter(row, null, attempt.waiter);
|
|
477
|
-
reportAttachmentError(error, "
|
|
487
|
+
reportAttachmentError(error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
|
|
478
488
|
}
|
|
479
489
|
pumpQueue();
|
|
480
490
|
}
|
|
@@ -572,7 +582,7 @@ function useAssistantAttachments({
|
|
|
572
582
|
return Promise.resolve(null);
|
|
573
583
|
}
|
|
574
584
|
if (row.phase === "cancelled" && retainedItemCount.value >= maxItems) {
|
|
575
|
-
reportAttachmentError(
|
|
585
|
+
reportAttachmentError(tooMany());
|
|
576
586
|
return Promise.resolve(null);
|
|
577
587
|
}
|
|
578
588
|
if (row.failureStage === "handoff" && row.receipt) {
|
|
@@ -588,10 +598,10 @@ function useAssistantAttachments({
|
|
|
588
598
|
return waiter.promise;
|
|
589
599
|
}
|
|
590
600
|
if (row.size > maxBytes) {
|
|
591
|
-
row.error =
|
|
601
|
+
row.error = tooLarge(row);
|
|
592
602
|
row.failureStage = "validation";
|
|
593
603
|
row.phase = "failed";
|
|
594
|
-
reportAttachmentError(row.error, "
|
|
604
|
+
reportAttachmentError(row.error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
|
|
595
605
|
return Promise.resolve(null);
|
|
596
606
|
}
|
|
597
607
|
row.bytesSent = 0;
|
|
@@ -763,18 +773,20 @@ function useAssistantAttachments({
|
|
|
763
773
|
function queuePreparedFile(row, file, waiter = row.waiter) {
|
|
764
774
|
const preparedFile = assistantAttachmentFiles([file])[0];
|
|
765
775
|
if (!preparedFile) {
|
|
766
|
-
throw new Error("The prepared attachment did not produce a file.")
|
|
776
|
+
throw Object.assign(new Error("The prepared attachment did not produce a file."), {
|
|
777
|
+
messageCode: "assistant.attachments.error.noFile", messageParams: {}
|
|
778
|
+
});
|
|
767
779
|
}
|
|
768
780
|
row.file = preparedFile;
|
|
769
781
|
row.fileName = attachmentFileName(preparedFile);
|
|
770
782
|
row.size = Number(preparedFile.size) || 0;
|
|
771
783
|
row.totalBytes = row.size;
|
|
772
784
|
if (row.size > maxBytes) {
|
|
773
|
-
row.error =
|
|
785
|
+
row.error = tooLarge(row);
|
|
774
786
|
row.failureStage = "validation";
|
|
775
787
|
row.phase = "failed";
|
|
776
788
|
settleWaiter(row, null, waiter);
|
|
777
|
-
reportAttachmentError(row.error, "
|
|
789
|
+
reportAttachmentError(row.error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
|
|
778
790
|
return false;
|
|
779
791
|
}
|
|
780
792
|
row.phase = "queued";
|
|
@@ -807,11 +819,11 @@ function useAssistantAttachments({
|
|
|
807
819
|
try {
|
|
808
820
|
queuePreparedFile(row, file, waiter);
|
|
809
821
|
} catch (error) {
|
|
810
|
-
row.error =
|
|
822
|
+
row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.preparationFailed"));
|
|
811
823
|
row.failureStage = "preparing";
|
|
812
824
|
row.phase = "failed";
|
|
813
825
|
settleWaiter(row, null, waiter);
|
|
814
|
-
reportAttachmentError(error, "
|
|
826
|
+
reportAttachmentError(error, t("jskit.assistant.attachments.error.preparationFailed"), { notify: false });
|
|
815
827
|
}
|
|
816
828
|
return file;
|
|
817
829
|
}, (error) => {
|
|
@@ -825,10 +837,10 @@ function useAssistantAttachments({
|
|
|
825
837
|
row.phase = "cancelled";
|
|
826
838
|
row.error = "";
|
|
827
839
|
} else {
|
|
828
|
-
row.error =
|
|
840
|
+
row.error = attachmentErrorText(error, t("jskit.assistant.attachments.error.preparationFailed"));
|
|
829
841
|
row.failureStage = "preparing";
|
|
830
842
|
row.phase = "failed";
|
|
831
|
-
reportAttachmentError(error, "
|
|
843
|
+
reportAttachmentError(error, t("jskit.assistant.attachments.error.preparationFailed"), { notify: false });
|
|
832
844
|
}
|
|
833
845
|
settleWaiter(row, null, waiter);
|
|
834
846
|
return null;
|
|
@@ -836,13 +848,13 @@ function useAssistantAttachments({
|
|
|
836
848
|
}
|
|
837
849
|
|
|
838
850
|
function uploadFileProducer({
|
|
839
|
-
fileName = "
|
|
851
|
+
fileName = t("jskit.assistant.attachments.preparingName"),
|
|
840
852
|
produce
|
|
841
853
|
} = {}) {
|
|
842
854
|
const currentSessionId = assistantAttachmentSessionId(sessionId);
|
|
843
855
|
if (!canAddFiles.value || typeof produce !== "function") {
|
|
844
856
|
if (atCapacity.value) {
|
|
845
|
-
reportAttachmentError(
|
|
857
|
+
reportAttachmentError(tooMany());
|
|
846
858
|
}
|
|
847
859
|
return Promise.resolve(null);
|
|
848
860
|
}
|
|
@@ -875,7 +887,7 @@ function useAssistantAttachments({
|
|
|
875
887
|
const availableSlots = Math.max(0, maxItems - retainedItemCount.value);
|
|
876
888
|
const acceptedFiles = uploadableFiles.slice(0, availableSlots);
|
|
877
889
|
if (acceptedFiles.length < uploadableFiles.length) {
|
|
878
|
-
reportAttachmentError(
|
|
890
|
+
reportAttachmentError(tooMany());
|
|
879
891
|
}
|
|
880
892
|
|
|
881
893
|
const waiters = [];
|
|
@@ -884,11 +896,11 @@ function useAssistantAttachments({
|
|
|
884
896
|
const waiter = row.waiter;
|
|
885
897
|
waiters.push(waiter.promise);
|
|
886
898
|
if (row.size > maxBytes) {
|
|
887
|
-
row.error =
|
|
899
|
+
row.error = tooLarge(row);
|
|
888
900
|
row.failureStage = "validation";
|
|
889
901
|
row.phase = "failed";
|
|
890
902
|
settleWaiter(row, null);
|
|
891
|
-
reportAttachmentError(row.error, "
|
|
903
|
+
reportAttachmentError(row.error, t("jskit.assistant.attachments.error.uploadFailed"), { notify: false });
|
|
892
904
|
} else {
|
|
893
905
|
uploadQueue.push(row);
|
|
894
906
|
}
|
|
@@ -910,9 +922,7 @@ function useAssistantAttachments({
|
|
|
910
922
|
const files = assistantAttachmentFilesFromPasteEvent(event);
|
|
911
923
|
if (files.length < 1) {
|
|
912
924
|
if (assistantAttachmentClipboardLocalFileReference(event)) {
|
|
913
|
-
reportAttachmentError(
|
|
914
|
-
"Copied local files cannot be pasted from this browser. Drop the file or use Attach files."
|
|
915
|
-
);
|
|
925
|
+
reportAttachmentError(t("jskit.assistant.attachments.error.localFilePaste"));
|
|
916
926
|
}
|
|
917
927
|
return [];
|
|
918
928
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { computed, onScopeDispose, ref, toValue, unref, watch } from "vue";
|
|
2
|
+
import { useJskitText } from "../i18n.js";
|
|
3
|
+
|
|
4
|
+
const { serverMessage } = useJskitText();
|
|
2
5
|
|
|
3
6
|
/** Suggestion requests use an application-supplied agent; credentials stay on its server. */
|
|
4
7
|
export function useAssistantSuggestions({
|
|
@@ -53,7 +56,7 @@ export function useAssistantSuggestions({
|
|
|
53
56
|
}
|
|
54
57
|
items.value = result.map(({ label, prompt }) => ({ label: label.trim(), prompt: prompt.trim() }));
|
|
55
58
|
} catch (failure) {
|
|
56
|
-
if (request === controller && !controller.signal.aborted) error.value = String(failure
|
|
59
|
+
if (request === controller && !controller.signal.aborted) error.value = String(serverMessage(failure) || failure);
|
|
57
60
|
} finally {
|
|
58
61
|
if (request === controller) {
|
|
59
62
|
request = null;
|
|
@@ -9,10 +9,22 @@ import {
|
|
|
9
9
|
normalizeAssistantStreamEventType
|
|
10
10
|
} from "../../shared/index.js";
|
|
11
11
|
|
|
12
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
// A stream error event keeps its code and English message, plus the params and
|
|
15
|
+
// messageCode useJskitText().serverMessage(error) needs to show it in the active language.
|
|
12
16
|
function buildStreamEventError(event) {
|
|
13
|
-
const
|
|
14
|
-
const error = new Error(
|
|
17
|
+
const sent = String(event?.message || "");
|
|
18
|
+
const error = new Error(sent || "Assistant request failed.");
|
|
15
19
|
error.code = String(event?.code || "assistant_stream_error");
|
|
20
|
+
error.params = isRecord(event?.params) ? event.params : {};
|
|
21
|
+
if (event?.messageCode) {
|
|
22
|
+
error.messageCode = String(event.messageCode);
|
|
23
|
+
error.messageParams = isRecord(event.messageParams) ? event.messageParams : {};
|
|
24
|
+
} else if (!sent) {
|
|
25
|
+
error.messageCode = "assistant.api.requestFailed";
|
|
26
|
+
error.messageParams = {};
|
|
27
|
+
}
|
|
16
28
|
error.status = Number(event?.status || 500);
|
|
17
29
|
error.event = event && typeof event === "object" ? { ...event } : null;
|
|
18
30
|
return error;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { ASSISTANT_ATTACHMENT_ID_MAX_LENGTH, ASSISTANT_ATTACHMENT_MAX_ITEMS, conversationAttachmentManifest } from "../../shared/conversation/attachments.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// messageCode names the sentence; the shared code stays for callers.
|
|
5
|
+
function invalid(message, { messageCode, messageParams = {} }) {
|
|
6
|
+
return Object.assign(new Error(message), { code: "conversation_invalid_attachments", statusCode: 400, messageCode, messageParams });
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function conversationAttachmentIds(value = []) {
|
|
9
10
|
if (!Array.isArray(value) || value.length > ASSISTANT_ATTACHMENT_MAX_ITEMS ||
|
|
10
11
|
value.some(id => typeof id !== "string" || !id.trim() || id.length > ASSISTANT_ATTACHMENT_ID_MAX_LENGTH) ||
|
|
11
12
|
new Set(value).size !== value.length) {
|
|
12
|
-
throw invalid(`Attachments require at most ${ASSISTANT_ATTACHMENT_MAX_ITEMS} distinct, nonempty attachment IDs
|
|
13
|
+
throw invalid(`Attachments require at most ${ASSISTANT_ATTACHMENT_MAX_ITEMS} distinct, nonempty attachment IDs.`, {
|
|
14
|
+
messageCode: "assistant.conversation.attachmentIdsInvalid", messageParams: { max: ASSISTANT_ATTACHMENT_MAX_ITEMS }
|
|
15
|
+
});
|
|
13
16
|
}
|
|
14
17
|
return [...value];
|
|
15
18
|
}
|
|
@@ -20,7 +23,8 @@ export function createConversationAttachmentReader({ attachments, context, conve
|
|
|
20
23
|
return async references => {
|
|
21
24
|
if (!references?.length) return { attachments: [], content: [], localFiles: [], attachmentManifest: "" };
|
|
22
25
|
if (typeof attachments?.resolve !== "function") {
|
|
23
|
-
throw Object.assign(new Error("File attachments are not configured for this conversation."), { code: "conversation_unsupported", statusCode: 400
|
|
26
|
+
throw Object.assign(new Error("File attachments are not configured for this conversation."), { code: "conversation_unsupported", statusCode: 400,
|
|
27
|
+
messageCode: "assistant.conversation.attachmentsNotConfigured", messageParams: {} });
|
|
24
28
|
}
|
|
25
29
|
const receipts = [];
|
|
26
30
|
const content = [];
|
|
@@ -38,16 +42,19 @@ export function createConversationAttachmentReader({ attachments, context, conve
|
|
|
38
42
|
(local ? !Array.isArray(resolved.localFiles) || resolved.localFiles.length !== attachmentIds.length ||
|
|
39
43
|
resolved.content !== undefined && (!Array.isArray(resolved.content) || resolved.content.length)
|
|
40
44
|
: !Array.isArray(resolved.content) || !resolved.content.length || resolved.content.length > 100)) {
|
|
41
|
-
throw invalid("The attachment resolver must return a receipt for every requested file and bounded model content."
|
|
45
|
+
throw invalid("The attachment resolver must return a receipt for every requested file and bounded model content.",
|
|
46
|
+
{ messageCode: "assistant.conversation.attachmentResolverIncomplete" });
|
|
42
47
|
}
|
|
43
48
|
if (local && !types.includes("localFile")) {
|
|
44
|
-
throw invalid("This engine cannot accept local attachment files. Supply extracted text or supported image bytes."
|
|
49
|
+
throw invalid("This engine cannot accept local attachment files. Supply extracted text or supported image bytes.",
|
|
50
|
+
{ messageCode: "assistant.conversation.attachmentLocalFilesUnsupported" });
|
|
45
51
|
}
|
|
46
52
|
for (const [index, receipt] of resolved.attachments.entries()) {
|
|
47
53
|
if (receipt?.attachmentId !== attachmentIds[index] || typeof receipt.fileName !== "string" || !receipt.fileName.trim() ||
|
|
48
54
|
receipt.fileName.length > 256 || !Number.isSafeInteger(receipt.size) || receipt.size < 0 ||
|
|
49
55
|
receipt.reference !== undefined && (typeof receipt.reference !== "string" || receipt.reference.length > 256)) {
|
|
50
|
-
throw invalid("Attachment receipts require the requested ID, a bounded fileName and a nonnegative size."
|
|
56
|
+
throw invalid("Attachment receipts require the requested ID, a bounded fileName and a nonnegative size.",
|
|
57
|
+
{ messageCode: "assistant.conversation.attachmentReceiptInvalid" });
|
|
51
58
|
}
|
|
52
59
|
receipts.push({ attachmentId: receipt.attachmentId, fileName: receipt.fileName, size: receipt.size,
|
|
53
60
|
...(receipt.reference ? { reference: receipt.reference } : {}) });
|
|
@@ -57,16 +64,18 @@ export function createConversationAttachmentReader({ attachments, context, conve
|
|
|
57
64
|
typeof receipt.reference !== "string" || !receipt.reference.trim() || file.reference !== receipt.reference ||
|
|
58
65
|
typeof file.path !== "string" || !path.isAbsolute(file.path) || file.path.includes("\0") ||
|
|
59
66
|
typeof file.contentType !== "string" || !/^[\w.+-]+\/[\w.+-]+$/u.test(file.contentType)) {
|
|
60
|
-
throw invalid("Local attachment files require their exact requested receipt and an authorized absolute native path."
|
|
67
|
+
throw invalid("Local attachment files require their exact requested receipt and an authorized absolute native path.",
|
|
68
|
+
{ messageCode: "assistant.conversation.attachmentLocalFileInvalid" });
|
|
61
69
|
}
|
|
62
70
|
localFiles.push({ attachmentId: file.attachmentId, fileName: file.fileName, size: file.size,
|
|
63
71
|
reference: file.reference, contentType: file.contentType, path: file.path });
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
for (const part of resolved.content || []) {
|
|
67
|
-
if (!types.includes(part?.type)) throw invalid("This engine cannot accept that attachment format. Supply extracted text or a supported image."
|
|
75
|
+
if (!types.includes(part?.type)) throw invalid("This engine cannot accept that attachment format. Supply extracted text or a supported image.",
|
|
76
|
+
{ messageCode: "assistant.conversation.attachmentFormatUnsupported" });
|
|
68
77
|
if (part.type === "text") {
|
|
69
|
-
if (typeof part.text !== "string" || !part.text.trim()) throw invalid("Attachment text must be nonempty.");
|
|
78
|
+
if (typeof part.text !== "string" || !part.text.trim()) throw invalid("Attachment text must be nonempty.", { messageCode: "assistant.conversation.attachmentTextEmpty" });
|
|
70
79
|
bytes += Buffer.byteLength(part.text);
|
|
71
80
|
content.push({ type: "text", text: part.text });
|
|
72
81
|
} else {
|
|
@@ -75,19 +84,20 @@ export function createConversationAttachmentReader({ attachments, context, conve
|
|
|
75
84
|
!/^[\w.+-]+\/[\w.+-]+$/u.test(part.mediaType) ||
|
|
76
85
|
part.type === "image" && !["image/png", "image/jpeg", "image/webp", "image/gif"].includes(part.mediaType) ||
|
|
77
86
|
part.filename !== undefined && (typeof part.filename !== "string" || part.filename.length > 256)) {
|
|
78
|
-
throw invalid("Attachments require authorized bytes and a supported media type; remote URLs and paths are not accepted."
|
|
87
|
+
throw invalid("Attachments require authorized bytes and a supported media type; remote URLs and paths are not accepted.",
|
|
88
|
+
{ messageCode: "assistant.conversation.attachmentBytesRequired" });
|
|
79
89
|
}
|
|
80
90
|
bytes += data.byteLength;
|
|
81
|
-
if (bytes > maximumBytes) throw invalid("Attachment content exceeds the configured byte limit for this request.");
|
|
91
|
+
if (bytes > maximumBytes) throw invalid("Attachment content exceeds the configured byte limit for this request.", { messageCode: "assistant.conversation.attachmentBytesExceeded" });
|
|
82
92
|
content.push(part.type === "image" ? { type: "image", image: Buffer.from(data), mediaType: part.mediaType }
|
|
83
93
|
: { type: "file", data: Buffer.from(data), mediaType: part.mediaType, ...(part.filename ? { filename: part.filename } : {}) });
|
|
84
94
|
}
|
|
85
|
-
if (bytes > maximumBytes) throw invalid("Attachment content exceeds the configured byte limit for this request.");
|
|
95
|
+
if (bytes > maximumBytes) throw invalid("Attachment content exceeds the configured byte limit for this request.", { messageCode: "assistant.conversation.attachmentBytesExceeded" });
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
98
|
const attachmentManifest = conversationAttachmentManifest(localFiles);
|
|
89
99
|
bytes += Buffer.byteLength(attachmentManifest);
|
|
90
|
-
if (bytes > maximumBytes) throw invalid("Attachment content exceeds the configured byte limit for this request.");
|
|
100
|
+
if (bytes > maximumBytes) throw invalid("Attachment content exceeds the configured byte limit for this request.", { messageCode: "assistant.conversation.attachmentBytesExceeded" });
|
|
91
101
|
await authorize();
|
|
92
102
|
return { attachments: receipts, content, localFiles, attachmentManifest };
|
|
93
103
|
};
|
|
@@ -67,7 +67,8 @@ async function readNativeClaudeCodeAuthStatus({ env, credentialHome, command, co
|
|
|
67
67
|
credentialHome, cwd: credentialHome.home, mode: "capture",
|
|
68
68
|
timeout: 30_000, maxBuffer: 64 * 1024, ...(signal ? { signal } : {})
|
|
69
69
|
});
|
|
70
|
-
const failure = { loggedIn: false, error: result.error
|
|
70
|
+
const failure = result.error ? { loggedIn: false, error: result.error }
|
|
71
|
+
: { loggedIn: false, error: "Claude Code is not connected.", messageCode: "assistant.claude.notConnected", messageParams: {} };
|
|
71
72
|
if (!result.ok && (result.exitCode !== 1 || result.timedOut || result.signal)) return failure;
|
|
72
73
|
try {
|
|
73
74
|
const status = JSON.parse(result.stdout || result.output);
|
|
@@ -77,7 +78,8 @@ async function readNativeClaudeCodeAuthStatus({ env, credentialHome, command, co
|
|
|
77
78
|
return { loggedIn: status.loggedIn === true, email: String(status.email || ""),
|
|
78
79
|
authMethod: String(status.authMethod || ""), subscriptionType: String(status.subscriptionType || "") };
|
|
79
80
|
} catch {
|
|
80
|
-
return result.ok ? { loggedIn: false, error: "Claude Code returned an invalid account status."
|
|
81
|
+
return result.ok ? { loggedIn: false, error: "Claude Code returned an invalid account status.",
|
|
82
|
+
messageCode: "assistant.claude.accountStatusInvalid", messageParams: {} } : failure;
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -108,8 +108,8 @@ export async function runClaudeRenewalTurn(conversation, {
|
|
|
108
108
|
export function createClaudeConversationGoals({ conversationId, readHistory, readState, interrupt, send, readRetainedGoal, saveRetainedGoal }) {
|
|
109
109
|
// These checks run before interrupt/send. Native execution and receipt
|
|
110
110
|
// failures keep their own codes and must not be treated as safe rejections.
|
|
111
|
-
const goalError = message => Object.assign(failure(message, "conversation_invalid_goal"), {
|
|
112
|
-
statusCode: 400, delivery: "not-sent"
|
|
111
|
+
const goalError = (message, { messageCode }) => Object.assign(failure(message, "conversation_invalid_goal"), {
|
|
112
|
+
statusCode: 400, delivery: "not-sent", messageCode, messageParams: {}
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
async function read(history = null) {
|
|
@@ -127,13 +127,14 @@ export function createClaudeConversationGoals({ conversationId, readHistory, rea
|
|
|
127
127
|
const action = input.action;
|
|
128
128
|
if (action !== "set" && (!goal || input.threadId !== conversationId ||
|
|
129
129
|
input.createdAt !== goal.createdAt || input.objective !== goal.objective)) {
|
|
130
|
-
throw goalError("The Claude goal changed. Refresh before trying again.");
|
|
130
|
+
throw goalError("The Claude goal changed. Refresh before trying again.", { messageCode: "assistant.claude.goalChanged" });
|
|
131
131
|
}
|
|
132
|
-
if (input.tokenBudget != null) throw goalError("Claude goals do not support a token budget.");
|
|
132
|
+
if (input.tokenBudget != null) throw goalError("Claude goals do not support a token budget.", { messageCode: "assistant.claude.goalTokenBudgetUnsupported" });
|
|
133
133
|
if (action === "pause" || action === "block") {
|
|
134
134
|
const reason = typeof input.reason === "string" ? input.reason.trim() : "";
|
|
135
135
|
if (action === "block" && (!reason || reason.length > 1000 || goal.status !== "active")) {
|
|
136
|
-
throw goalError("An active Claude goal needs a blocker reason of up to 1,000 characters."
|
|
136
|
+
throw goalError("An active Claude goal needs a blocker reason of up to 1,000 characters.",
|
|
137
|
+
{ messageCode: "assistant.claude.goalBlockerReasonRequired" });
|
|
137
138
|
}
|
|
138
139
|
if (goal.status !== "complete") await saveRetainedGoal({ ...goal,
|
|
139
140
|
status: action === "block" ? "blocked" : "paused", ...(action === "block" ? { reason } : {}) });
|
|
@@ -144,13 +145,13 @@ export function createClaudeConversationGoals({ conversationId, readHistory, rea
|
|
|
144
145
|
} else if (action === "set" || action === "resume") {
|
|
145
146
|
const objective = String((action === "set" ? input.objective : goal.objective) ?? "").trim();
|
|
146
147
|
if (!objective || objective.length > 4000 || /^(clear|stop|off|reset|none|cancel)$/iu.test(objective)) {
|
|
147
|
-
throw goalError("Enter a goal condition of up to 4,000 characters.");
|
|
148
|
+
throw goalError("Enter a goal condition of up to 4,000 characters.", { messageCode: "assistant.claude.goalConditionRequired" });
|
|
148
149
|
}
|
|
149
150
|
if (action === "set" && goal && goal.status !== "complete") {
|
|
150
|
-
throw goalError("Cancel the existing goal before setting another.");
|
|
151
|
+
throw goalError("Cancel the existing goal before setting another.", { messageCode: "assistant.claude.goalAlreadySet" });
|
|
151
152
|
}
|
|
152
153
|
await sendGoal({ message: `/goal ${objective}`, signal });
|
|
153
|
-
} else throw goalError("Unknown Claude goal action.");
|
|
154
|
+
} else throw goalError("Unknown Claude goal action.", { messageCode: "assistant.claude.goalActionUnknown" });
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
return Object.freeze({ read, update });
|
|
@@ -552,7 +553,8 @@ export function createClaudeConversationOwner({
|
|
|
552
553
|
async function compactContext(entry, input = {}) {
|
|
553
554
|
input.signal?.throwIfAborted();
|
|
554
555
|
if (entry.turn?.active || entry.nativeTurn.read().active) {
|
|
555
|
-
throw Object.assign(createError("Claude is still working. Wait for it to finish before freeing its context."), { statusCode: 409
|
|
556
|
+
throw Object.assign(createError("Claude is still working. Wait for it to finish before freeing its context."), { statusCode: 409,
|
|
557
|
+
messageCode: "assistant.claude.compactWhileWorking", messageParams: {} });
|
|
556
558
|
}
|
|
557
559
|
await ensureProcess(entry, input);
|
|
558
560
|
input.signal?.throwIfAborted();
|
|
@@ -1684,7 +1686,8 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1684
1686
|
if (!active || stopped || cancelling) throw Object.assign(
|
|
1685
1687
|
failure("Claude has no active turn for this message.", !active && !stopped && !cancelling
|
|
1686
1688
|
? "conversation_steer_turn_finished" : "assistant_claude_turn_failed"),
|
|
1687
|
-
{ delivery: "not-sent",
|
|
1689
|
+
{ delivery: "not-sent", messageCode: "assistant.claude.noActiveTurn", messageParams: {},
|
|
1690
|
+
...(!active && !stopped && !cancelling ? { statusCode: 409 } : {}) }
|
|
1688
1691
|
);
|
|
1689
1692
|
if (!uuid || typeof accept !== "function" || admissions.has(uuid)) throw new TypeError("A distinct message id and admission writer are required.");
|
|
1690
1693
|
clearFinalAssistantResult();
|
|
@@ -1714,7 +1717,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent = async (
|
|
|
1714
1717
|
const completed = !active && !stopped && !cancelling && outcome?.status === "completed";
|
|
1715
1718
|
throw Object.assign(failure("This Claude turn cannot accept steering now.", completed
|
|
1716
1719
|
? "conversation_steer_turn_finished" : "assistant_claude_turn_failed"),
|
|
1717
|
-
{ delivery: "not-sent", ...(completed ? { statusCode: 409 } : {}) });
|
|
1720
|
+
{ delivery: "not-sent", messageCode: "assistant.claude.steeringUnavailable", messageParams: {}, ...(completed ? { statusCode: 409 } : {}) });
|
|
1718
1721
|
}
|
|
1719
1722
|
// Claude Code acknowledges streamed input as queued and folds it into the
|
|
1720
1723
|
// running turn after the current tool call, or starts it once the main
|
|
@@ -655,6 +655,8 @@ export function createCodexConversationCommands({
|
|
|
655
655
|
return {
|
|
656
656
|
code: `${errorPrefix}codex_detached_thread_delete_unavailable`,
|
|
657
657
|
error: "Codex app-server thread deletion is not available.",
|
|
658
|
+
messageCode: "assistant.codex.threadDeleteUnavailable",
|
|
659
|
+
messageParams: {},
|
|
658
660
|
ok: false,
|
|
659
661
|
statusCode: 409,
|
|
660
662
|
threadId
|
|
@@ -869,6 +871,8 @@ export function createCodexConversationCommands({
|
|
|
869
871
|
return {
|
|
870
872
|
code: `${errorPrefix}temporary_conversation_turn_active`,
|
|
871
873
|
error: "Temporary AI is already working on this conversation.",
|
|
874
|
+
messageCode: "assistant.codex.temporaryConversationBusy",
|
|
875
|
+
messageParams: {},
|
|
872
876
|
ok: false
|
|
873
877
|
};
|
|
874
878
|
}
|
|
@@ -27,11 +27,11 @@ export function createCodexGoalCommands({
|
|
|
27
27
|
|
|
28
28
|
function validateCodexAppServerGoalInput(input = {}) {
|
|
29
29
|
if (!["set", "pause", "resume", "cancel"].includes(input.action)) {
|
|
30
|
-
return { ok: false, error: "Choose set, pause, resume, or cancel." };
|
|
30
|
+
return { ok: false, error: "Choose set, pause, resume, or cancel.", messageCode: "assistant.codex.goalActionInvalid", messageParams: {} };
|
|
31
31
|
}
|
|
32
32
|
if (input.action === "set" && (typeof input.objective !== "string" || !input.objective.trim() ||
|
|
33
33
|
(input.tokenBudget !== undefined && (!Number.isSafeInteger(input.tokenBudget) || input.tokenBudget <= 0)))) {
|
|
34
|
-
return { ok: false, error: "Enter a goal objective and an optional positive whole-number token budget." };
|
|
34
|
+
return { ok: false, error: "Enter a goal objective and an optional positive whole-number token budget.", messageCode: "assistant.codex.goalObjectiveInvalid", messageParams: {} };
|
|
35
35
|
}
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
@@ -46,11 +46,11 @@ export function createCodexGoalCommands({
|
|
|
46
46
|
if (context.ok === false) return context;
|
|
47
47
|
let threadId = context.threadId;
|
|
48
48
|
if (threadId !== input.threadId || (!threadId && input.action !== "set")) {
|
|
49
|
-
return { ok: false, error: "The Codex conversation changed. Refresh the goal before trying again." };
|
|
49
|
+
return { ok: false, error: "The Codex conversation changed. Refresh the goal before trying again.", messageCode: "assistant.codex.goalConversationChanged", messageParams: {} };
|
|
50
50
|
}
|
|
51
51
|
if (input.action === "set") {
|
|
52
52
|
if (turnState(context.session).status === "observation_lost") {
|
|
53
|
-
return { ok: false, error: "Resume or send a message to restore the stopped conversation before setting a goal." };
|
|
53
|
+
return { ok: false, error: "Resume or send a message to restore the stopped conversation before setting a goal.", messageCode: "assistant.codex.goalConversationStopped", messageParams: {} };
|
|
54
54
|
}
|
|
55
55
|
if (!threadId) {
|
|
56
56
|
context = await prepareConversation();
|
|
@@ -59,7 +59,7 @@ export function createCodexGoalCommands({
|
|
|
59
59
|
}
|
|
60
60
|
const { goal: previousGoal } = await context.provider.readGoal(threadId);
|
|
61
61
|
if (previousGoal && (previousGoal.status !== "complete" || previousGoal.createdAt !== input.createdAt)) {
|
|
62
|
-
return { ok: false, error: "The Codex goal changed. Refresh it before setting a new goal." };
|
|
62
|
+
return { ok: false, error: "The Codex goal changed. Refresh it before setting a new goal.", messageCode: "assistant.codex.goalChangedBeforeSet", messageParams: {} };
|
|
63
63
|
}
|
|
64
64
|
signal?.throwIfAborted();
|
|
65
65
|
subscribeCodexAppServerEvents(sessionId, context.provider, threadId, context);
|
|
@@ -74,7 +74,7 @@ export function createCodexGoalCommands({
|
|
|
74
74
|
}
|
|
75
75
|
const { goal } = await context.provider.readGoal(threadId);
|
|
76
76
|
if (!goal || goal.status === "complete" || goal.createdAt !== input.createdAt || goal.objective !== input.objective) {
|
|
77
|
-
return { ok: false, error: "The Codex goal changed. Refresh it before trying again." };
|
|
77
|
+
return { ok: false, error: "The Codex goal changed. Refresh it before trying again.", messageCode: "assistant.codex.goalChanged", messageParams: {} };
|
|
78
78
|
}
|
|
79
79
|
if (input.action === "cancel") {
|
|
80
80
|
subscribeCodexAppServerEvents(sessionId, context.provider, threadId, context);
|
|
@@ -87,14 +87,14 @@ export function createCodexGoalCommands({
|
|
|
87
87
|
return { ok: true, status: "available", threadId, goal: null };
|
|
88
88
|
}
|
|
89
89
|
if (input.action === "pause" && !["active", "paused"].includes(goal.status)) {
|
|
90
|
-
return { ok: false, error: "This goal is already stopped. Refresh its details." };
|
|
90
|
+
return { ok: false, error: "This goal is already stopped. Refresh its details.", messageCode: "assistant.codex.goalAlreadyStopped", messageParams: {} };
|
|
91
91
|
}
|
|
92
92
|
const observationLost = turnState(context.session).status === "observation_lost";
|
|
93
93
|
if (observationLost && turnState(context.session).active) {
|
|
94
|
-
return { ok: false, error: "Codex has not yet confirmed a stop. Retry after the service has stopped." };
|
|
94
|
+
return { ok: false, error: "Codex has not yet confirmed a stop. Retry after the service has stopped.", messageCode: "assistant.codex.goalStopUnconfirmed", messageParams: {} };
|
|
95
95
|
}
|
|
96
96
|
if (input.action === "resume" && !["paused", "blocked", "usageLimited", ...(observationLost ? ["active"] : [])].includes(goal.status)) {
|
|
97
|
-
return { ok: false, error: "This goal cannot be resumed from its current status. Refresh its details." };
|
|
97
|
+
return { ok: false, error: "This goal cannot be resumed from its current status. Refresh its details.", messageCode: "assistant.codex.goalNotResumable", messageParams: {} };
|
|
98
98
|
}
|
|
99
99
|
subscribeCodexAppServerEvents(sessionId, context.provider, threadId, context);
|
|
100
100
|
if (observationLost && input.action === "resume") {
|
|
@@ -10,6 +10,10 @@ export function createCodexHelperOwnershipError({ applicationName = "Application
|
|
|
10
10
|
);
|
|
11
11
|
error.code = `${errorPrefix}codex_helper_ownership_blocked`;
|
|
12
12
|
error.statusCode = 409;
|
|
13
|
+
if (!normalizeText(message)) {
|
|
14
|
+
error.messageCode = "assistant.codex.helperOwnershipBlocked";
|
|
15
|
+
error.messageParams = { applicationName };
|
|
16
|
+
}
|
|
13
17
|
error.retryable = details.retryable !== false;
|
|
14
18
|
error.details = {
|
|
15
19
|
...details,
|
|
@@ -73,6 +73,8 @@ export function createCodexHelperThreadLifecycle({
|
|
|
73
73
|
);
|
|
74
74
|
error.code = `${errorPrefix}codex_helper_thread_unavailable`;
|
|
75
75
|
error.statusCode = 409;
|
|
76
|
+
error.messageCode = "assistant.codex.helperThreadUnavailable";
|
|
77
|
+
error.messageParams = {};
|
|
76
78
|
error.threadId = normalizeText(threadId);
|
|
77
79
|
return error;
|
|
78
80
|
}
|
|
@@ -184,6 +184,8 @@ export function createCodexMessageCommands({
|
|
|
184
184
|
code: steerFailedCode,
|
|
185
185
|
delivered: false,
|
|
186
186
|
error: "The active assistant turn is not ready to accept this message yet.",
|
|
187
|
+
messageCode: "assistant.codex.turnNotReady",
|
|
188
|
+
messageParams: {},
|
|
187
189
|
ok: false,
|
|
188
190
|
operationOutcome: "active_turn_not_ready",
|
|
189
191
|
refreshRecommended: true,
|
|
@@ -266,7 +268,9 @@ export function createCodexMessageCommands({
|
|
|
266
268
|
if (!userRequest && options.preparedInput === undefined) {
|
|
267
269
|
return { value: {
|
|
268
270
|
ok: false,
|
|
269
|
-
error: "Codex message is empty."
|
|
271
|
+
error: "Codex message is empty.",
|
|
272
|
+
messageCode: "assistant.codex.messageEmpty",
|
|
273
|
+
messageParams: {}
|
|
270
274
|
} };
|
|
271
275
|
}
|
|
272
276
|
|
|
@@ -363,6 +367,8 @@ export function createCodexMessageCommands({
|
|
|
363
367
|
return { value: {
|
|
364
368
|
code: steerFailedCode,
|
|
365
369
|
error: "Codex message input is empty.",
|
|
370
|
+
messageCode: "assistant.codex.messageInputEmpty",
|
|
371
|
+
messageParams: {},
|
|
366
372
|
ok: false,
|
|
367
373
|
operationOutcome: "message_empty",
|
|
368
374
|
refreshRecommended: false
|