clay-server 4.1.0-beta.5 → 4.1.0-beta.7

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 (155) hide show
  1. package/LICENSE +661 -21
  2. package/LICENSES/MIT-legacy.txt +21 -0
  3. package/LICENSES/README.md +7 -0
  4. package/README.md +2 -2
  5. package/bin/cli.js +0 -2
  6. package/lib/autonomous-run-record.js +32 -0
  7. package/lib/autonomous-run-scheduler.js +92 -0
  8. package/lib/daemon.js +0 -2
  9. package/lib/default-ai-runtime.js +117 -0
  10. package/lib/durable-scheduler-record.js +61 -0
  11. package/lib/durable-scheduler-store.js +185 -0
  12. package/lib/durable-scheduler.js +495 -0
  13. package/lib/git-cli.js +1 -0
  14. package/lib/issue-work-session.js +10 -0
  15. package/lib/issues-comments.js +40 -0
  16. package/lib/issues-commit-verification.js +30 -0
  17. package/lib/issues-mcp-server.js +27 -0
  18. package/lib/issues-schema.js +119 -0
  19. package/lib/issues-service.js +339 -0
  20. package/lib/issues-store.js +210 -0
  21. package/lib/loop-cron.js +47 -0
  22. package/lib/loop-durable-schedule.js +159 -0
  23. package/lib/loop-guidance.js +35 -0
  24. package/lib/loop-registry-occurrence.js +76 -0
  25. package/lib/model-entry.js +15 -0
  26. package/lib/project-autonomous-run.js +498 -0
  27. package/lib/project-capsule-turn.js +1 -0
  28. package/lib/project-connection.js +6 -1
  29. package/lib/project-delegated-session.js +1 -0
  30. package/lib/project-filesystem.js +10 -10
  31. package/lib/project-full-access.js +86 -0
  32. package/lib/project-http.js +3 -1
  33. package/lib/project-issue-launch.js +154 -0
  34. package/lib/project-issues.js +58 -0
  35. package/lib/project-logs-mcp-server.js +8 -12
  36. package/lib/project-loop-interview.js +163 -0
  37. package/lib/project-loop.js +150 -64
  38. package/lib/project-message-delivery.js +4 -2
  39. package/lib/project-models.js +71 -3
  40. package/lib/project-pair-autonomous-stop.js +16 -0
  41. package/lib/project-pair-owned-stop.js +18 -0
  42. package/lib/project-pair-task-control.js +14 -1
  43. package/lib/project-pending-message-queue.js +264 -0
  44. package/lib/project-scheduled-messages.js +316 -0
  45. package/lib/project-scheduled-task-execution.js +236 -0
  46. package/lib/project-scheduled-tasks.js +481 -0
  47. package/lib/project-session-pair.js +16 -19
  48. package/lib/project-sessions.js +114 -31
  49. package/lib/project-tool-llm-runtime.js +48 -0
  50. package/lib/project-user-message.js +202 -60
  51. package/lib/project-workspace-query.js +25 -0
  52. package/lib/project.js +361 -69
  53. package/lib/public/app.js +85 -22
  54. package/lib/public/css/default-ai.css +33 -0
  55. package/lib/public/css/home-hub.css +15 -2
  56. package/lib/public/css/home-sidebar.css +50 -2
  57. package/lib/public/css/input.css +160 -5
  58. package/lib/public/css/issues.css +101 -0
  59. package/lib/public/css/messages.css +19 -0
  60. package/lib/public/css/pane.css +58 -7
  61. package/lib/public/css/pending-message-queue.css +187 -0
  62. package/lib/public/css/rewind.css +11 -2
  63. package/lib/public/css/scheduled-tasks.css +122 -0
  64. package/lib/public/index.html +42 -150
  65. package/lib/public/modules/app-connection.js +3 -0
  66. package/lib/public/modules/app-cursors.js +29 -12
  67. package/lib/public/modules/app-loop-ui.js +4 -8
  68. package/lib/public/modules/app-message-router.js +8 -0
  69. package/lib/public/modules/app-messages.js +34 -9
  70. package/lib/public/modules/app-projects.js +2 -0
  71. package/lib/public/modules/app-rate-limit.js +8 -5
  72. package/lib/public/modules/autonomous-run.js +285 -0
  73. package/lib/public/modules/clay-file-links.js +111 -0
  74. package/lib/public/modules/clay-issue-links.js +131 -0
  75. package/lib/public/modules/clay-log-links.js +2 -0
  76. package/lib/public/modules/cursor-sharing-preference.js +38 -0
  77. package/lib/public/modules/default-ai.js +377 -0
  78. package/lib/public/modules/filebrowser.js +43 -2
  79. package/lib/public/modules/home-sidebar.js +16 -8
  80. package/lib/public/modules/home-surface.js +5 -0
  81. package/lib/public/modules/input.js +11 -20
  82. package/lib/public/modules/issues-context.js +117 -0
  83. package/lib/public/modules/issues-render.js +29 -0
  84. package/lib/public/modules/issues.js +270 -0
  85. package/lib/public/modules/loop-interview.js +151 -0
  86. package/lib/public/modules/markdown.js +4 -0
  87. package/lib/public/modules/message-delivery.js +5 -3
  88. package/lib/public/modules/pending-message-image-editor.js +143 -0
  89. package/lib/public/modules/pending-message-queue-model.js +22 -0
  90. package/lib/public/modules/pending-message-queue.js +449 -0
  91. package/lib/public/modules/project-logs.js +2 -2
  92. package/lib/public/modules/scheduled-message-state.js +46 -0
  93. package/lib/public/modules/scheduled-task-edit-state.js +7 -0
  94. package/lib/public/modules/scheduled-task-engine.js +48 -0
  95. package/lib/public/modules/scheduled-task-form.js +122 -0
  96. package/lib/public/modules/scheduled-task-recurrence.js +46 -0
  97. package/lib/public/modules/scheduled-task-schedule-rule.js +115 -0
  98. package/lib/public/modules/scheduled-tasks.js +459 -0
  99. package/lib/public/modules/scheduler-config.js +6 -52
  100. package/lib/public/modules/scheduler.js +9 -21
  101. package/lib/public/modules/sidebar-mobile.js +3 -0
  102. package/lib/public/modules/sidebar.js +0 -11
  103. package/lib/public/modules/split-view.js +2 -0
  104. package/lib/public/modules/sticky-note-markdown.js +1 -0
  105. package/lib/public/modules/terminal.js +2 -0
  106. package/lib/public/modules/tool-palette-order.js +3 -3
  107. package/lib/public/modules/tools.js +5 -2
  108. package/lib/public/style.css +3 -0
  109. package/lib/record-revision.js +11 -0
  110. package/lib/schedule-rule.js +151 -0
  111. package/lib/schedule-validation.js +124 -0
  112. package/lib/scheduled-run-state.js +87 -0
  113. package/lib/scheduled-task-records.js +127 -0
  114. package/lib/scheduler.js +179 -196
  115. package/lib/sdk-bridge.js +157 -22
  116. package/lib/sdk-message-processor.js +3 -0
  117. package/lib/server-cursor-sharing.js +65 -0
  118. package/lib/server-default-ai.js +265 -0
  119. package/lib/server-durable-scheduler.js +26 -0
  120. package/lib/server-home-chat.js +29 -9
  121. package/lib/server-home-clay-entry.js +4 -1
  122. package/lib/server-home-debate-planning.js +32 -14
  123. package/lib/server-home-mate-creation.js +30 -11
  124. package/lib/server-home-models.js +14 -6
  125. package/lib/server-issue-links.js +18 -0
  126. package/lib/server.js +63 -0
  127. package/lib/session-notes-mcp-server.js +1 -1
  128. package/lib/session-pair-factory.js +42 -4
  129. package/lib/session-pair-query-start.js +22 -0
  130. package/lib/session-query-tool-bridge.js +52 -0
  131. package/lib/session-search.js +4 -1
  132. package/lib/session-split-groups.js +35 -1
  133. package/lib/sessions.js +51 -3
  134. package/lib/tool-llm.js +1 -0
  135. package/lib/users-default-ai-preferences.js +80 -0
  136. package/lib/users-home-surface-preferences.js +2 -1
  137. package/lib/users-preferences.js +35 -0
  138. package/lib/users-scheduled-task-preferences.js +44 -0
  139. package/lib/users.js +16 -0
  140. package/lib/workspace-query-projection.js +29 -0
  141. package/lib/workspace-query-service.js +19 -29
  142. package/lib/ws-schema.js +61 -1
  143. package/lib/yoke/adapters/antigravity.js +1 -0
  144. package/lib/yoke/adapters/claude-worker.js +11 -6
  145. package/lib/yoke/adapters/claude.js +35 -1
  146. package/lib/yoke/adapters/codex.js +3 -0
  147. package/lib/yoke/claude-user-input.js +2 -0
  148. package/lib/yoke/index.js +2 -0
  149. package/lib/yoke/mcp-bridge-server.js +7 -1
  150. package/lib/yoke/package.json +1 -1
  151. package/lib/yoke/session-tool-transport.js +53 -0
  152. package/lib/yoke/user-input.js +21 -7
  153. package/package.json +5 -6
  154. package/bin/claude-relay.js +0 -6
  155. package/lib/public/modules/app-loop-wizard.js +0 -439
package/lib/server.js CHANGED
@@ -16,6 +16,8 @@ var serverMates = require("./server-mates");
16
16
  var serverHomeChat = require("./server-home-chat");
17
17
  var shouldSuppressHomeResponse = require("./home-response-notifications").shouldSuppressHomeResponse;
18
18
  var serverHomePreferences = require("./server-home-preferences");
19
+ var serverDefaultAi = require("./server-default-ai");
20
+ var serverCursorSharing = require("./server-cursor-sharing");
19
21
  var serverTools = require("./server-tools");
20
22
  var serverAdmin = require("./server-admin");
21
23
  var serverSettings = require("./server-settings");
@@ -28,6 +30,8 @@ var { attachMateKnowledgeService } = require("./mate-knowledge-service");
28
30
  var serverEmail = require("./server-email");
29
31
  var serverGlobalWs = require("./server-global-ws");
30
32
  var { parseWsRequestUrl } = require("./ws-request");
33
+ var { attachDurableScheduler } = require("./durable-scheduler");
34
+ var { attachDurableSchedulerLifecycle } = require("./server-durable-scheduler");
31
35
 
32
36
  var { CONFIG_DIR } = require("./config");
33
37
  var { provisionLinuxUser } = require("./os-users");
@@ -185,6 +189,8 @@ function createServer(opts) {
185
189
  var onUserProvisioned = opts.onUserProvisioned || null;
186
190
  var onUserDeleted = opts.onUserDeleted || null;
187
191
  var getRemovedProjects = opts.getRemovedProjects || function () { return []; };
192
+ var durableScheduler = null;
193
+ var durableSchedulerLifecycle = null;
188
194
 
189
195
  // --- Auth module ---
190
196
  var auth = serverAuth.attachAuth({
@@ -368,6 +374,26 @@ function createServer(opts) {
368
374
  },
369
375
  findUserById: function (userId) { return users.findUserById(userId); },
370
376
  });
377
+ var issuesService = require("./issues-service").attachIssuesService({
378
+ getProjects: function () { return projects; },
379
+ isMultiUser: function () { return users.isMultiUser(); },
380
+ canAccessProject: function (userId, status) {
381
+ if (!status || !status.slug || !onGetProjectAccess) return false;
382
+ var access = onGetProjectAccess(status.slug);
383
+ if (!access || access.error) return false;
384
+ return users.canAccessProject(userId, access) === true;
385
+ },
386
+ hasFullProjectAccess: function (userId, status) {
387
+ if (!status || !status.slug || !onGetProjectAccess) return false;
388
+ var access = onGetProjectAccess(status.slug);
389
+ if (!access || access.error) return false;
390
+ var governing = access.isWorktree && access.parentAccess ? access.parentAccess : access;
391
+ return users.canAccessProject(userId, governing) === true;
392
+ },
393
+ osUsersEnabled: function () { return osUsers; },
394
+ getOsUserInfoForActor: function (user) { return user && user.linuxUser ? require("./os-users").resolveOsUserInfo(user.linuxUser) : null; },
395
+ findUserById: function (userId) { return users.findUserById(userId); },
396
+ });
371
397
  // Mate Knowledge reads the synchronized Knowledge records. The Mate list is
372
398
  // derived from the owning user alone, so a binding can never span users.
373
399
  var mateKnowledgeService = attachMateKnowledgeService({
@@ -1040,6 +1066,8 @@ function createServer(opts) {
1040
1066
  // --- WebSocket ---
1041
1067
  var wss = new WebSocketServer({ noServer: true });
1042
1068
  var homePreferencesHandler = null;
1069
+ var defaultAiService = null;
1070
+ var cursorSharingService = null;
1043
1071
 
1044
1072
  // Slug-less /ws handler: lets a user with no projects yet load the regular
1045
1073
  // app shell and create/add/clone their first one. Only knows about the
@@ -1059,6 +1087,7 @@ function createServer(opts) {
1059
1087
  },
1060
1088
  onConnection: function (ws) {
1061
1089
  sendProjectsUpdatedToClient(ws);
1090
+ if (cursorSharingService) cursorSharingService.sendStateTo(ws);
1062
1091
  },
1063
1092
  });
1064
1093
 
@@ -1302,7 +1331,9 @@ function createServer(opts) {
1302
1331
  },
1303
1332
  workspaceQueryService: workspaceQueryService,
1304
1333
  projectLogsService: projectLogsService,
1334
+ issuesService: issuesService,
1305
1335
  mateKnowledgeService: mateKnowledgeService,
1336
+ durableScheduler: durableScheduler,
1306
1337
  canAccessProjectSlug: function (userId, targetSlug) {
1307
1338
  var target = projects.get(targetSlug);
1308
1339
  if (!target) return false;
@@ -1331,6 +1362,10 @@ function createServer(opts) {
1331
1362
  port: portNum,
1332
1363
  tls: !!tlsOptions,
1333
1364
  authToken: pinHash || null,
1365
+ resolveDefaultAi: function (ws) {
1366
+ if (!defaultAiService) return Promise.resolve({ ready: false, error: "Default AI is not ready." });
1367
+ return defaultAiService.resolveForWs(ws);
1368
+ },
1334
1369
  getProjectCount: function () { return projects.size; },
1335
1370
  getProjectList: function (userId) {
1336
1371
  return getVisibleProjects(userId);
@@ -1460,6 +1495,7 @@ function createServer(opts) {
1460
1495
  onShutdown: onShutdown,
1461
1496
  onRestart: onRestart,
1462
1497
  onDmMessage: handleDmMessage,
1498
+ sendCursorSharingState: function (ws) { if (cursorSharingService) cursorSharingService.sendStateTo(ws); },
1463
1499
  broadcastAll: broadcastAll,
1464
1500
  notificationsModule: _globalNotifications,
1465
1501
  refreshVendorAuthEverywhere: refreshVendorAuthEverywhere,
@@ -1503,6 +1539,11 @@ function createServer(opts) {
1503
1539
  addProject: addProject,
1504
1540
  workspaceQueryService: workspaceQueryService,
1505
1541
  projectLogsService: projectLogsService,
1542
+ issuesService: issuesService,
1543
+ resolveDefaultAi: function (ws) {
1544
+ if (!defaultAiService) return Promise.resolve({ ready: false, error: "Default AI is not ready." });
1545
+ return defaultAiService.resolveForWs(ws);
1546
+ },
1506
1547
  });
1507
1548
 
1508
1549
  homePreferencesHandler = serverHomePreferences.attachHomePreferences({
@@ -1510,6 +1551,9 @@ function createServer(opts) {
1510
1551
  projects: projects,
1511
1552
  });
1512
1553
 
1554
+ defaultAiService = serverDefaultAi.attachDefaultAiService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1555
+ cursorSharingService = serverCursorSharing.attachCursorSharingService({ users: users, forEachAppClient: forEachAppClient });
1556
+
1513
1557
  // --- User-installed home tools handler ---
1514
1558
  var toolsHandler = serverTools.attachTools({
1515
1559
  users: users,
@@ -1535,6 +1579,8 @@ function createServer(opts) {
1535
1579
  }
1536
1580
 
1537
1581
  function handleDmMessage(ws, msg, routedProjectSlug) {
1582
+ if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
1583
+ if (defaultAiService && defaultAiService.handleMessage(ws, msg)) return true;
1538
1584
  if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
1539
1585
  if (dmHandler.handleMessage(ws, msg)) return true;
1540
1586
  if (mateHandler && mateHandler.handleMessage(ws, msg)) return true;
@@ -1817,8 +1863,24 @@ function createServer(opts) {
1817
1863
  });
1818
1864
  }
1819
1865
 
1866
+ var schedulerNamespace = process.env.CLAY_DEV ? "dev" : "prod";
1867
+ durableScheduler = opts.durableScheduler || attachDurableScheduler({
1868
+ storageDir: path.join(CONFIG_DIR, "scheduler"),
1869
+ namespace: schedulerNamespace,
1870
+ globalConcurrency: opts.schedulerGlobalConcurrency || 4,
1871
+ perOwnerConcurrency: opts.schedulerPerOwnerConcurrency || 1,
1872
+ });
1873
+ durableSchedulerLifecycle = attachDurableSchedulerLifecycle({
1874
+ server: server,
1875
+ scheduler: durableScheduler,
1876
+ onError: function (err) {
1877
+ console.error("[server] Durable scheduler shutdown failed:", err && err.message ? err.message : err);
1878
+ },
1879
+ });
1880
+
1820
1881
  function destroyAll() {
1821
1882
  var shutdowns = [];
1883
+ shutdowns.push(durableSchedulerLifecycle.shutdown());
1822
1884
  projects.forEach(function (ctx, slug) {
1823
1885
  console.log("[server] Destroying project:", slug);
1824
1886
  var result = ctx.destroy();
@@ -1898,6 +1960,7 @@ function createServer(opts) {
1898
1960
  forEachProject: forEachProject,
1899
1961
  destroyProject: removeProject,
1900
1962
  destroyAll: destroyAll,
1963
+ durableScheduler: durableScheduler,
1901
1964
  setProjectLogContextState: setProjectLogContextState,
1902
1965
  };
1903
1966
  }
@@ -6,7 +6,7 @@ var MEMORY_CONTRACT =
6
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
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
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. " +
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. For a Project Driver session with Issues authority, search, reuse, or create the Issue first and put the primary details there: observable evidence, affected component, impact, and next action. Then make the sticky note only a concise title, one-line actionable cue, and the actual opaque issue: reference; do not duplicate the full defect details in the note. If you lack Issues authority, route through an eligible Project Driver; never invent an issue reference or drop the alert. 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
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
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
12
  "Updates are visible too: update only when durable state materially changes. " +
@@ -67,11 +67,11 @@ function attachPairFactory(ctx) {
67
67
  throw new Error("model is not available for vendor \"" + vendor + "\": " + requested);
68
68
  }
69
69
 
70
- function validateEffort(vendor, effort) {
70
+ function validateEffort(vendor, effort, exact) {
71
71
  var requested = typeof effort === "string" ? effort.trim() : "";
72
72
  var fallback = (sm.currentEffortByVendor && sm.currentEffortByVendor[vendor]) || sm.currentEffort || "medium";
73
73
  var clamped = yoke.clampEffort(vendor, requested || fallback) || null;
74
- if (requested && !clamped) {
74
+ if (requested && (!clamped || (exact && clamped !== requested))) {
75
75
  throw new Error("reasoning effort is not supported for vendor \"" + vendor + "\": " + requested);
76
76
  }
77
77
  return clamped;
@@ -79,13 +79,16 @@ function attachPairFactory(ctx) {
79
79
 
80
80
  // Resolve and validate one session runtime request without creating
81
81
  // anything. Throws on the first unusable field.
82
- function preflightRuntime(spec, defaultVendor) {
82
+ function preflightRuntime(spec, defaultVendor, exact) {
83
83
  var request = spec || {};
84
84
  var vendor = validateVendor((typeof request.vendor === "string" && request.vendor.trim()) || defaultVendor);
85
+ var info = yoke.getVendorInfo(vendor) || {};
86
+ if (exact && !(typeof request.model === "string" && request.model.trim())) throw new Error("an explicit model is required for vendor \"" + vendor + "\"");
87
+ if (exact && Array.isArray(info.effortLevels) && info.effortLevels.length && !(typeof request.effort === "string" && request.effort.trim())) throw new Error("an explicit reasoning effort is required for vendor \"" + vendor + "\"");
85
88
  return {
86
89
  vendor: vendor,
87
90
  model: validateModel(vendor, request.model),
88
- effort: validateEffort(vendor, request.effort),
91
+ effort: validateEffort(vendor, request.effort, exact),
89
92
  explicitEffort: !!(typeof request.effort === "string" && request.effort.trim()),
90
93
  };
91
94
  }
@@ -227,6 +230,39 @@ function attachPairFactory(ctx) {
227
230
  return created;
228
231
  }
229
232
 
233
+ function createOwnedPair(ownerId, args) {
234
+ if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
235
+ var request = args || {};
236
+ var driverRuntime = preflightRuntime(request.driver || {}, "claude");
237
+ var workerRuntime = preflightRuntime(request.worker || {}, null);
238
+ var driver = sm.createSessionRaw({ ownerId: ownerId || null, vendor: driverRuntime.vendor, model: driverRuntime.model, effort: driverRuntime.effort });
239
+ var worker = null;
240
+ try {
241
+ driver.title = request.name ? request.name + " · Driver" : "Scheduled task · Driver";
242
+ if (driverRuntime.explicitEffort) driver.loopSettings = { effort: driverRuntime.effort };
243
+ worker = sm.createSessionRaw({ ownerId: ownerId || null, vendor: workerRuntime.vendor, model: workerRuntime.model, effort: workerRuntime.effort });
244
+ worker.title = request.name ? request.name + " · Split Worker" : "Scheduled task · Split Worker";
245
+ if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
246
+ sessionProvenance.markWorker(driver, worker, sm.sessions);
247
+ if (typeof sm.saveSessionFile === "function") { sm.saveSessionFile(driver); sm.saveSessionFile(worker); }
248
+ var result = store.createOwned(ownerId || null, { members: [driver.localId, worker.localId], pair: { driverId: driver.localId, workerId: worker.localId }, name: request.name || "Scheduled task" });
249
+ if (!result.ok) throw new Error(result.error);
250
+ sm.broadcastSessionList();
251
+ return { driver: driver, worker: worker, group: result.group };
252
+ } catch (error) {
253
+ removeCreated(worker);
254
+ removeCreated(driver);
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ function removeOwnedPair(ownerId, created) {
260
+ if (!created || !created.group || !store.dissolveOwned(ownerId || null, created.group.id)) return false;
261
+ removeCreated(created.worker);
262
+ removeCreated(created.driver);
263
+ return true;
264
+ }
265
+
230
266
  function createPair(ws, msg) {
231
267
  try {
232
268
  var created = createPairRecord(ws, msg);
@@ -240,6 +276,8 @@ function attachPairFactory(ctx) {
240
276
  createPair: createPair,
241
277
  createPairRecord: createPairRecord,
242
278
  createWorkerForDriver: createWorkerForDriver,
279
+ createOwnedPair: createOwnedPair,
280
+ removeOwnedPair: removeOwnedPair,
243
281
  preflightRuntime: preflightRuntime,
244
282
  preflightWorkerForDriver: preflightWorkerForDriver,
245
283
  validateModel: validateModel,
@@ -0,0 +1,22 @@
1
+ function beginPairQuery(ctx, session, text) {
2
+ session._queryStartTs = Date.now();
3
+ var promise = ctx.sdk.startQuery(session, text, undefined, ctx.linuxUser);
4
+ var owner = {
5
+ session: session,
6
+ generation: Number(session._queryGeneration || 0),
7
+ };
8
+ session._pairResumeQuery = owner;
9
+ Promise.resolve(promise).catch(function (error) {
10
+ var current = ctx.sm.sessions.get(session.localId);
11
+ var ownsCurrent = current === session && session._pairResumeQuery === owner &&
12
+ Number(session._queryGeneration || 0) === owner.generation;
13
+ if (!ownsCurrent) return;
14
+ delete session._pairResumeQuery;
15
+ session.isProcessing = false;
16
+ ctx.sm.sendAndRecord(session, { type: "error", text: error.message || String(error) });
17
+ ctx.onProcessingChanged();
18
+ });
19
+ return owner;
20
+ }
21
+
22
+ module.exports = { beginPairQuery: beginPairQuery };
@@ -0,0 +1,52 @@
1
+ // Exact-query projection of SDK-owned tools into the project stdio MCP bridge.
2
+
3
+ var sessionToolTransport = require("./yoke/session-tool-transport");
4
+
5
+ function normalizeInputSchema(inputSchema) {
6
+ if (inputSchema && typeof inputSchema.type === "string") return inputSchema;
7
+ try {
8
+ var zod = require("zod");
9
+ var schema = inputSchema && inputSchema.safeParse ? inputSchema : zod.object(inputSchema || {});
10
+ return zod.toJSONSchema(schema);
11
+ } catch (e) {
12
+ return { type: "object", properties: {} };
13
+ }
14
+ }
15
+
16
+ function createSessionQueryToolBridge(ctx) {
17
+ function defs(queryGeneration) {
18
+ if (!ctx.session || !ctx.sdk || !Number.isInteger(queryGeneration)) return [];
19
+ return ctx.sdk.getQueryToolDefs(ctx.session, queryGeneration) || [];
20
+ }
21
+
22
+ function listTools(queryGeneration, normalizeToolSchema) {
23
+ var tools = [];
24
+ var queryTools = defs(queryGeneration);
25
+ var normalize = normalizeToolSchema || normalizeInputSchema;
26
+ for (var i = 0; i < queryTools.length; i++) {
27
+ var server = sessionToolTransport.bridgeServerForTool(queryTools[i].name);
28
+ if (!server) continue;
29
+ tools.push({
30
+ server: server,
31
+ name: queryTools[i].name,
32
+ description: queryTools[i].description || queryTools[i].name,
33
+ inputSchema: normalize(queryTools[i].inputSchema),
34
+ });
35
+ }
36
+ return tools;
37
+ }
38
+
39
+ function callTool(queryGeneration, serverName, toolName, args) {
40
+ var queryTools = defs(queryGeneration);
41
+ for (var i = 0; i < queryTools.length; i++) {
42
+ if (sessionToolTransport.bridgeServerForTool(queryTools[i].name) !== serverName || queryTools[i].name !== toolName) continue;
43
+ if (typeof queryTools[i].handler !== "function") return Promise.reject(new Error("Session tool handler unavailable: " + serverName + "/" + toolName));
44
+ return Promise.resolve(queryTools[i].handler(args || {}));
45
+ }
46
+ return null;
47
+ }
48
+
49
+ return { listTools: listTools, callTool: callTool };
50
+ }
51
+
52
+ module.exports = { createSessionQueryToolBridge: createSessionQueryToolBridge, normalizeInputSchema: normalizeInputSchema };
@@ -576,7 +576,10 @@ function searchPalette(projectSessions, query, opts) {
576
576
  if (allDocs.length === 0) return [];
577
577
 
578
578
  var index = buildIndex(allDocs);
579
- var raw = searchIndex(index, query, maxResults * 3); // over-fetch for dedup
579
+ // Rank every matching document before deduplication. A session with many
580
+ // matching turns must not consume the over-fetch window and hide other
581
+ // matching sessions from the palette.
582
+ var raw = searchIndex(index, query, allDocs.length);
580
583
 
581
584
  // Deduplicate: keep best match per session
582
585
  var bestPerSession = {};
@@ -179,6 +179,40 @@ function createSplitGroupStore(opts) {
179
179
  return { ok: true, group: group };
180
180
  }
181
181
 
182
+ function createOwned(ownerId, msg) {
183
+ var members = msg && msg.members;
184
+ if (!Array.isArray(members) || members.length !== 2) return { ok: false, error: "A split group requires exactly two sessions" };
185
+ var left = sessions.get(members[0]);
186
+ var right = sessions.get(members[1]);
187
+ if (!left || !right || left === right) return { ok: false, error: "Session not found" };
188
+ if ((left.ownerId || null) !== (ownerId || null) || (right.ownerId || null) !== (ownerId || null)) return { ok: false, error: "Session owner does not match the scheduled task owner" };
189
+ if (groupForMember(left.localId) || groupForMember(right.localId)) return { ok: false, error: "A session can belong to only one split group" };
190
+ var pair = msg && msg.pair;
191
+ if (!pair || pair.driverId !== left.localId || pair.workerId !== right.localId) return { ok: false, error: "Pair roles must reference both split group members" };
192
+ var requestedName = typeof msg.name === "string" ? msg.name.trim().slice(0, 80) : "";
193
+ var group = {
194
+ id: "sg_" + Date.now().toString(36) + "_" + crypto.randomBytes(3).toString("hex"),
195
+ name: requestedName || autoGroupName(left.title, right.title), nameCustomized: !!requestedName,
196
+ ownerId: ownerId || null, members: members.slice(),
197
+ pair: { driverId: pair.driverId, workerId: pair.workerId }, createdAt: Date.now(),
198
+ };
199
+ groups.push(group);
200
+ try { save(); } catch (error) { groups.pop(); return { ok: false, error: error.message || String(error) }; }
201
+ broadcast();
202
+ onPairChanged(group);
203
+ return { ok: true, group: group };
204
+ }
205
+
206
+ function dissolveOwned(ownerId, id) {
207
+ var index = groups.findIndex(function (item) { return item.id === id && (item.ownerId || null) === (ownerId || null); });
208
+ if (index === -1) return false;
209
+ var removed = groups.splice(index, 1)[0];
210
+ try { save(); } catch (error) { groups.splice(index, 0, removed); return false; }
211
+ broadcast();
212
+ onPairChanged(removed);
213
+ return true;
214
+ }
215
+
182
216
  function rename(ws, msg) {
183
217
  var group = groups.find(function (item) { return item.id === (msg && msg.id); });
184
218
  if (!group) return { ok: false, error: "Split group not found" };
@@ -265,7 +299,7 @@ function createSplitGroupStore(opts) {
265
299
  }
266
300
 
267
301
  load();
268
- return { create: create, rename: rename, setPair: setPair, dissolve: dissolve, dissolveBySession: dissolveBySession,
302
+ return { create: create, createOwned: createOwned, dissolveOwned: dissolveOwned, rename: rename, setPair: setPair, dissolve: dissolve, dissolveBySession: dissolveBySession,
269
303
  refreshAutoName: refreshAutoName, refreshAnchors: refreshAnchors,
270
304
  listFor: listFor, groupForMember: groupForMember, get groups() { return groups; } };
271
305
  }
package/lib/sessions.js CHANGED
@@ -8,6 +8,8 @@ var hygiene = require("./session-hygiene");
8
8
  var backgroundTaskTiming = require("./background-task-timing");
9
9
  var sessionProvenance = require("./session-provenance");
10
10
  var sessionVisibility = require("./session-visibility");
11
+ var projectAutonomousRun = require("./project-autonomous-run").projectAutonomousRun;
12
+ var restoreStoredRun = require("./project-autonomous-run").restoreStoredRun;
11
13
 
12
14
  function createSessionManager(opts) {
13
15
  var cwd = opts.cwd;
@@ -17,6 +19,8 @@ function createSessionManager(opts) {
17
19
  var sendAndRecord = null; // set after init via setSendAndRecord
18
20
  var onSessionDone = opts.onSessionDone || function () {};
19
21
  var onSessionDeleted = opts.onSessionDeleted || function () {};
22
+ var sessionDeletedListeners = [];
23
+ var sessionViewedListeners = [];
20
24
  var onSessionRenamed = opts.onSessionRenamed || function () {};
21
25
  var onSessionIdentityAssigned = opts.onSessionIdentityAssigned || function () {};
22
26
 
@@ -153,6 +157,7 @@ function createSessionManager(opts) {
153
157
  };
154
158
  if (session.ownerId) metaObj.ownerId = session.ownerId;
155
159
  if (session.vendor) metaObj.vendor = session.vendor;
160
+ if (session.codexDynamicToolCatalogVersion) metaObj.codexDynamicToolCatalogVersion = session.codexDynamicToolCatalogVersion;
156
161
  if (session.model) metaObj.model = session.model;
157
162
  if (session.effort) metaObj.effort = session.effort;
158
163
  // Persist the session's "born" mode so TUI sessions reappear after a
@@ -167,6 +172,11 @@ function createSessionManager(opts) {
167
172
  if (session.dangerouslySkipPermissions) metaObj.dangerouslySkipPermissions = true;
168
173
  if (session.permissionMode) metaObj.permissionMode = session.permissionMode;
169
174
  if (session.permissionModeBeforeFullAccess) metaObj.permissionModeBeforeFullAccess = session.permissionModeBeforeFullAccess;
175
+ if (session.autonomousRun) metaObj.autonomousRun = session.autonomousRun;
176
+ if (session.loopInterviewBrief) metaObj.loopInterviewBrief = session.loopInterviewBrief;
177
+ if (session.loopInterviewHandoff) metaObj.loopInterviewHandoff = session.loopInterviewHandoff;
178
+ if (session.scheduledTaskDraft) metaObj.scheduledTaskDraft = session.scheduledTaskDraft;
179
+ if (session.scheduledTaskRun) metaObj.scheduledTaskRun = session.scheduledTaskRun;
170
180
  metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
171
181
  if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
172
182
  if (session.bookmarked) metaObj.bookmarked = true;
@@ -293,6 +303,7 @@ function createSessionManager(opts) {
293
303
  lastRewindUuid: m.lastRewindUuid || null,
294
304
  };
295
305
  if (m.vendor) session.vendor = m.vendor;
306
+ session.codexDynamicToolCatalogVersion = Number(m.codexDynamicToolCatalogVersion || 0);
296
307
  session.model = m.model || deriveSessionModel(loaded[i].history);
297
308
  session.effort = m.effort || null;
298
309
  if (m.loop) session.loop = m.loop;
@@ -322,6 +333,11 @@ function createSessionManager(opts) {
322
333
  session.dangerouslySkipPermissions = session.mode === "tui" && !!m.dangerouslySkipPermissions;
323
334
  session.permissionMode = m.permissionMode || (session.mode === "gui" && m.dangerouslySkipPermissions ? "bypassPermissions" : null);
324
335
  session.permissionModeBeforeFullAccess = m.permissionModeBeforeFullAccess || null;
336
+ session.autonomousRun = restoreStoredRun(m.autonomousRun);
337
+ session.loopInterviewBrief = m.loopInterviewBrief && typeof m.loopInterviewBrief === "object" ? m.loopInterviewBrief : null;
338
+ session.loopInterviewHandoff = m.loopInterviewHandoff && typeof m.loopInterviewHandoff === "object" ? m.loopInterviewHandoff : null;
339
+ session.scheduledTaskDraft = m.scheduledTaskDraft && typeof m.scheduledTaskDraft === "object" ? m.scheduledTaskDraft : null;
340
+ session.scheduledTaskRun = m.scheduledTaskRun && typeof m.scheduledTaskRun === "object" ? m.scheduledTaskRun : null;
325
341
  session.terminalId = null;
326
342
  session.runtimeMode = null;
327
343
  session.runtimeTerminalId = null;
@@ -554,6 +570,13 @@ function createSessionManager(opts) {
554
570
  runtimeTerminalId: typeof s.runtimeTerminalId === "number" ? s.runtimeTerminalId : null,
555
571
  dangerouslySkipPermissions: !!s.dangerouslySkipPermissions,
556
572
  permissionMode: s.permissionMode || null,
573
+ autonomousRun: s.autonomousRun ? {
574
+ id: s.autonomousRun.id,
575
+ state: s.autonomousRun.state,
576
+ continuationCount: s.autonomousRun.continuationCount || 0,
577
+ maxContinuations: s.autonomousRun.maxContinuations,
578
+ deadlineAt: s.autonomousRun.deadlineAt || null,
579
+ } : null,
557
580
  };
558
581
  }
559
582
 
@@ -636,6 +659,11 @@ function createSessionManager(opts) {
636
659
  dangerouslySkipPermissions: !!(sessionOpts && sessionOpts.mode === "tui" && sessionOpts.dangerouslySkipPermissions),
637
660
  permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
638
661
  permissionModeBeforeFullAccess: null,
662
+ autonomousRun: null,
663
+ loopInterviewBrief: null,
664
+ loopInterviewHandoff: null,
665
+ scheduledTaskDraft: null,
666
+ scheduledTaskRun: null,
639
667
  terminalId: null,
640
668
  sessionOriginId: null,
641
669
  };
@@ -678,6 +706,11 @@ function createSessionManager(opts) {
678
706
  dangerouslySkipPermissions: !!(sessionOpts && sessionOpts.mode === "tui" && sessionOpts.dangerouslySkipPermissions),
679
707
  permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
680
708
  permissionModeBeforeFullAccess: null,
709
+ autonomousRun: null,
710
+ loopInterviewBrief: null,
711
+ loopInterviewHandoff: null,
712
+ scheduledTaskDraft: null,
713
+ scheduledTaskRun: null,
681
714
  terminalId: null,
682
715
  sessionOriginId: null,
683
716
  };
@@ -738,6 +771,9 @@ function createSessionManager(opts) {
738
771
  }
739
772
 
740
773
  _send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, contextUsage: session.lastContextUsage || null });
774
+ for (var vi = 0; vi < sessionViewedListeners.length; vi++) {
775
+ try { sessionViewedListeners[vi](session, targetWs, _send); } catch (e) {}
776
+ }
741
777
  }
742
778
 
743
779
  function switchSession(localId, targetWs, transform) {
@@ -768,7 +804,7 @@ function createSessionManager(opts) {
768
804
  var _capsByVendor = capabilitiesByVendor || {};
769
805
  var _sessionVendor = session.vendor || defaultVendor || "claude";
770
806
  var _vendorCaps = _capsByVendor[_sessionVendor] || {};
771
- _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: session.permissionMode || null });
807
+ _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: session.permissionMode || null, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
772
808
  // Send vendor-specific slash commands
773
809
  var _vendorCmds = slashCommandsByVendor[_sessionVendor] || slashCommands || [];
774
810
  _send({ type: "slash_commands", commands: _vendorCmds, vendor: _sessionVendor });
@@ -841,9 +877,14 @@ function createSessionManager(opts) {
841
877
  }
842
878
 
843
879
  sessions.delete(localId);
844
- try { onSessionDeleted(localId); } catch (e) {
880
+ try { onSessionDeleted(localId, session); } catch (e) {
845
881
  console.error("[session] Session deletion hook failed:", e.message || e);
846
882
  }
883
+ for (var di = 0; di < sessionDeletedListeners.length; di++) {
884
+ try { sessionDeletedListeners[di](localId, session); } catch (e) {
885
+ console.error("[session] Session deletion listener failed:", e.message || e);
886
+ }
887
+ }
847
888
 
848
889
  if (activeSessionId === localId) {
849
890
  var remaining = [...sessions.keys()];
@@ -882,9 +923,14 @@ function createSessionManager(opts) {
882
923
  try { fs.unlinkSync(sessionFilePath(session.cliSessionId)); } catch(e) {}
883
924
  }
884
925
  sessions.delete(localId);
885
- try { onSessionDeleted(localId); } catch (e) {
926
+ try { onSessionDeleted(localId, session); } catch (e) {
886
927
  console.error("[session] Session deletion hook failed:", e.message || e);
887
928
  }
929
+ for (var di = 0; di < sessionDeletedListeners.length; di++) {
930
+ try { sessionDeletedListeners[di](localId, session); } catch (e) {
931
+ console.error("[session] Session deletion listener failed:", e.message || e);
932
+ }
933
+ }
888
934
  }
889
935
 
890
936
  function deleteSessionsBulk(localIds, targetWs) {
@@ -1250,6 +1296,8 @@ function createSessionManager(opts) {
1250
1296
  sessions: sessions,
1251
1297
  sessionsDir: sessionsDir,
1252
1298
  setOnSessionDeleted: function (fn) { onSessionDeleted = fn || function () {}; },
1299
+ addOnSessionDeleted: function (fn) { if (typeof fn === "function") sessionDeletedListeners.push(fn); },
1300
+ addOnSessionViewed: function (fn) { if (typeof fn === "function") sessionViewedListeners.push(fn); },
1253
1301
  setOnSessionRenamed: function (fn) { onSessionRenamed = fn || function () {}; },
1254
1302
  setOnSessionIdentityAssigned: function (fn) { onSessionIdentityAssigned = fn || function () {}; },
1255
1303
  notifySessionRenamed: function (localId) { onSessionRenamed(localId); },
package/lib/tool-llm.js CHANGED
@@ -32,6 +32,7 @@ async function complete(opts) {
32
32
  query: {
33
33
  cwd: opts.cwd,
34
34
  linuxUser: opts.linuxUser || undefined,
35
+ effort: selection.effort || undefined,
35
36
  skipProjectInstructions: true,
36
37
  skipSkills: true,
37
38
  canUseTool: function () { return Promise.resolve({ behavior: "deny", message: "Capsule LLM calls cannot use tools." }); },
@@ -0,0 +1,80 @@
1
+ // Durable user-scoped Default AI preference storage.
2
+
3
+ var PREFERENCE_KEY = "defaultAiPreference";
4
+ var LEGACY_KEY = "scheduledTaskInterviewEngine";
5
+
6
+ function isPlainObject(value) {
7
+ return !!value && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+
10
+ function validText(value, maxLength) {
11
+ return typeof value === "string" && value.length <= maxLength && !/[\u0000-\u001f\u007f]/.test(value);
12
+ }
13
+
14
+ function validatePreference(value) {
15
+ if (!isPlainObject(value)) return { ok: false, error: "Default AI preference must be an object." };
16
+ if (!validText(value.vendor, 40) || !/^[a-z0-9][a-z0-9_-]{0,39}$/.test(value.vendor)) return { ok: false, error: "Default AI vendor is invalid." };
17
+ if (value.model !== undefined && (!validText(value.model, 200) || (value.model !== "" && (value.model.trim() !== value.model || value.model.trim() === "")))) return { ok: false, error: "Default AI model is invalid." };
18
+ if (value.effort !== undefined && (!validText(value.effort, 40) || (value.effort !== "" && (value.effort.trim() !== value.effort || !/^[a-zA-Z0-9_-]{1,40}$/.test(value.effort))))) return { ok: false, error: "Default AI effort is invalid." };
19
+ return { ok: true, preference: { vendor: value.vendor, model: value.model === undefined ? "" : value.model, effort: value.effort === undefined ? "" : value.effort } };
20
+ }
21
+
22
+ function attachDefaultAiPreferences(deps) {
23
+ var loadUsers = deps.loadUsers;
24
+ var saveUsers = deps.saveUsers;
25
+ var isMultiUser = deps.isMultiUser || function () { return false; };
26
+ var configModule = deps.configModule || require("./config");
27
+
28
+ function userRecord(data, userId) {
29
+ var users = Array.isArray(data.users) ? data.users : [];
30
+ for (var i = 0; i < users.length; i++) if (users[i] && users[i].id === userId) return users[i];
31
+ return null;
32
+ }
33
+
34
+ function readValue(value, source) {
35
+ if (value === undefined) return { present: false, preference: null, source: null };
36
+ var checked = validatePreference(value);
37
+ if (!checked.ok) return { present: true, preference: null, source: source, error: checked.error };
38
+ return { present: true, preference: checked.preference, source: source };
39
+ }
40
+
41
+ function getDefaultAiPreference(userId) {
42
+ var data;
43
+ try { data = loadUsers(); } catch (error) { return { present: false, preference: null, source: null, error: "Unable to read Default AI preference: " + (error.message || error) }; }
44
+ var record = userRecord(data, userId);
45
+ if (isMultiUser() && !record) return { present: false, preference: null, source: null, error: "User not found." };
46
+ if (record && Object.prototype.hasOwnProperty.call(record, PREFERENCE_KEY)) return readValue(record[PREFERENCE_KEY], "explicit");
47
+ if (record && Object.prototype.hasOwnProperty.call(record, LEGACY_KEY)) return readValue(record[LEGACY_KEY], "legacy");
48
+ if (userId === "default") {
49
+ var config;
50
+ try { config = configModule.loadConfig() || {}; } catch (configError) { return { present: false, preference: null, source: null, error: "Unable to read Default AI preference: " + (configError.message || configError) }; }
51
+ if (Object.prototype.hasOwnProperty.call(config, PREFERENCE_KEY)) return readValue(config[PREFERENCE_KEY], "explicit");
52
+ if (Object.prototype.hasOwnProperty.call(config, LEGACY_KEY)) return readValue(config[LEGACY_KEY], "legacy");
53
+ }
54
+ return { present: false, preference: null, source: null };
55
+ }
56
+
57
+ function setDefaultAiPreference(userId, value) {
58
+ var checked = validatePreference(value);
59
+ if (!checked.ok) return { ok: false, error: checked.error };
60
+ var data;
61
+ try { data = loadUsers(); } catch (error3) { return { ok: false, error: "Unable to read Default AI preference: " + (error3.message || error3) }; }
62
+ var record = userRecord(data, userId);
63
+ if (isMultiUser() && !record) return { ok: false, error: "User not found." };
64
+ if (record) {
65
+ var nextData = Object.assign({}, data, { users: (data.users || []).map(function (item) { return item === record ? Object.assign({}, item, { defaultAiPreference: checked.preference }) : item; }) });
66
+ try { saveUsers(nextData); } catch (error) { return { ok: false, error: "Unable to save Default AI preference: " + (error.message || error) }; }
67
+ return { ok: true, preference: checked.preference };
68
+ }
69
+ if (userId !== "default") return { ok: false, error: "User not found." };
70
+ try {
71
+ var current = configModule.loadConfig() || {};
72
+ configModule.saveConfig(Object.assign({}, current, { defaultAiPreference: checked.preference }));
73
+ return { ok: true, preference: checked.preference };
74
+ } catch (error2) { return { ok: false, error: "Unable to save Default AI preference: " + (error2.message || error2) }; }
75
+ }
76
+
77
+ return { getDefaultAiPreference: getDefaultAiPreference, setDefaultAiPreference: setDefaultAiPreference, validatePreference: validatePreference };
78
+ }
79
+
80
+ module.exports = { attachDefaultAiPreferences: attachDefaultAiPreferences, validatePreference: validatePreference, PREFERENCE_KEY: PREFERENCE_KEY };
@@ -5,7 +5,7 @@ function attachHomeSurfacePreferences(deps) {
5
5
  var saveUsers = deps.saveUsers;
6
6
 
7
7
  function defaults() {
8
- return { surface: null, projectSlug: null, activeMateId: null, activeSessionByMate: {}, sidebarCollapsed: false, chatScope: "all", subSurface: "chat" };
8
+ return { surface: null, projectSlug: null, activeMateId: null, activeSessionByMate: {}, sidebarCollapsed: false, matesCollapsed: false, chatScope: "all", subSurface: "chat" };
9
9
  }
10
10
 
11
11
  function validId(value) {
@@ -37,6 +37,7 @@ function attachHomeSurfacePreferences(deps) {
37
37
  activeMateId: validId(source.activeMateId) ? source.activeMateId : null,
38
38
  activeSessionByMate: normalizedSessions,
39
39
  sidebarCollapsed: source.sidebarCollapsed === true,
40
+ matesCollapsed: source.matesCollapsed === true,
40
41
  chatScope: source.chatScope === "current" ? "current" : "all",
41
42
  subSurface: source.subSurface === "debates" ? "debates" : "chat",
42
43
  };