@jini-ai/chat 0.6.5 → 0.6.7
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 +12 -0
- 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 +184 -97
- 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 +35 -6
- package/dist/core/durable-runs/ledger-contracts.d.ts +2 -1
- package/dist/core/durable-runs/policies.d.ts +1 -0
- package/dist/core/durable-runs/policies.js +1 -0
- 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.d.ts +2 -1
- package/dist/core/turn-usage.js +56 -27
- 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.js +48 -32
- package/dist/react/components/MessageRow.usage.d.ts +5 -2
- package/dist/react/components/MessageRow.usage.js +20 -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/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 +18 -1
- package/dist/react/features/chat-pane/model-options.js +49 -20
- 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 +156 -37
- package/dist/react/features/chat-pane/types.d.ts +17 -8
- 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.js +19 -17
- package/dist/server/durable-runs/recover.d.ts +1 -1
- package/dist/server/durable-runs/recover.js +139 -76
- 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 +12 -10
|
@@ -1,4 +1,84 @@
|
|
|
1
1
|
import { assistantContentFromEvents, isReplyBoundary, legacyAssistantContentFromEvents } from '../core/index.js';
|
|
2
|
+
/** Whether there is anything besides plain text to place: a tool row, an ext event, or (when asked
|
|
3
|
+
* for) non-blank thinking. With none of these the flat path is identical. */
|
|
4
|
+
function hasSomethingToInterleave({ events, rows }, { includeThinking }) {
|
|
5
|
+
if (rows.length > 0)
|
|
6
|
+
return true;
|
|
7
|
+
return events.some((ev) => ev.kind === 'ext' || (includeThinking && ev.kind === 'thinking' && ev.text.trim() !== ''));
|
|
8
|
+
}
|
|
9
|
+
/** Whether `content` is demonstrably built from the text events. */
|
|
10
|
+
function contentMatchesEvents({ events, content }) {
|
|
11
|
+
// Both producers' shapes count: today's (a paragraph break between steps, see
|
|
12
|
+
// `assistantContentFromEvents`) and the old plain concatenation older saved rows carry.
|
|
13
|
+
return content === assistantContentFromEvents({ events }) || content === legacyAssistantContentFromEvents({ events });
|
|
14
|
+
}
|
|
15
|
+
function flushText({ state }) {
|
|
16
|
+
if (state.buffer.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
state.blocks.push({ kind: 'text', text: state.buffer, key: `text-${state.blocks.length}` });
|
|
19
|
+
state.buffer = '';
|
|
20
|
+
}
|
|
21
|
+
function placeThinking({ state, text }) {
|
|
22
|
+
flushText({ state });
|
|
23
|
+
const { blocks } = state;
|
|
24
|
+
const last = blocks[blocks.length - 1];
|
|
25
|
+
if (last && last.kind === 'thinking')
|
|
26
|
+
blocks[blocks.length - 1] = { kind: 'thinking', text: last.text + text, key: last.key };
|
|
27
|
+
else
|
|
28
|
+
blocks.push({ kind: 'thinking', text, key: `thinking-${blocks.length}` });
|
|
29
|
+
}
|
|
30
|
+
function placeReplyEnd({ state }) {
|
|
31
|
+
flushText({ state });
|
|
32
|
+
state.blocks.push({ kind: 'reply-end', index: state.replies, key: `reply-end-${state.replies}` });
|
|
33
|
+
state.replies += 1;
|
|
34
|
+
}
|
|
35
|
+
function placeExt({ state, ev }, { slotOf }) {
|
|
36
|
+
// One block per SLOT, at its first occurrence — later events in the same slot fold into it
|
|
37
|
+
// (`useExtEventGroups`'s job in `MessageRow.tsx`), not a second block. A slot is the `name`
|
|
38
|
+
// unless its renderer keys events into separate surfaces (see `ExtEventSlotKey`).
|
|
39
|
+
const slot = slotOf(ev);
|
|
40
|
+
if (state.emittedExtSlots.has(slot))
|
|
41
|
+
return;
|
|
42
|
+
state.emittedExtSlots.add(slot);
|
|
43
|
+
flushText({ state });
|
|
44
|
+
state.blocks.push({ kind: 'ext', name: ev.name, slot, key: `ext-${slot}` });
|
|
45
|
+
}
|
|
46
|
+
function placeToolUse({ state, id }) {
|
|
47
|
+
const row = state.rowById.get(id);
|
|
48
|
+
// A `tool_use` the timeline did not surface (deduped away as a repeat of one already seen).
|
|
49
|
+
// Skipping it keeps this function's output consistent with `rows`, which is the authority on
|
|
50
|
+
// what gets a card.
|
|
51
|
+
if (!row || state.emitted.has(id))
|
|
52
|
+
return;
|
|
53
|
+
state.emitted.add(id);
|
|
54
|
+
flushText({ state });
|
|
55
|
+
const { blocks } = state;
|
|
56
|
+
const last = blocks[blocks.length - 1];
|
|
57
|
+
if (last && last.kind === 'tools') {
|
|
58
|
+
blocks[blocks.length - 1] = { kind: 'tools', rows: [...last.rows, row], key: last.key };
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
blocks.push({ kind: 'tools', rows: [row], key: `tools-${blocks.length}` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function placeEvent({ state, ev }, { slotOf, includeThinking }) {
|
|
65
|
+
if (ev.kind === 'text') {
|
|
66
|
+
state.buffer += ev.text;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (ev.kind === 'thinking') {
|
|
70
|
+
if (includeThinking)
|
|
71
|
+
placeThinking({ state, text: ev.text });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Emitted per boundary, before any slot dedupe below, so every reply gets its own end marker.
|
|
75
|
+
if (isReplyBoundary({ event: ev }))
|
|
76
|
+
placeReplyEnd({ state });
|
|
77
|
+
if (ev.kind === 'ext')
|
|
78
|
+
placeExt({ state, ev }, { slotOf });
|
|
79
|
+
else if (ev.kind === 'tool_use')
|
|
80
|
+
placeToolUse({ state, id: ev.id });
|
|
81
|
+
}
|
|
2
82
|
/**
|
|
3
83
|
* Rebuilds an assistant message as ordered blocks, or returns `null` if that cannot be done
|
|
4
84
|
* losslessly.
|
|
@@ -17,85 +97,27 @@ import { assistantContentFromEvents, isReplyBoundary, legacyAssistantContentFrom
|
|
|
17
97
|
export function interleaveMessageBlocks({ events, content, rows }, { slotOf = (ev) => ev.name, includeThinking = false } = {}) {
|
|
18
98
|
if (!events || events.length === 0)
|
|
19
99
|
return null;
|
|
20
|
-
|
|
21
|
-
const hasThinking = includeThinking && events.some((ev) => ev.kind === 'thinking' && ev.text.trim() !== '');
|
|
22
|
-
if (rows.length === 0 && !hasExtEvent && !hasThinking)
|
|
100
|
+
if (!hasSomethingToInterleave({ events, rows }, { includeThinking }))
|
|
23
101
|
return null;
|
|
24
102
|
// The reconstruction is only trustworthy if the text events are demonstrably the source `content`
|
|
25
103
|
// was built from. Anything else and the offsets below are fiction.
|
|
26
|
-
|
|
27
|
-
// `assistantContentFromEvents`) and the old plain concatenation older saved rows carry.
|
|
28
|
-
if (content !== assistantContentFromEvents({ events }) && content !== legacyAssistantContentFromEvents({ events: events }))
|
|
104
|
+
if (!contentMatchesEvents({ events, content }))
|
|
29
105
|
return null;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (buffer.length === 0)
|
|
38
|
-
return;
|
|
39
|
-
blocks.push({ kind: 'text', text: buffer, key: `text-${blocks.length}` });
|
|
40
|
-
buffer = '';
|
|
106
|
+
const state = {
|
|
107
|
+
rowById: new Map(rows.map((row) => [row.id, row])),
|
|
108
|
+
blocks: [],
|
|
109
|
+
emitted: new Set(),
|
|
110
|
+
emittedExtSlots: new Set(),
|
|
111
|
+
buffer: '',
|
|
112
|
+
replies: 0,
|
|
41
113
|
};
|
|
42
|
-
for (const ev of events)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (ev.kind === 'thinking') {
|
|
48
|
-
if (!includeThinking)
|
|
49
|
-
continue;
|
|
50
|
-
flushText();
|
|
51
|
-
const last = blocks[blocks.length - 1];
|
|
52
|
-
if (last && last.kind === 'thinking')
|
|
53
|
-
blocks[blocks.length - 1] = { kind: 'thinking', text: last.text + ev.text, key: last.key };
|
|
54
|
-
else
|
|
55
|
-
blocks.push({ kind: 'thinking', text: ev.text, key: `thinking-${blocks.length}` });
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (isReplyBoundary({ event: ev })) {
|
|
59
|
-
// Emitted per boundary, before any slot dedupe below, so every reply gets its own end marker.
|
|
60
|
-
flushText();
|
|
61
|
-
blocks.push({ kind: 'reply-end', index: replies, key: `reply-end-${replies}` });
|
|
62
|
-
replies += 1;
|
|
63
|
-
}
|
|
64
|
-
if (ev.kind === 'ext') {
|
|
65
|
-
// One block per SLOT, at its first occurrence — later events in the same slot fold into it
|
|
66
|
-
// (`useExtEventGroups`'s job in `MessageRow.tsx`), not a second block. A slot is the `name`
|
|
67
|
-
// unless its renderer keys events into separate surfaces (see `ExtEventSlotKey`).
|
|
68
|
-
const slot = slotOf(ev);
|
|
69
|
-
if (emittedExtSlots.has(slot))
|
|
70
|
-
continue;
|
|
71
|
-
emittedExtSlots.add(slot);
|
|
72
|
-
flushText();
|
|
73
|
-
blocks.push({ kind: 'ext', name: ev.name, slot, key: `ext-${slot}` });
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (ev.kind !== 'tool_use')
|
|
77
|
-
continue;
|
|
78
|
-
const row = rowById.get(ev.id);
|
|
79
|
-
// A `tool_use` the timeline did not surface (deduped away as a repeat of one already seen).
|
|
80
|
-
// Skipping it keeps this function's output consistent with `rows`, which is the authority on
|
|
81
|
-
// what gets a card.
|
|
82
|
-
if (!row || emitted.has(ev.id))
|
|
83
|
-
continue;
|
|
84
|
-
emitted.add(ev.id);
|
|
85
|
-
flushText();
|
|
86
|
-
const last = blocks[blocks.length - 1];
|
|
87
|
-
if (last && last.kind === 'tools') {
|
|
88
|
-
blocks[blocks.length - 1] = { kind: 'tools', rows: [...last.rows, row], key: last.key };
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
blocks.push({ kind: 'tools', rows: [row], key: `tools-${blocks.length}` });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
flushText();
|
|
114
|
+
for (const ev of events)
|
|
115
|
+
placeEvent({ state, ev }, { slotOf, includeThinking });
|
|
116
|
+
flushText({ state });
|
|
95
117
|
// Every row must have found its place. If one did not, the message would render with a tool card
|
|
96
118
|
// missing entirely — strictly worse than the flat layout it would have had.
|
|
97
|
-
if (emitted.size !== rows.length)
|
|
119
|
+
if (state.emitted.size !== rows.length)
|
|
98
120
|
return null;
|
|
99
|
-
return blocks;
|
|
121
|
+
return state.blocks;
|
|
100
122
|
}
|
|
101
123
|
//# sourceMappingURL=message-blocks.js.map
|
package/dist/react/slots.d.ts
CHANGED
|
@@ -179,8 +179,8 @@ export interface ComposerSlots {
|
|
|
179
179
|
* Host controls rendered at the START of the footer row, immediately after the attach/discovery
|
|
180
180
|
* "+" trigger — for a control that belongs with the composer's other action-row buttons (e.g. a
|
|
181
181
|
* push-to-talk mic button) rather than in {@link leadingAccessories}'s pinned-context zone above
|
|
182
|
-
* the input, or in {@link footerAccessories}'s
|
|
183
|
-
*
|
|
182
|
+
* the input, or in {@link footerAccessories}'s slot (reserved for an agent/model picker, which
|
|
183
|
+
* follows this one; only the send group is pushed to the row's far end).
|
|
184
184
|
*/
|
|
185
185
|
footerLeadingAccessory?: ReactNode;
|
|
186
186
|
onAttach?: (a: ChatAttachment) => void;
|
|
@@ -30,34 +30,36 @@ function isRunningToolRow(row) {
|
|
|
30
30
|
export function applyToolCallDisplay({ blocks, mode }) {
|
|
31
31
|
if (mode === 'show')
|
|
32
32
|
return blocks;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
out.push({ kind: 'tools', rows: failed, key: block.key });
|
|
33
|
+
return mode === 'hidden' ? hideToolCalls({ blocks }) : collapseToolCalls({ blocks });
|
|
34
|
+
}
|
|
35
|
+
/** `'hidden'`: keeps only failed rows and drops `tools` blocks left empty. */
|
|
36
|
+
function hideToolCalls({ blocks }) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const block of blocks) {
|
|
39
|
+
if (block.kind !== 'tools') {
|
|
40
|
+
out.push(block);
|
|
41
|
+
continue;
|
|
43
42
|
}
|
|
44
|
-
|
|
43
|
+
const failed = block.rows.filter((row) => isFailedToolRow({ row }));
|
|
44
|
+
if (failed.length > 0)
|
|
45
|
+
out.push({ kind: 'tools', rows: failed, key: block.key });
|
|
45
46
|
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** `'collapsed'`: merges every `tools` block of a turn into the turn's first one (see above). */
|
|
50
|
+
function collapseToolCalls({ blocks }) {
|
|
46
51
|
const out = [];
|
|
47
52
|
let turnToolsIndex = -1;
|
|
48
53
|
for (const block of blocks) {
|
|
49
54
|
if (block.kind === 'reply-end')
|
|
50
55
|
turnToolsIndex = -1;
|
|
51
|
-
if (block.kind !== 'tools') {
|
|
52
|
-
out.push(block);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
56
|
const first = turnToolsIndex === -1 ? undefined : out[turnToolsIndex];
|
|
56
|
-
if (
|
|
57
|
+
if (block.kind === 'tools' && first?.kind === 'tools') {
|
|
57
58
|
out[turnToolsIndex] = { kind: 'tools', rows: [...first.rows, ...block.rows], key: first.key };
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
if (block.kind === 'tools')
|
|
62
|
+
turnToolsIndex = out.length;
|
|
61
63
|
out.push(block);
|
|
62
64
|
}
|
|
63
65
|
return out;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DurableRecovery, RecoveryPorts } from "../../core/durable-runs/ports.js";
|
|
2
|
-
export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
2
|
+
export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, AWAITING_ANSWER_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
3
3
|
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
|
|
4
4
|
* Each recovery decision logs one `logger.info` line (default console) of run metadata. */
|
|
5
5
|
export declare function createDurableRecovery(ports: RecoveryPorts, { logger }?: {
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { recoveredRunEvents } from "../../core/index.js";
|
|
2
2
|
import { classifyAgentServiceFailure } from "@jini-ai/agent-runtime";
|
|
3
3
|
import { continuationRequest } from "../../core/durable-runs/continuation.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { isAwaitingAnswer } from "../../core/run-activity.js";
|
|
5
|
+
import { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, AWAITING_ANSWER_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
6
|
+
export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, AWAITING_ANSWER_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
|
|
6
7
|
// translateRunFrame saves a run error frame as a status label holding only its message: the
|
|
7
8
|
// daemon's MCP_BRIDGE_UNAVAILABLE code is dropped, so the bridge failure is matched by its text.
|
|
8
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
|
+
}
|
|
9
37
|
/** Retryability is independent of session resumability. Inspect executor diagnostics, never
|
|
10
38
|
* answer text or tool results: those can quote unrelated failures from completed work. Old
|
|
11
39
|
* attempt diagnostics remain visible but cannot decide the fate of their continuation. */
|
|
@@ -13,51 +41,36 @@ function attemptDiagnostics({ run }, _optional = {}) {
|
|
|
13
41
|
const events = run.message.events ?? [];
|
|
14
42
|
// Checkpoints merge adjacent text, so a base's event count is not a durable offset. The
|
|
15
43
|
// coordinator's recovery marker separates attempts and survives that compaction.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (event.kind === "status" && event.code === "run_recovering") {
|
|
21
|
-
marker = index;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return events.slice(marker >= 0 ? marker + 1 : run.attemptBase.length).flatMap((event) => {
|
|
26
|
-
if (event.kind === "status")
|
|
27
|
-
return [event.label, event.detail ?? ""];
|
|
28
|
-
if (event.kind !== "raw")
|
|
29
|
-
return [];
|
|
30
|
-
try {
|
|
31
|
-
const result = JSON.parse(event.line);
|
|
32
|
-
// Successful CLI summaries contain usage metadata and answer prose, not failure evidence.
|
|
33
|
-
if (result?.type === "result" && result.is_error === false)
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// Non-JSON stdout can still contain the executor's failure reason.
|
|
38
|
-
}
|
|
39
|
-
return [event.line];
|
|
40
|
-
}).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");
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
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 = {}) {
|
|
44
58
|
// MCP_BRIDGE_UNAVAILABLE needs bridge repair; another attempt repeats the same failure.
|
|
45
59
|
// Classify it before interpreting the bridge's stderr as an agent auth/watchdog failure.
|
|
46
60
|
if (MCP_BRIDGE_FAILURE.test(diagnostics))
|
|
47
61
|
return "permanent";
|
|
48
62
|
if (/inactivity[_ -]?(timeout|watchdog)|watchdog.*(kill|stop)/i.test(diagnostics))
|
|
49
63
|
return "inactivity-watchdog";
|
|
50
|
-
|
|
51
|
-
if (service === "AGENT_AUTH_REQUIRED")
|
|
64
|
+
if (classifyAgentServiceFailure({ text: diagnostics }) === "AGENT_AUTH_REQUIRED")
|
|
52
65
|
return "auth-failure";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
66
|
+
return needsHumanRepair({ diagnostics }) ? "permanent" : "retryable";
|
|
67
|
+
}
|
|
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
|
+
}
|
|
72
|
+
function attemptFailureDetail({ run }, _optional = {}) {
|
|
73
|
+
return lastDiagnosticLine({ diagnostics: attemptDiagnostics({ run }) });
|
|
61
74
|
}
|
|
62
75
|
function exclusion(run) {
|
|
63
76
|
if (run.cancelReason)
|
|
@@ -66,23 +79,56 @@ function exclusion(run) {
|
|
|
66
79
|
return "excluded";
|
|
67
80
|
return null;
|
|
68
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
|
+
}
|
|
69
100
|
function budgetNotice(run, now) {
|
|
70
101
|
if (now - run.lastProgressAt >= RUN_STALENESS_MS)
|
|
71
|
-
return SAVED_WORK_NOTICE;
|
|
102
|
+
return awaitingAnswer({ run }) ? AWAITING_ANSWER_NOTICE : SAVED_WORK_NOTICE;
|
|
72
103
|
if (run.recoveryCount >= RECOVERY_LIMIT)
|
|
73
104
|
return EXHAUSTED_NOTICE;
|
|
74
105
|
if (run.recoveryDeadline !== null && now >= run.recoveryDeadline)
|
|
75
106
|
return EXHAUSTED_NOTICE;
|
|
76
107
|
return null;
|
|
77
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
|
+
}
|
|
78
124
|
/** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot.
|
|
79
125
|
* Each recovery decision logs one `logger.info` line (default console) of run metadata. */
|
|
80
126
|
export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
81
|
-
async function finalize(run, notice, canceled = false) {
|
|
127
|
+
async function finalize(run, notice, { canceled = false, detail } = {}) {
|
|
82
128
|
await ports.cancelAttempt(run, {}).catch(() => undefined);
|
|
83
129
|
// Persist the notice as an event: hook errors are live-only and would leave a reloaded
|
|
84
130
|
// terminal message without its explanation. Only this coordinator decides exhaustion.
|
|
85
|
-
const events = [...(run.message.events ?? []), { kind: "status", label: notice }];
|
|
131
|
+
const events = [...(run.message.events ?? []), { kind: "status", label: notice, ...(detail ? { detail } : {}) }];
|
|
86
132
|
await ports.settle({ ...run, content: run.message.content, events, status: canceled ? "canceled" : "failed", endedAt: ports.now() }, {});
|
|
87
133
|
return "finalized";
|
|
88
134
|
}
|
|
@@ -93,6 +139,16 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
93
139
|
return false;
|
|
94
140
|
return ports.verifyChildDead(run.child, {}).catch(() => false);
|
|
95
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
|
+
}
|
|
96
152
|
async function continueDead(run) {
|
|
97
153
|
await ports.cancelAttempt(run, {}).catch(() => undefined);
|
|
98
154
|
// A locator captured before a hard cancel can outlive its rollout. Reuse reconstruction,
|
|
@@ -100,10 +156,7 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
100
156
|
const missingResume = run.request.agentId === 'codex' && /no rollout found for thread id/.test(attemptDiagnostics({ run }, {}));
|
|
101
157
|
const native = !missingResume && await nativeAllowed(run);
|
|
102
158
|
const nextRunId = ports.mintRunId();
|
|
103
|
-
const
|
|
104
|
-
if (!saved.some((event) => event.kind === "text") && run.message.content)
|
|
105
|
-
saved.unshift({ kind: "text", text: run.message.content });
|
|
106
|
-
const events = recoveredRunEvents({ saved }, { notice: missingResume ? 'silent' : 'visible' });
|
|
159
|
+
const events = recoveredRunEvents({ saved: savedWork({ run }) }, { notice: missingResume ? 'silent' : 'visible' });
|
|
107
160
|
if (!await ports.store.advance({ run, nextRunId, now: ports.now(), events }, {}))
|
|
108
161
|
return "superseded";
|
|
109
162
|
const next = await ports.store.load({ messageId: run.messageId }, {});
|
|
@@ -112,39 +165,46 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
112
165
|
if (!next || next.runId !== nextRunId)
|
|
113
166
|
return "superseded";
|
|
114
167
|
if (next.cancelReason)
|
|
115
|
-
return finalize(next,
|
|
116
|
-
|
|
117
|
-
await ports.launch({ run: next, request: continuationRequest({ run: { ...run, message: next.message, transcript: next.transcript }, native }, {}) }, {});
|
|
118
|
-
await ports.store.recoveryClock({ runId: next.runId, now: ports.now(), active: false }, {});
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// A rejected/lost start response is uncertain: the daemon may already be executing. The
|
|
122
|
-
// same idempotent attempt is probed by the attached watcher before another CAS is possible.
|
|
123
|
-
}
|
|
168
|
+
return finalize(next, STOPPED_NOTICE, { canceled: true });
|
|
169
|
+
await launchNext({ run, next, native });
|
|
124
170
|
ports.attach(next, {});
|
|
125
171
|
return "continued";
|
|
126
172
|
}
|
|
127
|
-
async function
|
|
128
|
-
if (exclusion(run))
|
|
129
|
-
return finalize(run, "Stopped. Saved work is above.", true);
|
|
173
|
+
async function probeAttempt({ run, trigger, liveRunId }, _optional = {}) {
|
|
130
174
|
// A terminal executor end confirms this attempt is dead even when the daemon retains its
|
|
131
175
|
// replayable record (HTTP 200). Other triggers must obtain fresh liveness evidence.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
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) };
|
|
138
201
|
// Diagnostics decide only the fate of an attempt that is no longer running. Read before the
|
|
139
202
|
// probe, raw stdout quoting a tool description ("OAuth-authenticated") canceled a live turn
|
|
140
203
|
// as "Not logged in" (luvira, 2026-10-08).
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (failure === "permanent")
|
|
146
|
-
return finalize(run, attemptDiagnostics({ run }, {}).match(MCP_BRIDGE_FAILURE)?.[0] ?? EXHAUSTED_NOTICE);
|
|
147
|
-
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 };
|
|
148
208
|
}
|
|
149
209
|
async function resolveMissing(run, probe, trigger) {
|
|
150
210
|
await ports.store.recoveryClock({ runId: run.runId, now: ports.now(), active: true }, {});
|
|
@@ -153,12 +213,16 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
153
213
|
// a newer attempt committed by another caller while this one was awaiting storage/HTTP.
|
|
154
214
|
if (timed.runId !== run.runId)
|
|
155
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);
|
|
156
220
|
const notice = budgetNotice(timed, ports.now());
|
|
157
221
|
if (notice)
|
|
158
|
-
return finalize(run, notice);
|
|
222
|
+
return finalize(run, notice, { detail: notice === EXHAUSTED_NOTICE ? exhaustedDetail({ run: timed }) : undefined });
|
|
159
223
|
// A queued attempt may be between durable acceptance and the daemon's start transaction.
|
|
160
224
|
// Give that idempotent launch time to appear before a 404 can advance its generation.
|
|
161
|
-
if (
|
|
225
|
+
if (continuesNow({ run, probe, trigger, now: ports.now() }))
|
|
162
226
|
return continueDead(timed);
|
|
163
227
|
ports.attach(run, {});
|
|
164
228
|
return "waiting";
|
|
@@ -172,12 +236,11 @@ export function createDurableRecovery(ports, { logger = console } = {}) {
|
|
|
172
236
|
return "terminal";
|
|
173
237
|
if (expectedRunId && expectedRunId !== run.runId)
|
|
174
238
|
return "superseded";
|
|
175
|
-
const
|
|
176
|
-
const decision = await decide(run, trigger, options.liveRunId, seen);
|
|
239
|
+
const { decision, probe, failure } = await decide(run, trigger, options.liveRunId);
|
|
177
240
|
// The watcher re-confirms its own live run every resolution window; log only real recovery
|
|
178
241
|
// decisions. Metadata only: diagnostics, answers and cancel reasons can hold secrets.
|
|
179
242
|
if (options.liveRunId !== run.runId)
|
|
180
|
-
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 }));
|
|
181
244
|
return decision;
|
|
182
245
|
},
|
|
183
246
|
};
|