clay-server 4.1.0-beta.13 → 4.1.0-beta.15

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 (38) hide show
  1. package/lib/issues-mcp-server.js +1 -1
  2. package/lib/project-file-path.js +13 -0
  3. package/lib/project-file-watch.js +19 -8
  4. package/lib/project-filesystem.js +10 -16
  5. package/lib/project-http.js +19 -11
  6. package/lib/project-logs-mcp-server.js +8 -8
  7. package/lib/project.js +3 -2
  8. package/lib/public/app.js +15 -9
  9. package/lib/public/css/git-panel.css +13 -12
  10. package/lib/public/css/git-placard.css +4 -4
  11. package/lib/public/css/mobile-nav.css +7 -0
  12. package/lib/public/css/sidebar.css +7 -0
  13. package/lib/public/modules/app-connection.js +3 -0
  14. package/lib/public/modules/app-messages.js +7 -5
  15. package/lib/public/modules/app-panels.js +47 -12
  16. package/lib/public/modules/app-rate-limit.js +148 -61
  17. package/lib/public/modules/default-vendor.js +116 -0
  18. package/lib/public/modules/filebrowser-tabs.js +1 -0
  19. package/lib/public/modules/filebrowser.js +45 -18
  20. package/lib/public/modules/issues.js +4 -6
  21. package/lib/public/modules/project-logs.js +4 -9
  22. package/lib/public/modules/right-workbench.js +44 -0
  23. package/lib/public/modules/scheduled-tasks.js +4 -5
  24. package/lib/public/modules/sidebar-mobile.js +51 -3
  25. package/lib/public/modules/sidebar-sessions.js +65 -5
  26. package/lib/public/modules/sticky-notes-browser.js +4 -13
  27. package/lib/public/modules/terminal.js +4 -4
  28. package/lib/sdk-message-processor.js +11 -2
  29. package/lib/server-default-vendor.js +60 -0
  30. package/lib/server.js +4 -0
  31. package/lib/session-notes-mcp-server.js +2 -4
  32. package/lib/users-default-vendor-preferences.js +80 -0
  33. package/lib/users.js +6 -0
  34. package/lib/ws-schema.js +3 -0
  35. package/lib/yoke/acp-agent-profiles.js +15 -2
  36. package/lib/yoke/adapters/codex.js +56 -13
  37. package/lib/yoke/vendor-registry.js +1 -1
  38. package/package.json +1 -1
@@ -14,6 +14,7 @@ import { renderMiniMarkdown, getTitle } from './sticky-note-markdown.js';
14
14
  import { enhanceClayLogLinks } from './clay-log-links.js';
15
15
  import { send } from './sticky-notes-shared.js';
16
16
  import { listNoteData, focusNoteOnCanvas, hideNotes, showNotes } from './sticky-notes.js';
17
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
17
18
 
18
19
  var panel = null;
19
20
  var gridEl = null;
@@ -21,18 +22,6 @@ var tabsEl = null;
21
22
 
22
23
  // Registered by app.js so this module never imports the other right-pane tools
23
24
  // and cannot form an import cycle with them.
24
- var exclusiveClosers = [];
25
-
26
- export function registerExclusiveClosers(fns) {
27
- exclusiveClosers = (fns || []).filter(function (fn) { return typeof fn === "function"; });
28
- }
29
-
30
- function closeOtherRightTools() {
31
- for (var i = 0; i < exclusiveClosers.length; i++) {
32
- try { exclusiveClosers[i](); } catch (e) { /* a missing tool must not block opening */ }
33
- }
34
- }
35
-
36
25
  function isClosed(data) {
37
26
  if (!data) return false;
38
27
  if (data.state === "closed") return true;
@@ -283,7 +272,7 @@ export function renderNotesBrowser() {
283
272
  export function openNotesBrowser() {
284
273
  ensurePanel();
285
274
  if (!panel) return;
286
- closeOtherRightTools();
275
+ claimRightWorkbench("notes-browser");
287
276
  // The floating canvas would otherwise sit on top of the pane.
288
277
  hideNotes();
289
278
  panel.classList.remove("hidden");
@@ -295,6 +284,7 @@ export function openNotesBrowser() {
295
284
  }
296
285
 
297
286
  export function closeNotesBrowser() {
287
+ releaseRightWorkbench("notes-browser");
298
288
  if (!store.get('notesBrowserOpen')) return;
299
289
  if (panel) panel.classList.add("hidden");
300
290
  // Fullscreen is always dropped on close so the next open is a bounded pane.
@@ -311,6 +301,7 @@ export function isNotesBrowserOpen() {
311
301
  }
312
302
 
313
303
  export function initNotesBrowser() {
304
+ registerRightWorkbench("notes-browser", closeNotesBrowser);
314
305
  document.addEventListener("keydown", function (e) {
315
306
  if (e.key !== "Escape") return;
316
307
  if (!store.get('notesBrowserOpen')) return;
@@ -1,11 +1,10 @@
1
1
  import { iconHtml, refreshIcons } from './icons.js';
2
2
  import { closeSidebar } from './sidebar.js';
3
- import { closeFileViewer } from './filebrowser.js';
3
+ import { claimRightWorkbench, registerRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
4
4
  import { copyToClipboard } from './utils.js';
5
5
  import { getTerminalTheme } from './theme.js';
6
6
  import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
7
7
  import { createKeyToolbar } from './terminal-toolbar.js';
8
- import { closeIssues } from './issues.js';
9
8
 
10
9
  var ctx;
11
10
  var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
@@ -136,6 +135,7 @@ function createMultiLineLinkProvider(xterm) {
136
135
  // --- Init ---
137
136
  export function initTerminal(_ctx) {
138
137
  ctx = _ctx;
138
+ registerRightWorkbench("terminal", closeTerminal);
139
139
 
140
140
  // Close panel button
141
141
  document.getElementById("terminal-close").addEventListener("click", function () {
@@ -193,8 +193,7 @@ export function openTerminal() {
193
193
  var container = ctx.terminalContainerEl;
194
194
 
195
195
  // Hide file viewer if open (also unwatches)
196
- closeFileViewer();
197
- closeIssues();
196
+ claimRightWorkbench("terminal");
198
197
 
199
198
  container.classList.remove("hidden");
200
199
  isOpen = true;
@@ -224,6 +223,7 @@ export function openTerminal() {
224
223
 
225
224
  // --- Close terminal panel (hide, detach, but keep PTYs alive) ---
226
225
  export function closeTerminal() {
226
+ releaseRightWorkbench("terminal");
227
227
  var container = ctx.terminalContainerEl;
228
228
  container.classList.remove("panel-fullscreen");
229
229
  container.classList.add("hidden");
@@ -583,7 +583,12 @@ function attachMessageProcessor(ctx) {
583
583
  && /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
584
584
  // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
585
585
  if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
586
+ var contextQuery = session.queryInstance;
587
+ var contextTurn = session.turnCount;
586
588
  contextUsage.readUsage(session, "summary").then(function(ctxUsage) {
589
+ if (session.queryInstance !== contextQuery || session.turnCount !== contextTurn) return;
590
+ // null means the shared reader suppressed a superseded response;
591
+ // only an adapter's explicit unavailable object may clear the UI.
587
592
  if (!ctxUsage) return;
588
593
  session.lastContextUsage = ctxUsage;
589
594
  sendToSession(session, { type: "context_usage", data: ctxUsage });
@@ -778,10 +783,12 @@ function attachMessageProcessor(ctx) {
778
783
  var info = parsed.rateLimitInfo;
779
784
  console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
780
785
 
781
- // Broadcast reset time for top-bar usage link
786
+ // Persist the scoped reset snapshot so reconnect/history restore can reproject it.
782
787
  if (info.rateLimitType && info.resetsAt) {
783
- send({
788
+ sendAndRecord(session, {
784
789
  type: "rate_limit_usage",
790
+ sessionId: session.localId,
791
+ vendor: session.vendor || "claude",
785
792
  rateLimitType: info.rateLimitType,
786
793
  resetsAt: info.resetsAt * 1000,
787
794
  status: info.status,
@@ -792,6 +799,8 @@ function attachMessageProcessor(ctx) {
792
799
  if (info.status === "allowed_warning" || info.status === "rejected") {
793
800
  sendAndRecord(session, {
794
801
  type: "rate_limit",
802
+ sessionId: session.localId,
803
+ vendor: session.vendor || "claude",
795
804
  status: info.status,
796
805
  resetsAt: info.resetsAt ? info.resetsAt * 1000 : null,
797
806
  rateLimitType: info.rateLimitType || null,
@@ -0,0 +1,60 @@
1
+ // Authenticated user-wide New session vendor preference service.
2
+
3
+ function attachDefaultVendorService(deps) {
4
+ var users = deps.users;
5
+ var homeChatHandler = deps.homeChatHandler;
6
+ var forEachAppClient = deps.forEachAppClient;
7
+ var revisions = {};
8
+ var epoch = deps.serverEpoch || require("crypto").randomBytes(12).toString("hex");
9
+
10
+ function multiUser() { return users.isMultiUser() === true; }
11
+ function actor(ws) {
12
+ if (!multiUser()) return { userId: "default", user: true };
13
+ var claimed = ws && ws._clayUser && ws._clayUser.id;
14
+ var user = claimed ? users.findUserById(claimed) : null;
15
+ return user ? { userId: user.id, user: user } : { userId: null, user: null };
16
+ }
17
+ function sameActor(ws, userId) { var current = actor(ws); return current.user && current.userId === userId; }
18
+ function send(ws, message) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(Object.assign({ serverEpoch: epoch }, message))); }
19
+ function revision(userId) { return revisions[userId] || 0; }
20
+ function installedVendors(ws, who) {
21
+ var found;
22
+ try { found = homeChatHandler.findMateProject(multiUser() ? who.userId : null, null, true); } catch (e) { return []; }
23
+ if (!found || !found.ctx || typeof found.ctx.getVendorModelAvailability !== "function") return [];
24
+ var socket = Object.create(ws || null);
25
+ socket._clayUser = who.user === true ? null : who.user;
26
+ var choices = found.ctx.getVendorModelAvailability(socket) || [];
27
+ return choices.filter(function (choice) { return choice && choice.installed === true; }).map(function (choice) { return choice.id; });
28
+ }
29
+ function broadcast(userId, message, except) {
30
+ forEachAppClient(function (client) { if (client === except || !sameActor(client, userId)) return; send(client, message); });
31
+ }
32
+ function handle(ws, msg, projectSlug) {
33
+ if (!msg || ["default_vendor_get", "default_vendor_set"].indexOf(msg.type) === -1) return false;
34
+ var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 200) : null;
35
+ var who = actor(ws);
36
+ if (!who.user) { send(ws, { type: "default_vendor_state", requestId: requestId, accountAvailable: false, accountId: null, projectSlug: projectSlug || null, installedVendors: [], preference: null, error: "Your account is no longer available." }); return true; }
37
+ var installed = installedVendors(ws, who);
38
+ if (msg.type === "default_vendor_get") {
39
+ var saved = users.getDefaultVendorPreference(who.userId);
40
+ send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: saved.vendor, preferencePresent: saved.present, ready: !saved.error, canonicalRevision: revision(who.userId), error: saved.error || "" });
41
+ return true;
42
+ }
43
+ if (msg.vendor !== "" && (typeof msg.vendor !== "string" || installed.indexOf(msg.vendor) === -1)) {
44
+ send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: users.getDefaultVendorPreference(who.userId).vendor, ready: false, canonicalRevision: revision(who.userId), error: "That vendor is not installed or authorized." });
45
+ return true;
46
+ }
47
+ var savedResult = users.setDefaultVendorPreference(who.userId, msg.vendor);
48
+ if (!savedResult.ok) { send(ws, { type: "default_vendor_state", requestId: requestId, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, ready: false, error: savedResult.error }); return true; }
49
+ revisions[who.userId] = revision(who.userId) + 1;
50
+ var state = { type: "default_vendor_state", requestId: null, projectSlug: projectSlug || null, accountAvailable: true, accountId: who.userId, installedVendors: installed, preference: msg.vendor || null, preferencePresent: !!msg.vendor, ready: true, canonicalRevision: revisions[who.userId], error: "" };
51
+ var broadcastState = Object.assign({}, state);
52
+ delete broadcastState.projectSlug;
53
+ broadcast(who.userId, broadcastState, ws);
54
+ send(ws, Object.assign({}, state, { requestId: requestId }));
55
+ return true;
56
+ }
57
+ return { handleMessage: handle };
58
+ }
59
+
60
+ module.exports = { attachDefaultVendorService: attachDefaultVendorService };
package/lib/server.js CHANGED
@@ -17,6 +17,7 @@ var serverHomeChat = require("./server-home-chat");
17
17
  var shouldSuppressHomeResponse = require("./home-response-notifications").shouldSuppressHomeResponse;
18
18
  var serverHomePreferences = require("./server-home-preferences");
19
19
  var serverDefaultAi = require("./server-default-ai");
20
+ var serverDefaultVendor = require("./server-default-vendor");
20
21
  var serverCursorSharing = require("./server-cursor-sharing");
21
22
  var serverTools = require("./server-tools");
22
23
  var serverAdmin = require("./server-admin");
@@ -1067,6 +1068,7 @@ function createServer(opts) {
1067
1068
  var wss = new WebSocketServer({ noServer: true });
1068
1069
  var homePreferencesHandler = null;
1069
1070
  var defaultAiService = null;
1071
+ var defaultVendorService = null;
1070
1072
  var cursorSharingService = null;
1071
1073
 
1072
1074
  // Slug-less /ws handler: lets a user with no projects yet load the regular
@@ -1552,6 +1554,7 @@ function createServer(opts) {
1552
1554
  });
1553
1555
 
1554
1556
  defaultAiService = serverDefaultAi.attachDefaultAiService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1557
+ defaultVendorService = serverDefaultVendor.attachDefaultVendorService({ users: users, homeChatHandler: homeChatHandler, forEachAppClient: forEachAppClient });
1555
1558
  cursorSharingService = serverCursorSharing.attachCursorSharingService({ users: users, forEachAppClient: forEachAppClient });
1556
1559
 
1557
1560
  // --- User-installed home tools handler ---
@@ -1580,6 +1583,7 @@ function createServer(opts) {
1580
1583
 
1581
1584
  function handleDmMessage(ws, msg, routedProjectSlug) {
1582
1585
  if (cursorSharingService && cursorSharingService.handleMessage(ws, msg)) return true;
1586
+ if (defaultVendorService && defaultVendorService.handleMessage(ws, msg, routedProjectSlug)) return true;
1583
1587
  if (defaultAiService && defaultAiService.handleMessage(ws, msg)) return true;
1584
1588
  if (assignmentService.handleMessage(ws, msg, routedProjectSlug)) return true;
1585
1589
  if (dmHandler.handleMessage(ws, msg)) return true;
@@ -4,10 +4,8 @@ var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
5
  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
- "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. 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
- "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. " +
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, an Issue, or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; they are not mandatory duplicates of Project Issues. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
11
9
  "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
10
  "Updates are visible too: update only when durable state materially changes. " +
13
11
  "A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
@@ -0,0 +1,80 @@
1
+ // Durable user-scoped New session vendor preference storage.
2
+
3
+ var PREFERENCE_KEY = "defaultVendorPreference";
4
+
5
+ function validVendor(value) {
6
+ return typeof value === "string" && /^[a-z0-9][a-z0-9_-]{0,39}$/.test(value);
7
+ }
8
+
9
+ function attachDefaultVendorPreferences(deps) {
10
+ var loadUsers = deps.loadUsers;
11
+ var saveUsers = deps.saveUsers;
12
+ var isMultiUser = deps.isMultiUser || function () { return false; };
13
+ var configModule = deps.configModule || require("./config");
14
+
15
+ function getRecord(data, userId) {
16
+ var records = Array.isArray(data.users) ? data.users : [];
17
+ for (var i = 0; i < records.length; i++) if (records[i] && records[i].id === userId) return records[i];
18
+ return null;
19
+ }
20
+
21
+ function readValue(value, source) {
22
+ if (value === undefined || value === null || value === "") return { present: false, vendor: null, source: null };
23
+ if (!validVendor(value)) return { present: true, vendor: null, source: source, error: "Default coding vendor is invalid." };
24
+ return { present: true, vendor: value, source: source };
25
+ }
26
+
27
+ function getDefaultVendor(userId) {
28
+ var data;
29
+ try { data = loadUsers(); } catch (error) { return { present: false, vendor: null, source: null, error: "Unable to read default coding vendor: " + (error.message || error) }; }
30
+ var record = getRecord(data, userId);
31
+ if (isMultiUser() && !record) return { present: false, vendor: null, source: null, error: "User not found." };
32
+ if (record && Object.prototype.hasOwnProperty.call(record, PREFERENCE_KEY)) return readValue(record[PREFERENCE_KEY], "explicit");
33
+ if (record) return { present: false, vendor: null, source: null };
34
+ if (userId === "default") {
35
+ var config;
36
+ try { config = configModule.loadConfig() || {}; } catch (configError) { return { present: false, vendor: null, source: null, error: "Unable to read default coding vendor: " + (configError.message || configError) }; }
37
+ if (Object.prototype.hasOwnProperty.call(config, PREFERENCE_KEY)) return readValue(config[PREFERENCE_KEY], "explicit");
38
+ }
39
+ return { present: false, vendor: null, source: null };
40
+ }
41
+
42
+ function setDefaultVendor(userId, vendor) {
43
+ if (vendor !== "" && !validVendor(vendor)) return { ok: false, error: "Default coding vendor is invalid." };
44
+ var data;
45
+ try { data = loadUsers(); } catch (error) { return { ok: false, error: "Unable to read default coding vendor: " + (error.message || error) }; }
46
+ var record = getRecord(data, userId);
47
+ if (isMultiUser() && !record) return { ok: false, error: "User not found." };
48
+ if (record) {
49
+ var nextData = Object.assign({}, data, { users: (data.users || []).map(function (item) {
50
+ if (item !== record) return item;
51
+ var next = Object.assign({}, item);
52
+ if (vendor) next.defaultVendorPreference = vendor;
53
+ else delete next.defaultVendorPreference;
54
+ return next;
55
+ }) });
56
+ try { saveUsers(nextData); } catch (saveError) { return { ok: false, error: "Unable to save default coding vendor: " + (saveError.message || saveError) }; }
57
+ if (!vendor && userId === "default") {
58
+ try {
59
+ var recordConfig = configModule.loadConfig() || {};
60
+ delete recordConfig[PREFERENCE_KEY];
61
+ configModule.saveConfig(Object.assign({}, recordConfig));
62
+ } catch (recordConfigError) { return { ok: false, error: "Unable to clear default coding vendor: " + (recordConfigError.message || recordConfigError) }; }
63
+ }
64
+ return { ok: true, vendor: vendor };
65
+ }
66
+ if (userId !== "default") return { ok: false, error: "User not found." };
67
+ try {
68
+ var current = configModule.loadConfig() || {};
69
+ var nextConfig = Object.assign({}, current);
70
+ if (vendor) nextConfig.defaultVendorPreference = vendor;
71
+ else delete nextConfig.defaultVendorPreference;
72
+ configModule.saveConfig(nextConfig);
73
+ return { ok: true, vendor: vendor };
74
+ } catch (configSaveError) { return { ok: false, error: "Unable to save default coding vendor: " + (configSaveError.message || configSaveError) }; }
75
+ }
76
+
77
+ return { getDefaultVendor: getDefaultVendor, setDefaultVendor: setDefaultVendor, validVendor: validVendor };
78
+ }
79
+
80
+ module.exports = { attachDefaultVendorPreferences: attachDefaultVendorPreferences, validVendor: validVendor, PREFERENCE_KEY: PREFERENCE_KEY };
package/lib/users.js CHANGED
@@ -11,6 +11,7 @@ var { attachHomeSurfacePreferences } = require("./users-home-surface-preferences
11
11
  var { attachExperimentalPreferences } = require("./users-experimental-preferences");
12
12
  var { attachScheduledTaskPreferences } = require("./users-scheduled-task-preferences");
13
13
  var { attachDefaultAiPreferences } = require("./users-default-ai-preferences");
14
+ var { attachDefaultVendorPreferences } = require("./users-default-vendor-preferences");
14
15
 
15
16
  var USERS_FILE = path.join(CONFIG_DIR, "users.json");
16
17
 
@@ -404,6 +405,7 @@ var homeSurfacePreferences = attachHomeSurfacePreferences({ loadUsers: loadUsers
404
405
  var experimentalPreferences = attachExperimentalPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
405
406
  var scheduledTaskPreferences = attachScheduledTaskPreferences({ loadUsers: loadUsers, saveUsers: saveUsers });
406
407
  var defaultAiPreferences = attachDefaultAiPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
408
+ var defaultVendorPreferences = attachDefaultVendorPreferences({ loadUsers: loadUsers, saveUsers: saveUsers, isMultiUser: auth.isMultiUser });
407
409
 
408
410
  // Alias auth functions
409
411
  var isMultiUser = auth.isMultiUser;
@@ -464,6 +466,8 @@ var getScheduledTaskInterviewEngine = scheduledTaskPreferences.get;
464
466
  var setScheduledTaskInterviewEngine = scheduledTaskPreferences.set;
465
467
  var getDefaultAiPreference = defaultAiPreferences.getDefaultAiPreference;
466
468
  var setDefaultAiPreference = defaultAiPreferences.setDefaultAiPreference;
469
+ var getDefaultVendorPreference = defaultVendorPreferences.getDefaultVendor;
470
+ var setDefaultVendorPreference = defaultVendorPreferences.setDefaultVendor;
467
471
 
468
472
  module.exports = {
469
473
  USERS_FILE: USERS_FILE,
@@ -545,6 +549,8 @@ module.exports = {
545
549
  setScheduledTaskInterviewEngine: setScheduledTaskInterviewEngine,
546
550
  getDefaultAiPreference: getDefaultAiPreference,
547
551
  setDefaultAiPreference: setDefaultAiPreference,
552
+ getDefaultVendorPreference: getDefaultVendorPreference,
553
+ setDefaultVendorPreference: setDefaultVendorPreference,
548
554
  getDeletedBuiltinKeys: getDeletedBuiltinKeys,
549
555
  addDeletedBuiltinKey: addDeletedBuiltinKey,
550
556
  removeDeletedBuiltinKey: removeDeletedBuiltinKey,
package/lib/ws-schema.js CHANGED
@@ -723,6 +723,9 @@ var schema = {
723
723
  "default_ai_get": { direction: "c2s", handler: "lib/server-default-ai.js", description: "Read the authenticated user's shared Default AI and available providers" },
724
724
  "default_ai_catalog_get": { direction: "c2s", handler: "lib/server-default-ai.js", description: "Read one installed provider catalog for the Default AI picker" },
725
725
  "default_ai_set": { direction: "c2s", handler: "lib/server-default-ai.js", description: "Validate and persist the authenticated user's shared Default AI" },
726
+ "default_vendor_get": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Read the authenticated user's New session vendor preference" },
727
+ "default_vendor_set": { direction: "c2s", handler: "lib/server-default-vendor.js", description: "Validate and persist the authenticated user's New session vendor preference" },
728
+ "default_vendor_state": { direction: "s2c", handler: "lib/public/modules/default-vendor.js", description: "Correlated or same-user-broadcast New session vendor preference" },
726
729
  "default_ai_state": { direction: "s2c", handler: "lib/public/modules/default-ai.js", description: "Correlated or same-user-broadcast canonical Default AI state" },
727
730
  "default_ai_catalog": { direction: "s2c", handler: "lib/public/modules/default-ai.js", description: "Correlated Default AI provider catalog" },
728
731
  "home_mate_history": { direction: "s2c", handler: "lib/public/modules/home-mate-chat.js", description: "Selected mate's initial or refreshed chat history" },
@@ -82,13 +82,26 @@ function isSafeOpenCodePermission(value) {
82
82
  }
83
83
 
84
84
  function validateOpenCodeConfig(config) {
85
- if (!config || config.permission !== "ask") {
85
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
86
+ throw new Error("OpenCode resolved configuration is malformed");
87
+ }
88
+ var globalPermission = config.permission;
89
+ var globalAsk = globalPermission === "ask" || (
90
+ isSafeOpenCodePermission(globalPermission) && globalPermission["*"] === "ask"
91
+ );
92
+ if (!globalAsk) {
86
93
  throw new Error("OpenCode resolved configuration does not preserve global ask permissions");
87
94
  }
95
+ if (config.agent !== undefined && (!config.agent || typeof config.agent !== "object" || Array.isArray(config.agent))) {
96
+ throw new Error("OpenCode resolved configuration has malformed agents");
97
+ }
88
98
  var agents = config.agent || {};
89
99
  var names = Object.keys(agents);
90
100
  for (var i = 0; i < names.length; i++) {
91
- var agent = agents[names[i]] || {};
101
+ var agent = agents[names[i]];
102
+ if (!agent || typeof agent !== "object" || Array.isArray(agent)) {
103
+ throw new Error("OpenCode agent has malformed resolved configuration: " + names[i]);
104
+ }
92
105
  if (agent.permission === undefined) continue;
93
106
  if (!isSafeOpenCodePermission(agent.permission)) {
94
107
  throw new Error("OpenCode agent has unsafe resolved permissions: " + names[i]);
@@ -181,6 +181,11 @@ function flattenEvent(notification, state) {
181
181
 
182
182
  if (method === "turn/started") {
183
183
  state.turnStarted = true;
184
+ state.currentTurnId = params.turnId || params.turn_id || (params.turn && params.turn.id) || null;
185
+ state.lastInputTokens = null;
186
+ state.lastCachedTokens = null;
187
+ state.lastOutputTokens = null;
188
+ state.currentContextUsage = null;
184
189
  var userUuid = generateUuid();
185
190
  events.push({ yokeType: "turn_start", uuid: userUuid, messageType: "user" });
186
191
  return events;
@@ -194,16 +199,38 @@ function flattenEvent(notification, state) {
194
199
  if (turnStatus === "interrupted" || state.aborted) {
195
200
  events.push({ yokeType: "interrupted" });
196
201
  }
197
- var inputTokens = state.lastInputTokens !== null && state.lastInputTokens !== undefined
198
- ? state.lastInputTokens : (usage ? (usage.input_tokens || 0) : 0);
199
- var outputTokens = (usage ? (usage.output_tokens || 0) : 0) || state.lastOutputTokens || 0;
200
- var cachedTokens = (usage ? (usage.cached_input_tokens || 0) : 0) || state.lastCachedTokens || 0;
202
+ // Generic usage reporting is independent from current context occupancy.
203
+ // The latter is supplied only by a valid thread/tokenUsage snapshot.
204
+ // Some normal protocol completions omit usage even though the preceding
205
+ // token snapshot carried useful per-turn accounting. Keep that fallback
206
+ // for the generic usage panel, but never use it as context occupancy.
207
+ var inputTokens = usage && typeof usage.input_tokens === "number"
208
+ ? usage.input_tokens : (typeof state.lastInputTokens === "number" ? state.lastInputTokens : 0);
209
+ var outputTokens = usage && typeof usage.output_tokens === "number"
210
+ ? usage.output_tokens : (typeof state.lastOutputTokens === "number" ? state.lastOutputTokens : 0);
211
+ var cachedTokens = usage && typeof usage.cached_input_tokens === "number"
212
+ ? usage.cached_input_tokens : (typeof state.lastCachedTokens === "number" ? state.lastCachedTokens : 0);
201
213
  var hasTokenData = inputTokens > 0 || outputTokens > 0;
202
214
  var resultModelUsage = {};
203
215
  // Codex reports the real window in thread/tokenUsage/updated. Dropping it
204
216
  // made the client fall back to its model-name table, which has no entry
205
217
  // for codex model ids and defaulted to Claude's 200k denominator.
206
- resultModelUsage[state.model] = { contextWindow: state.modelContextWindow || null };
218
+ var contextSnapshot = { valid: false };
219
+ if (state.currentContextUsage
220
+ && typeof state.currentContextUsage.input_tokens === "number"
221
+ && typeof state.currentContextUsage.contextWindow === "number"
222
+ && state.currentContextUsage.contextWindow > 0) {
223
+ contextSnapshot = {
224
+ valid: true,
225
+ inputTokens: state.currentContextUsage.input_tokens,
226
+ contextWindow: state.currentContextUsage.contextWindow,
227
+ };
228
+ if (state.currentContextUsage.turnId) contextSnapshot.turnId = state.currentContextUsage.turnId;
229
+ }
230
+ resultModelUsage[state.model] = {
231
+ contextWindow: state.modelContextWindow || null,
232
+ contextSnapshot: contextSnapshot,
233
+ };
207
234
  var assistantUuid = generateUuid();
208
235
  events.push({
209
236
  yokeType: "result",
@@ -683,24 +710,36 @@ function flattenEvent(notification, state) {
683
710
  if (method === "thread/tokenUsage/updated") {
684
711
  var tu = params.tokenUsage;
685
712
  if (tu) {
713
+ var tokenTurnId = params.turnId || params.turn_id || tu.turnId || null;
714
+ if (tokenTurnId && state.currentTurnId && tokenTurnId !== state.currentTurnId) {
715
+ state.lastInputTokens = null;
716
+ state.lastCachedTokens = null;
717
+ state.lastOutputTokens = null;
718
+ state.currentContextUsage = null;
719
+ return events;
720
+ }
721
+ var currentWindow = null;
686
722
  var turnUsage = tu.lastTurn;
687
723
  if (turnUsage) {
688
724
  var candidateInput = typeof turnUsage.inputTokens === "number" ? turnUsage.inputTokens : null;
689
- var currentWindow = typeof tu.modelContextWindow === "number" ? tu.modelContextWindow : null;
690
- state.lastInputTokens = candidateInput !== null && (currentWindow === null || candidateInput <= currentWindow) ? candidateInput : null;
725
+ currentWindow = typeof tu.modelContextWindow === "number" && isFinite(tu.modelContextWindow) && tu.modelContextWindow > 0
726
+ ? tu.modelContextWindow : null;
727
+ state.lastInputTokens = candidateInput !== null && isFinite(candidateInput) && candidateInput >= 0 && currentWindow !== null && candidateInput <= currentWindow ? candidateInput : null;
691
728
  state.lastCachedTokens = turnUsage.cachedInputTokens || 0;
692
729
  state.lastOutputTokens = turnUsage.outputTokens || 0;
693
- state.currentContextUsage = state.lastInputTokens !== null &&
694
- (currentWindow === null || state.lastInputTokens <= currentWindow)
695
- ? { input_tokens: state.lastInputTokens, contextWindow: currentWindow }
696
- : { input_tokens: null, contextWindow: currentWindow };
730
+ if (state.lastInputTokens !== null && currentWindow !== null) {
731
+ state.currentContextUsage = { input_tokens: state.lastInputTokens, contextWindow: currentWindow };
732
+ if (tokenTurnId || state.currentTurnId) state.currentContextUsage.turnId = tokenTurnId || state.currentTurnId;
733
+ } else {
734
+ state.currentContextUsage = null;
735
+ }
697
736
  } else {
698
737
  state.lastInputTokens = null;
699
738
  state.lastCachedTokens = null;
700
739
  state.lastOutputTokens = null;
701
740
  state.currentContextUsage = null;
702
741
  }
703
- if (tu.modelContextWindow) state.modelContextWindow = tu.modelContextWindow;
742
+ if (currentWindow !== null) state.modelContextWindow = currentWindow;
704
743
  }
705
744
  return events;
706
745
  }
@@ -750,6 +789,7 @@ function createEventState(model) {
750
789
  lastCachedTokens: null,
751
790
  lastOutputTokens: null,
752
791
  currentContextUsage: null,
792
+ currentTurnId: null,
753
793
  modelContextWindow: null,
754
794
  done: false,
755
795
  aborted: false,
@@ -1288,7 +1328,10 @@ function createCodexQueryHandle(appServer, queryOpts) {
1288
1328
  },
1289
1329
 
1290
1330
  getContextUsage: function() {
1291
- return Promise.resolve(state.currentContextUsage);
1331
+ return Promise.resolve(state.currentContextUsage || {
1332
+ unavailable: true,
1333
+ reason: "current_context_unavailable",
1334
+ });
1292
1335
  },
1293
1336
 
1294
1337
  abort: function() {
@@ -38,7 +38,7 @@ var VENDOR_REGISTRY = {
38
38
  usageDashboard: {
39
39
  icon: "/codex-avatar.png",
40
40
  alt: "Codex",
41
- href: "https://chatgpt.com/admin/usage",
41
+ href: "https://chatgpt.com/codex/settings/usage",
42
42
  title: "Check usage on ChatGPT",
43
43
  },
44
44
  rateLimitTracking: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.1.0-beta.13",
3
+ "version": "4.1.0-beta.15",
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"