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.
- package/lib/background-task-timing.js +105 -0
- package/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +14 -1
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-vendor-login.js +20 -3
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +178 -6
- package/lib/public/app.js +58 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +163 -28
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +34 -1
- package/lib/public/index.html +45 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +109 -4
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +84 -7
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +46 -4
- package/lib/public/modules/app-projects.js +13 -8
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +21 -1
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -12
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +132 -46
- package/lib/sdk-message-processor.js +60 -14
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +124 -18
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +39 -0
- package/lib/sessions.js +61 -11
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +2 -2
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +81 -4
- package/lib/yoke/adapters/codex.js +63 -15
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/vendor-registry.js +1 -1
- 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:
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
182
|
+
return { error: "this session can only " + verb + " notes it created" };
|
|
170
183
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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,
|