clay-server 4.2.0 → 4.3.0-beta.2
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/driver-continuation-access.js +54 -0
- package/lib/driver-continuation-lease.js +52 -0
- package/lib/driver-continuation-pair-transfer.js +86 -0
- package/lib/driver-continuation-pair.js +82 -0
- package/lib/driver-continuation-record.js +40 -0
- package/lib/driver-continuation-startup.js +68 -0
- package/lib/driver-continuation-transaction.js +61 -0
- package/lib/driver-continuation-trigger.js +110 -0
- package/lib/project-connection.js +1 -1
- package/lib/project-driver-continuation.js +499 -0
- package/lib/project-message-delivery.js +20 -0
- package/lib/project-pair-lifecycle.js +73 -73
- package/lib/project-pair-replacement-state.js +5 -1
- package/lib/project-session-handoff.js +48 -55
- package/lib/project-user-message.js +21 -0
- package/lib/project-worker-proposal.js +1 -1
- package/lib/project.js +49 -2
- package/lib/public/css/driver-continuation.css +56 -0
- package/lib/public/css/overlays.css +44 -16
- package/lib/public/css/pane.css +9 -5
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +169 -51
- package/lib/public/modules/app-messages.js +19 -3
- package/lib/public/modules/driver-continuation-state.js +76 -0
- package/lib/public/modules/driver-continuation.js +260 -0
- package/lib/public/modules/message-delivery-ui.js +63 -0
- package/lib/public/modules/message-delivery.js +163 -6
- package/lib/public/modules/permission-control.js +8 -3
- package/lib/public/modules/websocket-lifecycle.js +178 -0
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +77 -0
- package/lib/session-handoff-discovery.js +443 -0
- package/lib/session-handoff-mcp-server.js +32 -4
- package/lib/session-pair-prompts.js +5 -0
- package/lib/session-pair-turn-control.js +19 -3
- package/lib/session-split-group-anchors.js +44 -0
- package/lib/session-split-groups.js +209 -59
- package/lib/sessions.js +21 -3
- package/lib/ws-schema.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { addToMessages, scrollToBottom } from './app-rendering.js';
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { continuationKey, authoritativeState, beginRequest, applyResponse, failRequest, sendContinuationPayload, continuationCanAct } from './driver-continuation-state.js';
|
|
6
|
+
|
|
7
|
+
var REQUEST_TIMEOUT_MS = 12000;
|
|
8
|
+
|
|
9
|
+
function states() {
|
|
10
|
+
return store.get("driverContinuations") || {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stateFor(key) {
|
|
14
|
+
return states()[key] || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function setState(key, value) {
|
|
18
|
+
var next = Object.assign({}, states());
|
|
19
|
+
next[key] = value;
|
|
20
|
+
store.set({ driverContinuations: next });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function currentProjectAllows(message) {
|
|
24
|
+
var current = store.get("currentSlug");
|
|
25
|
+
return !current || !message.projectSlug || current === message.projectSlug;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hydrate(message) {
|
|
29
|
+
if (!currentProjectAllows(message)) return null;
|
|
30
|
+
var normalized = message;
|
|
31
|
+
var activeSessionId = store.get("activeSessionId");
|
|
32
|
+
if (message.type === "driver_continuation_proposal" && activeSessionId !== undefined && activeSessionId !== null) {
|
|
33
|
+
normalized = Object.assign({}, message, { sourceSessionId: activeSessionId });
|
|
34
|
+
}
|
|
35
|
+
var state = authoritativeState(normalized);
|
|
36
|
+
if (!state.key) return null;
|
|
37
|
+
setState(state.key, state);
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cardsFor(key) {
|
|
42
|
+
var cards = document.querySelectorAll("[data-driver-continuation-key]");
|
|
43
|
+
var found = [];
|
|
44
|
+
for (var i = 0; i < cards.length; i++) {
|
|
45
|
+
if (cards[i].dataset.driverContinuationKey === key) found.push(cards[i]);
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function statusLabel(status) {
|
|
51
|
+
if (status === "starting") return "Starting new session";
|
|
52
|
+
if (status === "accepted") return "Continued in new session";
|
|
53
|
+
if (status === "declined") return "Staying here";
|
|
54
|
+
if (status === "superseded") return "Work changed";
|
|
55
|
+
return "Your choice";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function syncCard(card) {
|
|
59
|
+
var state = stateFor(card.dataset.driverContinuationKey);
|
|
60
|
+
if (!state) return;
|
|
61
|
+
card.dataset.status = state.status;
|
|
62
|
+
card.dataset.inflight = state.inflight ? "true" : "false";
|
|
63
|
+
var badge = card.querySelector(".driver-continuation-status");
|
|
64
|
+
var error = card.querySelector(".driver-continuation-error");
|
|
65
|
+
if (badge) badge.textContent = statusLabel(state.status);
|
|
66
|
+
if (error) {
|
|
67
|
+
error.textContent = state.error;
|
|
68
|
+
error.hidden = !state.error;
|
|
69
|
+
}
|
|
70
|
+
var enabled = continuationCanAct(state, store.get("connected"));
|
|
71
|
+
var buttons = card.querySelectorAll(".driver-continuation-action");
|
|
72
|
+
for (var i = 0; i < buttons.length; i++) buttons[i].disabled = !enabled;
|
|
73
|
+
var sourceButton = card.querySelector(".driver-continuation-source");
|
|
74
|
+
if (sourceButton) sourceButton.disabled = state.inflight || store.get("connected") !== true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function syncKey(key) {
|
|
78
|
+
var cards = cardsFor(key);
|
|
79
|
+
for (var i = 0; i < cards.length; i++) syncCard(cards[i]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requestId() {
|
|
83
|
+
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
84
|
+
return "continuation_" + Date.now() + "_" + Math.random().toString(16).slice(2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fail(key, exactRequestId, message) {
|
|
88
|
+
var next = failRequest(stateFor(key), exactRequestId, message);
|
|
89
|
+
if (!next) return false;
|
|
90
|
+
setState(key, next);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sendRequest(key, kind, payload) {
|
|
95
|
+
var ws = getWs();
|
|
96
|
+
var id = requestId();
|
|
97
|
+
var next = beginRequest(stateFor(key), kind, id, store.get("connected"));
|
|
98
|
+
if (!next || !ws || ws.readyState !== 1) return false;
|
|
99
|
+
setState(key, next);
|
|
100
|
+
payload.requestId = id;
|
|
101
|
+
payload.projectSlug = next.projectSlug;
|
|
102
|
+
payload.sourceOriginId = next.sourceOriginId;
|
|
103
|
+
payload.proposalId = next.proposalId;
|
|
104
|
+
var sent = sendContinuationPayload(ws, payload);
|
|
105
|
+
if (!sent.ok) {
|
|
106
|
+
fail(key, id, sent.error);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
setTimeout(function () {
|
|
110
|
+
fail(key, id, "Clay did not confirm the request. Check the connection and try again.");
|
|
111
|
+
}, REQUEST_TIMEOUT_MS);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sendChoice(card, accepted) {
|
|
116
|
+
sendRequest(card.dataset.driverContinuationKey, "decision", {
|
|
117
|
+
type: "driver_continuation_response",
|
|
118
|
+
accepted: accepted === true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function addDetail(details, label, value) {
|
|
123
|
+
if (!value) return;
|
|
124
|
+
var term = document.createElement("dt");
|
|
125
|
+
term.textContent = label;
|
|
126
|
+
var description = document.createElement("dd");
|
|
127
|
+
description.textContent = value;
|
|
128
|
+
details.appendChild(term);
|
|
129
|
+
details.appendChild(description);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function evidenceText(evidence) {
|
|
133
|
+
if (!evidence) return "";
|
|
134
|
+
if (evidence.kind === "current_context_pressure" && typeof evidence.usedRatio === "number") {
|
|
135
|
+
var percent = Math.round(evidence.usedRatio * 100);
|
|
136
|
+
var threshold = Math.round(Number(evidence.thresholdRatio || 0) * 100);
|
|
137
|
+
return percent + "% of the current context is in use (proposal threshold: " + threshold + "%).";
|
|
138
|
+
}
|
|
139
|
+
if (evidence.kind === "recorded_compaction") {
|
|
140
|
+
var count = Number(evidence.observedCount || 0);
|
|
141
|
+
return "Clay recorded " + count + " completed context compaction" + (count === 1 ? "" : "s") + " in this session.";
|
|
142
|
+
}
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function renderDriverContinuation(msg) {
|
|
147
|
+
var state = hydrate(msg);
|
|
148
|
+
if (!state) return;
|
|
149
|
+
var existing = cardsFor(state.key);
|
|
150
|
+
if (existing.length) { syncKey(state.key); return; }
|
|
151
|
+
var handoff = msg.handoff || {};
|
|
152
|
+
var card = document.createElement("section");
|
|
153
|
+
card.className = "driver-continuation-card";
|
|
154
|
+
card.dataset.driverContinuationKey = state.key;
|
|
155
|
+
card.innerHTML = '<header><span class="driver-continuation-mark">' + iconHtml("arrow-up-right") + '</span><div><span class="driver-continuation-kicker">DRIVER CONTINUATION</span><strong>Carry this work into a fresh session?</strong></div><span class="driver-continuation-status"></span></header>';
|
|
156
|
+
var reason = document.createElement("p");
|
|
157
|
+
reason.className = "driver-continuation-reason";
|
|
158
|
+
reason.textContent = msg.reason || "The Driver identified a safe boundary for a compact handoff.";
|
|
159
|
+
card.appendChild(reason);
|
|
160
|
+
var summary = document.createElement("div");
|
|
161
|
+
summary.className = "driver-continuation-summary";
|
|
162
|
+
addDetail(summary, "Observed evidence", evidenceText(msg.triggerEvidence));
|
|
163
|
+
addDetail(summary, "Completed milestone", msg.milestone);
|
|
164
|
+
addDetail(summary, "Why a fresh session helps", msg.benefit);
|
|
165
|
+
addDetail(summary, "Goal", handoff.goal);
|
|
166
|
+
addDetail(summary, "Next action", handoff.nextAction);
|
|
167
|
+
card.appendChild(summary);
|
|
168
|
+
var disclosure = document.createElement("details");
|
|
169
|
+
disclosure.className = "driver-continuation-details";
|
|
170
|
+
disclosure.innerHTML = "<summary>Review inherited context</summary>";
|
|
171
|
+
var details = document.createElement("dl");
|
|
172
|
+
addDetail(details, "Constraints", handoff.constraints);
|
|
173
|
+
addDetail(details, "Decisions and why", handoff.decisions);
|
|
174
|
+
addDetail(details, "Rejected approaches", handoff.rejectedApproaches);
|
|
175
|
+
addDetail(details, "Unresolved", handoff.unresolved);
|
|
176
|
+
addDetail(details, "Repository state", handoff.repositoryState);
|
|
177
|
+
addDetail(details, "Verification", handoff.verification);
|
|
178
|
+
disclosure.appendChild(details);
|
|
179
|
+
card.appendChild(disclosure);
|
|
180
|
+
var error = document.createElement("p");
|
|
181
|
+
error.className = "driver-continuation-error";
|
|
182
|
+
error.hidden = true;
|
|
183
|
+
card.appendChild(error);
|
|
184
|
+
var actions = document.createElement("div");
|
|
185
|
+
actions.className = "driver-continuation-actions";
|
|
186
|
+
var stay = document.createElement("button");
|
|
187
|
+
stay.type = "button";
|
|
188
|
+
stay.className = "driver-continuation-action secondary";
|
|
189
|
+
stay.textContent = "Stay here";
|
|
190
|
+
stay.addEventListener("click", function () { sendChoice(card, false); });
|
|
191
|
+
var proceed = document.createElement("button");
|
|
192
|
+
proceed.type = "button";
|
|
193
|
+
proceed.className = "driver-continuation-action primary";
|
|
194
|
+
proceed.innerHTML = iconHtml("arrow-right") + "<span>Continue in new session</span>";
|
|
195
|
+
proceed.addEventListener("click", function () { sendChoice(card, true); });
|
|
196
|
+
actions.appendChild(stay);
|
|
197
|
+
actions.appendChild(proceed);
|
|
198
|
+
card.appendChild(actions);
|
|
199
|
+
addToMessages(card);
|
|
200
|
+
syncCard(card);
|
|
201
|
+
refreshIcons();
|
|
202
|
+
scrollToBottom();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function updateDriverContinuation(msg) {
|
|
206
|
+
if (!currentProjectAllows(msg)) return false;
|
|
207
|
+
var key = continuationKey(msg);
|
|
208
|
+
var current = stateFor(key);
|
|
209
|
+
if (!current) return false;
|
|
210
|
+
var next = applyResponse(current, msg);
|
|
211
|
+
if (!next) return false;
|
|
212
|
+
setState(key, next);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function renderDriverContinuationContext(msg) {
|
|
217
|
+
var seed = Object.assign({ status: "accepted" }, msg);
|
|
218
|
+
var key = continuationKey(seed);
|
|
219
|
+
if (!stateFor(key)) hydrate(seed);
|
|
220
|
+
var card = document.createElement("section");
|
|
221
|
+
card.className = "driver-continuation-context";
|
|
222
|
+
card.dataset.driverContinuationKey = key;
|
|
223
|
+
card.innerHTML = '<span class="driver-continuation-mark">' + iconHtml("history") + '</span><div class="driver-continuation-context-copy"><span class="driver-continuation-kicker">INHERITED CONTEXT</span></div>';
|
|
224
|
+
var copy = card.querySelector(".driver-continuation-context-copy");
|
|
225
|
+
var title = document.createElement("strong");
|
|
226
|
+
title.textContent = msg.goal || "Continued Driver work";
|
|
227
|
+
var next = document.createElement("p");
|
|
228
|
+
next.textContent = "Next: " + (msg.nextAction || "Revalidate the current state and continue.");
|
|
229
|
+
var error = document.createElement("p");
|
|
230
|
+
error.className = "driver-continuation-error";
|
|
231
|
+
error.hidden = true;
|
|
232
|
+
var source = document.createElement("button");
|
|
233
|
+
source.type = "button";
|
|
234
|
+
source.className = "driver-continuation-source";
|
|
235
|
+
source.textContent = "Open original session";
|
|
236
|
+
source.addEventListener("click", function () {
|
|
237
|
+
sendRequest(key, "open_source", { type: "driver_continuation_open_source" });
|
|
238
|
+
});
|
|
239
|
+
copy.appendChild(title);
|
|
240
|
+
copy.appendChild(next);
|
|
241
|
+
copy.appendChild(error);
|
|
242
|
+
copy.appendChild(source);
|
|
243
|
+
addToMessages(card);
|
|
244
|
+
syncCard(card);
|
|
245
|
+
refreshIcons();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
store.subscribe(function (state, previous) {
|
|
249
|
+
if (state.connected === previous.connected &&
|
|
250
|
+
state.driverContinuations === previous.driverContinuations) return;
|
|
251
|
+
if (state.connected === false && previous.connected === true) {
|
|
252
|
+
var current = states();
|
|
253
|
+
var keys = Object.keys(current);
|
|
254
|
+
for (var i = 0; i < keys.length; i++) {
|
|
255
|
+
if (current[keys[i]].inflight) fail(keys[i], current[keys[i]].requestId, "Connection lost before Clay confirmed the request. Try again after reconnecting.");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
var cards = document.querySelectorAll("[data-driver-continuation-key]");
|
|
259
|
+
for (var j = 0; j < cards.length; j++) syncCard(cards[j]);
|
|
260
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// message-delivery-ui.js - receipt recovery notices
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { retryPendingMessage } from './message-delivery.js';
|
|
5
|
+
|
|
6
|
+
var initialized = false;
|
|
7
|
+
var rendered = {};
|
|
8
|
+
var unsubscribe = null;
|
|
9
|
+
|
|
10
|
+
function currentContext(receipt) {
|
|
11
|
+
var currentOriginId = store.get('sessionOriginId') || null;
|
|
12
|
+
return receipt && receipt.projectSlug === store.get('currentSlug') && String(receipt.sessionId) === String(store.get('activeSessionId')) && receipt.accountId === (store.get('myUserId') || "_default") && (!receipt.sessionOriginId || !currentOriginId || receipt.sessionOriginId === currentOriginId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderReceipt(receipt) {
|
|
16
|
+
var messages = document.getElementById("messages");
|
|
17
|
+
if (!messages || !currentContext(receipt) || rendered[receipt.clientMessageId]) return;
|
|
18
|
+
var notice = document.createElement("div");
|
|
19
|
+
notice.className = "sys-msg error";
|
|
20
|
+
notice.dataset.deliveryReceipt = receipt.clientMessageId;
|
|
21
|
+
var text = document.createElement("span");
|
|
22
|
+
text.className = "sys-text";
|
|
23
|
+
text.textContent = "Receipt unconfirmed. Retry this message.";
|
|
24
|
+
notice.appendChild(text);
|
|
25
|
+
var retry = document.createElement("button");
|
|
26
|
+
retry.type = "button";
|
|
27
|
+
retry.className = "sys-retry";
|
|
28
|
+
retry.textContent = "Retry";
|
|
29
|
+
retry.addEventListener("click", function () { retryPendingMessage(receipt.clientMessageId); });
|
|
30
|
+
notice.appendChild(retry);
|
|
31
|
+
messages.appendChild(notice);
|
|
32
|
+
rendered[receipt.clientMessageId] = notice;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sync(state) {
|
|
36
|
+
var receipts = state.deliveryReceipts || {};
|
|
37
|
+
Object.keys(rendered).forEach(function (id) {
|
|
38
|
+
if (!rendered[id].parentNode || !receipts[id] || !currentContext(receipts[id])) {
|
|
39
|
+
if (rendered[id] && rendered[id].parentNode) rendered[id].parentNode.removeChild(rendered[id]);
|
|
40
|
+
delete rendered[id];
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
Object.keys(receipts).forEach(function (id) { renderReceipt(receipts[id]); });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function initMessageDeliveryUi() {
|
|
47
|
+
if (initialized) return;
|
|
48
|
+
initialized = true;
|
|
49
|
+
unsubscribe = store.subscribe(function (state, previous) {
|
|
50
|
+
if (state.deliveryReceipts !== previous.deliveryReceipts || state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.sessionOriginId !== previous.sessionOriginId || state.myUserId !== previous.myUserId || state.replayingHistory !== previous.replayingHistory) sync(state);
|
|
51
|
+
});
|
|
52
|
+
sync(store.snap());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function disposeMessageDeliveryUi() {
|
|
56
|
+
Object.keys(rendered).forEach(function (id) {
|
|
57
|
+
if (rendered[id] && rendered[id].parentNode) rendered[id].parentNode.removeChild(rendered[id]);
|
|
58
|
+
});
|
|
59
|
+
rendered = {};
|
|
60
|
+
if (unsubscribe) unsubscribe();
|
|
61
|
+
unsubscribe = null;
|
|
62
|
+
initialized = false;
|
|
63
|
+
}
|
|
@@ -4,7 +4,10 @@ import { store } from './store.js';
|
|
|
4
4
|
import { getWs } from './ws-ref.js';
|
|
5
5
|
|
|
6
6
|
var ACK_TIMEOUT_MS = 5000;
|
|
7
|
+
var ACK_RETRY_LIMIT = 3;
|
|
7
8
|
var ackTimers = {};
|
|
9
|
+
var socketRefs = {};
|
|
10
|
+
var activatedContext = null;
|
|
8
11
|
|
|
9
12
|
function createClientMessageId() {
|
|
10
13
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
@@ -21,6 +24,39 @@ function replacePending(messages) {
|
|
|
21
24
|
store.set({ pendingOutboundMessages: messages });
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
function receipts() {
|
|
28
|
+
return store.get('deliveryReceipts') || {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setReceipt(entry) {
|
|
32
|
+
var next = Object.assign({}, receipts());
|
|
33
|
+
next[entry.payload.clientMessageId] = {
|
|
34
|
+
clientMessageId: entry.payload.clientMessageId,
|
|
35
|
+
projectSlug: entry.projectSlug,
|
|
36
|
+
sessionId: entry.sessionId,
|
|
37
|
+
accountId: entry.accountId,
|
|
38
|
+
sessionOriginId: entry.sessionOriginId || null,
|
|
39
|
+
attempts: entry.attempts
|
|
40
|
+
};
|
|
41
|
+
store.set({ deliveryReceipts: next });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clearReceipt(clientMessageId) {
|
|
45
|
+
var current = receipts();
|
|
46
|
+
if (!current[clientMessageId]) return;
|
|
47
|
+
var next = Object.assign({}, current);
|
|
48
|
+
delete next[clientMessageId];
|
|
49
|
+
store.set({ deliveryReceipts: next });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function markUnconfirmed(entry) {
|
|
53
|
+
var unconfirmed = Object.assign({}, entry, { status: "unconfirmed" });
|
|
54
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
55
|
+
return candidate.payload.clientMessageId === entry.payload.clientMessageId ? unconfirmed : candidate;
|
|
56
|
+
}));
|
|
57
|
+
setReceipt(unconfirmed);
|
|
58
|
+
}
|
|
59
|
+
|
|
24
60
|
function clearAckTimer(clientMessageId) {
|
|
25
61
|
if (!ackTimers[clientMessageId]) return;
|
|
26
62
|
clearTimeout(ackTimers[clientMessageId]);
|
|
@@ -35,25 +71,92 @@ function isPending(clientMessageId) {
|
|
|
35
71
|
return false;
|
|
36
72
|
}
|
|
37
73
|
|
|
74
|
+
function pendingEntry(clientMessageId) {
|
|
75
|
+
var pending = pendingMessages();
|
|
76
|
+
for (var i = 0; i < pending.length; i++) {
|
|
77
|
+
if (pending[i].payload.clientMessageId === clientMessageId) return pending[i];
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matchesCurrentContext(entry) {
|
|
83
|
+
return entry && entry.projectSlug === store.get('currentSlug') && String(entry.sessionId) === String(store.get('activeSessionId')) && entry.accountId === (store.get('myUserId') || "_default");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasAutomaticReplayIdentity(entry, socket) {
|
|
87
|
+
if (!activatedContext || activatedContext.socket !== socket) return false;
|
|
88
|
+
if (entry.sessionOriginId && activatedContext.sessionOriginId && entry.sessionOriginId !== activatedContext.sessionOriginId) return false;
|
|
89
|
+
if (entry.sessionOriginId && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId) return true;
|
|
90
|
+
if (entry.cliSessionId && activatedContext.cliSessionId) return entry.cliSessionId === activatedContext.cliSessionId;
|
|
91
|
+
return socketRefs[entry.payload.clientMessageId] === socket;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasKnownCliMismatch(entry) {
|
|
95
|
+
if (entry.sessionOriginId && activatedContext && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId) return false;
|
|
96
|
+
return !!entry.cliSessionId && !!activatedContext && !!activatedContext.cliSessionId && entry.cliSessionId !== activatedContext.cliSessionId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function matchesActivatedContext(entry) {
|
|
100
|
+
var currentCliSessionId = store.get('cliSessionId') || null;
|
|
101
|
+
var sameOrigin = !!(entry.sessionOriginId && activatedContext && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId);
|
|
102
|
+
return !!activatedContext && activatedContext.socket === getWs() && matchesCurrentContext(entry) &&
|
|
103
|
+
activatedContext.projectSlug === store.get('currentSlug') && String(activatedContext.sessionId) === String(store.get('activeSessionId')) &&
|
|
104
|
+
activatedContext.accountId === (store.get('myUserId') || "_default") && (activatedContext.sessionOriginId || activatedContext.cliSessionId === currentCliSessionId) &&
|
|
105
|
+
(!entry.sessionOriginId || !activatedContext.sessionOriginId || entry.sessionOriginId === activatedContext.sessionOriginId) &&
|
|
106
|
+
(sameOrigin || !entry.cliSessionId || !activatedContext.cliSessionId || entry.cliSessionId === activatedContext.cliSessionId);
|
|
107
|
+
}
|
|
108
|
+
|
|
38
109
|
function armAckTimer(clientMessageId, socket) {
|
|
39
110
|
clearAckTimer(clientMessageId);
|
|
40
111
|
ackTimers[clientMessageId] = setTimeout(function () {
|
|
41
112
|
delete ackTimers[clientMessageId];
|
|
42
113
|
if (!isPending(clientMessageId) || getWs() !== socket) return;
|
|
114
|
+
var entry = pendingEntry(clientMessageId);
|
|
115
|
+
if (!matchesActivatedContext(entry)) return;
|
|
43
116
|
window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
|
|
44
117
|
detail: { socket: socket, clientMessageId: clientMessageId },
|
|
45
118
|
}));
|
|
119
|
+
var attempts = entry.attempts || 0;
|
|
120
|
+
if (attempts < ACK_RETRY_LIMIT && entry && socket.readyState === 1) {
|
|
121
|
+
var nextEntry = Object.assign({}, entry, { attempts: attempts + 1 });
|
|
122
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
123
|
+
return candidate.payload.clientMessageId === clientMessageId ? nextEntry : candidate;
|
|
124
|
+
}));
|
|
125
|
+
transmit(nextEntry, false);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (attempts >= ACK_RETRY_LIMIT) {
|
|
129
|
+
markUnconfirmed(entry);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
46
132
|
}, ACK_TIMEOUT_MS);
|
|
47
133
|
}
|
|
48
134
|
|
|
49
|
-
function transmit(entry) {
|
|
135
|
+
function transmit(entry, automatic) {
|
|
50
136
|
var socket = getWs();
|
|
51
|
-
if (!socket || socket.readyState !== 1) return false;
|
|
137
|
+
if (!matchesCurrentContext(entry) || !socket || socket.readyState !== 1) return false;
|
|
138
|
+
if (automatic && entry.status === "unconfirmed") return false;
|
|
139
|
+
if (automatic && !hasAutomaticReplayIdentity(entry, socket)) {
|
|
140
|
+
markUnconfirmed(entry);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (automatic) {
|
|
144
|
+
if ((entry.attempts || 0) >= ACK_RETRY_LIMIT) {
|
|
145
|
+
markUnconfirmed(entry);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
entry = Object.assign({}, entry, { attempts: (entry.attempts || 0) + 1 });
|
|
149
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
150
|
+
return candidate.payload.clientMessageId === entry.payload.clientMessageId ? entry : candidate;
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
socketRefs[entry.payload.clientMessageId] = socket;
|
|
52
154
|
try {
|
|
53
155
|
socket.send(JSON.stringify(entry.payload));
|
|
54
156
|
armAckTimer(entry.payload.clientMessageId, socket);
|
|
55
157
|
return true;
|
|
56
158
|
} catch (e) {
|
|
159
|
+
markUnconfirmed(entry);
|
|
57
160
|
return false;
|
|
58
161
|
}
|
|
59
162
|
}
|
|
@@ -63,10 +166,22 @@ export function sendAcknowledgedMessage(payload) {
|
|
|
63
166
|
var entry = {
|
|
64
167
|
projectSlug: store.get('currentSlug'),
|
|
65
168
|
sessionId: store.get('activeSessionId'),
|
|
169
|
+
accountId: store.get('myUserId') || "_default",
|
|
170
|
+
cliSessionId: store.get('cliSessionId') || null,
|
|
171
|
+
sessionOriginId: store.get('sessionOriginId') || null,
|
|
172
|
+
attempts: 0,
|
|
173
|
+
status: "pending",
|
|
66
174
|
payload: nextPayload,
|
|
67
175
|
};
|
|
176
|
+
nextPayload.projectSlug = entry.projectSlug;
|
|
177
|
+
nextPayload.sessionId = entry.sessionId;
|
|
178
|
+
nextPayload.accountId = entry.accountId;
|
|
179
|
+
nextPayload.cliSessionId = entry.cliSessionId;
|
|
180
|
+
nextPayload.sessionOriginId = entry.sessionOriginId;
|
|
181
|
+
entry.payload = nextPayload;
|
|
68
182
|
replacePending(pendingMessages().concat([entry]));
|
|
69
|
-
if (!transmit(entry)) {
|
|
183
|
+
if (!transmit(entry, false)) {
|
|
184
|
+
if (!getWs() || getWs().readyState !== 1) markUnconfirmed(entry);
|
|
70
185
|
window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
|
|
71
186
|
detail: { socket: getWs(), clientMessageId: nextPayload.clientMessageId },
|
|
72
187
|
}));
|
|
@@ -74,6 +189,21 @@ export function sendAcknowledgedMessage(payload) {
|
|
|
74
189
|
return nextPayload.clientMessageId;
|
|
75
190
|
}
|
|
76
191
|
|
|
192
|
+
export function retryPendingMessage(clientMessageId) {
|
|
193
|
+
var entry = pendingEntry(clientMessageId);
|
|
194
|
+
if (!matchesActivatedContext(entry) || hasKnownCliMismatch(entry)) return false;
|
|
195
|
+
var retryEntry = Object.assign({}, entry, { attempts: 0, status: "pending" });
|
|
196
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
197
|
+
return candidate.payload.clientMessageId === clientMessageId ? retryEntry : candidate;
|
|
198
|
+
}));
|
|
199
|
+
if (transmit(retryEntry, false)) {
|
|
200
|
+
clearReceipt(clientMessageId);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
markUnconfirmed(retryEntry);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
77
207
|
export function acknowledgeMessage(clientMessageId) {
|
|
78
208
|
if (typeof clientMessageId !== "string") return null;
|
|
79
209
|
var pending = pendingMessages();
|
|
@@ -82,9 +212,14 @@ export function acknowledgeMessage(clientMessageId) {
|
|
|
82
212
|
if (entry.payload.clientMessageId === clientMessageId) acknowledged = entry;
|
|
83
213
|
return entry.payload.clientMessageId !== clientMessageId;
|
|
84
214
|
});
|
|
85
|
-
if (next.length === pending.length)
|
|
215
|
+
if (next.length === pending.length) {
|
|
216
|
+
clearReceipt(clientMessageId);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
86
219
|
clearAckTimer(clientMessageId);
|
|
87
220
|
replacePending(next);
|
|
221
|
+
delete socketRefs[clientMessageId];
|
|
222
|
+
clearReceipt(clientMessageId);
|
|
88
223
|
return acknowledged;
|
|
89
224
|
}
|
|
90
225
|
|
|
@@ -92,8 +227,30 @@ export function replayPendingMessages(sessionId) {
|
|
|
92
227
|
var pending = pendingMessages();
|
|
93
228
|
var projectSlug = store.get('currentSlug');
|
|
94
229
|
for (var i = 0; i < pending.length; i++) {
|
|
95
|
-
if (pending[i].projectSlug === projectSlug && pending[i].sessionId === sessionId) {
|
|
96
|
-
transmit(pending[i]);
|
|
230
|
+
if (pending[i].projectSlug === projectSlug && String(pending[i].sessionId) === String(sessionId) && matchesCurrentContext(pending[i])) {
|
|
231
|
+
transmit(pending[i], true);
|
|
97
232
|
}
|
|
98
233
|
}
|
|
99
234
|
}
|
|
235
|
+
|
|
236
|
+
export function activateDeliverySession(sessionId, cliSessionId, sessionOriginId) {
|
|
237
|
+
var socket = getWs();
|
|
238
|
+
activatedContext = {
|
|
239
|
+
socket: socket,
|
|
240
|
+
projectSlug: store.get('currentSlug'),
|
|
241
|
+
sessionId: sessionId,
|
|
242
|
+
accountId: store.get('myUserId') || "_default",
|
|
243
|
+
cliSessionId: cliSessionId || null,
|
|
244
|
+
sessionOriginId: sessionOriginId || null
|
|
245
|
+
};
|
|
246
|
+
replayPendingMessages(sessionId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function refreshDeliverySessionIdentity(cliSessionId, sessionOriginId) {
|
|
250
|
+
if (!activatedContext || activatedContext.socket !== getWs()) return false;
|
|
251
|
+
if (activatedContext.projectSlug !== store.get('currentSlug') || String(activatedContext.sessionId) !== String(store.get('activeSessionId')) || activatedContext.accountId !== (store.get('myUserId') || "_default")) return false;
|
|
252
|
+
if (activatedContext.sessionOriginId && sessionOriginId && activatedContext.sessionOriginId !== sessionOriginId) return false;
|
|
253
|
+
activatedContext.cliSessionId = cliSessionId || null;
|
|
254
|
+
if (!activatedContext.sessionOriginId && sessionOriginId) activatedContext.sessionOriginId = sessionOriginId;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
@@ -46,10 +46,11 @@ export function createPermissionControl(className, ariaLabel) {
|
|
|
46
46
|
control.setAttribute("role", "group");
|
|
47
47
|
control.setAttribute("aria-label", ariaLabel || "Session permission mode");
|
|
48
48
|
control.innerHTML = '<div class="session-permission-segmented">' +
|
|
49
|
-
'<button type="button" data-permission-mode="default">Ask</button>' +
|
|
50
|
-
'<button type="button" data-permission-mode="auto">Auto</button>' +
|
|
51
|
-
'<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions"><span class="permission-label-long">Skip permissions</span><span class="permission-label-short" aria-hidden="true">Skip</span></button>' +
|
|
49
|
+
'<button type="button" data-permission-mode="default"><span class="permission-label">Ask</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
|
|
50
|
+
'<button type="button" data-permission-mode="auto"><span class="permission-label">Auto</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
|
|
51
|
+
'<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions" title="Skip permissions"><span class="permission-label-long permission-label">Skip permissions</span><span class="permission-label-short permission-label" aria-hidden="true">Skip</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
|
|
52
52
|
'</div><span class="session-permission-status" aria-live="polite"></span>';
|
|
53
|
+
control.querySelector(".session-permission-status").setAttribute("aria-atomic", "true");
|
|
53
54
|
return control;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -99,15 +100,19 @@ export function renderPermissionControl(control, state) {
|
|
|
99
100
|
control.classList.toggle("permission-auto-unsupported", autoVisible && !autoSupported);
|
|
100
101
|
control.classList.toggle("permission-globally-forced", globallyForced);
|
|
101
102
|
control.classList.toggle("permission-runtime-fallback", state.permissionMode === "auto" && !!state.effectivePermissionMode && state.effectivePermissionMode !== "auto");
|
|
103
|
+
control.setAttribute("aria-busy", pending ? "true" : "false");
|
|
102
104
|
var buttons = control.querySelectorAll("[data-permission-mode]");
|
|
103
105
|
for (var i = 0; i < buttons.length; i++) {
|
|
104
106
|
var mode = buttons[i].dataset.permissionMode;
|
|
105
107
|
var unsupported = mode === "auto" && autoVisible && !autoSupported;
|
|
108
|
+
var requested = !!pending && pending.mode === mode;
|
|
106
109
|
buttons[i].hidden = mode === "auto" && !autoVisible;
|
|
107
110
|
var selected = globallyForced ? mode === "bypassPermissions" : mode === state.permissionMode;
|
|
108
111
|
buttons[i].disabled = globallyForced || unsupported || !!pending || !connected || state.locked === true;
|
|
109
112
|
buttons[i].classList.toggle("active", selected);
|
|
113
|
+
buttons[i].classList.toggle("pending", requested);
|
|
110
114
|
buttons[i].setAttribute("aria-pressed", selected ? "true" : "false");
|
|
115
|
+
buttons[i].setAttribute("aria-busy", requested ? "true" : "false");
|
|
111
116
|
if (unsupported) buttons[i].title = "Auto permissions are unavailable for this Claude session.";
|
|
112
117
|
else buttons[i].removeAttribute("title");
|
|
113
118
|
}
|