clay-server 4.1.0-beta.8 → 4.1.0

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 (119) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/issues-mcp-server.js +1 -1
  3. package/lib/issues-service.js +3 -1
  4. package/lib/loop-registry-occurrence.js +1 -0
  5. package/lib/notes.js +20 -3
  6. package/lib/os-users.js +6 -0
  7. package/lib/project-connection.js +14 -3
  8. package/lib/project-file-path.js +13 -0
  9. package/lib/project-file-watch.js +19 -8
  10. package/lib/project-filesystem.js +10 -16
  11. package/lib/project-http.js +53 -48
  12. package/lib/project-logs-mcp-server.js +8 -8
  13. package/lib/project-logs-schema.js +1 -1
  14. package/lib/project-logs-service.js +46 -13
  15. package/lib/project-logs-store.js +17 -15
  16. package/lib/project-logs.js +70 -0
  17. package/lib/project-loop.js +1 -0
  18. package/lib/project-notifications.js +1 -1
  19. package/lib/project-pending-message-queue.js +71 -7
  20. package/lib/project-scheduled-messages.js +235 -52
  21. package/lib/project-scheduled-result-store.js +131 -0
  22. package/lib/project-scheduled-task-execution.js +35 -2
  23. package/lib/project-session-notes.js +11 -4
  24. package/lib/project-sessions.js +7 -0
  25. package/lib/project-user-message.js +72 -5
  26. package/lib/project.js +60 -5
  27. package/lib/public/app.js +27 -12
  28. package/lib/public/css/avatar-imprints.css +1 -1
  29. package/lib/public/css/command-palette.css +20 -0
  30. package/lib/public/css/git-panel.css +13 -12
  31. package/lib/public/css/git-placard.css +4 -4
  32. package/lib/public/css/input.css +0 -109
  33. package/lib/public/css/issues.css +78 -31
  34. package/lib/public/css/mobile-nav.css +7 -0
  35. package/lib/public/css/notifications-center.css +3 -3
  36. package/lib/public/css/pending-message-queue.css +3 -0
  37. package/lib/public/css/project-logs.css +15 -0
  38. package/lib/public/css/sidebar.css +7 -0
  39. package/lib/public/css/sticky-notes.css +29 -0
  40. package/lib/public/modules/app-connection.js +9 -1
  41. package/lib/public/modules/app-favicon.js +10 -0
  42. package/lib/public/modules/app-messages.js +49 -37
  43. package/lib/public/modules/app-notifications.js +2 -0
  44. package/lib/public/modules/app-panels.js +77 -23
  45. package/lib/public/modules/app-projects.js +3 -0
  46. package/lib/public/modules/app-rate-limit.js +148 -243
  47. package/lib/public/modules/avatar-imprint.js +25 -70
  48. package/lib/public/modules/avatar.js +12 -4
  49. package/lib/public/modules/chat-render-runtime.js +20 -2
  50. package/lib/public/modules/clay-issue-links.js +23 -2
  51. package/lib/public/modules/clay-log-links.js +11 -1
  52. package/lib/public/modules/context-view-preference.js +143 -0
  53. package/lib/public/modules/default-vendor.js +116 -0
  54. package/lib/public/modules/filebrowser-tabs.js +1 -0
  55. package/lib/public/modules/filebrowser.js +45 -18
  56. package/lib/public/modules/input.js +81 -20
  57. package/lib/public/modules/issues-render.js +36 -10
  58. package/lib/public/modules/issues.js +95 -13
  59. package/lib/public/modules/markdown.js +3 -1
  60. package/lib/public/modules/pane-bridge.js +14 -8
  61. package/lib/public/modules/pane-issue-bridge.js +27 -0
  62. package/lib/public/modules/pending-message-queue.js +43 -1
  63. package/lib/public/modules/permission-hints.js +13 -0
  64. package/lib/public/modules/profile.js +9 -9
  65. package/lib/public/modules/project-activation.js +49 -0
  66. package/lib/public/modules/project-logs-render.js +13 -3
  67. package/lib/public/modules/project-logs.js +45 -15
  68. package/lib/public/modules/right-workbench.js +44 -0
  69. package/lib/public/modules/scheduled-message-state.js +12 -36
  70. package/lib/public/modules/scheduled-result-client.js +33 -0
  71. package/lib/public/modules/scheduled-result-display.js +50 -0
  72. package/lib/public/modules/scheduled-result-notification.js +31 -0
  73. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  74. package/lib/public/modules/scheduled-tasks.js +9 -21
  75. package/lib/public/modules/sidebar-mobile.js +51 -3
  76. package/lib/public/modules/sidebar-sessions.js +65 -5
  77. package/lib/public/modules/split-session-boundary.js +28 -0
  78. package/lib/public/modules/split-view.js +42 -7
  79. package/lib/public/modules/sticky-notes-browser.js +130 -18
  80. package/lib/public/modules/sticky-notes.js +2 -3
  81. package/lib/public/modules/terminal.js +4 -4
  82. package/lib/public/modules/thinking-lifecycle.js +4 -1
  83. package/lib/public/modules/tools.js +5 -1
  84. package/lib/scheduled-run-state.js +10 -2
  85. package/lib/scheduler.js +1 -0
  86. package/lib/sdk-bridge.js +97 -61
  87. package/lib/sdk-message-processor.js +44 -13
  88. package/lib/sdk-skill-discovery.js +6 -28
  89. package/lib/sdk-turn-queue.js +14 -0
  90. package/lib/server-context-view.js +53 -0
  91. package/lib/server-default-vendor.js +60 -0
  92. package/lib/server.js +23 -6
  93. package/lib/session-notes-mcp-server.js +3 -5
  94. package/lib/session-pair-factory.js +2 -2
  95. package/lib/session-pair-mcp-server.js +5 -4
  96. package/lib/session-title-generator.js +89 -0
  97. package/lib/session-title-policy.js +8 -2
  98. package/lib/sessions.js +26 -1
  99. package/lib/user-presence.js +6 -0
  100. package/lib/users-context-view-preferences.js +65 -0
  101. package/lib/users-default-vendor-preferences.js +80 -0
  102. package/lib/users.js +12 -0
  103. package/lib/ws-schema.js +11 -0
  104. package/lib/yoke/acp-agent-profiles.js +15 -2
  105. package/lib/yoke/adapters/acp.js +3 -2
  106. package/lib/yoke/adapters/antigravity.js +2 -2
  107. package/lib/yoke/adapters/claude-worker.js +11 -12
  108. package/lib/yoke/adapters/claude.js +75 -22
  109. package/lib/yoke/adapters/codex.js +216 -29
  110. package/lib/yoke/adapters/kiro.js +6 -5
  111. package/lib/yoke/claude-context-usage.js +57 -0
  112. package/lib/yoke/claude-message-correlation.js +31 -0
  113. package/lib/yoke/claude-model-switch.js +15 -0
  114. package/lib/yoke/index.js +5 -1
  115. package/lib/yoke/mcp-bridge-server.js +18 -5
  116. package/lib/yoke/session-skill-catalog.js +45 -0
  117. package/lib/yoke/skill-discovery.js +60 -20
  118. package/lib/yoke/vendor-registry.js +1 -1
  119. package/package.json +5 -7
@@ -1,7 +1,7 @@
1
1
  import { escapeHtml } from './utils.js';
2
2
  import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { store } from './store.js';
4
- import { addToMessages, scrollToBottom } from './chat-render-runtime.js';
4
+ import { addToMessages, keepActiveThinkingAtTail, scrollToBottom } from './chat-render-runtime.js';
5
5
  import { prepareThinkingView, bindThinkingView, updateThinkingView, beginThinkingView, setThinkingViewLive, finishThinkingView } from './thinking-view.js';
6
6
 
7
7
  function getThinkingState() {
@@ -92,6 +92,7 @@ export function startThinkingSegment() {
92
92
  segmentText: "",
93
93
  startTime: Date.now(),
94
94
  });
95
+ if (!store.get('replayingHistory')) keepActiveThinkingAtTail();
95
96
  beginThinkingView(thinkingState.el, !store.get('replayingHistory') && !isMateThinking());
96
97
  showMateActivity(thinkingState.el, true, !!thinkingState.detailsText.trim());
97
98
  refreshIcons();
@@ -109,6 +110,7 @@ export function appendThinkingText(text) {
109
110
  detailsText: thinkingState.detailsText + separator + text,
110
111
  latestText: segmentText.trim() ? segmentText : thinkingState.latestText,
111
112
  });
113
+ if (!store.get('replayingHistory')) keepActiveThinkingAtTail();
112
114
  updateThinkingView(thinkingState.el, thinkingState.segmentText || thinkingState.latestText, thinkingState.detailsText);
113
115
  maybeScrollToBottom();
114
116
  }
@@ -156,6 +158,7 @@ export function restoreThinkingState(saved) {
156
158
  export function resumeThinkingAfterReplay(processing) {
157
159
  var thinkingState = getThinkingState();
158
160
  if (!thinkingState || !thinkingState.active || !processing) return;
161
+ keepActiveThinkingAtTail();
159
162
  setThinkingViewLive(thinkingState.el, !isMateThinking());
160
163
  showMateActivity(thinkingState.el, true, !!thinkingState.detailsText.trim());
161
164
  }
@@ -6,6 +6,7 @@ import { openFile } from './filebrowser.js';
6
6
  import { mateAvatarUrl } from './avatar.js';
7
7
  import { getChatLayout } from './theme.js';
8
8
  import { store } from './store.js';
9
+ import { applyPermissionHints } from './permission-hints.js';
9
10
  import { VENDOR_NAMES } from './app-rendering.js';
10
11
  import { startThinkingSegment, appendThinkingText, stopThinkingSegment, resetThinkingTurn, clearThinkingState, saveThinkingState, restoreThinkingState, updateThinkingTokenEstimate } from './thinking-lifecycle.js';
11
12
  import { findAskUserCard, getAskUserRequestKey, hasAskUserRequest, rememberAskUserRequest, markAskUserRequestAnswered, saveAskUserState, restoreAskUserState, resetAskUserState } from './ask-user-state.js';
@@ -542,7 +543,7 @@ function permissionInputSummary(toolName, input) {
542
543
  }
543
544
  }
544
545
 
545
- export function renderPermissionRequest(requestId, toolName, toolInput, decisionReason, mateId, vendor) {
546
+ export function renderPermissionRequest(requestId, toolName, toolInput, decisionReason, mateId, vendor, hints) {
546
547
  if (pendingPermissions[requestId]) return;
547
548
  ctx.finalizeAssistantBlock();
548
549
  stopThinking();
@@ -551,17 +552,20 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
551
552
  // ExitPlanMode: render as plan confirmation instead of generic permission
552
553
  if (toolName === "ExitPlanMode") {
553
554
  renderPlanPermission(requestId);
555
+ applyPermissionHints(pendingPermissions[requestId], { defaultToNo: hints && hints.defaultToNo });
554
556
  return;
555
557
  }
556
558
 
557
559
  // Channel layout or Mate DM: conversational "Can I ...?" style
558
560
  if ((ctx.isMateDm && ctx.isMateDm()) || getChatLayout() === "channel") {
559
561
  renderConversationalPermission(requestId, toolName, toolInput, mateId, vendor);
562
+ applyPermissionHints(pendingPermissions[requestId], hints);
560
563
  return;
561
564
  }
562
565
 
563
566
  // Bubble layout: formal "Permission Required" dialog
564
567
  renderFormalPermission(requestId, toolName, toolInput, decisionReason);
568
+ applyPermissionHints(pendingPermissions[requestId], hints);
565
569
  }
566
570
 
567
571
  function renderFormalPermission(requestId, toolName, toolInput, decisionReason) {
@@ -42,9 +42,11 @@ function createScheduledRunState(ctx) {
42
42
  }
43
43
 
44
44
  function complete(id, runId, result) {
45
- return mutate(id, function (rec) {
45
+ var completed = null;
46
+ var saved = mutate(id, function (rec) {
46
47
  if (!rec.activeRun || rec.activeRun.runId !== runId) return false;
47
48
  var run = Object.assign({}, rec.activeRun, result || {}, { finishedAt: Date.now() });
49
+ completed = run;
48
50
  rec.lastRunAt = run.startedAt || Date.now();
49
51
  rec.lastRunResult = run.outcome || "failed";
50
52
  rec.runs = Array.isArray(rec.runs) ? rec.runs : [];
@@ -54,6 +56,8 @@ function createScheduledRunState(ctx) {
54
56
  rec.activeRun = null;
55
57
  return true;
56
58
  });
59
+ if (saved && completed && ctx.onTerminal) ctx.onTerminal(ctx.getRecord(id), completed);
60
+ return saved;
57
61
  }
58
62
 
59
63
  function recover(rec) {
@@ -81,7 +85,11 @@ function createScheduledRunState(ctx) {
81
85
  return true;
82
86
  }
83
87
 
84
- return { begin: begin, complete: complete, prepareRecord: prepareRecord, recordLegacy: recordLegacy, recover: recover, update: update };
88
+ function notifyTerminal(rec, run) {
89
+ if (ctx.onTerminal && rec && run) ctx.onTerminal(rec, run);
90
+ }
91
+
92
+ return { begin: begin, complete: complete, notifyTerminal: notifyTerminal, prepareRecord: prepareRecord, recordLegacy: recordLegacy, recover: recover, update: update };
85
93
  }
86
94
 
87
95
  module.exports = { createScheduledRunState: createScheduledRunState };
package/lib/scheduler.js CHANGED
@@ -36,6 +36,7 @@ function createLoopRegistry(opts) {
36
36
  var scheduledRuns = createScheduledRunState({
37
37
  getRecord: getById, save: save,
38
38
  onChange: function () { if (onChange) onChange(records); },
39
+ onTerminal: opts.onRunTerminal,
39
40
  });
40
41
  var durableOccurrence;
41
42
 
package/lib/sdk-bridge.js CHANGED
@@ -7,12 +7,14 @@ var usersModule = require("./users");
7
7
  var { getCodexConfig } = require("./codex-defaults");
8
8
  var { getKiroConfig } = require("./kiro-defaults");
9
9
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
10
+ var skillDiscovery = require("./yoke/skill-discovery");
10
11
  var { isSafeBashSegment } = require("./safe-bash-commands");
11
12
  var { createMessageQueue } = require("./sdk-message-queue");
12
13
  var { attachMessageProcessor } = require("./sdk-message-processor");
13
14
  var homeDebateToolPolicy = require("./home-debate-tool-policy");
14
15
  var yoke = require("./yoke");
15
16
  var sessionTitlePolicy = require("./session-title-policy");
17
+ var sessionTitleGenerator = require("./session-title-generator");
16
18
  var loopInterview = require("./project-loop-interview");
17
19
 
18
20
  // Extract serializable tool descriptors from MCP server instances.
@@ -130,6 +132,7 @@ function createSDKBridge(opts) {
130
132
  var clayPort = opts.clayPort || 2633;
131
133
  var clayTls = opts.clayTls || false;
132
134
  var clayAuthToken = opts.clayAuthToken || null;
135
+ var clayBridgeToken = opts.clayBridgeToken || null;
133
136
  var getSessionToolDefs = opts.getSessionToolDefs || function () { return []; };
134
137
  var canUseSessionTools = opts.canUseSessionTools || function () { return true; };
135
138
  var queryToolSnapshots = new WeakMap();
@@ -153,6 +156,7 @@ function createSDKBridge(opts) {
153
156
  if (clayTls) bridgeArgs.push("--tls");
154
157
  var env = [];
155
158
  if (clayAuthToken) env.push({ name: "CLAY_AUTH_TOKEN", value: clayAuthToken });
159
+ if (clayBridgeToken) env.push({ name: "CLAY_BRIDGE_TOKEN", value: clayBridgeToken });
156
160
  return [{
157
161
  name: "clay-tools",
158
162
  command: process.execPath,
@@ -161,6 +165,23 @@ function createSDKBridge(opts) {
161
165
  }];
162
166
  }
163
167
 
168
+ function getCodexSessionMcpServer(session, queryGeneration) {
169
+ if (!session || !Number.isInteger(queryGeneration)) return null;
170
+ var bridgeArgs = [
171
+ path.join(__dirname, "yoke", "mcp-bridge-server.js"),
172
+ "--port", String(clayPort),
173
+ "--slug", slug,
174
+ "--session", String(session.localId),
175
+ "--query-generation", String(queryGeneration),
176
+ "--session-only",
177
+ ];
178
+ if (clayTls) bridgeArgs.push("--tls");
179
+ var bridgeEnv = {};
180
+ if (clayAuthToken) bridgeEnv.CLAY_AUTH_TOKEN = clayAuthToken;
181
+ if (clayBridgeToken) bridgeEnv.CLAY_BRIDGE_TOKEN = clayBridgeToken;
182
+ return { name: "clay-session-tools", command: process.execPath, args: bridgeArgs, env: bridgeEnv };
183
+ }
184
+
164
185
  function getFreshAuthState(force) {
165
186
  var now = Date.now();
166
187
  if (!force && _cachedFreshAuthState && now - _cachedFreshAuthAt < 15000) {
@@ -337,7 +358,7 @@ function createSDKBridge(opts) {
337
358
  }
338
359
 
339
360
  // --- Skill discovery (extracted to sdk-skill-discovery.js) ---
340
- var skills = attachSkillDiscovery({ cwd: cwd });
361
+ var skills = attachSkillDiscovery({ cwd: cwd, getOptions: function () { return { env: getRuntimeEnv() }; } });
341
362
  var discoverSkillDirs = skills.discoverSkillDirs;
342
363
  var mergeSkills = skills.mergeSkills;
343
364
 
@@ -350,50 +371,27 @@ function createSDKBridge(opts) {
350
371
  return owner && owner.linuxUser ? owner.linuxUser : null;
351
372
  }
352
373
 
353
- // Auto-generate a session title via YOKE adapter.generateTitle().
354
- // Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
355
- function autoGenerateTitle(session) {
356
- var sessionAdapter = getAdapterForSession(session);
357
- if (typeof sessionAdapter.generateTitle !== "function") {
358
- console.log("[auto-title] adapter.generateTitle not available for vendor=" + sessionAdapter.vendor);
359
- return;
360
- }
361
- var userMessages = [];
362
- for (var i = 0; i < session.history.length; i++) {
363
- var entry = session.history[i];
364
- if (entry.type === "user_message" && entry.text) {
365
- userMessages.push(entry.text.substring(0, 200));
366
- if (userMessages.length >= 5) break;
367
- }
368
- }
369
- if (userMessages.length === 0) {
370
- console.log("[auto-title] No user messages found in session " + session.localId);
371
- return;
374
+ function getSessionSkillOptions(session) {
375
+ var runtimeEnv = getRuntimeEnv(session || {});
376
+ if (!osUsers) return { env: runtimeEnv };
377
+ var linuxUser = getSessionLinuxUser(session);
378
+ if (!linuxUser) return { disable: true };
379
+ try {
380
+ var info = require("./os-users").resolveOsUserInfo(linuxUser);
381
+ return Object.assign({}, skillDiscovery.optionsForOsUser(info), { env: runtimeEnv });
382
+ } catch (e) {
383
+ return { disable: true };
372
384
  }
373
- console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
374
-
375
- sessionAdapter.generateTitle(userMessages, {
376
- cwd: cwd,
377
- linuxUser: getSessionLinuxUser(session) || undefined,
378
- env: getRuntimeEnv(session),
379
- }).then(function(title) {
380
- if (!title || title.length < 2) return;
381
- title = title.substring(0, 100);
382
- if (!session.titleManuallySet) {
383
- session.title = title;
384
- session.titleAutoGenerated = true;
385
- sm.saveSessionFile(session);
386
- sm.broadcastSessionList();
387
- if (session.cliSessionId && typeof adapter.renameSession === "function") {
388
- adapter.renameSession(session.cliSessionId, title, { dir: cwd }).catch(function () {});
389
- }
390
- console.log("[auto-title] Generated title for session " + session.localId + ": " + title);
391
- }
392
- }).catch(function(e) {
393
- console.error("[auto-title] Failed:", e.message || e);
394
- });
395
385
  }
396
386
 
387
+ var titleGenerator = sessionTitleGenerator.attachSessionTitleGenerator({
388
+ sm: sm,
389
+ cwd: cwd,
390
+ getAdapter: getAdapterForSession,
391
+ getLinuxUser: getSessionLinuxUser,
392
+ getRuntimeEnv: getRuntimeEnv,
393
+ });
394
+
397
395
  var msgProcessor = attachMessageProcessor({
398
396
  sm: sm,
399
397
  send: send,
@@ -407,9 +405,10 @@ function createSDKBridge(opts) {
407
405
  adapter: adapter,
408
406
  saveImageFile: opts.saveImageFile,
409
407
  getSessionLinuxUser: getSessionLinuxUser,
408
+ getSessionSkillOptions: getSessionSkillOptions,
410
409
  onProcessingChanged: onProcessingChanged,
411
410
  onTurnDone: onTurnDone,
412
- onAutoTitle: function (session) { autoGenerateTitle(session); },
411
+ onAutoTitle: function (session) { titleGenerator.generate(session); },
413
412
  opts: opts,
414
413
  discoverSkillDirs: discoverSkillDirs,
415
414
  mergeSkills: mergeSkills,
@@ -856,6 +855,8 @@ function createSDKBridge(opts) {
856
855
  input: input,
857
856
  toolUseId: opts.toolUseID,
858
857
  decisionReason: opts.decisionReason || "",
858
+ defaultToNo: opts.defaultToNo === true,
859
+ suppressAlwaysAllowRule: opts.suppressAlwaysAllowRule === true,
859
860
  signal: opts.signal,
860
861
  });
861
862
  if (routed) return routed;
@@ -872,6 +873,8 @@ function createSDKBridge(opts) {
872
873
  toolInput: input,
873
874
  toolUseId: opts.toolUseID,
874
875
  decisionReason: opts.decisionReason || "",
876
+ defaultToNo: opts.defaultToNo === true,
877
+ suppressAlwaysAllowRule: opts.suppressAlwaysAllowRule === true,
875
878
  };
876
879
 
877
880
  var permMsg = {
@@ -881,6 +884,8 @@ function createSDKBridge(opts) {
881
884
  toolInput: input,
882
885
  toolUseId: opts.toolUseID,
883
886
  decisionReason: opts.decisionReason || "",
887
+ defaultToNo: opts.defaultToNo === true,
888
+ suppressAlwaysAllowRule: opts.suppressAlwaysAllowRule === true,
884
889
  vendor: session.vendor || (adapter && adapter.vendor) || "claude",
885
890
  };
886
891
  sendAndRecord(session, permMsg);
@@ -907,6 +912,8 @@ function createSDKBridge(opts) {
907
912
  requestId: requestId,
908
913
  toolName: toolName,
909
914
  toolInput: input,
915
+ defaultToNo: opts.defaultToNo === true,
916
+ suppressAlwaysAllowRule: opts.suppressAlwaysAllowRule === true,
910
917
  });
911
918
  }
912
919
 
@@ -1031,7 +1038,8 @@ function createSDKBridge(opts) {
1031
1038
  && Number(session._sdkQueryGeneration || 0) === codexCatalogCandidate.generation
1032
1039
  && session.cliSessionId === msg.sessionId
1033
1040
  && (!sm.sessions || typeof sm.sessions.get !== "function" || sm.sessions.get(session.localId) === session)) {
1034
- session.codexDynamicToolCatalogVersion = codexCatalogCandidate.version;
1041
+ if (codexCatalogCandidate.version) session.codexDynamicToolCatalogVersion = codexCatalogCandidate.version;
1042
+ if (codexCatalogCandidate.issuesVersion) session.codexIssuesToolCatalogVersion = codexCatalogCandidate.issuesVersion;
1035
1043
  sm.saveSessionFile(session);
1036
1044
  codexCatalogCandidate = null;
1037
1045
  }
@@ -1379,6 +1387,7 @@ function createSDKBridge(opts) {
1379
1387
  clayPort: clayPort,
1380
1388
  clayTls: clayTls,
1381
1389
  clayAuthToken: clayAuthToken,
1390
+ clayBridgeToken: clayBridgeToken,
1382
1391
  slug: slug,
1383
1392
  env: sessionRuntimeEnv,
1384
1393
  });
@@ -1393,6 +1402,7 @@ function createSDKBridge(opts) {
1393
1402
  clayPort: clayPort,
1394
1403
  clayTls: clayTls,
1395
1404
  clayAuthToken: clayAuthToken,
1405
+ clayBridgeToken: clayBridgeToken,
1396
1406
  slug: slug,
1397
1407
  env: sessionRuntimeEnv,
1398
1408
  });
@@ -1601,7 +1611,11 @@ function createSDKBridge(opts) {
1601
1611
  if (linuxUser) {
1602
1612
  claudeOpts.linuxUser = linuxUser;
1603
1613
  claudeOpts.singleTurn = !!session.singleTurn;
1604
- claudeOpts.originalHome = require("./config").REAL_HOME || null;
1614
+ try {
1615
+ claudeOpts.originalHome = require("./os-users").resolveOsUserInfo(linuxUser).home;
1616
+ } catch (e) {
1617
+ claudeOpts.originalHome = null;
1618
+ }
1605
1619
  claudeOpts.projectPath = session.cwd || null;
1606
1620
  claudeOpts._perfT0 = t0;
1607
1621
  // Pass previous worker state for reuse
@@ -1643,6 +1657,17 @@ function createSDKBridge(opts) {
1643
1657
  // delivered by this provider.
1644
1658
  var sessionToolDefs = getSessionToolDefs(session) || [];
1645
1659
  var queryGeneration = Number(session._sdkQueryGeneration || 0);
1660
+ var hasIssueCatalog = false;
1661
+ for (var issueToolIndex = 0; issueToolIndex < sessionToolDefs.length; issueToolIndex++) {
1662
+ if (sessionToolDefs[issueToolIndex].name === "create_issue") {
1663
+ hasIssueCatalog = true;
1664
+ break;
1665
+ }
1666
+ }
1667
+ var recoverCodexIssueTools = sessionAdapter.vendor === "codex"
1668
+ && !!session.cliSessionId
1669
+ && hasIssueCatalog
1670
+ && Number(session.codexIssuesToolCatalogVersion || 0) < 1;
1646
1671
  function queryContextAvailable() {
1647
1672
  if (Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
1648
1673
  if (sm && sm.sessions && typeof sm.sessions.get === "function" && sm.sessions.get(session.localId) !== session) return false;
@@ -1767,10 +1792,22 @@ function createSDKBridge(opts) {
1767
1792
  initialTitleProvisional = initialTitleChoice.provisional;
1768
1793
  }
1769
1794
 
1795
+ var modelSwitchGeneration = session._queryGeneration;
1770
1796
  var queryOpts = {
1797
+ onModelSwitch: function(change) {
1798
+ if (session._queryGeneration !== modelSwitchGeneration || session.taskStopRequested) return;
1799
+ if (!change || !change.fromModel || !change.toModel || change.fromModel === change.toModel) return;
1800
+ sendAndRecord(session, { type: "informational", level: "info", content: "Model changed from " + change.fromModel + " to " + change.toModel + " (" + change.source + ")." });
1801
+ },
1771
1802
  cwd: cwd,
1803
+ slug: slug,
1804
+ clayPort: clayPort,
1805
+ clayTls: clayTls,
1806
+ clayAuthToken: clayAuthToken,
1807
+ clayBridgeToken: clayBridgeToken,
1772
1808
  linuxUser: linuxUser || undefined,
1773
1809
  env: sessionRuntimeEnv,
1810
+ skillOptions: getSessionSkillOptions(session),
1774
1811
  // appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
1775
1812
  // REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
1776
1813
  // would strip the driver session down to the two-sentence pair preset.
@@ -1792,6 +1829,9 @@ function createSDKBridge(opts) {
1792
1829
  toolServers: mergedMcpServers,
1793
1830
  toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
1794
1831
  dynamicTools: sessionToolDefs,
1832
+ // Codex persists dynamic tools with a thread. A stale authorized Driver
1833
+ // gets this exact-query bridge; its handlers still reauthorize live.
1834
+ sessionMcpServer: recoverCodexIssueTools ? getCodexSessionMcpServer(session, queryGeneration) : undefined,
1795
1835
  callDynamicTool: function (toolName, args) {
1796
1836
  var toolDef = findSessionToolDef(toolName);
1797
1837
  if (toolDef && typeof toolDef.handler === "function") {
@@ -1862,10 +1902,12 @@ function createSDKBridge(opts) {
1862
1902
  // project threshold); seeded titles remain fixed.
1863
1903
  if (initialTitle && !session.title) {
1864
1904
  session.title = initialTitle;
1905
+ session.titleProvisional = initialTitleProvisional;
1865
1906
  session.titleAutoGenerated = !initialTitleProvisional;
1866
1907
  sm.saveSessionFile(session);
1867
1908
  sm.broadcastSessionList();
1868
1909
  } else if (initialTitle && session.title === initialTitle) {
1910
+ session.titleProvisional = initialTitleProvisional;
1869
1911
  session.titleAutoGenerated = !initialTitleProvisional;
1870
1912
  sm.saveSessionFile(session);
1871
1913
  }
@@ -1976,6 +2018,12 @@ function createSDKBridge(opts) {
1976
2018
  if (completeInterviewCatalog || requiredCatalogTools.report_scheduled_task_outcome) {
1977
2019
  codexCatalogCandidate = { generation: queryGeneration, version: yoke.sessionTools.CODEX_CATALOG_VERSION };
1978
2020
  }
2021
+ if (hasIssueCatalog) {
2022
+ codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, {
2023
+ generation: queryGeneration,
2024
+ issuesVersion: 1,
2025
+ });
2026
+ }
1979
2027
  }
1980
2028
  session.streamPromise = processQueryStream(session, codexCatalogCandidate).catch(function(err) {
1981
2029
  });
@@ -2216,24 +2264,12 @@ function createSDKBridge(opts) {
2216
2264
  clayTls: clayTls,
2217
2265
  clayAuthToken: clayAuthToken,
2218
2266
  slug: slug,
2267
+ clayBridgeToken: clayBridgeToken,
2219
2268
  });
2220
2269
 
2221
- var fsSkills = discoverSkillDirs();
2222
- sm.skillNames = mergeSkills(result.skills, fsSkills);
2223
- if (result.slashCommands) {
2224
- var seen = new Set();
2225
- var combined = [];
2226
- var all = result.slashCommands.concat(Array.from(sm.skillNames));
2227
- for (var k = 0; k < all.length; k++) {
2228
- if (!seen.has(all[k])) {
2229
- seen.add(all[k]);
2230
- combined.push(all[k]);
2231
- }
2232
- }
2233
- sm.slashCommands = combined;
2234
- sm.setSlashCommandsForVendor(defaultVendor, combined);
2235
- send({ type: "slash_commands", commands: combined, vendor: defaultVendor });
2236
- }
2270
+ // Skill and slash-command catalogs are session-owned. The warmup has
2271
+ // no authenticated session target, so never publish its personal
2272
+ // inventory through the project-wide session manager or broadcast.
2237
2273
  if (result.defaultModel) {
2238
2274
  sm.defaultModelByVendor = sm.defaultModelByVendor || {};
2239
2275
  sm.defaultModelByVendor[defaultVendor] = sm.defaultModelByVendor[defaultVendor] || result.defaultModel;
@@ -1,4 +1,6 @@
1
1
  var usersModule = require("./users");
2
+ var turnQueue = require("./sdk-turn-queue");
3
+ var contextUsage = require("./context-usage");
2
4
  var yoke = require("./yoke");
3
5
  var backgroundTaskTiming = require("./background-task-timing");
4
6
  var sessionTitlePolicy = require("./session-title-policy");
@@ -26,6 +28,7 @@ function attachMessageProcessor(ctx) {
26
28
  var mergeSkills = ctx.mergeSkills;
27
29
  var saveImageFile = ctx.saveImageFile;
28
30
  var getSessionLinuxUser = ctx.getSessionLinuxUser || function () { return null; };
31
+ var getSessionSkillOptions = ctx.getSessionSkillOptions || function () { return {}; };
29
32
 
30
33
  function getMateIdForNotification() {
31
34
  if (!isMate) return null;
@@ -206,21 +209,26 @@ function attachMessageProcessor(ctx) {
206
209
  session.activeBackgroundTasks = [];
207
210
  sendToSession(session, { type: "active_background_tasks", tasks: [] });
208
211
  if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
209
- var fsSkills = discoverSkillDirs();
210
- sm.skillNames = mergeSkills(parsed.skills, fsSkills);
212
+ var fsSkills = discoverSkillDirs(getSessionSkillOptions(session));
213
+ session.skillNames = Array.from(mergeSkills(parsed.skills, fsSkills));
211
214
  if (parsed.slashCommands) {
212
215
  // Union: SDK slash_commands + merged skills (deduplicated)
216
+ session.vendorSlashCommands = parsed.slashCommands.filter(function (name) {
217
+ return session.skillNames.indexOf(name) === -1;
218
+ });
213
219
  var seen = new Set();
214
220
  var combined = [];
215
- var all = parsed.slashCommands.concat(Array.from(sm.skillNames));
221
+ var all = session.vendorSlashCommands.concat(session.skillNames);
216
222
  for (var k = 0; k < all.length; k++) {
217
223
  if (!seen.has(all[k])) {
218
224
  seen.add(all[k]);
219
225
  combined.push(all[k]);
220
226
  }
221
227
  }
222
- sm.slashCommands = combined;
223
- send({ type: "slash_commands", commands: sm.slashCommands });
228
+ session.slashCommands = combined;
229
+ session.slashCommandsByVendor = session.slashCommandsByVendor || {};
230
+ session.slashCommandsByVendor[session.vendor || (adapter && adapter.vendor) || "claude"] = combined;
231
+ sendToSession(session, { type: "slash_commands", commands: combined, vendor: session.vendor || (adapter && adapter.vendor) || "claude" });
224
232
  }
225
233
  if (parsed.model) {
226
234
  var initVendor = session.vendor || (adapter && adapter.vendor) || "claude";
@@ -563,6 +571,7 @@ function attachMessageProcessor(ctx) {
563
571
  }
564
572
 
565
573
  session._lastAdapterError = null;
574
+ session._queuedTurnCount = turnQueue.pendingSends(session, parsed);
566
575
  var hasQueuedTurn = (session._queuedTurnCount || 0) > 0;
567
576
  if (hasQueuedTurn) {
568
577
  session._queuedTurnCount--;
@@ -580,7 +589,13 @@ function attachMessageProcessor(ctx) {
580
589
  && /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
581
590
  // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
582
591
  if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
583
- session.queryInstance.getContextUsage().then(function(ctxUsage) {
592
+ var contextQuery = session.queryInstance;
593
+ var contextTurn = session.turnCount;
594
+ contextUsage.readUsage(session, "summary").then(function(ctxUsage) {
595
+ if (session.queryInstance !== contextQuery || session.turnCount !== contextTurn) return;
596
+ // null means the shared reader suppressed a superseded response;
597
+ // only an adapter's explicit unavailable object may clear the UI.
598
+ if (!ctxUsage) return;
584
599
  session.lastContextUsage = ctxUsage;
585
600
  sendToSession(session, { type: "context_usage", data: ctxUsage });
586
601
  }).catch(function(e) {
@@ -597,6 +612,11 @@ function attachMessageProcessor(ctx) {
597
612
  modelUsage: parsed.modelUsage || null,
598
613
  sessionId: parsed.sessionId,
599
614
  lastStreamInputTokens: lastStreamInput,
615
+ userMessageId: parsed.userMessageId,
616
+ userMessageIds: parsed.userMessageIds,
617
+ resultIndex: parsed.resultIndex,
618
+ resumeReason: parsed.resumeReason,
619
+ localCommand: parsed.localCommand,
600
620
  });
601
621
  if (parsed.fastModeState) {
602
622
  sendAndRecord(session, { type: "fast_mode_state", state: parsed.fastModeState });
@@ -647,11 +667,13 @@ function attachMessageProcessor(ctx) {
647
667
  session.streamedText = false;
648
668
  sm.broadcastSessionList();
649
669
 
650
- // Auto-generate title after N turns (skip if loop or already auto-generated)
651
- if (session.turnCount === sessionTitlePolicy.generationTurn(isMate)
670
+ // Auto-generate after the first completed substantive turn. The title
671
+ // coordinator handles retries while generation is pending or failed.
672
+ if (session.turnCount >= sessionTitlePolicy.generationTurn(isMate)
652
673
  && !session.titleAutoGenerated
653
674
  && !session.titleManuallySet
654
675
  && !session.loop
676
+ && !(session.sessionProvenance && session.sessionProvenance.kind === "worker")
655
677
  && onAutoTitle) {
656
678
  try { onAutoTitle(session); } catch (e) {
657
679
  console.error("[auto-title] onAutoTitle threw:", e.message || e);
@@ -769,10 +791,12 @@ function attachMessageProcessor(ctx) {
769
791
  var info = parsed.rateLimitInfo;
770
792
  console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
771
793
 
772
- // Broadcast reset time for top-bar usage link
794
+ // Persist the scoped reset snapshot so reconnect/history restore can reproject it.
773
795
  if (info.rateLimitType && info.resetsAt) {
774
- send({
796
+ sendAndRecord(session, {
775
797
  type: "rate_limit_usage",
798
+ sessionId: session.localId,
799
+ vendor: session.vendor || "claude",
776
800
  rateLimitType: info.rateLimitType,
777
801
  resetsAt: info.resetsAt * 1000,
778
802
  status: info.status,
@@ -783,6 +807,8 @@ function attachMessageProcessor(ctx) {
783
807
  if (info.status === "allowed_warning" || info.status === "rejected") {
784
808
  sendAndRecord(session, {
785
809
  type: "rate_limit",
810
+ sessionId: session.localId,
811
+ vendor: session.vendor || "claude",
786
812
  status: info.status,
787
813
  resetsAt: info.resetsAt ? info.resetsAt * 1000 : null,
788
814
  rateLimitType: info.rateLimitType || null,
@@ -879,15 +905,20 @@ function attachMessageProcessor(ctx) {
879
905
  // init) and push a full replacement to the client.
880
906
  var cmdSeen = new Set();
881
907
  var cmdCombined = [];
882
- var cmdAll = (parsed.commandNames || []).concat(Array.from(sm.skillNames || []));
908
+ session.vendorSlashCommands = (parsed.commandNames || []).filter(function (name) {
909
+ return (session.skillNames || []).indexOf(name) === -1;
910
+ });
911
+ var cmdAll = session.vendorSlashCommands.concat(session.skillNames || []);
883
912
  for (var ci = 0; ci < cmdAll.length; ci++) {
884
913
  if (!cmdSeen.has(cmdAll[ci])) {
885
914
  cmdSeen.add(cmdAll[ci]);
886
915
  cmdCombined.push(cmdAll[ci]);
887
916
  }
888
917
  }
889
- sm.slashCommands = cmdCombined;
890
- send({ type: "slash_commands", commands: sm.slashCommands });
918
+ session.slashCommands = cmdCombined;
919
+ session.slashCommandsByVendor = session.slashCommandsByVendor || {};
920
+ session.slashCommandsByVendor[session.vendor || (adapter && adapter.vendor) || "claude"] = cmdCombined;
921
+ sendToSession(session, { type: "slash_commands", commands: cmdCombined, vendor: session.vendor || (adapter && adapter.vendor) || "claude" });
891
922
 
892
923
  } else if (parsed.yokeType === "worker_shutting_down") {
893
924
  // Surface non-routine teardown reasons; "host_exit" is the normal close.
@@ -1,5 +1,4 @@
1
- var fs = require("fs");
2
- var path = require("path");
1
+ var skillDiscovery = require("./yoke/skill-discovery");
3
2
 
4
3
  // Split shell command on operators (&&, ||, ;, |) while respecting quotes
5
4
  // and parentheses. Returns array of command segments.
@@ -79,34 +78,13 @@ function splitShellSegments(cmd) {
79
78
 
80
79
  function attachSkillDiscovery(ctx) {
81
80
  var cwd = ctx.cwd;
81
+ var getOptions = ctx.getOptions || function () { return {}; };
82
82
 
83
- function discoverSkillDirs() {
83
+ function discoverSkillDirs(options) {
84
+ var discovered = skillDiscovery.discoverSkills(cwd, options || getOptions());
84
85
  var skills = {};
85
- var dirs = [
86
- path.join(require("./config").REAL_HOME, ".claude", "skills"),
87
- path.join(cwd, ".claude", "skills"),
88
- ];
89
- for (var d = 0; d < dirs.length; d++) {
90
- var base = dirs[d];
91
- var entries;
92
- try {
93
- entries = fs.readdirSync(base, { withFileTypes: true });
94
- } catch (e) {
95
- continue; // directory doesn't exist
96
- }
97
- for (var i = 0; i < entries.length; i++) {
98
- var entry = entries[i];
99
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
100
- var skillDir = path.join(base, entry.name);
101
- var skillMd = path.join(skillDir, "SKILL.md");
102
- try {
103
- fs.accessSync(skillMd, fs.constants.R_OK);
104
- // project skills override global skills with same name
105
- skills[entry.name] = skillDir;
106
- } catch (e) {
107
- // no SKILL.md, skip
108
- }
109
- }
86
+ for (var i = 0; i < discovered.length; i++) {
87
+ skills[discovered[i].name] = discovered[i].path.replace(/\/SKILL\.md$/, "");
110
88
  }
111
89
  return skills;
112
90
  }
@@ -0,0 +1,14 @@
1
+ // Results report queued sends, which can merge into fewer model turns.
2
+ function pendingSends(session, event) {
3
+ if (Number.isInteger(event.queuedTurnCount) && event.queuedTurnCount >= 0) {
4
+ var query = session.queryInstance;
5
+ var current = query && typeof query.getSubmittedMessageCount === "function" ? query.getSubmittedMessageCount() : event.submittedMessageCount;
6
+ var later = Number.isInteger(current) && Number.isInteger(event.submittedMessageCount)
7
+ ? Math.max(0, current - event.submittedMessageCount) : 0;
8
+ return event.queuedTurnCount + later;
9
+ }
10
+ var answered = Number.isInteger(event.answeredUserMessageCount) && event.answeredUserMessageCount > 0 ? event.answeredUserMessageCount : 1;
11
+ return Math.max(0, (session._queuedTurnCount || 0) - answered + 1);
12
+ }
13
+
14
+ module.exports = { pendingSends: pendingSends };