clay-server 3.4.0-beta.2 → 3.4.0-beta.21

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 (82) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +59 -59
  3. package/lib/project-connection.js +35 -7
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-mate-interaction.js +21 -1
  6. package/lib/project-memory.js +3 -0
  7. package/lib/project-models.js +220 -0
  8. package/lib/project-session-handoff.js +162 -0
  9. package/lib/project-session-pair.js +14 -7
  10. package/lib/project-session-spawn.js +1 -1
  11. package/lib/project-sessions.js +13 -45
  12. package/lib/project-worker-proposal.js +51 -14
  13. package/lib/project.js +38 -80
  14. package/lib/public/antigravity-avatar.png +0 -0
  15. package/lib/public/app.js +29 -0
  16. package/lib/public/copilot-avatar.svg +5 -0
  17. package/lib/public/css/command-palette.css +22 -2
  18. package/lib/public/css/icon-strip.css +29 -13
  19. package/lib/public/css/input.css +56 -0
  20. package/lib/public/css/mates.css +6 -0
  21. package/lib/public/css/menus.css +38 -25
  22. package/lib/public/css/messages.css +9 -0
  23. package/lib/public/css/pane.css +16 -3
  24. package/lib/public/css/pwa-mobile.css +17 -0
  25. package/lib/public/css/session-actions.css +98 -0
  26. package/lib/public/css/title-bar.css +19 -0
  27. package/lib/public/grok-avatar.svg +4 -0
  28. package/lib/public/index.html +29 -7
  29. package/lib/public/junie-avatar.svg +11 -0
  30. package/lib/public/kimi-avatar.svg +4 -0
  31. package/lib/public/modules/agent-config-selects.js +69 -0
  32. package/lib/public/modules/app-header.js +4 -22
  33. package/lib/public/modules/app-messages.js +54 -11
  34. package/lib/public/modules/app-panels.js +36 -83
  35. package/lib/public/modules/app-projects.js +3 -1
  36. package/lib/public/modules/app-rendering.js +19 -4
  37. package/lib/public/modules/background-tasks-ui.js +55 -0
  38. package/lib/public/modules/mate-sidebar.js +11 -3
  39. package/lib/public/modules/model-picker.js +263 -0
  40. package/lib/public/modules/notifications.js +7 -1
  41. package/lib/public/modules/pane-bridge.js +11 -0
  42. package/lib/public/modules/pane-links.js +23 -0
  43. package/lib/public/modules/project-switcher.js +7 -6
  44. package/lib/public/modules/session-actions.js +294 -0
  45. package/lib/public/modules/sidebar-mates.js +1 -1
  46. package/lib/public/modules/sidebar-mobile.js +2 -1
  47. package/lib/public/modules/sidebar-projects.js +34 -43
  48. package/lib/public/modules/sidebar-sessions.js +20 -6
  49. package/lib/public/modules/split-pair-ui.js +16 -77
  50. package/lib/public/modules/split-view.js +3 -0
  51. package/lib/public/modules/tools.js +6 -1
  52. package/lib/public/modules/vendor-priority.js +14 -0
  53. package/lib/public/modules/vendor-selection.js +20 -0
  54. package/lib/public/modules/worker-proposal.js +6 -1
  55. package/lib/public/modules/worktree-location.js +17 -0
  56. package/lib/public/qwen-avatar.svg +4 -0
  57. package/lib/public/style.css +4 -2
  58. package/lib/sdk-bridge.js +103 -56
  59. package/lib/sdk-message-processor.js +26 -4
  60. package/lib/session-handoff-context.js +110 -0
  61. package/lib/session-notes-mcp-server.js +8 -1
  62. package/lib/sessions.js +4 -0
  63. package/lib/worktree.js +9 -4
  64. package/lib/ws-schema.js +9 -1
  65. package/lib/yoke/acp-agent-profiles.js +64 -14
  66. package/lib/yoke/adapters/antigravity.js +417 -0
  67. package/lib/yoke/adapters/claude.js +34 -0
  68. package/lib/yoke/adapters/codex.js +101 -5
  69. package/lib/yoke/adapters/copilot.js +7 -0
  70. package/lib/yoke/adapters/grok.js +7 -0
  71. package/lib/yoke/adapters/junie.js +7 -0
  72. package/lib/yoke/adapters/kimi.js +7 -0
  73. package/lib/yoke/adapters/kiro.js +2 -2
  74. package/lib/yoke/adapters/qwen.js +7 -0
  75. package/lib/yoke/codex-app-server.js +25 -8
  76. package/lib/yoke/index.js +67 -28
  77. package/lib/yoke/instructions.js +3 -0
  78. package/lib/yoke/interface.js +12 -0
  79. package/lib/yoke/vendor-registry.js +61 -6
  80. package/package.json +1 -1
  81. package/lib/public/gemini-avatar.svg +0 -11
  82. package/lib/yoke/adapters/gemini.js +0 -7
package/lib/sdk-bridge.js CHANGED
@@ -110,6 +110,7 @@ function createSDKBridge(opts) {
110
110
  var adapters = opts.adapters || {};
111
111
  var mateDisplayName = opts.mateDisplayName || "";
112
112
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
113
+ var osUsers = !!opts.osUsers;
113
114
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
114
115
  // mcpServers may be either a static object or a getter function. The
115
116
  // getter form lets callers gate individual servers at call time (e.g.
@@ -244,17 +245,24 @@ function createSDKBridge(opts) {
244
245
  return null;
245
246
  }
246
247
 
247
- function sendModelInfoForVendor(vendor, model) {
248
+ function defaultModelForVendor(vendor) {
249
+ return (sm.defaultModelByVendor && sm.defaultModelByVendor[vendor]) || "";
250
+ }
251
+
252
+ function sendModelInfoForVendor(session, vendor, model) {
248
253
  var resolvedVendor = vendor || (adapter && adapter.vendor) || "claude";
249
- send({
254
+ var message = {
250
255
  type: "model_info",
251
256
  model: model || "",
252
257
  models: getModelsForVendor(resolvedVendor),
253
258
  vendor: resolvedVendor,
259
+ sessionId: session ? session.localId : null,
254
260
  capabilities: (sm.capabilitiesByVendor && sm.capabilitiesByVendor[resolvedVendor]) || {},
255
261
  availableVendors: sm.availableVendors || [],
256
262
  installedVendors: sm.installedVendors || [],
257
- });
263
+ };
264
+ if (session) sendToSession(session, message);
265
+ else send(message);
258
266
  }
259
267
 
260
268
  function rememberAdapterReady(vendor, result) {
@@ -323,6 +331,14 @@ function createSDKBridge(opts) {
323
331
  var mergeSkills = skills.mergeSkills;
324
332
 
325
333
  // --- Message processing (extracted to sdk-message-processor.js) ---
334
+ function getSessionLinuxUser(session) {
335
+ if (!osUsers || !session) return null;
336
+ if (session.lastLinuxUser) return session.lastLinuxUser;
337
+ if (!session.ownerId) return null;
338
+ var owner = usersModule.findUserById(session.ownerId);
339
+ return owner && owner.linuxUser ? owner.linuxUser : null;
340
+ }
341
+
326
342
  // Auto-generate a session title via YOKE adapter.generateTitle().
327
343
  // Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
328
344
  function autoGenerateTitle(session) {
@@ -345,7 +361,10 @@ function createSDKBridge(opts) {
345
361
  }
346
362
  console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
347
363
 
348
- sessionAdapter.generateTitle(userMessages, { cwd: cwd }).then(function(title) {
364
+ sessionAdapter.generateTitle(userMessages, {
365
+ cwd: cwd,
366
+ linuxUser: getSessionLinuxUser(session) || undefined,
367
+ }).then(function(title) {
349
368
  if (!title || title.length < 2) return;
350
369
  title = title.substring(0, 100);
351
370
  if (!session.titleManuallySet) {
@@ -837,19 +856,20 @@ function createSDKBridge(opts) {
837
856
  case "model_changed":
838
857
  session.model = metaData.model;
839
858
  sm.saveSessionFile(session);
840
- sm.currentModel = metaData.model;
841
- sendModelInfoForVendor(session.vendor || (adapter && adapter.vendor) || "claude", metaData.model);
842
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
859
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
860
+ sm.defaultModelByVendor[session.vendor || "claude"] = metaData.model;
861
+ sendModelInfoForVendor(session, session.vendor || (adapter && adapter.vendor) || "claude", metaData.model);
862
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
843
863
  break;
844
864
  case "effort_changed":
845
865
  session.effort = metaData.effort;
846
866
  sm.saveSessionFile(session);
847
867
  sm.currentEffort = metaData.effort;
848
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
868
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort, betas: sm.currentBetas || [] });
849
869
  break;
850
870
  case "permission_mode_changed":
851
871
  sm.currentPermissionMode = metaData.mode;
852
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
872
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
853
873
  break;
854
874
  case "worker_error":
855
875
  send({ type: "error", text: metaData.error });
@@ -865,7 +885,10 @@ function createSDKBridge(opts) {
865
885
  console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
866
886
  var stillOwnsRuntime = session.queryInstance === myQueryInstance;
867
887
  if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
888
+ session._lastTurnInterrupted = true;
868
889
  session.isProcessing = false;
890
+ session._awaitingTurnResult = false;
891
+ session._queuedTurnCount = 0;
869
892
  onProcessingChanged();
870
893
  send({ type: "status", processing: false });
871
894
  sendAndRecord(session, { type: "thinking_stop" });
@@ -880,6 +903,8 @@ function createSDKBridge(opts) {
880
903
  // iterator without a result event. Treat any result-less stream end as
881
904
  // a failed turn so the UI cannot remain on processing forever.
882
905
  session.isProcessing = false;
906
+ session._awaitingTurnResult = false;
907
+ session._queuedTurnCount = 0;
883
908
  onProcessingChanged();
884
909
  if (!session._lastAdapterError) {
885
910
  sendAndRecord(session, {
@@ -894,8 +919,11 @@ function createSDKBridge(opts) {
894
919
  } catch (err) {
895
920
  if (session.isProcessing && session.queryInstance === myQueryInstance) {
896
921
  session.isProcessing = false;
922
+ session._awaitingTurnResult = false;
923
+ session._queuedTurnCount = 0;
897
924
  onProcessingChanged();
898
925
  if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
926
+ session._lastTurnInterrupted = true;
899
927
  if (!session.destroying) {
900
928
  sendAndRecord(session, { type: "thinking_stop" });
901
929
  var interruptMsg2 = (session.vendor && session.vendor !== "claude")
@@ -1131,7 +1159,9 @@ function createSDKBridge(opts) {
1131
1159
  async function rollbackConversation(session, numTurns) {
1132
1160
  var sessionAdapter = getAdapterForSession(session);
1133
1161
  if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
1134
- await sessionAdapter.rollbackThread(session.cliSessionId, numTurns);
1162
+ await sessionAdapter.rollbackThread(session.cliSessionId, numTurns, {
1163
+ linuxUser: getSessionLinuxUser(session) || undefined,
1164
+ });
1135
1165
  }
1136
1166
  // Claude: conversation rollback is handled by rewindFiles + local history trim
1137
1167
  }
@@ -1143,7 +1173,11 @@ function createSDKBridge(opts) {
1143
1173
 
1144
1174
  async function forkSessionUnified(session, uuid) {
1145
1175
  var sessionAdapter = getAdapterForSession(session);
1146
- var result = await sessionAdapter.forkSession(session.cliSessionId, { upToMessageId: uuid, dir: cwd });
1176
+ var result = await sessionAdapter.forkSession(session.cliSessionId, {
1177
+ upToMessageId: uuid,
1178
+ dir: cwd,
1179
+ linuxUser: getSessionLinuxUser(session) || undefined,
1180
+ });
1147
1181
  if (!result || !result.sessionId) throw new Error("Fork returned no session id");
1148
1182
 
1149
1183
  // Adapters with rollbackThread (e.g. Codex) use local history copy
@@ -1157,6 +1191,7 @@ function createSDKBridge(opts) {
1157
1191
  // Wrapper: marks the boot window so pushMessage can buffer instead of
1158
1192
  // dropping messages that arrive while createQuery is still awaiting.
1159
1193
  async function startQuery(session, text, images, linuxUser) {
1194
+ delete session._lastTurnInterrupted;
1160
1195
  session._queryStarting = true;
1161
1196
  try {
1162
1197
  return await startQueryInner(session, text, images, linuxUser);
@@ -1208,10 +1243,10 @@ function createSDKBridge(opts) {
1208
1243
  sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
1209
1244
  }
1210
1245
  var vendorModels = getModelsForVendor(vendor);
1211
- var modelForVendor = resolveModelInList(vendorModels, sm.currentModel)
1246
+ var modelForVendor = resolveModelInList(vendorModels, defaultModelForVendor(vendor))
1212
1247
  || modelEntryValue(vendorModels[0])
1213
1248
  || "";
1214
- sendModelInfoForVendor(vendor, modelForVendor);
1249
+ sendModelInfoForVendor(session, vendor, modelForVendor);
1215
1250
  }
1216
1251
  return vendorAdapter;
1217
1252
  }
@@ -1414,14 +1449,9 @@ function createSDKBridge(opts) {
1414
1449
  }
1415
1450
  }
1416
1451
 
1417
- // Pick a model that belongs to the session's vendor. sm.currentModel is
1418
- // shared project-wide, so a Codex session that last set it to
1419
- // "gpt-5.4-mini" would otherwise leak into a Claude session in the same
1420
- // project (or in another session that switches vendor to claude) and
1421
- // Claude would reject the unknown model. We validate against the
1422
- // session vendor's model list regardless of which vendor happens to be
1423
- // the project's default adapter.
1424
- var queryModel = (ls.model && ls.model !== "default" ? ls.model : null) || session.model || sm.currentModel || undefined;
1452
+ // Pick a model from the session or its vendor-specific default, then
1453
+ // validate it against the session vendor's known model list.
1454
+ var queryModel = (ls.model && ls.model !== "default" ? ls.model : null) || session.model || defaultModelForVendor(session.vendor || "claude") || undefined;
1425
1455
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1426
1456
  if (sessionVendor) {
1427
1457
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
@@ -1437,8 +1467,7 @@ function createSDKBridge(opts) {
1437
1467
  }
1438
1468
 
1439
1469
  // Bind the resolved model to the session so it survives a daemon
1440
- // restart. Without this, a session started under sm.currentModel (an
1441
- // in-memory value) came back after restart on the adapter default.
1470
+ // restart. This preserves a session-specific selection across restarts.
1442
1471
  if (queryModel && session.model !== queryModel) {
1443
1472
  session.model = queryModel;
1444
1473
  sm.saveSessionFile(session);
@@ -1471,6 +1500,7 @@ function createSDKBridge(opts) {
1471
1500
 
1472
1501
  var queryOpts = {
1473
1502
  cwd: cwd,
1503
+ linuxUser: linuxUser || undefined,
1474
1504
  // appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
1475
1505
  // REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
1476
1506
  // would strip the driver session down to the two-sentence pair preset.
@@ -1528,6 +1558,10 @@ function createSDKBridge(opts) {
1528
1558
  ACP: {
1529
1559
  mcpServers: getAcpMcpServers(session),
1530
1560
  },
1561
+ ANTIGRAVITY: {
1562
+ dangerouslySkipPermissions: dangerouslySkipPermissions
1563
+ || session.permissionMode === "bypassPermissions",
1564
+ },
1531
1565
  KIRO: {
1532
1566
  engine: kiroConfig.engine,
1533
1567
  mode: kiroConfig.mode,
@@ -1561,6 +1595,8 @@ function createSDKBridge(opts) {
1561
1595
  console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
1562
1596
  console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
1563
1597
  session.isProcessing = false;
1598
+ session._awaitingTurnResult = false;
1599
+ session._queuedTurnCount = 0;
1564
1600
  onProcessingChanged();
1565
1601
  session.queryInstance = null;
1566
1602
  session.messageQueue = null;
@@ -1602,6 +1638,8 @@ function createSDKBridge(opts) {
1602
1638
  session.messageQueue = null;
1603
1639
  session.abortController = null;
1604
1640
  session.isProcessing = false;
1641
+ session._awaitingTurnResult = false;
1642
+ session._queuedTurnCount = 0;
1605
1643
  onProcessingChanged();
1606
1644
  sendAndRecord(session, { type: "error", text: "The agent connection closed before it received your message. Please send it again." });
1607
1645
  sendAndRecord(session, { type: "done", code: 1 });
@@ -1672,6 +1710,7 @@ function createSDKBridge(opts) {
1672
1710
  console.error("[sdk-bridge] QueryHandle rejected message for session " + session.localId + ":", e.message || e);
1673
1711
  }
1674
1712
  if (delivered) {
1713
+ delete session._lastTurnInterrupted;
1675
1714
  if (session._awaitingTurnResult) {
1676
1715
  session._queuedTurnCount = (session._queuedTurnCount || 0) + 1;
1677
1716
  } else {
@@ -1790,16 +1829,20 @@ function createSDKBridge(opts) {
1790
1829
  } catch (e) {}
1791
1830
  if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
1792
1831
 
1793
- var acpVendorKeys = ["gemini", "opencode"];
1832
+ var subprocessVendorKeys = ["antigravity", "opencode", "kimi", "grok", "copilot", "qwen", "junie"];
1794
1833
  var acpProfiles = require("./yoke/acp-agent-profiles");
1795
- for (var acpVendorIndex = 0; acpVendorIndex < acpVendorKeys.length; acpVendorIndex++) {
1796
- var acpVendor = acpVendorKeys[acpVendorIndex];
1797
- var acpInfo = yoke.getVendorInfo(acpVendor);
1798
- if (!linuxUser || acpInfo.osUserIsolation) {
1799
- var acpProfile = acpProfiles.getAcpAgentProfile(acpVendor);
1800
- if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(acpVendor);
1834
+ for (var subprocessVendorIndex = 0; subprocessVendorIndex < subprocessVendorKeys.length; subprocessVendorIndex++) {
1835
+ var subprocessVendor = subprocessVendorKeys[subprocessVendorIndex];
1836
+ var subprocessInfo = yoke.getVendorInfo(subprocessVendor);
1837
+ if (!linuxUser || subprocessInfo.osUserIsolation) {
1838
+ if (subprocessVendor === "antigravity") {
1839
+ if (tryLookup(subprocessInfo.binaryName)) result.push(subprocessVendor);
1840
+ } else {
1841
+ var acpProfile = acpProfiles.getAcpAgentProfile(subprocessVendor);
1842
+ if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(subprocessVendor);
1843
+ }
1801
1844
  } else {
1802
- console.log("[sdk-bridge] " + acpInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1845
+ console.log("[sdk-bridge] " + subprocessInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
1803
1846
  }
1804
1847
  }
1805
1848
 
@@ -1832,6 +1875,13 @@ function createSDKBridge(opts) {
1832
1875
  var defaultVendor = adapter ? adapter.vendor : "claude";
1833
1876
  sm.defaultVendor = defaultVendor;
1834
1877
 
1878
+ // Installation detection is independent of adapter initialization. Publish
1879
+ // it before warmup so a new project does not render the initial empty state
1880
+ // as if every CLI were missing while the default adapter starts up.
1881
+ sm.installedVendors = detectInstalledVendors(linuxUser);
1882
+ sm.availableVendors = getAvailableVendors(linuxUser);
1883
+ sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
1884
+
1835
1885
  // Initialize default adapter first (provides skills, slash commands, etc.)
1836
1886
  if (adapter) {
1837
1887
  try {
@@ -1862,7 +1912,8 @@ function createSDKBridge(opts) {
1862
1912
  send({ type: "slash_commands", commands: combined, vendor: defaultVendor });
1863
1913
  }
1864
1914
  if (result.defaultModel) {
1865
- sm.currentModel = sm.currentModel || sm._savedDefaultModel || result.defaultModel;
1915
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1916
+ sm.defaultModelByVendor[defaultVendor] = sm.defaultModelByVendor[defaultVendor] || result.defaultModel;
1866
1917
  }
1867
1918
  sm.availableModels = result.models || [];
1868
1919
  // Store per-vendor models and capabilities
@@ -1883,19 +1934,8 @@ function createSDKBridge(opts) {
1883
1934
  // actually issues a query with it.
1884
1935
  sm.modelsByVendor = sm.modelsByVendor || {};
1885
1936
 
1886
- // Detect installed vendors per-user (binary existence check)
1887
- sm.installedVendors = detectInstalledVendors(linuxUser);
1888
- sm.availableVendors = getAvailableVendors(linuxUser);
1889
-
1890
- // Send initial state to client
1891
- send({
1892
- type: "model_info",
1893
- model: sm.currentModel || "",
1894
- models: getModelsForVendor(defaultVendor),
1895
- vendor: defaultVendor,
1896
- availableVendors: sm.availableVendors,
1897
- installedVendors: sm.installedVendors,
1898
- });
1937
+ // Send the fully initialized state to the client.
1938
+ sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
1899
1939
  }
1900
1940
 
1901
1941
  async function setModel(session, model) {
@@ -1907,22 +1947,28 @@ function createSDKBridge(opts) {
1907
1947
  // No active query — just store the model for next startQuery
1908
1948
  session.model = model;
1909
1949
  sm.saveSessionFile(session);
1910
- sm.currentModel = model;
1911
- // Don't send vendor here: session vendor not yet bound, let client keep its selection
1912
- sendModelInfoForVendor(null, model);
1913
- sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1914
- return;
1950
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1951
+ // Confirm against the session vendor so a non-default vendor does not
1952
+ // receive model metadata labeled as the project's default adapter.
1953
+ var pendingVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1954
+ sm.defaultModelByVendor[pendingVendor] = model;
1955
+ sendModelInfoForVendor(session, pendingVendor, model);
1956
+ sendToSession(session, { type: "config_state", model: model, vendor: pendingVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1957
+ return { ok: true, model: model };
1915
1958
  }
1916
1959
  try {
1917
1960
  await session.queryInstance.setModel(model);
1918
1961
  session.model = model;
1919
1962
  sm.saveSessionFile(session);
1920
- sm.currentModel = model;
1963
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
1921
1964
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || "claude";
1922
- sendModelInfoForVendor(sessionVendor, model);
1923
- sendToSession(session, { type: "config_state", model: sm.currentModel, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1965
+ sm.defaultModelByVendor[sessionVendor] = model;
1966
+ sendModelInfoForVendor(session, sessionVendor, model);
1967
+ sendToSession(session, { type: "config_state", model: model, vendor: sessionVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1968
+ return { ok: true, model: model };
1924
1969
  } catch (e) {
1925
1970
  send({ type: "error", text: "Failed to switch model: " + (e.message || e) });
1971
+ return { ok: false, model: session.model || "", error: e.message || String(e) };
1926
1972
  }
1927
1973
  }
1928
1974
 
@@ -1951,14 +1997,14 @@ function createSDKBridge(opts) {
1951
1997
  if (!session.queryInstance) {
1952
1998
  // No active query — just store the mode for next startQuery
1953
1999
  sm.currentPermissionMode = mode;
1954
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
2000
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1955
2001
  return;
1956
2002
  }
1957
2003
  try {
1958
2004
  // Route through QueryHandle (works for both in-process and worker paths)
1959
2005
  await session.queryInstance.setPermissionMode(mode);
1960
2006
  sm.currentPermissionMode = mode;
1961
- sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
2007
+ sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: sm.currentPermissionMode, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1962
2008
  } catch (e) {
1963
2009
  sendToSession(session, { type: "error", text: "Failed to set permission mode: " + (e.message || e) });
1964
2010
  }
@@ -2009,7 +2055,7 @@ function createSDKBridge(opts) {
2009
2055
  // Creates a mention session that can be reused across multiple mentions
2010
2056
  // within a conversation flow (session continuity).
2011
2057
  async function createMentionSession(opts) {
2012
- // opts: { vendor, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
2058
+ // opts: { vendor, linuxUser, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
2013
2059
  var abortController = new AbortController();
2014
2060
 
2015
2061
  // Current response callbacks (swapped on each pushMessage)
@@ -2029,6 +2075,7 @@ function createSDKBridge(opts) {
2029
2075
  try {
2030
2076
  handle = await mentionAdapter.createQuery({
2031
2077
  cwd: cwd,
2078
+ linuxUser: opts.linuxUser || undefined,
2032
2079
  systemPrompt: opts.claudeMd,
2033
2080
  model: opts.model || undefined,
2034
2081
  toolServers: opts.includeMcpServers ? (mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined) : undefined,
@@ -175,6 +175,10 @@ function attachMessageProcessor(ctx) {
175
175
 
176
176
  // Cache slash_commands and model from CLI init message
177
177
  if (parsed.yokeType === "init") {
178
+ var previousBackgroundTaskCount = (session.activeBackgroundTasks || []).length;
179
+ session.activeBackgroundTasks = [];
180
+ sendToSession(session, { type: "active_background_tasks", tasks: [] });
181
+ if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
178
182
  var fsSkills = discoverSkillDirs();
179
183
  sm.skillNames = mergeSkills(parsed.skills, fsSkills);
180
184
  if (parsed.slashCommands) {
@@ -192,13 +196,20 @@ function attachMessageProcessor(ctx) {
192
196
  send({ type: "slash_commands", commands: sm.slashCommands });
193
197
  }
194
198
  if (parsed.model) {
195
- sm.currentModel = sm.currentModel || sm._savedDefaultModel || parsed.model;
196
199
  var initVendor = session.vendor || (adapter && adapter.vendor) || "claude";
197
- send({
200
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
201
+ sm.defaultModelByVendor[initVendor] = sm.defaultModelByVendor[initVendor] || parsed.model;
202
+ var initModels = getModelsForVendor(initVendor);
203
+ var initModel = session.model || sm.defaultModelByVendor[initVendor] || "";
204
+ if (initModels.length > 0 && !initModels.some(function(entry) {
205
+ return entry === initModel || (entry && (entry.value === initModel || entry.id === initModel || entry.resolvedModel === initModel));
206
+ })) initModel = typeof initModels[0] === "string" ? initModels[0] : (initModels[0].value || initModels[0].id || "");
207
+ sendToSession(session, {
198
208
  type: "model_info",
199
- model: sm.currentModel,
200
- models: getModelsForVendor(initVendor),
209
+ model: initModel,
210
+ models: initModels,
201
211
  vendor: initVendor,
212
+ sessionId: session.localId,
202
213
  availableVendors: sm.availableVendors || [],
203
214
  installedVendors: sm.installedVendors || [],
204
215
  });
@@ -612,6 +623,15 @@ function attachMessageProcessor(ctx) {
612
623
  });
613
624
  }
614
625
 
626
+ } else if (parsed.yokeType === "background_tasks_changed") {
627
+ var previousBackgroundTaskCount = (session.activeBackgroundTasks || []).length;
628
+ var activeBackgroundTasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
629
+ session.activeBackgroundTasks = activeBackgroundTasks;
630
+ sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
631
+ if (previousBackgroundTaskCount !== activeBackgroundTasks.length) {
632
+ sm.broadcastSessionList();
633
+ }
634
+
615
635
  } else if (parsed.yokeType === "tool_progress") {
616
636
  // Sub-agent tool_progress: forward as activity update
617
637
  var parentId = parsed.parentToolId;
@@ -783,6 +803,8 @@ function attachMessageProcessor(ctx) {
783
803
  // app-server returned an unauthorized/token-revoked error). Trigger the
784
804
  // same login flow as the Claude login-prompt path.
785
805
  session.isProcessing = false;
806
+ session._awaitingTurnResult = false;
807
+ session._queuedTurnCount = 0;
786
808
  onProcessingChanged();
787
809
  emitAuthRequired(session);
788
810
 
@@ -0,0 +1,110 @@
1
+ var spawnSync = require("child_process").spawnSync;
2
+ var yoke = require("./yoke");
3
+
4
+ var MAX_CONTEXT_CHARS = 36000;
5
+ var MAX_TURNS = 12;
6
+ var MAX_USER_CHARS = 5000;
7
+ var MAX_ASSISTANT_CHARS = 9000;
8
+ var MAX_GIT_CHARS = 5000;
9
+ var MAX_TRANSCRIPT_CHARS = 24000;
10
+
11
+ function trimText(value, limit) {
12
+ var text = typeof value === "string" ? value.trim() : "";
13
+ if (text.length <= limit) return text;
14
+ return text.slice(0, limit) + "\n[truncated]";
15
+ }
16
+
17
+ function recentTurns(history) {
18
+ var turns = [];
19
+ var current = null;
20
+ history = Array.isArray(history) ? history : [];
21
+ for (var i = 0; i < history.length; i++) {
22
+ var entry = history[i];
23
+ if (!entry) continue;
24
+ if (entry.type === "user_message" || (entry.type === "handoff_context" && entry.request)) {
25
+ current = { user: trimText(entry.text || entry.request, MAX_USER_CHARS), assistant: "" };
26
+ turns.push(current);
27
+ } else if (entry.type === "delta" && entry.text) {
28
+ if (!current) {
29
+ current = { user: "", assistant: "" };
30
+ turns.push(current);
31
+ }
32
+ current.assistant += entry.text;
33
+ if (current.assistant.length > MAX_ASSISTANT_CHARS) {
34
+ current.assistant = current.assistant.slice(-MAX_ASSISTANT_CHARS);
35
+ }
36
+ }
37
+ }
38
+ return turns.slice(-MAX_TURNS);
39
+ }
40
+
41
+ function latestUserRequest(history) {
42
+ var turns = recentTurns(history);
43
+ for (var i = turns.length - 1; i >= 0; i--) {
44
+ if (turns[i].user) return turns[i].user;
45
+ }
46
+ return "";
47
+ }
48
+
49
+ function gitCommand(cwd, args) {
50
+ var result = spawnSync("git", args, {
51
+ cwd: cwd,
52
+ encoding: "utf8",
53
+ timeout: 5000,
54
+ maxBuffer: 1024 * 1024,
55
+ });
56
+ if (result.error || result.status !== 0) return "";
57
+ return trimText(result.stdout, MAX_GIT_CHARS);
58
+ }
59
+
60
+ function repositoryState(cwd) {
61
+ var status = gitCommand(cwd, ["status", "--short", "--branch"]);
62
+ var lines = [];
63
+ lines.push("Working tree:\n" + (status || "clean"));
64
+ return lines.join("\n\n");
65
+ }
66
+
67
+ function transcriptText(turns) {
68
+ var sections = [];
69
+ for (var i = 0; i < turns.length; i++) {
70
+ var turn = turns[i];
71
+ var parts = ["Turn " + (i + 1)];
72
+ if (turn.user) parts.push("USER:\n" + turn.user);
73
+ if (turn.assistant) parts.push("ASSISTANT:\n" + trimText(turn.assistant, MAX_ASSISTANT_CHARS));
74
+ sections.push(parts.join("\n\n"));
75
+ }
76
+ while (sections.length > 1 && sections.join("\n\n---\n\n").length > MAX_TRANSCRIPT_CHARS) {
77
+ sections.shift();
78
+ }
79
+ return sections.join("\n\n---\n\n");
80
+ }
81
+
82
+ function buildHandoffContext(options) {
83
+ var source = options.source;
84
+ var targetVendor = options.targetVendor;
85
+ var sourceVendor = source.vendor || "claude";
86
+ var sourceName = (yoke.getVendorInfo(sourceVendor) || {}).displayName || sourceVendor;
87
+ var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
88
+ var turns = recentTurns(source.history);
89
+ var latestUser = latestUserRequest(source.history);
90
+ var parts = [
91
+ "[Clay session handoff]",
92
+ "You are continuing work from another Clay coding-agent session. This is a snapshot, not a native conversation resume. Verify the current filesystem state before acting.",
93
+ "Source agent: " + sourceName,
94
+ "Target agent: " + targetName,
95
+ "Source session: " + (source.title || "Untitled session") + " (#" + source.localId + ")",
96
+ ];
97
+ if (latestUser) parts.push("Current user request, verbatim:\n" + latestUser);
98
+ parts.push("Repository state at handoff:\n" + repositoryState(options.cwd));
99
+ parts.push("Recent conversation:\n" + transcriptText(turns));
100
+ parts.push("Continue from the unresolved work above. Preserve the user's decisions and constraints, inspect the actual files before making assumptions, and proceed without asking the user to repeat context.");
101
+ return trimText(parts.join("\n\n"), MAX_CONTEXT_CHARS);
102
+ }
103
+
104
+ module.exports = {
105
+ MAX_CONTEXT_CHARS: MAX_CONTEXT_CHARS,
106
+ buildHandoffContext: buildHandoffContext,
107
+ latestUserRequest: latestUserRequest,
108
+ recentTurns: recentTurns,
109
+ repositoryState: repositoryState,
110
+ };
@@ -2,7 +2,14 @@
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
- var MEMORY_CONTRACT = "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
5
+ var MEMORY_CONTRACT =
6
+ "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
7
+ "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. " +
9
+ "Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. Include the observable evidence, affected component, likely impact, and a clear next action. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
10
+ "Important exception for deferred proposals: when you propose work (a fix, follow-up, improvement, or next step) and the user defers it rather than declining it (\"let's do that later\", \"next time\", \"after this\"), actively create a sticky note before the topic moves on. Do not wait to be asked. Capture what was proposed, why it mattered, and the agreed timing if any. Deferred agreements scroll out of chat history quickly and are hard to track; the board is where they survive. If the user declines the idea outright, do not write a note. " +
11
+ "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
12
+ "Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
6
13
 
7
14
  function getToolDefs(handlers) {
8
15
  return [
package/lib/sessions.js CHANGED
@@ -169,6 +169,7 @@ function createSessionManager(opts) {
169
169
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
170
170
  if (session.loop) metaObj.loop = session.loop;
171
171
  if (session.spawn) metaObj.spawn = session.spawn;
172
+ if (session.handoff) metaObj.handoff = session.handoff;
172
173
  if (session.debateState) metaObj.debateState = session.debateState;
173
174
  if (session.debateSetupMode) metaObj.debateSetupMode = true;
174
175
  var meta = JSON.stringify(metaObj);
@@ -281,6 +282,7 @@ function createSessionManager(opts) {
281
282
  session.effort = m.effort || null;
282
283
  if (m.loop) session.loop = m.loop;
283
284
  if (m.spawn) session.spawn = m.spawn;
285
+ if (m.handoff) session.handoff = m.handoff;
284
286
  if (m.debateState) session.debateState = m.debateState;
285
287
  if (m.debateSetupMode) session.debateSetupMode = true;
286
288
  if (m.ownerId) session.ownerId = m.ownerId;
@@ -493,6 +495,7 @@ function createSessionManager(opts) {
493
495
  title: s.title || "New Session",
494
496
  active: isActive,
495
497
  isProcessing: s.isProcessing,
498
+ backgroundTaskCount: (s.activeBackgroundTasks || []).length,
496
499
  lastActivity: s.lastActivity || s.createdAt || 0,
497
500
  loop: loop,
498
501
  spawn: s.spawn || null,
@@ -715,6 +718,7 @@ function createSessionManager(opts) {
715
718
  if (session.isProcessing) {
716
719
  _send({ type: "status", status: "processing" });
717
720
  }
721
+ _send({ type: "active_background_tasks", tasks: session.activeBackgroundTasks || [] });
718
722
 
719
723
  // Re-send any pending permission requests
720
724
  var pendingIds = Object.keys(session.pendingPermissions);
package/lib/worktree.js CHANGED
@@ -43,9 +43,14 @@ function isWorktree(projectPath) {
43
43
  }
44
44
  }
45
45
 
46
+ function isPathInside(parentPath, candidatePath) {
47
+ var relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
48
+ return relative !== "" && relative !== ".." && relative.indexOf(".." + path.sep) !== 0 && !path.isAbsolute(relative);
49
+ }
50
+
46
51
  // Scan worktrees for a given project path
47
- // Returns array of { path, branch, bare, detached, accessible }
48
- // accessible = true if worktree path is inside parentPath
52
+ // Returns array of { path, branch, bare, detached, external }
53
+ // external = true when Git registered the worktree outside the main project folder
49
54
  function scanWorktrees(projectPath) {
50
55
  var resolvedParent = path.resolve(projectPath);
51
56
  try {
@@ -63,7 +68,7 @@ function scanWorktrees(projectPath) {
63
68
  if (wt.bare) continue;
64
69
  var resolvedWt = path.resolve(wt.path);
65
70
  if (resolvedWt === resolvedParent) continue;
66
- wt.accessible = resolvedWt.indexOf(resolvedParent + path.sep) === 0;
71
+ wt.external = !isPathInside(resolvedParent, resolvedWt);
67
72
  wt.dirName = path.basename(wt.path);
68
73
  results.push(wt);
69
74
  }
@@ -131,4 +136,4 @@ function removeWorktree(projectPath, worktreeDirName) {
131
136
  }
132
137
  }
133
138
 
134
- module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree };
139
+ module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree, isPathInside: isPathInside };