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.
- package/lib/project-log-feedback-delivery.js +76 -0
- package/lib/project-logs-mcp-server.js +14 -12
- package/lib/project-logs.js +4 -0
- package/lib/project-pair-lifecycle.js +16 -13
- package/lib/project-session-pair.js +34 -34
- package/lib/project-sessions.js +2 -1
- package/lib/project-user-message.js +1 -0
- package/lib/project-worker-permission.js +3 -2
- package/lib/project.js +10 -0
- package/lib/public/app.js +0 -2
- package/lib/public/css/project-logs.css +25 -79
- package/lib/public/modules/project-logs-render.js +2 -2
- package/lib/public/modules/project-logs.js +62 -59
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/sdk-bridge.js +21 -19
- package/lib/sdk-message-processor.js +36 -7
- package/lib/server-home-chat.js +5 -0
- package/lib/session-driver-eligibility.js +14 -167
- package/lib/session-pair-factory.js +4 -13
- package/lib/session-pair-mcp-server.js +2 -0
- package/lib/session-pair-prompts.js +3 -3
- package/lib/session-pair-turn-control.js +153 -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/yoke/adapters/claude.js +12 -0
- package/package.json +1 -1
- package/lib/public/modules/project-logs-ambient.js +0 -238
|
@@ -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
|
-
//
|
|
25
|
-
// rather than an option.
|
|
26
|
-
//
|
|
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
|
-
"
|
|
31
|
-
"
|
|
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
|
|
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
|
|
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,
|
|
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"),
|
package/lib/project-logs.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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) {
|
|
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:
|
|
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,
|
package/lib/project-sessions.js
CHANGED
|
@@ -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
|
-
|
|
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: [],
|