clay-server 4.3.0-beta.6 → 4.3.0-beta.8

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.
@@ -358,8 +358,7 @@
358
358
  background: rgba(var(--overlay-rgb), 0.06);
359
359
  }
360
360
 
361
- /* Split "New session" row: main action on the last-used vendor + a chevron
362
- that expands .mobile-vendor-list below it. Mirrors the desktop sidebar. */
361
+ /* Primary creation action + a labeled provider selector. */
363
362
  .mobile-session-new-row {
364
363
  display: flex;
365
364
  gap: 6px;
@@ -377,26 +376,30 @@
377
376
  color: var(--text);
378
377
  gap: 8px;
379
378
  }
380
- .mobile-session-new-row .mobile-session-new-chevron {
381
- flex: 0 0 auto;
382
- width: 44px;
379
+ .mobile-session-new-row .mobile-session-new-provider {
380
+ flex: 0 1 132px;
381
+ width: 132px;
382
+ min-width: 104px;
383
383
  margin-bottom: 0;
384
- padding: 10px 0;
384
+ padding: 6px 9px;
385
385
  border: 1px solid var(--border-subtle);
386
386
  border-radius: 8px;
387
387
  background: rgba(var(--overlay-rgb), 0.03);
388
388
  color: var(--text-muted);
389
- justify-content: center;
390
- gap: 0;
389
+ display: grid;
390
+ gap: 2px;
391
391
  }
392
- .mobile-session-new-row .mobile-session-new-chevron.expanded svg {
392
+ .mobile-session-new-row .mobile-session-new-provider.expanded .session-create-provider-chevron svg {
393
393
  transform: rotate(180deg);
394
394
  }
395
395
  .mobile-session-new-row .mobile-session-new-main:active,
396
- .mobile-session-new-row .mobile-session-new-chevron:active {
396
+ .mobile-session-new-row .mobile-session-new-provider:active {
397
397
  background: rgba(var(--overlay-rgb), 0.1);
398
398
  }
399
399
 
400
+ .mobile-session-new-row .session-create-provider-label { font-size: 9px; }
401
+ .mobile-session-new-row .session-create-provider-name { font-size: 12px; }
402
+
400
403
  .mobile-vendor-list {
401
404
  display: flex;
402
405
  flex-direction: column;
@@ -1262,26 +1262,77 @@
1262
1262
  opacity: 1;
1263
1263
  }
1264
1264
 
1265
- /* Split button: main action + chevron sharing one grid cell. */
1265
+ /* Primary creation action + a visibly grouped provider selector. */
1266
1266
  .session-top-action-split {
1267
1267
  display: flex;
1268
+ flex-wrap: wrap;
1268
1269
  align-items: stretch;
1269
- gap: 2px;
1270
+ gap: 6px;
1270
1271
  min-width: 0;
1271
1272
  }
1272
1273
 
1273
1274
  .session-top-action-split .split-main {
1274
- flex: 1 1 auto;
1275
+ flex: 1 1 110px;
1275
1276
  width: auto;
1277
+ min-width: 110px;
1278
+ border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
1279
+ background: color-mix(in srgb, var(--accent) 13%, transparent);
1280
+ color: var(--text);
1281
+ font-weight: 600;
1282
+ white-space: nowrap;
1276
1283
  }
1277
1284
 
1278
- .session-top-action-split .split-chevron {
1279
- flex: 0 0 auto;
1280
- width: 28px;
1281
- min-width: 28px;
1282
- padding: 0;
1283
- justify-content: center;
1284
- gap: 0;
1285
+ .session-top-action-split .split-main:hover {
1286
+ border-color: color-mix(in srgb, var(--accent) 50%, transparent);
1287
+ background: color-mix(in srgb, var(--accent) 19%, transparent);
1288
+ }
1289
+
1290
+ .session-top-action-split .split-provider {
1291
+ flex: 1 1 104px;
1292
+ width: auto;
1293
+ min-width: 104px;
1294
+ padding: 5px 8px;
1295
+ border: 1px solid var(--border-subtle);
1296
+ background: color-mix(in srgb, var(--sidebar-hover) 48%, transparent);
1297
+ border-radius: 9px;
1298
+ display: grid;
1299
+ gap: 1px;
1300
+ }
1301
+
1302
+ .session-create-provider-label {
1303
+ display: block;
1304
+ color: var(--text-dimmer);
1305
+ font-size: 9px;
1306
+ font-weight: 600;
1307
+ line-height: 1;
1308
+ letter-spacing: 0.03em;
1309
+ text-transform: uppercase;
1310
+ }
1311
+
1312
+ .session-create-provider-choice {
1313
+ display: flex;
1314
+ align-items: center;
1315
+ gap: 5px;
1316
+ min-width: 0;
1317
+ }
1318
+
1319
+ .session-create-provider-name {
1320
+ overflow: hidden;
1321
+ color: var(--text-secondary);
1322
+ font-size: 11px;
1323
+ font-weight: 600;
1324
+ line-height: 1.2;
1325
+ text-overflow: ellipsis;
1326
+ white-space: nowrap;
1327
+ }
1328
+
1329
+ .session-create-provider-chevron {
1330
+ display: inline-flex;
1331
+ margin-left: auto;
1332
+ }
1333
+
1334
+ .session-create-provider[aria-expanded="true"] .session-create-provider-chevron svg {
1335
+ transform: rotate(180deg);
1285
1336
  }
1286
1337
 
1287
1338
  .session-favorites-section {
@@ -0,0 +1,33 @@
1
+ // Shared, presentation-only session creation controls for desktop and mobile.
2
+
3
+ import { iconHtml } from './icons.js';
4
+
5
+ export function renderSessionCreationCta(options) {
6
+ var row = document.createElement("div");
7
+ row.className = options.rowClass + " session-create-cta";
8
+
9
+ var createButton = document.createElement("button");
10
+ createButton.className = options.createClass + " session-create-primary";
11
+ createButton.type = "button";
12
+ createButton.textContent = "Create session";
13
+ createButton.title = options.createTitle;
14
+ createButton.setAttribute("aria-label", options.createTitle);
15
+ createButton.addEventListener("click", options.onCreate);
16
+ row.appendChild(createButton);
17
+
18
+ var providerButton = document.createElement("button");
19
+ providerButton.className = options.providerClass + " session-create-provider";
20
+ providerButton.type = "button";
21
+ providerButton.title = options.providerTitle;
22
+ providerButton.setAttribute("aria-label", "AI provider: " + options.vendorName + ". " + options.providerTitle);
23
+ providerButton.setAttribute("aria-expanded", "false");
24
+ providerButton.setAttribute("aria-controls", options.menuId);
25
+ providerButton.innerHTML = '<span class="session-create-provider-label">AI provider</span>' +
26
+ '<span class="session-create-provider-choice"><img src="' + options.vendorAvatar +
27
+ '" class="' + options.iconClass + ' session-create-provider-icon" alt=""><span class="session-create-provider-name">' +
28
+ options.vendorName + '</span>' + iconHtml("chevron-down", "session-create-provider-chevron") + '</span>';
29
+ providerButton.addEventListener("click", options.onProvider);
30
+ row.appendChild(providerButton);
31
+
32
+ return { element: row, createButton: createButton, providerButton: providerButton };
33
+ }
@@ -16,6 +16,7 @@ import {
16
16
  } from './sidebar-sessions.js';
17
17
  import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
18
18
  import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
19
+ import { renderSessionCreationCta } from './session-creation-cta.js';
19
20
  import {
20
21
  getCachedProjectList,
21
22
  getCachedCurrentSlug,
@@ -671,42 +672,49 @@ function createMobileLoopGroup(loopId, children, groupKey) {
671
672
  }
672
673
 
673
674
  // Helper: render sorted sessions into a container with date groups (with loop session grouping)
674
- function renderMobileSessionsInto(container) {
675
- // Mirrors the desktop sidebar's split button: one "New session" action on
676
- // the project's last-used vendor, plus a chevron that expands the full
677
- // vendor list. The list expands inline rather than as a fixed-position
678
- // popover because it lives inside a scrolling bottom sheet.
675
+ export function renderMobileSessionsInto(container) {
679
676
  var mobileDefaultVendor = resolvePreferredVendor(store.get('installedVendors') || []) || resolveDefaultVendor();
680
677
  var mobileInstalled = store.get('installedVendors') || [];
681
678
  var mobileDefaultState = store.get('defaultVendorState') || {};
682
679
 
683
- var newRow = document.createElement("div");
684
- newRow.className = "mobile-session-new-row";
685
-
686
- var mainBtn = document.createElement("button");
687
- mainBtn.className = "mobile-session-new mobile-session-new-main";
688
- mainBtn.title = "New " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + " session";
689
- if (defaultVendorUnavailable(mobileInstalled)) mainBtn.title += " (saved default is unavailable; using installed priority)";
690
- mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude) +
691
- '" class="mobile-session-new-icon" alt=""><span>New ' + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ' session</span>';
692
- mainBtn.addEventListener("click", function () {
693
- startNewSession(mobileDefaultVendor);
694
- closeMobileSheet();
695
- });
696
- newRow.appendChild(mainBtn);
697
-
698
680
  var vendorList = document.createElement("div");
699
681
  vendorList.className = "mobile-vendor-list hidden";
700
-
701
- var chevronBtn = document.createElement("button");
702
- chevronBtn.className = "mobile-session-new mobile-session-new-chevron";
703
- chevronBtn.setAttribute("aria-label", "Choose a vendor");
704
- chevronBtn.innerHTML = iconHtml("chevron-down");
705
- chevronBtn.addEventListener("click", function () {
706
- vendorList.classList.toggle("hidden");
707
- chevronBtn.classList.toggle("expanded");
682
+ vendorList.id = "mobile-session-provider-menu";
683
+ var mobileVendorName = VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor;
684
+ var mobileCreateTitle = "Create a " + mobileVendorName + " session";
685
+ if (defaultVendorUnavailable(mobileInstalled)) mobileCreateTitle += " (saved default is unavailable; using installed priority)";
686
+ var controls;
687
+ controls = renderSessionCreationCta({
688
+ rowClass: "mobile-session-new-row",
689
+ createClass: "mobile-session-new mobile-session-new-main",
690
+ providerClass: "mobile-session-new mobile-session-new-provider",
691
+ iconClass: "mobile-session-new-icon",
692
+ vendorName: mobileVendorName,
693
+ vendorAvatar: VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude,
694
+ createTitle: mobileCreateTitle,
695
+ providerTitle: "Choose an AI provider",
696
+ menuId: vendorList.id,
697
+ onCreate: function () { startNewSession(mobileDefaultVendor); closeMobileSheet(); },
698
+ onProvider: function () {
699
+ var opening = vendorList.classList.contains("hidden");
700
+ vendorList.classList.toggle("hidden", !opening);
701
+ controls.providerButton.classList.toggle("expanded", opening);
702
+ controls.providerButton.setAttribute("aria-expanded", opening ? "true" : "false");
703
+ if (opening) {
704
+ var first = vendorList.querySelector("button:not(:disabled)");
705
+ if (first) first.focus();
706
+ }
707
+ },
708
+ });
709
+ var newRow = controls.element;
710
+ vendorList.addEventListener("keydown", function (e) {
711
+ if (e.key !== "Escape") return;
712
+ e.preventDefault();
713
+ vendorList.classList.add("hidden");
714
+ controls.providerButton.classList.remove("expanded");
715
+ controls.providerButton.setAttribute("aria-expanded", "false");
716
+ controls.providerButton.focus();
708
717
  });
709
- newRow.appendChild(chevronBtn);
710
718
 
711
719
  if (defaultVendorUnavailable(mobileInstalled) && mobileDefaultState.preference) {
712
720
  var mobileFallbackNote = document.createElement("div");
@@ -727,7 +735,7 @@ function renderMobileSessionsInto(container) {
727
735
  if (!isInstalled) vBtn.classList.add("disabled");
728
736
  if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
729
737
  vBtn.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="mobile-session-new-icon" alt="">' +
730
- '<span>' + name + '</span>' +
738
+ '<span>' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
731
739
  (isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
732
740
  (isInstalled ? "" : '<span class="mobile-vendor-note">Not installed</span>');
733
741
  vBtn.addEventListener("click", function () {
@@ -735,6 +743,9 @@ function renderMobileSessionsInto(container) {
735
743
  window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
736
744
  return;
737
745
  }
746
+ vendorList.classList.add("hidden");
747
+ controls.providerButton.classList.remove("expanded");
748
+ controls.providerButton.setAttribute("aria-expanded", "false");
738
749
  startNewSession(vendor);
739
750
  closeMobileSheet();
740
751
  });
@@ -17,6 +17,7 @@ import { openGroup, separateGroup } from './split-view.js';
17
17
  import { groupedSessionIds } from './split-group-helpers.js';
18
18
  import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
19
19
  import { openPairDialog } from './split-pair-ui.js';
20
+ import { renderSessionCreationCta } from './session-creation-cta.js';
20
21
  import {
21
22
  hierarchyItemMatches,
22
23
  prepareSidebarHierarchy,
@@ -53,10 +54,11 @@ var armedDeleteTimer = null;
53
54
  store.subscribe(function (state, previous) {
54
55
  if (state.defaultVendorState !== previous.defaultVendorState || state.installedVendors !== previous.installedVendors) {
55
56
  var reopenNewMenu = !!(sessionCtxMenu && sessionCtxMenuAnchor);
57
+ var sessionList = getSessionListEl();
56
58
  if (reopenNewMenu) closeSessionCtxMenu();
57
- if (getSessionListEl()) renderSessionList();
58
- if (reopenNewMenu) {
59
- var freshAnchor = getSessionListEl().querySelector(".session-top-action.split-chevron");
59
+ if (sessionList) renderSessionList();
60
+ if (reopenNewMenu && sessionList) {
61
+ var freshAnchor = sessionList.querySelector(".session-top-action.split-provider");
60
62
  if (freshAnchor) showNewSessionMenu(freshAnchor, resolveDefaultVendor());
61
63
  }
62
64
  }
@@ -356,58 +358,49 @@ export function startNewSession(vendor, extra) {
356
358
  });
357
359
  }
358
360
 
359
- function renderSessionTopActions() {
361
+ export function renderSessionTopActions() {
360
362
  var wrap = document.createElement("div");
361
363
  wrap.className = "session-top-actions";
362
364
 
363
- // Single split button. The main half creates a session with the project's
364
- // last-used vendor (for Claude the server applies the claudeOpenMode pref);
365
- // the chevron opens the vendor picker plus alternate launch modes.
366
365
  var defaultVendor = resolveDefaultVendor();
367
-
368
- var cell = document.createElement("div");
369
- cell.className = "session-top-action-split";
370
-
371
- var mainBtn = document.createElement("button");
372
- mainBtn.className = "session-top-action split-main";
373
- mainBtn.type = "button";
374
- mainBtn.title = "New " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + " session";
375
- if (defaultVendorUnavailable(store.get('installedVendors') || [])) mainBtn.title += " (saved default is unavailable; using installed priority)";
376
- mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude) +
377
- '" class="session-top-action-icon" alt=""><span>New session</span>';
378
- mainBtn.addEventListener("click", function () {
379
- startNewSession(defaultVendor);
380
- });
381
- cell.appendChild(mainBtn);
382
-
383
- var chevron = document.createElement("button");
384
- chevron.className = "session-top-action split-chevron";
385
- chevron.type = "button";
386
- chevron.title = "Choose a vendor or launch mode";
387
- chevron.setAttribute("aria-label", "Choose a vendor or launch mode");
388
- chevron.innerHTML = iconHtml("chevron-down");
389
- chevron.addEventListener("click", function (e) {
390
- e.preventDefault();
391
- e.stopPropagation();
392
- if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
393
- showNewSessionMenu(chevron, defaultVendor);
366
+ var vendorName = VENDOR_NAMES[defaultVendor] || defaultVendor;
367
+ var createTitle = "Create a " + vendorName + " session";
368
+ if (defaultVendorUnavailable(store.get('installedVendors') || [])) createTitle += " (saved default is unavailable; using installed priority)";
369
+ var controls;
370
+ controls = renderSessionCreationCta({
371
+ rowClass: "session-top-action-split",
372
+ createClass: "session-top-action split-main",
373
+ providerClass: "session-top-action split-provider",
374
+ iconClass: "session-top-action-icon",
375
+ vendorName: vendorName,
376
+ vendorAvatar: VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude,
377
+ createTitle: createTitle,
378
+ providerTitle: "Choose an AI provider or launch mode",
379
+ menuId: "session-new-menu",
380
+ onCreate: function () { startNewSession(defaultVendor); },
381
+ onProvider: function (e) {
382
+ e.preventDefault();
383
+ e.stopPropagation();
384
+ if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
385
+ showNewSessionMenu(controls.providerButton, defaultVendor);
386
+ },
394
387
  });
395
- cell.appendChild(chevron);
396
-
397
- wrap.appendChild(cell);
388
+ wrap.appendChild(controls.element);
398
389
  return wrap;
399
390
  }
400
391
 
401
- // Dropdown anchored to the split-button chevron. Reuses the session-ctx-menu
402
- // element/var so the global document click handler closes it.
392
+ // Provider disclosure anchored to the grouped provider control. Reuses the
393
+ // session-ctx-menu element/var so the global document click handler closes it.
403
394
  function showNewSessionMenu(anchorBtn, defaultVendor) {
404
395
  closeSessionCtxMenu();
405
396
 
406
397
  var menu = document.createElement("div");
407
398
  menu.className = "session-ctx-menu session-new-menu";
399
+ menu.id = "session-new-menu";
408
400
  var installed = store.get('installedVendors') || [];
409
401
  var defaultVendorState = store.get('defaultVendorState') || {};
410
402
  sessionCtxMenuAnchor = anchorBtn;
403
+ anchorBtn.setAttribute("aria-expanded", "true");
411
404
 
412
405
  if (defaultVendorUnavailable(installed) && defaultVendorState.preference) {
413
406
  var fallbackNote = document.createElement("div");
@@ -430,7 +423,7 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
430
423
  if (vendor === defaultVendor) item.classList.add("active");
431
424
  item.type = "button";
432
425
  item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
433
- '<span class="session-new-vendor-name">' + name + '</span>' +
426
+ '<span class="session-new-vendor-name">' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
434
427
  (isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
435
428
  (isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
436
429
  item.title = isInstalled
@@ -526,6 +519,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
526
519
 
527
520
  document.body.appendChild(menu);
528
521
  sessionCtxMenu = menu;
522
+ menu.addEventListener("keydown", function (e) {
523
+ if (e.key !== "Escape") return;
524
+ e.preventDefault();
525
+ e.stopPropagation();
526
+ closeSessionCtxMenu(true);
527
+ });
529
528
  refreshIcons();
530
529
 
531
530
  requestAnimationFrame(function () {
@@ -542,6 +541,8 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
542
541
  if (menuRect.bottom > window.innerHeight - 8) {
543
542
  menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
544
543
  }
544
+ var firstMenuItem = menu.querySelector("button:not(:disabled)");
545
+ if (firstMenuItem) firstMenuItem.focus();
545
546
  });
546
547
  }
547
548
 
@@ -718,13 +719,16 @@ export function getExpandedLoopRuns() {
718
719
 
719
720
  // --- Context menu ---
720
721
 
721
- function closeSessionCtxMenu() {
722
+ function closeSessionCtxMenu(restoreFocus) {
723
+ var anchor = sessionCtxMenuAnchor;
724
+ if (anchor) anchor.setAttribute("aria-expanded", "false");
722
725
  sessionCtxMenuAnchor = null;
723
726
  if (sessionCtxMenu) {
724
727
  sessionCtxMenu.remove();
725
728
  sessionCtxMenu = null;
726
729
  sessionCtxSessionId = null;
727
730
  }
731
+ if (restoreFocus && anchor && document.contains(anchor)) anchor.focus();
728
732
  }
729
733
 
730
734
  function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
package/lib/sessions.js CHANGED
@@ -144,17 +144,32 @@ function createSessionManager(opts) {
144
144
  }
145
145
  }
146
146
 
147
- function sessionFilePath(cliSessionId) {
148
- return path.join(sessionsDir, cliSessionId + ".jsonl");
147
+ function sessionFilePath(recordId) {
148
+ return path.join(sessionsDir, recordId + ".jsonl");
149
+ }
150
+
151
+ // A Clay record must exist before a provider assigns its resumable id.
152
+ // Existing records keep their filename; fresh provider-less sessions use
153
+ // the already durable origin id and later store cliSessionId as metadata.
154
+ function sessionRecordId(session) {
155
+ if (!session) return null;
156
+ return session._sessionRecordId || session.cliSessionId || session.sessionOriginId || null;
157
+ }
158
+
159
+ function existingSessionRecordId(session) {
160
+ if (!session) return null;
161
+ return session._sessionRecordId || session.cliSessionId || null;
149
162
  }
150
163
 
151
164
  function saveSessionFile(session) {
152
- if (!session.cliSessionId) return false;
153
165
  try {
166
+ var provenanceMeta = sessionProvenance.metadata(session);
167
+ var recordId = sessionRecordId(session);
168
+ if (!recordId) return false;
154
169
  var metaObj = {
155
170
  type: "meta",
156
171
  localId: session.localId,
157
- cliSessionId: session.cliSessionId,
172
+ cliSessionId: session.cliSessionId || null,
158
173
  title: session.title,
159
174
  titleProvisional: session.titleProvisional === true,
160
175
  titleAutoGenerated: session.titleAutoGenerated === true,
@@ -204,7 +219,6 @@ function createSessionManager(opts) {
204
219
  if (session.homeDebateInitialTopic) metaObj.homeDebateInitialTopic = session.homeDebateInitialTopic;
205
220
  if (session.homeClayEntryRequestId) metaObj.homeClayEntryRequestId = session.homeClayEntryRequestId;
206
221
  if (session.homeClayEntryMode) metaObj.homeClayEntryMode = session.homeClayEntryMode;
207
- var provenanceMeta = sessionProvenance.metadata(session);
208
222
  metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
209
223
  if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
210
224
  if (Array.isArray(session._workerGenerations)) metaObj.workerGenerations = session._workerGenerations;
@@ -213,7 +227,7 @@ function createSessionManager(opts) {
213
227
  for (var i = 0; i < session.history.length; i++) {
214
228
  lines.push(JSON.stringify(session.history[i]));
215
229
  }
216
- var sfPath = sessionFilePath(session.cliSessionId);
230
+ var sfPath = sessionFilePath(recordId);
217
231
  // Atomic write: write to temp file then rename, so a crash mid-write
218
232
  // cannot leave a truncated/corrupted session file.
219
233
  var tmpPath = sfPath + ".tmp." + process.pid;
@@ -222,6 +236,7 @@ function createSessionManager(opts) {
222
236
  try { fs.chmodSync(tmpPath, 0o600); } catch (chmodErr) {}
223
237
  }
224
238
  fs.renameSync(tmpPath, sfPath);
239
+ session._sessionRecordId = recordId;
225
240
  return true;
226
241
  } catch(e) {
227
242
  console.error("[session] Failed to save session file:", e.message);
@@ -230,10 +245,11 @@ function createSessionManager(opts) {
230
245
  }
231
246
 
232
247
  function appendToSessionFile(session, obj) {
233
- if (!session.cliSessionId) return true;
248
+ var recordId = existingSessionRecordId(session);
249
+ if (!recordId) return true;
234
250
  session.lastActivity = Date.now();
235
251
  try {
236
- var afPath = sessionFilePath(session.cliSessionId);
252
+ var afPath = sessionFilePath(recordId);
237
253
  fs.appendFileSync(afPath, JSON.stringify(obj) + "\n");
238
254
  if (process.platform !== "win32") {
239
255
  try { fs.chmodSync(afPath, 0o600); } catch (chmodErr) {}
@@ -276,7 +292,8 @@ function createSessionManager(opts) {
276
292
 
277
293
  var meta;
278
294
  try { meta = JSON.parse(lines[0]); } catch { continue; }
279
- if (meta.type !== "meta" || !meta.cliSessionId) continue;
295
+ var pendingOrigin = typeof meta.sessionOriginId === "string" && meta.sessionOriginId.trim() && meta.sessionOriginId.length <= 128;
296
+ if (meta.type !== "meta" || (!meta.cliSessionId && !pendingOrigin)) continue;
280
297
 
281
298
  var history = [];
282
299
  for (var j = 1; j < lines.length; j++) {
@@ -285,7 +302,7 @@ function createSessionManager(opts) {
285
302
 
286
303
  var fileMtime = 0;
287
304
  try { fileMtime = fs.statSync(path.join(sessionsDir, files[i])).mtimeMs; } catch {}
288
- loaded.push({ meta: meta, history: history, mtime: fileMtime });
305
+ loaded.push({ meta: meta, history: history, mtime: fileMtime, recordId: files[i].slice(0, -".jsonl".length) });
289
306
  }
290
307
 
291
308
  loaded.sort(function(a, b) { return a.meta.createdAt - b.meta.createdAt; });
@@ -310,6 +327,7 @@ function createSessionManager(opts) {
310
327
  queryInstance: null,
311
328
  messageQueue: null,
312
329
  cliSessionId: m.cliSessionId,
330
+ _sessionRecordId: loaded[i].recordId,
313
331
  blocks: {},
314
332
  sentToolResults: {},
315
333
  pendingPermissions: {},
@@ -388,7 +406,7 @@ function createSessionManager(opts) {
388
406
  saveSessionFile(session);
389
407
  try {
390
408
  var preservedTime = new Date(activityTime);
391
- fs.utimesSync(sessionFilePath(session.cliSessionId), preservedTime, preservedTime);
409
+ fs.utimesSync(sessionFilePath(sessionRecordId(session)), preservedTime, preservedTime);
392
410
  } catch (e) {}
393
411
  delete session._provenanceMigrationNeeded;
394
412
  });
@@ -917,10 +935,9 @@ function createSessionManager(opts) {
917
935
  session.worker = null;
918
936
  }
919
937
 
920
- if (session.cliSessionId) {
921
- rememberDeletedCliSession(session.cliSessionId);
922
- try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
923
- }
938
+ if (session.cliSessionId) rememberDeletedCliSession(session.cliSessionId);
939
+ var recordId = existingSessionRecordId(session);
940
+ if (recordId) try { fs.unlinkSync(sessionFilePath(recordId)); } catch(e) {}
924
941
 
925
942
  sessions.delete(localId);
926
943
  try { onSessionDeleted(localId, session); } catch (e) {
@@ -964,10 +981,9 @@ function createSessionManager(opts) {
964
981
  try { session.worker.kill(); } catch(e) {}
965
982
  session.worker = null;
966
983
  }
967
- if (session.cliSessionId) {
968
- rememberDeletedCliSession(session.cliSessionId);
969
- try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
970
- }
984
+ if (session.cliSessionId) rememberDeletedCliSession(session.cliSessionId);
985
+ var recordId = existingSessionRecordId(session);
986
+ if (recordId) try { fs.unlinkSync(sessionFilePath(recordId)); } catch(e) {}
971
987
  sessions.delete(localId);
972
988
  try { onSessionDeleted(localId, session); } catch (e) {
973
989
  console.error("[session] Session deletion hook failed:", e.message || e);
@@ -1093,7 +1109,8 @@ function createSessionManager(opts) {
1093
1109
  }
1094
1110
 
1095
1111
  function sendAndRecordDurably(session, obj, clientObj) {
1096
- if (!session || !session.cliSessionId) return false;
1112
+ var recordId = existingSessionRecordId(session);
1113
+ if (!recordId || !fs.existsSync(sessionFilePath(recordId))) return false;
1097
1114
  var saved;
1098
1115
  try { saved = doSendAndRecord(session, obj, clientObj); }
1099
1116
  catch (error) {
@@ -1107,9 +1124,10 @@ function createSessionManager(opts) {
1107
1124
  }
1108
1125
 
1109
1126
  function hasDurableSessionRecord(session, predicate) {
1110
- if (!session || !session.cliSessionId || typeof predicate !== "function") return false;
1127
+ var recordId = existingSessionRecordId(session);
1128
+ if (!recordId || typeof predicate !== "function") return false;
1111
1129
  try {
1112
- var lines = fs.readFileSync(sessionFilePath(session.cliSessionId), "utf8").split("\n");
1130
+ var lines = fs.readFileSync(sessionFilePath(recordId), "utf8").split("\n");
1113
1131
  for (var i = lines.length - 1; i >= 0; i--) {
1114
1132
  if (!lines[i]) continue;
1115
1133
  try { if (predicate(JSON.parse(lines[i]))) return true; } catch (error) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.3.0-beta.6",
3
+ "version": "4.3.0-beta.8",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"