@guuey/mcp-apps-host 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action.d.ts +39 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +61 -10
- package/package.json +5 -2
- package/src/action.ts +92 -9
package/dist/action.d.ts
CHANGED
|
@@ -137,8 +137,28 @@ export interface CreateMcpUiActionRelayDeps {
|
|
|
137
137
|
* surfaces a visible "this session ended — start a new chat" state and drops
|
|
138
138
|
* the stale thread, so a tripped circuit is never a silent frozen card.
|
|
139
139
|
* Optional: a host that only wants the storm bounded omits it.
|
|
140
|
+
*
|
|
141
|
+
* Once per trip: it fires again only after {@link onSessionRestored} has
|
|
142
|
+
* cleared an earlier verdict for the same locator and the circuit trips anew.
|
|
140
143
|
*/
|
|
141
144
|
onSessionUnrestorable?: (resourceUri: string) => void;
|
|
145
|
+
/**
|
|
146
|
+
* Fired when a locator whose pull circuit OPENED is seen live again on the
|
|
147
|
+
* pull rung itself, so the host can withdraw the "session ended" state it
|
|
148
|
+
* showed. Wiring it is what lets an open circuit recover in place at all:
|
|
149
|
+
* without it the circuit stays open for the mount (recovery is a fresh
|
|
150
|
+
* mount), exactly as a host that wires only {@link onSessionUnrestorable}
|
|
151
|
+
* expects.
|
|
152
|
+
*
|
|
153
|
+
* The recovery is evidence on the channel that tripped: a successful
|
|
154
|
+
* `ggui_runtime_refresh_ws_token` for the locator HALF-OPENS the circuit,
|
|
155
|
+
* which lets the view's next pull through as a single probe. A probe that
|
|
156
|
+
* the session answers closes the circuit and fires this, once per close; a
|
|
157
|
+
* probe that fails leaves it open, and the host hears nothing new (it
|
|
158
|
+
* already holds the verdict). A token refresh alone never reverses it: the
|
|
159
|
+
* token channel can work while pulls still fail.
|
|
160
|
+
*/
|
|
161
|
+
onSessionRestored?: (resourceUri: string) => void;
|
|
142
162
|
}
|
|
143
163
|
/** The request shape a mounted card's `onCallTool` bridge produces. */
|
|
144
164
|
export interface UiActionRequest {
|
|
@@ -165,6 +185,20 @@ export declare const PULL_CIRCUIT_THRESHOLD = 3;
|
|
|
165
185
|
* and nothing retries unattended.
|
|
166
186
|
*/
|
|
167
187
|
export declare const UI_ACTION_PULL_CIRCUIT_OPEN = "[guuey] card updates stopped \u2014 the session behind this card could not be restored; the widget reports it, and a new conversation starts fresh";
|
|
188
|
+
/**
|
|
189
|
+
* Logged when an open circuit closes again: the probe pull a successful token
|
|
190
|
+
* refresh let through was answered, and the host was told
|
|
191
|
+
* (`onSessionRestored`). It can only happen for a host that wires that signal.
|
|
192
|
+
*/
|
|
193
|
+
export declare const UI_ACTION_PULL_CIRCUIT_CLOSED = "[guuey] card updates resumed \u2014 the session behind this card answered again";
|
|
194
|
+
/**
|
|
195
|
+
* Logged when a host's `onSessionUnrestorable` or `onSessionRestored`
|
|
196
|
+
* callback throws. The relay never rejects into the card, so it goes on; this
|
|
197
|
+
* line is the page's only trace that its own "session ended" state may now be
|
|
198
|
+
* out of step with the card. It names the callback and the error's `name`,
|
|
199
|
+
* never the error's text, which is the host's and can carry anything.
|
|
200
|
+
*/
|
|
201
|
+
export declare const UI_ACTION_HOST_CALLBACK_THREW = "[guuey] the page's card-session handler threw \u2014 the card went on, but the page may not show its current state";
|
|
168
202
|
/**
|
|
169
203
|
* Assemble the sandbox-facing action relay from a host transport. The
|
|
170
204
|
* returned function is shaped for an `onCallTool` bridge: it always
|
|
@@ -184,6 +218,11 @@ export declare const UI_ACTION_PULL_CIRCUIT_OPEN = "[guuey] card updates stopped
|
|
|
184
218
|
* The complement — telling the USER the session is unrestorable so a tripped
|
|
185
219
|
* circuit is not a silent freeze — is the `onSessionUnrestorable` signal
|
|
186
220
|
* (guuey#1249 item 4); this leg only bounds the hammer.
|
|
221
|
+
*
|
|
222
|
+
* An OPEN circuit is terminal for the mount unless the host wires
|
|
223
|
+
* `onSessionRestored`. Then a successful `ggui_runtime_refresh_ws_token` for
|
|
224
|
+
* the locator HALF-OPENS it: the next pull goes through as one probe, and only
|
|
225
|
+
* a probe the session answers closes it (see `onSessionRestored`).
|
|
187
226
|
*/
|
|
188
227
|
export declare function createMcpUiActionRelay(deps: CreateMcpUiActionRelayDeps): (request: UiActionRequest) => Promise<McpToolCallResult>;
|
|
189
228
|
//# sourceMappingURL=action.d.ts.map
|
package/dist/action.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACjD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,CAC3C;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAChB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CACnB,CAAC;CACH,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAK9C,CAAC;AAEH,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,mBAAmB,CAAC;AAE7F;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,qBAAqB,CAYxE,CAAC;AAEH,gDAAgD;AAChD,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAmE1C;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,wBAAwB,GAAG,SAAS,GACzC,wBAAwB,GAAG,SAAS,CAatC;AAOD;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,EAAE,WAAW,CAAC,MAAM,CAEvD,CAAC;AAEH,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,2CACG,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,iBAAiB,CAK7D;AAkCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,GAAG,SAAS,CAgB9E;AAED,iFAAiF;AACjF,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,QAAQ,EAAE,CACR,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,wBAAwB,GAAG,SAAS,KACvC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtB
|
|
1
|
+
{"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"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"}
|
package/dist/action.js
CHANGED
|
@@ -229,6 +229,8 @@ export function asToolCallResult(value) {
|
|
|
229
229
|
}
|
|
230
230
|
/** The host-relayed auto-poll rung (guuey#1235). A dead session's pull is the storm. */
|
|
231
231
|
const PULL_TOOL = "ggui_runtime_pull";
|
|
232
|
+
/** The live-channel token refresh: a success half-opens a tripped locator for a host that can hear a restore. */
|
|
233
|
+
const REFRESH_WS_TOKEN_TOOL = "ggui_runtime_refresh_ws_token";
|
|
232
234
|
/**
|
|
233
235
|
* Consecutive `unavailable` pull results (per card locator) that OPEN the
|
|
234
236
|
* circuit (guuey#1235 leg 1). At the #1233 storm's ~12-18 pulls/min a 3-strike
|
|
@@ -247,6 +249,33 @@ export const PULL_CIRCUIT_THRESHOLD = 3;
|
|
|
247
249
|
* and nothing retries unattended.
|
|
248
250
|
*/
|
|
249
251
|
export const UI_ACTION_PULL_CIRCUIT_OPEN = "[guuey] card updates stopped — the session behind this card could not be restored; the widget reports it, and a new conversation starts fresh";
|
|
252
|
+
/**
|
|
253
|
+
* Logged when an open circuit closes again: the probe pull a successful token
|
|
254
|
+
* refresh let through was answered, and the host was told
|
|
255
|
+
* (`onSessionRestored`). It can only happen for a host that wires that signal.
|
|
256
|
+
*/
|
|
257
|
+
export const UI_ACTION_PULL_CIRCUIT_CLOSED = "[guuey] card updates resumed — the session behind this card answered again";
|
|
258
|
+
/**
|
|
259
|
+
* Logged when a host's `onSessionUnrestorable` or `onSessionRestored`
|
|
260
|
+
* callback throws. The relay never rejects into the card, so it goes on; this
|
|
261
|
+
* line is the page's only trace that its own "session ended" state may now be
|
|
262
|
+
* out of step with the card. It names the callback and the error's `name`,
|
|
263
|
+
* never the error's text, which is the host's and can carry anything.
|
|
264
|
+
*/
|
|
265
|
+
export const UI_ACTION_HOST_CALLBACK_THREW = "[guuey] the page's card-session handler threw — the card went on, but the page may not show its current state";
|
|
266
|
+
/** Call a host's session callback; a throw is logged by name and never escapes the relay. */
|
|
267
|
+
function callHostCallback(callback, fn, resourceUri) {
|
|
268
|
+
try {
|
|
269
|
+
fn?.(resourceUri);
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.warn(UI_ACTION_HOST_CALLBACK_THREW, {
|
|
273
|
+
callback,
|
|
274
|
+
resourceUri,
|
|
275
|
+
name: err instanceof Error ? err.name : typeof err,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
250
279
|
/**
|
|
251
280
|
* Assemble the sandbox-facing action relay from a host transport. The
|
|
252
281
|
* returned function is shaped for an `onCallTool` bridge: it always
|
|
@@ -266,14 +295,29 @@ export const UI_ACTION_PULL_CIRCUIT_OPEN = "[guuey] card updates stopped — the
|
|
|
266
295
|
* The complement — telling the USER the session is unrestorable so a tripped
|
|
267
296
|
* circuit is not a silent freeze — is the `onSessionUnrestorable` signal
|
|
268
297
|
* (guuey#1249 item 4); this leg only bounds the hammer.
|
|
298
|
+
*
|
|
299
|
+
* An OPEN circuit is terminal for the mount unless the host wires
|
|
300
|
+
* `onSessionRestored`. Then a successful `ggui_runtime_refresh_ws_token` for
|
|
301
|
+
* the locator HALF-OPENS it: the next pull goes through as one probe, and only
|
|
302
|
+
* a probe the session answers closes it (see `onSessionRestored`).
|
|
269
303
|
*/
|
|
270
304
|
export function createMcpUiActionRelay(deps) {
|
|
271
305
|
// Per-relay-instance (one card mount). A recovered/absent locator is deleted,
|
|
272
306
|
// so this stays as small as the mounted cards; the mount tears it down.
|
|
273
307
|
const pullFailures = new Map();
|
|
308
|
+
// Open locators a successful token refresh has half-opened: the next pull is
|
|
309
|
+
// let through as the probe. Only ever filled when the host wires the restore.
|
|
310
|
+
const halfOpen = new Set();
|
|
311
|
+
const isOpen = (uri) => (pullFailures.get(uri) ?? 0) >= PULL_CIRCUIT_THRESHOLD;
|
|
274
312
|
const recordPull = (uri, unavailable) => {
|
|
275
313
|
if (!unavailable) {
|
|
314
|
+
const wasOpen = isOpen(uri);
|
|
276
315
|
pullFailures.delete(uri); // the session answered → close the circuit
|
|
316
|
+
if (wasOpen) {
|
|
317
|
+
console.warn(UI_ACTION_PULL_CIRCUIT_CLOSED, { resourceUri: uri });
|
|
318
|
+
// The circuit is closed either way: a throwing host callback is logged, never rethrown.
|
|
319
|
+
callHostCallback("onSessionRestored", deps.onSessionRestored, uri);
|
|
320
|
+
}
|
|
277
321
|
return;
|
|
278
322
|
}
|
|
279
323
|
const next = (pullFailures.get(uri) ?? 0) + 1;
|
|
@@ -287,13 +331,8 @@ export function createMcpUiActionRelay(deps) {
|
|
|
287
331
|
// guuey#1249 item 4: tell the host the session is unrestorable so the
|
|
288
332
|
// bounded circuit isn't a silent freeze. A throw from the host callback
|
|
289
333
|
// must never break the relay's never-reject contract.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
// A host-supplied callback that throws is the host's bug, not the
|
|
295
|
-
// relay's — the storm is already bounded either way.
|
|
296
|
-
}
|
|
334
|
+
// The storm is bounded either way: a throwing host callback is logged, never rethrown.
|
|
335
|
+
callHostCallback("onSessionUnrestorable", deps.onSessionUnrestorable, uri);
|
|
297
336
|
}
|
|
298
337
|
};
|
|
299
338
|
return async (request) => {
|
|
@@ -311,9 +350,11 @@ export function createMcpUiActionRelay(deps) {
|
|
|
311
350
|
return telemetryNothingToRelayResult();
|
|
312
351
|
}
|
|
313
352
|
const isPull = request.name === PULL_TOOL;
|
|
314
|
-
// Circuit OPEN for this locator's pull → fail-fast, never touch the door
|
|
315
|
-
|
|
316
|
-
|
|
353
|
+
// Circuit OPEN for this locator's pull → fail-fast, never touch the door,
|
|
354
|
+
// unless a token refresh half-opened it: then this one pull is the probe.
|
|
355
|
+
if (isPull && isOpen(request.resourceUri)) {
|
|
356
|
+
if (!halfOpen.delete(request.resourceUri))
|
|
357
|
+
return unavailableToolCallResult();
|
|
317
358
|
}
|
|
318
359
|
let raw;
|
|
319
360
|
try {
|
|
@@ -327,6 +368,16 @@ export function createMcpUiActionRelay(deps) {
|
|
|
327
368
|
const result = raw === undefined ? undefined : asToolCallResult(raw);
|
|
328
369
|
if (isPull)
|
|
329
370
|
recordPull(request.resourceUri, result === undefined);
|
|
371
|
+
// A refresh the session granted is fresh evidence it is live, but on the
|
|
372
|
+
// token channel, not the pull channel that tripped: it half-opens, and the
|
|
373
|
+
// probe pull decides. Only for a host that can withdraw its verdict.
|
|
374
|
+
if (request.name === REFRESH_WS_TOKEN_TOOL &&
|
|
375
|
+
deps.onSessionRestored !== undefined &&
|
|
376
|
+
result !== undefined &&
|
|
377
|
+
result.isError !== true &&
|
|
378
|
+
isOpen(request.resourceUri)) {
|
|
379
|
+
halfOpen.add(request.resourceUri);
|
|
380
|
+
}
|
|
330
381
|
return result ?? unavailableToolCallResult();
|
|
331
382
|
};
|
|
332
383
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/mcp-apps-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "The MCP Apps (SEP-1865) Host role for guuey's chat surfaces — view-mount narrowing across UI channels, ui:// locator rehydration by resources/read, and the sandbox-trust channel contract. Vendor-neutral: any spec-following MCP App mounts through it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"!src/**/*.test.ts",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.12.0"
|
|
17
|
+
},
|
|
15
18
|
"exports": {
|
|
16
19
|
".": {
|
|
17
20
|
"react-native": "./src/index.ts",
|
|
@@ -35,7 +38,7 @@
|
|
|
35
38
|
"dependencies": {
|
|
36
39
|
"@ggui-ai/protocol": "0.24.0",
|
|
37
40
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
38
|
-
"@silverprotocol/core": "0.
|
|
41
|
+
"@silverprotocol/core": "0.12.1"
|
|
39
42
|
},
|
|
40
43
|
"peerDependencies": {
|
|
41
44
|
"react": ">=18"
|
package/src/action.ts
CHANGED
|
@@ -305,8 +305,28 @@ export interface CreateMcpUiActionRelayDeps {
|
|
|
305
305
|
* surfaces a visible "this session ended — start a new chat" state and drops
|
|
306
306
|
* the stale thread, so a tripped circuit is never a silent frozen card.
|
|
307
307
|
* Optional: a host that only wants the storm bounded omits it.
|
|
308
|
+
*
|
|
309
|
+
* Once per trip: it fires again only after {@link onSessionRestored} has
|
|
310
|
+
* cleared an earlier verdict for the same locator and the circuit trips anew.
|
|
308
311
|
*/
|
|
309
312
|
onSessionUnrestorable?: (resourceUri: string) => void;
|
|
313
|
+
/**
|
|
314
|
+
* Fired when a locator whose pull circuit OPENED is seen live again on the
|
|
315
|
+
* pull rung itself, so the host can withdraw the "session ended" state it
|
|
316
|
+
* showed. Wiring it is what lets an open circuit recover in place at all:
|
|
317
|
+
* without it the circuit stays open for the mount (recovery is a fresh
|
|
318
|
+
* mount), exactly as a host that wires only {@link onSessionUnrestorable}
|
|
319
|
+
* expects.
|
|
320
|
+
*
|
|
321
|
+
* The recovery is evidence on the channel that tripped: a successful
|
|
322
|
+
* `ggui_runtime_refresh_ws_token` for the locator HALF-OPENS the circuit,
|
|
323
|
+
* which lets the view's next pull through as a single probe. A probe that
|
|
324
|
+
* the session answers closes the circuit and fires this, once per close; a
|
|
325
|
+
* probe that fails leaves it open, and the host hears nothing new (it
|
|
326
|
+
* already holds the verdict). A token refresh alone never reverses it: the
|
|
327
|
+
* token channel can work while pulls still fail.
|
|
328
|
+
*/
|
|
329
|
+
onSessionRestored?: (resourceUri: string) => void;
|
|
310
330
|
}
|
|
311
331
|
|
|
312
332
|
/** The request shape a mounted card's `onCallTool` bridge produces. */
|
|
@@ -320,6 +340,9 @@ export interface UiActionRequest {
|
|
|
320
340
|
/** The host-relayed auto-poll rung (guuey#1235). A dead session's pull is the storm. */
|
|
321
341
|
const PULL_TOOL = "ggui_runtime_pull";
|
|
322
342
|
|
|
343
|
+
/** The live-channel token refresh: a success half-opens a tripped locator for a host that can hear a restore. */
|
|
344
|
+
const REFRESH_WS_TOKEN_TOOL = "ggui_runtime_refresh_ws_token";
|
|
345
|
+
|
|
323
346
|
/**
|
|
324
347
|
* Consecutive `unavailable` pull results (per card locator) that OPEN the
|
|
325
348
|
* circuit (guuey#1235 leg 1). At the #1233 storm's ~12-18 pulls/min a 3-strike
|
|
@@ -341,6 +364,41 @@ export const PULL_CIRCUIT_THRESHOLD = 3;
|
|
|
341
364
|
export const UI_ACTION_PULL_CIRCUIT_OPEN =
|
|
342
365
|
"[guuey] card updates stopped — the session behind this card could not be restored; the widget reports it, and a new conversation starts fresh";
|
|
343
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Logged when an open circuit closes again: the probe pull a successful token
|
|
369
|
+
* refresh let through was answered, and the host was told
|
|
370
|
+
* (`onSessionRestored`). It can only happen for a host that wires that signal.
|
|
371
|
+
*/
|
|
372
|
+
export const UI_ACTION_PULL_CIRCUIT_CLOSED =
|
|
373
|
+
"[guuey] card updates resumed — the session behind this card answered again";
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Logged when a host's `onSessionUnrestorable` or `onSessionRestored`
|
|
377
|
+
* callback throws. The relay never rejects into the card, so it goes on; this
|
|
378
|
+
* line is the page's only trace that its own "session ended" state may now be
|
|
379
|
+
* out of step with the card. It names the callback and the error's `name`,
|
|
380
|
+
* never the error's text, which is the host's and can carry anything.
|
|
381
|
+
*/
|
|
382
|
+
export const UI_ACTION_HOST_CALLBACK_THREW =
|
|
383
|
+
"[guuey] the page's card-session handler threw — the card went on, but the page may not show its current state";
|
|
384
|
+
|
|
385
|
+
/** Call a host's session callback; a throw is logged by name and never escapes the relay. */
|
|
386
|
+
function callHostCallback(
|
|
387
|
+
callback: "onSessionUnrestorable" | "onSessionRestored",
|
|
388
|
+
fn: ((resourceUri: string) => void) | undefined,
|
|
389
|
+
resourceUri: string,
|
|
390
|
+
): void {
|
|
391
|
+
try {
|
|
392
|
+
fn?.(resourceUri);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
console.warn(UI_ACTION_HOST_CALLBACK_THREW, {
|
|
395
|
+
callback,
|
|
396
|
+
resourceUri,
|
|
397
|
+
name: err instanceof Error ? err.name : typeof err,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
344
402
|
/**
|
|
345
403
|
* Assemble the sandbox-facing action relay from a host transport. The
|
|
346
404
|
* returned function is shaped for an `onCallTool` bridge: it always
|
|
@@ -360,6 +418,11 @@ export const UI_ACTION_PULL_CIRCUIT_OPEN =
|
|
|
360
418
|
* The complement — telling the USER the session is unrestorable so a tripped
|
|
361
419
|
* circuit is not a silent freeze — is the `onSessionUnrestorable` signal
|
|
362
420
|
* (guuey#1249 item 4); this leg only bounds the hammer.
|
|
421
|
+
*
|
|
422
|
+
* An OPEN circuit is terminal for the mount unless the host wires
|
|
423
|
+
* `onSessionRestored`. Then a successful `ggui_runtime_refresh_ws_token` for
|
|
424
|
+
* the locator HALF-OPENS it: the next pull goes through as one probe, and only
|
|
425
|
+
* a probe the session answers closes it (see `onSessionRestored`).
|
|
363
426
|
*/
|
|
364
427
|
export function createMcpUiActionRelay(
|
|
365
428
|
deps: CreateMcpUiActionRelayDeps,
|
|
@@ -367,10 +430,21 @@ export function createMcpUiActionRelay(
|
|
|
367
430
|
// Per-relay-instance (one card mount). A recovered/absent locator is deleted,
|
|
368
431
|
// so this stays as small as the mounted cards; the mount tears it down.
|
|
369
432
|
const pullFailures = new Map<string, number>();
|
|
433
|
+
// Open locators a successful token refresh has half-opened: the next pull is
|
|
434
|
+
// let through as the probe. Only ever filled when the host wires the restore.
|
|
435
|
+
const halfOpen = new Set<string>();
|
|
436
|
+
|
|
437
|
+
const isOpen = (uri: string): boolean => (pullFailures.get(uri) ?? 0) >= PULL_CIRCUIT_THRESHOLD;
|
|
370
438
|
|
|
371
439
|
const recordPull = (uri: string, unavailable: boolean): void => {
|
|
372
440
|
if (!unavailable) {
|
|
441
|
+
const wasOpen = isOpen(uri);
|
|
373
442
|
pullFailures.delete(uri); // the session answered → close the circuit
|
|
443
|
+
if (wasOpen) {
|
|
444
|
+
console.warn(UI_ACTION_PULL_CIRCUIT_CLOSED, { resourceUri: uri });
|
|
445
|
+
// The circuit is closed either way: a throwing host callback is logged, never rethrown.
|
|
446
|
+
callHostCallback("onSessionRestored", deps.onSessionRestored, uri);
|
|
447
|
+
}
|
|
374
448
|
return;
|
|
375
449
|
}
|
|
376
450
|
const next = (pullFailures.get(uri) ?? 0) + 1;
|
|
@@ -384,12 +458,8 @@ export function createMcpUiActionRelay(
|
|
|
384
458
|
// guuey#1249 item 4: tell the host the session is unrestorable so the
|
|
385
459
|
// bounded circuit isn't a silent freeze. A throw from the host callback
|
|
386
460
|
// must never break the relay's never-reject contract.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
} catch {
|
|
390
|
-
// A host-supplied callback that throws is the host's bug, not the
|
|
391
|
-
// relay's — the storm is already bounded either way.
|
|
392
|
-
}
|
|
461
|
+
// The storm is bounded either way: a throwing host callback is logged, never rethrown.
|
|
462
|
+
callHostCallback("onSessionUnrestorable", deps.onSessionUnrestorable, uri);
|
|
393
463
|
}
|
|
394
464
|
};
|
|
395
465
|
|
|
@@ -407,9 +477,10 @@ export function createMcpUiActionRelay(
|
|
|
407
477
|
}
|
|
408
478
|
|
|
409
479
|
const isPull = request.name === PULL_TOOL;
|
|
410
|
-
// Circuit OPEN for this locator's pull → fail-fast, never touch the door
|
|
411
|
-
|
|
412
|
-
|
|
480
|
+
// Circuit OPEN for this locator's pull → fail-fast, never touch the door,
|
|
481
|
+
// unless a token refresh half-opened it: then this one pull is the probe.
|
|
482
|
+
if (isPull && isOpen(request.resourceUri)) {
|
|
483
|
+
if (!halfOpen.delete(request.resourceUri)) return unavailableToolCallResult();
|
|
413
484
|
}
|
|
414
485
|
|
|
415
486
|
let raw: unknown;
|
|
@@ -421,6 +492,18 @@ export function createMcpUiActionRelay(
|
|
|
421
492
|
}
|
|
422
493
|
const result = raw === undefined ? undefined : asToolCallResult(raw);
|
|
423
494
|
if (isPull) recordPull(request.resourceUri, result === undefined);
|
|
495
|
+
// A refresh the session granted is fresh evidence it is live, but on the
|
|
496
|
+
// token channel, not the pull channel that tripped: it half-opens, and the
|
|
497
|
+
// probe pull decides. Only for a host that can withdraw its verdict.
|
|
498
|
+
if (
|
|
499
|
+
request.name === REFRESH_WS_TOKEN_TOOL &&
|
|
500
|
+
deps.onSessionRestored !== undefined &&
|
|
501
|
+
result !== undefined &&
|
|
502
|
+
result.isError !== true &&
|
|
503
|
+
isOpen(request.resourceUri)
|
|
504
|
+
) {
|
|
505
|
+
halfOpen.add(request.resourceUri);
|
|
506
|
+
}
|
|
424
507
|
return result ?? unavailableToolCallResult();
|
|
425
508
|
};
|
|
426
509
|
}
|