clay-server 4.0.0-beta.1 → 4.0.0-beta.11

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 (99) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/config.js +25 -1
  3. package/lib/daemon.js +22 -0
  4. package/lib/git-cli.js +94 -1
  5. package/lib/knowledge-import.js +436 -0
  6. package/lib/knowledge-record-store.js +188 -0
  7. package/lib/knowledge-search.js +240 -0
  8. package/lib/mate-knowledge-mcp-server.js +151 -0
  9. package/lib/mate-knowledge-migration.js +283 -0
  10. package/lib/mate-knowledge-service.js +488 -0
  11. package/lib/mate-knowledge-sync.js +482 -0
  12. package/lib/notes-lifecycle.js +149 -0
  13. package/lib/notes.js +60 -3
  14. package/lib/project-connection.js +17 -0
  15. package/lib/project-http.js +15 -1
  16. package/lib/project-knowledge.js +22 -0
  17. package/lib/project-logs-comments.js +137 -0
  18. package/lib/project-logs-mcp-server.js +314 -0
  19. package/lib/project-logs-query.js +167 -0
  20. package/lib/project-logs-root.js +67 -0
  21. package/lib/project-logs-schema.js +259 -0
  22. package/lib/project-logs-service.js +357 -0
  23. package/lib/project-logs-snapshot.js +202 -0
  24. package/lib/project-logs-store.js +498 -0
  25. package/lib/project-logs-versioning.js +128 -0
  26. package/lib/project-logs.js +348 -0
  27. package/lib/project-mate-interaction.js +14 -0
  28. package/lib/project-mate-knowledge.js +110 -0
  29. package/lib/project-memory.js +22 -0
  30. package/lib/project-pair-lifecycle.js +461 -0
  31. package/lib/project-session-notes.js +64 -13
  32. package/lib/project-session-pair.js +124 -111
  33. package/lib/project-sessions.js +46 -6
  34. package/lib/project-user-message.js +41 -4
  35. package/lib/project-vendor-login.js +414 -0
  36. package/lib/project-worker-permission.js +429 -0
  37. package/lib/project-worker-proposal.js +16 -1
  38. package/lib/project.js +164 -4
  39. package/lib/public/app.js +49 -20
  40. package/lib/public/css/git-placard.css +154 -0
  41. package/lib/public/css/input.css +79 -25
  42. package/lib/public/css/notifications-center.css +67 -0
  43. package/lib/public/css/pane.css +52 -0
  44. package/lib/public/css/project-logs.css +234 -0
  45. package/lib/public/css/scheduler.css +1 -1
  46. package/lib/public/css/sticky-notes.css +231 -226
  47. package/lib/public/index.html +12 -0
  48. package/lib/public/modules/app-connection.js +90 -4
  49. package/lib/public/modules/app-messages.js +54 -19
  50. package/lib/public/modules/app-notifications.js +15 -124
  51. package/lib/public/modules/app-panels.js +43 -2
  52. package/lib/public/modules/app-projects.js +2 -2
  53. package/lib/public/modules/background-tasks-ui.js +139 -16
  54. package/lib/public/modules/git-agent-sessions.js +85 -0
  55. package/lib/public/modules/git-panel.js +111 -84
  56. package/lib/public/modules/git-placard.js +161 -0
  57. package/lib/public/modules/home-sidebar.js +7 -0
  58. package/lib/public/modules/mate-sidebar.js +0 -8
  59. package/lib/public/modules/pane-bridge.js +9 -0
  60. package/lib/public/modules/project-logs-ambient.js +238 -0
  61. package/lib/public/modules/project-logs-render.js +359 -0
  62. package/lib/public/modules/project-logs.js +329 -0
  63. package/lib/public/modules/scheduler.js +17 -3
  64. package/lib/public/modules/sidebar-mobile.js +5 -9
  65. package/lib/public/modules/sidebar-sessions.js +25 -1
  66. package/lib/public/modules/sidebar.js +17 -5
  67. package/lib/public/modules/split-view.js +7 -0
  68. package/lib/public/modules/sticky-notes-browser.js +324 -0
  69. package/lib/public/modules/sticky-notes-card.js +358 -0
  70. package/lib/public/modules/sticky-notes-editor.js +288 -0
  71. package/lib/public/modules/sticky-notes-shared.js +90 -0
  72. package/lib/public/modules/sticky-notes.js +168 -946
  73. package/lib/public/modules/tool-palette-order.js +143 -0
  74. package/lib/public/modules/tool-palette-overlays.js +159 -0
  75. package/lib/public/modules/tool-palette.js +21 -188
  76. package/lib/public/modules/tui-attention.js +10 -0
  77. package/lib/public/modules/update-snooze.js +169 -0
  78. package/lib/public/modules/vendor-login.js +287 -0
  79. package/lib/public/modules/worker-pane-lock.js +174 -0
  80. package/lib/public/style.css +2 -0
  81. package/lib/sdk-bridge.js +77 -10
  82. package/lib/sdk-message-processor.js +24 -7
  83. package/lib/server.js +61 -6
  84. package/lib/session-driver-eligibility.js +183 -0
  85. package/lib/session-notes-mcp-server.js +25 -5
  86. package/lib/session-pair-factory.js +233 -0
  87. package/lib/session-pair-mcp-server.js +45 -5
  88. package/lib/session-pair-prompts.js +60 -0
  89. package/lib/sessions.js +4 -1
  90. package/lib/tools-registry.js +11 -1
  91. package/lib/update-snooze.js +437 -0
  92. package/lib/workspace-query-access.js +102 -0
  93. package/lib/workspace-query-service.js +24 -17
  94. package/lib/ws-schema.js +28 -3
  95. package/lib/yoke/adapters/claude.js +69 -4
  96. package/lib/yoke/adapters/codex.js +42 -5
  97. package/lib/yoke/codex-app-server.js +20 -2
  98. package/lib/yoke/codex-background-tasks.js +8 -2
  99. package/package.json +3 -3
@@ -0,0 +1,233 @@
1
+ // Split-group pair creation.
2
+ //
3
+ // Extracted from project-session-pair.js, which operates a pair once it
4
+ // exists; this module is only about how one comes into being. Both the
5
+ // explicit "Add Split Worker" flow and the Driver-initiated
6
+ // send_to_partner-creates-a-pair flow land here, so vendor validation, owner
7
+ // derivation and the group record are written in exactly one place.
8
+ //
9
+ // Ownership always comes from the connection or the Driver session, never from
10
+ // the incoming message.
11
+ //
12
+ // Two ordering rules matter and are load-bearing:
13
+ //
14
+ // 1. Nothing is created until everything is validated. Driver eligibility and
15
+ // both runtime requests are settled first, so a rejected request leaves no
16
+ // orphan session behind. For a newly requested Driver the tier is checked
17
+ // against the model that session would actually resolve to, before either
18
+ // session exists.
19
+ // 2. `preflightRuntime` is side-effect free, so a caller that is about to
20
+ // destroy something (replacement dissolving a live pair) can validate the
21
+ // replacement first and abort while the old pair is still intact.
22
+
23
+ var driverEligibility = require("./session-driver-eligibility");
24
+ var models = require("./project-models");
25
+ var yoke = require("./yoke");
26
+
27
+ function attachPairFactory(ctx) {
28
+ var sm = ctx.sm;
29
+ var store = ctx.splitStore;
30
+
31
+ function validateVendor(vendor) {
32
+ var installed = sm.installedVendors || [];
33
+ if (!vendor || installed.indexOf(vendor) === -1) throw new Error("vendor is not installed: " + (vendor || "unknown"));
34
+ return vendor;
35
+ }
36
+
37
+ // An explicitly requested model must exist in the server's own catalog for
38
+ // that vendor, matched through the repo's canonical matcher so an alias, an
39
+ // id or a resolvedModel all count. Caller text is never taken on trust: if
40
+ // the catalog is not populated we cannot confirm the model, so an explicit
41
+ // request fails closed rather than being forwarded blindly.
42
+ function validateModel(vendor, model) {
43
+ var requested = typeof model === "string" ? model.trim() : "";
44
+ if (!requested) return null;
45
+ var catalog = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
46
+ if (catalog.length === 0) {
47
+ throw new Error("no models are available yet for vendor \"" + vendor +
48
+ "\", so model \"" + requested + "\" cannot be confirmed");
49
+ }
50
+ for (var i = 0; i < catalog.length; i++) {
51
+ if (models.modelEntryMatches(catalog[i], requested)) return requested;
52
+ }
53
+ throw new Error("model is not available for vendor \"" + vendor + "\": " + requested);
54
+ }
55
+
56
+ function validateEffort(vendor, effort) {
57
+ var requested = typeof effort === "string" ? effort.trim() : "";
58
+ var fallback = (sm.currentEffortByVendor && sm.currentEffortByVendor[vendor]) || sm.currentEffort || "medium";
59
+ var clamped = yoke.clampEffort(vendor, requested || fallback) || null;
60
+ if (requested && !clamped) {
61
+ throw new Error("reasoning effort is not supported for vendor \"" + vendor + "\": " + requested);
62
+ }
63
+ return clamped;
64
+ }
65
+
66
+ // Resolve and validate one session runtime request without creating
67
+ // anything. Throws on the first unusable field.
68
+ function preflightRuntime(spec, defaultVendor) {
69
+ var request = spec || {};
70
+ var vendor = validateVendor((typeof request.vendor === "string" && request.vendor.trim()) || defaultVendor);
71
+ return {
72
+ vendor: vendor,
73
+ model: validateModel(vendor, request.model),
74
+ effort: validateEffort(vendor, request.effort),
75
+ explicitEffort: !!(typeof request.effort === "string" && request.effort.trim()),
76
+ };
77
+ }
78
+
79
+ // The vendor a Driver-initiated Worker gets when none is named: prefer a
80
+ // different engine from the Driver's own, so the pair does not double up.
81
+ function defaultWorkerVendorFor(driver) {
82
+ var installed = sm.installedVendors || [];
83
+ if (installed.length === 0) throw new Error("no coding agent is installed for a Split Worker session");
84
+ if (installed.indexOf("codex") !== -1 && driver.vendor !== "codex") return "codex";
85
+ for (var i = 0; i < installed.length; i++) {
86
+ if (installed[i] !== driver.vendor) return installed[i];
87
+ }
88
+ return installed[0];
89
+ }
90
+
91
+ // Side-effect-free check that this exact Driver could create this exact
92
+ // Worker right now. Used by replacement before it touches anything.
93
+ function preflightWorkerForDriver(driver, args) {
94
+ if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
95
+ if (!driver) throw new Error("driver session not found");
96
+ var verdict = driverEligibility.evaluateDriverSession(driver, sm);
97
+ if (!verdict.ok) throw new Error(verdict.error);
98
+ var options = args || {};
99
+ var vendor = typeof options.workerVendor === "string" ? options.workerVendor.trim() : "";
100
+ return preflightRuntime({
101
+ vendor: vendor || defaultWorkerVendorFor(driver),
102
+ model: options.workerModel,
103
+ effort: options.workerEffort,
104
+ }, null);
105
+ }
106
+
107
+ function createPairRecord(ws, msg) {
108
+ if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
109
+ var driverSpec = msg.driver || {};
110
+ var workerSpec = msg.worker || {};
111
+ var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
112
+
113
+ // --- Validate everything first; create nothing yet. ---
114
+
115
+ var workerRuntime = preflightRuntime(workerSpec, sm.lastVendor || "codex");
116
+
117
+ var existingDriver = null;
118
+ var driverRuntime = null;
119
+ var groupName;
120
+ if (Number.isInteger(driverSpec.sessionId)) {
121
+ // "Add Split Worker" on an existing session: the current session becomes
122
+ // the Driver with its full conversation context intact.
123
+ existingDriver = sm.sessions.get(driverSpec.sessionId);
124
+ if (!existingDriver) throw new Error("driver session not found");
125
+ if ((existingDriver.ownerId || null) !== ownerId) throw new Error("driver session access denied");
126
+ if (store.groupForMember(existingDriver.localId)) throw new Error("this session is already in a split group");
127
+ var existingVerdict = driverEligibility.evaluateDriverSession(existingDriver, sm);
128
+ if (!existingVerdict.ok) throw new Error(existingVerdict.error);
129
+ groupName = undefined; // auto name from member titles
130
+ } else {
131
+ driverRuntime = preflightRuntime(driverSpec, "claude");
132
+ // The tier is judged on the model this session would really resolve to:
133
+ // the explicit request if there is one, else that vendor's default.
134
+ var effectiveDriverModel = driverRuntime.model ||
135
+ (sm.defaultModelByVendor && sm.defaultModelByVendor[driverRuntime.vendor]) || "";
136
+ var newVerdict = driverEligibility.evaluateDriverModel(
137
+ driverRuntime.vendor, effectiveDriverModel, sm.modelsByVendor
138
+ );
139
+ if (!newVerdict.ok) throw new Error(newVerdict.error);
140
+ groupName = "Agent pair";
141
+ }
142
+
143
+ // --- Everything is valid; now create. ---
144
+
145
+ var driver = existingDriver;
146
+ var createdDriver = null;
147
+ if (!driver) {
148
+ driver = sm.createSessionRaw({
149
+ ownerId: ownerId,
150
+ vendor: driverRuntime.vendor,
151
+ model: driverRuntime.model,
152
+ effort: driverRuntime.effort,
153
+ });
154
+ createdDriver = driver;
155
+ driver.title = "Driver · " + ((yoke.getVendorInfo(driverRuntime.vendor) || {}).displayName || driverRuntime.vendor);
156
+ if (driverRuntime.explicitEffort) driver.loopSettings = { effort: driverRuntime.effort };
157
+ }
158
+
159
+ var worker = sm.createSessionRaw({
160
+ ownerId: ownerId,
161
+ vendor: workerRuntime.vendor,
162
+ model: workerRuntime.model,
163
+ effort: workerRuntime.effort,
164
+ });
165
+ worker.title = "Split Worker · " + ((yoke.getVendorInfo(workerRuntime.vendor) || {}).displayName || workerRuntime.vendor);
166
+ if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
167
+
168
+ var result = store.create(ws, {
169
+ members: [driver.localId, worker.localId],
170
+ pair: { driverId: driver.localId, workerId: worker.localId },
171
+ name: groupName,
172
+ });
173
+ if (!result.ok) {
174
+ // The group is the last step, so a failure here would otherwise strand
175
+ // the sessions this call just made. Only sessions created by this call
176
+ // are removed; an existing Driver is never touched.
177
+ removeCreated(worker);
178
+ removeCreated(createdDriver);
179
+ throw new Error(result.error);
180
+ }
181
+ sm.broadcastSessionList();
182
+ return { driver: driver, worker: worker, group: result.group };
183
+ }
184
+
185
+ function removeCreated(session) {
186
+ if (!session) return;
187
+ if (typeof sm.deleteSessionQuiet === "function") {
188
+ try { sm.deleteSessionQuiet(session.localId); return; } catch (e) {}
189
+ }
190
+ try { sm.sessions.delete(session.localId); } catch (e) {}
191
+ }
192
+
193
+ function createWorkerForDriver(driver, args) {
194
+ // Validates the Driver's eligibility and the whole Worker runtime before
195
+ // any session is made, so a rejection creates nothing.
196
+ var runtime = preflightWorkerForDriver(driver, args);
197
+ var ws = {
198
+ _clayActiveSession: driver.localId,
199
+ _clayUser: driver.ownerId ? { id: driver.ownerId } : null,
200
+ };
201
+ var created = createPairRecord(ws, {
202
+ driver: { sessionId: driver.localId },
203
+ worker: {
204
+ vendor: runtime.vendor,
205
+ model: runtime.model || "",
206
+ effort: runtime.explicitEffort ? (args && args.workerEffort) || "" : "",
207
+ },
208
+ });
209
+ sm.sendToSession(driver, { type: "pair_session_created", ok: true, group: created.group });
210
+ return created;
211
+ }
212
+
213
+ function createPair(ws, msg) {
214
+ try {
215
+ var created = createPairRecord(ws, msg);
216
+ ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
217
+ } catch (e) {
218
+ ctx.sendTo(ws, { type: "pair_session_created", ok: false, error: e.message || String(e) });
219
+ }
220
+ }
221
+
222
+ return {
223
+ createPair: createPair,
224
+ createPairRecord: createPairRecord,
225
+ createWorkerForDriver: createWorkerForDriver,
226
+ preflightRuntime: preflightRuntime,
227
+ preflightWorkerForDriver: preflightWorkerForDriver,
228
+ validateModel: validateModel,
229
+ validateVendor: validateVendor,
230
+ };
231
+ }
232
+
233
+ module.exports = { attachPairFactory: attachPairFactory };
@@ -2,11 +2,14 @@
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
- function getToolDefs(handlers) {
6
- return [
5
+ // `options.lifecycle` adds the autonomous management tools. They are omitted
6
+ // for a plain side-by-side split, which has no Driver role to exercise them.
7
+ function getToolDefs(handlers, options) {
8
+ var lifecycle = !!(options && options.lifecycle);
9
+ var defs = [
7
10
  {
8
11
  name: "send_to_partner",
9
- description: "Delegate one concrete task to the visible Split Worker. If this session has no pair yet, Clay creates a real Split Worker session and opens it visibly in the right pane before starting the task. A completed turn does not end the Split Worker session: reuse the same Split Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
12
+ description: "Delegate one concrete task to the visible Split Worker. If this session has no pair yet, Clay creates a real Split Worker session and opens it visibly in the right pane before starting the task — no approval step and no suggestion card. A completed turn does not end the Split Worker session: reuse the same Split Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
10
13
  inputSchema: buildShape({
11
14
  message: { type: "string", description: "The complete task or question for the partner." },
12
15
  wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
@@ -19,9 +22,9 @@ function getToolDefs(handlers) {
19
22
  },
20
23
  {
21
24
  name: "read_partner",
22
- description: "Read the partner's current status and recent conversation turns. Use this for an interim check after a non-waiting delegation or timeout; completed results are pushed automatically.",
25
+ description: "Read the Split Worker's current status and recent conversation turns, plus the same bounded capacity report partner_status returns. Use this for an interim check after a non-waiting delegation or timeout; completed results are pushed automatically.",
23
26
  inputSchema: buildShape({
24
- lastTurns: { type: "number", description: "Number of recent user-message-delimited turns, from 1 to 5. Defaults to 1." },
27
+ lastTurns: { type: "number", description: "Number of recent user-message-delimited turns, from 0 to 5. Use 0 for status only. Defaults to 1." },
25
28
  }),
26
29
  handler: function (args) { return handlers.read(args || {}); },
27
30
  },
@@ -38,6 +41,43 @@ function getToolDefs(handlers) {
38
41
  handler: function (args) { return handlers.close(args || {}); },
39
42
  },
40
43
  ];
44
+ if (!lifecycle) return defs;
45
+ return defs.concat([
46
+ {
47
+ name: "partner_status",
48
+ description: "Bounded capacity and continuity report for your Split Worker, for deciding reuse against replacement: context tokens used and the ratio of its window, current task and activity, vendor/model/effort, history size and idle time, whether it is safe to replace right now, and your own recorded results for earlier Worker generations. Returns no transcript. Prefer this over reading turns when you only need to decide.",
49
+ inputSchema: buildShape({}),
50
+ handler: function (args) { return handlers.status(args || {}); },
51
+ },
52
+ {
53
+ name: "replace_partner",
54
+ description: "Replace your Split Worker with a fresh, compact one in a single operation: the old pair is dissolved, a new Worker session is created and opened visibly, and an optional task is delivered to it. The previous Worker's conversation is preserved and stays browsable. Use this when the current Worker is context-bloated, stale, or working on something unrelated to the next task. Refuses while the Worker is mid-turn unless you pass interrupt true.",
55
+ inputSchema: buildShape({
56
+ interrupt: { type: "boolean", description: "Stop the Worker first if it is mid-turn or holding a delegated task. Required to replace an active Worker." },
57
+ message: { type: "string", description: "Optional task to deliver to the new Worker immediately after it is created." },
58
+ wait: { type: "boolean", description: "When a message is given, wait for that turn to finish. Defaults to true." },
59
+ timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
60
+ workerVendor: { type: "string", description: "Optional vendor for the new Worker. Must be installed and available." },
61
+ workerModel: { type: "string", description: "Optional model for the new Worker. Must be offered by that vendor." },
62
+ workerEffort: { type: "string", description: "Optional reasoning effort for the new Worker." },
63
+ evaluation: {
64
+ type: "object",
65
+ description: "Optional bounded assessment of the Worker being replaced, recorded against that exact generation.",
66
+ },
67
+ }),
68
+ handler: function (args) { return handlers.replace(args || {}); },
69
+ },
70
+ {
71
+ name: "record_partner_evaluation",
72
+ description: "Record a bounded assessment of one Split Worker generation so your later model choices can use observed outcomes instead of re-reading transcripts. Records your judgement alongside the objective signals Clay already measured (turns, errors, interruption, context used). This is scoped to your own pair and forms no global model ranking.",
73
+ inputSchema: buildShape({
74
+ outcome: { type: "string", description: 'One of "succeeded", "partial", "failed", "abandoned".' },
75
+ note: { type: "string", description: "Optional short reason, up to 400 characters." },
76
+ generation: { type: "number", description: "Optional generation number. Defaults to the current Worker." },
77
+ }, ["outcome"]),
78
+ handler: function (args) { return handlers.evaluate(args || {}); },
79
+ },
80
+ ]);
41
81
  }
42
82
 
43
83
  module.exports = { getToolDefs: getToolDefs };
@@ -0,0 +1,60 @@
1
+ // Driver-facing system prompt text for the visible Driver/Split Worker pair.
2
+ //
3
+ // Pure data, extracted from project-session-pair.js so that module stays under
4
+ // the size limit and so the guidance can be reviewed as prose. There is no
5
+ // proposal or approval language here by design: a qualified Driver manages its
6
+ // Worker on its own authority, so telling the model to suggest or ask would
7
+ // contradict the tools it actually has.
8
+
9
+ var DRIVER = [
10
+ "You are the Driver of a visible Driver/Split Worker pair, and you manage that Split Worker yourself.",
11
+ "The tools send_to_partner, read_partner, partner_status, replace_partner, interrupt_partner, close_partner,",
12
+ "and record_partner_evaluation are provided directly to you.",
13
+ "",
14
+ "Your management objective: protect your own context, keep the Split Worker compact, and put execution where",
15
+ "it runs best. Delegate implementation-heavy work rather than doing it here. Reuse the existing Split Worker",
16
+ "only when its accumulated context genuinely helps the next task; when it is context-bloated, stale, or",
17
+ "working on something unrelated, replace it instead of carrying that cost forward. Call partner_status to",
18
+ "decide: it reports context tokens used and the ratio of its window, current activity, vendor/model/effort,",
19
+ "history size and idle time, whether replacing is safe right now, and the results you recorded for earlier",
20
+ "Worker generations. It returns no transcript, so it is cheap to consult.",
21
+ "",
22
+ "Act on your own authority. You create, reuse, interrupt, replace, and close the Split Worker without asking",
23
+ "the user and without posting any suggestion or approval card. send_to_partner creates and opens a visible",
24
+ "Split Worker when none exists. replace_partner dissolves the current pair and opens a fresh compact Worker in",
25
+ "one step, optionally delivering the next task with it; the replaced Worker keeps its conversation, so nothing",
26
+ "is lost. Replacing an actively running Worker requires interrupt true, which stops it first. The human may",
27
+ "still message or stop the Split Worker directly; treat that intervention as authoritative, and use",
28
+ "close_partner immediately when they ask to close or dismiss the pane.",
29
+ "",
30
+ "Choose the Worker vendor, model, and effort for the task from what is actually installed and offered; an",
31
+ "unavailable choice is refused rather than silently substituted. After a Worker generation finishes or is",
32
+ "replaced, call record_partner_evaluation with succeeded, partial, failed, or abandoned and a short reason.",
33
+ "Clay stores that against that exact generation alongside what it measured itself, and partner_status hands it",
34
+ "back, so your next model choice can use observed results instead of guesswork. This is your own record for",
35
+ "this pair, not a general ranking of models.",
36
+ "",
37
+ "Internal Sub-agents are a distinct execution mechanism, not a lexical category; use them only when the user",
38
+ "clearly intends internal or background parallel delegation rather than the visible paired session. When",
39
+ "ambiguous and a visible pair exists, prefer the visible Split Worker. If review or user feedback requires",
40
+ "corrections to the Split Worker's implementation, delegate a follow-up turn to that Split Worker instead of",
41
+ "editing its files yourself. If a non-waiting delegation finishes later, Clay pushes the result back and",
42
+ "resumes you automatically. You also decide the Split Worker's tool-permission requests when they arise;",
43
+ "approve only what falls inside the task the user authorized. Integrate and verify the final outcome. Do not",
44
+ "search the project for implementations of these tools, and do not delegate work that must be performed",
45
+ "sequentially in this same session.",
46
+ ].join(" ").replace(/ {2,}/g, " ");
47
+
48
+ var UNPAIRED = [
49
+ "Infer the user's intended target from conversational and UI context. References to a user-visible paired or",
50
+ "split pane, its session, its activity or status, or a collaborator the user wants opened resolve to Clay's",
51
+ "Split Worker and its partner tools. Internal Sub-agents are a distinct execution mechanism, not a lexical",
52
+ "category; use them only when the user clearly intends internal or background parallel delegation rather than a",
53
+ "visible paired session. When ambiguity would materially change where work runs, ask a concise clarification",
54
+ "instead of guessing. When a task is implementation-heavy, call send_to_partner directly: Clay creates the",
55
+ "paired Split Worker session and opens it in the right pane automatically before delivering the task. Do not",
56
+ "ask the user for permission to start one, do not ask them to enable split mode, and do not use a background",
57
+ "Sub-agent as a substitute for a visible Split Worker.",
58
+ ].join(" ").replace(/ {2,}/g, " ");
59
+
60
+ module.exports = { DRIVER: DRIVER, UNPAIRED: UNPAIRED };
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,
@@ -137,7 +137,17 @@ function seedBuiltInCapsules(ctx) {
137
137
  for (var ri = 0; ri < REMOVED_BUILTIN_IDS.length; ri++) {
138
138
  fs.rmSync(path.join(root, REMOVED_BUILTIN_IDS[ri]), { recursive: true, force: true });
139
139
  }
140
- var entries = fs.readdirSync(CAPSULES_ROOT, { withFileTypes: true });
140
+ // Clay ships no built-in Capsules any more, so lib/capsules/ is absent from a
141
+ // fresh checkout and from the npm tarball (neither git nor npm carries an
142
+ // empty directory). Seeding still owns the removed-built-in migration above
143
+ // and the marker below, so an absent root means "nothing to copy" rather than
144
+ // a failure. Anything other than a missing root is still a real error.
145
+ var entries = [];
146
+ try {
147
+ entries = fs.readdirSync(CAPSULES_ROOT, { withFileTypes: true });
148
+ } catch (e) {
149
+ if (e.code !== "ENOENT") throw e;
150
+ }
141
151
  var seeded = [];
142
152
  for (var i = 0; i < entries.length; i++) {
143
153
  if (!entries[i].isDirectory()) continue;