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
package/lib/public/style.css
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
@import url("css/title-bar.css?v=20261018-workbench-controls2");
|
|
10
10
|
@import url("css/clay-workspace.css?v=20261010-tint");
|
|
11
11
|
@import url("css/pane.css");
|
|
12
|
+
@import url("css/split-view-layout.css");
|
|
13
|
+
@import url("css/pane-header-controls.css");
|
|
14
|
+
@import url("css/split-view-worker-activity.css");
|
|
12
15
|
@import url("css/worker-proposal.css");
|
|
13
16
|
@import url("css/workspace-assignment.css");
|
|
14
17
|
@import url("css/session-actions.css?v=20260828-contrast1");
|
|
@@ -76,7 +79,7 @@
|
|
|
76
79
|
@import url("./css/mcp-skills-workbench.css");
|
|
77
80
|
@import url("css/skills-workbench.css");
|
|
78
81
|
|
|
79
|
-
@import url("css/right-workbench.css?v=
|
|
82
|
+
@import url("css/right-workbench.css?v=20261020-workbench-overlay");
|
|
80
83
|
|
|
81
84
|
@import url("css/mate-conversation.css");
|
|
82
85
|
|
|
@@ -94,4 +97,5 @@
|
|
|
94
97
|
@import url("css/clay-tool-activity.css");
|
|
95
98
|
|
|
96
99
|
@import url("css/contextual-clay.css?v=20261018-loading-scene");
|
|
100
|
+
@import url("css/popup-chat.css");
|
|
97
101
|
@import url("css/mate-loading-scene.css");
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -1413,7 +1413,7 @@ function createSDKBridge(opts) {
|
|
|
1413
1413
|
guardedPush.exactProductDelivery = !!(beforePush && beforePush.exactProductDelivery);
|
|
1414
1414
|
session._queryGeneration = (session._queryGeneration || 0) + 1;
|
|
1415
1415
|
session._queryStarting = true;
|
|
1416
|
-
var starting = startQueryInner(session, text, images, linuxUser, guardedPush, onAccepted, recoveryState);
|
|
1416
|
+
var starting = startQueryInner(session, text, images, linuxUser, guardedPush, onAccepted, recoveryState, payload);
|
|
1417
1417
|
if (recoveryState) recoveryState.starting = starting;
|
|
1418
1418
|
try {
|
|
1419
1419
|
var accepted = await starting;
|
|
@@ -1494,7 +1494,7 @@ function createSDKBridge(opts) {
|
|
|
1494
1494
|
};
|
|
1495
1495
|
}
|
|
1496
1496
|
|
|
1497
|
-
async function startQueryInner(session, text, images, linuxUser, beforePush, onAccepted, recoveryState) {
|
|
1497
|
+
async function startQueryInner(session, text, images, linuxUser, beforePush, onAccepted, recoveryState, admissionPayload) {
|
|
1498
1498
|
var sessionRuntimeEnv = getRuntimeEnv(session);
|
|
1499
1499
|
var resumedAtQueryStart = !!session.cliSessionId;
|
|
1500
1500
|
async function ensureVendorReady(vendor) {
|
|
@@ -1939,6 +1939,10 @@ function createSDKBridge(opts) {
|
|
|
1939
1939
|
if (waiting) session._providerRecoveryWaiting = queryGeneration;
|
|
1940
1940
|
else delete session._providerRecoveryWaiting;
|
|
1941
1941
|
},
|
|
1942
|
+
onRecoveryDiagnostic: function(evidence) {
|
|
1943
|
+
if (sm.sessions.get(session.localId) !== session || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return;
|
|
1944
|
+
console.info('[sdk-bridge] Codex recovery admission ' + JSON.stringify({ sessionId: session.localId, queryGeneration: queryGeneration, evidence: evidence }));
|
|
1945
|
+
},
|
|
1942
1946
|
onRecoveryWaiting: function(waiting) {
|
|
1943
1947
|
if (!queryContextAvailable()) return;
|
|
1944
1948
|
sendAndRecord(session, { type: 'informational', level: 'info', content: waiting
|
|
@@ -2050,7 +2054,16 @@ function createSDKBridge(opts) {
|
|
|
2050
2054
|
session.queryInstance = null;
|
|
2051
2055
|
session.messageQueue = null;
|
|
2052
2056
|
session.abortController = null;
|
|
2053
|
-
|
|
2057
|
+
var startupError = { type: "error", text: "Failed to start query: " + (e.message || e) };
|
|
2058
|
+
if (e.code === 'CODEX_RECOVERY_UNAVAILABLE' || e.code === 'CODEX_RECOVERY_PROBE_UNAVAILABLE') {
|
|
2059
|
+
// Keep undelivered input with the persisted failure, outside the live
|
|
2060
|
+
// input queue. A subsequent explicit send must not replay this batch.
|
|
2061
|
+
startupError.recoveryAdmission = { state: 'not_sent', messages: [admissionPayload].concat(session.pendingPush || []).filter(Boolean).map(function(message) {
|
|
2062
|
+
return { text: message.text, images: message.images || null };
|
|
2063
|
+
}) };
|
|
2064
|
+
startupError.text += ' Messages received during this wait were also not sent; they remain retained with this conversation. Send the messages you want to retry explicitly.';
|
|
2065
|
+
}
|
|
2066
|
+
sendAndRecord(session, startupError);
|
|
2054
2067
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
2055
2068
|
sm.broadcastSessionList();
|
|
2056
2069
|
return false;
|
package/lib/server-owner-ui.js
CHANGED
|
@@ -3,6 +3,7 @@ var crypto = require('crypto');
|
|
|
3
3
|
function attachOwnerUi(ctx) {
|
|
4
4
|
var identities = new WeakMap(), capabilities = new WeakMap(), deliveries = new Map();
|
|
5
5
|
var activityOrder = 0;
|
|
6
|
+
var viewObservation = require('./server-owner-view-observation').attachViewObservation({ check: check, timeout: ctx.renderTimeoutMs });
|
|
6
7
|
function activity(owner, slug, sessionId, mateId, selected, source, validateSource, navigationTarget) {
|
|
7
8
|
var order = ++activityOrder;
|
|
8
9
|
var viewers = selected ? [check(owner, selected)] : sockets(owner).filter(function (viewer) {
|
|
@@ -80,6 +81,7 @@ function attachOwnerUi(ctx) {
|
|
|
80
81
|
return Object.assign({}, record.result);
|
|
81
82
|
}
|
|
82
83
|
function handleMessage(ws, msg) {
|
|
84
|
+
if (viewObservation.handleMessage(ws, msg)) return true;
|
|
83
85
|
if (['clay_ui_ready', 'clay_navigation_claim', 'clay_navigation_ack', 'clay_navigation_rendered'].indexOf(msg.type) < 0) return false;
|
|
84
86
|
var owner = ctx.isMultiUser() ? ws._clayUser && ws._clayUser.id : null;
|
|
85
87
|
if (!sockets(owner).some(function (item) { return item.ws === ws; })) return true;
|
|
@@ -95,7 +97,9 @@ function attachOwnerUi(ctx) {
|
|
|
95
97
|
if (!record.renderClosed && Date.now() > record.created + (ctx.renderTimeoutMs || 5000)) { record.renderClosed = true; clearTimeout(record.renderTimer); record.result.renderStatus = 'unknown'; if (record.activity) record.activity.navigation('unknown'); }
|
|
96
98
|
if (record.renderClosed || !record.claimed || !record.result.acknowledged || record.result.clientStatus !== 'accepted' || msg.renderToken !== record.renderToken) return true;
|
|
97
99
|
var current = inspect(owner, record.viewer.ref);
|
|
98
|
-
if (
|
|
100
|
+
if (record.target.surface === 'split_view') {
|
|
101
|
+
require('./clay-split-view-render-proof').validate(record.target, msg, current, ws);
|
|
102
|
+
} else if (current.ws !== ws || msg.sessionOriginId !== record.target.sessionOriginId || !record.target.sessionOriginId || msg.projectSlug !== record.target.projectSlug || msg.sessionId !== record.target.sessionId || msg.surface !== 'conversation' || msg.entryIndex !== record.target.entryIndex || current.slug !== record.target.projectSlug || current.sessionId !== record.target.sessionId) throw new Error('Rendered target or authenticated viewer changed.');
|
|
99
103
|
record.renderClosed = true; clearTimeout(record.renderTimer);
|
|
100
104
|
record.result.renderStatus = msg.visible === true ? 'rendered' : 'unknown'; record.result.visible = msg.visible === true ? true : 'unknown';
|
|
101
105
|
if (record.activity) record.activity.navigation(msg.visible === true ? 'rendered' : 'unknown');
|
|
@@ -113,6 +117,6 @@ function attachOwnerUi(ctx) {
|
|
|
113
117
|
} catch (error) { record.renderClosed = true; clearTimeout(record.renderTimer); record.result.visible = 'unknown'; record.result.status = 'failed'; record.result.error = error.message; record.result.retrySafe = false; if (record.activity) record.activity.cancel(); }
|
|
114
118
|
return true;
|
|
115
119
|
}
|
|
116
|
-
return { list: sockets, inspect: inspect, check: check, deliver: deliver, read: read, handleMessage: handleMessage, activity: activity };
|
|
120
|
+
return { observeView: viewObservation.observe, list: sockets, inspect: inspect, check: check, deliver: deliver, read: read, handleMessage: handleMessage, activity: activity };
|
|
117
121
|
}
|
|
118
122
|
module.exports = { attachOwnerUi: attachOwnerUi };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
var crypto = require('crypto');
|
|
2
|
+
// A short-lived challenge addresses one authenticated socket. It never navigates.
|
|
3
|
+
function attachViewObservation(ctx) {
|
|
4
|
+
var pending = new Map();
|
|
5
|
+
function observe(owner, viewer, revalidate) {
|
|
6
|
+
revalidate(); ctx.check(owner, viewer);
|
|
7
|
+
if (!viewer.supported) return Promise.resolve({ status: 'unsupported', visible: 'unknown' });
|
|
8
|
+
if (pending.size >= 32) throw new Error('Too many pending screen observations.');
|
|
9
|
+
return new Promise(function (resolve) {
|
|
10
|
+
var id = crypto.randomUUID(), timer;
|
|
11
|
+
function finish(value) { clearTimeout(timer); pending.delete(id); resolve(value); }
|
|
12
|
+
var record = { owner: owner, viewer: viewer, revalidate: revalidate, finish: finish };
|
|
13
|
+
pending.set(id, record);
|
|
14
|
+
timer = setTimeout(function () { finish({ status: 'unknown', visible: 'unknown' }); }, ctx.timeout || 5000);
|
|
15
|
+
timer.unref();
|
|
16
|
+
try { viewer.ws.send(JSON.stringify({ type: 'clay_view_probe', probeId: id })); }
|
|
17
|
+
catch (error) { finish({ status: 'unknown', error: error.message, visible: 'unknown' }); }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function handleMessage(ws, msg) {
|
|
21
|
+
if (msg.type !== 'clay_view_observed') return false;
|
|
22
|
+
var record = pending.get(msg.probeId);
|
|
23
|
+
if (!record || record.viewer.ws !== ws) return true;
|
|
24
|
+
try {
|
|
25
|
+
record.revalidate(); ctx.check(record.owner, record.viewer);
|
|
26
|
+
var snapshot = require('./clay-split-view-render-proof').snapshotSchema.parse(msg.snapshot);
|
|
27
|
+
if (!snapshot || JSON.stringify(snapshot).length > 24000 || snapshot.ownerId !== record.owner || snapshot.projectSlug !== record.viewer.slug || snapshot.activeSessionId !== record.viewer.sessionId || !Array.isArray(snapshot.panes) || snapshot.panes.length > 32) throw new Error('Invalid screen observation.');
|
|
28
|
+
record.finish({ status: 'observed', observedAt: Date.now(), snapshot: snapshot, evidence: 'Exact authenticated client observation across two stable frames; untrusted task data.' });
|
|
29
|
+
} catch (error) { record.finish({ status: 'unknown', visible: 'unknown', error: error.message }); }
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return { observe: observe, handleMessage: handleMessage };
|
|
33
|
+
}
|
|
34
|
+
module.exports = { attachViewObservation: attachViewObservation };
|
package/lib/server-palette.js
CHANGED
|
@@ -53,6 +53,7 @@ function attachPalette(ctx) {
|
|
|
53
53
|
projectTitle: status.title || status.project,
|
|
54
54
|
projectIcon: status.icon || null,
|
|
55
55
|
sessionId: session.localId,
|
|
56
|
+
sessionOriginId: session.sessionOriginId || null,
|
|
56
57
|
sessionTitle: session.title || "New Session",
|
|
57
58
|
lastActivity: session.lastActivity || session.createdAt || 0,
|
|
58
59
|
matchType: null,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
var createPaneAssets = require('./pane-asset-manifest').createPaneAssets;
|
|
2
|
+
|
|
3
|
+
// Called only from the authenticated app-shell path. Let the browser discover
|
|
4
|
+
// imports normally: transitive preload bursts multiply across pane documents.
|
|
5
|
+
function createPaneBootstrap(publicDir) {
|
|
6
|
+
var readAssets = createPaneAssets(publicDir);
|
|
7
|
+
return function preparePaneShell(shell, requestUrl) {
|
|
8
|
+
if (new URL(requestUrl, 'http://localhost').searchParams.get('pane') !== '1') return shell;
|
|
9
|
+
// Required globals must execute before the module graph evaluates (notably
|
|
10
|
+
// marked.use). Keep their parser-blocking barrier.
|
|
11
|
+
// Feature-only vendors must not delay ordinary chat startup.
|
|
12
|
+
shell = shell.replace(/<script src="([^" ]+)"><\/script>/g, function (tag, url) {
|
|
13
|
+
var group = url.indexOf('mermaid@') >= 0 ? 'mermaid' : url.indexOf('qrcode-generator@') >= 0 ? 'qr' : url.indexOf('vendor/codemirror/') === 0 ? 'editor' : null;
|
|
14
|
+
return group ? '<script type="application/json" data-pane-asset-group="' + group + '" data-src="' + url + '"></script>' : tag;
|
|
15
|
+
});
|
|
16
|
+
var assets = readAssets();
|
|
17
|
+
if (assets) {
|
|
18
|
+
shell = shell.replace(/(<script type="module" src=")app\.js(?:\?[^"<>]*)?("><\/script>)/, '$1' + assets.js + '$2');
|
|
19
|
+
shell = shell.replace(/(<link rel="stylesheet" href=")style\.css(?:\?[^"<>]*)?(">)/, '$1' + assets.css + '$2');
|
|
20
|
+
}
|
|
21
|
+
return shell;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
module.exports = { createPaneBootstrap: createPaneBootstrap };
|
package/lib/server-settings.js
CHANGED
|
@@ -13,12 +13,14 @@ function attachSettings(ctx) {
|
|
|
13
13
|
|
|
14
14
|
var profilePath = path.join(CONFIG_DIR, "profile.json");
|
|
15
15
|
var contextualClaySettings = require('./server-contextual-clay-settings').attachContextualClaySettings(ctx);
|
|
16
|
+
var workbenchLayoutSettings = require('./server-workbench-layout-settings').attachWorkbenchLayoutSettings(ctx);
|
|
16
17
|
var appearanceSettings = require('./server-appearance-settings').attachAppearanceSettings(ctx);
|
|
17
18
|
var notificationSettings = require('./server-notification-settings').attachNotificationSettings(ctx);
|
|
18
19
|
var experimentalSettings = attachExperimentalSettings(ctx);
|
|
19
20
|
|
|
20
21
|
function handleRequest(req, res, fullUrl) {
|
|
21
22
|
if (contextualClaySettings.handleRequest(req, res, fullUrl)) return true;
|
|
23
|
+
if (workbenchLayoutSettings.handleRequest(req, res, fullUrl)) return true;
|
|
22
24
|
if (appearanceSettings.handleRequest(req, res, fullUrl)) return true;
|
|
23
25
|
if (notificationSettings.handleRequest(req, res, fullUrl)) return true;
|
|
24
26
|
if (experimentalSettings.handleRequest(req, res, fullUrl)) return true;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
var MIME_TYPES = {
|
|
6
|
+
".html": "text/html",
|
|
7
|
+
".css": "text/css",
|
|
8
|
+
".js": "application/javascript",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".png": "image/png",
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".gif": "image/gif",
|
|
14
|
+
".webp": "image/webp",
|
|
15
|
+
".bmp": "image/bmp",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".ico": "image/x-icon",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// This helper runs only after the caller's existing routing and authorization.
|
|
21
|
+
// Re-read and hash the actual bytes: timestamps/length cannot detect every edit.
|
|
22
|
+
function matches(header, etag) {
|
|
23
|
+
if (typeof header !== 'string') return false;
|
|
24
|
+
if (header.trim() === '*') return true;
|
|
25
|
+
var tag = '(?:W/)?"[\x21\x23-\x7e\x80-\xff]*"';
|
|
26
|
+
if (!new RegExp('^[ \t]*' + tag + '(?:[ \t]*,[ \t]*' + tag + ')*[ \t]*$').test(header)) return false;
|
|
27
|
+
return (header.match(new RegExp(tag, 'g')) || []).some(function (value) { return value.replace(/^W\//, '') === etag; });
|
|
28
|
+
}
|
|
29
|
+
function createStaticAssets(publicDir) {
|
|
30
|
+
return function serveStatic(urlPath, req, res) {
|
|
31
|
+
if (urlPath === '/') urlPath = '/index.html';
|
|
32
|
+
var filePath = path.join(publicDir, urlPath);
|
|
33
|
+
if (!filePath.startsWith(publicDir + path.sep)) {
|
|
34
|
+
res.writeHead(403); res.end('Forbidden'); return true;
|
|
35
|
+
}
|
|
36
|
+
var content;
|
|
37
|
+
try { content = fs.readFileSync(filePath); } catch (error) { return false; }
|
|
38
|
+
var ext = path.extname(filePath);
|
|
39
|
+
var mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
40
|
+
var isImage = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'].indexOf(ext) >= 0;
|
|
41
|
+
var headers = { 'Content-Type': mime + (isImage ? '' : '; charset=utf-8'),
|
|
42
|
+
'Cache-Control': isImage ? 'public, max-age=86400, immutable' : 'no-cache' };
|
|
43
|
+
if ((ext === '.js' || ext === '.css') && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
44
|
+
headers.ETag = '"' + crypto.createHash('sha256').update(content).digest('hex') + '"';
|
|
45
|
+
if (matches(req.headers['if-none-match'], headers.ETag)) {
|
|
46
|
+
res.writeHead(304, headers); res.end(); return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
res.writeHead(200, headers); res.end(content); return true;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
module.exports = { createStaticAssets: createStaticAssets };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// User Settings deliberately have no canonical Clay control endpoint.
|
|
2
|
+
function attachWorkbenchLayoutSettings(ctx) {
|
|
3
|
+
function reply(res, status, data) {
|
|
4
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
5
|
+
res.end(JSON.stringify(data));
|
|
6
|
+
}
|
|
7
|
+
function identity(req) {
|
|
8
|
+
if (!ctx.isRequestAuthed(req)) return null;
|
|
9
|
+
var user = ctx.users.isMultiUser() ? ctx.getMultiUserFromReq(req) : { id: 'default' };
|
|
10
|
+
return user && (!ctx.users.isMultiUser() || ctx.users.findUserById(user.id)) ? user.id : null;
|
|
11
|
+
}
|
|
12
|
+
function handleRequest(req, res, url) {
|
|
13
|
+
if (url !== '/api/user/workbench-layout') return false;
|
|
14
|
+
var accountId = identity(req);
|
|
15
|
+
if (!accountId) { reply(res, 401, { error: 'Sign in to manage workbench layout.' }); return true; }
|
|
16
|
+
if (req.headers['x-clay-account-id'] !== accountId) { reply(res, 403, { error: 'Account changed. Reload and try again.' }); return true; }
|
|
17
|
+
if (req.method === 'GET') {
|
|
18
|
+
try { reply(res, 200, ctx.users.getWorkbenchLayoutPreference(accountId)); }
|
|
19
|
+
catch (error) { reply(res, 500, { error: 'Workbench layout could not be loaded. Try again.' }); }
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (req.method !== 'PUT') { reply(res, 405, { error: 'Method not allowed.' }); return true; }
|
|
23
|
+
if (req.headers['x-clay-settings'] !== '1' || req.headers['sec-fetch-site'] && ['same-origin', 'none'].indexOf(req.headers['sec-fetch-site']) < 0) {
|
|
24
|
+
reply(res, 403, { error: 'Save preferences from Clay.' }); return true;
|
|
25
|
+
}
|
|
26
|
+
if (req.headers.origin) {
|
|
27
|
+
try { if (new URL(req.headers.origin).host !== req.headers.host) throw new Error('Foreign origin'); }
|
|
28
|
+
catch (error) { reply(res, 403, { error: 'Save preferences from Clay.' }); return true; }
|
|
29
|
+
}
|
|
30
|
+
var body = '', finished = false;
|
|
31
|
+
req.setEncoding('utf8');
|
|
32
|
+
req.on('data', function (chunk) {
|
|
33
|
+
if (finished) return;
|
|
34
|
+
body += chunk;
|
|
35
|
+
if (Buffer.byteLength(body) > 1024) { finished = true; body = ''; reply(res, 413, { error: 'Request too large.' }); }
|
|
36
|
+
});
|
|
37
|
+
req.on('aborted', function () { finished = true; });
|
|
38
|
+
req.on('error', function () { finished = true; });
|
|
39
|
+
req.on('end', function () {
|
|
40
|
+
if (finished) return; finished = true;
|
|
41
|
+
if (identity(req) !== accountId) { reply(res, 401, { error: 'Account changed. Sign in again.' }); return; }
|
|
42
|
+
var input;
|
|
43
|
+
try { input = JSON.parse(body); } catch (error) { reply(res, 400, { error: 'Invalid JSON.' }); return; }
|
|
44
|
+
if (!input || Array.isArray(input) || Object.keys(input).length !== 2 || !Object.prototype.hasOwnProperty.call(input, 'mode') || !Object.prototype.hasOwnProperty.call(input, 'revision')) {
|
|
45
|
+
reply(res, 400, { error: 'Invalid workbench preference.' }); return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
var result = ctx.users.setWorkbenchLayoutPreference(accountId, input.mode, input.revision);
|
|
49
|
+
reply(res, result.error ? result.status || 400 : 200, result);
|
|
50
|
+
} catch (error) { reply(res, 500, { error: 'Workbench layout could not be saved. Your previous layout is retained.' }); }
|
|
51
|
+
});
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return { handleRequest: handleRequest };
|
|
55
|
+
}
|
|
56
|
+
module.exports = { attachWorkbenchLayoutSettings: attachWorkbenchLayoutSettings };
|
package/lib/server.js
CHANGED
|
@@ -70,20 +70,8 @@ function httpGetBinary(url) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
var
|
|
74
|
-
|
|
75
|
-
".css": "text/css",
|
|
76
|
-
".js": "application/javascript",
|
|
77
|
-
".json": "application/json",
|
|
78
|
-
".png": "image/png",
|
|
79
|
-
".jpg": "image/jpeg",
|
|
80
|
-
".jpeg": "image/jpeg",
|
|
81
|
-
".gif": "image/gif",
|
|
82
|
-
".webp": "image/webp",
|
|
83
|
-
".bmp": "image/bmp",
|
|
84
|
-
".svg": "image/svg+xml",
|
|
85
|
-
".ico": "image/x-icon",
|
|
86
|
-
};
|
|
73
|
+
var serveStatic = require('./server-static-assets').createStaticAssets(publicDir);
|
|
74
|
+
var preparePaneShell = require('./server-pane-bootstrap').createPaneBootstrap(publicDir);
|
|
87
75
|
|
|
88
76
|
var wsRateLimit = require("./ws-rate-limit");
|
|
89
77
|
var applyWsRateLimit = wsRateLimit.applyWsRateLimit;
|
|
@@ -91,34 +79,6 @@ var WS_RATE_LIMIT = wsRateLimit.WS_RATE_LIMIT;
|
|
|
91
79
|
var generateAuthToken = serverAuth.generateAuthToken;
|
|
92
80
|
var verifyPin = serverAuth.verifyPin;
|
|
93
81
|
|
|
94
|
-
function serveStatic(urlPath, res) {
|
|
95
|
-
if (urlPath === "/") urlPath = "/index.html";
|
|
96
|
-
|
|
97
|
-
var filePath = path.join(publicDir, urlPath);
|
|
98
|
-
|
|
99
|
-
if (!filePath.startsWith(publicDir)) {
|
|
100
|
-
res.writeHead(403);
|
|
101
|
-
res.end("Forbidden");
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
var content = fs.readFileSync(filePath);
|
|
107
|
-
var ext = path.extname(filePath);
|
|
108
|
-
var mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
109
|
-
var isImage = ext === ".png" || ext === ".jpg" || ext === ".jpeg" || ext === ".gif" || ext === ".svg" || ext === ".webp" || ext === ".ico";
|
|
110
|
-
var cacheControl = isImage ? "public, max-age=86400, immutable" : "no-cache";
|
|
111
|
-
res.writeHead(200, {
|
|
112
|
-
"Content-Type": mime + (isImage ? "" : "; charset=utf-8"),
|
|
113
|
-
"Cache-Control": cacheControl,
|
|
114
|
-
});
|
|
115
|
-
res.end(content);
|
|
116
|
-
return true;
|
|
117
|
-
} catch (e) {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
82
|
/**
|
|
123
83
|
* Extract slug from URL path: /p/{slug}/... → slug
|
|
124
84
|
* Returns null if path doesn't match /p/{slug}
|
|
@@ -565,6 +525,7 @@ function createServer(opts) {
|
|
|
565
525
|
try {
|
|
566
526
|
var shell = fs.readFileSync(path.join(publicDir, "index.html"), "utf8");
|
|
567
527
|
if (slug) shell = injectRootProjectSlug(shell, slug);
|
|
528
|
+
shell = preparePaneShell(shell, req.url);
|
|
568
529
|
var appearance = null;
|
|
569
530
|
try { appearance = settings.getInitialAppearance(req); } catch (e) {}
|
|
570
531
|
shell = injectAppearanceSeed(shell, appearance);
|
|
@@ -1076,7 +1037,7 @@ function createServer(opts) {
|
|
|
1076
1037
|
sendAppShell(req, res, null);
|
|
1077
1038
|
return;
|
|
1078
1039
|
}
|
|
1079
|
-
if (serveStatic(fullUrl, res)) return;
|
|
1040
|
+
if (serveStatic(fullUrl, req, res)) return;
|
|
1080
1041
|
}
|
|
1081
1042
|
|
|
1082
1043
|
// Project-scoped routes: /p/{slug}/...
|
|
@@ -1175,7 +1136,7 @@ function createServer(opts) {
|
|
|
1175
1136
|
sendAppShell(req, res, slug);
|
|
1176
1137
|
return;
|
|
1177
1138
|
}
|
|
1178
|
-
if (serveStatic(projectUrl, res)) return;
|
|
1139
|
+
if (serveStatic(projectUrl, req, res)) return;
|
|
1179
1140
|
}
|
|
1180
1141
|
|
|
1181
1142
|
res.writeHead(404);
|
|
@@ -1230,7 +1191,7 @@ function createServer(opts) {
|
|
|
1230
1191
|
|
|
1231
1192
|
// Static files at root (favicon, manifest, icons, etc.)
|
|
1232
1193
|
if (url.lastIndexOf("/") === 0 && !url.includes("..")) {
|
|
1233
|
-
if (serveStatic(url, res)) return;
|
|
1194
|
+
if (serveStatic(url, req, res)) return;
|
|
1234
1195
|
}
|
|
1235
1196
|
|
|
1236
1197
|
// Everything else → redirect to HTTPS setup
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function count(value) { return value && typeof value === 'object' ? Object.keys(value).length : 0; }
|
|
2
|
+
function sessionActivity(session) {
|
|
3
|
+
if (session.isProcessing) return 'processing';
|
|
4
|
+
if (count(session.pendingPermissions)) return 'waiting_for_approval';
|
|
5
|
+
return 'idle';
|
|
6
|
+
}
|
|
7
|
+
function currentState(session) {
|
|
8
|
+
var approvals = count(session.pendingPermissions);
|
|
9
|
+
var input = count(session.pendingAskUser) + count(session.pendingUserDialogs) + count(session.pendingElicitations);
|
|
10
|
+
var processing = session.isProcessing === true, starting = session._queryStarting === true;
|
|
11
|
+
var recoveryStopPending = !!session._responseRecoveryStopPending;
|
|
12
|
+
var questions = count(session.pendingAskUser), dialogs = count(session.pendingUserDialogs), elicitations = count(session.pendingElicitations);
|
|
13
|
+
var stopRequested = session.taskStopRequested === true, queuePaused = session._pendingMessageDrainPaused === true;
|
|
14
|
+
// Review requests are a subset of pending questions, never an approval result.
|
|
15
|
+
var reviews = Object.values(session.pendingAskUser || {}).filter(function (pending) { return pending && pending.request && ['product-control', 'mcp-import', 'mate-schedule'].indexOf(pending.request.source) >= 0; }).length;
|
|
16
|
+
var state = 'unknown';
|
|
17
|
+
if (stopRequested || queuePaused) state = processing || starting || recoveryStopPending ? 'stopping' : 'stopped';
|
|
18
|
+
else if (approvals || reviews) state = 'awaiting_approval';
|
|
19
|
+
else if (input) state = 'awaiting_input';
|
|
20
|
+
else if (recoveryStopPending) state = 'recovering';
|
|
21
|
+
else if (processing || starting) state = 'working';
|
|
22
|
+
else if (session.isProcessing === false) state = 'idle';
|
|
23
|
+
return { state: state, observedAt: Date.now(), recoveryStopPending: recoveryStopPending, queryGeneration: Number.isInteger(session._sdkQueryGeneration) ? session._sdkQueryGeneration : null,
|
|
24
|
+
pendingReviewCount: reviews, pendingQuestionCount: questions, pendingDialogCount: dialogs, pendingElicitationCount: elicitations, processing: processing, starting: starting, stopRequested: stopRequested, queuePaused: queuePaused,
|
|
25
|
+
pendingApprovalCount: approvals, pendingInputCount: input, providerConnection: 'unknown',
|
|
26
|
+
evidence: 'Live server session flags and pending request registries; idle does not establish task completion.' };
|
|
27
|
+
}
|
|
28
|
+
module.exports = { sessionActivity: sessionActivity, currentState: currentState };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Cache only an already authorized, serialized projection for this live socket.
|
|
2
|
+
// Callers must recompute visibility before every delivery; this is not an access
|
|
3
|
+
// cache. Ordinary refreshes always send, even when their content is unchanged.
|
|
4
|
+
function createSessionListDelivery() {
|
|
5
|
+
var delivered = new WeakMap();
|
|
6
|
+
var MAX_CACHED_BYTES = 1024 * 1024;
|
|
7
|
+
return function (ws, message, skipUnchanged) {
|
|
8
|
+
if (!ws || ws.readyState !== 1) { if (ws) delivered.delete(ws); return false; }
|
|
9
|
+
var encoded = JSON.stringify(message);
|
|
10
|
+
if (skipUnchanged && delivered.get(ws) === encoded) return false;
|
|
11
|
+
ws.send(encoded);
|
|
12
|
+
if (Buffer.byteLength(encoded) <= MAX_CACHED_BYTES) delivered.set(ws, encoded);
|
|
13
|
+
else delivered.delete(ws);
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
module.exports = { createSessionListDelivery: createSessionListDelivery };
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
// A human Stop is a cancellation barrier, not merely an AbortController
|
|
4
4
|
// signal. It blocks delegation and Worker replacement until the next ordinary
|
|
5
5
|
// human message reaches the Driver. Internal result and permission messages do
|
|
6
|
-
// not clear it.
|
|
6
|
+
// not clear it. Replacement budgets and operation ids also make retries bounded
|
|
7
7
|
// and idempotent even when no Stop occurred.
|
|
8
8
|
|
|
9
|
-
var MAX_CREATIONS_PER_TURN = 2;
|
|
10
9
|
var MAX_REPLACEMENTS_PER_TURN = 1;
|
|
11
10
|
var MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN = 2;
|
|
12
11
|
var MAX_OPERATION_ID_CHARS = 120;
|
|
@@ -101,9 +100,6 @@ function attachPairTurnControl(ctx) {
|
|
|
101
100
|
if (kind === "replace" && state.failedReplacementAttempts >= MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN) {
|
|
102
101
|
throw new Error("the Split Worker replacement failure limit for this human turn has been reached; wait for a new human message before retrying replacement");
|
|
103
102
|
}
|
|
104
|
-
if (state.creations >= MAX_CREATIONS_PER_TURN) {
|
|
105
|
-
throw new Error("the Split Worker creation limit for this human turn has been reached; wait for a new human message before creating another Worker");
|
|
106
|
-
}
|
|
107
103
|
state.creations += 1;
|
|
108
104
|
if (kind === "replace") state.replacements += 1;
|
|
109
105
|
return { driver: driver, kind: kind, turnSerial: state.serial, active: true };
|
|
@@ -172,7 +168,6 @@ function attachPairTurnControl(ctx) {
|
|
|
172
168
|
}
|
|
173
169
|
|
|
174
170
|
module.exports = {
|
|
175
|
-
MAX_CREATIONS_PER_TURN: MAX_CREATIONS_PER_TURN,
|
|
176
171
|
MAX_REPLACEMENTS_PER_TURN: MAX_REPLACEMENTS_PER_TURN,
|
|
177
172
|
MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN: MAX_FAILED_REPLACEMENT_ATTEMPTS_PER_TURN,
|
|
178
173
|
attachPairTurnControl: attachPairTurnControl,
|
package/lib/session-search.js
CHANGED
|
@@ -539,6 +539,7 @@ function searchPalette(projectSessions, query, opts) {
|
|
|
539
539
|
projectTitle: proj.projectTitle,
|
|
540
540
|
projectIcon: proj.projectIcon || null,
|
|
541
541
|
sessionId: session.localId,
|
|
542
|
+
sessionOriginId: session.sessionOriginId || null,
|
|
542
543
|
sessionTitle: title,
|
|
543
544
|
lastActivity: session.lastActivity || session.createdAt || 0,
|
|
544
545
|
isMate: proj.isMate || false,
|
|
@@ -561,6 +562,7 @@ function searchPalette(projectSessions, query, opts) {
|
|
|
561
562
|
projectTitle: proj.projectTitle,
|
|
562
563
|
projectIcon: proj.projectIcon || null,
|
|
563
564
|
sessionId: session.localId,
|
|
565
|
+
sessionOriginId: session.sessionOriginId || null,
|
|
564
566
|
sessionTitle: title,
|
|
565
567
|
lastActivity: session.lastActivity || session.createdAt || 0,
|
|
566
568
|
isMate: proj.isMate || false,
|
|
@@ -592,6 +594,7 @@ function searchPalette(projectSessions, query, opts) {
|
|
|
592
594
|
projectTitle: m.projectTitle,
|
|
593
595
|
projectIcon: m.projectIcon,
|
|
594
596
|
sessionId: m.sessionId,
|
|
597
|
+
sessionOriginId: m.sessionOriginId,
|
|
595
598
|
sessionTitle: m.sessionTitle,
|
|
596
599
|
lastActivity: m.lastActivity,
|
|
597
600
|
matchType: m.matchType,
|
|
@@ -46,7 +46,7 @@ function versionedMemberAnchors(group, sessions) {
|
|
|
46
46
|
|
|
47
47
|
function resolveVersionedPair(sessions, anchors) {
|
|
48
48
|
if (!anchors || anchors.version !== 2 || !anchors.driver || !Array.isArray(anchors.workers) ||
|
|
49
|
-
|
|
49
|
+
anchors.workers.length < 1) return null;
|
|
50
50
|
var driver = findSession(sessions, anchors.driver.cli, anchors.driver.origin);
|
|
51
51
|
if (!driver) return null;
|
|
52
52
|
var workers = [];
|
|
@@ -62,7 +62,7 @@ function resolveVersionedPair(sessions, anchors) {
|
|
|
62
62
|
|
|
63
63
|
function resolveVersionedMembers(sessions, anchors) {
|
|
64
64
|
if (!anchors || anchors.version !== 2 || !Array.isArray(anchors.members) ||
|
|
65
|
-
|
|
65
|
+
anchors.members.length < 2) return null;
|
|
66
66
|
var resolved = [];
|
|
67
67
|
for (var i = 0; i < anchors.members.length; i++) {
|
|
68
68
|
var anchor = anchors.members[i];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Shared role and target identity rules for Split Groups.
|
|
2
2
|
//
|
|
3
3
|
// Version 1 is the persisted two-session pair shape. Version 2 describes one
|
|
4
|
-
// Driver and
|
|
4
|
+
// Driver and one or more Workers, but callers must explicitly opt into it until
|
|
5
5
|
// all production consumers can address more than one Worker.
|
|
6
6
|
// Remaining direct pair.workerId reads are deliberate compatibility branches:
|
|
7
7
|
// session-split-groups owns legacy-only create and Driver-transfer transactions,
|
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
// Production activation must still migrate the client hierarchy, pane lock, and
|
|
10
10
|
// split-group helper projections before the server gate is enabled.
|
|
11
11
|
|
|
12
|
-
var MAX_WORKERS = 2;
|
|
13
|
-
|
|
14
12
|
function isInteger(value) {
|
|
15
13
|
return Number.isInteger(value);
|
|
16
14
|
}
|
|
@@ -22,8 +20,8 @@ function containsOnce(values, value) {
|
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
function validateMembers(members) {
|
|
25
|
-
if (!Array.isArray(members) ||
|
|
26
|
-
return { ok: false, error: "A split group requires
|
|
23
|
+
if (!Array.isArray(members) || members.length < 2) {
|
|
24
|
+
return { ok: false, error: "A split group requires at least two sessions" };
|
|
27
25
|
}
|
|
28
26
|
for (var i = 0; i < members.length; i++) {
|
|
29
27
|
if (!isInteger(members[i])) return { ok: false, error: "Split group members must be distinct integer session ids" };
|
|
@@ -34,8 +32,8 @@ function validateMembers(members) {
|
|
|
34
32
|
|
|
35
33
|
function validateRoleIds(driverId, workerIds, members) {
|
|
36
34
|
if (!isInteger(driverId)) return { ok: false, error: "The Driver role must reference an integer session id" };
|
|
37
|
-
if (!Array.isArray(workerIds) || workerIds.length < 1
|
|
38
|
-
return { ok: false, error: "A configured role requires
|
|
35
|
+
if (!Array.isArray(workerIds) || workerIds.length < 1) {
|
|
36
|
+
return { ok: false, error: "A configured role requires at least one Worker" };
|
|
39
37
|
}
|
|
40
38
|
if (!containsOnce(members, driverId)) return { ok: false, error: "The Driver role must reference exactly one split group member" };
|
|
41
39
|
var seen = [driverId];
|
|
@@ -150,7 +148,6 @@ function tokenMatchesWorker(worker, token) {
|
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
module.exports = {
|
|
153
|
-
MAX_WORKERS: MAX_WORKERS,
|
|
154
151
|
normalizePair: normalizePair,
|
|
155
152
|
matchesLiveTarget: matchesLiveTarget,
|
|
156
153
|
resolveLiveRoles: resolveLiveRoles,
|
|
@@ -102,9 +102,6 @@ function attachVersionedMutations(ctx) {
|
|
|
102
102
|
if (denied) return denied;
|
|
103
103
|
var exact = exactGroup(ownerId, msg || {}, false);
|
|
104
104
|
if (exact.error) return { ok: false, error: exact.error };
|
|
105
|
-
if (exact.normalized.workerIds.length >= splitRoles.MAX_WORKERS) {
|
|
106
|
-
return { ok: false, error: "A Driver can have at most two Workers" };
|
|
107
|
-
}
|
|
108
105
|
var added = newWorker(ownerId, msg);
|
|
109
106
|
if (added.error) return { ok: false, error: added.error };
|
|
110
107
|
var workerIds = exact.normalized.workerIds.concat([added.worker.localId]);
|
package/lib/sessions.js
CHANGED
|
@@ -35,6 +35,7 @@ function createSessionManager(opts) {
|
|
|
35
35
|
var slashCommands = null; // shared across sessions (deprecated, use slashCommandsByVendor)
|
|
36
36
|
var slashCommandsByVendor = {}; // vendor -> array of slash commands
|
|
37
37
|
var skillNames = null; // Claude-only skills to filter from slash menu
|
|
38
|
+
var deliverSessionList = require('./session-list-delivery').createSessionListDelivery();
|
|
38
39
|
var singleUserUnread = {}; // sessionLocalId -> unread count (single-user mode)
|
|
39
40
|
var permissionRequestIndex = {}; // requestId -> sessionLocalId (O(1) lookup)
|
|
40
41
|
var capabilitiesByVendor = null; // set by sdk-bridge after adapter init
|
|
@@ -652,7 +653,7 @@ function createSessionManager(opts) {
|
|
|
652
653
|
});
|
|
653
654
|
}
|
|
654
655
|
|
|
655
|
-
function broadcastSessionList() {
|
|
656
|
+
function broadcastSessionList(skipUnchanged) {
|
|
656
657
|
var allVisible = getVisibleSessions();
|
|
657
658
|
if (sendEach) {
|
|
658
659
|
// Per-client filtering (multi-user mode)
|
|
@@ -661,10 +662,10 @@ function createSessionManager(opts) {
|
|
|
661
662
|
var clientActiveId = ws._clayActiveSession;
|
|
662
663
|
var wsUnread = ws._clayUnread || {};
|
|
663
664
|
if (ws.readyState === 1) {
|
|
664
|
-
ws
|
|
665
|
+
deliverSessionList(ws, {
|
|
665
666
|
type: "session_list",
|
|
666
667
|
sessions: mapSessionsForClient(filtered, clientActiveId, wsUnread, ws._clayUser ? ws._clayUser.id : null),
|
|
667
|
-
})
|
|
668
|
+
}, skipUnchanged === true);
|
|
668
669
|
}
|
|
669
670
|
});
|
|
670
671
|
} else {
|
|
@@ -879,7 +880,7 @@ function createSessionManager(opts) {
|
|
|
879
880
|
|| session.slashCommands
|
|
880
881
|
|| [];
|
|
881
882
|
_send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
|
|
882
|
-
broadcastSessionList();
|
|
883
|
+
broadcastSessionList(!!(snapshotSend && targetWs && targetWs._clayPane));
|
|
883
884
|
replayHistory(session, undefined, targetWs, transform, snapshotSend);
|
|
884
885
|
|
|
885
886
|
if (session.isProcessing) {
|
|
@@ -973,9 +974,18 @@ function createSessionManager(opts) {
|
|
|
973
974
|
}
|
|
974
975
|
}
|
|
975
976
|
|
|
976
|
-
function deleteSessionQuiet(localId) {
|
|
977
|
+
function deleteSessionQuiet(localId, options) {
|
|
977
978
|
var session = sessions.get(localId);
|
|
978
979
|
if (!session) return;
|
|
980
|
+
// Exact reviewed removal must observe durable deletion before releasing identity.
|
|
981
|
+
var strictRemoval = options && options.requireDurableRemoval === true;
|
|
982
|
+
if (strictRemoval) {
|
|
983
|
+
var strictRecordId = existingSessionRecordId(session);
|
|
984
|
+
if (strictRecordId) {
|
|
985
|
+
try { fs.unlinkSync(sessionFilePath(strictRecordId)); }
|
|
986
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
987
|
+
}
|
|
988
|
+
}
|
|
979
989
|
delete singleUserUnread[localId];
|
|
980
990
|
cleanupMentionSessions(session);
|
|
981
991
|
if (session.abortController) {
|
|
@@ -995,7 +1005,7 @@ function createSessionManager(opts) {
|
|
|
995
1005
|
}
|
|
996
1006
|
if (session.cliSessionId) rememberDeletedCliSession(session.cliSessionId);
|
|
997
1007
|
var recordId = existingSessionRecordId(session);
|
|
998
|
-
if (recordId) try { fs.unlinkSync(sessionFilePath(recordId)); } catch(e) {}
|
|
1008
|
+
if (recordId && !strictRemoval) try { fs.unlinkSync(sessionFilePath(recordId)); } catch(e) {}
|
|
999
1009
|
sessions.delete(localId);
|
|
1000
1010
|
try { onSessionDeleted(localId, session); } catch (e) {
|
|
1001
1011
|
console.error("[session] Session deletion hook failed:", e.message || e);
|