clay-server 4.0.0-beta.3 → 4.0.0-beta.5

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.
@@ -1,4 +1,6 @@
1
- // Background task start-time tracking.
1
+ // Background task list shaping shared by the server-side consumers:
2
+ // start-time tracking, plus the ambient/user split every activity indicator
3
+ // needs.
2
4
  //
3
5
  // Vendors report background tasks as a full replacement list on every change
4
6
  // and none of them carries a reliable start time today (see
@@ -66,7 +68,38 @@ function mergeStartTimes(previousTasks, nextTasks, now) {
66
68
  return merged;
67
69
  }
68
70
 
71
+ // Ambient tasks are the CLI's own housekeeping (every skip_transcript task
72
+ // plus auto-started live-update watchers). The SDK asks hosts to keep them out
73
+ // of activity indicators, so every count that drives a badge, a summary, or a
74
+ // change notification must go through here rather than reading `.length`.
75
+ // Absent means false: vendors without an ambient concept (Codex terminals)
76
+ // and older CLI builds report only real user work.
77
+ function isAmbientTask(task) {
78
+ return !!(task && task.ambient === true);
79
+ }
80
+
81
+ function userTasks(tasks) {
82
+ if (!Array.isArray(tasks)) return [];
83
+ return tasks.filter(function (task) {
84
+ return task && !isAmbientTask(task);
85
+ });
86
+ }
87
+
88
+ function ambientTasks(tasks) {
89
+ if (!Array.isArray(tasks)) return [];
90
+ return tasks.filter(isAmbientTask);
91
+ }
92
+
93
+ // The count that represents "user-visible background work is running".
94
+ function countUserTasks(tasks) {
95
+ return userTasks(tasks).length;
96
+ }
97
+
69
98
  module.exports = {
70
99
  mergeStartTimes: mergeStartTimes,
71
100
  normalizeTimestamp: normalizeTimestamp,
101
+ isAmbientTask: isAmbientTask,
102
+ userTasks: userTasks,
103
+ ambientTasks: ambientTasks,
104
+ countUserTasks: countUserTasks,
72
105
  };
@@ -742,6 +742,12 @@
742
742
  .background-task-icon { display: inline-flex; flex: none; color: var(--text-muted); }
743
743
  .background-task-icon i { width: 13px; height: 13px; }
744
744
  .background-task-description { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
745
+ /* Housekeeping group: visible so the user can see what Clay runs on its own,
746
+ but subordinate to the work they actually asked for. */
747
+ .background-tasks-section-label { padding: 9px 0 1px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 10px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; }
748
+ .background-tasks-section-label + .background-task-row { border-top: 0; }
749
+ .background-task-row.ambient .background-task-description { color: var(--text-muted); }
750
+ .background-task-row.ambient .background-task-icon { opacity: .7; }
745
751
  .background-task-meta { display: inline-flex; align-items: center; gap: 7px; flex: none; color: var(--text-muted); }
746
752
  .background-task-type { font-size: 11px; }
747
753
  .background-task-elapsed { font-variant-numeric: tabular-nums; font-size: 11px; min-width: 44px; text-align: right; }
@@ -501,12 +501,14 @@
501
501
  <div class="usage-section-label">Session Tokens</div>
502
502
  <div class="usage-row"><span class="usage-label">Input</span><span id="context-input" class="usage-value">0</span></div>
503
503
  <div class="usage-row"><span class="usage-label">Output</span><span id="context-output" class="usage-value">0</span></div>
504
+ <div class="usage-row hidden" id="context-thinking-row" title="Extended thinking tokens, already counted inside Output"><span class="usage-label">Thinking</span><span id="context-thinking" class="usage-value">0</span></div>
504
505
  <div class="usage-row"><span class="usage-label">Cache read</span><span id="context-cache-read" class="usage-value">0</span></div>
505
506
  <div class="usage-row"><span class="usage-label">Cache write</span><span id="context-cache-write" class="usage-value">0</span></div>
506
507
  <div class="usage-divider"></div>
507
508
  <div class="usage-section-label">Model</div>
508
509
  <div class="usage-row"><span class="usage-label">Name</span><span id="context-model" class="usage-value">-</span></div>
509
510
  <div class="usage-row"><span class="usage-label">Cost</span><span id="context-cost" class="usage-value">$0.0000</span></div>
511
+ <div class="usage-row hidden" id="context-cost-basis-row" title="Which price table this cost was computed from"><span class="usage-label">Priced at</span><span id="context-cost-basis" class="usage-value">-</span></div>
510
512
  <div class="usage-row"><span class="usage-label">Turns</span><span id="context-turns" class="usage-value">0</span></div>
511
513
  </div>
512
514
  </div>
@@ -10,7 +10,7 @@ import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelP
10
10
 
11
11
  // --- Module-owned state (not in store) ---
12
12
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
13
- var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
13
+ var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
14
14
  var ctxPopoverEl = null;
15
15
  var ctxHoverTimer = null;
16
16
  var statusRefreshTimer = null;
@@ -72,6 +72,10 @@ var contextCacheReadEl = null;
72
72
  var contextCacheWriteEl = null;
73
73
  var contextModelEl = null;
74
74
  var contextCostEl = null;
75
+ var contextThinkingEl = null;
76
+ var contextThinkingRow = null;
77
+ var contextCostBasisEl = null;
78
+ var contextCostBasisRow = null;
75
79
  var contextTurnsEl = null;
76
80
  var contextMini = null;
77
81
  var contextMiniFill = null;
@@ -449,6 +453,10 @@ export function initPanels() {
449
453
  contextCacheWriteEl = $("context-cache-write");
450
454
  contextModelEl = $("context-model");
451
455
  contextCostEl = $("context-cost");
456
+ contextThinkingEl = $("context-thinking");
457
+ contextThinkingRow = $("context-thinking-row");
458
+ contextCostBasisEl = $("context-cost-basis");
459
+ contextCostBasisRow = $("context-cost-basis-row");
452
460
  contextTurnsEl = $("context-turns");
453
461
  contextMini = $("context-mini");
454
462
  contextMiniFill = $("context-mini-fill");
@@ -776,6 +784,16 @@ export function updateContextPanel() {
776
784
  contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
777
785
  contextModelEl.textContent = contextData.model;
778
786
  contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
787
+ // Optional SDK fields: shown only once the CLI has actually reported them.
788
+ if (contextThinkingRow && contextThinkingEl) {
789
+ var hasThinking = typeof contextData.thinking === "number";
790
+ contextThinkingRow.classList.toggle("hidden", !hasThinking);
791
+ if (hasThinking) contextThinkingEl.textContent = formatTokens(contextData.thinking);
792
+ }
793
+ if (contextCostBasisRow && contextCostBasisEl) {
794
+ contextCostBasisRow.classList.toggle("hidden", !contextData.costBasis);
795
+ if (contextData.costBasis) contextCostBasisEl.textContent = contextData.costBasis;
796
+ }
779
797
  contextTurnsEl.textContent = String(contextData.turns);
780
798
  }
781
799
 
@@ -811,6 +829,29 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
811
829
  contextData.model = displayModel;
812
830
  contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
813
831
  if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
832
+
833
+ // thinkingTokens and costBasis are per-model and optional: absent on
834
+ // older CLI builds and, for a resumed session, on the turns that ran
835
+ // before the field existed. Leave them null in that case so the rows
836
+ // stay hidden rather than claiming a confident zero.
837
+ var thinkingTotal = null;
838
+ var basis = null;
839
+ var basisMixed = false;
840
+ for (var mi = 0; mi < models.length; mi++) {
841
+ var entry = modelUsage[models[mi]];
842
+ if (!entry) continue;
843
+ if (typeof entry.thinkingTokens === "number") {
844
+ thinkingTotal = (thinkingTotal || 0) + entry.thinkingTokens;
845
+ }
846
+ if (entry.costBasis) {
847
+ if (basis === null) basis = entry.costBasis;
848
+ else if (basis !== entry.costBasis) basisMixed = true;
849
+ }
850
+ }
851
+ contextData.thinking = thinkingTotal;
852
+ // Several models in one query can be priced differently; say so rather
853
+ // than reporting whichever happened to come first.
854
+ contextData.costBasis = basisMixed ? "mixed" : basis;
814
855
  }
815
856
  }
816
857
  if (!store.get('replayingHistory')) updateContextPanel();
@@ -832,7 +873,7 @@ export function applyContextView(view) {
832
873
  }
833
874
 
834
875
  export function resetContextData() {
835
- contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
876
+ contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
836
877
  store.set({ richContextUsage: null });
837
878
  // hideCtxPopover + updateContextPanel handled by store subscriber
838
879
  }
@@ -76,13 +76,33 @@ function activityDotsHtml() {
76
76
  return '<span class="background-tasks-activity" aria-hidden="true"><i></i><i></i><i></i></span>';
77
77
  }
78
78
 
79
+ // Ambient tasks are the CLI's own housekeeping. The SDK asks hosts to keep
80
+ // them out of activity indicators; Clay still lists them so the user can see
81
+ // what runs on their behalf. Absent means false (Codex has no ambient concept,
82
+ // and older CLI builds report only user work).
83
+ export function isAmbientTask(task) {
84
+ return !!(task && task.ambient === true);
85
+ }
86
+
87
+ export function splitTasks(tasks) {
88
+ var user = [];
89
+ var ambient = [];
90
+ var list = Array.isArray(tasks) ? tasks : [];
91
+ for (var i = 0; i < list.length; i++) {
92
+ if (!list[i]) continue;
93
+ if (isAmbientTask(list[i])) ambient.push(list[i]);
94
+ else user.push(list[i]);
95
+ }
96
+ return { user: user, ambient: ambient };
97
+ }
98
+
79
99
  function taskRowHtml(task, now) {
80
100
  var taskType = task.task_type || 'other';
81
101
  var icon = TASK_TYPE_ICONS[taskType] || TASK_TYPE_ICONS.other;
82
102
  var description = task.description || 'Background task';
83
103
  var startedAt = typeof task.started_at === 'number' ? task.started_at : 0;
84
104
  var elapsed = formatElapsed(startedAt, now);
85
- return '<div class="background-task-row">' +
105
+ return '<div class="background-task-row' + (isAmbientTask(task) ? ' ambient' : '') + '">' +
86
106
  '<span class="background-task-icon" title="' + escapeHtml(taskType) + '">' + iconHtml(icon) + '</span>' +
87
107
  '<span class="background-task-description" title="' + escapeHtml(description) + '">' +
88
108
  escapeHtml(description) + '</span>' +
@@ -97,8 +117,11 @@ function taskRowHtml(task, now) {
97
117
 
98
118
  function renderBackgroundTasks() {
99
119
  var tasks = store.get('activeBackgroundTasks') || [];
120
+ var split = splitTasks(tasks);
100
121
  var existing = document.getElementById('background-tasks-bar');
101
- if (tasks.length === 0) {
122
+ // Ambient-only means nothing the user asked for is running, so the composer
123
+ // stays quiet: no bar, no dots, no count.
124
+ if (split.user.length === 0) {
102
125
  if (existing) existing.remove();
103
126
  expanded = false;
104
127
  stopElapsedTimer();
@@ -111,8 +134,8 @@ function renderBackgroundTasks() {
111
134
  bar.id = 'background-tasks-bar';
112
135
  bar.className = 'background-tasks-bar' + (expanded ? ' expanded' : '');
113
136
 
114
- var taskLabel = tasks.length === 1 ? 'background task' : 'background tasks';
115
- var summary = tasks.length + ' ' + taskLabel;
137
+ var taskLabel = split.user.length === 1 ? 'background task' : 'background tasks';
138
+ var summary = split.user.length + ' ' + taskLabel;
116
139
  var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '"' +
117
140
  ' aria-controls="background-tasks-list" aria-label="' + summary + ', ' +
118
141
  (expanded ? 'collapse' : 'expand') + ' details">' +
@@ -124,7 +147,13 @@ function renderBackgroundTasks() {
124
147
  if (expanded) {
125
148
  var now = Date.now();
126
149
  html += '<div class="background-tasks-list" id="background-tasks-list">';
127
- for (var i = 0; i < tasks.length; i++) html += taskRowHtml(tasks[i], now);
150
+ for (var i = 0; i < split.user.length; i++) html += taskRowHtml(split.user[i], now);
151
+ // Housekeeping is shown, not hidden: the user can see what Clay runs on
152
+ // its own and stop a runaway one. Kept visually subordinate to real work.
153
+ if (split.ambient.length > 0) {
154
+ html += '<div class="background-tasks-section-label">Housekeeping</div>';
155
+ for (var a = 0; a < split.ambient.length; a++) html += taskRowHtml(split.ambient[a], now);
156
+ }
128
157
  html += '</div>';
129
158
  }
130
159
  bar.innerHTML = html;
@@ -193,7 +193,9 @@ function attachMessageProcessor(ctx) {
193
193
 
194
194
  // Cache slash_commands and model from CLI init message
195
195
  if (parsed.yokeType === "init") {
196
- var previousBackgroundTaskCount = (session.activeBackgroundTasks || []).length;
196
+ // Ambient housekeeping never drove the sidebar badge, so a reset that
197
+ // only clears ambient watchers must not rebroadcast the session list.
198
+ var previousBackgroundTaskCount = backgroundTaskTiming.countUserTasks(session.activeBackgroundTasks);
197
199
  session.activeBackgroundTasks = [];
198
200
  sendToSession(session, { type: "active_background_tasks", tasks: [] });
199
201
  if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
@@ -688,7 +690,9 @@ function attachMessageProcessor(ctx) {
688
690
 
689
691
  } else if (parsed.yokeType === "background_tasks_changed") {
690
692
  var previousBackgroundTasks = session.activeBackgroundTasks || [];
691
- var previousBackgroundTaskCount = previousBackgroundTasks.length;
693
+ // Count user work only: an ambient watcher starting or stopping must not
694
+ // rebroadcast the session list and flicker every client's sidebar badge.
695
+ var previousBackgroundTaskCount = backgroundTaskTiming.countUserTasks(previousBackgroundTasks);
692
696
  // Vendors re-send the whole list on every change, so carry each task's
693
697
  // first-seen time forward; otherwise the composer's elapsed timer would
694
698
  // restart whenever any other task starts or finishes.
@@ -698,7 +702,7 @@ function attachMessageProcessor(ctx) {
698
702
  );
699
703
  session.activeBackgroundTasks = activeBackgroundTasks;
700
704
  sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
701
- if (previousBackgroundTaskCount !== activeBackgroundTasks.length) {
705
+ if (previousBackgroundTaskCount !== backgroundTaskTiming.countUserTasks(activeBackgroundTasks)) {
702
706
  sm.broadcastSessionList();
703
707
  }
704
708
 
package/lib/sessions.js CHANGED
@@ -5,6 +5,7 @@ var utils = require("./utils");
5
5
  var users = require("./users");
6
6
  var { CODEX_DEFAULTS } = require("./codex-defaults");
7
7
  var hygiene = require("./session-hygiene");
8
+ var backgroundTaskTiming = require("./background-task-timing");
8
9
 
9
10
  function createSessionManager(opts) {
10
11
  var cwd = opts.cwd;
@@ -508,7 +509,9 @@ function createSessionManager(opts) {
508
509
  title: s.title || "New Session",
509
510
  active: isActive,
510
511
  isProcessing: s.isProcessing,
511
- backgroundTaskCount: (s.activeBackgroundTasks || []).length,
512
+ // Sidebar badge: user work only. Ambient housekeeping is surfaced in the
513
+ // composer's expanded list, never as a count demanding attention.
514
+ backgroundTaskCount: backgroundTaskTiming.countUserTasks(s.activeBackgroundTasks),
512
515
  lastActivity: s.lastActivity || s.createdAt || 0,
513
516
  loop: loop,
514
517
  spawn: s.spawn || null,
@@ -20,6 +20,49 @@ var claudeUserInput = require("../claude-user-input");
20
20
  // emitted. Keep in sync with the client's generic user-dialog renderer.
21
21
  var SUPPORTED_DIALOG_KINDS = ["refusal_fallback_prompt"];
22
22
 
23
+ // Clay renders a per-task Stop control in the composer's background-tasks bar
24
+ // (public/modules/background-tasks-ui.js), wired through project-sessions.js
25
+ // `stop_task` -> adapter.stopTask -> rawQuery.stopTask (worker path: the
26
+ // stop_task message in claude-worker.js). Declaring that affordance makes an
27
+ // interrupt spare running background tasks so Stop only aborts the current
28
+ // turn. The CLI fails CLOSED on absence: without this, every interrupt would
29
+ // also kill background tasks, since a consumer with no per-task control could
30
+ // not otherwise stop a runaway one.
31
+ //
32
+ // Declared identically on the in-process and OS-user worker paths. The CLI
33
+ // takes first-attached-client-wins on multi-client sessions, so the two paths
34
+ // must not disagree, or which one connected first would decide whether Stop
35
+ // kills background work.
36
+ var PER_TASK_STOP_AFFORDANCE = true;
37
+
38
+ // --- System prompt recording (SDK `snapshot`) ---
39
+ // Records the conversation's system prompt once and replays it verbatim on
40
+ // every later request and resume, which keeps the prompt-cache prefix and
41
+ // extended-thinking context stable across CLI upgrades and flag changes.
42
+ //
43
+ // OFF for Clay's persistent sessions, deliberately. getSessionSystemPrompt
44
+ // (project.js) recomposes the append from live state every time the query is
45
+ // rebuilt - sticky notes, pair/Driver role, and the Mate Capsule catalog - and
46
+ // three call sites exist purely to force that re-render mid-conversation
47
+ // (project-capsule-catalog.js refreshSessions, project.js pair regrouping,
48
+ // sdk-bridge refreshEnvironmentRuntime). Because Clay resumes those
49
+ // conversations, a recorded prompt would pin the first render and silently
50
+ // swallow every later update until compaction.
51
+ //
52
+ // Callers whose prompt is fixed for the life of the conversation opt in with
53
+ // queryOpts.systemPromptSnapshot. A bare string prompt cannot be recorded, so
54
+ // it is promoted to the custom form the SDK requires.
55
+ function applySystemPromptSnapshot(systemPrompt, enabled) {
56
+ if (!enabled || !systemPrompt) return systemPrompt;
57
+ if (typeof systemPrompt === "string" || Array.isArray(systemPrompt)) {
58
+ return { type: "custom", prompt: systemPrompt, snapshot: true };
59
+ }
60
+ if (typeof systemPrompt === "object") {
61
+ return Object.assign({}, systemPrompt, { snapshot: true });
62
+ }
63
+ return systemPrompt;
64
+ }
65
+
23
66
  function getSharedSkillPlugins(homeDir, existingPlugins) {
24
67
  var plugins = Array.isArray(existingPlugins) ? existingPlugins.slice() : [];
25
68
  var bridgePath = skillDiscovery.ensureClaudeCodexPlugin(homeDir ? { homeDir: homeDir } : null);
@@ -45,10 +88,16 @@ function buildWorkerQueryOptions(queryOpts, claudeOpts, workerCwd) {
45
88
  if (claudeOpts.permissionMode) queryOptions.permissionMode = claudeOpts.permissionMode;
46
89
  if (claudeOpts.allowDangerouslySkipPermissions) queryOptions.allowDangerouslySkipPermissions = true;
47
90
  if (claudeOpts.settings) queryOptions.settings = claudeOpts.settings;
48
- if (queryOpts.appendSystemPrompt) queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt };
91
+ if (queryOpts.appendSystemPrompt) {
92
+ queryOptions.systemPrompt = applySystemPromptSnapshot(
93
+ { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt },
94
+ queryOpts.systemPromptSnapshot
95
+ );
96
+ }
49
97
  queryOptions.plugins = queryOpts.skipSkills ? (claudeOpts.plugins || []) : getSharedSkillPlugins(claudeOpts.originalHome, claudeOpts.plugins);
50
98
  if (queryOpts.toolServerDescriptors) queryOptions.mcpServerDescriptors = queryOpts.toolServerDescriptors;
51
99
  if (queryOpts.onUserDialog) queryOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
100
+ queryOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
52
101
  if (queryOpts.model) queryOptions.model = queryOpts.model;
53
102
  if (queryOpts.effort) queryOptions.effort = queryOpts.effort;
54
103
  if (queryOpts.resumeSessionId) queryOptions.resume = queryOpts.resumeSessionId;
@@ -404,6 +453,11 @@ function normalizeBackgroundTasks(tasks) {
404
453
  task_id: task && task.task_id,
405
454
  task_type: taskType,
406
455
  description: (task && task.description) || "",
456
+ // Housekeeping the CLI does not surface as user work (skip_transcript
457
+ // tasks, auto-started live-update watchers). Clay keeps these out of its
458
+ // activity indicators and lists them as a subdued secondary group.
459
+ // Normalized to a boolean so consumers never have to test for absence.
460
+ ambient: !!(task && task.ambient),
407
461
  };
408
462
  // The SDK does not report a start time today. Forward one if a future
409
463
  // version adds it; otherwise the session stamps first-seen time.
@@ -1454,12 +1508,17 @@ function createClaudeAdapter(opts) {
1454
1508
  if (_claudeBinaryPath) sdkOptions.pathToClaudeCodeExecutable = _claudeBinaryPath;
1455
1509
 
1456
1510
  // YOKE standard options -> SDK options
1457
- if (queryOpts.systemPrompt) sdkOptions.systemPrompt = queryOpts.systemPrompt;
1511
+ if (queryOpts.systemPrompt) {
1512
+ sdkOptions.systemPrompt = applySystemPromptSnapshot(queryOpts.systemPrompt, queryOpts.systemPromptSnapshot);
1513
+ }
1458
1514
  // Additive variant: keep the full Claude Code system prompt and append.
1459
1515
  // A plain-string systemPrompt REPLACES the whole prompt, so pair
1460
1516
  // presets and other add-ons must come through here instead.
1461
1517
  if (!queryOpts.systemPrompt && queryOpts.appendSystemPrompt) {
1462
- sdkOptions.systemPrompt = { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt };
1518
+ sdkOptions.systemPrompt = applySystemPromptSnapshot(
1519
+ { type: "preset", preset: "claude_code", append: queryOpts.appendSystemPrompt },
1520
+ queryOpts.systemPromptSnapshot
1521
+ );
1463
1522
  }
1464
1523
  if (queryOpts.model) sdkOptions.model = queryOpts.model;
1465
1524
  if (queryOpts.effort) sdkOptions.effort = queryOpts.effort;
@@ -1475,6 +1534,7 @@ function createClaudeAdapter(opts) {
1475
1534
  sdkOptions.onUserDialog = queryOpts.onUserDialog;
1476
1535
  sdkOptions.supportedDialogKinds = SUPPORTED_DIALOG_KINDS;
1477
1536
  }
1537
+ sdkOptions.perTaskStopAffordance = PER_TASK_STOP_AFFORDANCE;
1478
1538
  if (queryOpts.resumeSessionId) sdkOptions.resume = queryOpts.resumeSessionId;
1479
1539
  if (queryOpts.persistSession === false) sdkOptions.persistSession = false;
1480
1540
 
@@ -12,6 +12,23 @@ var { resolveOsUserInfo } = require("../../os-users");
12
12
  var backgroundTasks = require("../codex-background-tasks");
13
13
  var codexUserInput = require("../codex-user-input");
14
14
 
15
+ // --- Reasoning defaults ---
16
+ // Reasoning summaries are the only readable thinking channel Codex offers:
17
+ // raw reasoning content is encrypted for ChatGPT-authenticated models, so the
18
+ // item/reasoning/* events this adapter already maps to thinking only carry
19
+ // text when the app-server is asked for summaries. The CLI default
20
+ // (`model_reasoning_summary = "auto"`) frequently produces none at all, and the
21
+ // app-server otherwise inherits whatever is in the user's ~/.codex/config.toml,
22
+ // so Clay asks for detailed summaries explicitly.
23
+ //
24
+ // `show_raw_agent_reasoning` is deliberately absent: it is encrypted on
25
+ // ChatGPT-auth models and noisy elsewhere, so it stays a user opt-in.
26
+ // Everything here is overridable via adapterOptions.CODEX.config.
27
+ var CODEX_REASONING_DEFAULTS = {
28
+ model_reasoning_summary: "detailed",
29
+ model_supports_reasoning_summaries: true,
30
+ };
31
+
15
32
  // --- Event flattening ---
16
33
  // Converts app-server JSON-RPC notifications into flat objects with a yokeType field.
17
34
  //
@@ -1567,12 +1584,16 @@ function createCodexAdapter(opts) {
1567
1584
  serverOpts.osUserInfo = _resolveOsUserInfo(_runtimeLinuxUser);
1568
1585
  }
1569
1586
 
1587
+ // Clay's config defaults go in first so any user-supplied
1588
+ // adapterOptions.CODEX.config key of the same name overrides them.
1589
+ serverOpts.config = Object.assign({}, CODEX_REASONING_DEFAULTS);
1590
+
1570
1591
  // Extract adapter options
1571
1592
  if (effectiveInitOpts && effectiveInitOpts.adapterOptions && effectiveInitOpts.adapterOptions.CODEX) {
1572
1593
  var co = effectiveInitOpts.adapterOptions.CODEX;
1573
1594
  if (co.apiKey) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_API_KEY: co.apiKey });
1574
1595
  if (co.baseUrl) serverOpts.env = Object.assign({}, serverOpts.env || {}, { OPENAI_BASE_URL: co.baseUrl });
1575
- if (co.config) serverOpts.config = co.config;
1596
+ if (co.config) serverOpts.config = Object.assign(serverOpts.config, co.config);
1576
1597
  }
1577
1598
 
1578
1599
  // Track 1: Read local MCP server definitions from ~/.clay/mcp.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.0.0-beta.3",
3
+ "version": "4.0.0-beta.5",
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",
@@ -49,7 +49,7 @@
49
49
  "homepage": "https://github.com/chadbyte/clay#readme",
50
50
  "author": "Chad",
51
51
  "dependencies": {
52
- "@anthropic-ai/claude-agent-sdk": "^0.3.241",
52
+ "@anthropic-ai/claude-agent-sdk": "^0.3.258",
53
53
  "@lydell/node-pty": "^1.2.0-beta.3",
54
54
  "@openai/codex": "^0.152.1",
55
55
  "@seald-io/nedb": "^4.1.2",