@jskit-ai/assistant-core 0.2.13 → 0.2.15
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 +346 -0
- package/examples/conversation/package.json +5 -5
- package/locales/en.json +171 -6
- package/package.json +5 -5
- package/src/server/conversation/boundedToolLoop.js +4 -2
- package/src/server/conversation/claudeClient.js +15 -10
- package/src/server/conversation/claudeCommandRun.js +16 -6
- package/src/server/conversation/claudeNativeTurn.js +18 -6
- package/src/server/conversation/claudeProcess.js +20 -8
- package/src/server/conversation/claudeProcessStart.js +1 -1
- package/src/server/conversation/claudeSessionCleanup.js +6 -10
- package/src/server/conversation/claudeTurn.js +3 -3
- package/src/server/conversation/codexAccountReader.js +15 -6
- package/src/server/conversation/codexConversationStops.js +3 -3
- package/src/server/conversation/codexConversations.js +1 -1
- package/src/server/conversation/codexDetachedTurn.js +4 -2
- package/src/server/conversation/codexGoalCommands.js +3 -1
- package/src/server/conversation/codexHelperExecution.js +4 -4
- package/src/server/conversation/codexHelperThreadRecovery.js +2 -2
- package/src/server/conversation/codexHistoryAdapter.js +1 -1
- package/src/server/conversation/codexMessageCommands.js +4 -2
- package/src/server/conversation/codexModelCatalog.js +1 -1
- package/src/server/conversation/codexObservationControl.js +1 -1
- package/src/server/conversation/codexProcess.js +3 -1
- package/src/server/conversation/codexProvider.js +4 -2
- package/src/server/conversation/codexProviderAuth.js +17 -9
- package/src/server/conversation/codexProviderInvalidation.js +6 -2
- package/src/server/conversation/codexProviderThreads.js +4 -2
- package/src/server/conversation/codexProviderTurns.js +4 -2
- package/src/server/conversation/codexRenewalThreads.js +2 -2
- package/src/server/conversation/conversationTurnDelivery.js +3 -1
- package/src/server/conversation/conversationTurns.js +3 -1
- package/src/server/conversation/openCodeProcess.js +5 -9
- package/src/server/conversation/openCodeRuntime.js +3 -1
- package/src/server/conversation/openCodeTurn.js +3 -2
- package/src/server/conversation/providers/claudeDriver.js +12 -4
- package/src/server/conversation/providers/codex.js +8 -4
- package/src/server/conversation/providers/codexDriver.js +23 -10
- package/src/server/conversation/providers/codexDriverConnection.js +18 -6
- package/src/server/conversation/providers/codexDriverTurns.js +3 -1
- package/src/server/conversation/providers/opencode.js +3 -1
- package/src/server/conversation/providers/opencodeDriver.js +3 -1
- package/src/server/conversation/providers/opencodeStandalone.js +37 -13
- package/src/server/conversation/providers/scoped.js +3 -1
- package/src/server/conversation/systemPrompt.js +3 -1
- package/src/server/conversation/tools.js +6 -2
- package/src/shared/assistantSettingsResource.js +14 -2
- package/test/claudeLifecycle.test.js +60 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
52
52
|
"@ai-sdk/xai": "^4.0.58",
|
|
53
53
|
"@jskit-ai/agent": "0.1.0",
|
|
54
|
-
"@jskit-ai/resource-core": "0.1.
|
|
55
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
54
|
+
"@jskit-ai/resource-core": "0.1.197",
|
|
55
|
+
"@jskit-ai/resource-crud-core": "0.1.197",
|
|
56
56
|
"@mdi/js": "^7.4.47",
|
|
57
57
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
58
58
|
"ai": "^7.0.152",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"ws": "^8.20.1"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
65
|
-
"@jskit-ai/kernel": "0.2.
|
|
64
|
+
"@jskit-ai/http-runtime": "0.2.6",
|
|
65
|
+
"@jskit-ai/kernel": "0.2.6",
|
|
66
66
|
"vue": "^3.5.13",
|
|
67
67
|
"vuetify": "^4.0.0"
|
|
68
68
|
},
|
|
@@ -59,8 +59,10 @@ function boundedResponseSchema(outputSchema, descriptors) {
|
|
|
59
59
|
|
|
60
60
|
/** Original bounded response/operation order, with one shared reply/tool protocol. */
|
|
61
61
|
async function runBoundedAssistantToolLoop({ prompt, signal, policy, complete, outputSchema, limitError,
|
|
62
|
-
invalidResponseError = new Error("The assistant returned an invalid response."),
|
|
63
|
-
failureError = new Error("The assistant could not complete this request."),
|
|
62
|
+
invalidResponseError = Object.assign(new Error("The assistant returned an invalid response."), { messageCode: "assistant.turn.invalidResponse", messageParams: {} }),
|
|
63
|
+
failureError = Object.assign(new Error("The assistant could not complete this request."), {
|
|
64
|
+
messageCode: "assistant.turn.requestIncomplete", messageParams: {}
|
|
65
|
+
}), toolCatalog, toolContext,
|
|
64
66
|
completedEnvelope = false, settle }) {
|
|
65
67
|
if (typeof completedEnvelope !== "boolean") throw new TypeError("Completed envelope mode must be a boolean.");
|
|
66
68
|
if (completedEnvelope && (policy.maximumResponses !== COMPLETED_ENVELOPE_RESPONSES || typeof settle !== "function")) {
|
|
@@ -3,8 +3,9 @@ import { TextDecoder } from "node:util";
|
|
|
3
3
|
|
|
4
4
|
const CLAUDE_JSON_MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// messageCode names the sentence, since one protocol code covers several.
|
|
7
|
+
function claudeProtocolError(message, { code = "assistant_claude_protocol_error", params, messageCode } = {}) {
|
|
8
|
+
return Object.assign(new Error(message), { code, ...(params ? { params } : {}), ...(messageCode ? { messageCode, messageParams: {} } : {}) });
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
// Geometric growth keeps fragmented frames linear with bounded memory. Decode complete
|
|
@@ -116,7 +117,7 @@ function createClaudeJsonClient({
|
|
|
116
117
|
}
|
|
117
118
|
const next = writes.then(() => new Promise((resolve, reject) => {
|
|
118
119
|
if (failure || closed || stream.destroyed) {
|
|
119
|
-
reject(failure || claudeProtocolError("Claude connection is closed."));
|
|
120
|
+
reject(failure || claudeProtocolError("Claude connection is closed.", { messageCode: "assistant.claude.connectionClosed" }));
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
123
|
// A write callback, unlike write()'s boolean return, respects backpressure
|
|
@@ -128,7 +129,7 @@ function createClaudeJsonClient({
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
async function request(request, options = {}) {
|
|
131
|
-
if (failure || closed) throw failure || claudeProtocolError("Claude connection is closed.");
|
|
132
|
+
if (failure || closed) throw failure || claudeProtocolError("Claude connection is closed.", { messageCode: "assistant.claude.connectionClosed" });
|
|
132
133
|
const requestId = randomUUID();
|
|
133
134
|
const { onProgress = reportNoProgress } = options;
|
|
134
135
|
function progress(stage) {
|
|
@@ -139,7 +140,9 @@ function createClaudeJsonClient({
|
|
|
139
140
|
const response = new Promise((resolve, reject) => {
|
|
140
141
|
timer = setTimeout(() => {
|
|
141
142
|
requests.delete(requestId);
|
|
142
|
-
reject(claudeProtocolError(`Claude ${request.subtype} timed out.`,
|
|
143
|
+
reject(claudeProtocolError(`Claude ${request.subtype} timed out.`, {
|
|
144
|
+
code: "assistant_claude_control_timeout", params: { request: request.subtype }
|
|
145
|
+
}));
|
|
143
146
|
}, options.timeoutMs ?? timeoutMs);
|
|
144
147
|
requests.set(requestId, { resolve, reject, received: () => progress("response_received") });
|
|
145
148
|
});
|
|
@@ -175,7 +178,7 @@ function createClaudeJsonClient({
|
|
|
175
178
|
// Waiting here does not block the reader from answering interrupt controls.
|
|
176
179
|
await precedingEvents;
|
|
177
180
|
controller.signal.throwIfAborted();
|
|
178
|
-
if (!onControlRequest) throw claudeProtocolError("This control request requires the native Claude Code terminal.");
|
|
181
|
+
if (!onControlRequest) throw claudeProtocolError("This control request requires the native Claude Code terminal.", { messageCode: "assistant.claude.controlNeedsTerminal" });
|
|
179
182
|
const value = await onControlRequest(frame.request, { signal: controller.signal });
|
|
180
183
|
response = { subtype: "success", request_id: id, response: value || {} };
|
|
181
184
|
} catch (error) {
|
|
@@ -223,14 +226,16 @@ function createClaudeJsonClient({
|
|
|
223
226
|
}
|
|
224
227
|
}
|
|
225
228
|
await events;
|
|
226
|
-
if (!closed) throw claudeProtocolError("Claude event connection ended unexpectedly.",
|
|
229
|
+
if (!closed) throw claudeProtocolError("Claude event connection ended unexpectedly.", {
|
|
230
|
+
code: "assistant_claude_observation_lost", messageCode: "assistant.claude.eventConnectionEnded"
|
|
231
|
+
});
|
|
227
232
|
} catch (error) {
|
|
228
233
|
if (!closed) {
|
|
229
234
|
fail(error);
|
|
230
235
|
await onFailure(failure);
|
|
231
236
|
}
|
|
232
237
|
} finally {
|
|
233
|
-
fail(failure || claudeProtocolError("Claude connection is closed."));
|
|
238
|
+
fail(failure || claudeProtocolError("Claude connection is closed.", { messageCode: "assistant.claude.connectionClosed" }));
|
|
234
239
|
}
|
|
235
240
|
})();
|
|
236
241
|
// Callers can await completion; the background observer must never leak an
|
|
@@ -262,7 +267,7 @@ function createClaudeJsonClient({
|
|
|
262
267
|
// Older CLIs treat this control as a plain Stop, cancelling background
|
|
263
268
|
// workers. Require the native capability before writing either frame.
|
|
264
269
|
if (sendNow && !supportsSendNow) throw Object.assign(
|
|
265
|
-
claudeProtocolError("This Claude version does not support targeted Send now."), { delivery: "not-sent" }
|
|
270
|
+
claudeProtocolError("This Claude version does not support targeted Send now.", { messageCode: "assistant.claude.sendNowUnsupported" }), { delivery: "not-sent" }
|
|
266
271
|
);
|
|
267
272
|
await write({ type: "user", uuid: messageId, session_id: sessionId,
|
|
268
273
|
parent_tool_use_id: null, message: { role: "user", content: message } });
|
|
@@ -272,7 +277,7 @@ function createClaudeJsonClient({
|
|
|
272
277
|
/** Closes the connection; pending requests fail. */
|
|
273
278
|
function close() {
|
|
274
279
|
closed = true;
|
|
275
|
-
fail(claudeProtocolError("Claude connection was closed."));
|
|
280
|
+
fail(claudeProtocolError("Claude connection was closed.", { messageCode: "assistant.claude.connectionWasClosed" }));
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
return Object.freeze({
|
|
@@ -41,7 +41,9 @@ export function createClaudeCommandRun({ bound, preparation, send, interrupt })
|
|
|
41
41
|
const active = control.current;
|
|
42
42
|
const applicationCommand = active && (active.tools || active.finishIfCurrent) ? { active, accept: command.accept } : null;
|
|
43
43
|
if (applicationCommand) {
|
|
44
|
-
if (entry.disposed || entry.command && entry.command !== active) throw new Error("This Claude conversation is closed or already working.")
|
|
44
|
+
if (entry.disposed || entry.command && entry.command !== active) throw Object.assign(new Error("This Claude conversation is closed or already working."), {
|
|
45
|
+
messageCode: "assistant.claude.closedOrWorking", messageParams: {}
|
|
46
|
+
});
|
|
45
47
|
if (active.entry && active.entry !== entry) throw new Error("Claude's application tools belong to another native conversation.");
|
|
46
48
|
active.entry = entry;
|
|
47
49
|
entry.command = active;
|
|
@@ -61,12 +63,16 @@ export function createClaudeCommandRun({ bound, preparation, send, interrupt })
|
|
|
61
63
|
|
|
62
64
|
function steer(control, command, options) {
|
|
63
65
|
if (!bound) return dispatch(control, command, options).then(delivered => ({ value: delivered.value }));
|
|
64
|
-
if (!control.current?.steer || !control.current.entry.nativeTurn.read().active) throw new Error("Claude has no active turn to steer.")
|
|
66
|
+
if (!control.current?.steer || !control.current.entry.nativeTurn.read().active) throw Object.assign(new Error("Claude has no active turn to steer."), {
|
|
67
|
+
messageCode: "assistant.claude.noTurnToSteer", messageParams: {}
|
|
68
|
+
});
|
|
65
69
|
return control.current.steer(command);
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
async function run(control, command, { acquire, reportFailure, entry: selectedEntry, maximumOutput, maximumToolCalls = 32, timeoutMs } = {}) {
|
|
69
|
-
if (control.disposed || control.current) throw new Error("This Claude conversation is closed or already working.")
|
|
73
|
+
if (control.disposed || control.current) throw Object.assign(new Error("This Claude conversation is closed or already working."), {
|
|
74
|
+
messageCode: "assistant.claude.closedOrWorking", messageParams: {}
|
|
75
|
+
});
|
|
70
76
|
const { configuration, context, input, tools, signal, beforeDispatch, accept, onMessage, onEvent, onNativeToolUse } = command;
|
|
71
77
|
const completion = Promise.withResolvers();
|
|
72
78
|
completion.promise.catch(() => {});
|
|
@@ -108,7 +114,9 @@ export function createClaudeCommandRun({ bound, preparation, send, interrupt })
|
|
|
108
114
|
try {
|
|
109
115
|
if (bound) {
|
|
110
116
|
entry = selectedEntry;
|
|
111
|
-
if (entry.disposed || entry.command) throw new Error("This Claude conversation is closed or already working.")
|
|
117
|
+
if (entry.disposed || entry.command) throw Object.assign(new Error("This Claude conversation is closed or already working."), {
|
|
118
|
+
messageCode: "assistant.claude.closedOrWorking", messageParams: {}
|
|
119
|
+
});
|
|
112
120
|
active.entry = entry;
|
|
113
121
|
entry.command = active;
|
|
114
122
|
entry.completion = completion;
|
|
@@ -138,12 +146,14 @@ export function createClaudeCommandRun({ bound, preparation, send, interrupt })
|
|
|
138
146
|
problem = dispatchFailure || failure || problem;
|
|
139
147
|
if (!problem && settling?.completion && (bound ? result.status !== "completed" : !result.ok)) {
|
|
140
148
|
// readable-check: allow patched-error - data: status is the native turn's state, not an HTTP status
|
|
141
|
-
problem = Object.assign(new Error(result.error || (bound ? "Claude interrupted this answer." : `Claude turn ${result.status}.`)), {
|
|
149
|
+
problem = Object.assign(new Error(result.error || (bound ? "Claude interrupted this answer." : `Claude turn ${result.status}.`)), {
|
|
150
|
+
status: result.status, ...(bound ? { messageCode: "assistant.claude.answerInterrupted", messageParams: {} } : {})
|
|
151
|
+
});
|
|
142
152
|
}
|
|
143
153
|
break;
|
|
144
154
|
}
|
|
145
155
|
} catch (error) { problem = error; }
|
|
146
|
-
ready.reject(problem || new Error("Claude work ended."));
|
|
156
|
+
ready.reject(problem || Object.assign(new Error("Claude work ended."), { messageCode: "assistant.claude.workEnded", messageParams: {} }));
|
|
147
157
|
signal.removeEventListener("abort", abort);
|
|
148
158
|
if (bound) {
|
|
149
159
|
try {
|
|
@@ -3,8 +3,9 @@ import { claudeMessageBlocks } from "./claudeHistory.js";
|
|
|
3
3
|
import { stopClaudeCodeProcess } from "./claudeProcess.js";
|
|
4
4
|
import { createClaudeConversationAdapter } from "./providers/claude.js";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// messageCode names the sentence, since one turn-failure code covers several.
|
|
7
|
+
export function claudeTurnFailure(message, { code = "assistant_claude_turn_failed", messageCode } = {}) {
|
|
8
|
+
return Object.assign(new Error(message), { code, ...(messageCode ? { messageCode, messageParams: {} } : {}) });
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
// Input for a turn that already finished is the caller's refusal; input that
|
|
@@ -109,7 +110,8 @@ function retainStoppedClaudeMessages(messages, pendingAssistantMessages) {
|
|
|
109
110
|
* The owner's control of one native process, as a native turn uses it.
|
|
110
111
|
* @typedef {import("./providers/claude.js").ClaudeProcessControl & {
|
|
111
112
|
* stopExecution?: (executionId: string, options?: object) => Promise<object>,
|
|
112
|
-
* releaseExecution?: () => Promise<unknown
|
|
113
|
+
* releaseExecution?: () => Promise<unknown>,
|
|
114
|
+
* ownerClosing?: () => boolean
|
|
113
115
|
* }} ClaudeTurnProcess
|
|
114
116
|
*/
|
|
115
117
|
|
|
@@ -275,6 +277,16 @@ export function createClaudeConversationTurn({ conversationId, onEvent, process,
|
|
|
275
277
|
if (active) entry.completion?.resolve({ status: "interrupted", error: reason || "Work stopped." });
|
|
276
278
|
return;
|
|
277
279
|
}
|
|
280
|
+
if (entry.turn?.active && process.ownerClosing()) {
|
|
281
|
+
// At shutdown the turn's end belongs to the next server, as after a
|
|
282
|
+
// crash: its saved record keeps the execution and the active turn, so
|
|
283
|
+
// that server's recovery proves this exit, interrupts the turn and runs
|
|
284
|
+
// the host's checkpoint. Run here, that checkpoint (a Git snapshot, say)
|
|
285
|
+
// would hold the host's bounded shutdown for as long as it takes.
|
|
286
|
+
resolveCompletion({ ...readResult(), status: "interrupted", error: reason });
|
|
287
|
+
if (entry.startupCleanup === startup) entry.startupCleanup = null;
|
|
288
|
+
return entry.exitProof;
|
|
289
|
+
}
|
|
278
290
|
if (entry.turn?.active) {
|
|
279
291
|
try { await updateState("interrupted", reason); }
|
|
280
292
|
catch (error) {
|
|
@@ -358,7 +370,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent, process,
|
|
|
358
370
|
|
|
359
371
|
function checkOutput(value, role = "assistant") {
|
|
360
372
|
if (value.length > (role === "thinking" ? 4 * 1024 * 1024 : maximumOutput)) {
|
|
361
|
-
throw claudeTurnFailure("Claude output exceeded its size limit.");
|
|
373
|
+
throw claudeTurnFailure("Claude output exceeded its size limit.", { messageCode: "assistant.claude.outputTooLarge" });
|
|
362
374
|
}
|
|
363
375
|
}
|
|
364
376
|
|
|
@@ -600,7 +612,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent, process,
|
|
|
600
612
|
}
|
|
601
613
|
|
|
602
614
|
function begin({ id, maxOutputCharacters = 4 * 1024 * 1024 } = {}) {
|
|
603
|
-
if (active) throw claudeTurnFailure("This Claude turn is still working.");
|
|
615
|
+
if (active) throw claudeTurnFailure("This Claude turn is still working.", { messageCode: "assistant.claude.turnStillWorking" });
|
|
604
616
|
if (!id || !Number.isSafeInteger(maxOutputCharacters) || maxOutputCharacters < 1) throw new TypeError("Invalid Claude turn configuration.");
|
|
605
617
|
generation++;
|
|
606
618
|
clearTurnView();
|
|
@@ -615,7 +627,7 @@ export function createClaudeConversationTurn({ conversationId, onEvent, process,
|
|
|
615
627
|
}
|
|
616
628
|
|
|
617
629
|
function reset() {
|
|
618
|
-
if (active || inFlight.size || tasks.size) throw claudeTurnFailure("Stop Claude before resetting its turn view.");
|
|
630
|
+
if (active || inFlight.size || tasks.size) throw claudeTurnFailure("Stop Claude before resetting its turn view.", { messageCode: "assistant.claude.stopBeforeReset" });
|
|
619
631
|
clearTurnView();
|
|
620
632
|
}
|
|
621
633
|
|
|
@@ -134,7 +134,9 @@ export function claudeConnectionIdentity(configRoot, providerId, baseUrl, apiKey
|
|
|
134
134
|
export function claudeModelConfiguration({ providerId, model: modelId }, connection) {
|
|
135
135
|
const provider = nativeAiProvider(providerId);
|
|
136
136
|
const definition = provider?.models.find(({ id }) => id === modelId);
|
|
137
|
-
if (provider && (!definition || !connection?.apiKey)) throw new Error("Choose a connected Claude Code model.")
|
|
137
|
+
if (provider && (!definition || !connection?.apiKey)) throw Object.assign(new Error("Choose a connected Claude Code model."), {
|
|
138
|
+
messageCode: "assistant.claude.chooseConnectedModel", messageParams: {}
|
|
139
|
+
});
|
|
138
140
|
const model = definition?.contextWindow >= 1000000 ? `${definition.id}[1m]` : modelId;
|
|
139
141
|
const externalModel = provider ? model : "";
|
|
140
142
|
// Reset every provider override when returning to the native subscription.
|
|
@@ -328,7 +330,9 @@ export function createClaudeAccountQueries({
|
|
|
328
330
|
processes = noOwnedProcesses,
|
|
329
331
|
isClosing = neverClosing
|
|
330
332
|
}) {
|
|
331
|
-
const error = message => Object.assign(new Error(message), {
|
|
333
|
+
const error = (message, { messageCode }) => Object.assign(new Error(message), {
|
|
334
|
+
code: "claude_account_query_failed", messageCode, messageParams: {}
|
|
335
|
+
});
|
|
332
336
|
const accountProcessStops = new Set();
|
|
333
337
|
const accountReads = new Set();
|
|
334
338
|
let catalog;
|
|
@@ -339,13 +343,17 @@ export function createClaudeAccountQueries({
|
|
|
339
343
|
let revision = 0;
|
|
340
344
|
|
|
341
345
|
function assertCurrent(expectedRevision) {
|
|
342
|
-
if (expectedRevision !== revision)
|
|
343
|
-
|
|
346
|
+
if (expectedRevision !== revision) {
|
|
347
|
+
throw error("Claude account query was cancelled for reconnection.", { messageCode: "assistant.claude.accountQueryCancelled" });
|
|
348
|
+
}
|
|
349
|
+
if (isClosing()) throw error("Claude is reconnecting.", { messageCode: "assistant.claude.reconnecting" });
|
|
344
350
|
}
|
|
345
351
|
|
|
346
352
|
async function stopAccountProcess(native) {
|
|
347
353
|
accountProcessStops.add(native);
|
|
348
|
-
if (!(await native.stop()).scopeEmpty) throw error("Claude account query process cleanup could not be confirmed."
|
|
354
|
+
if (!(await native.stop()).scopeEmpty) throw error("Claude account query process cleanup could not be confirmed.", {
|
|
355
|
+
messageCode: "assistant.claude.accountQueryCleanupUnconfirmed"
|
|
356
|
+
});
|
|
349
357
|
accountProcessStops.delete(native);
|
|
350
358
|
}
|
|
351
359
|
|
|
@@ -406,7 +414,9 @@ export function createClaudeAccountQueries({
|
|
|
406
414
|
const { value, command } = await readAccountProcess(identity, expectedRevision, "catalogue",
|
|
407
415
|
async (native) => ({ value: await native.initialization, command: native.command }));
|
|
408
416
|
const answered = await catalogueRevision(command);
|
|
409
|
-
if (identity !== await accountIdentity(context)) throw error("The signed-in Claude account changed. Refresh the model list."
|
|
417
|
+
if (identity !== await accountIdentity(context)) throw error("The signed-in Claude account changed. Refresh the model list.", {
|
|
418
|
+
messageCode: "assistant.claude.accountChangedRefreshModels"
|
|
419
|
+
});
|
|
410
420
|
assertCurrent(expectedRevision);
|
|
411
421
|
catalog = { identity, command, revision: answered, value };
|
|
412
422
|
return value;
|
|
@@ -480,7 +490,9 @@ export function createClaudeAccountQueries({
|
|
|
480
490
|
revision++;
|
|
481
491
|
// Dropped at once: a read started before this cannot store its answer.
|
|
482
492
|
catalog = null;
|
|
483
|
-
for (const controller of accountReads)
|
|
493
|
+
for (const controller of accountReads) {
|
|
494
|
+
controller.abort(error("Claude account query was cancelled for reconnection.", { messageCode: "assistant.claude.accountQueryCancelled" }));
|
|
495
|
+
}
|
|
484
496
|
await Promise.allSettled([...catalogStarts.values(), ...usageReads]);
|
|
485
497
|
// The account changed: the next request for an account this owner used reads again.
|
|
486
498
|
for (const identity of usageAccounts) planUsageReads.delete(identity);
|
|
@@ -588,7 +600,7 @@ export async function createClaudeCodeProcess({
|
|
|
588
600
|
// execution receipt after startup instead of waiting forever for pipe EOF.
|
|
589
601
|
if (native.exited) void native.exited.then(processExit => {
|
|
590
602
|
if (!stopped && !stopping) stream.destroy(Object.assign(new Error("Claude exited unexpectedly."), {
|
|
591
|
-
code: "assistant_claude_observation_lost", processExit
|
|
603
|
+
code: "assistant_claude_observation_lost", messageCode: "assistant.claude.exitedUnexpectedly", messageParams: {}, processExit
|
|
592
604
|
}));
|
|
593
605
|
}, error => {
|
|
594
606
|
if (!stopped && !stopping) stream.destroy(error);
|
|
@@ -97,7 +97,7 @@ export function createClaudeProcessStart({
|
|
|
97
97
|
if (current && frame.type === "assistant" && !frame.parent_tool_use_id) {
|
|
98
98
|
for (const tool of frame.message?.content || []) if (tool.type === "tool_use" && tool.id && tool.name?.startsWith("mcp__application__")) {
|
|
99
99
|
if (current.nativeToolCount >= current.maximumToolCalls && !current.nativeTools.has(tool.id)) {
|
|
100
|
-
throw new Error("Claude exceeded the application tool-call limit.");
|
|
100
|
+
throw Object.assign(new Error("Claude exceeded the application tool-call limit."), { messageCode: "assistant.claude.toolCallLimit", messageParams: {} });
|
|
101
101
|
}
|
|
102
102
|
if (!current.nativeTools.has(tool.id)) {
|
|
103
103
|
current.nativeToolCount++;
|
|
@@ -36,24 +36,21 @@ export function createClaudeSessionCleanup({
|
|
|
36
36
|
return closing || closingSessions.has(contextKey);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Reconciliation retires the work an earlier server left, as a session's
|
|
40
|
+
// close does: it reopens only saved custody, then stops each of the
|
|
41
|
+
// session's entries whose execution is unproven. Idle saved conversations
|
|
42
|
+
// stay closed and unwritten.
|
|
39
43
|
async function reconcileSessions(sessions, options = {}, application) {
|
|
40
44
|
const results = [];
|
|
41
45
|
for (const session of sessions) {
|
|
42
46
|
const ctx = await application.readContext({ ...options, session, sessionId: session.sessionId });
|
|
43
|
-
await
|
|
47
|
+
await recoverSavedCustody(ctx);
|
|
48
|
+
await recover([...entries.values()].filter((entry) => entry.context.key === ctx.key));
|
|
44
49
|
results.push({ ok: true, sessionId: session.sessionId, resumed: false });
|
|
45
50
|
}
|
|
46
51
|
return { ok: true, results, failed: [], sessionCount: results.length };
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
async function restoreSessionEntries(context, { acquire }) {
|
|
50
|
-
let saved = store.readSessionConversations(context);
|
|
51
|
-
// readable-check: allow method-probe - data: a store lists a session's conversations synchronously or with a promise
|
|
52
|
-
if (saved && typeof saved.then === "function") saved = await saved;
|
|
53
|
-
for (const conversationId of saved.ids) await acquire(saved.context, conversationId);
|
|
54
|
-
return [...entries.values()].filter((entry) => entry.context.key === context.key);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
54
|
// A session's close stops what this server runs for it, then reopens only
|
|
58
55
|
// saved custody an earlier server left. Once the owner is closing for an
|
|
59
56
|
// account change or shutdown it reopens nothing.
|
|
@@ -233,7 +230,6 @@ export function createClaudeSessionCleanup({
|
|
|
233
230
|
closingSessions,
|
|
234
231
|
isClosing,
|
|
235
232
|
reconcileSessions,
|
|
236
|
-
restoreSessionEntries,
|
|
237
233
|
closeSession,
|
|
238
234
|
releaseSession,
|
|
239
235
|
closeProject,
|
|
@@ -318,8 +318,8 @@ export function createClaudeConversationOwner({
|
|
|
318
318
|
if (!saved) return {};
|
|
319
319
|
try { return JSON.parse(saved); }
|
|
320
320
|
catch {
|
|
321
|
-
throw createError(`The saved Claude conversation ${conversationId} is unreadable. Restore its saved conversation state.`,
|
|
322
|
-
{ code: "conversation_claude_saved_state_unreadable" });
|
|
321
|
+
throw Object.assign(createError(`The saved Claude conversation ${conversationId} is unreadable. Restore its saved conversation state.`,
|
|
322
|
+
{ code: "conversation_claude_saved_state_unreadable" }), { params: { conversationId } });
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
|
|
@@ -368,6 +368,7 @@ export function createClaudeConversationOwner({
|
|
|
368
368
|
process: {
|
|
369
369
|
stopExecution: process.stopExecution,
|
|
370
370
|
...(bound ? { releaseExecution: () => store.releaseExecution(entry) } : {}),
|
|
371
|
+
ownerClosing: () => cleanup.closing,
|
|
371
372
|
getProcess: () => entry.stopping ? null : entry.process,
|
|
372
373
|
isActive: () => isTurnActive(entry),
|
|
373
374
|
startProcess: input => processStart.startProcess(entry, input),
|
|
@@ -697,7 +698,6 @@ export function createClaudeConversationOwner({
|
|
|
697
698
|
hasActiveConversation,
|
|
698
699
|
retireConversationHistory,
|
|
699
700
|
reconcileSessions: cleanup.reconcileSessions,
|
|
700
|
-
restoreSessionEntries: cleanup.restoreSessionEntries,
|
|
701
701
|
closeSession: cleanup.closeSession,
|
|
702
702
|
releaseSession: cleanup.releaseSession,
|
|
703
703
|
closeProject: cleanup.closeProject,
|
|
@@ -16,11 +16,15 @@ export function createCodexAccountReader({
|
|
|
16
16
|
identityNamespace = "codex-account-v1",
|
|
17
17
|
secretNamespace = "codex-auth-secret-v1"
|
|
18
18
|
} = {}) {
|
|
19
|
-
|
|
19
|
+
// messageCode names the sentence, since one code covers several.
|
|
20
|
+
function codexAppServerHelperAuthError(code = "", message = "", { messageCode } = {}) {
|
|
20
21
|
const error = new Error(
|
|
21
22
|
normalizeNullableText(message) || "Codex helper authentication is unavailable. Reconnect Codex and retry."
|
|
22
23
|
);
|
|
23
24
|
error.code = normalizeNullableText(code) || `${errorPrefix}codex_helper_auth_unavailable`;
|
|
25
|
+
if (!normalizeNullableText(message)) error.messageCode = "assistant.codex.helperAuthUnavailable";
|
|
26
|
+
else if (messageCode) error.messageCode = messageCode;
|
|
27
|
+
if (error.messageCode) error.messageParams = {};
|
|
24
28
|
return error;
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -61,7 +65,8 @@ export function createCodexAccountReader({
|
|
|
61
65
|
if (!fileStat.isFile() || fileStat.size > CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES) {
|
|
62
66
|
throw codexAppServerHelperAuthError(
|
|
63
67
|
`${errorPrefix}codex_helper_auth_invalid`,
|
|
64
|
-
"The selected Codex authentication record is invalid. Reconnect Codex and retry."
|
|
68
|
+
"The selected Codex authentication record is invalid. Reconnect Codex and retry.",
|
|
69
|
+
{ messageCode: "assistant.codex.authRecordInvalid" }
|
|
65
70
|
);
|
|
66
71
|
}
|
|
67
72
|
const buffer = Buffer.alloc(CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES + 1);
|
|
@@ -76,7 +81,8 @@ export function createCodexAccountReader({
|
|
|
76
81
|
if (offset > CODEX_APP_SERVER_SELECTED_AUTH_MAX_BYTES) {
|
|
77
82
|
throw codexAppServerHelperAuthError(
|
|
78
83
|
`${errorPrefix}codex_helper_auth_invalid`,
|
|
79
|
-
"The selected Codex authentication record is too large. Reconnect Codex and retry."
|
|
84
|
+
"The selected Codex authentication record is too large. Reconnect Codex and retry.",
|
|
85
|
+
{ messageCode: "assistant.codex.authRecordTooLarge" }
|
|
80
86
|
);
|
|
81
87
|
}
|
|
82
88
|
const parsed = JSON.parse(buffer.subarray(0, offset).toString("utf8"));
|
|
@@ -91,7 +97,8 @@ export function createCodexAccountReader({
|
|
|
91
97
|
}
|
|
92
98
|
throw codexAppServerHelperAuthError(
|
|
93
99
|
`${errorPrefix}codex_helper_auth_unavailable`,
|
|
94
|
-
"The selected Codex authentication record could not be read. Reconnect Codex and retry."
|
|
100
|
+
"The selected Codex authentication record could not be read. Reconnect Codex and retry.",
|
|
101
|
+
{ messageCode: "assistant.codex.authRecordUnreadable" }
|
|
95
102
|
);
|
|
96
103
|
} finally {
|
|
97
104
|
await handle?.close().catch(() => null);
|
|
@@ -103,7 +110,8 @@ export function createCodexAccountReader({
|
|
|
103
110
|
if (!toolHomeSource || !path.isAbsolute(toolHomeSource)) {
|
|
104
111
|
throw codexAppServerHelperAuthError(
|
|
105
112
|
`${errorPrefix}codex_helper_auth_unavailable`,
|
|
106
|
-
"The selected Codex account is unavailable. Reconnect Codex and retry."
|
|
113
|
+
"The selected Codex account is unavailable. Reconnect Codex and retry.",
|
|
114
|
+
{ messageCode: "assistant.codex.accountUnavailable" }
|
|
107
115
|
);
|
|
108
116
|
}
|
|
109
117
|
const auth = await readBoundedCodexAuthJson(path.join(toolHomeSource, ".codex", "auth.json"));
|
|
@@ -148,7 +156,8 @@ export function createCodexAccountReader({
|
|
|
148
156
|
}
|
|
149
157
|
throw codexAppServerHelperAuthError(
|
|
150
158
|
`${errorPrefix}codex_helper_auth_invalid`,
|
|
151
|
-
"The selected Codex authentication mode is unsupported. Reconnect Codex and retry."
|
|
159
|
+
"The selected Codex authentication mode is unsupported. Reconnect Codex and retry.",
|
|
160
|
+
{ messageCode: "assistant.codex.authModeUnsupported" }
|
|
152
161
|
);
|
|
153
162
|
}
|
|
154
163
|
|
|
@@ -109,7 +109,7 @@ export function createCodexConversationStops({
|
|
|
109
109
|
if (conversationState) {
|
|
110
110
|
conversationState.status = "interrupted";
|
|
111
111
|
if (conversationState.goal?.status === "active") conversationState.goal = { ...conversationState.goal, status: "paused" };
|
|
112
|
-
conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
|
|
112
|
+
conversationState.watcher?.failNow(Object.assign(new Error("Temporary AI turn was stopped."), { messageCode: "assistant.codex.temporaryTurnStopped", messageParams: {} }));
|
|
113
113
|
conversationState.watcher = null;
|
|
114
114
|
holdWhileActive(conversationState);
|
|
115
115
|
}
|
|
@@ -143,7 +143,7 @@ export function createCodexConversationStops({
|
|
|
143
143
|
});
|
|
144
144
|
if (result.ok !== false && conversationState) {
|
|
145
145
|
conversationState.status = "interrupted";
|
|
146
|
-
conversationState.watcher?.failNow(new Error("Temporary AI turn was stopped."));
|
|
146
|
+
conversationState.watcher?.failNow(Object.assign(new Error("Temporary AI turn was stopped."), { messageCode: "assistant.codex.temporaryTurnStopped", messageParams: {} }));
|
|
147
147
|
conversationState.watcher = null;
|
|
148
148
|
holdWhileActive(conversationState);
|
|
149
149
|
}
|
|
@@ -212,7 +212,7 @@ export function createCodexConversationStops({
|
|
|
212
212
|
if (result.ok !== false && conversation) {
|
|
213
213
|
const { conversationId, sessionKey, conversations } = conversation;
|
|
214
214
|
const state = conversations?.get(conversationId);
|
|
215
|
-
state?.watcher?.failNow(new Error("Temporary AI conversation was closed."));
|
|
215
|
+
state?.watcher?.failNow(Object.assign(new Error("Temporary AI conversation was closed."), { messageCode: "assistant.codex.temporaryConversationClosed", messageParams: {} }));
|
|
216
216
|
if (state) forgetObservation(state);
|
|
217
217
|
conversations?.delete(conversationId);
|
|
218
218
|
if (conversations?.size === 0) {
|
|
@@ -250,7 +250,7 @@ export function createCodexConversationCommands({
|
|
|
250
250
|
const thread = await context.provider.readThreadStatus(conversationId);
|
|
251
251
|
if (codexAppServerThreadRawValue(thread).historyMode !== "paginated") {
|
|
252
252
|
throw Object.assign(new Error("This conversation uses an unsupported Codex history format. Start a fresh conversation."), {
|
|
253
|
-
code: `${errorPrefix}codex_history_unsupported
|
|
253
|
+
code: `${errorPrefix}codex_history_unsupported`, messageCode: "assistant.codex.historyFormatStartFresh", messageParams: {}
|
|
254
254
|
});
|
|
255
255
|
}
|
|
256
256
|
const turns = [];
|
|
@@ -217,7 +217,7 @@ function createCodexAppServerDetachedTurnWatcher(provider, threadId = "", {
|
|
|
217
217
|
rejectWaiter = reject;
|
|
218
218
|
if (timeoutMs > 0) {
|
|
219
219
|
timeout = setTimeout(() => {
|
|
220
|
-
fail(new Error("Timed out waiting for Codex app-server response."));
|
|
220
|
+
fail(Object.assign(new Error("Timed out waiting for Codex app-server response."), { messageCode: "assistant.codex.responseTimeout", messageParams: {} }));
|
|
221
221
|
}, timeoutMs);
|
|
222
222
|
} else {
|
|
223
223
|
// Interactive repair turns may take longer than bounded helper jobs.
|
|
@@ -226,7 +226,9 @@ function createCodexAppServerDetachedTurnWatcher(provider, threadId = "", {
|
|
|
226
226
|
connectionCheck = setInterval(() => {
|
|
227
227
|
if (provider.isAvailable() === false ||
|
|
228
228
|
provider.currentConnectionGeneration() !== generation) {
|
|
229
|
-
fail(new Error("Connection to Codex was lost while the temporary AI was working.")
|
|
229
|
+
fail(Object.assign(new Error("Connection to Codex was lost while the temporary AI was working."), {
|
|
230
|
+
messageCode: "assistant.codex.temporaryConnectionLost", messageParams: {}
|
|
231
|
+
}));
|
|
230
232
|
}
|
|
231
233
|
}, 1000);
|
|
232
234
|
connectionCheck.unref();
|
|
@@ -113,7 +113,9 @@ export function createCodexGoalCommands({
|
|
|
113
113
|
if (recovered.reason === "error") throw new Error(recovered.error);
|
|
114
114
|
await context.runtime.store.mutateSession(sessionId, async () => {
|
|
115
115
|
const run = await readCodexAppServerAgentRunForSession(context.runtime.store, sessionId);
|
|
116
|
-
if (run.active || run.providerThreadId !== threadId) throw new Error("The assistant changed. Refresh before resuming.")
|
|
116
|
+
if (run.active || run.providerThreadId !== threadId) throw Object.assign(new Error("The assistant changed. Refresh before resuming."), {
|
|
117
|
+
messageCode: "assistant.codex.assistantChangedRefresh", messageParams: {}
|
|
118
|
+
});
|
|
117
119
|
await context.runtime.store.writeAgentRunEvent(sessionId, CODEX_APP_SERVER_AGENT_RUN_ID, {
|
|
118
120
|
event: { kind: "codex-observation-explicit-resume" },
|
|
119
121
|
patch: { providerStatus: "interrupted", error: "" }
|
|
@@ -47,15 +47,15 @@ export async function assertCodexAppServerHelperAccountIdentity(provider, expect
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
if (!/^sha256:[a-f0-9]{64}$/u.test(expected) || !provider) {
|
|
50
|
-
throw codexAppServerHelperOwnershipError(
|
|
50
|
+
throw Object.assign(codexAppServerHelperOwnershipError(
|
|
51
51
|
"The requested low-cost assistant account identity is invalid. Refresh the task and retry."
|
|
52
|
-
);
|
|
52
|
+
), { messageCode: "assistant.codex.helperAccountIdentityInvalid", messageParams: {} });
|
|
53
53
|
}
|
|
54
54
|
const actual = normalizeText((await provider.currentRuntimeInfo())?.accountIdentitySignature);
|
|
55
55
|
if (actual !== expected) {
|
|
56
|
-
throw codexAppServerHelperOwnershipError(
|
|
56
|
+
throw Object.assign(codexAppServerHelperOwnershipError(
|
|
57
57
|
"The selected Codex account changed before low-cost assistant work could run. Retry the task with the current account."
|
|
58
|
-
);
|
|
58
|
+
), { messageCode: "assistant.codex.helperAccountChangedBeforeRun", messageParams: {} });
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -66,10 +66,10 @@ export function createCodexHelperThreadRecovery(lifecycle, {
|
|
|
66
66
|
const accountChanged = record.identity.runtime.accountIdentitySignature !==
|
|
67
67
|
normalizeText(currentRuntime.accountIdentitySignature);
|
|
68
68
|
if (accountChanged && !cleanupRequired) {
|
|
69
|
-
throw lifecycle.ownershipError(
|
|
69
|
+
throw Object.assign(lifecycle.ownershipError(
|
|
70
70
|
"Reconnect the original Codex account to resume or delete this helper thread.",
|
|
71
71
|
{ retryable: false, sessionId: record.sessionId, threadId: record.threadId }
|
|
72
|
-
);
|
|
72
|
+
), { messageCode: "assistant.codex.helperThreadReconnectAccount", messageParams: {} });
|
|
73
73
|
}
|
|
74
74
|
if (accountChanged || contextChanged) {
|
|
75
75
|
// Context drift cannot reuse the previous provider configuration.
|
|
@@ -467,7 +467,7 @@ async function prepareCodexHistory(params, client, { baseUrl, modelProviderId, .
|
|
|
467
467
|
if (modelProvider !== "openai") return params;
|
|
468
468
|
const { account } = await client.request("account/read", { refreshToken: false }, options);
|
|
469
469
|
if (!["chatgpt", "apiKey"].includes(account?.type)) {
|
|
470
|
-
throw new Error("Reconnect the OpenAI account before using this model.");
|
|
470
|
+
throw Object.assign(new Error("Reconnect the OpenAI account before using this model."), { messageCode: "assistant.codex.reconnectOpenAi", messageParams: {} });
|
|
471
471
|
}
|
|
472
472
|
return { ...params, config: { ...params.config,
|
|
473
473
|
openai_base_url: `${baseUrl}/${account.type}`
|
|
@@ -83,7 +83,7 @@ export function createCodexMessageCommands({
|
|
|
83
83
|
if (turn.status === "observation_lost") {
|
|
84
84
|
if (turn.active) {
|
|
85
85
|
throw Object.assign(new Error("Codex observation was lost and a stop is not yet verified. Retry after the service has stopped."), {
|
|
86
|
-
code: `${errorPrefix}codex_observation_lost
|
|
86
|
+
code: `${errorPrefix}codex_observation_lost`, messageCode: "assistant.codex.observationLostStopUnverified", messageParams: {}
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
const recovered = await output.submitAssistantResult(sessionId, turn.threadId, turn.turnId, { recoverFromProvider: true });
|
|
@@ -207,7 +207,9 @@ export function createCodexMessageCommands({
|
|
|
207
207
|
workdir: source.workdir ?? workdir
|
|
208
208
|
});
|
|
209
209
|
if (source.requirePaginatedHistory && thread.thread.raw?.historyMode !== "paginated") {
|
|
210
|
-
throw new Error("Codex did not create the required paginated history. Update Codex before starting this conversation.")
|
|
210
|
+
throw Object.assign(new Error("Codex did not create the required paginated history. Update Codex before starting this conversation."), {
|
|
211
|
+
messageCode: "assistant.codex.paginatedHistoryMissing", messageParams: {}
|
|
212
|
+
});
|
|
211
213
|
}
|
|
212
214
|
debugLog("appServerPrompt.stage", {
|
|
213
215
|
durationMs: Date.now() - stageStartedAt, messageId, sessionId, stage: "thread"
|
|
@@ -38,7 +38,7 @@ export function createCodexAppServerModelCatalogCache({
|
|
|
38
38
|
return async function readCodexAppServerModelCatalog(provider, { signal = null } = {}) {
|
|
39
39
|
// readable-check: allow method-probe - safety invariant: a provider without live model discovery is refused with codex_model_catalog_unavailable
|
|
40
40
|
if (typeof provider.listModels !== "function") {
|
|
41
|
-
const error = new Error("Codex live model discovery is unavailable.");
|
|
41
|
+
const error = Object.assign(new Error("Codex live model discovery is unavailable."), { messageCode: "assistant.codex.modelDiscoveryUnavailable", messageParams: {} });
|
|
42
42
|
error.code = `${errorPrefix}codex_model_catalog_unavailable`;
|
|
43
43
|
throw error;
|
|
44
44
|
}
|
|
@@ -38,7 +38,7 @@ export function createCodexObservationControl({
|
|
|
38
38
|
function assertCodexAppServerThreadCanResume(run, threadId) {
|
|
39
39
|
if (run?.providerThreadId === threadId && run.providerStatus === "observation_lost") {
|
|
40
40
|
throw Object.assign(new Error(run.error || "Codex is stopped. Use Resume or Send to continue."), {
|
|
41
|
-
code: `${errorPrefix}codex_observation_lost
|
|
41
|
+
code: `${errorPrefix}codex_observation_lost`, messageCode: "assistant.codex.stoppedUseResume", messageParams: {}
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
}
|