clay-server 5.5.0-beta.2 → 5.5.0-beta.4
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-product-control-input.js +2 -2
- package/lib/clay-product-control-tools.js +1 -1
- package/lib/clay-product-control.js +4 -3
- package/lib/daemon-project-access.js +10 -2
- package/lib/daemon-projects.js +16 -4
- package/lib/daemon-worktree-controls.js +171 -0
- package/lib/daemon.js +14 -63
- package/lib/personal-provider-install.js +85 -0
- package/lib/personal-provider-settings.js +94 -0
- package/lib/project-logs-service.js +2 -2
- package/lib/project-pair-task-control.js +1 -1
- package/lib/project-pending-message-queue.js +6 -2
- package/lib/project-product-control.js +1 -0
- package/lib/project-session-actions.js +46 -3
- package/lib/project-session-pair.js +2 -0
- package/lib/project-sessions.js +11 -3
- package/lib/project-user-message.js +5 -1
- package/lib/project-vendor-login.js +95 -32
- package/lib/project-worker-auth.js +71 -0
- package/lib/project-worker-conversation.js +11 -2
- package/lib/project.js +9 -0
- package/lib/public/app.js +22 -0
- package/lib/public/css/icon-strip.css +5 -0
- package/lib/public/css/overlays.css +18 -0
- package/lib/public/css/settings-shell.css +13 -0
- package/lib/public/css/settings-ui.css +9 -0
- package/lib/public/css/split-view-layout.css +16 -11
- package/lib/public/modules/agent-behavior-nav.js +1 -0
- package/lib/public/modules/agent-message-bubbles.js +33 -4
- package/lib/public/modules/app-messages.js +7 -1
- package/lib/public/modules/app-projects.js +3 -4
- package/lib/public/modules/clay-control-copy.js +1 -0
- package/lib/public/modules/clay-tool-presentation.js +1 -1
- package/lib/public/modules/generation-stop.js +4 -4
- package/lib/public/modules/pane-activation.js +2 -2
- package/lib/public/modules/pane-bridge.js +4 -2
- package/lib/public/modules/personal-provider-context.js +11 -0
- package/lib/public/modules/project-settings.js +1 -1
- package/lib/public/modules/settings-section-nav.js +58 -0
- package/lib/public/modules/settings-shell-registry.js +7 -4
- package/lib/public/modules/settings-shell.js +17 -6
- package/lib/public/modules/sidebar-projects.js +23 -9
- package/lib/public/modules/split-pane-force-stop.js +47 -0
- package/lib/public/modules/split-pane-header-state.js +11 -1
- package/lib/public/modules/split-pane-renderer.js +2 -2
- package/lib/public/modules/split-view-workers.js +32 -17
- package/lib/public/modules/stt-settings.js +21 -4
- package/lib/public/modules/user-settings-layout.js +22 -18
- package/lib/public/modules/user-settings-provider-install.js +42 -0
- package/lib/public/modules/user-settings-providers.js +102 -0
- package/lib/public/modules/user-settings-voice.js +50 -41
- package/lib/public/modules/vendor-login-browser.js +1 -1
- package/lib/public/modules/vendor-login.js +72 -12
- package/lib/public/modules/worker-activity-indicator.js +49 -0
- package/lib/public/modules/worker-message-previews.js +47 -11
- package/lib/public/modules/worker-stop-controls.js +36 -0
- package/lib/public/modules/worktree-controls.js +86 -0
- package/lib/public/pane-assets/{app-SEMVZ4AY.js → app-PV3SEOTX.js} +1 -1
- package/lib/public/pane-assets/{chunk-C6P7PICY.js → chunk-2G6DB3ES.js} +1 -1
- package/lib/public/pane-assets/{chunk-BNNPJJVG.js → chunk-4WYURTPH.js} +1 -1
- package/lib/public/pane-assets/{chunk-E4VM2IDB.js → chunk-53KRXJDL.js} +1 -1
- package/lib/public/pane-assets/{chunk-4YE4RU27.js → chunk-AMZ7P526.js} +1 -1
- package/lib/public/pane-assets/chunk-BK2NRZFM.js +1 -0
- package/lib/public/pane-assets/{chunk-5UBN6GR7.js → chunk-D3WR2TYZ.js} +1 -1
- package/lib/public/pane-assets/chunk-E7D5A27I.js +1 -0
- package/lib/public/pane-assets/{chunk-CQNPEH2M.js → chunk-FB5B2WYF.js} +1 -1
- package/lib/public/pane-assets/chunk-FDBSE7BE.js +174 -0
- package/lib/public/pane-assets/{chunk-FK3YLGAP.js → chunk-FHB76KH2.js} +1 -1
- package/lib/public/pane-assets/{chunk-YR5GH3GI.js → chunk-FNGO6432.js} +1 -1
- package/lib/public/pane-assets/chunk-HF7AK56Q.js +1 -0
- package/lib/public/pane-assets/chunk-ILVRGTDF.js +1 -0
- package/lib/public/pane-assets/{chunk-QWQB2YRF.js → chunk-L3SFMSLP.js} +1 -1
- package/lib/public/pane-assets/{chunk-QBCM7QEA.js → chunk-PAY3A4JS.js} +1 -1
- package/lib/public/pane-assets/chunk-QVHC324H.js +1 -0
- package/lib/public/pane-assets/chunk-SFANEWZA.js +1 -0
- package/lib/public/pane-assets/{chunk-M5CWIIIT.js → chunk-SGMEO3P7.js} +1 -1
- package/lib/public/pane-assets/{chunk-YVVT3CTV.js → chunk-TK446XFF.js} +1 -1
- package/lib/public/pane-assets/{chunk-U7EHYRJZ.js → chunk-TU3AHPSN.js} +1 -1
- package/lib/public/pane-assets/chunk-US5VELGB.js +1 -0
- package/lib/public/pane-assets/{chunk-K4PAN77B.js → chunk-UVTS6GUM.js} +1 -1
- package/lib/public/pane-assets/{chunk-XYZGVYWW.js → chunk-X45WI2U7.js} +1 -1
- package/lib/public/pane-assets/manifest.json +401 -393
- package/lib/public/pane-assets/style-563D4UN6.css +1 -0
- package/lib/sdk-bridge.js +40 -6
- package/lib/sdk-force-stop.js +62 -0
- package/lib/sdk-message-processor.js +2 -1
- package/lib/server-worktree-controls.js +34 -0
- package/lib/server.js +11 -1
- package/lib/session-force-stop-support.js +15 -0
- package/lib/sessions.js +6 -1
- package/lib/terminal-manager.js +1 -1
- package/lib/terminal.js +13 -1
- package/lib/vendor-login-browser-flow.js +1 -1
- package/lib/worker-activity.js +26 -0
- package/lib/worker-message-previews.js +12 -7
- package/lib/worktree-branch.js +57 -0
- package/lib/worktree-identity.js +39 -0
- package/lib/worktree-preferences.js +25 -0
- package/lib/worktree-ref-transaction.js +36 -0
- package/lib/yoke/adapters/antigravity.js +3 -0
- package/lib/yoke/force-termination.js +26 -0
- package/lib/yoke/interface.js +14 -0
- package/lib/yoke/process-force-termination.js +24 -0
- package/lib/yoke/provider-install-recipes.js +20 -0
- package/lib/yoke/vendor-registry.js +20 -1
- package/package.json +1 -1
- package/lib/public/pane-assets/chunk-6AGSOAOD.js +0 -1
- package/lib/public/pane-assets/chunk-AATAIAXL.js +0 -1
- package/lib/public/pane-assets/chunk-D3CE5FZL.js +0 -1
- package/lib/public/pane-assets/chunk-GKSLBGVE.js +0 -168
- package/lib/public/pane-assets/chunk-J5PTSKJ6.js +0 -1
- package/lib/public/pane-assets/chunk-QPBO6ID4.js +0 -1
- package/lib/public/pane-assets/chunk-RA6VUJTY.js +0 -1
- package/lib/public/pane-assets/chunk-XOZUM5ET.js +0 -1
- package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
package/lib/sdk-bridge.js
CHANGED
|
@@ -1075,7 +1075,7 @@ function createSDKBridge(opts) {
|
|
|
1075
1075
|
console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
|
|
1076
1076
|
var stillOwnsRuntime = session.queryInstance === myQueryInstance && !responseRecovery.stopping(session);
|
|
1077
1077
|
settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, "The successor query ended before initialization.");
|
|
1078
|
-
if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
|
|
1078
|
+
if ((session.isProcessing || session._awaitingTurnResult) && session.taskStopRequested && stillOwnsRuntime) {
|
|
1079
1079
|
session._lastTurnInterrupted = true;
|
|
1080
1080
|
session.isProcessing = false;
|
|
1081
1081
|
session._awaitingTurnResult = false;
|
|
@@ -1088,7 +1088,7 @@ function createSDKBridge(opts) {
|
|
|
1088
1088
|
sendAndRecord(session, { type: "info", text: interruptMsg });
|
|
1089
1089
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
1090
1090
|
sm.broadcastSessionList();
|
|
1091
|
-
} else if (session.isProcessing && stillOwnsRuntime) {
|
|
1091
|
+
} else if ((session.isProcessing || session._awaitingTurnResult) && stillOwnsRuntime) {
|
|
1092
1092
|
// Codex and Kiro can report a protocol error and then close their
|
|
1093
1093
|
// iterator without a result event. Treat any result-less stream end as
|
|
1094
1094
|
// a failed turn so the UI cannot remain on processing forever.
|
|
@@ -1109,7 +1109,7 @@ function createSDKBridge(opts) {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
} catch (err) {
|
|
1111
1111
|
settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, err.message || "The successor query failed before initialization.");
|
|
1112
|
-
if (session.isProcessing && session.queryInstance === myQueryInstance && !responseRecovery.stopping(session)) {
|
|
1112
|
+
if ((session.isProcessing || session._awaitingTurnResult) && session.queryInstance === myQueryInstance && !responseRecovery.stopping(session)) {
|
|
1113
1113
|
session.isProcessing = false;
|
|
1114
1114
|
session._awaitingTurnResult = false;
|
|
1115
1115
|
session._queuedTurnCount = 0;
|
|
@@ -1360,6 +1360,15 @@ function createSDKBridge(opts) {
|
|
|
1360
1360
|
// Claude: conversation rollback is handled by rewindFiles + local history trim
|
|
1361
1361
|
}
|
|
1362
1362
|
|
|
1363
|
+
function forceProvider(session) {
|
|
1364
|
+
var vendor = session.vendor || sm.defaultVendor || adapter && adapter.vendor;
|
|
1365
|
+
return adapters[vendor] || (adapter && adapter.vendor === vendor ? adapter : vendor);
|
|
1366
|
+
}
|
|
1367
|
+
sm.getForceStopSupport = function (session) {
|
|
1368
|
+
if (session._queryStarting) return { supported: false, reason: 'Force termination is unavailable while this runtime is starting' };
|
|
1369
|
+
return require('./yoke/force-termination').support(forceProvider(session), session.queryInstance);
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1363
1372
|
function getAdapterForSession(session) {
|
|
1364
1373
|
var vendor = session.vendor || sm.defaultVendor || "claude";
|
|
1365
1374
|
return adapters[vendor] || adapter;
|
|
@@ -1405,8 +1414,9 @@ function createSDKBridge(opts) {
|
|
|
1405
1414
|
var payload = { text: text, images: images, linuxUser: linuxUser, beforePush: beforePush };
|
|
1406
1415
|
var recoveryState = responseRecovery.begin(session, payload, retryState);
|
|
1407
1416
|
if (recoveryState) recoveryState.handle = null;
|
|
1417
|
+
var startRevision = session._forceStopRevision || 0;
|
|
1408
1418
|
var guardedPush = function() {
|
|
1409
|
-
return !session.destroying && !session.taskStopRequested
|
|
1419
|
+
return (session._forceStopRevision || 0) === startRevision && !session.destroying && !session.taskStopRequested
|
|
1410
1420
|
&& (!recoveryState || responseRecovery.valid(session, recoveryState) && !recoveryState.stopping)
|
|
1411
1421
|
&& (typeof beforePush !== "function" || beforePush() === true);
|
|
1412
1422
|
};
|
|
@@ -1417,6 +1427,7 @@ function createSDKBridge(opts) {
|
|
|
1417
1427
|
if (recoveryState) recoveryState.starting = starting;
|
|
1418
1428
|
try {
|
|
1419
1429
|
var accepted = await starting;
|
|
1430
|
+
if ((session._forceStopRevision || 0) !== startRevision) return false;
|
|
1420
1431
|
if (accepted === false && (!recoveryState || !recoveryState.stopping)) {
|
|
1421
1432
|
responseRecovery.cancel(session);
|
|
1422
1433
|
session.isProcessing = false;
|
|
@@ -1424,6 +1435,7 @@ function createSDKBridge(opts) {
|
|
|
1424
1435
|
}
|
|
1425
1436
|
return accepted;
|
|
1426
1437
|
} catch (error) {
|
|
1438
|
+
if ((session._forceStopRevision || 0) !== startRevision) return false;
|
|
1427
1439
|
// Includes synchronous input queue failures, before stream consumption.
|
|
1428
1440
|
var failedHandle = session.queryInstance;
|
|
1429
1441
|
if (failedHandle) {
|
|
@@ -1446,6 +1458,7 @@ function createSDKBridge(opts) {
|
|
|
1446
1458
|
}
|
|
1447
1459
|
return false;
|
|
1448
1460
|
} finally {
|
|
1461
|
+
if ((session._forceStopRevision || 0) !== startRevision) return false;
|
|
1449
1462
|
session._queryStarting = false;
|
|
1450
1463
|
delete session._providerRecoveryWaiting;
|
|
1451
1464
|
if (!session.queryInstance && session.pendingPush && session.pendingPush.length) session.pendingPush = [];
|
|
@@ -1580,11 +1593,18 @@ function createSDKBridge(opts) {
|
|
|
1580
1593
|
}
|
|
1581
1594
|
}
|
|
1582
1595
|
}
|
|
1596
|
+
if (typeof beforePush === "function" && beforePush() !== true) return false;
|
|
1583
1597
|
// If still not available after lazy check, send auth_required
|
|
1584
1598
|
if (session.vendor && !adapters[session.vendor]) {
|
|
1585
1599
|
var missingVendorInfo = yoke.getVendorInfo(session.vendor);
|
|
1586
1600
|
var vendorName = (missingVendorInfo && missingVendorInfo.displayName)
|
|
1587
1601
|
|| session.vendor.charAt(0).toUpperCase() + session.vendor.slice(1);
|
|
1602
|
+
var installedForIdentity = linuxUser ? require('./yoke/discovery').installed({ user: linuxUser, env: sessionRuntimeEnv }) : yoke.checkInstalled();
|
|
1603
|
+
if (!installedForIdentity[session.vendor]) {
|
|
1604
|
+
sendAndRecord(session, { type: 'error', text: vendorName + ' is not installed for this runtime. Install the provider before retrying the task.' });
|
|
1605
|
+
sendAndRecord(session, { type: 'done', code: 1 });
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1588
1608
|
var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
|
|
1589
1609
|
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
1590
1610
|
var canAutoLogin = !usersModule.isMultiUser()
|
|
@@ -1650,6 +1670,7 @@ function createSDKBridge(opts) {
|
|
|
1650
1670
|
if (typeof preparation === "function") preparation(linuxUser, session, cwd);
|
|
1651
1671
|
}
|
|
1652
1672
|
|
|
1673
|
+
if (typeof beforePush === "function" && beforePush() !== true) return false;
|
|
1653
1674
|
session.blocks = {};
|
|
1654
1675
|
session.sentToolResults = {};
|
|
1655
1676
|
taskLifecycle.clearAll(session);
|
|
@@ -1882,6 +1903,7 @@ function createSDKBridge(opts) {
|
|
|
1882
1903
|
// the SDK ignores Options.title in favor of the persisted title.
|
|
1883
1904
|
var initialTitle = null;
|
|
1884
1905
|
var initialTitleProvisional = false;
|
|
1906
|
+
if (typeof beforePush === "function" && beforePush() !== true) return false;
|
|
1885
1907
|
if (!session.cliSessionId && !session.titleManuallySet && !session.titleAutoGenerated) {
|
|
1886
1908
|
var initialTitleChoice = sessionTitlePolicy.selectInitialTitle(session, text);
|
|
1887
1909
|
initialTitle = initialTitleChoice.title;
|
|
@@ -2014,6 +2036,15 @@ function createSDKBridge(opts) {
|
|
|
2014
2036
|
console.log("[sdk-bridge] calling adapter.createQuery... vendor=" + sessionAdapter.vendor);
|
|
2015
2037
|
try {
|
|
2016
2038
|
handle = await sessionAdapter.createQuery(queryOpts);
|
|
2039
|
+
if (Number(session._sdkQueryGeneration || 0) !== queryGeneration || !(recoveryState && recoveryState.stopping) && typeof beforePush === "function" && beforePush() !== true) {
|
|
2040
|
+
try { if (handle.abort) Promise.resolve(handle.abort()).catch(function () {}); } catch (e) {}
|
|
2041
|
+
try { if (handle.close) Promise.resolve(handle.close()).catch(function () {}); } catch (e) {}
|
|
2042
|
+
if (Number(session._sdkQueryGeneration || 0) === queryGeneration) {
|
|
2043
|
+
sendAndRecord(session, { type: "info", text: "Stopped before sending your message." });
|
|
2044
|
+
sendAndRecord(session, { type: "done", code: 0 });
|
|
2045
|
+
}
|
|
2046
|
+
return false;
|
|
2047
|
+
}
|
|
2017
2048
|
// A guarded delivery must still own the exact startup after provider
|
|
2018
2049
|
// creation. Never replace a newer handle or publish to a deleted target.
|
|
2019
2050
|
if (beforePush && beforePush.exactProductDelivery) {
|
|
@@ -2046,7 +2077,7 @@ function createSDKBridge(opts) {
|
|
|
2046
2077
|
console.error("[sdk-bridge] Failed to create query for session " + session.localId + ":", e.message || e);
|
|
2047
2078
|
console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
|
|
2048
2079
|
console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
|
|
2049
|
-
if (
|
|
2080
|
+
if (sm.sessions.get(session.localId) !== session || session.queryInstance !== startupHandle || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
|
|
2050
2081
|
session.isProcessing = false;
|
|
2051
2082
|
session._awaitingTurnResult = false;
|
|
2052
2083
|
session._queuedTurnCount = 0;
|
|
@@ -2110,7 +2141,8 @@ function createSDKBridge(opts) {
|
|
|
2110
2141
|
if (typeof beforePush === "function" && beforePush() !== true) {
|
|
2111
2142
|
try { if (typeof handle.abort === "function") handle.abort(); } catch (e) {}
|
|
2112
2143
|
try { handle.close(); } catch (e) {}
|
|
2113
|
-
if (session.queryInstance
|
|
2144
|
+
if (session.queryInstance !== handle || Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
|
|
2145
|
+
session.queryInstance = null;
|
|
2114
2146
|
session.messageQueue = null;
|
|
2115
2147
|
session.abortController = null;
|
|
2116
2148
|
session.isProcessing = false;
|
|
@@ -2735,6 +2767,8 @@ function createSDKBridge(opts) {
|
|
|
2735
2767
|
}
|
|
2736
2768
|
|
|
2737
2769
|
return {
|
|
2770
|
+
getForceStopSupport: sm.getForceStopSupport,
|
|
2771
|
+
forceStopSession: require("./sdk-force-stop").createForceStop({ sm: sm, provider: forceProvider, record: sendAndRecord, changed: onProcessingChanged }),
|
|
2738
2772
|
createMessageQueue: createMessageQueue,
|
|
2739
2773
|
processSDKMessage: processSDKMessage,
|
|
2740
2774
|
checkToolWhitelist: checkToolWhitelist,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Retire one exact query and invoke only its declared YOKE force operation.
|
|
2
|
+
function createForceStop(ctx) {
|
|
3
|
+
return async function forceStop(session) {
|
|
4
|
+
if (ctx.sm.sessions.get(session.localId) !== session) throw new Error('Session was deleted or replaced.');
|
|
5
|
+
if (session._queryStarting) throw new Error('Force termination is unavailable while this runtime is starting');
|
|
6
|
+
var terminate = require("./yoke/force-termination").operation(ctx.provider(session), session.queryInstance);
|
|
7
|
+
var undelivered = session.pendingPush || [];
|
|
8
|
+
session._forceStopRevision = (session._forceStopRevision || 0) + 1;
|
|
9
|
+
session._sdkQueryGeneration = Number(session._sdkQueryGeneration || 0) + 1;
|
|
10
|
+
session._queryGeneration = (session._queryGeneration || 0) + 1;
|
|
11
|
+
session.taskStopRequested = true;
|
|
12
|
+
session._pendingMessageDrainPaused = true;
|
|
13
|
+
session.queryInstance = null;
|
|
14
|
+
session.abortController = null;
|
|
15
|
+
session.messageQueue = null;
|
|
16
|
+
session._queryStarting = false;
|
|
17
|
+
session._responseRecoveryStopPending = null;
|
|
18
|
+
session.isProcessing = false;
|
|
19
|
+
session._awaitingTurnResult = false;
|
|
20
|
+
session._queuedTurnCount = 0;
|
|
21
|
+
session.pendingPush = [];
|
|
22
|
+
delete session._foregroundAdmission;
|
|
23
|
+
delete session._providerRecoveryWaiting;
|
|
24
|
+
session._lastTurnInterrupted = true;
|
|
25
|
+
var probe = session._continuationStartupProbe;
|
|
26
|
+
if (probe && probe.timer) clearTimeout(probe.timer);
|
|
27
|
+
delete session._continuationStartupProbe;
|
|
28
|
+
if (probe && typeof probe.resolve === 'function') probe.resolve({ accepted: false, reason: 'Force stopped.' });
|
|
29
|
+
var permissions = session.pendingPermissions || {};
|
|
30
|
+
Object.keys(permissions).forEach(function (id) {
|
|
31
|
+
if (typeof permissions[id].resolve === 'function') permissions[id].resolve({ behavior: 'deny', message: 'Force stopped.' });
|
|
32
|
+
if (ctx.sm.permissionRequestIndex) delete ctx.sm.permissionRequestIndex[id];
|
|
33
|
+
});
|
|
34
|
+
var questions = session.pendingAskUser || {};
|
|
35
|
+
Object.keys(questions).forEach(function (id) {
|
|
36
|
+
var respond = questions[id] && questions[id].respond;
|
|
37
|
+
if (respond && typeof respond.cancel === 'function') respond.cancel('Force stopped.');
|
|
38
|
+
});
|
|
39
|
+
session.pendingPermissions = {};
|
|
40
|
+
session.pendingAskUser = {};
|
|
41
|
+
var dialogs = session.pendingUserDialogs || {};
|
|
42
|
+
Object.keys(dialogs).forEach(function (id) { if (typeof dialogs[id].resolve === 'function') dialogs[id].resolve({ behavior: 'cancelled' }); });
|
|
43
|
+
var elicitations = session.pendingElicitations || {};
|
|
44
|
+
Object.keys(elicitations).forEach(function (id) { if (typeof elicitations[id].resolve === 'function') elicitations[id].resolve({ action: 'cancel' }); });
|
|
45
|
+
session.pendingUserDialogs = {};
|
|
46
|
+
session.pendingElicitations = {};
|
|
47
|
+
var revision = session._forceStopRevision, generation = session._sdkQueryGeneration, owner = session.ownerId, vendor = session.vendor;
|
|
48
|
+
ctx.changed(); ctx.sm.broadcastSessionList();
|
|
49
|
+
var outcome;
|
|
50
|
+
try { outcome = await terminate(); }
|
|
51
|
+
catch (error) { outcome = { status: 'unconfirmed', externalEffects: 'unconfirmed' }; }
|
|
52
|
+
if (ctx.sm.sessions.get(session.localId) !== session || session._forceStopRevision !== revision || session._sdkQueryGeneration !== generation || session.ownerId !== owner || session.vendor !== vendor || session.queryInstance || session._queryStarting) return { status: 'failed', runtimeEffect: 'connection_retired', providerStop: outcome.status, externalEffects: 'unconfirmed', queuePaused: true, detail: 'Session was replaced during termination.' };
|
|
53
|
+
ctx.changed();
|
|
54
|
+
if (undelivered.length) ctx.record(session, { type: 'error', text: 'Buffered messages were not replayed after force stop. Review them before sending again.', recoveryAdmission: { state: 'not_sent', messages: undelivered.map(function (item) { return { text: item.text, images: item.images || null }; }) } });
|
|
55
|
+
ctx.record(session, { type: 'thinking_stop' });
|
|
56
|
+
ctx.record(session, { type: 'info', text: (outcome.status === 'terminated' ? 'The query runtime exited.' : 'Query runtime termination could not be confirmed.') + ' Queued messages are paused. Commands or external requests already sent may still be running; inspect their outcome before continuing.' });
|
|
57
|
+
ctx.record(session, { type: 'done', code: outcome.status === 'terminated' ? 0 : 1 });
|
|
58
|
+
ctx.sm.broadcastSessionList();
|
|
59
|
+
return { status: outcome.status === 'terminated' ? 'completed' : 'failed', runtimeEffect: 'connection_retired', providerStop: outcome.status, externalEffects: 'unconfirmed', queuePaused: true };
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
module.exports = { createForceStop: createForceStop };
|
|
@@ -81,8 +81,9 @@ function attachMessageProcessor(ctx) {
|
|
|
81
81
|
// per session to avoid spamming when multiple auth errors arrive in a burst.
|
|
82
82
|
function emitAuthRequired(session) {
|
|
83
83
|
var now = Date.now();
|
|
84
|
-
if (session._lastAuthEmit && (now - session._lastAuthEmit) < 5000) return;
|
|
84
|
+
if (session._lastAuthQuery === session._queryGeneration && session._lastAuthEmit && (now - session._lastAuthEmit) < 5000) return;
|
|
85
85
|
session._lastAuthEmit = now;
|
|
86
|
+
session._lastAuthQuery = session._queryGeneration;
|
|
86
87
|
|
|
87
88
|
var vendor = session.vendor || "claude";
|
|
88
89
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// HTTP keeps the observed result reachable after the target project's sockets close.
|
|
2
|
+
var crypto = require('crypto');
|
|
3
|
+
function attachWorktreeHttp(ctx) {
|
|
4
|
+
function reply(res, code, data) { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(data)); }
|
|
5
|
+
function handleRequest(req, res, url) {
|
|
6
|
+
if (url !== '/api/worktree-removal/preview' && url !== '/api/worktree-removal/execute') return false;
|
|
7
|
+
if (!ctx.isRequestAuthed(req)) { reply(res, 401, { ok: false, error: 'Authentication required.' }); return true; }
|
|
8
|
+
if (!ctx.service) { reply(res, 503, { ok: false, error: 'Worktree removal is unavailable in this server runtime.' }); return true; }
|
|
9
|
+
if (req.method !== 'POST' || req.headers['x-clay-worktree-control'] !== '1' ||
|
|
10
|
+
(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json' ||
|
|
11
|
+
req.headers['sec-fetch-site'] && ['same-origin', 'none'].indexOf(req.headers['sec-fetch-site']) < 0) {
|
|
12
|
+
reply(res, 403, { ok: false, error: 'Use the worktree menu in Clay Studio.' }); return true;
|
|
13
|
+
}
|
|
14
|
+
var actor = ctx.getMultiUserFromReq(req), userId = actor && actor.id || null;
|
|
15
|
+
function validate() {
|
|
16
|
+
var current = ctx.getMultiUserFromReq(req);
|
|
17
|
+
if (!ctx.isRequestAuthed(req) || (current && current.id || null) !== userId) throw new Error('The authenticated owner session changed.');
|
|
18
|
+
}
|
|
19
|
+
var connection = crypto.createHash('sha256').update(req.headers.cookie || '').digest('hex');
|
|
20
|
+
var body = '', bytes = 0, oversized = false;
|
|
21
|
+
req.setEncoding('utf8');
|
|
22
|
+
req.on('data', function (chunk) { bytes += Buffer.byteLength(chunk); if (bytes > 4096) { if (!oversized) reply(res, 413, { ok: false, error: 'Request is too large.' }); oversized = true; } else body += chunk; });
|
|
23
|
+
req.on('end', function () {
|
|
24
|
+
if (oversized) return;
|
|
25
|
+
Promise.resolve().then(function () {
|
|
26
|
+
validate(); var data = JSON.parse(body);
|
|
27
|
+
return url.endsWith('/preview') ? ctx.service.preview(data.slug, data.action, userId, connection, data.deleteBranch) : ctx.service.execute(data.token, userId, connection, validate, data.deleteBranch);
|
|
28
|
+
}).then(function (result) { reply(res, 200, result); }, function (error) { reply(res, 409, Object.assign({ ok: false, error: error.message }, error.lifecycleResult || {})); });
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return { handleRequest: handleRequest };
|
|
33
|
+
}
|
|
34
|
+
module.exports = { attachWorktreeHttp: attachWorktreeHttp };
|
package/lib/server.js
CHANGED
|
@@ -636,6 +636,10 @@ function createServer(opts) {
|
|
|
636
636
|
res.setHeader("Content-Security-Policy", securityHeaders["Content-Security-Policy"] + " frame-src " + frameSrcFor(req) + ";");
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
var worktreeHttp = require("./server-worktree-controls").attachWorktreeHttp({
|
|
640
|
+
service: opts.worktreeControls, isRequestAuthed: isRequestAuthed, getMultiUserFromReq: getMultiUserFromReq,
|
|
641
|
+
});
|
|
642
|
+
|
|
639
643
|
// --- HTTP handler ---
|
|
640
644
|
var appHandler = function (req, res) {
|
|
641
645
|
setSecurityHeaders(res, req);
|
|
@@ -899,6 +903,7 @@ function createServer(opts) {
|
|
|
899
903
|
if (linear.handleRequest(req, res, fullUrl)) return;
|
|
900
904
|
if (speech.handleRequest(req, res, fullUrl)) return;
|
|
901
905
|
if (githubStars.handleRequest(req, res, fullUrl)) return;
|
|
906
|
+
if (worktreeHttp.handleRequest(req, res, fullUrl)) return;
|
|
902
907
|
if (networkSettings.handleRequest(req, res, fullUrl)) return;
|
|
903
908
|
if (storageSettings.handleRequest(req, res, fullUrl)) return;
|
|
904
909
|
if (settings.handleRequest(req, res, fullUrl)) return;
|
|
@@ -1475,7 +1480,7 @@ function createServer(opts) {
|
|
|
1475
1480
|
var mate = mates.getMate(mates.buildMateCtx(users.isMultiUser() ? projectOwnerId : null), extra.mateId || path.basename(cwd));
|
|
1476
1481
|
return { isMate: true, visibility: "private", ownerId: projectOwnerId || null, allowedUsers: [], lifecycleExecutionPaused: !mate || !!mate.lifecycleDeletion };
|
|
1477
1482
|
}
|
|
1478
|
-
return onGetProjectAccess ? onGetProjectAccess(slug) : { visibility: "public", ownerId: projectOwnerId || null };
|
|
1483
|
+
return onGetProjectAccess ? onGetProjectAccess(slug, { slug: slug, path: cwd, changeSetId: worktreeMeta && worktreeMeta.changeSetId }) : { visibility: "public", ownerId: projectOwnerId || null };
|
|
1479
1484
|
},
|
|
1480
1485
|
shouldSuppressResponseNotification: function (session) {
|
|
1481
1486
|
return shouldSuppressHomeResponse(projects, slug, session);
|
|
@@ -2118,6 +2123,11 @@ function createServer(opts) {
|
|
|
2118
2123
|
destroyAll: destroyAll,
|
|
2119
2124
|
durableScheduler: durableScheduler,
|
|
2120
2125
|
setProjectLogContextState: setProjectLogContextState,
|
|
2126
|
+
captureWorktreeLogContext: function (slug) {
|
|
2127
|
+
var project = projects.get(slug), status = project && project.getStatus();
|
|
2128
|
+
if (!status || !status.isWorktree) throw new Error('Worktree context is unavailable.');
|
|
2129
|
+
return function (state) { return projectLogsService.setContextState(slug, state, status); };
|
|
2130
|
+
},
|
|
2121
2131
|
};
|
|
2122
2132
|
}
|
|
2123
2133
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
var provenance = require('./session-provenance');
|
|
2
|
+
var termination = require('./yoke/force-termination');
|
|
3
|
+
function targets(sm, session) {
|
|
4
|
+
return [session].concat(provenance.isWorker(session) ? [] : provenance.ownedWorkersByDriver(Array.from(sm.sessions.values()))[session.localId] || []);
|
|
5
|
+
}
|
|
6
|
+
function one(sm, session) {
|
|
7
|
+
if (session._queryStarting) return { supported: false, reason: 'Force termination is unavailable while this runtime is starting' };
|
|
8
|
+
return typeof sm.getForceStopSupport === 'function' ? sm.getForceStopSupport(session) : termination.support(session.vendor || sm.defaultVendor, session.queryInstance);
|
|
9
|
+
}
|
|
10
|
+
function describe(sm, session) {
|
|
11
|
+
var scope = targets(sm, session), unavailable = scope.find(function (target) { return !one(sm, target).supported; });
|
|
12
|
+
if (unavailable) return { supported: false, reason: one(sm, unavailable).reason + (unavailable === session ? '' : ' (owned Worker)') };
|
|
13
|
+
return { supported: true, scope: 'query-runtime', externalEffects: 'unconfirmed' };
|
|
14
|
+
}
|
|
15
|
+
module.exports = { targets: targets, one: one, describe: describe };
|
package/lib/sessions.js
CHANGED
|
@@ -634,6 +634,7 @@ function createSessionManager(opts) {
|
|
|
634
634
|
var node = hierarchy[session.localId];
|
|
635
635
|
var ownedCount = ownedWorkerCountFor(session, node, owned, viewerId);
|
|
636
636
|
if (ownedCount !== undefined) mapped.ownedWorkerCount = ownedCount;
|
|
637
|
+
mapped.forceTermination = ownedCount !== undefined && (!users.isMultiUser() || viewerId && session.ownerId === viewerId) ? require('./session-force-stop-support').describe(api || { sessions: sessions, defaultVendor: defaultVendor }, session) : { supported: false, reason: 'Force termination requires the session owner' };
|
|
637
638
|
mapped.sessionRole = node.role;
|
|
638
639
|
mapped.parentSessionId = node.parentSessionId;
|
|
639
640
|
mapped.parentAvailable = node.parentAvailable;
|
|
@@ -1058,6 +1059,7 @@ function createSessionManager(opts) {
|
|
|
1058
1059
|
}
|
|
1059
1060
|
|
|
1060
1061
|
function doSendToSession(session, obj) {
|
|
1062
|
+
if (obj.type === 'auth_required' && opts.onAuthRequired && opts.onAuthRequired(session, obj)) return;
|
|
1061
1063
|
// Send to active clients without recording to history/disk (ephemeral data)
|
|
1062
1064
|
if (sendEach) {
|
|
1063
1065
|
var data = JSON.stringify(obj);
|
|
@@ -1074,12 +1076,14 @@ function createSessionManager(opts) {
|
|
|
1074
1076
|
}
|
|
1075
1077
|
|
|
1076
1078
|
function doSendAndRecord(session, obj, clientObj) {
|
|
1079
|
+
if (obj.type === 'auth_required') { doSendToSession(session, clientObj || obj); return true; }
|
|
1077
1080
|
// Stamp every recorded message so history replay preserves original times
|
|
1078
1081
|
if (!obj._ts) obj._ts = Date.now();
|
|
1079
1082
|
var outgoingObj = clientObj || obj;
|
|
1080
1083
|
if (!outgoingObj._ts) outgoingObj._ts = obj._ts;
|
|
1081
1084
|
session.history.push(obj);
|
|
1082
1085
|
if (appendToSessionFile(session, obj) === false) return false;
|
|
1086
|
+
require('./worker-activity').observe(session, outgoingObj);
|
|
1083
1087
|
// Per-session out-of-band subscribers mirror mate session events into
|
|
1084
1088
|
// a parallel UI without joining the project's
|
|
1085
1089
|
// ws clients set). Subscribers receive the same obj that goes to ws
|
|
@@ -1394,7 +1398,7 @@ function createSessionManager(opts) {
|
|
|
1394
1398
|
}).catch(function() {});
|
|
1395
1399
|
}
|
|
1396
1400
|
|
|
1397
|
-
|
|
1401
|
+
var api = {
|
|
1398
1402
|
setSessionHydrator: function (fn) { onSessionHydrate = typeof fn === "function" ? fn : null; },
|
|
1399
1403
|
get activeSessionId() { return activeSessionId; },
|
|
1400
1404
|
get nextLocalId() { return nextLocalId; },
|
|
@@ -1567,6 +1571,7 @@ function createSessionManager(opts) {
|
|
|
1567
1571
|
},
|
|
1568
1572
|
permissionRequestIndex: permissionRequestIndex,
|
|
1569
1573
|
};
|
|
1574
|
+
return api;
|
|
1570
1575
|
}
|
|
1571
1576
|
|
|
1572
1577
|
module.exports = { createSessionManager };
|
package/lib/terminal-manager.js
CHANGED
|
@@ -163,7 +163,7 @@ function createTerminalManager(opts) {
|
|
|
163
163
|
|
|
164
164
|
function write(id, data) {
|
|
165
165
|
var session = terminals.get(id);
|
|
166
|
-
if (session && session.pty) {
|
|
166
|
+
if (session && session.pty && session.kind !== "provider-install") {
|
|
167
167
|
session.lastActivityAt = Date.now(); // user input counts as activity
|
|
168
168
|
session.pty.write(data);
|
|
169
169
|
}
|
package/lib/terminal.js
CHANGED
|
@@ -43,12 +43,24 @@ function createTerminal(cwd, cols, rows, osUserInfo, opts) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
var args = osUserInfo ? ["-l"] : [];
|
|
46
|
+
if (opts && opts.providerInstall) {
|
|
47
|
+
var recipe = require('./yoke/vendor-registry').getVendorInfo(opts.providerInstall);
|
|
48
|
+
recipe = recipe && recipe.installRecipe;
|
|
49
|
+
var reason = require('./yoke/provider-install-recipes').eligible(recipe,osUserInfo);
|
|
50
|
+
if (reason || typeof opts.revalidateInstall !== 'function') throw new Error(reason || 'Installation authority is missing.');
|
|
51
|
+
opts.revalidateInstall();
|
|
52
|
+
shell = '/bin/bash';
|
|
53
|
+
args = ['--noprofile','--norc','-o','pipefail','-c',recipe.command];
|
|
54
|
+
spawnOpts.cwd = osUserInfo.home;
|
|
55
|
+
// Bash must not source daemon/user startup files; installers receive no keys.
|
|
56
|
+
delete termEnv.NODE_OPTIONS;
|
|
57
|
+
}
|
|
46
58
|
// Route through setpriv when group inheritance is enabled so the terminal
|
|
47
59
|
// gets the user's supplementary groups (docker, etc.), not just primary gid.
|
|
48
60
|
var ptySpawn = require("./os-users").wrapSpawnAsUser(shell, args, spawnOpts);
|
|
49
61
|
var term = pty.spawn(ptySpawn.command, ptySpawn.args, ptySpawn.options);
|
|
50
62
|
|
|
51
|
-
if (opts && opts.initialInput) {
|
|
63
|
+
if (opts && opts.initialInput && !opts.providerInstall) {
|
|
52
64
|
try { term.write(opts.initialInput); } catch (e) {}
|
|
53
65
|
}
|
|
54
66
|
|
|
@@ -3,7 +3,7 @@ var createLoginBrowser = require("./vendor-login-browser").createLoginBrowser;
|
|
|
3
3
|
function attachBrowserLogin(ctx) {
|
|
4
4
|
function allowed(ws, flow) {
|
|
5
5
|
var userId = ws && ws._clayUser ? String(ws._clayUser.id) : null;
|
|
6
|
-
return userId === flow.ownerId && ctx.hasTerminalPermission(ws);
|
|
6
|
+
return userId === flow.ownerId && ctx.hasTerminalPermission(ws) && (!ctx.validate || ctx.validate(ws, flow));
|
|
7
7
|
}
|
|
8
8
|
function prepare(ws, flow, identity) {
|
|
9
9
|
if (ctx.osUsers && !identity.osUserInfo) throw new Error("OS user identity is unavailable");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Live recorded output only. Reading history or projecting a snapshot never stamps activity.
|
|
2
|
+
var crypto = require('crypto');
|
|
3
|
+
var provenance = require('./session-provenance');
|
|
4
|
+
var tools = { Bash: 'Running a command', Read: 'Reading files', Edit: 'Editing files', Write: 'Writing files',
|
|
5
|
+
Grep: 'Searching files', Glob: 'Finding files', WebSearch: 'Searching the web', WebFetch: 'Reading a web page', Task: 'Delegating work' };
|
|
6
|
+
function summary(event) {
|
|
7
|
+
if (!event) return null;
|
|
8
|
+
if (event.type === 'delta' || event.type === 'assistant_message') return typeof event.text === 'string' && event.text.trim() ? 'Writing a reply' : null;
|
|
9
|
+
if (event.type === 'thinking_delta') return typeof event.text === 'string' && event.text.trim() ? 'Thinking' : null;
|
|
10
|
+
if (event.type === 'thinking_start') return 'Thinking';
|
|
11
|
+
if (event.type === 'tool_start' || event.type === 'tool_executing') return Object.prototype.hasOwnProperty.call(tools, event.name) ? tools[event.name] : 'Using a tool';
|
|
12
|
+
if (event.type === 'tool_result') return 'Tool result received';
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function observe(session, event) {
|
|
16
|
+
if (!provenance.isWorker(session)) return;
|
|
17
|
+
var label = summary(event); if (!label) return;
|
|
18
|
+
var previous = session._workerActivity;
|
|
19
|
+
session._workerActivity = { epoch: previous ? previous.epoch : crypto.randomUUID(), sequence: previous ? previous.sequence + 1 : 1,
|
|
20
|
+
queryGeneration: Number(session._sdkQueryGeneration || 0), timestamp: Date.now(), summary: label };
|
|
21
|
+
}
|
|
22
|
+
function project(session) {
|
|
23
|
+
var activity = session._workerActivity;
|
|
24
|
+
return activity && activity.queryGeneration === Number(session._sdkQueryGeneration || 0) ? Object.assign({}, activity) : null;
|
|
25
|
+
}
|
|
26
|
+
module.exports = { observe: observe, project: project, summary: summary };
|
|
@@ -29,16 +29,21 @@ function attach(ctx) {
|
|
|
29
29
|
function send(live) {
|
|
30
30
|
var current = ctx.resolve(ws, target);
|
|
31
31
|
if (subscriptions.get(ws) !== state || ws.readyState !== 1 || !current || current.driver !== pair.driver || current.worker !== pair.worker) { if (entry.release) entry.release(); return; }
|
|
32
|
-
var preview = latest(pair.worker.history || [])
|
|
33
|
-
preview.vendor = pair.worker.vendor || null;
|
|
34
|
-
|
|
35
|
-
entry.last
|
|
32
|
+
var preview = latest(pair.worker.history || []), activity = require('./worker-activity').project(pair.worker);
|
|
33
|
+
if (preview) preview.vendor = pair.worker.vendor || null;
|
|
34
|
+
var signature = JSON.stringify([preview, activity, pair.worker.isProcessing === true]);
|
|
35
|
+
if (entry.last === signature) return;
|
|
36
|
+
entry.last = signature;
|
|
36
37
|
ctx.send(ws, { type: 'split_worker_message_preview', requestId: msg.requestId, parentSessionOriginId: target.parentSessionOriginId,
|
|
37
|
-
workerSessionOriginId: target.workerSessionOriginId, generation: target.generation, live: live, preview: preview });
|
|
38
|
+
workerSessionOriginId: target.workerSessionOriginId, generation: target.generation, live: live, preview: preview, activity: activity, isProcessing: pair.worker.isProcessing === true, observedAt: Date.now(), vendor: pair.worker.vendor || null });
|
|
38
39
|
}
|
|
39
40
|
entry.release = ctx.sm.subscribeSession(pair.worker.localId, function (event) {
|
|
40
|
-
if (!event
|
|
41
|
-
|
|
41
|
+
if (!event) return;
|
|
42
|
+
var genuine = !!require('./worker-activity').summary(event);
|
|
43
|
+
if (!genuine && event.type !== 'done' && event.type !== 'error') return;
|
|
44
|
+
entry.pendingLive = entry.pendingLive || genuine;
|
|
45
|
+
if (entry.timer) return;
|
|
46
|
+
entry.timer = setTimeout(function () { entry.timer = null; var live = !!entry.pendingLive; entry.pendingLive = false; send(live); }, 60);
|
|
42
47
|
});
|
|
43
48
|
state.entries.push(entry); send(false);
|
|
44
49
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Optional local-ref removal, bound to a reviewed tip and retained parent HEAD.
|
|
2
|
+
var identities = require('./worktree-identity');
|
|
3
|
+
var git = require('./git-cli').runGitSync;
|
|
4
|
+
var refTransaction = require('./worktree-ref-transaction');
|
|
5
|
+
function run(snapshot, args, os, options) { return git(snapshot.parent.path, args, options || null, os).trim(); }
|
|
6
|
+
function symbolic(snapshot, ref, os) {
|
|
7
|
+
try { return run(snapshot, ['symbolic-ref', '--quiet', ref], os); }
|
|
8
|
+
catch (error) { if (error.status === 1) return null; throw error; }
|
|
9
|
+
}
|
|
10
|
+
function inputFor(snapshot, branch, os, deleting) {
|
|
11
|
+
var input = 'start\noption no-deref\n';
|
|
12
|
+
// Git cannot verify HEAD's symref and its referent in one transaction.
|
|
13
|
+
if (symbolic(snapshot, 'HEAD', os) !== branch.parentRef) throw new Error('The retained parent HEAD/ref changed.');
|
|
14
|
+
input += 'verify ' + (branch.parentRef || 'HEAD') + ' ' + branch.parentHead + '\n';
|
|
15
|
+
input += 'option no-deref\n' + (deleting ? 'delete ' : 'verify ') + branch.ref + ' ' + branch.tip + '\nprepare\n';
|
|
16
|
+
// No dereferencing: a replacement symbolic ref must never delete its target.
|
|
17
|
+
return input;
|
|
18
|
+
}
|
|
19
|
+
function inspect(snapshot, os, removed, locked) {
|
|
20
|
+
if (!/^refs\/heads\//.test(snapshot.branch)) throw new Error('Detached HEAD has no local branch to delete.');
|
|
21
|
+
if (JSON.stringify(identities.identity(snapshot.parent.path, true)) !== JSON.stringify(snapshot.parent) ||
|
|
22
|
+
JSON.stringify(identities.identity(snapshot.common.path, true)) !== JSON.stringify(snapshot.common)) throw new Error('The retained repository identity changed.');
|
|
23
|
+
var ref = snapshot.branch;
|
|
24
|
+
run(snapshot, ['check-ref-format', ref], os);
|
|
25
|
+
if (symbolic(snapshot, ref, os)) throw new Error('The local branch was replaced by a symbolic ref.');
|
|
26
|
+
var tip = run(snapshot, ['show-ref', '--verify', '--hash', ref], os);
|
|
27
|
+
var parentRef = symbolic(snapshot, 'HEAD', os);
|
|
28
|
+
var parentHead = run(snapshot, ['rev-parse', '--verify', 'HEAD'], os);
|
|
29
|
+
if (parentRef === ref) throw new Error('The local branch is the current parent branch.');
|
|
30
|
+
if (parentRef && symbolic(snapshot, parentRef, os)) throw new Error('The retained parent branch is a symbolic ref.');
|
|
31
|
+
var blocks = run(snapshot, ['worktree', 'list', '--porcelain', '-z'], os).split('\0\0').filter(Boolean);
|
|
32
|
+
blocks.forEach(function (block) {
|
|
33
|
+
var fields = block.split('\0');
|
|
34
|
+
if (fields.indexOf('branch ' + ref) >= 0 && (removed || fields[0] !== 'worktree ' + snapshot.target.path)) throw new Error('The local branch is checked out by another worktree.');
|
|
35
|
+
});
|
|
36
|
+
try { run(snapshot, ['merge-base', '--is-ancestor', tip, parentHead], os); }
|
|
37
|
+
catch (error) { throw new Error('The local branch is not merged into the retained parent HEAD. Keep the branch or merge it first.'); }
|
|
38
|
+
var branch = { ref: ref, tip: tip, parentRef: parentRef, parentHead: parentHead };
|
|
39
|
+
// Probe the fixed transaction syntax before runtime or directory effects.
|
|
40
|
+
if (!locked) run(snapshot, ['update-ref', '--stdin'], os, { input: inputFor(snapshot, branch, os, false) + 'commit\n' });
|
|
41
|
+
return branch;
|
|
42
|
+
}
|
|
43
|
+
function exact(snapshot, branch, os, removed, locked) {
|
|
44
|
+
if (JSON.stringify(inspect(snapshot, os, removed, locked)) !== JSON.stringify(branch)) throw new Error('The reviewed local branch tip or retained parent HEAD/ref changed. Review again.');
|
|
45
|
+
}
|
|
46
|
+
async function remove(snapshot, branch, os, validate) {
|
|
47
|
+
exact(snapshot, branch, os, true);
|
|
48
|
+
await refTransaction.remove(snapshot.parent.path, inputFor(snapshot, branch, os, true), os, function () {
|
|
49
|
+
if (validate) validate();
|
|
50
|
+
// CAS alone permits same-tip symbolic replacements. Inspect under the prepared locks.
|
|
51
|
+
exact(snapshot, branch, os, true, true);
|
|
52
|
+
});
|
|
53
|
+
var refs = run(snapshot, ['for-each-ref', '--format=%(refname)', branch.ref], os).split('\n');
|
|
54
|
+
if (refs.indexOf(branch.ref) >= 0) throw new Error('Local branch absence was not confirmed. Inspect it before recovery.');
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
module.exports = { inspect: inspect, exact: exact, remove: remove };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Exact Git and filesystem identities for reviewed linked-worktree removal.
|
|
2
|
+
var fs = require('fs');
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var git = require('./git-cli').runGitSync;
|
|
5
|
+
function identity(value, directory) {
|
|
6
|
+
var resolved = path.resolve(value), real = fs.realpathSync(resolved), stat = fs.lstatSync(resolved);
|
|
7
|
+
if (resolved !== real || stat.isSymbolicLink() || (directory && !stat.isDirectory())) throw new Error('Worktree paths must be real directories without symlink aliases.');
|
|
8
|
+
return { path: real, dev: stat.dev, ino: stat.ino, uid: stat.uid };
|
|
9
|
+
}
|
|
10
|
+
function inspect(parentPath, targetPath, osUserInfo, clean) {
|
|
11
|
+
var parent = identity(parentPath, true), target = identity(targetPath, true);
|
|
12
|
+
function run(root, args) { return git(root, args, null, osUserInfo).trim(); }
|
|
13
|
+
if (parent.path === target.path) throw new Error('The main repository cannot be deleted as a worktree.');
|
|
14
|
+
var common = identity(path.resolve(parent.path, run(parent.path, ['rev-parse', '--git-common-dir'])), true);
|
|
15
|
+
var parentGit = identity(run(parent.path, ['rev-parse', '--absolute-git-dir']), true);
|
|
16
|
+
if (JSON.stringify(common) !== JSON.stringify(parentGit)) throw new Error('Choose the main repository as the parent.');
|
|
17
|
+
if (run(parent.path, ['rev-parse', '--show-toplevel']) !== parent.path || run(target.path, ['rev-parse', '--show-toplevel']) !== target.path) throw new Error('The registered path is not an exact repository root.');
|
|
18
|
+
var targetCommon = identity(path.resolve(target.path, run(target.path, ['rev-parse', '--git-common-dir'])), true);
|
|
19
|
+
var targetGit = identity(run(target.path, ['rev-parse', '--absolute-git-dir']), true);
|
|
20
|
+
if (JSON.stringify(common) !== JSON.stringify(targetCommon) || targetGit.path === common.path || path.dirname(targetGit.path) !== path.join(common.path, 'worktrees')) throw new Error('The target is not a linked worktree of this parent.');
|
|
21
|
+
var marker = identity(path.join(target.path, '.git'), false);
|
|
22
|
+
if (!fs.lstatSync(marker.path).isFile() || fs.readFileSync(marker.path, 'utf8').trim() !== 'gitdir: ' + targetGit.path) throw new Error('Worktree Git binding is invalid.');
|
|
23
|
+
if (fs.readFileSync(path.join(targetGit.path, 'gitdir'), 'utf8').trim() !== marker.path) throw new Error('Worktree Git backlink changed.');
|
|
24
|
+
var records = git(parent.path, ['worktree', 'list', '--porcelain', '-z'], null, osUserInfo).split('\0\0').filter(Boolean);
|
|
25
|
+
var matches = records.filter(function (block) { return block.split('\0')[0] === 'worktree ' + target.path; });
|
|
26
|
+
if (matches.length !== 1) throw new Error('Exact Git worktree membership is unavailable.');
|
|
27
|
+
var fields = matches[0].split('\0');
|
|
28
|
+
if (fields.some(function (field) { return /^prunable(?: |$)|^bare$/.test(field) || clean && /^locked(?: |$)/.test(field); })) throw new Error('The worktree is locked, prunable or unavailable.');
|
|
29
|
+
if (clean && git(target.path, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignored=matching'], null, osUserInfo)) throw new Error('Worktree contains changed, untracked or ignored files. Save or remove them before deleting.');
|
|
30
|
+
return { parent: parent, target: target, common: common, git: targetGit, marker: marker, membership: matches[0], branch: (fields.find(function (field) { return field.indexOf('branch ') === 0; }) || 'branch (detached)').slice(7) };
|
|
31
|
+
}
|
|
32
|
+
function remove(snapshot, osUserInfo) {
|
|
33
|
+
var current = inspect(snapshot.parent.path, snapshot.target.path, osUserInfo, true);
|
|
34
|
+
if (JSON.stringify(current) !== JSON.stringify(snapshot)) throw new Error('Worktree identity changed after review.');
|
|
35
|
+
git(snapshot.parent.path, ['worktree', 'remove', '--', snapshot.target.path], { timeout: 15000 }, osUserInfo);
|
|
36
|
+
if (fs.existsSync(snapshot.target.path) || fs.existsSync(snapshot.git.path)) throw new Error('Git returned without confirming removal. Inspect the worktree before another attempt.');
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
module.exports = { identity: identity, inspect: inspect, remove: remove };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Worktree display preferences are parent-owned and bound to the Git change set.
|
|
2
|
+
function entry(config, parentSlug, slug) {
|
|
3
|
+
var parent = (config.projects || []).find(function (item) { return item.slug === parentSlug; });
|
|
4
|
+
return parent && parent.worktreePreferences && parent.worktreePreferences[slug];
|
|
5
|
+
}
|
|
6
|
+
function matches(record, targetPath, changeSetId) {
|
|
7
|
+
return !!record && record.path === targetPath && record.changeSetId === changeSetId;
|
|
8
|
+
}
|
|
9
|
+
function icon(config, parentSlug, slug, targetPath, changeSetId) {
|
|
10
|
+
var record = entry(config, parentSlug, slug);
|
|
11
|
+
return matches(record, targetPath, changeSetId) ? record.icon || null : null;
|
|
12
|
+
}
|
|
13
|
+
function blocked(config, parentSlug, slug, targetPath, changeSetId) {
|
|
14
|
+
var record = entry(config, parentSlug, slug);
|
|
15
|
+
return matches(record, targetPath, changeSetId) && (record.excluded || record.lifecycleState);
|
|
16
|
+
}
|
|
17
|
+
// A durable shutdown reservation fences parent mutations even if a display record is replaced.
|
|
18
|
+
function pendingRemoval(config, parentSlug) {
|
|
19
|
+
var parent = (config.projects || []).find(function (item) { return item.slug === parentSlug; });
|
|
20
|
+
return !!parent && Object.values(config.projectLifecycleOperations || {}).some(function (operation) {
|
|
21
|
+
return operation.phase === 'pending' && operation.action === 'remove' &&
|
|
22
|
+
operation.projectKnowledgeId === parent.projectKnowledgeId && operation.slug.indexOf(parentSlug + '--') === 0;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
module.exports = { pendingRemoval: pendingRemoval, entry: entry, matches: matches, icon: icon, blocked: blocked };
|