@jini-ai/chat 0.6.6 → 0.6.10
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 +129 -1
- package/dist/core/accepted-input.d.ts +54 -0
- package/dist/core/accepted-input.js +165 -0
- package/dist/core/agent-plugin-chat-state.d.ts +43 -0
- package/dist/core/agent-plugin-chat-state.js +48 -0
- package/dist/core/assistant-client/a2ui-action-poster.js +32 -28
- package/dist/core/assistant-client/messages.d.ts +1 -1
- package/dist/core/assistant-client/messages.js +2 -2
- package/dist/core/assistant-client/transport.d.ts +2 -2
- package/dist/core/assistant-client/transport.js +187 -98
- package/dist/core/assistant-content.d.ts +2 -1
- package/dist/core/assistant-content.js +9 -11
- package/dist/core/browser/page-actions.js +4 -2
- package/dist/core/composer-history.d.ts +16 -9
- package/dist/core/composer-history.js +28 -16
- package/dist/core/conversation-title.d.ts +3 -0
- package/dist/core/conversation-title.js +3 -0
- package/dist/core/durable-projection.js +20 -13
- package/dist/core/durable-runs/continuation.d.ts +4 -0
- package/dist/core/durable-runs/continuation.js +31 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/ports.d.ts +6 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/messages.d.ts +2 -0
- package/dist/core/partial-json.d.ts +6 -7
- package/dist/core/partial-json.js +81 -47
- package/dist/core/persistence/title.js +4 -2
- package/dist/core/question-answer.d.ts +23 -0
- package/dist/core/question-answer.js +66 -0
- package/dist/core/question-form/format.d.ts +6 -6
- package/dist/core/question-form/format.js +16 -17
- package/dist/core/question-form/normalize.d.ts +18 -0
- package/dist/core/question-form/normalize.js +112 -122
- package/dist/core/question-form/scan.js +141 -203
- package/dist/core/run-activity.js +62 -49
- package/dist/core/run-events/entry.d.ts +10 -1
- package/dist/core/run-events/entry.js +117 -87
- package/dist/core/todos.d.ts +1 -1
- package/dist/core/todos.js +60 -85
- package/dist/core/tool-events.js +14 -12
- package/dist/core/transcript.js +62 -16
- package/dist/core/turn-usage.js +54 -32
- package/dist/core/user-text-redaction.js +3 -0
- package/dist/core/util/manifest.js +77 -82
- package/dist/core/util/markdown-context.js +51 -53
- package/dist/core/util/parser.js +136 -144
- package/dist/core/util/pointer.js +16 -16
- package/dist/core/util/strip.js +57 -67
- package/dist/core/util/validate.js +15 -17
- package/dist/embed-widget/browser/session-store.js +21 -12
- package/dist/embed-widget/transports/fetch-sse.js +108 -95
- package/dist/embed-widget/vanilla/index.js +81 -70
- package/dist/react/agent-bridge/frontend-session-bridge.js +80 -52
- package/dist/react/components/AttachmentPreviewModal.js +25 -1
- package/dist/react/components/ChatSettingsDialog.d.ts +13 -0
- package/dist/react/components/ChatSettingsDialog.hooks.d.ts +115 -0
- package/dist/react/components/ChatSettingsDialog.hooks.js +40 -0
- package/dist/react/components/ChatSettingsDialog.js +72 -0
- package/dist/react/components/ChatSettingsSegmented.d.ts +18 -0
- package/dist/react/components/ChatSettingsSegmented.js +11 -0
- package/dist/react/components/Composer.d.ts +5 -1
- package/dist/react/components/Composer.js +126 -82
- package/dist/react/components/ConversationList.js +25 -39
- package/dist/react/components/ConversationTitleEditor.d.ts +27 -0
- package/dist/react/components/ConversationTitleEditor.js +54 -0
- package/dist/react/components/Icon.d.ts +1 -1
- package/dist/react/components/Icon.js +25 -45
- package/dist/react/components/Markdown.js +134 -123
- package/dist/react/components/McpUiSurfaceCard.d.ts +2 -2
- package/dist/react/components/McpUiSurfaceCard.js +30 -9
- package/dist/react/components/MessageList.d.ts +13 -13
- package/dist/react/components/MessageList.hooks.d.ts +6 -1
- package/dist/react/components/MessageList.hooks.js +19 -3
- package/dist/react/components/MessageList.js +34 -10
- package/dist/react/components/MessageRow.d.ts +1 -1
- package/dist/react/components/MessageRow.js +75 -47
- package/dist/react/components/MessageRow.usage.js +16 -11
- package/dist/react/components/MidRunUserMessage.js +16 -3
- package/dist/react/components/QuestionForm.js +148 -76
- package/dist/react/components/QuestionsPanel.js +1 -1
- package/dist/react/components/TodoCard.js +12 -6
- package/dist/react/components/ToolCallGroup.d.ts +4 -3
- package/dist/react/components/ToolCallGroup.js +7 -5
- package/dist/react/components/ToolCard.js +47 -40
- package/dist/react/components/composer-discovery.js +6 -1
- package/dist/react/components/mcp-ui-surface-state.d.ts +6 -11
- package/dist/react/components/mcp-ui-surface-state.js +23 -8
- package/dist/react/ext-event-renderer-registry.d.ts +8 -0
- package/dist/react/ext-event-renderer-registry.js +5 -2
- package/dist/react/features/chat-pane/components/AgentPluginUsage.d.ts +7 -0
- package/dist/react/features/chat-pane/components/AgentPluginUsage.js +45 -0
- package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +55 -19
- package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
- package/dist/react/features/chat-pane/components/ChatPane.js +192 -74
- package/dist/react/features/chat-pane/components/RuntimeDropUp.d.ts +29 -0
- package/dist/react/features/chat-pane/components/RuntimeDropUp.js +45 -0
- package/dist/react/features/chat-pane/create-daemon-attachment-uploader.js +80 -47
- package/dist/react/features/chat-pane/create-mcp-ui-tool-caller.js +44 -40
- package/dist/react/features/chat-pane/create-mid-run-message-poster.d.ts +6 -6
- package/dist/react/features/chat-pane/create-mid-run-message-poster.js +8 -8
- package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +3 -3
- package/dist/react/features/chat-pane/create-typed-answer-poster.js +3 -3
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.d.ts +6 -1
- package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +11 -5
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +12 -7
- package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +340 -149
- package/dist/react/features/chat-pane/hooks/useChatPaneControlsHeight.hooks.js +7 -6
- package/dist/react/features/chat-pane/hooks/useChatPaneNewTabLinks.hooks.js +28 -10
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.d.ts +3 -3
- package/dist/react/features/chat-pane/hooks/useChatPaneRuntimeInventory.hooks.js +60 -12
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.d.ts +49 -0
- package/dist/react/features/chat-pane/hooks/useRuntimeDropUp.hooks.js +143 -0
- package/dist/react/features/chat-pane/model-options.d.ts +14 -0
- package/dist/react/features/chat-pane/model-options.js +45 -25
- package/dist/react/features/chat-pane/rules.d.ts +8 -9
- package/dist/react/features/chat-pane/rules.js +84 -46
- package/dist/react/features/chat-pane/styles.d.ts +1 -1
- package/dist/react/features/chat-pane/styles.js +152 -36
- package/dist/react/features/chat-pane/types.d.ts +19 -9
- package/dist/react/hooks/assistant-chats.js +12 -5
- package/dist/react/hooks/composer-draft-cache.js +116 -118
- package/dist/react/hooks/composer-history-storage.js +5 -5
- package/dist/react/hooks/fab-position.js +7 -4
- package/dist/react/hooks/useAnsweredCards.d.ts +21 -4
- package/dist/react/hooks/useAnsweredCards.js +38 -11
- package/dist/react/hooks/useArtifactStream.js +28 -22
- package/dist/react/hooks/useComposer.d.ts +20 -0
- package/dist/react/hooks/useComposer.js +22 -9
- package/dist/react/hooks/useComposerHistory.hooks.d.ts +6 -1
- package/dist/react/hooks/useComposerHistory.hooks.js +39 -19
- package/dist/react/hooks/useComposerHistoryKeys.hooks.js +24 -14
- package/dist/react/hooks/useConversation.d.ts +7 -0
- package/dist/react/hooks/useConversation.js +23 -10
- package/dist/react/hooks/useExtEventGroups.js +39 -39
- package/dist/react/hooks/useQuestionForms.js +21 -26
- package/dist/react/hooks/useRunStream.js +10 -2
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +9 -0
- package/dist/react/message-blocks.d.ts +6 -4
- package/dist/react/message-blocks.js +94 -72
- package/dist/react/slots.d.ts +2 -2
- package/dist/react/tool-call-display.d.ts +12 -6
- package/dist/react/tool-call-display.js +53 -24
- package/dist/server/durable-runs/recover.js +132 -80
- package/dist/server/run-finalizer/entry.js +25 -15
- package/dist/store/legacy/sqlite/conversations.d.ts +11 -11
- package/dist/store/legacy/sqlite/conversations.js +69 -98
- package/dist/store/legacy/sqlite/messages.d.ts +10 -2
- package/dist/store/legacy/sqlite/messages.js +113 -71
- package/dist/store/runtime/chat-history-store.js +9 -3
- package/dist/store/runtime/durable-run-store.d.ts +2 -2
- package/dist/store/runtime/durable-run-store.js +165 -117
- package/dist/store/runtime/history-tools.js +118 -82
- package/dist/store/runtime/run-ledger.d.ts +4 -1
- package/dist/store/runtime/run-ledger.js +56 -16
- package/dist/store/sql/cursor.js +38 -18
- package/dist/store/sql/store.d.ts +18 -0
- package/dist/store/sql/store.js +46 -15
- package/package.json +18 -16
|
@@ -7,6 +7,33 @@ export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE
|
|
|
7
7
|
// translateRunFrame saves a run error frame as a status label holding only its message: the
|
|
8
8
|
// daemon's MCP_BRIDGE_UNAVAILABLE code is dropped, so the bridge failure is matched by its text.
|
|
9
9
|
const MCP_BRIDGE_FAILURE = /The assistant's tools did not load \(MCP bridge "jini" status: [^\r\n]*/;
|
|
10
|
+
const STOPPED_NOTICE = "Stopped. Saved work is above.";
|
|
11
|
+
/** Index of the last event matching `match`, or -1.
|
|
12
|
+
* @complexity O(e) time, O(1) space, no effects. */
|
|
13
|
+
function lastIndexWhere({ events, match }, _optional = {}) {
|
|
14
|
+
// Reverse scan preserves the last-marker rule on the desktop's ES2022 target.
|
|
15
|
+
for (let index = events.length - 1; index >= 0; index--)
|
|
16
|
+
if (match(events[index]))
|
|
17
|
+
return index;
|
|
18
|
+
return -1;
|
|
19
|
+
}
|
|
20
|
+
/** One saved event's contribution to the executor diagnostics. */
|
|
21
|
+
function diagnosticLines({ event }, _optional = {}) {
|
|
22
|
+
if (event.kind === "status")
|
|
23
|
+
return [event.label, event.detail ?? ""];
|
|
24
|
+
if (event.kind !== "raw")
|
|
25
|
+
return [];
|
|
26
|
+
try {
|
|
27
|
+
const result = JSON.parse(event.line);
|
|
28
|
+
// Successful CLI summaries contain usage metadata and answer prose, not failure evidence.
|
|
29
|
+
if (result?.type === "result" && result.is_error === false)
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Non-JSON stdout can still contain the executor's failure reason.
|
|
34
|
+
}
|
|
35
|
+
return [event.line];
|
|
36
|
+
}
|
|
10
37
|
/** Retryability is independent of session resumability. Inspect executor diagnostics, never
|
|
11
38
|
* answer text or tool results: those can quote unrelated failures from completed work. Old
|
|
12
39
|
* attempt diagnostics remain visible but cannot decide the fate of their continuation. */
|
|
@@ -14,55 +41,36 @@ function attemptDiagnostics({ run }, _optional = {}) {
|
|
|
14
41
|
const events = run.message.events ?? [];
|
|
15
42
|
// Checkpoints merge adjacent text, so a base's event count is not a durable offset. The
|
|
16
43
|
// coordinator's recovery marker separates attempts and survives that compaction.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (event.kind === "status" && event.code === "run_recovering") {
|
|
22
|
-
marker = index;
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return events.slice(marker >= 0 ? marker + 1 : run.attemptBase.length).flatMap((event) => {
|
|
27
|
-
if (event.kind === "status")
|
|
28
|
-
return [event.label, event.detail ?? ""];
|
|
29
|
-
if (event.kind !== "raw")
|
|
30
|
-
return [];
|
|
31
|
-
try {
|
|
32
|
-
const result = JSON.parse(event.line);
|
|
33
|
-
// Successful CLI summaries contain usage metadata and answer prose, not failure evidence.
|
|
34
|
-
if (result?.type === "result" && result.is_error === false)
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// Non-JSON stdout can still contain the executor's failure reason.
|
|
39
|
-
}
|
|
40
|
-
return [event.line];
|
|
41
|
-
}).join("\n");
|
|
44
|
+
const marker = run.attemptBase.length
|
|
45
|
+
? lastIndexWhere({ events, match: (event) => event.kind === "status" && event.code === "run_recovering" })
|
|
46
|
+
: -1;
|
|
47
|
+
return events.slice(marker >= 0 ? marker + 1 : run.attemptBase.length).flatMap((event) => diagnosticLines({ event })).join("\n");
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
/** Quota/billing and bad requests need human repair. A 429 for rate throttling and a 5xx
|
|
50
|
+
* overload remain retryable; the provider's coarse RATE_LIMITED class also includes billing. */
|
|
51
|
+
function needsHumanRepair({ diagnostics }, _optional = {}) {
|
|
52
|
+
const requestStatuses = diagnostics.matchAll(/\b(?:http(?:[ /]?\d(?:\.\d)?)?|(?:status|error|response)(?:[ _-]?code)?|code(?=\s*[:=#]))[\s:=#-]*(4\d\d)\b/gi);
|
|
53
|
+
if ([...requestStatuses].some((match) => match[1] !== "408" && match[1] !== "429"))
|
|
54
|
+
return true;
|
|
55
|
+
return /another answer in this chat is still running|invalid[_ -]?request|context[_ -](length|window)|maximum context|model.*(?:not[_ -]found|does not exist|not supported)|unsupported|does not support|permission denied|forbidden|insufficient[_ -]?(quota|balance|credit|funds)|credit balance is too low|exceeded your current quota|quota[_ -]?exceeded|usage limit|billing|payment required/i.test(diagnostics);
|
|
56
|
+
}
|
|
57
|
+
function attemptFailure({ diagnostics }, _optional = {}) {
|
|
45
58
|
// MCP_BRIDGE_UNAVAILABLE needs bridge repair; another attempt repeats the same failure.
|
|
46
59
|
// Classify it before interpreting the bridge's stderr as an agent auth/watchdog failure.
|
|
47
60
|
if (MCP_BRIDGE_FAILURE.test(diagnostics))
|
|
48
61
|
return "permanent";
|
|
49
62
|
if (/inactivity[_ -]?(timeout|watchdog)|watchdog.*(kill|stop)/i.test(diagnostics))
|
|
50
63
|
return "inactivity-watchdog";
|
|
51
|
-
|
|
52
|
-
if (service === "AGENT_AUTH_REQUIRED")
|
|
64
|
+
if (classifyAgentServiceFailure({ text: diagnostics }) === "AGENT_AUTH_REQUIRED")
|
|
53
65
|
return "auth-failure";
|
|
54
|
-
|
|
55
|
-
// overload remain retryable; the provider's coarse RATE_LIMITED class also includes billing.
|
|
56
|
-
const requestStatuses = diagnostics.matchAll(/\b(?:http(?:[ /]?\d(?:\.\d)?)?|(?:status|error|response)(?:[ _-]?code)?|code(?=\s*[:=#]))[\s:=#-]*(4\d\d)\b/gi);
|
|
57
|
-
if ([...requestStatuses].some((match) => match[1] !== "408" && match[1] !== "429"))
|
|
58
|
-
return "permanent";
|
|
59
|
-
if (/another answer in this chat is still running|invalid[_ -]?request|context[_ -](length|window)|maximum context|model.*(?:not[_ -]found|does not exist|not supported)|unsupported|does not support|permission denied|forbidden|insufficient[_ -]?(quota|balance|credit|funds)|credit balance is too low|exceeded your current quota|quota[_ -]?exceeded|usage limit|billing|payment required/i.test(diagnostics))
|
|
60
|
-
return "permanent";
|
|
61
|
-
return "retryable";
|
|
66
|
+
return needsHumanRepair({ diagnostics }) ? "permanent" : "retryable";
|
|
62
67
|
}
|
|
63
68
|
/** Keep the notice bounded and scoped to the same executor diagnostics as failure classification. */
|
|
69
|
+
function lastDiagnosticLine({ diagnostics }, _optional = {}) {
|
|
70
|
+
return diagnostics.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1)?.slice(0, 300);
|
|
71
|
+
}
|
|
64
72
|
function attemptFailureDetail({ run }, _optional = {}) {
|
|
65
|
-
return attemptDiagnostics({ run }
|
|
73
|
+
return lastDiagnosticLine({ diagnostics: attemptDiagnostics({ run }) });
|
|
66
74
|
}
|
|
67
75
|
function exclusion(run) {
|
|
68
76
|
if (run.cancelReason)
|
|
@@ -71,15 +79,48 @@ function exclusion(run) {
|
|
|
71
79
|
return "excluded";
|
|
72
80
|
return null;
|
|
73
81
|
}
|
|
82
|
+
/** Preflight cards precede model tools, so the normal open-tool activity scan cannot see them.
|
|
83
|
+
* Chat saves the bare resource without its wire toolUseId. Saved consent or subsequent model
|
|
84
|
+
* execution closes this preflight wait, including cards saved before this fix.
|
|
85
|
+
* A model tool closes the wait and no later card can reopen it, so the wait is pending exactly
|
|
86
|
+
* when no tool ran and the last card follows the last consent for this conversation.
|
|
87
|
+
* @complexity O(e) time, O(1) space, no effects.
|
|
88
|
+
*/
|
|
89
|
+
function awaitingAnswer({ run }, _options = {}) {
|
|
90
|
+
if (isAwaitingAnswer({ events: run.message.events }))
|
|
91
|
+
return true;
|
|
92
|
+
const events = run.message.events ?? [];
|
|
93
|
+
if (events.some((event) => event.kind === "tool_use"))
|
|
94
|
+
return false;
|
|
95
|
+
const card = lastIndexWhere({ events, match: (event) => event.kind === "ext" && event.name === "mcp-ui" });
|
|
96
|
+
const consent = lastIndexWhere({ events, match: (event) => event.kind === "ext" && event.name === "agent_plugins"
|
|
97
|
+
&& event.data?.conversationId === run.conversationId });
|
|
98
|
+
return card > consent;
|
|
99
|
+
}
|
|
74
100
|
function budgetNotice(run, now) {
|
|
75
101
|
if (now - run.lastProgressAt >= RUN_STALENESS_MS)
|
|
76
|
-
return
|
|
102
|
+
return awaitingAnswer({ run }) ? AWAITING_ANSWER_NOTICE : SAVED_WORK_NOTICE;
|
|
77
103
|
if (run.recoveryCount >= RECOVERY_LIMIT)
|
|
78
104
|
return EXHAUSTED_NOTICE;
|
|
79
105
|
if (run.recoveryDeadline !== null && now >= run.recoveryDeadline)
|
|
80
106
|
return EXHAUSTED_NOTICE;
|
|
81
107
|
return null;
|
|
82
108
|
}
|
|
109
|
+
/** The exhausted notice's detail: the attempt's last diagnostic, else which budget ended. */
|
|
110
|
+
function exhaustedDetail({ run }, _optional = {}) {
|
|
111
|
+
return attemptFailureDetail({ run }) ?? (run.recoveryCount >= RECOVERY_LIMIT ? `Tried ${RECOVERY_LIMIT} times` : "Recovery window ended");
|
|
112
|
+
}
|
|
113
|
+
/** Saved work to carry into the next attempt, seeding the saved content when no text event holds it. */
|
|
114
|
+
function savedWork({ run }, _optional = {}) {
|
|
115
|
+
const saved = [...(run.message.events ?? [])];
|
|
116
|
+
if (!saved.some((event) => event.kind === "text") && run.message.content)
|
|
117
|
+
saved.unshift({ kind: "text", text: run.message.content });
|
|
118
|
+
return saved;
|
|
119
|
+
}
|
|
120
|
+
/** A dead attempt continues now; a fresh queued one waits (see the caller's comment). */
|
|
121
|
+
function continuesNow({ run, probe, trigger, now }, _optional = {}) {
|
|
122
|
+
return probe === "dead" && (trigger === "attempt-failed" || now - run.attemptStartedAt >= 10_000);
|
|
123
|
+
}
|
|
83
124
|
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
|
|
84
125
|
* Each recovery decision logs one `logger.info` line (default console) of run metadata. */
|
|
85
126
|
export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
@@ -98,6 +139,16 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
98
139
|
return false;
|
|
99
140
|
return ports.verifyChildDead(run.child, {}).catch(() => false);
|
|
100
141
|
}
|
|
142
|
+
async function launchNext({ run, next, native }, _optional = {}) {
|
|
143
|
+
try {
|
|
144
|
+
await ports.launch({ run: next, request: continuationRequest({ run: { ...run, message: next.message, transcript: next.transcript }, native }, {}) }, {});
|
|
145
|
+
await ports.store.recoveryClock({ runId: next.runId, now: ports.now(), active: false }, {});
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// A rejected/lost start response is uncertain: the daemon may already be executing. The
|
|
149
|
+
// same idempotent attempt is probed by the attached watcher before another CAS is possible.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
101
152
|
async function continueDead(run) {
|
|
102
153
|
await ports.cancelAttempt(run, {}).catch(() => undefined);
|
|
103
154
|
// A locator captured before a hard cancel can outlive its rollout. Reuse reconstruction,
|
|
@@ -105,10 +156,7 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
105
156
|
const missingResume = run.request.agentId === 'codex' && /no rollout found for thread id/.test(attemptDiagnostics({ run }, {}));
|
|
106
157
|
const native = !missingResume && await nativeAllowed(run);
|
|
107
158
|
const nextRunId = ports.mintRunId();
|
|
108
|
-
const
|
|
109
|
-
if (!saved.some((event) => event.kind === "text") && run.message.content)
|
|
110
|
-
saved.unshift({ kind: "text", text: run.message.content });
|
|
111
|
-
const events = recoveredRunEvents({ saved }, { notice: missingResume ? 'silent' : 'visible' });
|
|
159
|
+
const events = recoveredRunEvents({ saved: savedWork({ run }) }, { notice: missingResume ? 'silent' : 'visible' });
|
|
112
160
|
if (!await ports.store.advance({ run, nextRunId, now: ports.now(), events }, {}))
|
|
113
161
|
return "superseded";
|
|
114
162
|
const next = await ports.store.load({ messageId: run.messageId }, {});
|
|
@@ -117,41 +165,46 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
117
165
|
if (!next || next.runId !== nextRunId)
|
|
118
166
|
return "superseded";
|
|
119
167
|
if (next.cancelReason)
|
|
120
|
-
return finalize(next,
|
|
121
|
-
|
|
122
|
-
await ports.launch({ run: next, request: continuationRequest({ run: { ...run, message: next.message, transcript: next.transcript }, native }, {}) }, {});
|
|
123
|
-
await ports.store.recoveryClock({ runId: next.runId, now: ports.now(), active: false }, {});
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// A rejected/lost start response is uncertain: the daemon may already be executing. The
|
|
127
|
-
// same idempotent attempt is probed by the attached watcher before another CAS is possible.
|
|
128
|
-
}
|
|
168
|
+
return finalize(next, STOPPED_NOTICE, { canceled: true });
|
|
169
|
+
await launchNext({ run, next, native });
|
|
129
170
|
ports.attach(next, {});
|
|
130
171
|
return "continued";
|
|
131
172
|
}
|
|
132
|
-
async function
|
|
133
|
-
if (exclusion(run))
|
|
134
|
-
return finalize(run, "Stopped. Saved work is above.", { canceled: true });
|
|
173
|
+
async function probeAttempt({ run, trigger, liveRunId }, _optional = {}) {
|
|
135
174
|
// A terminal executor end confirms this attempt is dead even when the daemon retains its
|
|
136
175
|
// replayable record (HTTP 200). Other triggers must obtain fresh liveness evidence.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
176
|
+
if (trigger === "attempt-failed")
|
|
177
|
+
return "dead";
|
|
178
|
+
return liveRunId === run.runId ? "live" : ports.probe(run, {});
|
|
179
|
+
}
|
|
180
|
+
async function reattach(run) {
|
|
181
|
+
await ports.store.recoveryClock({ runId: run.runId, now: ports.now(), active: false }, {});
|
|
182
|
+
ports.attach(run, {});
|
|
183
|
+
return "reattached";
|
|
184
|
+
}
|
|
185
|
+
async function finalizeFailure({ run, failure, diagnostics }, _optional = {}) {
|
|
186
|
+
if (failure !== "permanent")
|
|
187
|
+
return finalize(run, failure === "auth-failure" ? "Not logged in. Saved work is above." : STOPPED_NOTICE, { canceled: true });
|
|
188
|
+
// A bridge failure shows its own reason (and stderr): the generic notice hides what needs repair.
|
|
189
|
+
const bridgeNotice = diagnostics.match(MCP_BRIDGE_FAILURE)?.[0];
|
|
190
|
+
return finalize(run, bridgeNotice ?? EXHAUSTED_NOTICE, { detail: bridgeNotice ? undefined : lastDiagnosticLine({ diagnostics }) });
|
|
191
|
+
}
|
|
192
|
+
async function decide(run, trigger, liveRunId) {
|
|
193
|
+
if (exclusion(run))
|
|
194
|
+
return { decision: await finalize(run, STOPPED_NOTICE, { canceled: true }) };
|
|
195
|
+
const probe = await probeAttempt({ run, trigger, liveRunId });
|
|
196
|
+
if (probe === "live")
|
|
197
|
+
return { probe, decision: await reattach(run) };
|
|
198
|
+
// A pending human answer takes precedence over executor exit diagnostics.
|
|
199
|
+
if (probe === "dead" && awaitingAnswer({ run }))
|
|
200
|
+
return { probe, decision: await resolveMissing(run, probe, trigger) };
|
|
143
201
|
// Diagnostics decide only the fate of an attempt that is no longer running. Read before the
|
|
144
202
|
// probe, raw stdout quoting a tool description ("OAuth-authenticated") canceled a live turn
|
|
145
203
|
// as "Not logged in" (luvira, 2026-10-08).
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (failure === "permanent") {
|
|
151
|
-
const bridgeNotice = attemptDiagnostics({ run }, {}).match(MCP_BRIDGE_FAILURE)?.[0];
|
|
152
|
-
return finalize(run, bridgeNotice ?? EXHAUSTED_NOTICE, { detail: bridgeNotice ? undefined : attemptFailureDetail({ run }, {}) });
|
|
153
|
-
}
|
|
154
|
-
return resolveMissing(run, probe, trigger);
|
|
204
|
+
const diagnostics = attemptDiagnostics({ run });
|
|
205
|
+
const failure = attemptFailure({ diagnostics });
|
|
206
|
+
const decision = failure === "retryable" ? await resolveMissing(run, probe, trigger) : await finalizeFailure({ run, failure, diagnostics });
|
|
207
|
+
return { probe, failure, decision };
|
|
155
208
|
}
|
|
156
209
|
async function resolveMissing(run, probe, trigger) {
|
|
157
210
|
await ports.store.recoveryClock({ runId: run.runId, now: ports.now(), active: true }, {});
|
|
@@ -160,16 +213,16 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
160
213
|
// a newer attempt committed by another caller while this one was awaiting storage/HTTP.
|
|
161
214
|
if (timed.runId !== run.runId)
|
|
162
215
|
return "superseded";
|
|
216
|
+
// A dead process cannot resume its question. End this turn with the existing card intact,
|
|
217
|
+
// rather than minting an attempt that runs preflight and asks the same question again.
|
|
218
|
+
if (probe === "dead" && awaitingAnswer({ run: timed }))
|
|
219
|
+
return finalize(timed, AWAITING_ANSWER_NOTICE);
|
|
163
220
|
const notice = budgetNotice(timed, ports.now());
|
|
164
|
-
if (notice)
|
|
165
|
-
|
|
166
|
-
? attemptFailureDetail({ run: timed }, {}) ?? (timed.recoveryCount >= RECOVERY_LIMIT ? `Tried ${RECOVERY_LIMIT} times` : "Recovery window ended")
|
|
167
|
-
: undefined;
|
|
168
|
-
return finalize(run, notice, { detail });
|
|
169
|
-
}
|
|
221
|
+
if (notice)
|
|
222
|
+
return finalize(run, notice, { detail: notice === EXHAUSTED_NOTICE ? exhaustedDetail({ run: timed }) : undefined });
|
|
170
223
|
// A queued attempt may be between durable acceptance and the daemon's start transaction.
|
|
171
224
|
// Give that idempotent launch time to appear before a 404 can advance its generation.
|
|
172
|
-
if (
|
|
225
|
+
if (continuesNow({ run, probe, trigger, now: ports.now() }))
|
|
173
226
|
return continueDead(timed);
|
|
174
227
|
ports.attach(run, {});
|
|
175
228
|
return "waiting";
|
|
@@ -183,12 +236,11 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
183
236
|
return "terminal";
|
|
184
237
|
if (expectedRunId && expectedRunId !== run.runId)
|
|
185
238
|
return "superseded";
|
|
186
|
-
const
|
|
187
|
-
const decision = await decide(run, trigger, options.liveRunId, seen);
|
|
239
|
+
const { decision, probe, failure } = await decide(run, trigger, options.liveRunId);
|
|
188
240
|
// The watcher re-confirms its own live run every resolution window; log only real recovery
|
|
189
241
|
// decisions. Metadata only: diagnostics, answers and cancel reasons can hold secrets.
|
|
190
242
|
if (options.liveRunId !== run.runId)
|
|
191
|
-
logger.info("[durable-recovery] " + JSON.stringify({ runId: run.runId, trigger, probe
|
|
243
|
+
logger.info("[durable-recovery] " + JSON.stringify({ runId: run.runId, trigger, probe, decision, failureClass: failure }));
|
|
192
244
|
return decision;
|
|
193
245
|
},
|
|
194
246
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { isTerminalRunStatus } from '../../core/messages.js';
|
|
2
2
|
import { readSseFrames, runContentFromEvents, runEventsForSave, runInterruptedNotice, translateRunFrame } from '../../core/run-events/entry.js';
|
|
3
|
+
/** An error frame keeps a later successful end from settling as a success. */
|
|
4
|
+
function endStatus({ failed, terminal }, _optional = {}) {
|
|
5
|
+
return failed && terminal === "succeeded" ? "failed" : terminal;
|
|
6
|
+
}
|
|
3
7
|
/** Follow replayable run streams and save through atomic host ledger operations.
|
|
4
8
|
* The host retains authorization, owner scope, kernel transactions, boot reconciliation, HTTP
|
|
5
9
|
* credentials and excluded run-ID prefixes. Replay starts at zero; error-before-success and
|
|
@@ -22,11 +26,14 @@ export function createAssistantRunFinalizer(args, options = {}) {
|
|
|
22
26
|
catch { /* The host owns reporting failures. */ }
|
|
23
27
|
}
|
|
24
28
|
const active = new Map();
|
|
25
|
-
|
|
26
|
-
watch.terminal = true;
|
|
29
|
+
function clearTrailing(watch) {
|
|
27
30
|
if (watch.trailing)
|
|
28
31
|
scheduler.cancel({ handle: watch.trailing.handle });
|
|
29
32
|
watch.trailing = undefined;
|
|
33
|
+
}
|
|
34
|
+
async function settle(watch, status, events) {
|
|
35
|
+
watch.terminal = true;
|
|
36
|
+
clearTrailing(watch);
|
|
30
37
|
await args.ledger.settle({
|
|
31
38
|
conversationId: watch.conversationId,
|
|
32
39
|
messageId: watch.messageId,
|
|
@@ -57,9 +64,7 @@ export function createAssistantRunFinalizer(args, options = {}) {
|
|
|
57
64
|
}
|
|
58
65
|
return;
|
|
59
66
|
}
|
|
60
|
-
|
|
61
|
-
scheduler.cancel({ handle: watch.trailing.handle });
|
|
62
|
-
watch.trailing = undefined;
|
|
67
|
+
clearTrailing(watch);
|
|
63
68
|
watch.checkpointedEvents = watch.events.length;
|
|
64
69
|
watch.checkpointedAt = now();
|
|
65
70
|
watch.checkpointPending = true;
|
|
@@ -77,29 +82,36 @@ export function createAssistantRunFinalizer(args, options = {}) {
|
|
|
77
82
|
void checkpoint(watch);
|
|
78
83
|
});
|
|
79
84
|
}
|
|
80
|
-
|
|
85
|
+
/** The connection's event body, or why it cannot be followed. Non-success bodies are cancelled. */
|
|
86
|
+
async function openStream(watch) {
|
|
81
87
|
const response = await daemon.openEvents({ runId: watch.runId, principalId: watch.principalId });
|
|
82
|
-
if (response.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
88
|
+
if (response.ok && response.body)
|
|
89
|
+
return { kind: "open", body: response.body };
|
|
90
|
+
await response.body?.cancel().catch(() => undefined);
|
|
91
|
+
return response.status === 404 ? { kind: "gone" } : { kind: "dropped" };
|
|
92
|
+
}
|
|
93
|
+
async function consumeFrames(watch, body) {
|
|
86
94
|
// Each connection replays the complete history, rather than appending duplicate deltas.
|
|
87
95
|
watch.events = [];
|
|
88
96
|
watch.failed = false;
|
|
89
97
|
const carry = {};
|
|
90
|
-
for await (const frame of readSseFrames({ body
|
|
98
|
+
for await (const frame of readSseFrames({ body })) {
|
|
91
99
|
const outcome = translateRunFrame({ kind: frame.event, raw: frame.data, notices: args.notices }, { carry });
|
|
92
100
|
watch.events.push(...outcome.events);
|
|
93
101
|
if (outcome.error)
|
|
94
102
|
watch.failed = true;
|
|
95
103
|
if (outcome.terminal) {
|
|
96
104
|
watch.terminal = true;
|
|
97
|
-
return { kind: "ended", status: watch.failed
|
|
105
|
+
return { kind: "ended", status: endStatus({ failed: watch.failed, terminal: outcome.terminal }) };
|
|
98
106
|
}
|
|
99
107
|
void checkpoint(watch);
|
|
100
108
|
}
|
|
101
109
|
return { kind: "dropped" };
|
|
102
110
|
}
|
|
111
|
+
async function readStream(watch) {
|
|
112
|
+
const opened = await openStream(watch);
|
|
113
|
+
return opened.kind === "open" ? consumeFrames(watch, opened.body) : opened;
|
|
114
|
+
}
|
|
103
115
|
async function follow(watch) {
|
|
104
116
|
for (let attempt = 0; attempt <= maxReconnects; attempt += 1) {
|
|
105
117
|
const result = await readStream(watch).catch(() => ({ kind: "dropped" }));
|
|
@@ -140,9 +152,7 @@ export function createAssistantRunFinalizer(args, options = {}) {
|
|
|
140
152
|
})
|
|
141
153
|
.finally(() => {
|
|
142
154
|
watch.terminal = true;
|
|
143
|
-
|
|
144
|
-
scheduler.cancel({ handle: watch.trailing.handle });
|
|
145
|
-
watch.trailing = undefined;
|
|
155
|
+
clearTrailing(watch);
|
|
146
156
|
active.delete(runId);
|
|
147
157
|
});
|
|
148
158
|
active.set(runId, { watch, done });
|
|
@@ -18,8 +18,8 @@ export declare function listConversations({ db, projectId }: {
|
|
|
18
18
|
}): {
|
|
19
19
|
latestRun: {
|
|
20
20
|
durationMs?: number;
|
|
21
|
-
endedAt?: number
|
|
22
|
-
startedAt?: number
|
|
21
|
+
endedAt?: number;
|
|
22
|
+
startedAt?: number;
|
|
23
23
|
status: string;
|
|
24
24
|
} | undefined;
|
|
25
25
|
totalDurationMs?: number;
|
|
@@ -41,13 +41,13 @@ export declare function getConversation({ db, id }: {
|
|
|
41
41
|
db: SqliteDb;
|
|
42
42
|
id: string;
|
|
43
43
|
}): {
|
|
44
|
-
totalDurationMs?: number;
|
|
45
44
|
latestRun: {
|
|
46
45
|
durationMs?: number;
|
|
47
|
-
endedAt?: number
|
|
48
|
-
startedAt?: number
|
|
46
|
+
endedAt?: number;
|
|
47
|
+
startedAt?: number;
|
|
49
48
|
status: string;
|
|
50
49
|
} | undefined;
|
|
50
|
+
totalDurationMs?: number;
|
|
51
51
|
id: any;
|
|
52
52
|
projectId: any;
|
|
53
53
|
title: any;
|
|
@@ -75,13 +75,13 @@ export declare function insertConversation({ db, c }: {
|
|
|
75
75
|
db: SqliteDb;
|
|
76
76
|
c: DbRow;
|
|
77
77
|
}): {
|
|
78
|
-
totalDurationMs?: number;
|
|
79
78
|
latestRun: {
|
|
80
79
|
durationMs?: number;
|
|
81
|
-
endedAt?: number
|
|
82
|
-
startedAt?: number
|
|
80
|
+
endedAt?: number;
|
|
81
|
+
startedAt?: number;
|
|
83
82
|
status: string;
|
|
84
83
|
} | undefined;
|
|
84
|
+
totalDurationMs?: number;
|
|
85
85
|
id: any;
|
|
86
86
|
projectId: any;
|
|
87
87
|
title: any;
|
|
@@ -102,13 +102,13 @@ export declare function updateConversation({ db, id, patch }: {
|
|
|
102
102
|
id: string;
|
|
103
103
|
patch: DbRow;
|
|
104
104
|
}): {
|
|
105
|
-
totalDurationMs?: number;
|
|
106
105
|
latestRun: {
|
|
107
106
|
durationMs?: number;
|
|
108
|
-
endedAt?: number
|
|
109
|
-
startedAt?: number
|
|
107
|
+
endedAt?: number;
|
|
108
|
+
startedAt?: number;
|
|
110
109
|
status: string;
|
|
111
110
|
} | undefined;
|
|
111
|
+
totalDurationMs?: number;
|
|
112
112
|
id: any;
|
|
113
113
|
projectId: any;
|
|
114
114
|
title: any;
|