clay-server 5.5.0-beta.6 → 5.5.0-beta.8
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/lib/sdk-bridge.js
CHANGED
|
@@ -1787,6 +1787,11 @@ function createSDKBridge(opts) {
|
|
|
1787
1787
|
if (structuredInputSession) requestedUserInputMode = "fallback";
|
|
1788
1788
|
if (requestedUserInputMode === "auto" && queryContract.defaultUserInputMode) requestedUserInputMode = queryContract.defaultUserInputMode;
|
|
1789
1789
|
var selectedUserInputMode = yoke.userInput.selectMode(sessionAdapter, requestedUserInputMode);
|
|
1790
|
+
// In fallback mode the adapter denies every native AskUserQuestion call,
|
|
1791
|
+
// so its tool_use block must not become an interactive card: such a card
|
|
1792
|
+
// has no pendingAskUser entry and silently swallows the user's answer.
|
|
1793
|
+
// The fallback dispatch path records the real, answerable card itself.
|
|
1794
|
+
session._askUserNativeDispatch = selectedUserInputMode !== "fallback";
|
|
1790
1795
|
var recoverQuestionTools = needsCatalogRecovery("questions", 1) && queryContextAvailable();
|
|
1791
1796
|
function structuredInputContextAvailable() {
|
|
1792
1797
|
if (!queryContextAvailable()) return false;
|
|
@@ -1972,6 +1977,18 @@ function createSDKBridge(opts) {
|
|
|
1972
1977
|
? 'Tool recovery is waiting for this conversation’s runtime to become safely available. Your message is retained; Stop cancels the wait.'
|
|
1973
1978
|
: 'Tool recovery is ready. Continuing your retained message.' });
|
|
1974
1979
|
},
|
|
1980
|
+
onRecoveryRepair: function(info) {
|
|
1981
|
+
if (!queryContextAvailable()) return;
|
|
1982
|
+
var commands = (info && Array.isArray(info.commands) ? info.commands : []).map(function(command) {
|
|
1983
|
+
command = String(command).replace(/\s+/g, ' ').trim();
|
|
1984
|
+
return command.length > 80 ? command.slice(0, 77) + '...' : command;
|
|
1985
|
+
}).filter(Boolean);
|
|
1986
|
+
var count = Number(info && info.terminatedBackgroundTerminals) || commands.length || 0;
|
|
1987
|
+
if (!count) return;
|
|
1988
|
+
sendAndRecord(session, { type: 'informational', level: 'info', content: 'Tool recovery stopped '
|
|
1989
|
+
+ count + ' background terminal' + (count === 1 ? '' : 's') + ' this conversation left running'
|
|
1990
|
+
+ (commands.length ? ' (' + commands.join('; ') + ')' : '') + ' so your message can continue.' });
|
|
1991
|
+
},
|
|
1975
1992
|
callDynamicTool: function (toolName, args) {
|
|
1976
1993
|
if (queryRetired() || !queryContextAvailable()) return Promise.reject(new Error("This session tool query is no longer available."));
|
|
1977
1994
|
var toolDef = findSessionToolDef(toolName);
|
|
@@ -435,7 +435,12 @@ function attachMessageProcessor(ctx) {
|
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
|
-
|
|
438
|
+
// A fallback-mode query denies every native AskUserQuestion, so its
|
|
439
|
+
// block must not render an answerable card: it has no pendingAskUser
|
|
440
|
+
// entry and would silently swallow the user's answer. The fallback
|
|
441
|
+
// dispatch (project-ask-user) records the real card itself.
|
|
442
|
+
var ghostAskCard = block.name === "AskUserQuestion" && session._askUserNativeDispatch === false && !alreadyRecordedAsk;
|
|
443
|
+
if (!alreadyRecordedAsk && !ghostAskCard) {
|
|
439
444
|
sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
|
|
440
445
|
}
|
|
441
446
|
|
|
@@ -445,7 +450,7 @@ function attachMessageProcessor(ctx) {
|
|
|
445
450
|
session.activeTaskToolIds[block.id] = true;
|
|
446
451
|
}
|
|
447
452
|
|
|
448
|
-
if (pushModule && block.name === "AskUserQuestion" && input.questions) {
|
|
453
|
+
if (pushModule && block.name === "AskUserQuestion" && input.questions && !ghostAskCard) {
|
|
449
454
|
var q = input.questions[0];
|
|
450
455
|
pushModule.sendPush({
|
|
451
456
|
sessionId: session.localId,
|
|
@@ -1899,8 +1899,8 @@ function createCodexAdapter(opts) {
|
|
|
1899
1899
|
return null;
|
|
1900
1900
|
},
|
|
1901
1901
|
|
|
1902
|
-
retireIdleForRecovery: function(checkCurrent) {
|
|
1903
|
-
return idleRecovery.retireIdleQueries(_activeQueries.slice(), _appServer, checkCurrent);
|
|
1902
|
+
retireIdleForRecovery: function(checkCurrent, repair) {
|
|
1903
|
+
return idleRecovery.retireIdleQueries(_activeQueries.slice(), _appServer, checkCurrent, repair);
|
|
1904
1904
|
},
|
|
1905
1905
|
|
|
1906
1906
|
createQuery: async function(queryOpts) {
|
|
@@ -10,15 +10,24 @@ function checkAbort(queryOpts) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function
|
|
14
|
-
if (!result || result.nextCursor) return
|
|
13
|
+
function listedTerminals(result) {
|
|
14
|
+
if (!result || result.nextCursor) return null;
|
|
15
15
|
var terminals = Array.isArray(result) ? result
|
|
16
16
|
: result.data || result.terminals || result.backgroundTerminals || result.items;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
return Array.isArray(terminals) ? terminals : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Current Codex lists only live processes without a status field; legacy
|
|
21
|
+
// builds reported finished states, so an explicit finished status still counts.
|
|
22
|
+
function terminalFinished(terminal) {
|
|
23
|
+
var status = String(terminal && (terminal.status || terminal.state) || "").toLowerCase();
|
|
24
|
+
return /^(exited|terminated|completed|failed)$/.test(status);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function terminalsAreIdle(result) {
|
|
28
|
+
var terminals = listedTerminals(result);
|
|
29
|
+
if (!terminals) return false;
|
|
30
|
+
return terminals.every(terminalFinished);
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
function observe(checkCurrent, reason, method, rpcCode, threadId) {
|
|
@@ -46,7 +55,55 @@ function probeFailure(error, method) {
|
|
|
46
55
|
return false;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
// Owner-approved unblock for the conversation being resumed: its own live
|
|
59
|
+
// background terminals would be orphaned in the retired process anyway, so
|
|
60
|
+
// terminate them instead of retaining the conversation blocked forever.
|
|
61
|
+
// Only the recovery target's thread is ever touched; sibling conversations
|
|
62
|
+
// keep their background work. Requires the provider to report the thread
|
|
63
|
+
// idle first so an active turn's terminals are never stopped.
|
|
64
|
+
async function terminateTargetTerminals(server, threadId, checkCurrent, onTerminated) {
|
|
65
|
+
var method = 'thread/read';
|
|
66
|
+
var live;
|
|
67
|
+
try {
|
|
68
|
+
var status = await probe(server, method, { threadId: threadId, includeTurns: false }, 5000, checkCurrent);
|
|
69
|
+
checkCurrent();
|
|
70
|
+
if (!status || !status.thread || status.thread.id !== threadId
|
|
71
|
+
|| !status.thread.status || status.thread.status.type !== 'idle') return false;
|
|
72
|
+
method = 'thread/backgroundTerminals/list';
|
|
73
|
+
var terminals = listedTerminals(await probe(server, method, { threadId: threadId }, 5000, checkCurrent));
|
|
74
|
+
checkCurrent();
|
|
75
|
+
if (!terminals) return false;
|
|
76
|
+
live = terminals.filter(function(terminal) { return !terminalFinished(terminal); });
|
|
77
|
+
} catch (error) { checkCurrent(); if (!server.started) throw error; return probeFailure(error, method); }
|
|
78
|
+
if (!live.length) return false;
|
|
79
|
+
var commands = [];
|
|
80
|
+
for (var i = 0; i < live.length; i++) {
|
|
81
|
+
var processId = live[i] && (live[i].processId !== undefined && live[i].processId !== null ? live[i].processId : live[i].id);
|
|
82
|
+
if (processId === undefined || processId === null || processId === '') return false;
|
|
83
|
+
var outcome;
|
|
84
|
+
try {
|
|
85
|
+
outcome = await probe(server, 'thread/backgroundTerminals/terminate', { threadId: threadId, processId: String(processId) }, 5000, checkCurrent);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
checkCurrent();
|
|
88
|
+
if (!server.started) throw error;
|
|
89
|
+
// An unsupported or failed terminate leaves the conversation retained,
|
|
90
|
+
// exactly like before this repair existed. Never escalate it.
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
checkCurrent();
|
|
94
|
+
observe(checkCurrent, outcome && outcome.terminated === true
|
|
95
|
+
? 'background_terminal_terminated' : 'background_terminal_terminate_unconfirmed',
|
|
96
|
+
'thread/backgroundTerminals/terminate', null, threadId);
|
|
97
|
+
if (!outcome || outcome.terminated !== true) return false;
|
|
98
|
+
if (typeof live[i].command === 'string' && live[i].command) commands.push(live[i].command);
|
|
99
|
+
}
|
|
100
|
+
if (typeof onTerminated === 'function') {
|
|
101
|
+
try { onTerminated(commands, live.length); } catch (error) {}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function retireIdleQueries(entries, server, checkCurrent, repair) {
|
|
50
107
|
checkCurrent();
|
|
51
108
|
var candidates = [];
|
|
52
109
|
for (var i = 0; i < entries.length; i++) {
|
|
@@ -63,7 +120,13 @@ async function retireIdleQueries(entries, server, checkCurrent) {
|
|
|
63
120
|
checkCurrent();
|
|
64
121
|
for (var ci = 0; ci < candidates.length; ci++) {
|
|
65
122
|
var current = candidates[ci].handle.getIdleRecoverySnapshot();
|
|
66
|
-
if (!safe[ci]) {
|
|
123
|
+
if (!safe[ci]) {
|
|
124
|
+
observe(checkCurrent, 'background_state_not_proven_idle', null, null, candidates[ci].snapshot.threadId);
|
|
125
|
+
if (repair && repair.threadId && repair.threadId === candidates[ci].snapshot.threadId) {
|
|
126
|
+
await terminateTargetTerminals(server, repair.threadId, checkCurrent, repair.onTerminated);
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
67
130
|
if (!current || current.revision !== candidates[ci].snapshot.revision) { observe(checkCurrent, 'input_changed_during_probe', null, null, candidates[ci].snapshot.threadId); return false; }
|
|
68
131
|
}
|
|
69
132
|
// No awaits between validation and closing every input. A new send either
|
|
@@ -144,19 +207,24 @@ async function threadIsIdle(server, threadId, checkCurrent) {
|
|
|
144
207
|
} catch (error) { checkCurrent(); if (!server.started) throw error; return probeFailure(error, method); }
|
|
145
208
|
}
|
|
146
209
|
|
|
147
|
-
async function releaseThread(entries, server, threadId, checkCurrent) {
|
|
210
|
+
async function releaseThread(entries, server, threadId, checkCurrent, repair) {
|
|
148
211
|
if (!server || !server.started) throw new Error('Codex disconnected while checking recovery admission');
|
|
149
212
|
var matching = entries.filter(function(entry) {
|
|
150
213
|
return entry.handle && entry.handle.getRecoveryThreadId() === threadId;
|
|
151
214
|
});
|
|
152
215
|
if (!matching.length && await threadIsAbsent(server, threadId, checkCurrent)) return true;
|
|
153
216
|
if (matching.length) {
|
|
154
|
-
if (!await retireIdleQueries(matching, server, checkCurrent)) return false;
|
|
217
|
+
if (!await retireIdleQueries(matching, server, checkCurrent, repair)) return false;
|
|
155
218
|
checkCurrent();
|
|
156
219
|
} else {
|
|
157
220
|
// A closed SDK handle can leave a loaded provider thread behind. Only an
|
|
158
221
|
// explicitly idle provider thread with verified terminal state may unload.
|
|
159
|
-
if (!await threadIsIdle(server, threadId, checkCurrent))
|
|
222
|
+
if (!await threadIsIdle(server, threadId, checkCurrent)) {
|
|
223
|
+
if (repair && repair.threadId === threadId) {
|
|
224
|
+
await terminateTargetTerminals(server, threadId, checkCurrent, repair.onTerminated);
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
160
228
|
}
|
|
161
229
|
try {
|
|
162
230
|
var result = await probe(server, 'thread/unsubscribe', { threadId: threadId }, 5000, checkCurrent);
|
|
@@ -71,20 +71,33 @@ function attachRecoveryAdmission(ctx) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// The owner's new message to this conversation authorizes stopping the
|
|
75
|
+
// conversation's OWN leftover background terminals: they would be orphaned
|
|
76
|
+
// by the runtime handoff anyway. Sibling conversations are never repaired.
|
|
77
|
+
function repairOptions(queryOpts) {
|
|
78
|
+
return {
|
|
79
|
+
threadId: queryOpts.resumeSessionId,
|
|
80
|
+
onTerminated: function(commands, count) {
|
|
81
|
+
if (typeof queryOpts.onRecoveryRepair !== 'function') return;
|
|
82
|
+
try { queryOpts.onRecoveryRepair({ terminatedBackgroundTerminals: count, commands: commands }); } catch (error) {}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function releaseTarget(threadId, checkCurrent, repair) {
|
|
75
88
|
checkCurrent.phase('isolated_target');
|
|
76
89
|
var prior = targets.get(threadId);
|
|
77
90
|
if (!prior) return true;
|
|
78
91
|
checkCurrent.observe('isolated_target_reserved');
|
|
79
92
|
if (cleanupFailures.has(prior)) throw cleanupFailures.get(prior);
|
|
80
|
-
await prior.retireIdleForRecovery(checkCurrent);
|
|
93
|
+
await prior.retireIdleForRecovery(checkCurrent, repair);
|
|
81
94
|
await cleanup;
|
|
82
95
|
checkCurrent();
|
|
83
96
|
if (cleanupFailures.has(prior)) throw cleanupFailures.get(prior);
|
|
84
97
|
return !targets.has(threadId);
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
async function prepareShared(threadId, checkCurrent) {
|
|
100
|
+
async function prepareShared(threadId, checkCurrent, repair) {
|
|
88
101
|
checkCurrent.phase('shared_target');
|
|
89
102
|
var state = ctx.state();
|
|
90
103
|
if (state.initializing) { checkCurrent.observe('shared_runtime_initializing'); return false; }
|
|
@@ -94,14 +107,14 @@ function attachRecoveryAdmission(ctx) {
|
|
|
94
107
|
if (state.server && state.entries.some(function(entry) {
|
|
95
108
|
return !entry.handle || entry.handle.getRecoveryThreadId() !== threadId;
|
|
96
109
|
})) {
|
|
97
|
-
if (await idleRecovery.releaseThread(state.entries, state.server, threadId, checkCurrent)) return true;
|
|
110
|
+
if (await idleRecovery.releaseThread(state.entries, state.server, threadId, checkCurrent, repair)) return true;
|
|
98
111
|
checkCurrent();
|
|
99
112
|
}
|
|
100
113
|
checkCurrent.phase('shared_idle_retirement');
|
|
101
114
|
var retiredThreads = [];
|
|
102
115
|
if (state.refCount > 0) {
|
|
103
116
|
var snapshots = state.entries.map(function(entry) { return entry.handle && entry.handle.getIdleRecoverySnapshot(); });
|
|
104
|
-
if (await idleRecovery.retireIdleQueries(state.entries, state.server, checkCurrent)) {
|
|
117
|
+
if (await idleRecovery.retireIdleQueries(state.entries, state.server, checkCurrent, repair)) {
|
|
105
118
|
retiredThreads = snapshots.filter(Boolean).map(function(snapshot) { return snapshot.threadId; });
|
|
106
119
|
}
|
|
107
120
|
}
|
|
@@ -115,7 +128,7 @@ function attachRecoveryAdmission(ctx) {
|
|
|
115
128
|
return stopped;
|
|
116
129
|
}
|
|
117
130
|
checkCurrent.phase('shared_target');
|
|
118
|
-
return idleRecovery.releaseThread(state.entries, state.server, threadId, checkCurrent);
|
|
131
|
+
return idleRecovery.releaseThread(state.entries, state.server, threadId, checkCurrent, repair);
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
async function waitUntilAvailable(queryOpts, action) {
|
|
@@ -179,9 +192,10 @@ function attachRecoveryAdmission(ctx) {
|
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
function createQuery(queryOpts) {
|
|
195
|
+
var repair = repairOptions(queryOpts);
|
|
182
196
|
return waitUntilAvailable(queryOpts, async function(checkCurrent) {
|
|
183
|
-
if (!await releaseTarget(queryOpts.resumeSessionId, checkCurrent)) return false;
|
|
184
|
-
if (!await prepareShared(queryOpts.resumeSessionId, checkCurrent)) return false;
|
|
197
|
+
if (!await releaseTarget(queryOpts.resumeSessionId, checkCurrent, repair)) return false;
|
|
198
|
+
if (!await prepareShared(queryOpts.resumeSessionId, checkCurrent, repair)) return false;
|
|
185
199
|
// Once ownership is reserved, normal startup/cleanup owns cancellation.
|
|
186
200
|
// A deadline must not discard a handle being created after admission.
|
|
187
201
|
checkCurrent.admit();
|
|
@@ -206,8 +220,9 @@ function attachRecoveryAdmission(ctx) {
|
|
|
206
220
|
checkSharedExit: checkSharedExit,
|
|
207
221
|
hasTarget: function(threadId) { return targets.has(threadId); },
|
|
208
222
|
waitForTarget: function(queryOpts) {
|
|
223
|
+
var repair = repairOptions(queryOpts);
|
|
209
224
|
return waitUntilAvailable(queryOpts, async function(checkCurrent) {
|
|
210
|
-
var released = await releaseTarget(queryOpts.resumeSessionId, checkCurrent);
|
|
225
|
+
var released = await releaseTarget(queryOpts.resumeSessionId, checkCurrent, repair);
|
|
211
226
|
if (released) checkCurrent.admit();
|
|
212
227
|
return released;
|
|
213
228
|
});
|
package/package.json
CHANGED