sliccy 6.105.1 → 6.106.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/README.md +22 -20
- package/dist/node-server/_cloud_core/src/operations/list.d.ts.map +1 -1
- package/dist/node-server/_cloud_core/src/operations/list.js +149 -115
- package/dist/node-server/electron-federated-cdp.d.ts +6 -4
- package/dist/ui/.vite/manifest.json +118 -118
- package/dist/ui/assets/{account-store-BzlZkBRi.js → account-store-B8Pfw-0V.js} +1 -1
- package/dist/ui/assets/account-store-Ll7wax8m.js +95 -0
- package/dist/ui/assets/adobe-BO7x6BuH.js +1 -0
- package/dist/ui/assets/{adobe-BihjR2aN.js → adobe-BxRSM1Pj.js} +1 -1
- package/dist/ui/assets/{agent-panels-BU1YRtd6.js → agent-panels-g8G49oRT.js} +1 -1
- package/dist/ui/assets/{apply-layout-D91iJnwj.js → apply-layout-C0TQPoEe.js} +1 -1
- package/dist/ui/assets/{apps-C_U-KsGp.js → apps-PymeL9Xa.js} +1 -1
- package/dist/ui/assets/{azure-openai-sF6H5kgW.js → azure-openai-C7os7J_U.js} +1 -1
- package/dist/ui/assets/{azure-openai-Bw0uWZGO.js → azure-openai-CAoXuOs-.js} +2 -2
- package/dist/ui/assets/cdp-ws-page-bridge-DI26JBMr.js +1 -0
- package/dist/ui/assets/{cerebras-YRvd81gB.js → cerebras-cGiD1OCz.js} +1 -1
- package/dist/ui/assets/{cerebras-BXHbMJZ4.js → cerebras-xgGcVRkO.js} +1 -1
- package/dist/ui/assets/{connect-surface-xmzMSsEa.js → connect-surface-BAyj8na7.js} +1 -1
- package/dist/ui/assets/{fs-C5irkDgy.js → fs-BYPE2GBU.js} +2 -2
- package/dist/ui/assets/{fs-CfV7z7u1.js → fs-BjD4IFcY.js} +1 -1
- package/dist/ui/assets/{github-dRIJp79S.js → github-D54uhROe.js} +2 -2
- package/dist/ui/assets/github-DRjUqzVj.js +1 -0
- package/dist/ui/assets/{github-copilot-BLe6fyKB.js → github-copilot-QrDT0dY9.js} +1 -1
- package/dist/ui/assets/{github-copilot-Cq69Q1S1.js → github-copilot-bhxJ4Fea.js} +1 -1
- package/dist/ui/assets/{github-install-BvY8B6fA.js → github-install-gnC0Lqek.js} +1 -1
- package/dist/ui/assets/{github-zip-D7XHOBXr.js → github-zip-BoeQELUI.js} +1 -1
- package/dist/ui/assets/{js-realm-worker-DYtqK8ky.js → js-realm-worker-DJSXeKmZ.js} +1 -1
- package/dist/ui/assets/{kernel-worker-Biv5_wUN.js → kernel-worker-BVymbN6I.js} +635 -635
- package/dist/ui/assets/{local-llm-0p1lFHjz.js → local-llm-ClcJIUQm.js} +1 -1
- package/dist/ui/assets/{local-llm-BtUdVk1V.js → local-llm-TTukOow-.js} +1 -1
- package/dist/ui/assets/{main-XDb3cs-j.js → main-C_KLKdNT.js} +3 -3
- package/dist/ui/assets/{mount-DrBtXals.js → mount-61x7SRyO.js} +2 -2
- package/dist/ui/assets/{mount-DMQpmO_S.js → mount-Rg_DgJfZ.js} +1 -1
- package/dist/ui/assets/{mount-recovery-0xhp73i7.js → mount-recovery-X_7jd5Ca.js} +1 -1
- package/dist/ui/assets/new-session-aYj3zOSa.js +2 -0
- package/dist/ui/assets/{oauth-bootstrap-DYTFzmoM.js → oauth-bootstrap-CdnaQjEw.js} +2 -2
- package/dist/ui/assets/{onboarding-orchestrator-C3hpm1Jf.js → onboarding-orchestrator-BMcTSuyD.js} +1 -1
- package/dist/ui/assets/{openai-codex-LoXO8Ljg.js → openai-codex-Dmivo_Gp.js} +1 -1
- package/dist/ui/assets/{openai-codex-BI6aAgz-.js → openai-codex-x88G9BUn.js} +1 -1
- package/dist/ui/assets/{openrouter-BKx5Z6bM.js → openrouter-BH2ZLksk.js} +1 -1
- package/dist/ui/assets/{openrouter-D2APEIUs.js → openrouter-BLqPXsSy.js} +1 -1
- package/dist/ui/assets/{openrouter-oauth-CwACvxY6.js → openrouter-oauth-BnLCn_80.js} +1 -1
- package/dist/ui/assets/{openrouter-oauth-D4wSzWV1.js → openrouter-oauth-sxiHJdE7.js} +1 -1
- package/dist/ui/assets/{panel-rpc-handlers-Brwn-P2J.js → panel-rpc-handlers-dknr7VAo.js} +2 -2
- package/dist/ui/assets/{panelize-shell-DOZyJ0fa.js → panelize-shell-CiIlvObk.js} +1 -1
- package/dist/ui/assets/{provider-Ck4d3QKr.js → provider-BZRMPYnW.js} +1 -1
- package/dist/ui/assets/provider-CU36fK-N.js +2 -0
- package/dist/ui/assets/provider-settings-BjYnLtEv.js +1 -0
- package/dist/ui/assets/provider-store-access-JEOqKjQY.js +1 -0
- package/dist/ui/assets/provider-store-access-imP4Yxar.js +1 -0
- package/dist/ui/assets/{providers-Bv-7eK6T.js → providers-BHS1IUoy.js} +1 -1
- package/dist/ui/assets/{providers-Db6Bcpqh.js → providers-DWrWxGe2.js} +2 -2
- package/dist/ui/assets/{quick-llm-r7iioCbc.js → quick-llm-BMCZ2LzV.js} +1 -1
- package/dist/ui/assets/{recovery-screen-0XV-viDe.js → recovery-screen-DvgY0yhj.js} +1 -1
- package/dist/ui/assets/redirect-uri-DwIRNp6Y.js +1 -0
- package/dist/ui/assets/{run-DtXEX6sA.js → run-BsDhczkr.js} +1 -1
- package/dist/ui/assets/{setup-feature-flags-remote-CqZBHh5l.js → setup-feature-flags-remote-DvwkLrAn.js} +1 -1
- package/dist/ui/assets/sidecar-repair-VvVQ6ToZ.js +1 -0
- package/dist/ui/assets/{skills-B1sr_Tq0.js → skills-DKYOl-F5.js} +1 -1
- package/dist/ui/assets/slicc-version-Hr_m9p8L.js +1 -0
- package/dist/ui/assets/{state-DreMrf2k.js → state-DuedGt2p.js} +1 -1
- package/dist/ui/assets/{store-TMWGaifB.js → store-CHCgbnT-.js} +1 -1
- package/dist/ui/assets/store-QC-CzXzQ.js +1 -0
- package/dist/ui/assets/{sudo-COiu75CG.js → sudo-BJ8CEK3P.js} +1 -1
- package/dist/ui/assets/{sync-dialog-D4gGyFPx.js → sync-dialog-Qehw3mQD.js} +1 -1
- package/dist/ui/assets/{tray-sidecar-CVKekXJl.js → tray-sidecar-DtT9uU9g.js} +1 -1
- package/dist/ui/assets/{upgrade-detection-yf4VBHJa.js → upgrade-detection-C8jf14L4.js} +1 -1
- package/dist/ui/assets/{voice-reply-CdQyjZ4U.js → voice-reply-B1kVlbBj.js} +1 -1
- package/dist/ui/assets/{wc-attach-DsY3Di7b.js → wc-attach-XdX6eYQo.js} +2 -2
- package/dist/ui/assets/{wc-extension-BJjoLztQ.js → wc-extension-CPwHtPzV.js} +2 -2
- package/dist/ui/assets/{wc-float-label-BYMhctbP.js → wc-float-label-CPvJ4KqB.js} +1 -1
- package/dist/ui/assets/{wc-floatbar-online-adsNHm5r.js → wc-floatbar-online-xb8_BJUU.js} +1 -1
- package/dist/ui/assets/{wc-follower-Dvb_NBfE.js → wc-follower-DCdJC8jL.js} +3 -3
- package/dist/ui/assets/{wc-follower-oauth-CkSuotR4.js → wc-follower-oauth-qVlXsjVO.js} +1 -1
- package/dist/ui/assets/{wc-live-ChSZdFGO.js → wc-live-CIvj4K_5.js} +6 -6
- package/dist/ui/assets/{wc-nav-DML7srXc.js → wc-nav-BNsPnWV1.js} +2 -2
- package/dist/ui/assets/{wc-onboarding-BJcv2HxO.js → wc-onboarding-BVAq5ev-.js} +2 -2
- package/dist/ui/assets/{wc-placeholder-CBTA32WV.js → wc-placeholder-CgUmZZyG.js} +3 -3
- package/dist/ui/assets/{wc-settings-CTbiADwY.js → wc-settings-DuWB_OaV.js} +3 -3
- package/dist/ui/assets/{wc-shell-DGGTq6Dg.js → wc-shell-DMynRWff.js} +6 -6
- package/dist/ui/assets/{wc-shortcut-config-6WhfEkWk.js → wc-shortcut-config-GlDHZhHy.js} +1 -1
- package/dist/ui/assets/{wc-sprinkles-CPStewTt.js → wc-sprinkles-Dr1Lkgq8.js} +2 -2
- package/dist/ui/assets/{wc-transcript-export-DBgvfYAE.js → wc-transcript-export-BWRBvTzT.js} +2 -2
- package/dist/ui/assets/{wc-tray-C5ousbvR.js → wc-tray-Cdy44C5u.js} +2 -2
- package/dist/ui/assets/welcome-detection-CmWVDuGh.js +1 -0
- package/dist/ui/assets/{xai-grok-BMwfBJts.js → xai-grok-CokTFH0m.js} +1 -1
- package/dist/ui/assets/{xai-grok-DrvDcz8F.js → xai-grok-D5CjF6cu.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/packages/webapp/index.html +3 -3
- package/package.json +2 -2
- package/dist/ui/assets/account-store-DLMQFidg.js +0 -95
- package/dist/ui/assets/adobe-BMlAh6hv.js +0 -1
- package/dist/ui/assets/cdp-ws-page-bridge-D_PY6ku1.js +0 -1
- package/dist/ui/assets/github-CvkSRuma.js +0 -1
- package/dist/ui/assets/new-session-tkh2Pth8.js +0 -2
- package/dist/ui/assets/provider-CMyVpl4V.js +0 -2
- package/dist/ui/assets/provider-settings-HVEYL_IN.js +0 -1
- package/dist/ui/assets/provider-store-access-C4sEMSPk.js +0 -1
- package/dist/ui/assets/provider-store-access-DyMm3fYa.js +0 -1
- package/dist/ui/assets/redirect-uri-D2xDbDb-.js +0 -1
- package/dist/ui/assets/sidecar-repair-Ct_a2-ms.js +0 -1
- package/dist/ui/assets/slicc-version-D3NdtGfe.js +0 -1
- package/dist/ui/assets/store-C7oltmmm.js +0 -1
- package/dist/ui/assets/welcome-detection-Bripv_Hp.js +0 -1
package/README.md
CHANGED
|
@@ -187,29 +187,31 @@ Why the name? SLICC stands for **Self-Licking Ice Cream Cone**: a recursive syst
|
|
|
187
187
|
|
|
188
188
|
## Keyboard mode
|
|
189
189
|
|
|
190
|
-
SLICC's main control is a text field, so it has no always-on shortcuts to collide with your typing. Instead it has a **mode**, like vim
|
|
191
|
-
|
|
192
|
-
| Key | Action
|
|
193
|
-
| ------------------------------------------ |
|
|
194
|
-
| <kbd>Esc</kbd> |
|
|
195
|
-
| <kbd>1</kbd>…<kbd>9</kbd> | Switch to that agent in the tab strip (<kbd>9</kbd> = last)
|
|
196
|
-
| <kbd>d</kbd> | Next agent, looping
|
|
197
|
-
| <kbd>c</kbd> or <kbd>⏎</kbd> | Back to the composer
|
|
198
|
-
| <kbd>n</kbd> | New conversation
|
|
199
|
-
| <kbd>b</kbd> | Toggle the left rail
|
|
200
|
-
| <kbd>x</kbd> | Toggle the right panel
|
|
201
|
-
| <kbd>f</kbd> | File browser
|
|
202
|
-
| <kbd>t</kbd> | Browser tabs
|
|
203
|
-
| <kbd>e</kbd> | Terminal
|
|
204
|
-
| <kbd>m</kbd> | Memory
|
|
205
|
-
| <kbd>s</kbd> | Sprinkles, looping
|
|
206
|
-
| <kbd>l</kbd> | Model picker
|
|
207
|
-
| <kbd>a</kbd> | Accounts
|
|
208
|
-
| <kbd>h</kbd>, <kbd>?</kbd> or <kbd>/</kbd> | This list, in an overlay
|
|
190
|
+
SLICC's main control is a text field, so it has no always-on shortcuts to collide with your typing. Instead it has a **mode**, like vim — and, like vim, the mode is where you rest: keyboard mode is on whenever nothing is focused for typing, and every key below is a bare letter. Press <kbd>Esc</kbd> to leave the composer for it, or just click somewhere that is not a text field. To type again, press <kbd>⏎</kbd> or <kbd>c</kbd> (or click into the composer). While the mode is on, a pill above the composer says so.
|
|
191
|
+
|
|
192
|
+
| Key | Action |
|
|
193
|
+
| ------------------------------------------ | -------------------------------------------------------------- |
|
|
194
|
+
| <kbd>Esc</kbd> | Leave the composer for keyboard mode (again: exit full screen) |
|
|
195
|
+
| <kbd>1</kbd>…<kbd>9</kbd> | Switch to that agent in the tab strip (<kbd>9</kbd> = last) |
|
|
196
|
+
| <kbd>d</kbd> | Next agent, looping |
|
|
197
|
+
| <kbd>c</kbd> or <kbd>⏎</kbd> | Back to the composer |
|
|
198
|
+
| <kbd>n</kbd> | New conversation |
|
|
199
|
+
| <kbd>b</kbd> | Toggle the left rail |
|
|
200
|
+
| <kbd>x</kbd> | Toggle the right panel |
|
|
201
|
+
| <kbd>f</kbd> | File browser |
|
|
202
|
+
| <kbd>t</kbd> | Browser tabs |
|
|
203
|
+
| <kbd>e</kbd> | Terminal |
|
|
204
|
+
| <kbd>m</kbd> | Memory |
|
|
205
|
+
| <kbd>s</kbd> | Sprinkles, looping |
|
|
206
|
+
| <kbd>l</kbd> | Model picker |
|
|
207
|
+
| <kbd>a</kbd> | Accounts |
|
|
208
|
+
| <kbd>h</kbd>, <kbd>?</kbd> or <kbd>/</kbd> | This list, in an overlay |
|
|
209
209
|
|
|
210
210
|
Every key you press shows up as a cap on the pill — dimmed if it was not bound to anything, so a mistyped key reads as "that did nothing" rather than as a dead keyboard. The strip clears after a moment of quiet.
|
|
211
211
|
|
|
212
|
-
Keys that navigate, or toggle chrome (digits, <kbd>d</kbd>, <kbd>b</kbd>, <kbd>x</kbd>, <kbd>s</kbd>, help), keep the mode on; anything that hands focus to a
|
|
212
|
+
Keys that navigate, or toggle chrome (digits, <kbd>d</kbd>, <kbd>b</kbd>, <kbd>x</kbd>, <kbd>s</kbd>, help), keep the mode on; anything that hands focus to a text field leaves it, whether that is a shortcut, a click, or a panel that opens with a field ready. Inside a text field nothing is intercepted at all, and a focused button keeps its own <kbd>⏎</kbd> and <kbd>Space</kbd>. The overlay is also in the avatar menu, under **Keyboard mode**.
|
|
213
|
+
|
|
214
|
+
**Switching agents keeps your place.** The mode you were in when you left a cone is the mode you land back in: leave it typing and the caret is waiting in the new cone's composer, leave it in keyboard mode and the pill is still up. A scoop's transcript is read-only and has no composer at all, so it always puts you in keyboard mode — but it does not overwrite what you left behind, so cone → scoop → cone gives your caret back.
|
|
213
215
|
|
|
214
216
|
**Rebinding.** The keys live in `/etc/slicc/keys.json` in the VFS, written the first time SLICC starts and never touched again — edit it and reload the tab. Each entry maps a key to a command (`"q": "terminal"`) and several keys can share one command. The file is applied _over_ the built-in keymap, so `null` removes a binding while deleting the line just restores its default — which is also how you inherit any keys a later version adds. The file lists every command it accepts. <kbd>Esc</kbd> and the digits are reserved. A bad line is skipped with a warning rather than costing you the rest of the file, and the help overlay always shows what is actually bound.
|
|
215
217
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/operations/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/operations/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,aAAa,CAAC;AAE7D,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAwLD;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,aAAa,EACnB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,SAAS,EAAE,CAAC,CA8BtB"}
|
|
@@ -1,3 +1,146 @@
|
|
|
1
|
+
const STALE_RESERVATION_MS = 10 * 60 * 1000;
|
|
2
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
3
|
+
/** True when entry metadata matches every key/value in the filter (or no filter). */
|
|
4
|
+
function matchesMetadataFilter(entry, metadata) {
|
|
5
|
+
if (!metadata)
|
|
6
|
+
return true;
|
|
7
|
+
if (!entry.metadata)
|
|
8
|
+
return false;
|
|
9
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
10
|
+
if (entry.metadata[k] !== v)
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
function isStaleReservation(entry) {
|
|
16
|
+
// Reclaim if stale OR if reservedAt is missing (legacy/malformed entry
|
|
17
|
+
// from before commit e9011ba6 — could otherwise wedge cap forever).
|
|
18
|
+
if (!entry.reservedAt)
|
|
19
|
+
return true;
|
|
20
|
+
return Date.now() - new Date(entry.reservedAt).getTime() > STALE_RESERVATION_MS;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Handle a reserved registry entry: reclaim if stale, otherwise keep.
|
|
24
|
+
* Returns the entry to include in the reconciled list, or null if reclaimed.
|
|
25
|
+
*/
|
|
26
|
+
async function reconcileReserved(entry, registry) {
|
|
27
|
+
if (!isStaleReservation(entry))
|
|
28
|
+
return entry;
|
|
29
|
+
await registry.remove(entry.sandboxId);
|
|
30
|
+
console.warn('[cloud-core] reclaimed stale reservation', {
|
|
31
|
+
sandboxId: entry.sandboxId,
|
|
32
|
+
reservedAt: entry.reservedAt ?? '(missing)',
|
|
33
|
+
});
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** Mark a missing real sandbox dead (idempotent registry write). */
|
|
37
|
+
async function markDead(entry, registry) {
|
|
38
|
+
if (entry.state !== 'dead') {
|
|
39
|
+
await registry.update(entry.sandboxId, { state: 'dead' });
|
|
40
|
+
}
|
|
41
|
+
return { ...entry, state: 'dead' };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reconcile one non-reserved registry entry against live substrate state.
|
|
45
|
+
* Mutates liveById (deletes matched sandbox ids).
|
|
46
|
+
*/
|
|
47
|
+
async function reconcileLiveEntry(entry, liveById, registry) {
|
|
48
|
+
const liveEntry = liveById.get(entry.sandboxId);
|
|
49
|
+
if (!liveEntry) {
|
|
50
|
+
// Substrate doesn't know about it — mark dead unless it's a placeholder.
|
|
51
|
+
// The 'pending-' prefix is a sentinel for "no real sandbox yet" (paired
|
|
52
|
+
// with state:'reserved' by reserveSlot before substrate.create).
|
|
53
|
+
if (entry.sandboxId.startsWith('pending-')) {
|
|
54
|
+
return entry;
|
|
55
|
+
}
|
|
56
|
+
return markDead(entry, registry);
|
|
57
|
+
}
|
|
58
|
+
if (entry.state !== liveEntry.state) {
|
|
59
|
+
await registry.update(entry.sandboxId, { state: liveEntry.state });
|
|
60
|
+
}
|
|
61
|
+
liveById.delete(entry.sandboxId);
|
|
62
|
+
return { ...entry, state: liveEntry.state };
|
|
63
|
+
}
|
|
64
|
+
async function reconcileRegistryEntry(entry, liveById, registry) {
|
|
65
|
+
// Reserved entries: check for stale reservations (TTL: 10 min) and reclaim.
|
|
66
|
+
// Stale reservations are from crashed operations that never completed or rolled back.
|
|
67
|
+
if (entry.state === 'reserved') {
|
|
68
|
+
return reconcileReserved(entry, registry);
|
|
69
|
+
}
|
|
70
|
+
return reconcileLiveEntry(entry, liveById, registry);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Recover joinUrl from /tmp/slicc-join.json ONLY when sandbox is running.
|
|
74
|
+
* Calling substrate.connect() on a paused sandbox would RESUME it, which:
|
|
75
|
+
* (a) burns sandbox runtime silently
|
|
76
|
+
* (b) destabilizes the leader chromium mid-list
|
|
77
|
+
* Paused orphans surface with joinUrl='' — UI hides the Open button.
|
|
78
|
+
*/
|
|
79
|
+
async function recoverJoinFromSandbox(summary, substrate, seed) {
|
|
80
|
+
if (seed.joinUrl || summary.state !== 'running')
|
|
81
|
+
return seed;
|
|
82
|
+
try {
|
|
83
|
+
const handle = await substrate.connect(summary.sandboxId);
|
|
84
|
+
const joinData = await handle.readFile('/tmp/slicc-join.json');
|
|
85
|
+
const parsed = JSON.parse(joinData);
|
|
86
|
+
return {
|
|
87
|
+
joinUrl: parsed.joinUrl ?? '',
|
|
88
|
+
trayId: seed.trayId ?? parsed.trayId,
|
|
89
|
+
lastJoinUpdatedAt: seed.lastJoinUpdatedAt ?? parsed.updatedAt,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// File not readable (sandbox is transitioning, or file doesn't exist).
|
|
94
|
+
// Leave joinUrl empty — UI will handle gracefully.
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
console.warn('[cloud-core] orphan recovery readFile failed', {
|
|
97
|
+
sandboxId: summary.sandboxId,
|
|
98
|
+
err: msg,
|
|
99
|
+
});
|
|
100
|
+
return seed;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Rebuild a ConeEntry for a substrate sandbox missing from the registry. */
|
|
104
|
+
async function recoverOrphan(summary, substrate) {
|
|
105
|
+
const now = new Date().toISOString();
|
|
106
|
+
const recoveredJoin = await recoverJoinFromSandbox(summary, substrate, {
|
|
107
|
+
joinUrl: summary.metadata?.['joinUrl'] ?? '',
|
|
108
|
+
trayId: summary.metadata?.['trayId'],
|
|
109
|
+
lastJoinUpdatedAt: summary.metadata?.['lastJoinUpdatedAt'],
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
sandboxId: summary.sandboxId,
|
|
113
|
+
substrate: 'e2b',
|
|
114
|
+
name: summary.metadata?.['name'] ?? summary.name,
|
|
115
|
+
createdAt: summary.metadata?.['createdAt'] ?? now,
|
|
116
|
+
joinUrl: recoveredJoin.joinUrl,
|
|
117
|
+
lastSeen: now,
|
|
118
|
+
state: summary.state,
|
|
119
|
+
trayId: recoveredJoin.trayId,
|
|
120
|
+
lastJoinUpdatedAt: recoveredJoin.lastJoinUpdatedAt,
|
|
121
|
+
metadata: summary.metadata,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Refresh the timeout of every running cone so active users keep their cones
|
|
126
|
+
* alive past the 1h default. Failures are non-fatal — a sandbox that
|
|
127
|
+
* disappears between substrate.list() and extendTimeout() will be discovered
|
|
128
|
+
* dead on the NEXT list call.
|
|
129
|
+
*/
|
|
130
|
+
async function extendRunningTimeouts(cones, substrate) {
|
|
131
|
+
await Promise.all(cones
|
|
132
|
+
.filter((c) => c.state === 'running')
|
|
133
|
+
.map(async (c) => {
|
|
134
|
+
try {
|
|
135
|
+
await substrate.extendTimeout(c.sandboxId, DEFAULT_TTL_MS);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
// Sandbox may have died between list and extendTimeout; ignore.
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
+
console.warn('[cloud-core] extendTimeout failed', { sandboxId: c.sandboxId, err: msg });
|
|
141
|
+
}
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
1
144
|
/**
|
|
2
145
|
* List cones reconciling registry against substrate.
|
|
3
146
|
* - Substrate is source of truth for state (running/paused).
|
|
@@ -11,18 +154,6 @@ export async function listCones(deps, opts = {}) {
|
|
|
11
154
|
// Pass metadata filter to substrate.list for server-side filtering
|
|
12
155
|
const live = await deps.substrate.list(opts.metadata ? { metadata: opts.metadata } : undefined);
|
|
13
156
|
const liveById = new Map(live.map((s) => [s.sandboxId, s]));
|
|
14
|
-
// Helper to check if an entry matches the metadata filter
|
|
15
|
-
const matchesFilter = (entry) => {
|
|
16
|
-
if (!opts.metadata)
|
|
17
|
-
return true;
|
|
18
|
-
if (!entry.metadata)
|
|
19
|
-
return false;
|
|
20
|
-
for (const [k, v] of Object.entries(opts.metadata)) {
|
|
21
|
-
if (entry.metadata[k] !== v)
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
return true;
|
|
25
|
-
};
|
|
26
157
|
// Pass 1: walk registry; reconcile against live.
|
|
27
158
|
// Reconciliation runs for EVERY registry entry regardless of metadata filter
|
|
28
159
|
// — otherwise zombie entries (e.g. legacy entries without userId metadata)
|
|
@@ -31,115 +162,18 @@ export async function listCones(deps, opts = {}) {
|
|
|
31
162
|
// per-user view.
|
|
32
163
|
const reconciled = [];
|
|
33
164
|
for (const entry of registryEntries) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const STALE_MS = 10 * 60 * 1000;
|
|
38
|
-
// Reclaim if stale OR if reservedAt is missing (legacy/malformed entry
|
|
39
|
-
// from before commit e9011ba6 — could otherwise wedge cap forever).
|
|
40
|
-
const isStale = !entry.reservedAt || Date.now() - new Date(entry.reservedAt).getTime() > STALE_MS;
|
|
41
|
-
if (isStale) {
|
|
42
|
-
await deps.registry.remove(entry.sandboxId);
|
|
43
|
-
console.warn('[cloud-core] reclaimed stale reservation', {
|
|
44
|
-
sandboxId: entry.sandboxId,
|
|
45
|
-
reservedAt: entry.reservedAt ?? '(missing)',
|
|
46
|
-
});
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
// Active reservation — preserve as-is (in-flight operation holding a cap slot)
|
|
50
|
-
reconciled.push(entry);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const liveEntry = liveById.get(entry.sandboxId);
|
|
54
|
-
if (!liveEntry) {
|
|
55
|
-
// Substrate doesn't know about it — mark dead unless it's a placeholder.
|
|
56
|
-
// The 'pending-' prefix is a sentinel for "no real sandbox yet" (paired
|
|
57
|
-
// with state:'reserved' by reserveSlot before substrate.create).
|
|
58
|
-
if (entry.sandboxId.startsWith('pending-')) {
|
|
59
|
-
// Reservation placeholder — no real sandbox yet. Keep it alive.
|
|
60
|
-
reconciled.push(entry);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
// Real sandbox is missing — mark dead
|
|
64
|
-
if (entry.state !== 'dead') {
|
|
65
|
-
await deps.registry.update(entry.sandboxId, { state: 'dead' });
|
|
66
|
-
}
|
|
67
|
-
reconciled.push({ ...entry, state: 'dead' });
|
|
68
|
-
}
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (entry.state !== liveEntry.state) {
|
|
72
|
-
await deps.registry.update(entry.sandboxId, { state: liveEntry.state });
|
|
73
|
-
}
|
|
74
|
-
reconciled.push({ ...entry, state: liveEntry.state });
|
|
75
|
-
liveById.delete(entry.sandboxId);
|
|
165
|
+
const next = await reconcileRegistryEntry(entry, liveById, deps.registry);
|
|
166
|
+
if (next)
|
|
167
|
+
reconciled.push(next);
|
|
76
168
|
}
|
|
77
169
|
// Pass 2: any substrate entries not in registry → recover.
|
|
78
170
|
for (const summary of liveById.values()) {
|
|
79
|
-
const
|
|
80
|
-
// Try to read the real joinUrl from the sandbox. Orphans created before this
|
|
81
|
-
// fix won't have it in metadata, but we can read /tmp/slicc-join.json directly.
|
|
82
|
-
let joinUrl = summary.metadata?.['joinUrl'] ?? '';
|
|
83
|
-
let trayId = summary.metadata?.['trayId'];
|
|
84
|
-
let lastJoinUpdatedAt = summary.metadata?.['lastJoinUpdatedAt'];
|
|
85
|
-
// Recover joinUrl from /tmp/slicc-join.json ONLY when sandbox is running.
|
|
86
|
-
// Calling substrate.connect() on a paused sandbox would RESUME it, which:
|
|
87
|
-
// (a) burns sandbox runtime silently
|
|
88
|
-
// (b) destabilizes the leader chromium mid-list
|
|
89
|
-
// Paused orphans surface with joinUrl='' — UI hides the Open button.
|
|
90
|
-
// User explicitly resumes via /api/cloud/resume which writes a fresh joinUrl.
|
|
91
|
-
if (!joinUrl && summary.state === 'running') {
|
|
92
|
-
try {
|
|
93
|
-
const handle = await deps.substrate.connect(summary.sandboxId);
|
|
94
|
-
const joinData = await handle.readFile('/tmp/slicc-join.json');
|
|
95
|
-
const parsed = JSON.parse(joinData);
|
|
96
|
-
joinUrl = parsed.joinUrl ?? '';
|
|
97
|
-
trayId = trayId ?? parsed.trayId;
|
|
98
|
-
lastJoinUpdatedAt = lastJoinUpdatedAt ?? parsed.updatedAt;
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
// File not readable (sandbox is transitioning, or file doesn't exist).
|
|
102
|
-
// Leave joinUrl empty — UI will handle gracefully.
|
|
103
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
104
|
-
console.warn('[cloud-core] orphan recovery readFile failed', {
|
|
105
|
-
sandboxId: summary.sandboxId,
|
|
106
|
-
err: msg,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const recovered = {
|
|
111
|
-
sandboxId: summary.sandboxId,
|
|
112
|
-
substrate: 'e2b',
|
|
113
|
-
name: summary.metadata?.['name'] ?? summary.name,
|
|
114
|
-
createdAt: summary.metadata?.['createdAt'] ?? now,
|
|
115
|
-
joinUrl,
|
|
116
|
-
lastSeen: now,
|
|
117
|
-
state: summary.state,
|
|
118
|
-
trayId,
|
|
119
|
-
lastJoinUpdatedAt,
|
|
120
|
-
metadata: summary.metadata,
|
|
121
|
-
};
|
|
171
|
+
const recovered = await recoverOrphan(summary, deps.substrate);
|
|
122
172
|
await deps.registry.append(recovered);
|
|
123
173
|
reconciled.push(recovered);
|
|
124
174
|
}
|
|
125
|
-
|
|
126
|
-
// alive past the 1h default. Failures are non-fatal — a sandbox that
|
|
127
|
-
// disappears between substrate.list() and extendTimeout() will be discovered
|
|
128
|
-
// dead on the NEXT list call.
|
|
129
|
-
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
130
|
-
await Promise.all(reconciled
|
|
131
|
-
.filter((c) => c.state === 'running')
|
|
132
|
-
.map(async (c) => {
|
|
133
|
-
try {
|
|
134
|
-
await deps.substrate.extendTimeout(c.sandboxId, DEFAULT_TTL_MS);
|
|
135
|
-
}
|
|
136
|
-
catch (err) {
|
|
137
|
-
// Sandbox may have died between list and extendTimeout; ignore.
|
|
138
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
-
console.warn('[cloud-core] extendTimeout failed', { sandboxId: c.sandboxId, err: msg });
|
|
140
|
-
}
|
|
141
|
-
}));
|
|
175
|
+
await extendRunningTimeouts(reconciled, deps.substrate);
|
|
142
176
|
// Apply the metadata filter to the RETURN value only — reconciliation
|
|
143
177
|
// above ran on all entries to keep zombies from accumulating.
|
|
144
|
-
return reconciled.filter(
|
|
178
|
+
return reconciled.filter((e) => matchesMetadataFilter(e, opts.metadata));
|
|
145
179
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FollowerToLeaderMessage } from './_shared/index.js';
|
|
1
|
+
import { type CDPPayload, type FollowerToLeaderMessage } from './_shared/index.js';
|
|
2
2
|
/** A CDP target as returned by `/json/list`. */
|
|
3
3
|
export interface FederatedCdpInspectableTarget {
|
|
4
4
|
id: string;
|
|
@@ -12,7 +12,8 @@ export interface FederatedCdpRequest {
|
|
|
12
12
|
requestId: string;
|
|
13
13
|
localTargetId: string;
|
|
14
14
|
method: string;
|
|
15
|
-
params
|
|
15
|
+
/** Per-method CDP params; shape is known only to the caller that issued the method. */
|
|
16
|
+
params?: CDPPayload;
|
|
16
17
|
sessionId?: string;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
@@ -30,7 +31,7 @@ export declare function buildTargetsAdvertise(runtimeId: string, targets: Federa
|
|
|
30
31
|
* oversize result is split into `chunkData` frames the leader reassembles.
|
|
31
32
|
*/
|
|
32
33
|
export declare function buildCdpResponses(requestId: string, outcome: {
|
|
33
|
-
result?:
|
|
34
|
+
result?: CDPPayload;
|
|
34
35
|
error?: string;
|
|
35
36
|
}): Array<Extract<FollowerToLeaderMessage, {
|
|
36
37
|
type: 'cdp.response';
|
|
@@ -38,7 +39,8 @@ export declare function buildCdpResponses(requestId: string, outcome: {
|
|
|
38
39
|
/** Translate a raw CDP event frame into a `cdp.event` tray-sync message. */
|
|
39
40
|
export declare function buildCdpEvent(frame: {
|
|
40
41
|
method: string;
|
|
41
|
-
params
|
|
42
|
+
/** Per-method CDP event params; shape depends on `method`. */
|
|
43
|
+
params?: CDPPayload;
|
|
42
44
|
sessionId?: string;
|
|
43
45
|
}): Extract<FollowerToLeaderMessage, {
|
|
44
46
|
type: 'cdp.event';
|