clay-server 5.2.1-beta.1 → 5.3.0-beta.1
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/clay-control-presentation-binding.js +5 -3
- package/lib/clay-product-control-tools.js +3 -3
- package/lib/clay-product-control.js +4 -2
- package/lib/clay-screen-controls.js +18 -3
- package/lib/clay-split-view-awareness.js +35 -0
- package/lib/clay-split-view-controls.js +122 -0
- package/lib/clay-split-view-input.js +20 -0
- package/lib/clay-split-view-render-proof.js +22 -0
- package/lib/pane-asset-manifest.js +63 -0
- package/lib/project-connection.js +19 -10
- package/lib/project-session-actions.js +1 -0
- package/lib/project-session-delete.js +2 -2
- package/lib/project-session-folders.js +1 -0
- package/lib/project-sessions.js +6 -1
- package/lib/project-split-view-document.js +33 -0
- package/lib/project-split-view-names.js +67 -0
- package/lib/project-split-view-organization.js +23 -0
- package/lib/project-split-views.js +479 -0
- package/lib/project-worker-conversation.js +158 -0
- package/lib/project-worker-creation-proposal.js +3 -4
- package/lib/project-worker-proposal.js +1 -1
- package/lib/project-worker-removal.js +101 -0
- package/lib/project.js +28 -1
- package/lib/public/app.js +32 -13
- package/lib/public/css/base.css +9 -0
- package/lib/public/css/filebrowser.css +0 -1
- package/lib/public/css/pane-header-controls.css +100 -0
- package/lib/public/css/pane.css +24 -5
- package/lib/public/css/popup-chat.css +123 -0
- package/lib/public/css/right-workbench.css +50 -18
- package/lib/public/css/split-view-layout.css +460 -0
- package/lib/public/css/split-view-worker-activity.css +97 -0
- package/lib/public/css/sticky-notes.css +13 -0
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +4 -1
- package/lib/public/modules/app-messages.js +25 -8
- package/lib/public/modules/chat-render-runtime.js +45 -0
- package/lib/public/modules/clay-control-copy.js +3 -0
- package/lib/public/modules/clay-navigation-render.js +1 -1
- package/lib/public/modules/clay-owner-navigation.js +15 -5
- package/lib/public/modules/clay-split-view-screen.js +74 -0
- package/lib/public/modules/clay-tool-activity.js +5 -0
- package/lib/public/modules/command-palette.js +30 -28
- package/lib/public/modules/contextual-clay-session.js +26 -2
- package/lib/public/modules/contextual-clay.js +5 -2
- package/lib/public/modules/contextual-mate-sessions.js +2 -0
- package/lib/public/modules/generation-stop.js +7 -1
- package/lib/public/modules/knowledge-workbench-entry.js +43 -0
- package/lib/public/modules/markdown.js +11 -0
- package/lib/public/modules/mate-knowledge-workbench.js +2 -7
- package/lib/public/modules/mate-sidebar.js +1 -1
- package/lib/public/modules/palette-clay-conversation.js +87 -0
- package/lib/public/modules/palette-render-helpers.js +25 -0
- package/lib/public/modules/pane-activation.js +46 -7
- package/lib/public/modules/pane-binding.js +12 -2
- package/lib/public/modules/pane-bridge.js +12 -3
- package/lib/public/modules/pane-deferred-bootstrap.js +32 -0
- package/lib/public/modules/pane-feature-assets.js +35 -0
- package/lib/public/modules/pane-search-receipt.js +12 -0
- package/lib/public/modules/permission-control.js +6 -6
- package/lib/public/modules/popup-chat-rendering.js +244 -0
- package/lib/public/modules/project-activation.js +2 -2
- package/lib/public/modules/qrcode.js +12 -1
- package/lib/public/modules/right-workbench.js +68 -0
- package/lib/public/modules/search-hints.js +79 -0
- package/lib/public/modules/session-context-menu.js +35 -2
- package/lib/public/modules/session-folder-context.js +24 -13
- package/lib/public/modules/session-folder-delete.js +18 -1
- package/lib/public/modules/session-folder-layout.js +7 -6
- package/lib/public/modules/session-search-pane-state.js +30 -0
- package/lib/public/modules/session-search.js +53 -13
- package/lib/public/modules/sidebar-mobile.js +3 -3
- package/lib/public/modules/sidebar-projects.js +1 -0
- package/lib/public/modules/sidebar-session-folders-ui.js +3 -3
- package/lib/public/modules/sidebar-sessions.js +57 -24
- package/lib/public/modules/sidebar.js +20 -3
- package/lib/public/modules/split-group-helpers.js +1 -1
- package/lib/public/modules/split-group-restore.js +8 -11
- package/lib/public/modules/split-pair-ui.js +18 -0
- package/lib/public/modules/split-pane-header-state.js +45 -0
- package/lib/public/modules/split-pane-reconciler.js +23 -4
- package/lib/public/modules/split-pane-renderer.js +330 -23
- package/lib/public/modules/split-pane-retention.js +157 -0
- package/lib/public/modules/split-session-boundary.js +21 -0
- package/lib/public/modules/split-view-create-action.js +15 -0
- package/lib/public/modules/split-view-create-menu.js +83 -0
- package/lib/public/modules/split-view-create-request.js +23 -0
- package/lib/public/modules/split-view-layout-client.js +198 -0
- package/lib/public/modules/split-view-layout-icons.js +37 -0
- package/lib/public/modules/split-view-layout-scope.js +38 -0
- package/lib/public/modules/split-view-organization.js +73 -0
- package/lib/public/modules/split-view-presentation-close.js +35 -0
- package/lib/public/modules/split-view-presentation-drag.js +254 -0
- package/lib/public/modules/split-view-presentation-drop.js +57 -0
- package/lib/public/modules/split-view-presentation-lookup.js +44 -0
- package/lib/public/modules/split-view-presentation-renderer.js +494 -0
- package/lib/public/modules/split-view-presentations.js +495 -0
- package/lib/public/modules/split-view-shell-controls.js +28 -0
- package/lib/public/modules/split-view-sidebar-menu.js +42 -0
- package/lib/public/modules/split-view-sidebar-renderer.js +30 -0
- package/lib/public/modules/split-view-singleton.js +21 -0
- package/lib/public/modules/split-view-worker-activity.js +94 -0
- package/lib/public/modules/split-view-worker-controls.js +30 -0
- package/lib/public/modules/split-view-worker-removal.js +137 -0
- package/lib/public/modules/split-view-worker-scroll.js +57 -0
- package/lib/public/modules/split-view-worker-transcript.js +46 -0
- package/lib/public/modules/split-view-workers.js +352 -0
- package/lib/public/modules/split-view.js +61 -22
- package/lib/public/modules/sticky-notes.js +9 -2
- package/lib/public/modules/tools.js +25 -5
- package/lib/public/modules/workbench-layout-preference.js +76 -0
- package/lib/public/pane-assets/app-OJEQXBBS.js +1 -0
- package/lib/public/pane-assets/chunk-4SMIVAB4.js +1 -0
- package/lib/public/pane-assets/chunk-623V5CEO.js +1 -0
- package/lib/public/pane-assets/chunk-6SM2WQJX.js +1 -0
- package/lib/public/pane-assets/chunk-7H36SUZA.js +2 -0
- package/lib/public/pane-assets/chunk-DGOFGEUK.js +1 -0
- package/lib/public/pane-assets/chunk-GML2R5JE.js +167 -0
- package/lib/public/pane-assets/chunk-I57URYSL.js +1 -0
- package/lib/public/pane-assets/chunk-I5P3MWQG.js +1 -0
- package/lib/public/pane-assets/chunk-UVGOPFEX.js +2 -0
- package/lib/public/pane-assets/manifest.json +551 -0
- package/lib/public/pane-assets/style-GN7R6XKV.css +1 -0
- package/lib/public/style.css +5 -1
- package/lib/sdk-bridge.js +16 -3
- package/lib/server-owner-ui.js +6 -2
- package/lib/server-owner-view-observation.js +34 -0
- package/lib/server-palette.js +1 -0
- package/lib/server-pane-bootstrap.js +24 -0
- package/lib/server-settings.js +2 -0
- package/lib/server-static-assets.js +52 -0
- package/lib/server-workbench-layout-settings.js +56 -0
- package/lib/server.js +6 -45
- package/lib/session-activity-state.js +28 -0
- package/lib/session-list-delivery.js +17 -0
- package/lib/session-pair-turn-control.js +1 -6
- package/lib/session-search.js +3 -0
- package/lib/session-split-group-anchors.js +2 -2
- package/lib/session-split-group-roles.js +5 -8
- package/lib/session-split-group-v2-store.js +0 -3
- package/lib/sessions.js +16 -6
- package/lib/split-view-layout.js +132 -0
- package/lib/users-workbench-layout-preferences.js +33 -0
- package/lib/users.js +3 -0
- package/lib/workspace-query-service.js +1 -5
- package/lib/ws-schema.js +15 -0
- package/lib/yoke/adapters/codex.js +1 -0
- package/lib/yoke/codex-app-server.js +3 -1
- package/lib/yoke/codex-idle-recovery.js +73 -20
- package/lib/yoke/codex-recovery-admission.js +65 -4
- package/package.json +6 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
var MAX_SESSIONS = 8;
|
|
2
|
+
var MIN_RATIO = 0.05;
|
|
3
|
+
var MAX_RATIO = 0.95;
|
|
4
|
+
var PRESETS = ["balanced", "columns", "rows", "grid", "main-left", "main-right"];
|
|
5
|
+
|
|
6
|
+
function validOrigin(value) {
|
|
7
|
+
return typeof value === "string" && value.length > 0 && value.length <= 128 && /^[A-Za-z0-9_-]+$/.test(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function validateTree(tree, expectedOrigins) {
|
|
11
|
+
var origins = [];
|
|
12
|
+
var invalid = null;
|
|
13
|
+
function walk(node, depth) {
|
|
14
|
+
if (invalid) return;
|
|
15
|
+
if (!node || typeof node !== "object" || depth > MAX_SESSIONS) { invalid = "Layout tree is malformed"; return; }
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(node, "id")) {
|
|
17
|
+
if (Object.keys(node).length !== 1 || !validOrigin(node.id) || origins.indexOf(node.id) !== -1) {
|
|
18
|
+
invalid = "Layout leaves must have unique valid session origins"; return;
|
|
19
|
+
}
|
|
20
|
+
origins.push(node.id);
|
|
21
|
+
if (origins.length > MAX_SESSIONS) invalid = "A Split view can contain at most eight sessions";
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (Object.keys(node).sort().join(",") !== "axis,left,ratio,right" ||
|
|
25
|
+
["horizontal", "vertical"].indexOf(node.axis) === -1 || typeof node.ratio !== "number" ||
|
|
26
|
+
!Number.isFinite(node.ratio) || node.ratio < MIN_RATIO || node.ratio > MAX_RATIO) {
|
|
27
|
+
invalid = "Layout branch must have a valid axis and bounded ratio"; return;
|
|
28
|
+
}
|
|
29
|
+
walk(node.left, depth + 1); walk(node.right, depth + 1);
|
|
30
|
+
}
|
|
31
|
+
walk(tree, 0);
|
|
32
|
+
if (invalid) return { ok: false, error: invalid, origins: origins };
|
|
33
|
+
if (origins.length < 2 || origins.length > MAX_SESSIONS) return { ok: false, error: "A Split view requires two to eight sessions", origins: origins };
|
|
34
|
+
if (expectedOrigins) {
|
|
35
|
+
if (!Array.isArray(expectedOrigins) || expectedOrigins.length !== origins.length ||
|
|
36
|
+
expectedOrigins.some(function (id) { return !validOrigin(id) || origins.indexOf(id) === -1; })) {
|
|
37
|
+
return { ok: false, error: "Layout leaves must exactly match the selected sessions", origins: origins };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, origins: origins };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateName(name) {
|
|
44
|
+
if (typeof name !== "string") return { ok: false, error: "Split view name is required" };
|
|
45
|
+
var normalized = name.trim();
|
|
46
|
+
if (!normalized || normalized.length > 80) return { ok: false, error: "Split view name must be between 1 and 80 characters" };
|
|
47
|
+
return { ok: true, name: normalized };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validatePreset(name) {
|
|
51
|
+
return PRESETS.indexOf(name) !== -1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function removeLeaf(tree, originId) {
|
|
55
|
+
if (!tree || typeof tree !== "object") return tree;
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(tree, "id")) return tree.id === originId ? null : tree;
|
|
57
|
+
var left = removeLeaf(tree.left, originId), right = removeLeaf(tree.right, originId);
|
|
58
|
+
if (!left) return right;
|
|
59
|
+
if (!right) return left;
|
|
60
|
+
if (left === tree.left && right === tree.right) return tree;
|
|
61
|
+
return { axis: tree.axis, ratio: tree.ratio, left: left, right: right };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function leaf(id) { return { id: id }; }
|
|
65
|
+
function chain(ids, axis) {
|
|
66
|
+
if (ids.length === 1) return leaf(ids[0]);
|
|
67
|
+
return { axis: axis, ratio: 1 / ids.length, left: leaf(ids[0]), right: chain(ids.slice(1), axis) };
|
|
68
|
+
}
|
|
69
|
+
function grid(ids) {
|
|
70
|
+
if (ids.length <= 2) return chain(ids, "horizontal");
|
|
71
|
+
var split = Math.ceil(ids.length / 2);
|
|
72
|
+
return { axis: "vertical", ratio: split / ids.length,
|
|
73
|
+
left: chain(ids.slice(0, split), "horizontal"), right: chain(ids.slice(split), "horizontal") };
|
|
74
|
+
}
|
|
75
|
+
function createPreset(origins, name) {
|
|
76
|
+
if (!Array.isArray(origins) || origins.length > MAX_SESSIONS ||
|
|
77
|
+
origins.some(function (id) { return !validOrigin(id); }) || new Set(origins).size !== origins.length) {
|
|
78
|
+
throw new TypeError("Preset input must contain zero to eight unique session origins");
|
|
79
|
+
}
|
|
80
|
+
if (!validatePreset(name)) throw new TypeError("Unknown Split view preset");
|
|
81
|
+
if (!origins.length) return null;
|
|
82
|
+
if (origins.length === 1) return leaf(origins[0]);
|
|
83
|
+
if (name === "columns") return chain(origins, "horizontal");
|
|
84
|
+
if (name === "rows") return chain(origins, "vertical");
|
|
85
|
+
if (name === "grid") return grid(origins);
|
|
86
|
+
if (name === "main-left" || name === "main-right") {
|
|
87
|
+
var main = leaf(origins[0]), rest = grid(origins.slice(1)), mainShare = origins.length === 2 ? 0.5 : 2 / 3;
|
|
88
|
+
return name === "main-left" ? { axis: "horizontal", ratio: mainShare, left: main, right: rest } : { axis: "horizontal", ratio: 1 - mainShare, left: rest, right: main };
|
|
89
|
+
}
|
|
90
|
+
if (origins.length <= 3) {
|
|
91
|
+
return { axis: "horizontal", ratio: 0.55, left: leaf(origins[0]), right: chain(origins.slice(1), "vertical") };
|
|
92
|
+
}
|
|
93
|
+
return grid(origins);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateViewShape(view) {
|
|
97
|
+
if (!view || typeof view !== "object" || typeof view.id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(view.id)) return { ok: false, error: "Split view identity is invalid" };
|
|
98
|
+
if (view.folderId != null && (typeof view.folderId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(view.folderId))) return { ok: false, error: 'Invalid Split view folder' };
|
|
99
|
+
if (view.favorite !== undefined && typeof view.favorite !== 'boolean') return { ok: false, error: 'Invalid Split view favorite' };
|
|
100
|
+
var name = validateName(view.name);
|
|
101
|
+
if (!name.ok) return name;
|
|
102
|
+
if (view.nameMode != null && ["automatic", "custom"].indexOf(view.nameMode) === -1) return { ok: false, error: "Split view name mode is invalid" };
|
|
103
|
+
if (!Number.isInteger(view.revision) || view.revision < 1 || !validatePreset(view.layout)) return { ok: false, error: "Split view revision or layout is invalid" };
|
|
104
|
+
var refs = view.sessionRefs;
|
|
105
|
+
if (!Array.isArray(refs) || refs.length < 2 || refs.length > MAX_SESSIONS) return { ok: false, error: "A Split view requires two to eight sessions" };
|
|
106
|
+
var ids = [];
|
|
107
|
+
for (var i = 0; i < refs.length; i++) {
|
|
108
|
+
var ref = refs[i];
|
|
109
|
+
if (!ref || !validOrigin(ref.originId) || ids.indexOf(ref.originId) !== -1 ||
|
|
110
|
+
!(ref.ownerId === null || typeof ref.ownerId === "string") ||
|
|
111
|
+
!(ref.linuxUser === null || typeof ref.linuxUser === "string")) return { ok: false, error: "Split view session references are invalid" };
|
|
112
|
+
ids.push(ref.originId);
|
|
113
|
+
}
|
|
114
|
+
var tree = validateTree(view.tree, ids);
|
|
115
|
+
if (!tree.ok) return tree;
|
|
116
|
+
if (typeof view.activeSessionOriginId !== "string" || ids.indexOf(view.activeSessionOriginId) === -1) return { ok: false, error: "Active session must belong to the Split view" };
|
|
117
|
+
return { ok: true, name: name.name, origins: ids };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
MAX_SESSIONS: MAX_SESSIONS,
|
|
122
|
+
MIN_RATIO: MIN_RATIO,
|
|
123
|
+
MAX_RATIO: MAX_RATIO,
|
|
124
|
+
PRESETS: PRESETS.slice(),
|
|
125
|
+
validOrigin: validOrigin,
|
|
126
|
+
validateTree: validateTree,
|
|
127
|
+
validateName: validateName,
|
|
128
|
+
validatePreset: validatePreset,
|
|
129
|
+
removeLeaf: removeLeaf,
|
|
130
|
+
createPreset: createPreset,
|
|
131
|
+
validateViewShape: validateViewShape,
|
|
132
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Account UI preferences are excluded from canonical Clay controls.
|
|
2
|
+
function validMode(mode) { return mode === 'overlay' || mode === 'docked'; }
|
|
3
|
+
function attachWorkbenchLayoutPreferences(deps) {
|
|
4
|
+
var config = deps.configModule || require('./config');
|
|
5
|
+
function context(accountId) {
|
|
6
|
+
var data = deps.loadUsers();
|
|
7
|
+
var user = data.users.find(function (entry) { return entry.id === accountId; });
|
|
8
|
+
if (deps.isMultiUser() ? !user : accountId !== 'default') throw new Error('Account unavailable.');
|
|
9
|
+
var settings = deps.isMultiUser() ? null : config.loadConfig() || {};
|
|
10
|
+
return { data: data, user: user, settings: settings, saved: settings ? settings.workbenchLayoutPreference : user.workbenchLayoutPreference };
|
|
11
|
+
}
|
|
12
|
+
function value(accountId, saved) {
|
|
13
|
+
if (saved === undefined) return { accountId: accountId, mode: 'overlay', revision: 0 };
|
|
14
|
+
if (!saved || !validMode(saved.mode) || !Number.isSafeInteger(saved.revision) || saved.revision < 1) throw new Error('Invalid stored preference.');
|
|
15
|
+
return { accountId: accountId, mode: saved.mode, revision: saved.revision };
|
|
16
|
+
}
|
|
17
|
+
function get(accountId) { return value(accountId, context(accountId).saved); }
|
|
18
|
+
function set(accountId, mode, revision) {
|
|
19
|
+
if (!validMode(mode) || !Number.isSafeInteger(revision) || revision < 0) return { status: 400, error: 'Invalid workbench layout.' };
|
|
20
|
+
var current = context(accountId), prior = value(accountId, current.saved);
|
|
21
|
+
if (prior.revision !== revision) return { status: 409, error: 'Workbench layout changed elsewhere. Reload the preference and try again.' };
|
|
22
|
+
if (prior.mode === mode) return prior;
|
|
23
|
+
if (revision === Number.MAX_SAFE_INTEGER) throw new Error('Preference revision exhausted.');
|
|
24
|
+
var saved = { mode: mode, revision: revision + 1 };
|
|
25
|
+
if (current.settings) config.saveConfig(Object.assign({}, current.settings, { workbenchLayoutPreference: saved }));
|
|
26
|
+
else deps.saveUsers(Object.assign({}, current.data, { users: current.data.users.map(function (entry) {
|
|
27
|
+
return entry === current.user ? Object.assign({}, entry, { workbenchLayoutPreference: saved }) : entry;
|
|
28
|
+
}) }));
|
|
29
|
+
return value(accountId, saved);
|
|
30
|
+
}
|
|
31
|
+
return { get: get, set: set };
|
|
32
|
+
}
|
|
33
|
+
module.exports = { attachWorkbenchLayoutPreferences: attachWorkbenchLayoutPreferences };
|
package/lib/users.js
CHANGED
|
@@ -333,6 +333,7 @@ var createUser = creation.createUser;
|
|
|
333
333
|
var createUserWithoutPin = creation.createUserWithoutPin;
|
|
334
334
|
var permissions = attachPermissions({ loadUsers: loadUsers, saveUsers: saveUsers, findUserById: findUserById });
|
|
335
335
|
var contextualClayPreferences = require("./users-contextual-clay-preferences").attachContextualClayPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
336
|
+
var workbenchLayoutPreferences = require('./users-workbench-layout-preferences').attachWorkbenchLayoutPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
|
|
336
337
|
var preferences = attachPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
337
338
|
var projectMateFilterPreferences = attachProjectMateFilterPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
338
339
|
var homeDockPreferences = attachHomeDockPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
|
|
@@ -479,6 +480,8 @@ module.exports = {
|
|
|
479
480
|
setToolPalette: setToolPalette,
|
|
480
481
|
getTerminalFont: getTerminalFont,
|
|
481
482
|
setTerminalFont: setTerminalFont,
|
|
483
|
+
getWorkbenchLayoutPreference: workbenchLayoutPreferences.get,
|
|
484
|
+
setWorkbenchLayoutPreference: workbenchLayoutPreferences.set,
|
|
482
485
|
getContextualClayGeometry: contextualClayPreferences.getContextualClayGeometry,
|
|
483
486
|
setContextualClayGeometry: contextualClayPreferences.setContextualClayGeometry,
|
|
484
487
|
getHomeDockPreference: getHomeDockPreference,
|
|
@@ -100,11 +100,7 @@ var projection = createProjection({
|
|
|
100
100
|
sessionRef: sessionRef,
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
if (session.isProcessing) return "processing";
|
|
105
|
-
if (session.pendingPermissions && Object.keys(session.pendingPermissions).length > 0) return "waiting_for_approval";
|
|
106
|
-
return "idle";
|
|
107
|
-
}
|
|
103
|
+
var sessionActivity = require('./session-activity-state').sessionActivity;
|
|
108
104
|
|
|
109
105
|
function attachWorkspaceQueryService(ctx) {
|
|
110
106
|
var getProjects = ctx.getProjects;
|
package/lib/ws-schema.js
CHANGED
|
@@ -59,6 +59,17 @@ var schema = {
|
|
|
59
59
|
"split_group_rename": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Rename a split group" },
|
|
60
60
|
"split_group_dissolve": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Separate a split group without deleting its sessions" },
|
|
61
61
|
"split_group_set_pair": { direction: "c2s", handler: "lib/session-split-groups.js", description: "Assign Driver/Worker roles on a split group (driverId null clears them)" },
|
|
62
|
+
"project_split_views_request": { direction: "c2s", handler: "lib/project-split-views.js", description: "Request the current owner's persisted presentation Split views for this project" },
|
|
63
|
+
"project_split_view_create": { direction: "c2s", handler: "lib/project-split-views.js", description: "Create a revision-checked presentation Split view from exact project session origins" },
|
|
64
|
+
"project_split_view_update": { direction: "c2s", handler: "lib/project-split-views.js", description: "Update exact Split view membership, layout tree and active session at the current owner revision" },
|
|
65
|
+
"project_split_view_rename": { direction: "c2s", handler: "lib/project-split-views.js", description: "Rename an exact presentation Split view at the current owner revision" },
|
|
66
|
+
"project_split_view_separate": { direction: "c2s", handler: "lib/project-split-views.js", description: "Remove a presentation Split view without changing its sessions or Driver/Worker pairing" },
|
|
67
|
+
"project_split_view_organize": { direction: "c2s", handler: "lib/project-split-views.js", description: "Organize an exact saved Split view in a live personal folder and Favorites at current revisions" },
|
|
68
|
+
"project_split_view_move": { direction: "c2s", handler: "lib/project-split-views.js", description: "Atomically move one exact session between presentation Split views or into a standalone target" },
|
|
69
|
+
"split_worker_conversation_request": { direction: "c2s", handler: "lib/project-worker-conversation.js", description: "Read a bounded transcript page for one exact owned Split Worker generation without switching sessions" },
|
|
70
|
+
"split_worker_conversation": { direction: "s2c", handler: "lib/public/modules/split-view-workers.js", description: "Read-only transcript page correlated to its exact Driver, Worker, and generation" },
|
|
71
|
+
"split_worker_remove": { direction: "c2s", handler: "lib/project-worker-removal.js", description: "Remove one exact owned Worker session, with a single-use human Stop confirmation when active" },
|
|
72
|
+
"split_worker_remove_result": { direction: "s2c", handler: "lib/public/modules/split-view-worker-removal.js", description: "Correlated observed removal or confirmation/failure for an exact Worker generation" },
|
|
62
73
|
"split_worker_close": { direction: "c2s", handler: "lib/project-pair-worker-close.js", description: "Close one exact authenticated Worker generation while preserving its peer and session history" },
|
|
63
74
|
"split_worker_close_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Correlated result of an exact targeted Worker close" },
|
|
64
75
|
"pair_session_options": { direction: "c2s", handler: "lib/project-session-pair.js", description: "Request vendors/models for the pair-session dialog (response reuses the same type)" },
|
|
@@ -121,6 +132,8 @@ var schema = {
|
|
|
121
132
|
"tui_transcript_state": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Assistant text index for a Claude TUI session (full replace; sent on request and after each new assistant message)" },
|
|
122
133
|
"split_groups": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Full persistent split-group list for the current user" },
|
|
123
134
|
"split_group_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of a split-group mutation" },
|
|
135
|
+
"project_split_views": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Owner-scoped persisted presentation Split views for the current project" },
|
|
136
|
+
"project_split_view_result": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Correlated presentation Split view mutation result" },
|
|
124
137
|
"pair_session_created": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Result of pair_session_create; carries the new group" },
|
|
125
138
|
"worker_proposal": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Recorded Split Worker creation/replacement runtime card with Driver rationale" },
|
|
126
139
|
"worker_proposal_update": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Worker card lifecycle and manual/auto decision audit update" },
|
|
@@ -820,6 +833,8 @@ var schema = {
|
|
|
820
833
|
"default_vendor_get": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Read the authenticated user's New session vendor preference" },
|
|
821
834
|
"default_vendor_set": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Validate and persist the authenticated user's New session vendor preference" },
|
|
822
835
|
"default_vendor_state": { direction: "s2c", handler: "lib/public/modules/default-vendor.js", description: "Correlated or same-user-broadcast New session vendor preference" },
|
|
836
|
+
"clay_view_probe": { direction: "s2c", handler: "lib/public/modules/clay-split-view-screen.js", description: "Observe the exact owner window across stable frames" },
|
|
837
|
+
"clay_view_observed": { direction: "c2s", handler: "lib/server-owner-ui.js", description: "Bounded correlated view observation, not approval" },
|
|
823
838
|
"clay_ui_ready": { direction: "c2s", handler: "lib/server-owner-ui.js", description: "Advertise exact owner UI navigation support on this socket" },
|
|
824
839
|
"clay_navigation_offer": { direction: "s2c", handler: "lib/public/modules/clay-owner-navigation.js", description: "Offer one exact reviewed navigation to a selected owner connection" },
|
|
825
840
|
"clay_navigation_claim": { direction: "c2s", handler: "lib/server-owner-ui.js", description: "Claim an offered navigation with live source/target revalidation" },
|
|
@@ -1480,6 +1480,7 @@ function createCodexAdapter(opts) {
|
|
|
1480
1480
|
var _recoveryIdleShutdown = null;
|
|
1481
1481
|
var _activeQueries = [];
|
|
1482
1482
|
var recoveryAdmission = require("../codex-recovery-admission").attachRecoveryAdmission({
|
|
1483
|
+
timeoutMs: opts.recoveryAdmissionTimeoutMs,
|
|
1483
1484
|
state: function() { return { epoch: _shutdownEpoch, shuttingDown: _shuttingDown, retiring: _recoveryIdleShutdown === _shutdownEpoch,
|
|
1484
1485
|
initializing: _initializing, refCount: _refCount, server: _appServer, entries: _activeQueries.slice() }; },
|
|
1485
1486
|
shutdownError: createShutdownError,
|
|
@@ -237,7 +237,9 @@ CodexAppServer.prototype._handleMessage = function(msg) {
|
|
|
237
237
|
delete this.pendingRequests[msg.id];
|
|
238
238
|
if (pending.timer) clearTimeout(pending.timer);
|
|
239
239
|
if (msg.error) {
|
|
240
|
-
|
|
240
|
+
var rpcError = new Error(msg.error.message || JSON.stringify(msg.error));
|
|
241
|
+
rpcError.code = msg.error.code;
|
|
242
|
+
pending.reject(rpcError);
|
|
241
243
|
} else {
|
|
242
244
|
pending.resolve(msg.result);
|
|
243
245
|
}
|
|
@@ -21,24 +21,50 @@ function terminalsAreIdle(result) {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function observe(checkCurrent, reason, method, rpcCode, threadId) {
|
|
25
|
+
if (typeof checkCurrent.observe === 'function') checkCurrent.observe(reason, method, rpcCode, threadId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function probe(server, method, params, timeout, checkCurrent) {
|
|
29
|
+
observe(checkCurrent, 'probe_started', method, null, params.threadId);
|
|
30
|
+
try {
|
|
31
|
+
var result = await server.send(method, params, timeout);
|
|
32
|
+
observe(checkCurrent, 'probe_returned', method, null, params.threadId);
|
|
33
|
+
return result;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
observe(checkCurrent, 'probe_failed', method, error && Number.isInteger(error.code) ? error.code : null, params.threadId);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function probeFailure(error, method) {
|
|
41
|
+
if (error && (error.code === -32601 || error.code === -32602)) {
|
|
42
|
+
var unsupported = new Error('Codex cannot verify recovery safety: ' + method + ' is unavailable or incompatible. No message was sent to the runtime.');
|
|
43
|
+
unsupported.code = 'CODEX_RECOVERY_PROBE_UNAVAILABLE';
|
|
44
|
+
throw unsupported;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
async function retireIdleQueries(entries, server, checkCurrent) {
|
|
25
50
|
checkCurrent();
|
|
26
51
|
var candidates = [];
|
|
27
52
|
for (var i = 0; i < entries.length; i++) {
|
|
28
53
|
var handle = entries[i].handle;
|
|
29
54
|
var snapshot = handle && handle.getIdleRecoverySnapshot && handle.getIdleRecoverySnapshot();
|
|
30
|
-
if (!snapshot) return false;
|
|
55
|
+
if (!snapshot) { observe(checkCurrent, 'idle_input_snapshot_unavailable', null, null, handle && handle.getRecoveryThreadId && handle.getRecoveryThreadId()); return false; }
|
|
31
56
|
candidates.push({ handle: handle, snapshot: snapshot });
|
|
32
57
|
}
|
|
33
58
|
var safe = await Promise.all(candidates.map(function(candidate) {
|
|
34
|
-
return server
|
|
59
|
+
return probe(server, "thread/backgroundTerminals/list", {
|
|
35
60
|
threadId: candidate.snapshot.threadId,
|
|
36
|
-
}, 5000).then(terminalsAreIdle, function() { return
|
|
61
|
+
}, 5000, checkCurrent).then(terminalsAreIdle, function(error) { return probeFailure(error, 'thread/backgroundTerminals/list'); });
|
|
37
62
|
}));
|
|
38
63
|
checkCurrent();
|
|
39
64
|
for (var ci = 0; ci < candidates.length; ci++) {
|
|
40
65
|
var current = candidates[ci].handle.getIdleRecoverySnapshot();
|
|
41
|
-
if (!safe[ci]
|
|
66
|
+
if (!safe[ci]) { observe(checkCurrent, 'background_state_not_proven_idle', null, null, candidates[ci].snapshot.threadId); return false; }
|
|
67
|
+
if (!current || current.revision !== candidates[ci].snapshot.revision) { observe(checkCurrent, 'input_changed_during_probe', null, null, candidates[ci].snapshot.threadId); return false; }
|
|
42
68
|
}
|
|
43
69
|
// No awaits between validation and closing every input. A new send either
|
|
44
70
|
// already invalidated its snapshot or sees a closed input and resumes anew.
|
|
@@ -58,33 +84,64 @@ async function loadedThreads(server, checkCurrent) {
|
|
|
58
84
|
var deadline = Date.now() + 5000;
|
|
59
85
|
for (var page = 0; page < 20; page++) {
|
|
60
86
|
var remaining = deadline - Date.now();
|
|
61
|
-
if (remaining <= 0) return null;
|
|
87
|
+
if (remaining <= 0) { observe(checkCurrent, 'loaded_list_deadline'); return null; }
|
|
62
88
|
var result;
|
|
63
89
|
try {
|
|
64
|
-
result = await server
|
|
90
|
+
result = await probe(server, 'thread/loaded/list', { cursor: cursor, limit: 100 }, remaining, checkCurrent);
|
|
65
91
|
} catch (error) {
|
|
66
92
|
checkCurrent();
|
|
67
93
|
if (!server.started) throw error;
|
|
94
|
+
probeFailure(error, 'thread/loaded/list');
|
|
68
95
|
return null;
|
|
69
96
|
}
|
|
70
97
|
checkCurrent();
|
|
71
|
-
if (!result || !Array.isArray(result.data) || !result.data.every(function(id) { return typeof id === 'string'; })) return null;
|
|
98
|
+
if (!result || !Array.isArray(result.data) || !result.data.every(function(id) { return typeof id === 'string'; })) { observe(checkCurrent, 'loaded_list_invalid'); return null; }
|
|
72
99
|
threads = threads.concat(result.data);
|
|
73
100
|
if (!result.nextCursor) return threads;
|
|
74
|
-
if (result.nextCursor === cursor) return null;
|
|
101
|
+
if (result.nextCursor === cursor) { observe(checkCurrent, 'loaded_list_cursor_repeated'); return null; }
|
|
75
102
|
cursor = result.nextCursor;
|
|
76
103
|
}
|
|
104
|
+
observe(checkCurrent, 'loaded_list_page_limit');
|
|
77
105
|
return null;
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
async function threadIsAbsent(server, threadId, checkCurrent) {
|
|
81
109
|
var threads = await loadedThreads(server, checkCurrent);
|
|
110
|
+
if (threads && threads.indexOf(threadId) !== -1) observe(checkCurrent, 'target_still_loaded', null, null, threadId);
|
|
82
111
|
return !!threads && threads.indexOf(threadId) === -1;
|
|
83
112
|
}
|
|
84
113
|
|
|
85
114
|
async function runtimeCanStop(server, retiredThreads, checkCurrent) {
|
|
86
115
|
var threads = await loadedThreads(server, checkCurrent);
|
|
87
|
-
|
|
116
|
+
if (!threads) return false;
|
|
117
|
+
// The caller has no live handles and holds admission. Closed handles can
|
|
118
|
+
// leave loaded threads that unsubscribe cannot release; prove every such
|
|
119
|
+
// thread idle before stopping their shared process, including siblings.
|
|
120
|
+
for (var threadId of threads) {
|
|
121
|
+
if (retiredThreads.indexOf(threadId) === -1 && !await threadIsIdle(server, threadId, checkCurrent)) return false;
|
|
122
|
+
}
|
|
123
|
+
checkCurrent();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function threadIsIdle(server, threadId, checkCurrent) {
|
|
128
|
+
var method = 'thread/read';
|
|
129
|
+
try {
|
|
130
|
+
var status = await probe(server, method, { threadId: threadId, includeTurns: false }, 5000, checkCurrent);
|
|
131
|
+
checkCurrent();
|
|
132
|
+
if (!status || !status.thread || status.thread.id !== threadId
|
|
133
|
+
|| !status.thread.status || status.thread.status.type !== 'idle') {
|
|
134
|
+
observe(checkCurrent, status && status.thread && status.thread.id === threadId && status.thread.status && status.thread.status.type === 'active'
|
|
135
|
+
? 'provider_reports_active' : 'provider_idle_not_proven', method, null, threadId);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
method = 'thread/backgroundTerminals/list';
|
|
139
|
+
var terminals = await probe(server, method, { threadId: threadId }, 5000, checkCurrent);
|
|
140
|
+
checkCurrent();
|
|
141
|
+
var idle = terminalsAreIdle(terminals);
|
|
142
|
+
if (!idle) observe(checkCurrent, 'background_state_not_proven_idle', method, null, threadId);
|
|
143
|
+
return idle;
|
|
144
|
+
} catch (error) { checkCurrent(); if (!server.started) throw error; return probeFailure(error, method); }
|
|
88
145
|
}
|
|
89
146
|
|
|
90
147
|
async function releaseThread(entries, server, threadId, checkCurrent) {
|
|
@@ -99,18 +156,14 @@ async function releaseThread(entries, server, threadId, checkCurrent) {
|
|
|
99
156
|
} else {
|
|
100
157
|
// A closed SDK handle can leave a loaded provider thread behind. Only an
|
|
101
158
|
// explicitly idle provider thread with verified terminal state may unload.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
checkCurrent();
|
|
109
|
-
if (!terminalsAreIdle(terminals)) return false;
|
|
110
|
-
} catch (error) { checkCurrent(); if (!server.started) throw error; return false; }
|
|
159
|
+
if (!await threadIsIdle(server, threadId, checkCurrent)) return false;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
var result = await probe(server, 'thread/unsubscribe', { threadId: threadId }, 5000, checkCurrent);
|
|
163
|
+
var status = result && result.status;
|
|
164
|
+
observe(checkCurrent, ['unsubscribed', 'notSubscribed', 'notLoaded'].indexOf(status) !== -1 ? 'unsubscribe_' + status : 'unsubscribe_status_unknown', 'thread/unsubscribe', null, threadId);
|
|
111
165
|
}
|
|
112
|
-
|
|
113
|
-
catch (error) { checkCurrent(); if (!server.started) throw error; return false; }
|
|
166
|
+
catch (error) { checkCurrent(); if (!server.started) throw error; return probeFailure(error, 'thread/unsubscribe'); }
|
|
114
167
|
return threadIsAbsent(server, threadId, checkCurrent);
|
|
115
168
|
}
|
|
116
169
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var idleRecovery = require('./codex-idle-recovery');
|
|
2
|
+
var crypto = require('crypto');
|
|
2
3
|
|
|
3
4
|
// Serialize decisions, not whole model turns. Waiting for a target must release
|
|
4
5
|
// the gate so an unrelated Driver can still answer its Worker's permissions.
|
|
@@ -57,6 +58,8 @@ function attachRecoveryAdmission(ctx) {
|
|
|
57
58
|
handoff = tail;
|
|
58
59
|
tail.then(function() { if (handoff === tail) handoff = null; });
|
|
59
60
|
try {
|
|
61
|
+
checkCurrent.phase('admission_gate');
|
|
62
|
+
if (prior) checkCurrent.observe('admission_gate_pending');
|
|
60
63
|
await idleRecovery.wait(prior || Promise.resolve(), checkCurrent);
|
|
61
64
|
await idleRecovery.wait(cleanup, checkCurrent);
|
|
62
65
|
checkSharedExit();
|
|
@@ -69,8 +72,10 @@ function attachRecoveryAdmission(ctx) {
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
async function releaseTarget(threadId, checkCurrent) {
|
|
75
|
+
checkCurrent.phase('isolated_target');
|
|
72
76
|
var prior = targets.get(threadId);
|
|
73
77
|
if (!prior) return true;
|
|
78
|
+
checkCurrent.observe('isolated_target_reserved');
|
|
74
79
|
if (cleanupFailures.has(prior)) throw cleanupFailures.get(prior);
|
|
75
80
|
await prior.retireIdleForRecovery(checkCurrent);
|
|
76
81
|
await cleanup;
|
|
@@ -80,8 +85,19 @@ function attachRecoveryAdmission(ctx) {
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
async function prepareShared(threadId, checkCurrent) {
|
|
88
|
+
checkCurrent.phase('shared_target');
|
|
83
89
|
var state = ctx.state();
|
|
84
|
-
if (state.initializing) return false;
|
|
90
|
+
if (state.initializing) { checkCurrent.observe('shared_runtime_initializing'); return false; }
|
|
91
|
+
// Independent conversations do not participate in target handoff. Probing
|
|
92
|
+
// every idle sibling first can consume the whole admission budget even
|
|
93
|
+
// when this target can be unloaded immediately.
|
|
94
|
+
if (state.server && state.entries.some(function(entry) {
|
|
95
|
+
return !entry.handle || entry.handle.getRecoveryThreadId() !== threadId;
|
|
96
|
+
})) {
|
|
97
|
+
if (await idleRecovery.releaseThread(state.entries, state.server, threadId, checkCurrent)) return true;
|
|
98
|
+
checkCurrent();
|
|
99
|
+
}
|
|
100
|
+
checkCurrent.phase('shared_idle_retirement');
|
|
85
101
|
var retiredThreads = [];
|
|
86
102
|
if (state.refCount > 0) {
|
|
87
103
|
var snapshots = state.entries.map(function(entry) { return entry.handle && entry.handle.getIdleRecoverySnapshot(); });
|
|
@@ -91,17 +107,52 @@ function attachRecoveryAdmission(ctx) {
|
|
|
91
107
|
}
|
|
92
108
|
state = ctx.state();
|
|
93
109
|
if (!state.server) return true;
|
|
110
|
+
checkCurrent.phase('shared_runtime_release');
|
|
94
111
|
if (!state.refCount && await idleRecovery.runtimeCanStop(state.server, retiredThreads, checkCurrent)) {
|
|
95
112
|
var proc = state.server.proc;
|
|
96
113
|
var stopped = await ctx.stopIdle();
|
|
97
114
|
if (stopped) { unconfirmedSharedProcess = proc || null; checkSharedExit(); }
|
|
98
115
|
return stopped;
|
|
99
116
|
}
|
|
117
|
+
checkCurrent.phase('shared_target');
|
|
100
118
|
return idleRecovery.releaseThread(state.entries, state.server, threadId, checkCurrent);
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
async function waitUntilAvailable(queryOpts, action) {
|
|
104
|
-
var
|
|
122
|
+
var checkLifecycle = checker(queryOpts);
|
|
123
|
+
var startedAt = Date.now();
|
|
124
|
+
var deadline = startedAt + (ctx.timeoutMs || 15000);
|
|
125
|
+
var admitted = false;
|
|
126
|
+
var observations = new Set();
|
|
127
|
+
var lastEvidence = null;
|
|
128
|
+
var ended = false;
|
|
129
|
+
var phase = 'admission_gate';
|
|
130
|
+
function targetKey(threadId) {
|
|
131
|
+
return crypto.createHash('sha256').update(String(threadId || queryOpts.resumeSessionId || '')).digest('hex').slice(0, 16);
|
|
132
|
+
}
|
|
133
|
+
checkCurrent.phase = function(value) { phase = value; };
|
|
134
|
+
function checkCurrent() {
|
|
135
|
+
checkLifecycle();
|
|
136
|
+
if (!admitted && Date.now() >= deadline) {
|
|
137
|
+
var error = new Error('Tool recovery could not verify that this conversation is safe to resume. No message was sent to the runtime. Review your retained messages and send again when the runtime is available.');
|
|
138
|
+
error.code = 'CODEX_RECOVERY_UNAVAILABLE';
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Fixed evidence labels only: never log thread contents, RPC payloads,
|
|
143
|
+
// commands, credentials, or provider error strings. Bound polling noise.
|
|
144
|
+
checkCurrent.observe = function(reason, method, rpcCode, threadId) {
|
|
145
|
+
if (ended) return;
|
|
146
|
+
var evidence = { reason: reason, phase: phase, targetKey: targetKey(threadId), method: method || null, rpcCode: Number.isInteger(rpcCode) ? rpcCode : null };
|
|
147
|
+
lastEvidence = evidence;
|
|
148
|
+
var key = JSON.stringify(evidence);
|
|
149
|
+
if (observations.has(key) || observations.size >= 24) return;
|
|
150
|
+
observations.add(key);
|
|
151
|
+
if (typeof queryOpts.onRecoveryDiagnostic === 'function') {
|
|
152
|
+
try { queryOpts.onRecoveryDiagnostic(Object.assign({ elapsedMs: Date.now() - startedAt }, evidence)); } catch (error) {}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
checkCurrent.admit = function() { checkCurrent(); admitted = true; };
|
|
105
156
|
var waiting = false;
|
|
106
157
|
if (typeof queryOpts.onRecoveryAdmission === 'function') queryOpts.onRecoveryAdmission(true);
|
|
107
158
|
try {
|
|
@@ -119,6 +170,10 @@ function attachRecoveryAdmission(ctx) {
|
|
|
119
170
|
await idleRecovery.wait(new Promise(function(resolve) { setTimeout(resolve, 50); }), checkCurrent);
|
|
120
171
|
}
|
|
121
172
|
} finally {
|
|
173
|
+
ended = true;
|
|
174
|
+
if (typeof queryOpts.onRecoveryDiagnostic === 'function') {
|
|
175
|
+
try { queryOpts.onRecoveryDiagnostic({ reason: admitted ? 'admission_granted' : 'admission_ended_without_grant', phase: phase, targetKey: targetKey(), elapsedMs: Date.now() - startedAt, lastEvidence: lastEvidence }); } catch (error) {}
|
|
176
|
+
}
|
|
122
177
|
if (typeof queryOpts.onRecoveryAdmission === 'function') queryOpts.onRecoveryAdmission(false);
|
|
123
178
|
}
|
|
124
179
|
}
|
|
@@ -127,7 +182,9 @@ function attachRecoveryAdmission(ctx) {
|
|
|
127
182
|
return waitUntilAvailable(queryOpts, async function(checkCurrent) {
|
|
128
183
|
if (!await releaseTarget(queryOpts.resumeSessionId, checkCurrent)) return false;
|
|
129
184
|
if (!await prepareShared(queryOpts.resumeSessionId, checkCurrent)) return false;
|
|
130
|
-
|
|
185
|
+
// Once ownership is reserved, normal startup/cleanup owns cancellation.
|
|
186
|
+
// A deadline must not discard a handle being created after admission.
|
|
187
|
+
checkCurrent.admit();
|
|
131
188
|
var runtime = ctx.createRuntime(queryOpts);
|
|
132
189
|
runtimes.push(runtime);
|
|
133
190
|
targets.set(queryOpts.resumeSessionId, runtime);
|
|
@@ -149,7 +206,11 @@ function attachRecoveryAdmission(ctx) {
|
|
|
149
206
|
checkSharedExit: checkSharedExit,
|
|
150
207
|
hasTarget: function(threadId) { return targets.has(threadId); },
|
|
151
208
|
waitForTarget: function(queryOpts) {
|
|
152
|
-
return waitUntilAvailable(queryOpts, function(checkCurrent) {
|
|
209
|
+
return waitUntilAvailable(queryOpts, async function(checkCurrent) {
|
|
210
|
+
var released = await releaseTarget(queryOpts.resumeSessionId, checkCurrent);
|
|
211
|
+
if (released) checkCurrent.admit();
|
|
212
|
+
return released;
|
|
213
|
+
});
|
|
153
214
|
},
|
|
154
215
|
shutdown: function() {
|
|
155
216
|
return Promise.all(runtimes.slice().map(cleanupRuntime)).then(function(results) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0-beta.1",
|
|
4
4
|
"description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clay-server": "./bin/cli.js"
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"dev": "node bin/cli.js --dev",
|
|
10
10
|
"test": "node --test --test-force-exit --test-skip-pattern=\"^real sidebar (normal|long-turn):\" test/*.test.js",
|
|
11
11
|
"semantic-release": "semantic-release",
|
|
12
|
-
"test:pane-performance": "node --test --test-force-exit test/pane-reuse-browser.test.js"
|
|
12
|
+
"test:pane-performance": "node --test --test-force-exit test/pane-reuse-browser.test.js",
|
|
13
|
+
"build:pane-assets": "node scripts/build-pane-assets.js",
|
|
14
|
+
"prepack": "npm run build:pane-assets"
|
|
13
15
|
},
|
|
14
16
|
"files": [
|
|
15
17
|
"bin/",
|
|
@@ -86,6 +88,7 @@
|
|
|
86
88
|
"devDependencies": {
|
|
87
89
|
"@semantic-release/changelog": "^6.0.3",
|
|
88
90
|
"@semantic-release/git": "^10.0.1",
|
|
89
|
-
"semantic-release": "^25.0.3"
|
|
91
|
+
"semantic-release": "^25.0.3",
|
|
92
|
+
"esbuild": "0.28.2"
|
|
90
93
|
}
|
|
91
94
|
}
|