clay-server 4.0.0-beta.2 → 4.0.0-beta.21

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 (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -0,0 +1,460 @@
1
+ // Split Worker lifecycle internals: bounded status, transactional replacement,
2
+ // and the per-generation evaluation ledger. The Driver-facing tool posts a
3
+ // user-controlled runtime card before accepted replacement reaches this module.
4
+ //
5
+ // Everything is bound to the exact live pair. The Driver is re-resolved from
6
+ // the split store on every call, its structural capability is re-checked every time, and
7
+ // ownership must match on both sessions. No active-tab or global-session
8
+ // fallback exists in this module.
9
+ //
10
+ // What replacement deliberately does NOT do: delete history. Dissolving a pair
11
+ // leaves both sessions in the project exactly as close_partner already does, so
12
+ // the previous Worker's conversation stays browsable and recoverable under the
13
+ // existing session semantics. There is no archive concept in the repo to hook
14
+ // into, and inventing a destructive one would lose work.
15
+
16
+ var eligibility = require("./session-driver-eligibility");
17
+
18
+ var MAX_GENERATIONS = 5;
19
+ var MAX_NOTE_CHARS = 400;
20
+ var MAX_TASK_PREVIEW_CHARS = 200;
21
+
22
+ var EVALUATION_OUTCOMES = ["succeeded", "partial", "failed", "abandoned"];
23
+
24
+ function toolResult(value) {
25
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
26
+ }
27
+
28
+ function toolError(message) {
29
+ return Promise.resolve({
30
+ content: [{ type: "text", text: "Error: " + message }],
31
+ isError: true,
32
+ });
33
+ }
34
+
35
+ function clampText(value, max) {
36
+ var text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
37
+ if (text.length > max) text = text.slice(0, max - 1) + "…";
38
+ return text;
39
+ }
40
+
41
+ function firstNumber() {
42
+ for (var i = 0; i < arguments.length; i++) {
43
+ var value = arguments[i];
44
+ if (typeof value === "number" && isFinite(value) && value >= 0) return value;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ // The Worker's context position, from authoritative session accounting only:
50
+ // the SDK's own getContextUsage snapshot when the adapter provides one, else
51
+ // the last result message's usage totals. Field names differ across adapters,
52
+ // so each candidate is probed and `source` states which reading was used.
53
+ // Never estimated from history text.
54
+ function contextStatus(session) {
55
+ var snapshot = session.lastContextUsage || null;
56
+ var used = null;
57
+ var window = null;
58
+ var source = "unavailable";
59
+
60
+ if (snapshot && typeof snapshot === "object") {
61
+ used = firstNumber(snapshot.totalTokens, snapshot.usedTokens, snapshot.tokens, snapshot.inputTokens);
62
+ window = firstNumber(snapshot.contextWindow, snapshot.maxTokens, snapshot.windowSize, snapshot.limit);
63
+ if (used !== null || window !== null) source = "sdk_context_usage";
64
+ }
65
+
66
+ if (used === null) {
67
+ var history = session.history || [];
68
+ for (var i = history.length - 1; i >= 0; i--) {
69
+ if (!history[i] || history[i].type !== "result") continue;
70
+ var usage = history[i].usage || null;
71
+ if (!usage) break;
72
+ var input = firstNumber(usage.input_tokens, usage.inputTokens) || 0;
73
+ var cacheRead = firstNumber(usage.cache_read_input_tokens, usage.cacheReadInputTokens) || 0;
74
+ var cacheWrite = firstNumber(usage.cache_creation_input_tokens, usage.cacheCreationInputTokens) || 0;
75
+ var output = firstNumber(usage.output_tokens, usage.outputTokens) || 0;
76
+ used = input + cacheRead + cacheWrite + output;
77
+ source = "last_result_usage";
78
+ break;
79
+ }
80
+ }
81
+
82
+ var ratio = null;
83
+ if (used !== null && window !== null && window > 0) {
84
+ ratio = Math.round((used / window) * 1000) / 1000;
85
+ if (ratio > 1) ratio = 1;
86
+ }
87
+ return {
88
+ source: source,
89
+ usedTokens: used,
90
+ contextWindow: window,
91
+ usedRatio: ratio,
92
+ };
93
+ }
94
+
95
+ function continuityStatus(session) {
96
+ var history = session.history || [];
97
+ var userTurns = 0;
98
+ var errors = 0;
99
+ for (var i = 0; i < history.length; i++) {
100
+ if (!history[i]) continue;
101
+ if (history[i].type === "user_message") userTurns++;
102
+ else if (history[i].type === "error") errors++;
103
+ }
104
+ var lastActivity = typeof session.lastActivity === "number" ? session.lastActivity : null;
105
+ return {
106
+ historyEntries: history.length,
107
+ userTurns: userTurns,
108
+ errorEntries: errors,
109
+ idleSeconds: lastActivity ? Math.max(0, Math.round((Date.now() - lastActivity) / 1000)) : null,
110
+ };
111
+ }
112
+
113
+ // The current task, as a bounded preview of the delegated instruction only.
114
+ // Never the transcript: a Driver deciding reuse needs to know what the Worker
115
+ // is on, not to re-read its conversation.
116
+ function activityStatus(session) {
117
+ var token = session._pairDelegation || null;
118
+ return {
119
+ isProcessing: !!(session.isProcessing || session._queryStarting),
120
+ delegated: !!token,
121
+ currentTask: token ? clampText(token.message, MAX_TASK_PREVIEW_CHARS) : "",
122
+ lastTurnInterrupted: !!session._lastTurnInterrupted,
123
+ };
124
+ }
125
+
126
+ function attachPairLifecycle(ctx) {
127
+ var sm = ctx.sm;
128
+ var store = ctx.splitStore;
129
+ var turnControl = ctx.turnControl;
130
+
131
+ // Per-Driver ledger of the Worker generations it has run, newest last.
132
+ // Lives on the live Driver session object: it informs the Driver's next
133
+ // choice within this session and is deliberately not persisted, because a
134
+ // bounded observation of a session that no longer exists would only be
135
+ // misleading after a restart.
136
+ function ledgerFor(driver) {
137
+ if (!Array.isArray(driver._workerGenerations)) driver._workerGenerations = [];
138
+ return driver._workerGenerations;
139
+ }
140
+
141
+ function recordGenerationStart(driver, worker) {
142
+ var ledger = ledgerFor(driver);
143
+ var generation = ledger.length + 1;
144
+ ledger.push({
145
+ generation: generation,
146
+ workerSessionId: worker.localId,
147
+ vendor: worker.vendor || null,
148
+ model: worker.model || null,
149
+ effort: worker.effort || null,
150
+ startedAt: Date.now(),
151
+ endedAt: null,
152
+ observed: null,
153
+ evaluation: null,
154
+ });
155
+ while (ledger.length > MAX_GENERATIONS) ledger.shift();
156
+ worker._pairGeneration = generation;
157
+ return generation;
158
+ }
159
+
160
+ function findGeneration(driver, workerSessionId) {
161
+ var ledger = ledgerFor(driver);
162
+ for (var i = ledger.length - 1; i >= 0; i--) {
163
+ if (ledger[i].workerSessionId === workerSessionId) return ledger[i];
164
+ }
165
+ return null;
166
+ }
167
+
168
+ // Objective signals the server already has. Recorded when a generation ends
169
+ // so the Driver's next decision can use them without a transcript.
170
+ function closeGeneration(driver, worker) {
171
+ var record = findGeneration(driver, worker.localId);
172
+ if (!record || record.endedAt) return record;
173
+ var continuity = continuityStatus(worker);
174
+ var context = contextStatus(worker);
175
+ record.endedAt = Date.now();
176
+ record.observed = {
177
+ userTurns: continuity.userTurns,
178
+ errorEntries: continuity.errorEntries,
179
+ interrupted: !!worker._lastTurnInterrupted,
180
+ usedTokens: context.usedTokens,
181
+ usedRatio: context.usedRatio,
182
+ };
183
+ return record;
184
+ }
185
+
186
+ // The exact pair, with the Driver's structural capability re-checked on every call.
187
+ function resolveDriverPair(caller) {
188
+ if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
189
+ // Exact live object identity, before anything is read off the caller. A
190
+ // tool handler captured by a query outlives the session it was bound to, so
191
+ // a stale object — or a different object that happens to carry the same
192
+ // localId — must not be able to act as the Driver.
193
+ if (sm.sessions.get(caller.localId) !== caller) {
194
+ throw new Error("this session is no longer live; the pair tools are bound to an exact session");
195
+ }
196
+ var verdict = eligibility.evaluateDriverSession(caller, sm);
197
+ if (!verdict.ok) throw new Error(verdict.error);
198
+ var group = store.groupForMember(caller.localId);
199
+ if (!group) throw new Error("this session is not in a split group");
200
+ if (!group.pair) throw new Error("this split group has no Driver/Split Worker roles");
201
+ if (group.pair.driverId !== caller.localId) throw new Error("only the configured Driver can direct this pair");
202
+ var worker = sm.sessions.get(group.pair.workerId);
203
+ if (!worker) throw new Error("split partner session was not found");
204
+ if ((caller.ownerId || null) !== (worker.ownerId || null)) throw new Error("split partner access denied");
205
+ return { group: group, worker: worker };
206
+ }
207
+
208
+ function replaceBlockedReason(worker) {
209
+ if (worker.isProcessing || worker._queryStarting) return "the Split Worker is mid-turn";
210
+ if (worker._pairDelegation) return "a delegated task is still open";
211
+ return null;
212
+ }
213
+
214
+ // Bounded status for the reuse-vs-replace decision. Exact pair only; no
215
+ // other user's data and no transcript.
216
+ function partnerStatus(caller) {
217
+ var resolved = resolveDriverPair(caller);
218
+ var worker = resolved.worker;
219
+ var blocked = turnControl.blockedReason(caller) || replaceBlockedReason(worker);
220
+ var ledger = ledgerFor(caller).map(function (record) {
221
+ return {
222
+ generation: record.generation,
223
+ vendor: record.vendor,
224
+ model: record.model,
225
+ effort: record.effort,
226
+ observed: record.observed,
227
+ evaluation: record.evaluation,
228
+ };
229
+ });
230
+ return {
231
+ worker: {
232
+ sessionId: worker.localId,
233
+ title: worker.title || "New Session",
234
+ vendor: worker.vendor || null,
235
+ model: worker.model || null,
236
+ effort: worker.effort || null,
237
+ generation: worker._pairGeneration || null,
238
+ },
239
+ activity: activityStatus(worker),
240
+ context: contextStatus(worker),
241
+ continuity: continuityStatus(worker),
242
+ orchestration: turnControl.status(caller),
243
+ replaceSafe: !blocked,
244
+ replaceBlockedReason: blocked,
245
+ generations: ledger,
246
+ };
247
+ }
248
+
249
+ // Transactional replacement. Rejects an active Worker unless interrupt is
250
+ // explicitly true, cancels anything the old Worker was waiting on, dissolves
251
+ // the exact pair without deleting history, and creates a fresh Worker.
252
+ // Idempotent in the sense that it either completes or leaves the existing
253
+ // pair untouched; it never half-dissolves.
254
+ function replacePartner(args, caller) {
255
+ var resolved = resolveDriverPair(caller);
256
+ var group = resolved.group;
257
+ var oldWorker = resolved.worker;
258
+ turnControl.assertWorkerAction(caller);
259
+
260
+ // Validate the replacement while the old pair is still fully intact. An
261
+ // uninstalled vendor, an unavailable model or an unsupported effort must
262
+ // never cost the user their running Worker, so this precedes the interrupt,
263
+ // the permission cancellation and the dissolve.
264
+ ctx.preflightWorkerForDriver(caller, {
265
+ workerVendor: args.workerVendor,
266
+ workerModel: args.workerModel,
267
+ workerEffort: args.workerEffort,
268
+ });
269
+
270
+ var blocked = replaceBlockedReason(oldWorker);
271
+ if (blocked && args.interrupt !== true) {
272
+ throw new Error("cannot replace the Split Worker because " + blocked +
273
+ "; call again with interrupt set to true to stop it first");
274
+ }
275
+
276
+ if (args.evaluation) validateEvaluation(args.evaluation);
277
+ var creationTicket = turnControl.reserveCreation(caller, "replace");
278
+
279
+ if (blocked) {
280
+ oldWorker.taskStopRequested = true;
281
+ if (oldWorker.abortController) {
282
+ try { oldWorker.abortController.abort(); } catch (e) {}
283
+ }
284
+ if (oldWorker._pairDelegation && typeof ctx.finishDelegation === "function") {
285
+ ctx.finishDelegation(group, caller, oldWorker, oldWorker._pairDelegation);
286
+ }
287
+ }
288
+
289
+ // Any permission decision the old Worker was waiting on dies with the pair.
290
+ if (typeof ctx.cancelWorkerPermissions === "function") {
291
+ ctx.cancelWorkerPermissions(oldWorker, "The Driver replaced this Split Worker.");
292
+ }
293
+
294
+ var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
295
+ var dissolved = store.dissolve(ws, { id: group.id });
296
+ if (!dissolved.ok) {
297
+ turnControl.releaseCreation(creationTicket);
298
+ throw new Error(dissolved.error || "could not dissolve the existing pair");
299
+ }
300
+
301
+ // History is preserved: the old Worker session stays in the project.
302
+ //
303
+ // Preflight already cleared every input, but the group write itself can
304
+ // still fail, so the dissolve is rolled back rather than leaving the Driver
305
+ // with no pair at all. The restored group is an equivalent record with the
306
+ // same members and roles; its group id is newly issued.
307
+ //
308
+ // What rollback can and cannot undo:
309
+ // - Idle replacement failure is fully recoverable. The Worker session,
310
+ // its history and its still-open ledger generation are all preserved,
311
+ // and the pair is restored.
312
+ // - An explicit interrupt=true is NOT reversible. Stopping a mid-turn
313
+ // Worker aborts its query and cancels the permission decisions it was
314
+ // waiting on; a later creation failure cannot resume that turn. The
315
+ // session and its history survive, but the interrupted work does not
316
+ // come back. The error says so rather than implying a clean restore.
317
+ var created;
318
+ try {
319
+ created = ctx.createWorkerForDriver(caller, {
320
+ workerVendor: typeof args.workerVendor === "string" ? args.workerVendor : "",
321
+ workerModel: typeof args.workerModel === "string" ? args.workerModel : "",
322
+ workerEffort: typeof args.workerEffort === "string" ? args.workerEffort : "",
323
+ });
324
+ } catch (e) {
325
+ var restored = store.create(ws, {
326
+ members: [caller.localId, oldWorker.localId],
327
+ pair: { driverId: caller.localId, workerId: oldWorker.localId },
328
+ });
329
+ var restoreNote = restored && restored.ok
330
+ ? "The previous pair was restored with its session, history and open generation intact."
331
+ : "The previous pair could not be restored; both sessions are intact and unpaired.";
332
+ var interruptNote = blocked
333
+ ? " Its interrupted turn cannot be resumed, because stopping it was explicitly requested."
334
+ : "";
335
+ turnControl.releaseCreation(creationTicket);
336
+ throw new Error("could not create the replacement Split Worker: " + (e.message || String(e)) +
337
+ ". " + restoreNote + interruptNote);
338
+ }
339
+ var closed = closeGeneration(caller, oldWorker);
340
+ if (closed && args.evaluation) applyEvaluation(closed, args.evaluation);
341
+ var generation = recordGenerationStart(caller, created.worker);
342
+
343
+ var result = {
344
+ status: "replaced",
345
+ previousWorkerSessionId: oldWorker.localId,
346
+ previousWorkerHistoryPreserved: true,
347
+ previousGeneration: closed ? closed.generation : null,
348
+ interrupted: !!blocked,
349
+ workerSessionId: created.worker.localId,
350
+ generation: generation,
351
+ vendor: created.worker.vendor || null,
352
+ model: created.worker.model || null,
353
+ effort: created.worker.effort || null,
354
+ };
355
+
356
+ var message = typeof args.message === "string" ? args.message.trim() : "";
357
+ if (!message) return Promise.resolve(result);
358
+ return Promise.resolve(ctx.sendToPartner({ message: message, wait: args.wait, timeoutSeconds: args.timeoutSeconds }, caller))
359
+ .then(function (delivered) {
360
+ result.delivery = delivered;
361
+ return result;
362
+ });
363
+ }
364
+
365
+ // Shape check only; writes nothing. Lets a caller that is about to destroy
366
+ // something reject a malformed assessment first.
367
+ function validateEvaluation(raw) {
368
+ var input = raw && typeof raw === "object" ? raw : {};
369
+ var outcome = typeof input.outcome === "string" ? input.outcome.trim().toLowerCase() : "";
370
+ if (EVALUATION_OUTCOMES.indexOf(outcome) === -1) {
371
+ throw new Error('evaluation outcome must be one of: ' + EVALUATION_OUTCOMES.join(", "));
372
+ }
373
+ return { outcome: outcome, note: clampText(input.note, MAX_NOTE_CHARS) };
374
+ }
375
+
376
+ function applyEvaluation(record, raw) {
377
+ var clean = validateEvaluation(raw);
378
+ record.evaluation = {
379
+ outcome: clean.outcome,
380
+ note: clean.note,
381
+ recordedAt: Date.now(),
382
+ };
383
+ return record.evaluation;
384
+ }
385
+
386
+ // Attach a bounded assessment to one exact Worker generation. The Driver
387
+ // supplies the judgement; the server supplies the objective observations and
388
+ // refuses anything outside the enum. No global or cross-user ranking is
389
+ // formed from this.
390
+ function recordEvaluation(args, caller) {
391
+ var resolved = resolveDriverPair(caller);
392
+ var target = Number.isInteger(args.generation)
393
+ ? (function () {
394
+ var ledger = ledgerFor(caller);
395
+ for (var i = 0; i < ledger.length; i++) {
396
+ if (ledger[i].generation === args.generation) return ledger[i];
397
+ }
398
+ return null;
399
+ })()
400
+ : findGeneration(caller, resolved.worker.localId);
401
+ if (!target) throw new Error("no such Split Worker generation for this Driver");
402
+ var evaluation = applyEvaluation(target, args);
403
+ return {
404
+ status: "recorded",
405
+ generation: target.generation,
406
+ vendor: target.vendor,
407
+ model: target.model,
408
+ evaluation: evaluation,
409
+ observed: target.observed,
410
+ };
411
+ }
412
+
413
+ // Tool handlers for the three lifecycle tools, with this module's own error
414
+ // shaping. Kept here so the pair coordinator only wires names to handlers.
415
+ // partnerStatus, or null when this session cannot legitimately ask for it.
416
+ // Lets read_partner fold the capacity report in without duplicating the
417
+ // guard chain or swallowing errors inline at the call site.
418
+ function optionalStatus(boundSession) {
419
+ try { return partnerStatus(boundSession); } catch (e) { return null; }
420
+ }
421
+
422
+ function toolHandlers(boundSession) {
423
+ return {
424
+ status: function () {
425
+ try { return toolResult(partnerStatus(boundSession)); }
426
+ catch (e) { return toolError(e.message || String(e)); }
427
+ },
428
+ replace: function (args) {
429
+ try {
430
+ return Promise.resolve(replacePartner(args || {}, boundSession))
431
+ .then(toolResult, function (e) { return toolError(e.message || String(e)); });
432
+ } catch (e) { return toolError(e.message || String(e)); }
433
+ },
434
+ evaluate: function (args) {
435
+ try { return toolResult(recordEvaluation(args || {}, boundSession)); }
436
+ catch (e) { return toolError(e.message || String(e)); }
437
+ },
438
+ };
439
+ }
440
+
441
+ return {
442
+ EVALUATION_OUTCOMES: EVALUATION_OUTCOMES,
443
+ optionalStatus: optionalStatus,
444
+ toolHandlers: toolHandlers,
445
+ closeGeneration: closeGeneration,
446
+ contextStatus: contextStatus,
447
+ ledgerFor: ledgerFor,
448
+ partnerStatus: partnerStatus,
449
+ recordEvaluation: recordEvaluation,
450
+ recordGenerationStart: recordGenerationStart,
451
+ replacePartner: replacePartner,
452
+ resolveDriverPair: resolveDriverPair,
453
+ };
454
+ }
455
+
456
+ module.exports = {
457
+ EVALUATION_OUTCOMES: EVALUATION_OUTCOMES,
458
+ attachPairLifecycle: attachPairLifecycle,
459
+ contextStatus: contextStatus,
460
+ };
@@ -78,7 +78,7 @@ function attachSessionHandoff(ctx) {
78
78
  ) || null;
79
79
  var target = sm.createSessionRaw({
80
80
  ownerId: source.ownerId || null,
81
- sessionVisibility: source.sessionVisibility || "shared",
81
+ sessionVisibility: "private",
82
82
  vendor: targetVendor,
83
83
  model: targetModel || null,
84
84
  effort: targetEffort,
@@ -1,4 +1,5 @@
1
1
  var sessionNotesMcp = require("./session-notes-mcp-server");
2
+ var lifecycle = require("./notes-lifecycle");
2
3
 
3
4
  var MAX_NOTE_TEXT_CHARS = 20000;
4
5
  var MAX_ACTIVE_NOTES = 20;
@@ -20,10 +21,16 @@ function toolError(message) {
20
21
  });
21
22
  }
22
23
 
24
+ // The active board is the open notes: the ones still asking for action. Closed
25
+ // notes remain on record and are reachable by asking for them explicitly.
23
26
  function activeNotes(notes) {
24
- return (notes || []).filter(function (note) {
25
- return note && !note.hidden;
26
- });
27
+ return lifecycle.openNotes(notes);
28
+ }
29
+
30
+ function notesForState(notes, state) {
31
+ if (state === "closed") return lifecycle.closedNotes(notes);
32
+ if (state === "all") return (notes || []).filter(Boolean);
33
+ return activeNotes(notes);
27
34
  }
28
35
 
29
36
  function memoryNotes(notes) {
@@ -39,6 +46,8 @@ function publicNote(note) {
39
46
  color: note.color,
40
47
  updatedAt: note.updatedAt,
41
48
  origin: note.origin || null,
49
+ state: lifecycle.stateOf(note),
50
+ closedAt: note.closedAt || null,
42
51
  };
43
52
  }
44
53
 
@@ -122,7 +131,9 @@ function attachSessionNotes(ctx) {
122
131
 
123
132
  function listNotes(args, caller) {
124
133
  if (!caller) return toolError("list_notes requires a session-bound tool server");
125
- return toolResult(activeNotes(nm.list()).map(publicNote));
134
+ var state = args && typeof args.state === "string" ? args.state : "open";
135
+ if (["open", "closed", "all"].indexOf(state) === -1) return toolError("state must be open, closed, or all");
136
+ return toolResult(notesForState(nm.list(), state).map(publicNote));
126
137
  }
127
138
 
128
139
  function writeNote(args, caller) {
@@ -145,7 +156,7 @@ function attachSessionNotes(ctx) {
145
156
  return toolResult(publicNote(updated));
146
157
  }
147
158
  if (activeNotes(notes).length >= MAX_ACTIVE_NOTES) {
148
- return toolError("20 active notes already exist; consolidate or remove stale notes before creating another");
159
+ return toolError("20 open notes already exist; consolidate or close resolved notes before creating another");
149
160
  }
150
161
  var placement = autoPlacement(notes);
151
162
  var created = nm.create({
@@ -161,16 +172,51 @@ function attachSessionNotes(ctx) {
161
172
  return toolResult(publicNote(created));
162
173
  }
163
174
 
164
- function removeNote(args, caller) {
165
- if (!caller) return toolError("remove_note requires a session-bound tool server");
166
- var note = findNote(nm.list() || [], args.id);
167
- if (!note) return toolError("note not found: " + (args.id || "unknown"));
175
+ // A session may only act on notes it created. Ownership is checked against the
176
+ // note's recorded origin and the server-bound caller, never against anything
177
+ // in the arguments.
178
+ function ownedNote(args, caller, verb) {
179
+ var note = findNote(nm.list() || [], args && args.id);
180
+ if (!note) return { error: "note not found: " + ((args && args.id) || "unknown") };
168
181
  if (!note.origin || note.origin.sessionId !== caller.localId) {
169
- return toolError("this session can only remove notes it created");
182
+ return { error: "this session can only " + verb + " notes it created" };
170
183
  }
171
- if (!nm.remove(note.id)) return toolError("note could not be removed: " + note.id);
172
- send({ type: "note_deleted", id: note.id });
173
- return toolResult({ removed: true, id: note.id });
184
+ return { note: note };
185
+ }
186
+
187
+ function closeNote(args, caller) {
188
+ if (!caller) return toolError("close_note requires a session-bound tool server");
189
+ var found = ownedNote(args, caller, "close");
190
+ if (found.error) return toolError(found.error);
191
+ // Actor provenance comes from the bound session object, not the payload.
192
+ var closed = nm.close(found.note.id, lifecycle.sessionActor(caller));
193
+ if (!closed) return toolError("note could not be closed: " + found.note.id);
194
+ send({ type: "note_updated", note: closed });
195
+ return toolResult(publicNote(closed));
196
+ }
197
+
198
+ function reopenNote(args, caller) {
199
+ if (!caller) return toolError("reopen_note requires a session-bound tool server");
200
+ var found = ownedNote(args, caller, "reopen");
201
+ if (found.error) return toolError(found.error);
202
+ var reopened = nm.reopen(found.note.id);
203
+ if (!reopened) return toolError("note could not be reopened: " + found.note.id);
204
+ send({ type: "note_updated", note: reopened });
205
+ return toolResult(publicNote(reopened));
206
+ }
207
+
208
+ // Deprecated compatibility path. An older caller asking to remove a note gets
209
+ // a close instead, so no existing agent can destroy a record. The response
210
+ // still reports `removed: true` because that is the shape the old caller
211
+ // parses, and `state` says what actually happened.
212
+ function removeNote(args, caller) {
213
+ if (!caller) return toolError("remove_note requires a session-bound tool server");
214
+ var found = ownedNote(args, caller, "close");
215
+ if (found.error) return toolError(found.error);
216
+ var closed = nm.close(found.note.id, lifecycle.sessionActor(caller));
217
+ if (!closed) return toolError("note could not be closed: " + found.note.id);
218
+ send({ type: "note_updated", note: closed });
219
+ return toolResult({ removed: true, closed: true, deleted: false, id: closed.id, state: lifecycle.stateOf(closed) });
174
220
  }
175
221
 
176
222
  function getToolDefs(boundSession) {
@@ -178,6 +224,8 @@ function attachSessionNotes(ctx) {
178
224
  return sessionNotesMcp.getToolDefs({
179
225
  list: function (args) { return listNotes(args, boundSession || null); },
180
226
  write: function (args) { return writeNote(args, boundSession || null); },
227
+ close: function (args) { return closeNote(args, boundSession || null); },
228
+ reopen: function (args) { return reopenNote(args, boundSession || null); },
181
229
  remove: function (args) { return removeNote(args, boundSession || null); },
182
230
  });
183
231
  }
@@ -211,6 +259,9 @@ module.exports = {
211
259
  NOTES_LABEL: NOTES_LABEL,
212
260
  PROACTIVE_POLICY: PROACTIVE_POLICY,
213
261
  attachSessionNotes: attachSessionNotes,
262
+ activeNotes: activeNotes,
263
+ notesForState: notesForState,
264
+ publicNote: publicNote,
214
265
  autoPlacement: autoPlacement,
215
266
  buildNotesPrompt: buildNotesPrompt,
216
267
  composeSystemPrompts: composeSystemPrompts,