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.
Files changed (40) hide show
  1. package/lib/driver-continuation-access.js +54 -0
  2. package/lib/driver-continuation-lease.js +52 -0
  3. package/lib/driver-continuation-pair-transfer.js +86 -0
  4. package/lib/driver-continuation-pair.js +82 -0
  5. package/lib/driver-continuation-record.js +40 -0
  6. package/lib/driver-continuation-startup.js +68 -0
  7. package/lib/driver-continuation-transaction.js +61 -0
  8. package/lib/driver-continuation-trigger.js +110 -0
  9. package/lib/project-connection.js +1 -1
  10. package/lib/project-driver-continuation.js +499 -0
  11. package/lib/project-message-delivery.js +20 -0
  12. package/lib/project-pair-lifecycle.js +73 -73
  13. package/lib/project-pair-replacement-state.js +5 -1
  14. package/lib/project-session-handoff.js +48 -55
  15. package/lib/project-user-message.js +21 -0
  16. package/lib/project-worker-proposal.js +1 -1
  17. package/lib/project.js +49 -2
  18. package/lib/public/css/driver-continuation.css +56 -0
  19. package/lib/public/css/overlays.css +44 -16
  20. package/lib/public/css/pane.css +9 -5
  21. package/lib/public/index.html +3 -3
  22. package/lib/public/modules/app-connection.js +169 -51
  23. package/lib/public/modules/app-messages.js +19 -3
  24. package/lib/public/modules/driver-continuation-state.js +76 -0
  25. package/lib/public/modules/driver-continuation.js +260 -0
  26. package/lib/public/modules/message-delivery-ui.js +63 -0
  27. package/lib/public/modules/message-delivery.js +163 -6
  28. package/lib/public/modules/permission-control.js +8 -3
  29. package/lib/public/modules/websocket-lifecycle.js +178 -0
  30. package/lib/public/style.css +1 -0
  31. package/lib/sdk-bridge.js +77 -0
  32. package/lib/session-handoff-discovery.js +443 -0
  33. package/lib/session-handoff-mcp-server.js +32 -4
  34. package/lib/session-pair-prompts.js +5 -0
  35. package/lib/session-pair-turn-control.js +19 -3
  36. package/lib/session-split-group-anchors.js +44 -0
  37. package/lib/session-split-groups.js +209 -59
  38. package/lib/sessions.js +21 -3
  39. package/lib/ws-schema.js +6 -0
  40. 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) return null;
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
  }