clay-server 4.0.0-beta.13 → 4.0.0-beta.15

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 (52) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +229 -0
  3. package/lib/capsule-pig-logic.js +268 -0
  4. package/lib/capsule-server-runtimes.js +41 -0
  5. package/lib/capsule-tictactoe-logic.js +260 -0
  6. package/lib/capsules/pig/display.js +190 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +46 -0
  9. package/lib/capsules/tictactoe/manifest.json +9 -0
  10. package/lib/capsules/tictactoe/ui.json +203 -0
  11. package/lib/project-capsule-catalog.js +9 -1
  12. package/lib/project-connection.js +1 -1
  13. package/lib/project-log-feedback-delivery.js +76 -0
  14. package/lib/project-logs.js +4 -0
  15. package/lib/project-pair-lifecycle.js +7 -12
  16. package/lib/project-session-pair.js +29 -39
  17. package/lib/project-worker-proposal.js +203 -72
  18. package/lib/project.js +13 -0
  19. package/lib/public/css/capsule-ui.css +18 -0
  20. package/lib/public/css/home-session-actions.css +66 -0
  21. package/lib/public/css/home-sidebar.css +66 -0
  22. package/lib/public/css/mobile-nav.css +80 -0
  23. package/lib/public/css/sidebar.css +92 -0
  24. package/lib/public/css/worker-proposal.css +34 -1
  25. package/lib/public/modules/app-messages.js +14 -1
  26. package/lib/public/modules/home-conversations-sheet.js +102 -48
  27. package/lib/public/modules/home-session-actions.js +6 -0
  28. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  29. package/lib/public/modules/home-tool-frame.js +165 -0
  30. package/lib/public/modules/home-tools.js +129 -1
  31. package/lib/public/modules/session-hierarchy.js +54 -0
  32. package/lib/public/modules/sidebar-mobile.js +35 -6
  33. package/lib/public/modules/sidebar-session-hierarchy.js +206 -0
  34. package/lib/public/modules/sidebar-sessions.js +49 -7
  35. package/lib/public/modules/worker-proposal-state.js +16 -0
  36. package/lib/public/modules/worker-proposal.js +49 -12
  37. package/lib/sdk-bridge.js +16 -8
  38. package/lib/sdk-message-processor.js +34 -4
  39. package/lib/server-home-chat.js +12 -1
  40. package/lib/server-tools.js +97 -9
  41. package/lib/server.js +3 -0
  42. package/lib/session-driver-eligibility.js +20 -165
  43. package/lib/session-pair-factory.js +31 -28
  44. package/lib/session-pair-mcp-server.js +5 -9
  45. package/lib/session-pair-prompts.js +15 -15
  46. package/lib/session-provenance.js +119 -0
  47. package/lib/session-spawn-mcp-server.js +1 -1
  48. package/lib/sessions.js +40 -2
  49. package/lib/tools-registry.js +44 -10
  50. package/lib/ws-schema.js +8 -3
  51. package/lib/yoke/adapters/claude.js +12 -0
  52. package/package.json +1 -1
@@ -0,0 +1,203 @@
1
+ {
2
+ "type": "stack",
3
+ "props": {
4
+ "gap": "md"
5
+ },
6
+ "children": [
7
+ {
8
+ "type": "text",
9
+ "props": {
10
+ "text": {
11
+ "$bind": "$state.statusText",
12
+ "fallback": "You (X) to play."
13
+ },
14
+ "role": "output"
15
+ }
16
+ },
17
+ {
18
+ "type": "row",
19
+ "props": {
20
+ "gap": "sm"
21
+ },
22
+ "children": [
23
+ {
24
+ "type": "button",
25
+ "action": "mark",
26
+ "props": {
27
+ "label": {
28
+ "$bind": "$state.cell0",
29
+ "fallback": " "
30
+ },
31
+ "accessibleLabel": "Mark cell 0",
32
+ "variant": "secondary",
33
+ "disabled": "$state.cell0Disabled",
34
+ "args": {
35
+ "cell": 0
36
+ }
37
+ }
38
+ },
39
+ {
40
+ "type": "button",
41
+ "action": "mark",
42
+ "props": {
43
+ "label": {
44
+ "$bind": "$state.cell1",
45
+ "fallback": " "
46
+ },
47
+ "accessibleLabel": "Mark cell 1",
48
+ "variant": "secondary",
49
+ "disabled": "$state.cell1Disabled",
50
+ "args": {
51
+ "cell": 1
52
+ }
53
+ }
54
+ },
55
+ {
56
+ "type": "button",
57
+ "action": "mark",
58
+ "props": {
59
+ "label": {
60
+ "$bind": "$state.cell2",
61
+ "fallback": " "
62
+ },
63
+ "accessibleLabel": "Mark cell 2",
64
+ "variant": "secondary",
65
+ "disabled": "$state.cell2Disabled",
66
+ "args": {
67
+ "cell": 2
68
+ }
69
+ }
70
+ }
71
+ ]
72
+ },
73
+ {
74
+ "type": "row",
75
+ "props": {
76
+ "gap": "sm"
77
+ },
78
+ "children": [
79
+ {
80
+ "type": "button",
81
+ "action": "mark",
82
+ "props": {
83
+ "label": {
84
+ "$bind": "$state.cell3",
85
+ "fallback": " "
86
+ },
87
+ "accessibleLabel": "Mark cell 3",
88
+ "variant": "secondary",
89
+ "disabled": "$state.cell3Disabled",
90
+ "args": {
91
+ "cell": 3
92
+ }
93
+ }
94
+ },
95
+ {
96
+ "type": "button",
97
+ "action": "mark",
98
+ "props": {
99
+ "label": {
100
+ "$bind": "$state.cell4",
101
+ "fallback": " "
102
+ },
103
+ "accessibleLabel": "Mark cell 4",
104
+ "variant": "secondary",
105
+ "disabled": "$state.cell4Disabled",
106
+ "args": {
107
+ "cell": 4
108
+ }
109
+ }
110
+ },
111
+ {
112
+ "type": "button",
113
+ "action": "mark",
114
+ "props": {
115
+ "label": {
116
+ "$bind": "$state.cell5",
117
+ "fallback": " "
118
+ },
119
+ "accessibleLabel": "Mark cell 5",
120
+ "variant": "secondary",
121
+ "disabled": "$state.cell5Disabled",
122
+ "args": {
123
+ "cell": 5
124
+ }
125
+ }
126
+ }
127
+ ]
128
+ },
129
+ {
130
+ "type": "row",
131
+ "props": {
132
+ "gap": "sm"
133
+ },
134
+ "children": [
135
+ {
136
+ "type": "button",
137
+ "action": "mark",
138
+ "props": {
139
+ "label": {
140
+ "$bind": "$state.cell6",
141
+ "fallback": " "
142
+ },
143
+ "accessibleLabel": "Mark cell 6",
144
+ "variant": "secondary",
145
+ "disabled": "$state.cell6Disabled",
146
+ "args": {
147
+ "cell": 6
148
+ }
149
+ }
150
+ },
151
+ {
152
+ "type": "button",
153
+ "action": "mark",
154
+ "props": {
155
+ "label": {
156
+ "$bind": "$state.cell7",
157
+ "fallback": " "
158
+ },
159
+ "accessibleLabel": "Mark cell 7",
160
+ "variant": "secondary",
161
+ "disabled": "$state.cell7Disabled",
162
+ "args": {
163
+ "cell": 7
164
+ }
165
+ }
166
+ },
167
+ {
168
+ "type": "button",
169
+ "action": "mark",
170
+ "props": {
171
+ "label": {
172
+ "$bind": "$state.cell8",
173
+ "fallback": " "
174
+ },
175
+ "accessibleLabel": "Mark cell 8",
176
+ "variant": "secondary",
177
+ "disabled": "$state.cell8Disabled",
178
+ "args": {
179
+ "cell": 8
180
+ }
181
+ }
182
+ }
183
+ ]
184
+ },
185
+ {
186
+ "type": "row",
187
+ "props": {
188
+ "gap": "sm"
189
+ },
190
+ "children": [
191
+ {
192
+ "type": "button",
193
+ "action": "reset",
194
+ "props": {
195
+ "label": "Reset",
196
+ "accessibleLabel": "Reset the game",
197
+ "variant": "ghost"
198
+ }
199
+ }
200
+ ]
201
+ }
202
+ ]
203
+ }
@@ -1,4 +1,12 @@
1
1
  // Bounded progressive-disclosure catalog for Capsules visible to a Mate.
2
+ //
3
+ // Skills are gated by Display: a Capsule the human cannot operate by hand is a
4
+ // Capsule no Mate may reach either, so an entry appears here only while its
5
+ // declarative Display floor validates. The gate reads the Capsule's own tree,
6
+ // carried alongside each manifest, and fails closed on a scan error, a missing
7
+ // ID, or a missing or invalid Display.
8
+
9
+ var capsuleFloor = require("./capsule-display-floor");
2
10
 
3
11
  var MAX_NAME_LENGTH = 80;
4
12
  var MAX_DESCRIPTION_LENGTH = 240;
@@ -35,7 +43,7 @@ function discoveryEntry(manifest) {
35
43
  function buildCapsuleCatalogPrompt(manifests) {
36
44
  if (!Array.isArray(manifests) || manifests.length === 0) return "";
37
45
  var sorted = manifests.filter(function (manifest) {
38
- return manifest && !manifest.error && manifest.id;
46
+ return capsuleFloor.hasUsableFloor(manifest);
39
47
  }).slice().sort(function (a, b) {
40
48
  var first = String(a.id);
41
49
  var second = String(b.id);
@@ -233,7 +233,7 @@ function attachConnection(ctx) {
233
233
  // so vendor badges showed Claude until the next broadcast).
234
234
  sendTo(ws, {
235
235
  type: "session_list",
236
- sessions: allSessions.map(function (s) { return sm.mapSessionForClient(s); }),
236
+ sessions: sm.mapSessionsForClient(allSessions),
237
237
  });
238
238
  if (_splitGroups) _splitGroups.sendConnectionState(ws);
239
239
 
@@ -0,0 +1,76 @@
1
+ // Deliver new Project Log comments to the exact live session responsible for
2
+ // the canonical entry. Comments remain durable in the ledger when that session
3
+ // is gone; this module never guesses another session or creates one.
4
+
5
+ function attachProjectLogFeedbackDelivery(ctx) {
6
+ var sm = ctx.sm;
7
+ var getSdk = ctx.getSdk;
8
+ var onProcessingChanged = ctx.onProcessingChanged || function () {};
9
+ var getLinuxUserForSession = ctx.getLinuxUserForSession || function () { return null; };
10
+
11
+ function findAuthorSession(entry) {
12
+ var author = entry && entry.updatedBy;
13
+ if (!author || author.type !== "session" || !author.sessionKey) return null;
14
+ var match = null;
15
+ sm.sessions.forEach(function (session) {
16
+ if (match || !session || session.destroying || session.hidden) return;
17
+ if (session.cliSessionId !== author.sessionKey && "local:" + session.localId !== author.sessionKey) return;
18
+ if ((session.ownerId || null) !== (author.userId || null)) return;
19
+ if (sm.sessions.get(session.localId) !== session) return;
20
+ match = session;
21
+ });
22
+ return match;
23
+ }
24
+
25
+ function feedbackPrompt(entry) {
26
+ return "[Project Log feedback]\n" +
27
+ "A user added a comment to " + entry.ref + ". Review the pending feedback now with list_log_feedback, " +
28
+ "then resolve this specific comment through review_log_comment. Apply supported corrections to the canonical " +
29
+ "entry; otherwise clarify or decline with a concise reason. Do not treat this internal notification as user " +
30
+ "chat or answer it only in the conversation.\n" +
31
+ "[End Project Log feedback]";
32
+ }
33
+
34
+ function deliver(entry) {
35
+ if (!entry || !entry.ref || !entry.pendingFeedbackCount) return false;
36
+ var session = findAuthorSession(entry);
37
+ var sdk = getSdk();
38
+ if (!session || !sdk) return false;
39
+ var text = feedbackPrompt(entry);
40
+ sm.sendAndRecord(session, {
41
+ type: "user_message",
42
+ text: text,
43
+ _internal: true,
44
+ projectLogFeedback: true,
45
+ projectLogRef: entry.ref,
46
+ });
47
+ session.lastActivity = Date.now();
48
+ if (!session.isProcessing) {
49
+ session.isProcessing = true;
50
+ session.sentToolResults = {};
51
+ onProcessingChanged();
52
+ sm.sendToSession(session, { type: "status", status: "processing" });
53
+ }
54
+ if (!sdk.pushMessage(session, text)) {
55
+ session._queryStartTs = Date.now();
56
+ Promise.resolve(sdk.startQuery(session, text, null, getLinuxUserForSession(session))).catch(function (error) {
57
+ session.isProcessing = false;
58
+ sm.sendAndRecord(session, {
59
+ type: "error",
60
+ text: "Could not deliver Project Log feedback: " + (error.message || String(error)),
61
+ _internal: true,
62
+ });
63
+ onProcessingChanged();
64
+ });
65
+ }
66
+ sm.broadcastSessionList();
67
+ return true;
68
+ }
69
+
70
+ return {
71
+ deliver: deliver,
72
+ findAuthorSession: findAuthorSession,
73
+ };
74
+ }
75
+
76
+ module.exports = { attachProjectLogFeedbackDelivery: attachProjectLogFeedbackDelivery };
@@ -88,6 +88,7 @@ function attachProjectLogs(ctx) {
88
88
  var sendTo = ctx.sendTo;
89
89
  var getClients = ctx.getClients || function () { return []; };
90
90
  var getProjectOwnerId = ctx.getProjectOwnerId || function () { return null; };
91
+ var onFeedback = ctx.onFeedback || function () {};
91
92
 
92
93
  // Every socket in this project's client set passed the project access check
93
94
  // at WebSocket upgrade, so a broadcast here is already scoped to authorized
@@ -204,6 +205,9 @@ function attachProjectLogs(ctx) {
204
205
  if (msg.type === "project_log_comment") {
205
206
  var commented = bound.commentLog({ ref: msg.ref, body: msg.body });
206
207
  sendTo(ws, { type: "project_log_commented", requestId: requestId, entry: commented });
208
+ try { onFeedback(commented); } catch (deliveryError) {
209
+ console.error("[project-logs] Failed to deliver comment:", deliveryError.message || deliveryError);
210
+ }
207
211
  return true;
208
212
  }
209
213
  } catch (e) {
@@ -1,13 +1,9 @@
1
- // Autonomous Split Worker lifecycle: bounded status, atomic replacement, and
2
- // the per-generation evaluation ledger.
3
- //
4
- // A qualified Driver manages its visible Worker without asking the user: it
5
- // decides reuse against replacement from exact server-derived accounting, and
6
- // it replaces in one operation rather than a close/create dance. Nothing here
7
- // posts a proposal or waits for an approval card.
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.
8
4
  //
9
5
  // Everything is bound to the exact live pair. The Driver is re-resolved from
10
- // the split store on every call, its eligibility is re-checked every time, and
6
+ // the split store on every call, its structural capability is re-checked every time, and
11
7
  // ownership must match on both sessions. No active-tab or global-session
12
8
  // fallback exists in this module.
13
9
  //
@@ -187,7 +183,7 @@ function attachPairLifecycle(ctx) {
187
183
  return record;
188
184
  }
189
185
 
190
- // The exact pair, with the Driver's eligibility re-checked on every call.
186
+ // The exact pair, with the Driver's structural capability re-checked on every call.
191
187
  function resolveDriverPair(caller) {
192
188
  if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
193
189
  // Exact live object identity, before anything is read off the caller. A
@@ -206,7 +202,7 @@ function attachPairLifecycle(ctx) {
206
202
  var worker = sm.sessions.get(group.pair.workerId);
207
203
  if (!worker) throw new Error("split partner session was not found");
208
204
  if ((caller.ownerId || null) !== (worker.ownerId || null)) throw new Error("split partner access denied");
209
- return { group: group, worker: worker, tier: verdict.tier };
205
+ return { group: group, worker: worker };
210
206
  }
211
207
 
212
208
  function replaceBlockedReason(worker) {
@@ -246,12 +242,11 @@ function attachPairLifecycle(ctx) {
246
242
  orchestration: turnControl.status(caller),
247
243
  replaceSafe: !blocked,
248
244
  replaceBlockedReason: blocked,
249
- driverTier: resolved.tier ? resolved.tier.name : null,
250
245
  generations: ledger,
251
246
  };
252
247
  }
253
248
 
254
- // Atomic replacement. Rejects an active Worker unless interrupt is
249
+ // Transactional replacement. Rejects an active Worker unless interrupt is
255
250
  // explicitly true, cancels anything the old Worker was waiting on, dissolves
256
251
  // the exact pair without deleting history, and creates a fresh Worker.
257
252
  // Idempotent in the sense that it either completes or leaves the existing
@@ -7,17 +7,13 @@ var driverEligibility = require("./session-driver-eligibility");
7
7
  var { attachWorkerPermission } = require("./project-worker-permission");
8
8
  var { attachWorkerProposal } = require("./project-worker-proposal");
9
9
  var MAX_RESPONSE_CHARS = 30000;
10
- function toolResult(value) {
11
- return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
12
- }
13
-
10
+ function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
14
11
  function toolError(err) {
15
12
  return Promise.resolve({
16
13
  content: [{ type: "text", text: "Error: " + (err.message || err) }],
17
14
  isError: true,
18
15
  });
19
16
  }
20
-
21
17
  function responseText(history, fromIndex) {
22
18
  var text = "";
23
19
  for (var i = Math.max(0, fromIndex || 0); i < history.length; i++) {
@@ -65,14 +61,14 @@ function attachSessionPair(ctx) {
65
61
  var workerPermission;
66
62
  var lifecycle;
67
63
  var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
68
- function groupAndPartner(caller) {
64
+ function groupAndPartner(caller, missingPairMessage) {
69
65
  if (!caller) throw new Error("partner tools require a session-bound tool server");
70
66
  // Captured handlers must remain bound to the exact live Session object.
71
67
  if (sm.sessions.get(caller.localId) !== caller) {
72
68
  throw new Error("this session is no longer live; the partner tools are bound to an exact session");
73
69
  }
74
70
  var group = store.groupForMember(caller.localId);
75
- if (!group) throw new Error("this session is not in a split group");
71
+ if (!group) throw new Error(missingPairMessage || "this session is not in a split group");
76
72
  if (group.pair && group.pair.driverId !== caller.localId) {
77
73
  throw new Error("only the configured Driver can direct this pair");
78
74
  }
@@ -102,7 +98,6 @@ function attachSessionPair(ctx) {
102
98
  broadcastDelegation(group, caller, partner, false);
103
99
  }
104
100
 
105
- // Shared resume path for delegated results and Worker permission routing.
106
101
  function resumeDriverWithMessage(caller, text, meta) {
107
102
  if (!caller || caller.destroying) return false;
108
103
  if (turnControl.blockedReason(caller)) return false;
@@ -160,7 +155,6 @@ function attachSessionPair(ctx) {
160
155
  });
161
156
  }
162
157
 
163
- // Give a waiting Driver its turn back so it can answer Worker permissions.
164
158
  function requestDetach(worker) {
165
159
  var token = worker && worker._pairDelegation;
166
160
  if (!token || token.detached || token.detachRequested) return false;
@@ -234,17 +228,11 @@ function attachSessionPair(ctx) {
234
228
  async function sendToPartner(args, caller) {
235
229
  try {
236
230
  if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
231
+ var resolved = groupAndPartner(caller,
232
+ "send_to_partner requires an exact existing Driver/Split Worker pair; call propose_worker while unpaired");
237
233
  var message = typeof args.message === "string" ? args.message.trim() : "";
238
234
  if (!message) throw new Error("message is required");
239
235
  turnControl.assertWorkerAction(caller);
240
- var created = null;
241
- if (caller && !store.groupForMember(caller.localId)) {
242
- var creationTicket = turnControl.reserveCreation(caller, "create");
243
- try { created = createWorkerForDriver(caller, args); }
244
- catch (createError) { turnControl.releaseCreation(creationTicket); throw createError; }
245
- }
246
- var resolved = groupAndPartner(caller);
247
- if (created && created.worker) lifecycle.recordGenerationStart(caller, created.worker);
248
236
  var wait = args.wait !== false;
249
237
  var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
250
238
  timeout = Math.max(1, Math.min(900, timeout));
@@ -282,13 +270,18 @@ function attachSessionPair(ctx) {
282
270
  if (!wait) {
283
271
  token.detached = true;
284
272
  monitorPartner(resolved.group, caller, partner, token);
285
- return toolResult({ status: "running", partnerId: partner.localId, workerCreated: !!created, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
273
+ return toolResult({ status: "running", partnerId: partner.localId, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
286
274
  }
287
- var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
288
- if (created) {
289
- completed.workerCreated = true;
290
- completed.partnerId = partner.localId;
275
+ if (!partner.isProcessing && !partner._queryStarting) {
276
+ finishDelegation(resolved.group, caller, partner, token);
277
+ var immediateFailure = errorSince(partner.history || [], token.startIndex);
278
+ return toolResult({
279
+ status: immediateFailure ? "error" : "complete",
280
+ response: responseText(partner.history || [], token.startIndex),
281
+ error: immediateFailure || undefined,
282
+ });
291
283
  }
284
+ var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
292
285
  return toolResult(completed);
293
286
  } catch (e) {
294
287
  if (caller) {
@@ -316,7 +309,6 @@ function attachSessionPair(ctx) {
316
309
  title: resolved.partner.title || "New Session",
317
310
  turns: count > 0 ? recentTurns(resolved.partner, count) : [],
318
311
  };
319
- // partner_status's bounded report, so reading turns needs no second call.
320
312
  payload.capacity = lifecycle.optionalStatus(caller);
321
313
  return toolResult(payload);
322
314
  } catch (e) {
@@ -349,8 +341,6 @@ function attachSessionPair(ctx) {
349
341
  if (partner.abortController) partner.abortController.abort();
350
342
  }
351
343
  if (partner._pairDelegation) finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
352
- // Anything the Worker was waiting on the Driver to decide dies with the
353
- // pair, immediately rather than on the next liveness sweep.
354
344
  workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
355
345
  var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
356
346
  var result = store.dissolve(ws, { id: resolved.group.id });
@@ -378,12 +368,14 @@ function attachSessionPair(ctx) {
378
368
  replace: function () { return toolError(new Error("replace_partner requires a session-bound tool server")); },
379
369
  evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
380
370
  });
381
- // Mount for the query lifetime; handlers re-resolve live pair state.
382
371
  var group = store.groupForMember(boundSession.localId);
383
372
  if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
384
- // Plain splits keep base tools; autonomous Drivers must pass the tier gate.
385
373
  var adHocSplit = !!(group && !group.pair);
386
- if (!adHocSplit && !driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
374
+ if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
375
+ if (!group) {
376
+ return workerProposal.getToolDefs(boundSession)
377
+ .concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
378
+ }
387
379
  var lifecycleHandlers = lifecycle.toolHandlers(boundSession);
388
380
  var tools = pairMcp.getToolDefs({
389
381
  send: function (args) {
@@ -397,17 +389,15 @@ function attachSessionPair(ctx) {
397
389
  status: lifecycleHandlers.status,
398
390
  replace: function (args) {
399
391
  return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
400
- return lifecycleHandlers.replace(args || {});
392
+ return workerProposal.proposeReplacement(args || {}, boundSession);
401
393
  });
402
394
  },
403
395
  evaluate: lifecycleHandlers.evaluate,
404
396
  }, { lifecycle: !adHocSplit });
405
397
  return tools
406
- .concat(workerPermission.getToolDefs(boundSession, { dormantDriver: !group }))
407
- .concat(workerProposal.getToolDefs(boundSession));
398
+ .concat(workerPermission.getToolDefs(boundSession, { dormantDriver: false }));
408
399
  }
409
400
 
410
- // Passed through as-is; re-wrapping drops fields. See session-pair-factory.js.
411
401
  var factory = attachPairFactory(ctx);
412
402
  var createPairRecord = factory.createPairRecord;
413
403
  var createWorkerForDriver = factory.createWorkerForDriver;
@@ -445,8 +435,11 @@ function attachSessionPair(ctx) {
445
435
  getLinuxUserForSession: ctx.getLinuxUserForSession,
446
436
  onProcessingChanged: ctx.onProcessingChanged,
447
437
  adapters: ctx.adapters,
438
+ dangerouslySkipPermissions: ctx.dangerouslySkipPermissions,
448
439
  createPairRecord: createPairRecord,
440
+ recordGenerationStart: function (driver, worker) { return lifecycle.recordGenerationStart(driver, worker); },
449
441
  sendToPartner: sendToPartner,
442
+ replacePartner: function (args, session) { return lifecycle.replacePartner(args, session); },
450
443
  });
451
444
 
452
445
  function handleMessage(ws, msg) {
@@ -474,8 +467,8 @@ function attachSessionPair(ctx) {
474
467
  var pairPrompt = "";
475
468
  if (group && group.pair && group.pair.driverId === session.localId) {
476
469
  pairPrompt = pairPrompts.DRIVER;
477
- } else if (!group && !ctx.isMate) {
478
- pairPrompt = pairPrompts.UNPAIRED;
470
+ } else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, sm)) {
471
+ pairPrompt = pairPrompts.UNPAIRED + " " + workerProposal.getSystemPrompt(session);
479
472
  }
480
473
  return pairPrompt;
481
474
  }
@@ -487,12 +480,9 @@ function attachSessionPair(ctx) {
487
480
  handleMessage: handleMessage,
488
481
  handleTurnDone: handleTurnDone,
489
482
  getSystemPrompt: getSystemPrompt,
483
+ respondToWorkerProposal: workerProposal.respondToProposal,
490
484
  workerPermission: workerPermission,
491
485
  };
492
486
  }
493
487
 
494
- module.exports = {
495
- attachSessionPair: attachSessionPair,
496
- responseText: responseText,
497
- recentTurns: recentTurns,
498
- };
488
+ module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };