clay-server 4.0.0-beta.12 → 4.0.0-beta.14

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.
@@ -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 };
@@ -21,21 +21,22 @@ var LOGS_CONTRACT =
21
21
  "Do not log conversation summaries, task narration, restatements of the request, completed-work announcements, speculation, or anything the repository and its history already record. " +
22
22
  "Every write is attributed and permanently revision-tracked, so keep entries short, concrete, and true.";
23
23
 
24
- // Learning moments are a durable project asset, so capturing them is a default
25
- // rather than an option. The point is to help the person's own model get
26
- // sharper, not to replace their thinking: a concept they can name is a concept
27
- // they can reuse without asking again.
24
+ // User learning moments are a durable project asset, so capturing them is a
25
+ // default rather than an option. This category is about a change in the user's
26
+ // conceptual model, never knowledge the Driver acquired while doing the work.
28
27
  var LEARNING_CONTRACT =
29
- "Capture durable learning moments as Project Logs, normally under the category `learning`. " +
30
- "There are two kinds. First, the person asks a conceptual question directly and the answer is durable and relevant to this project. " +
31
- "Second, and easier to miss: the person describes something in their own approximate words and you identify the precise term, model, or mechanism behind it. " +
28
+ "Capture durable user learning moments as Project Logs, normally under the category `learning`. " +
29
+ "A learning entry is exclusively about the user's learning: the user must have engaged with a concept they did not previously know, or expressed an approximate mental model that you made more precise. It is never a record of something you, the Driver, learned or discovered while inspecting the project. " +
30
+ "There are two kinds. First, the user asks a conceptual question directly and the answer is durable and relevant to this project. " +
31
+ "Second, and easier to miss: the user describes something in their own approximate words and you identify the precise term, model, or mechanism behind it. " +
32
32
  "If someone says the background is transparent and blurry and you name that as backdrop blur, implemented with the CSS backdrop-filter property, that is a learning moment and it should not evaporate when the conversation scrolls away. " +
33
- "Record four things: the person's original wording or mental model, the precise concept it corresponds to, why and how it applies in this project, and any boundary or common misconception worth knowing. " +
33
+ "Record four things: the user's original wording or mental model, the precise concept it corresponds to, why and how it applies in this project, and any boundary or common misconception worth knowing. " +
34
34
  "Write the title and summary so they teach at a glance: someone reading only the ledger row should come away knowing the concept. " +
35
35
  "When it comes to learning, always capture once these criteria are met; treat it as the default rather than a judgement call. " +
36
36
  "Never fabricate a learning moment, and never claim someone learned something they did not actually engage with. " +
37
37
  "Attribute respectfully and factually: write that a concept was clarified in discussion. Never grade, rank, or characterise the person's knowledge. " +
38
- "Do not log routine command syntax, trivial confirmations, facts the person clearly already knows, or every explanation you happen to give. Capture when a conceptual model becomes measurably more precise. " +
38
+ "Do not classify engineering lessons, repository discoveries, investigation outcomes, defect causes, implementation insights, decisions, or facts you learned during the work as learning; use an appropriate category such as `investigation`, `defect`, `decision`, or `reference`. " +
39
+ "Do not log routine command syntax, trivial confirmations, facts the user clearly already knows, or every explanation you happen to give. Capture when the user's conceptual model becomes measurably more precise. " +
39
40
  "When new learning refines or supersedes an existing learning entry, revise that entry instead of adding a near-duplicate.";
40
41
 
41
42
  // Sticky Notes and Project Logs are different layers, and the failure mode is
@@ -71,9 +72,10 @@ var CATEGORY_DESCRIPTION = "Record category: a short lowercase hyphen-separated
71
72
  "Letters and digits in any script are accepted, so a project may keep its vocabulary in the language it works in. " +
72
73
  "This project's own vocabulary, not a fixed list. Call list_logs or search_logs first and reuse an established category when one fits; " +
73
74
  "coin a new concise one only when this project needs a durable distinction it does not yet have. " +
74
- "Common starting points are " + logsSchema.SEED_CATEGORIES.join(", ") + ". A category is dry metadata, never a persona or an identifier.";
75
+ "Common starting points are " + logsSchema.SEED_CATEGORIES.join(", ") + ". A category is dry metadata, never a persona or an identifier. " +
76
+ "Use `learning` only for a user learning moment described by the learning contract, never for knowledge or lessons acquired by the Driver.";
75
77
  var PRIORITY_DESCRIPTION = "How much this outranks routine work: " + logsSchema.PRIORITIES.join(", ") + ". Defaults to normal. Priority is independent of category, so an urgent decision is both.";
76
- var SUMMARY_DESCRIPTION = "One or two sentences explaining what was decided or done and why. This is what a reader sees in the ledger, so it must stand alone. For a learning entry, name the concept plainly enough that the row itself teaches it.";
78
+ var SUMMARY_DESCRIPTION = "One or two sentences explaining what was decided or done and why. This is what a reader sees in the ledger, so it must stand alone. For a learning entry, identify the concept the user engaged with plainly enough that the row itself teaches it.";
77
79
  var REF_DESCRIPTION = "Opaque log reference returned by list_logs, search_logs, or create_log.";
78
80
 
79
81
  function textResult(value) {
@@ -152,7 +154,7 @@ function projectTools(bound) {
152
154
  priority: { type: "string", enum: logsSchema.PRIORITIES, description: PRIORITY_DESCRIPTION },
153
155
  title: { type: "string", description: "Short factual title, plain text, written like a good commit subject." },
154
156
  summary: { type: "string", description: SUMMARY_DESCRIPTION },
155
- body: { type: "string", description: "The durable facts, context, and outcome in Markdown. No narration of your own activity. For a learning entry, cover the original wording or mental model, the precise concept, how it applies here, and any boundary or misconception." },
157
+ body: { type: "string", description: "The durable facts, context, and outcome in Markdown. No narration of your own activity. For a learning entry, cover the user's original wording or mental model, the precise concept, how it applies here, and any boundary or misconception." },
156
158
  tags: { type: "string", description: "Optional JSON array of short tag strings." },
157
159
  }, ["kind", "title", "summary"]),
158
160
  handler: handler(bound, "createLog"),
@@ -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,13 @@
1
1
  // Autonomous Split Worker lifecycle: bounded status, atomic replacement, and
2
2
  // the per-generation evaluation ledger.
3
3
  //
4
- // A qualified Driver manages its visible Worker without asking the user: it
4
+ // A Driver manages its visible Worker without asking the user: it
5
5
  // decides reuse against replacement from exact server-derived accounting, and
6
6
  // it replaces in one operation rather than a close/create dance. Nothing here
7
7
  // posts a proposal or waits for an approval card.
8
8
  //
9
9
  // 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
10
+ // the split store on every call, its structural capability is re-checked every time, and
11
11
  // ownership must match on both sessions. No active-tab or global-session
12
12
  // fallback exists in this module.
13
13
  //
@@ -130,6 +130,7 @@ function activityStatus(session) {
130
130
  function attachPairLifecycle(ctx) {
131
131
  var sm = ctx.sm;
132
132
  var store = ctx.splitStore;
133
+ var turnControl = ctx.turnControl;
133
134
 
134
135
  // Per-Driver ledger of the Worker generations it has run, newest last.
135
136
  // Lives on the live Driver session object: it informs the Driver's next
@@ -186,7 +187,7 @@ function attachPairLifecycle(ctx) {
186
187
  return record;
187
188
  }
188
189
 
189
- // The exact pair, with the Driver's eligibility re-checked on every call.
190
+ // The exact pair, with the Driver's structural capability re-checked on every call.
190
191
  function resolveDriverPair(caller) {
191
192
  if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
192
193
  // Exact live object identity, before anything is read off the caller. A
@@ -205,7 +206,7 @@ function attachPairLifecycle(ctx) {
205
206
  var worker = sm.sessions.get(group.pair.workerId);
206
207
  if (!worker) throw new Error("split partner session was not found");
207
208
  if ((caller.ownerId || null) !== (worker.ownerId || null)) throw new Error("split partner access denied");
208
- return { group: group, worker: worker, tier: verdict.tier };
209
+ return { group: group, worker: worker };
209
210
  }
210
211
 
211
212
  function replaceBlockedReason(worker) {
@@ -219,7 +220,7 @@ function attachPairLifecycle(ctx) {
219
220
  function partnerStatus(caller) {
220
221
  var resolved = resolveDriverPair(caller);
221
222
  var worker = resolved.worker;
222
- var blocked = replaceBlockedReason(worker);
223
+ var blocked = turnControl.blockedReason(caller) || replaceBlockedReason(worker);
223
224
  var ledger = ledgerFor(caller).map(function (record) {
224
225
  return {
225
226
  generation: record.generation,
@@ -242,9 +243,9 @@ function attachPairLifecycle(ctx) {
242
243
  activity: activityStatus(worker),
243
244
  context: contextStatus(worker),
244
245
  continuity: continuityStatus(worker),
246
+ orchestration: turnControl.status(caller),
245
247
  replaceSafe: !blocked,
246
248
  replaceBlockedReason: blocked,
247
- driverTier: resolved.tier ? resolved.tier.name : null,
248
249
  generations: ledger,
249
250
  };
250
251
  }
@@ -258,6 +259,7 @@ function attachPairLifecycle(ctx) {
258
259
  var resolved = resolveDriverPair(caller);
259
260
  var group = resolved.group;
260
261
  var oldWorker = resolved.worker;
262
+ turnControl.assertWorkerAction(caller);
261
263
 
262
264
  // Validate the replacement while the old pair is still fully intact. An
263
265
  // uninstalled vendor, an unavailable model or an unsupported effort must
@@ -275,6 +277,9 @@ function attachPairLifecycle(ctx) {
275
277
  "; call again with interrupt set to true to stop it first");
276
278
  }
277
279
 
280
+ if (args.evaluation) validateEvaluation(args.evaluation);
281
+ var creationTicket = turnControl.reserveCreation(caller, "replace");
282
+
278
283
  if (blocked) {
279
284
  oldWorker.taskStopRequested = true;
280
285
  if (oldWorker.abortController) {
@@ -290,15 +295,12 @@ function attachPairLifecycle(ctx) {
290
295
  ctx.cancelWorkerPermissions(oldWorker, "The Driver replaced this Split Worker.");
291
296
  }
292
297
 
293
- // The outgoing generation is closed only after the replacement exists, so a
294
- // rollback leaves its ledger entry open and its evaluation untouched. The
295
- // evaluation shape is still checked here, before anything is destroyed, so
296
- // a malformed assessment cannot dissolve the pair and then be rejected.
297
- if (args.evaluation) validateEvaluation(args.evaluation);
298
-
299
298
  var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
300
299
  var dissolved = store.dissolve(ws, { id: group.id });
301
- if (!dissolved.ok) throw new Error(dissolved.error || "could not dissolve the existing pair");
300
+ if (!dissolved.ok) {
301
+ turnControl.releaseCreation(creationTicket);
302
+ throw new Error(dissolved.error || "could not dissolve the existing pair");
303
+ }
302
304
 
303
305
  // History is preserved: the old Worker session stays in the project.
304
306
  //
@@ -334,6 +336,7 @@ function attachPairLifecycle(ctx) {
334
336
  var interruptNote = blocked
335
337
  ? " Its interrupted turn cannot be resumed, because stopping it was explicitly requested."
336
338
  : "";
339
+ turnControl.releaseCreation(creationTicket);
337
340
  throw new Error("could not create the replacement Split Worker: " + (e.message || String(e)) +
338
341
  ". " + restoreNote + interruptNote);
339
342
  }
@@ -2,12 +2,11 @@ var pairMcp = require("./session-pair-mcp-server");
2
2
  var pairPrompts = require("./session-pair-prompts");
3
3
  var { attachPairFactory } = require("./session-pair-factory");
4
4
  var { attachPairLifecycle } = require("./project-pair-lifecycle");
5
+ var { attachPairTurnControl } = require("./session-pair-turn-control");
5
6
  var driverEligibility = require("./session-driver-eligibility");
6
7
  var { attachWorkerPermission } = require("./project-worker-permission");
7
8
  var { attachWorkerProposal } = require("./project-worker-proposal");
8
-
9
9
  var MAX_RESPONSE_CHARS = 30000;
10
-
11
10
  function toolResult(value) {
12
11
  return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
13
12
  }
@@ -65,13 +64,10 @@ function attachSessionPair(ctx) {
65
64
  var workerProposal;
66
65
  var workerPermission;
67
66
  var lifecycle;
68
-
67
+ var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
69
68
  function groupAndPartner(caller) {
70
69
  if (!caller) throw new Error("partner tools require a session-bound tool server");
71
- // Exact live object identity. MCP tool handlers are captured per query and
72
- // outlive the session they were bound to, so a stale session object — or a
73
- // different object reusing the same localId — must not be able to drive a
74
- // pair through a handler someone still holds a reference to.
70
+ // Captured handlers must remain bound to the exact live Session object.
75
71
  if (sm.sessions.get(caller.localId) !== caller) {
76
72
  throw new Error("this session is no longer live; the partner tools are bound to an exact session");
77
73
  }
@@ -106,11 +102,10 @@ function attachSessionPair(ctx) {
106
102
  broadcastDelegation(group, caller, partner, false);
107
103
  }
108
104
 
109
- // Deliver an internal message to the Driver and make sure it is running so
110
- // the message is actually consumed. Shared by the delegated-result push-back
111
- // and by Split Worker permission routing, so both use one resume path.
105
+ // Shared resume path for delegated results and Worker permission routing.
112
106
  function resumeDriverWithMessage(caller, text, meta) {
113
107
  if (!caller || caller.destroying) return false;
108
+ if (turnControl.blockedReason(caller)) return false;
114
109
  var record = {
115
110
  type: "user_message",
116
111
  text: text,
@@ -165,9 +160,7 @@ function attachSessionPair(ctx) {
165
160
  });
166
161
  }
167
162
 
168
- // A Driver blocked inside a waiting send_to_partner cannot answer a tool
169
- // call. Asking the wait to detach lets its turn continue; the delegated
170
- // result then arrives through the existing detached push-back path.
163
+ // Give a waiting Driver its turn back so it can answer Worker permissions.
171
164
  function requestDetach(worker) {
172
165
  var token = worker && worker._pairDelegation;
173
166
  if (!token || token.detached || token.detachRequested) return false;
@@ -216,9 +209,7 @@ function attachSessionPair(ctx) {
216
209
  });
217
210
  return;
218
211
  }
219
- // Either the deadline passed, or something needs the Driver's turn
220
- // back before then (a Split Worker permission request). Both detach
221
- // the same way, so the completed result still returns automatically.
212
+ // Timeouts and permission requests both detach into the push-back path.
222
213
  if (Date.now() >= deadline || token.detachRequested) {
223
214
  clearInterval(timer);
224
215
  token.detached = true;
@@ -245,9 +236,12 @@ function attachSessionPair(ctx) {
245
236
  if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
246
237
  var message = typeof args.message === "string" ? args.message.trim() : "";
247
238
  if (!message) throw new Error("message is required");
239
+ turnControl.assertWorkerAction(caller);
248
240
  var created = null;
249
241
  if (caller && !store.groupForMember(caller.localId)) {
250
- created = createWorkerForDriver(caller, args);
242
+ var creationTicket = turnControl.reserveCreation(caller, "create");
243
+ try { created = createWorkerForDriver(caller, args); }
244
+ catch (createError) { turnControl.releaseCreation(creationTicket); throw createError; }
251
245
  }
252
246
  var resolved = groupAndPartner(caller);
253
247
  if (created && created.worker) lifecycle.recordGenerationStart(caller, created.worker);
@@ -367,6 +361,13 @@ function attachSessionPair(ctx) {
367
361
  }
368
362
  }
369
363
 
364
+ function handleHumanStop(session) {
365
+ var roles = turnControl.markHumanStop(session);
366
+ if (!roles) return false;
367
+ workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
368
+ return true;
369
+ }
370
+
370
371
  function getToolDefs(boundSession) {
371
372
  if (!boundSession) return pairMcp.getToolDefs({
372
373
  send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
@@ -377,36 +378,32 @@ function attachSessionPair(ctx) {
377
378
  replace: function () { return toolError(new Error("replace_partner requires a session-bound tool server")); },
378
379
  evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
379
380
  });
380
- // Mount whenever we have a bound session: MCP servers are fixed for the
381
- // lifetime of a query, and Claude queries live across turns, so gating on
382
- // group membership HERE would permanently hide the tools from a session
383
- // whose split is created mid-conversation (the ad-hoc flow). Handlers
384
- // re-resolve the group on every call, so an ungrouped session gets a
385
- // clear error, and a session that gains a partner later just works.
386
- // The one structural exclusion: a session that is ALREADY a configured
387
- // pair worker at query start never sees the tools.
381
+ // Mount for the query lifetime; handlers re-resolve live pair state.
388
382
  var group = store.groupForMember(boundSession.localId);
389
383
  if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
390
- // A plain side-by-side split has no Driver role, so it keeps exactly the
391
- // partner tools it always had. Every path that would make this session a
392
- // Driver — an existing configured pair, or an unpaired session whose
393
- // send_to_partner would create one — requires the hard tier invariant. A
394
- // model below its family's threshold never receives the autonomous
395
- // lifecycle tools and cannot create or direct a pair.
384
+ // Every project chat model may drive; the check only excludes non-chat surfaces.
396
385
  var adHocSplit = !!(group && !group.pair);
397
386
  if (!adHocSplit && !driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
398
387
  var lifecycleHandlers = lifecycle.toolHandlers(boundSession);
399
388
  var tools = pairMcp.getToolDefs({
400
- send: function (args) { return sendToPartner(args, boundSession); },
389
+ send: function (args) {
390
+ return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
391
+ return sendToPartner(args || {}, boundSession);
392
+ });
393
+ },
401
394
  read: function (args) { return readPartner(args, boundSession); },
402
395
  interrupt: function (args) { return interruptPartner(args, boundSession); },
403
396
  close: function (args) { return closePartner(args, boundSession); },
404
397
  status: lifecycleHandlers.status,
405
- replace: lifecycleHandlers.replace,
398
+ replace: function (args) {
399
+ return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
400
+ return lifecycleHandlers.replace(args || {});
401
+ });
402
+ },
406
403
  evaluate: lifecycleHandlers.evaluate,
407
404
  }, { lifecycle: !adHocSplit });
408
405
  return tools
409
- .concat(workerPermission.getToolDefs(boundSession))
406
+ .concat(workerPermission.getToolDefs(boundSession, { dormantDriver: !group }))
410
407
  .concat(workerProposal.getToolDefs(boundSession));
411
408
  }
412
409
 
@@ -427,6 +424,7 @@ function attachSessionPair(ctx) {
427
424
  cancelWorkerPermissions: function (worker, reason) {
428
425
  return workerPermission.cancelForSession(worker, reason);
429
426
  },
427
+ turnControl: turnControl,
430
428
  });
431
429
 
432
430
  workerPermission = attachWorkerPermission({
@@ -483,7 +481,9 @@ function attachSessionPair(ctx) {
483
481
  }
484
482
 
485
483
  return {
484
+ beginHumanTurn: turnControl.beginHumanTurn,
486
485
  getToolDefs: getToolDefs,
486
+ handleHumanStop: handleHumanStop,
487
487
  handleMessage: handleMessage,
488
488
  handleTurnDone: handleTurnDone,
489
489
  getSystemPrompt: getSystemPrompt,
@@ -1004,7 +1004,8 @@ function attachSessions(ctx) {
1004
1004
 
1005
1005
  if (msg.type === "stop") {
1006
1006
  var session = getSessionForWs(ws);
1007
- if (session && session.isProcessing) {
1007
+ if (session && (session.isProcessing || session._queryStarting)) {
1008
+ if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
1008
1009
  session.taskStopRequested = true;
1009
1010
  if (session.abortController) session.abortController.abort();
1010
1011
  }
@@ -331,6 +331,7 @@ function attachUserMessage(ctx) {
331
331
  });
332
332
  return true;
333
333
  }
334
+ if (typeof ctx.beginHumanPairTurn === "function") ctx.beginHumanPairTurn(session);
334
335
  if (gitAttribution) gitAttribution.beginTurn(session);
335
336
 
336
337
  // Bind vendor to session on first message (if not already set)
@@ -328,10 +328,11 @@ function attachWorkerPermission(ctx) {
328
328
  }
329
329
 
330
330
  // Exposed to the Driver only, and only while it really is a pair Driver.
331
- function getToolDefs(boundSession) {
331
+ function getToolDefs(boundSession, options) {
332
332
  if (!boundSession || !store) return [];
333
333
  var group = store.groupForMember(boundSession.localId);
334
- if (!group || !group.pair || group.pair.driverId !== boundSession.localId) return [];
334
+ var dormantDriver = !!(options && options.dormantDriver);
335
+ if (!dormantDriver && (!group || !group.pair || group.pair.driverId !== boundSession.localId)) return [];
335
336
  return [{
336
337
  name: "respond_to_worker_permission",
337
338
  description: "Approve or deny one tool-permission request raised by your paired Split Worker. " +
package/lib/project.js CHANGED
@@ -51,6 +51,7 @@ var { attachSessionDocument } = require("./project-session-document");
51
51
  var { attachCapsuleCatalog } = require("./project-capsule-catalog");
52
52
  var { attachProjectWorkspaceQuery } = require("./project-workspace-query");
53
53
  var { attachProjectLogs } = require("./project-logs");
54
+ var { attachProjectLogFeedbackDelivery } = require("./project-log-feedback-delivery");
54
55
  var { attachProjectMateKnowledge } = require("./project-mate-knowledge");
55
56
  var { attachSplitGroups } = require("./session-split-groups");
56
57
  var toolControlMcp = require("./tool-control-mcp-server");
@@ -670,6 +671,12 @@ function createProjectContext(opts) {
670
671
  isMate: isMate,
671
672
  mateId: mateId,
672
673
  });
674
+ var _logFeedbackDelivery = attachProjectLogFeedbackDelivery({
675
+ sm: sm,
676
+ getSdk: function () { return sdk; },
677
+ onProcessingChanged: onProcessingChanged,
678
+ getLinuxUserForSession: getLinuxUserForSession,
679
+ });
673
680
  var _projectLogs = attachProjectLogs({
674
681
  service: opts.projectLogsService || null,
675
682
  sm: sm,
@@ -679,6 +686,7 @@ function createProjectContext(opts) {
679
686
  mateId: mateId,
680
687
  sendTo: sendTo,
681
688
  getClients: function () { return clients; },
689
+ onFeedback: _logFeedbackDelivery.deliver,
682
690
  });
683
691
  var _mateKnowledge = attachProjectMateKnowledge({
684
692
  service: opts.mateKnowledgeService || null,
@@ -1599,6 +1607,7 @@ function createProjectContext(opts) {
1599
1607
  matesModule: matesModule,
1600
1608
  pushModule: pushModule,
1601
1609
  getSessionForWs: getSessionForWs,
1610
+ onHumanPairStop: function (session) { return _sessionPair.handleHumanStop(session); },
1602
1611
  getLinuxUserForSession: getLinuxUserForSession,
1603
1612
  ensureProjectAccessForSession: ensureProjectAccessForSession,
1604
1613
  getOsUserInfoForWs: getOsUserInfoForWs,
@@ -1674,6 +1683,7 @@ function createProjectContext(opts) {
1674
1683
  if (isMate || !_sessionPair.workerPermission) return false;
1675
1684
  return _sessionPair.workerPermission.isDriverOperated(session);
1676
1685
  },
1686
+ beginHumanPairTurn: function (session) { return _sessionPair.beginHumanTurn(session); },
1677
1687
  cwd: cwd,
1678
1688
  slug: slug,
1679
1689
  isMate: isMate,
package/lib/public/app.js CHANGED
@@ -344,8 +344,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
344
344
  projectLogsCommentStatusEl: null,
345
345
  projectLogsUnread: 0,
346
346
  projectLogsSeenRevisions: {},
347
- projectLogsPreview: false,
348
- projectLogsPinned: false,
349
347
  installedTools: [],
350
348
  homeToolRegistryLoaded: false,
351
349
  toolScanErrors: [],