clay-server 4.0.0-beta.13 → 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.js +4 -0
- package/lib/project-pair-lifecycle.js +4 -5
- package/lib/project-session-pair.js +1 -1
- package/lib/project.js +8 -0
- package/lib/sdk-bridge.js +8 -0
- package/lib/sdk-message-processor.js +34 -4
- package/lib/session-driver-eligibility.js +14 -167
- package/lib/session-pair-factory.js +4 -13
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/yoke/adapters/claude.js +12 -0
- package/package.json +1 -1
|
@@ -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 };
|
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
|
//
|
|
@@ -187,7 +187,7 @@ function attachPairLifecycle(ctx) {
|
|
|
187
187
|
return record;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
// The exact pair, with the Driver's
|
|
190
|
+
// The exact pair, with the Driver's structural capability re-checked on every call.
|
|
191
191
|
function resolveDriverPair(caller) {
|
|
192
192
|
if (!caller) throw new Error("pair lifecycle tools require a session-bound tool server");
|
|
193
193
|
// Exact live object identity, before anything is read off the caller. A
|
|
@@ -206,7 +206,7 @@ function attachPairLifecycle(ctx) {
|
|
|
206
206
|
var worker = sm.sessions.get(group.pair.workerId);
|
|
207
207
|
if (!worker) throw new Error("split partner session was not found");
|
|
208
208
|
if ((caller.ownerId || null) !== (worker.ownerId || null)) throw new Error("split partner access denied");
|
|
209
|
-
return { group: group, worker: worker
|
|
209
|
+
return { group: group, worker: worker };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
function replaceBlockedReason(worker) {
|
|
@@ -246,7 +246,6 @@ function attachPairLifecycle(ctx) {
|
|
|
246
246
|
orchestration: turnControl.status(caller),
|
|
247
247
|
replaceSafe: !blocked,
|
|
248
248
|
replaceBlockedReason: blocked,
|
|
249
|
-
driverTier: resolved.tier ? resolved.tier.name : null,
|
|
250
249
|
generations: ledger,
|
|
251
250
|
};
|
|
252
251
|
}
|
|
@@ -381,7 +381,7 @@ function attachSessionPair(ctx) {
|
|
|
381
381
|
// Mount for the query lifetime; handlers re-resolve live pair state.
|
|
382
382
|
var group = store.groupForMember(boundSession.localId);
|
|
383
383
|
if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
|
|
384
|
-
//
|
|
384
|
+
// Every project chat model may drive; the check only excludes non-chat surfaces.
|
|
385
385
|
var adHocSplit = !!(group && !group.pair);
|
|
386
386
|
if (!adHocSplit && !driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
387
387
|
var lifecycleHandlers = lifecycle.toolHandlers(boundSession);
|
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,
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -664,6 +664,14 @@ function createSDKBridge(opts) {
|
|
|
664
664
|
return { behavior: "allow", updatedInput: input };
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
+
// Project Logs are exact-session, project-bound tools whose authority is
|
|
668
|
+
// enforced again by the server binding. Canonical changes are append-only
|
|
669
|
+
// revisions, and the surface exposes no destructive delete operation, so
|
|
670
|
+
// a second provider permission prompt adds no additional safety.
|
|
671
|
+
if (toolName.indexOf("mcp__clay-logs__") === 0) {
|
|
672
|
+
return { behavior: "allow", updatedInput: input };
|
|
673
|
+
}
|
|
674
|
+
|
|
667
675
|
// This tool only prepares a read-only UI projection for the document the
|
|
668
676
|
// agent is about to edit. The subsequent Edit/Write keeps its own normal
|
|
669
677
|
// permission behavior.
|
|
@@ -3,6 +3,8 @@ var yoke = require("./yoke");
|
|
|
3
3
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
4
4
|
var sessionTitlePolicy = require("./session-title-policy");
|
|
5
5
|
|
|
6
|
+
var OVERLOAD_RETRY_LIMIT = 3;
|
|
7
|
+
|
|
6
8
|
function attachMessageProcessor(ctx) {
|
|
7
9
|
var sm = ctx.sm;
|
|
8
10
|
var send = ctx.send;
|
|
@@ -487,6 +489,8 @@ function attachMessageProcessor(ctx) {
|
|
|
487
489
|
}
|
|
488
490
|
|
|
489
491
|
} else if (parsed.yokeType === "result") {
|
|
492
|
+
var overloadAbortRequested = !!session._overloadAbortRequested;
|
|
493
|
+
session._overloadAbortRequested = false;
|
|
490
494
|
session.blocks = {};
|
|
491
495
|
session.sentToolResults = {};
|
|
492
496
|
session.pendingPermissions = {};
|
|
@@ -532,7 +536,9 @@ function attachMessageProcessor(ctx) {
|
|
|
532
536
|
session._awaitingTurnResult = false;
|
|
533
537
|
session._queuedTurnCount = 0;
|
|
534
538
|
onProcessingChanged();
|
|
535
|
-
|
|
539
|
+
if (!overloadAbortRequested) {
|
|
540
|
+
sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
|
|
541
|
+
}
|
|
536
542
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
537
543
|
sm.broadcastSessionList();
|
|
538
544
|
return;
|
|
@@ -819,9 +825,33 @@ function attachMessageProcessor(ctx) {
|
|
|
819
825
|
}
|
|
820
826
|
|
|
821
827
|
} else if (parsed.yokeType === "api_retry") {
|
|
822
|
-
//
|
|
823
|
-
|
|
824
|
-
|
|
828
|
+
// The Claude SDK may otherwise paint one persistent red error for every
|
|
829
|
+
// retry and remain busy for many minutes. Show one transient status, and
|
|
830
|
+
// stop a repeatedly overloaded turn after a small bounded number of
|
|
831
|
+
// attempts so the user regains control and can retry or change models.
|
|
832
|
+
var apiRetryAttempt = parsed.attempt || 1;
|
|
833
|
+
if (parsed.error === "overloaded") {
|
|
834
|
+
if (apiRetryAttempt === 1) {
|
|
835
|
+
session._overloadAbortRequested = false;
|
|
836
|
+
sendToSession(session, {
|
|
837
|
+
type: "system_info",
|
|
838
|
+
text: "The model is overloaded. Retrying briefly...",
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (apiRetryAttempt >= OVERLOAD_RETRY_LIMIT && !session._overloadAbortRequested) {
|
|
842
|
+
session._overloadAbortRequested = true;
|
|
843
|
+
sendAndRecord(session, {
|
|
844
|
+
type: "error",
|
|
845
|
+
text: "The model remained overloaded after " + apiRetryAttempt +
|
|
846
|
+
" attempts, so this turn was stopped. Try again or choose another model.",
|
|
847
|
+
});
|
|
848
|
+
if (session.abortController && typeof session.abortController.abort === "function") {
|
|
849
|
+
try { session.abortController.abort(); } catch (e) {}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} else if (apiRetryAttempt === 1) {
|
|
853
|
+
sendToSession(session, { type: "system_info", text: "The model request failed temporarily. Retrying..." });
|
|
854
|
+
}
|
|
825
855
|
|
|
826
856
|
} else if (parsed.yokeType === "commands_changed") {
|
|
827
857
|
// Mid-session command list change: rebuild the union with skills (same as
|
|
@@ -1,183 +1,30 @@
|
|
|
1
|
-
// Driver
|
|
2
|
-
// Driver/Split Worker pair and receive the autonomous lifecycle tools.
|
|
1
|
+
// Driver capability for a visible Driver/Split Worker pair.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// project
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// claude family haiku 1 < sonnet 2 < opus 3 < fable 4
|
|
15
|
-
// real ids: claude-haiku-4-5-20251001, claude-sonnet-5,
|
|
16
|
-
// claude-opus-5, claude-fable-5
|
|
17
|
-
// Driver threshold: fable
|
|
18
|
-
//
|
|
19
|
-
// codex family gpt-5.2 1 < gpt-5.5 2 < luna 3 < sol 4 < terra 5
|
|
20
|
-
// real ids: the CODEX_MODELS catalog, strongest first
|
|
21
|
-
// Driver threshold: sol
|
|
22
|
-
//
|
|
23
|
-
// Matching is by token, not by exact id, so a later release in the same family
|
|
24
|
-
// (claude-fable-6, gpt-5.7-terra) stays eligible without a code change. A
|
|
25
|
-
// model in a known family whose tier token is unrecognized is NOT eligible:
|
|
26
|
-
// unknown capability fails closed rather than inheriting the threshold.
|
|
27
|
-
//
|
|
28
|
-
// Any other vendor is not eligible for the Driver role at all, because no
|
|
29
|
-
// equivalent tier metadata exists to compare against. Those vendors remain
|
|
30
|
-
// perfectly usable as Workers.
|
|
31
|
-
|
|
32
|
-
var models = require("./project-models");
|
|
33
|
-
|
|
34
|
-
var FAMILIES = {
|
|
35
|
-
claude: {
|
|
36
|
-
label: "Claude",
|
|
37
|
-
threshold: 4,
|
|
38
|
-
thresholdName: "Fable",
|
|
39
|
-
// Longest/most specific tokens first so "haiku" cannot shadow a future
|
|
40
|
-
// compound name. Each entry is a token searched in id and display name.
|
|
41
|
-
tiers: [
|
|
42
|
-
{ token: "fable", rank: 4, name: "Fable" },
|
|
43
|
-
{ token: "opus", rank: 3, name: "Opus" },
|
|
44
|
-
{ token: "sonnet", rank: 2, name: "Sonnet" },
|
|
45
|
-
{ token: "haiku", rank: 1, name: "Haiku" },
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
codex: {
|
|
49
|
-
label: "OpenAI",
|
|
50
|
-
threshold: 4,
|
|
51
|
-
thresholdName: "Sol",
|
|
52
|
-
tiers: [
|
|
53
|
-
{ token: "terra", rank: 5, name: "Terra" },
|
|
54
|
-
{ token: "sol", rank: 4, name: "Sol" },
|
|
55
|
-
{ token: "luna", rank: 3, name: "Luna" },
|
|
56
|
-
{ token: "gpt-5.5", rank: 2, name: "GPT-5.5" },
|
|
57
|
-
{ token: "gpt-5.2", rank: 1, name: "GPT-5.2" },
|
|
58
|
-
],
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function familyFor(vendor) {
|
|
63
|
-
var key = String(vendor || "").toLowerCase();
|
|
64
|
-
return Object.prototype.hasOwnProperty.call(FAMILIES, key) ? FAMILIES[key] : null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Every string the catalog knows this model by. The catalog entry is found with
|
|
68
|
-
// the repo's own matcher so an alias ("fable"), an id, or a resolvedModel all
|
|
69
|
-
// resolve to the same entry, and the display name participates in tier
|
|
70
|
-
// detection exactly as project-worker-proposal.js already relies on.
|
|
71
|
-
function searchTextFor(vendor, model, modelsByVendor) {
|
|
72
|
-
var parts = [String(model || "")];
|
|
73
|
-
var catalog = (modelsByVendor && modelsByVendor[vendor]) || [];
|
|
74
|
-
for (var i = 0; i < catalog.length; i++) {
|
|
75
|
-
if (!models.modelEntryMatches(catalog[i], model)) continue;
|
|
76
|
-
var entry = catalog[i];
|
|
77
|
-
if (typeof entry === "string") {
|
|
78
|
-
parts.push(entry);
|
|
79
|
-
} else {
|
|
80
|
-
parts.push(entry.value || "");
|
|
81
|
-
parts.push(entry.id || "");
|
|
82
|
-
parts.push(entry.resolvedModel || "");
|
|
83
|
-
parts.push(entry.displayName || "");
|
|
84
|
-
parts.push(entry.name || "");
|
|
85
|
-
}
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
return parts.join(" ").toLowerCase();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Resolve the tier of one model inside its vendor family. Returns null when
|
|
92
|
-
// the vendor has no tier metadata or the tier token is unrecognized.
|
|
93
|
-
function resolveTier(vendor, model, modelsByVendor) {
|
|
94
|
-
var family = familyFor(vendor);
|
|
95
|
-
if (!family || !model) return null;
|
|
96
|
-
var text = searchTextFor(vendor, model, modelsByVendor);
|
|
97
|
-
for (var i = 0; i < family.tiers.length; i++) {
|
|
98
|
-
if (text.indexOf(family.tiers[i].token) !== -1) {
|
|
99
|
-
return {
|
|
100
|
-
vendor: String(vendor).toLowerCase(),
|
|
101
|
-
family: family.label,
|
|
102
|
-
rank: family.tiers[i].rank,
|
|
103
|
-
name: family.tiers[i].name,
|
|
104
|
-
threshold: family.threshold,
|
|
105
|
-
thresholdName: family.thresholdName,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// The hard invariant. Returns { ok, tier, error } and never throws. `error` is
|
|
113
|
-
// a complete English sentence suitable for returning to a tool caller.
|
|
114
|
-
function evaluateDriverModel(vendor, model, modelsByVendor) {
|
|
115
|
-
var family = familyFor(vendor);
|
|
116
|
-
if (!family) {
|
|
117
|
-
return {
|
|
118
|
-
ok: false,
|
|
119
|
-
tier: null,
|
|
120
|
-
error: "The Driver role requires a Claude model of Fable tier or higher, or an OpenAI model of " +
|
|
121
|
-
"Sol tier or higher. Vendor \"" + String(vendor || "unknown") + "\" has no comparable tier " +
|
|
122
|
-
"metadata, so it cannot take the Driver role. It can still be used for the Split Worker.",
|
|
123
|
-
};
|
|
3
|
+
// Model and vendor choice belongs to the user. A session does not need to
|
|
4
|
+
// clear a model-tier threshold to coordinate a Split Worker; runtime catalog
|
|
5
|
+
// validation belongs to session-pair-factory.js and applies equally to every
|
|
6
|
+
// requested role. The only session-level exclusion here is structural: an
|
|
7
|
+
// embedded terminal has no project chat surface on which to host the Driver
|
|
8
|
+
// controls or the paired pane.
|
|
9
|
+
|
|
10
|
+
function evaluateDriverSession(session) {
|
|
11
|
+
if (!session) {
|
|
12
|
+
return { ok: false, error: "No session was bound to this request." };
|
|
124
13
|
}
|
|
125
|
-
if (!model) {
|
|
126
|
-
return {
|
|
127
|
-
ok: false,
|
|
128
|
-
tier: null,
|
|
129
|
-
error: "This session has no resolved model yet, so its " + family.label +
|
|
130
|
-
" tier cannot be confirmed. The Driver role requires " + family.thresholdName +
|
|
131
|
-
" tier or higher.",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
var tier = resolveTier(vendor, model, modelsByVendor);
|
|
135
|
-
if (!tier) {
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
tier: null,
|
|
139
|
-
error: "Model \"" + model + "\" is not a recognized " + family.label +
|
|
140
|
-
" tier, so it cannot take the Driver role. The Driver role requires " +
|
|
141
|
-
family.thresholdName + " tier or higher.",
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (tier.rank < tier.threshold) {
|
|
145
|
-
return {
|
|
146
|
-
ok: false,
|
|
147
|
-
tier: tier,
|
|
148
|
-
error: "The Driver role requires a " + family.label + " model of " + family.thresholdName +
|
|
149
|
-
" tier or higher. This session is " + tier.name + " tier.",
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
return { ok: true, tier: tier, error: null };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// The session-level form. Resolves the session's effective model the same way
|
|
156
|
-
// the rest of the server does: the session's own model, else the vendor
|
|
157
|
-
// default. Identity is read from the session object, never from a caller.
|
|
158
|
-
function evaluateDriverSession(session, sm) {
|
|
159
|
-
if (!session) return { ok: false, tier: null, error: "No session was bound to this request." };
|
|
160
14
|
if (session.mode === "tui") {
|
|
161
15
|
return {
|
|
162
16
|
ok: false,
|
|
163
|
-
tier: null,
|
|
164
17
|
error: "An embedded terminal session cannot take the Driver role.",
|
|
165
18
|
};
|
|
166
19
|
}
|
|
167
|
-
|
|
168
|
-
var fallback = (sm && sm.defaultModelByVendor && sm.defaultModelByVendor[vendor]) || "";
|
|
169
|
-
var model = session.model || fallback;
|
|
170
|
-
return evaluateDriverModel(vendor, model, sm && sm.modelsByVendor);
|
|
20
|
+
return { ok: true, error: null };
|
|
171
21
|
}
|
|
172
22
|
|
|
173
|
-
function isEligibleDriverSession(session
|
|
174
|
-
return evaluateDriverSession(session
|
|
23
|
+
function isEligibleDriverSession(session) {
|
|
24
|
+
return evaluateDriverSession(session).ok;
|
|
175
25
|
}
|
|
176
26
|
|
|
177
27
|
module.exports = {
|
|
178
|
-
FAMILIES: FAMILIES,
|
|
179
|
-
evaluateDriverModel: evaluateDriverModel,
|
|
180
28
|
evaluateDriverSession: evaluateDriverSession,
|
|
181
29
|
isEligibleDriverSession: isEligibleDriverSession,
|
|
182
|
-
resolveTier: resolveTier,
|
|
183
30
|
};
|
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
//
|
|
23
23
|
// Two ordering rules matter and are load-bearing:
|
|
24
24
|
//
|
|
25
|
-
// 1. Nothing is created until everything is validated. Driver
|
|
25
|
+
// 1. Nothing is created until everything is validated. Driver capability and
|
|
26
26
|
// both runtime requests are settled first, so a rejected request leaves no
|
|
27
|
-
// orphan session behind.
|
|
28
|
-
//
|
|
29
|
-
// session exists.
|
|
27
|
+
// orphan session behind. Any installed, available model may be the Driver;
|
|
28
|
+
// model choice is the user's rather than a server-side tier policy.
|
|
30
29
|
// 2. `preflightRuntime` is side-effect free, so a caller that is about to
|
|
31
30
|
// destroy something (replacement dissolving a live pair) can validate the
|
|
32
31
|
// replacement first and abort while the old pair is still intact.
|
|
@@ -140,14 +139,6 @@ function attachPairFactory(ctx) {
|
|
|
140
139
|
groupName = undefined; // auto name from member titles
|
|
141
140
|
} else {
|
|
142
141
|
driverRuntime = preflightRuntime(driverSpec, "claude");
|
|
143
|
-
// The tier is judged on the model this session would really resolve to:
|
|
144
|
-
// the explicit request if there is one, else that vendor's default.
|
|
145
|
-
var effectiveDriverModel = driverRuntime.model ||
|
|
146
|
-
(sm.defaultModelByVendor && sm.defaultModelByVendor[driverRuntime.vendor]) || "";
|
|
147
|
-
var newVerdict = driverEligibility.evaluateDriverModel(
|
|
148
|
-
driverRuntime.vendor, effectiveDriverModel, sm.modelsByVendor
|
|
149
|
-
);
|
|
150
|
-
if (!newVerdict.ok) throw new Error(newVerdict.error);
|
|
151
142
|
groupName = "Agent pair";
|
|
152
143
|
}
|
|
153
144
|
|
|
@@ -202,7 +193,7 @@ function attachPairFactory(ctx) {
|
|
|
202
193
|
}
|
|
203
194
|
|
|
204
195
|
function createWorkerForDriver(driver, args) {
|
|
205
|
-
// Validates the Driver's
|
|
196
|
+
// Validates the Driver's capability and the whole Worker runtime before
|
|
206
197
|
// any session is made, so a rejection creates nothing.
|
|
207
198
|
var runtime = preflightWorkerForDriver(driver, args);
|
|
208
199
|
var ws = {
|
|
@@ -27,7 +27,7 @@ function getToolDefs(handlers) {
|
|
|
27
27
|
return [
|
|
28
28
|
{
|
|
29
29
|
name: "spawn_sessions",
|
|
30
|
-
description: "Create sibling work sessions
|
|
30
|
+
description: "Create ordinary sibling work sessions for background parallel work. This does not create a visible Driver/Split Worker pair and must not be used when the user asks for a Worker, paired pane, or split collaborator; use send_to_partner for that. Set forkFromCurrent to copy this session's full conversation context into every child. Children share the project's working directory, so prefer parallel analysis or other non-conflicting work over concurrent edits. Results can be polled with check_spawned_sessions; spawned children cannot spawn further sessions.",
|
|
31
31
|
inputSchema: buildShape({
|
|
32
32
|
sessions: { type: "string", description: "JSON array of objects: [{\"title\":\"Task title\",\"prompt\":\"Task instructions\"}]" },
|
|
33
33
|
vendor: { type: "string", description: "Optional vendor id. Defaults to the parent session vendor, then the project default." },
|
|
@@ -260,6 +260,18 @@ function flattenEvent(raw) {
|
|
|
260
260
|
base.status = raw.status;
|
|
261
261
|
return base;
|
|
262
262
|
}
|
|
263
|
+
// API retry notices are transient transport state, not conversation
|
|
264
|
+
// errors. Preserve the structured counters so the host can bound overload
|
|
265
|
+
// churn without recording one red error row per SDK retry.
|
|
266
|
+
if (raw.subtype === "api_retry") {
|
|
267
|
+
base.yokeType = "api_retry";
|
|
268
|
+
base.attempt = raw.attempt || 0;
|
|
269
|
+
base.maxRetries = raw.max_retries || 0;
|
|
270
|
+
base.retryDelayMs = raw.retry_delay_ms || 0;
|
|
271
|
+
base.error = raw.error || "unknown";
|
|
272
|
+
base.errorStatus = raw.error_status == null ? null : raw.error_status;
|
|
273
|
+
return base;
|
|
274
|
+
}
|
|
263
275
|
if (raw.subtype === "task_started") {
|
|
264
276
|
base.yokeType = "task_started";
|
|
265
277
|
base.parentToolId = raw.tool_use_id;
|
package/package.json
CHANGED