clay-server 4.0.0-beta.9 → 4.0.0
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/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +185 -6
- package/lib/public/app.js +63 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +135 -46
- package/lib/sdk-message-processor.js +36 -7
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
|
@@ -9,14 +9,18 @@ var MEMORY_CONTRACT =
|
|
|
9
9
|
"Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. Include the observable evidence, affected component, likely impact, and a clear next action. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
|
|
10
10
|
"Important exception for deferred proposals: when you propose work (a fix, follow-up, improvement, or next step) and the user defers it rather than declining it (\"let's do that later\", \"next time\", \"after this\"), actively create a sticky note before the topic moves on. Do not wait to be asked. Capture what was proposed, why it mattered, and the agreed timing if any. Deferred agreements scroll out of chat history quickly and are hard to track; the board is where they survive. If the user declines the idea outright, do not write a note. " +
|
|
11
11
|
"Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
|
|
12
|
-
"Updates are visible too: update only when durable state materially changes
|
|
12
|
+
"Updates are visible too: update only when durable state materially changes. " +
|
|
13
|
+
"A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
|
|
14
|
+
"Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
|
|
13
15
|
|
|
14
16
|
function getToolDefs(handlers) {
|
|
15
17
|
return [
|
|
16
18
|
{
|
|
17
19
|
name: "list_notes",
|
|
18
|
-
description: MEMORY_CONTRACT + " List the
|
|
19
|
-
inputSchema: buildShape({
|
|
20
|
+
description: MEMORY_CONTRACT + " List the notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary. Lists open notes by default; pass state to see closed ones.",
|
|
21
|
+
inputSchema: buildShape({
|
|
22
|
+
state: { type: "string", enum: ["open", "closed", "all"], description: "Which notes to list. Defaults to open, the notes still asking for action." },
|
|
23
|
+
}),
|
|
20
24
|
handler: function (args) { return handlers.list(args || {}); },
|
|
21
25
|
},
|
|
22
26
|
{
|
|
@@ -29,11 +33,27 @@ function getToolDefs(handlers) {
|
|
|
29
33
|
}, ["text"]),
|
|
30
34
|
handler: function (args) { return handlers.write(args || {}); },
|
|
31
35
|
},
|
|
36
|
+
{
|
|
37
|
+
name: "close_note",
|
|
38
|
+
description: MEMORY_CONTRACT + " Close a note created by this same session once what it asked for is done. Closing is not deletion: the note leaves the active board, stays queryable under state closed, and can be reopened. Notes created by users or other sessions cannot be closed by this session.",
|
|
39
|
+
inputSchema: buildShape({
|
|
40
|
+
id: { type: "string", description: "Id of the note to close." },
|
|
41
|
+
}, ["id"]),
|
|
42
|
+
handler: function (args) { return handlers.close(args || {}); },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "reopen_note",
|
|
46
|
+
description: MEMORY_CONTRACT + " Reopen a closed note created by this same session when it turns out to still need action. The note returns to the active board unchanged.",
|
|
47
|
+
inputSchema: buildShape({
|
|
48
|
+
id: { type: "string", description: "Id of the note to reopen." },
|
|
49
|
+
}, ["id"]),
|
|
50
|
+
handler: function (args) { return handlers.reopen(args || {}); },
|
|
51
|
+
},
|
|
32
52
|
{
|
|
33
53
|
name: "remove_note",
|
|
34
|
-
description: MEMORY_CONTRACT + "
|
|
54
|
+
description: MEMORY_CONTRACT + " Deprecated: use close_note. This tool no longer deletes anything and now closes the note instead, so an older caller cannot destroy a record. Same ownership rule as close_note.",
|
|
35
55
|
inputSchema: buildShape({
|
|
36
|
-
id: { type: "string", description: "Id of the note to
|
|
56
|
+
id: { type: "string", description: "Id of the note to close. Nothing is deleted." },
|
|
37
57
|
}, ["id"]),
|
|
38
58
|
handler: function (args) { return handlers.remove(args || {}); },
|
|
39
59
|
},
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Split-group pair creation.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from project-session-pair.js, which operates a pair once it
|
|
4
|
+
// exists; this module is only about how one comes into being. Both the
|
|
5
|
+
// explicit "Add Split Worker", accepted proposal, and transactional replacement
|
|
6
|
+
// flows land here, so vendor validation, owner derivation and the group record
|
|
7
|
+
// are written in exactly one place.
|
|
8
|
+
//
|
|
9
|
+
// Ownership always comes from the connection or the Driver session, never from
|
|
10
|
+
// the incoming message.
|
|
11
|
+
//
|
|
12
|
+
// CONTEXT CONTRACT: this module reads `sm`, `splitStore`, `isMate`,
|
|
13
|
+
// `usersModule` and `sendTo` off its own argument, all at the top level.
|
|
14
|
+
// attachSessionPair's context already carries all five, so it is passed
|
|
15
|
+
// through unchanged. Re-wrapping it (it was once `{ sm, splitStore, ctx }`)
|
|
16
|
+
// leaves `isMate`, `usersModule` and `sendTo` undefined, which silently
|
|
17
|
+
// disables the Mate guard, breaks the pair_session_create reply, and throws
|
|
18
|
+
// "Cannot read properties of undefined (reading 'isMultiUser')" the moment a
|
|
19
|
+
// pair is created for an owned session — single-user installs escape it only
|
|
20
|
+
// because a Driver with no ownerId short-circuits that expression. Add any new
|
|
21
|
+
// dependency as a top-level field of that same context.
|
|
22
|
+
//
|
|
23
|
+
// Two ordering rules matter and are load-bearing:
|
|
24
|
+
//
|
|
25
|
+
// 1. Nothing is created until everything is validated. Driver capability and
|
|
26
|
+
// both runtime requests are settled first, so a rejected request leaves no
|
|
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.
|
|
29
|
+
// 2. `preflightRuntime` is side-effect free, so a caller that is about to
|
|
30
|
+
// destroy something (replacement dissolving a live pair) can validate the
|
|
31
|
+
// replacement first and abort while the old pair is still intact.
|
|
32
|
+
|
|
33
|
+
var driverEligibility = require("./session-driver-eligibility");
|
|
34
|
+
var models = require("./project-models");
|
|
35
|
+
var yoke = require("./yoke");
|
|
36
|
+
var sessionProvenance = require("./session-provenance");
|
|
37
|
+
|
|
38
|
+
function attachPairFactory(ctx) {
|
|
39
|
+
var sm = ctx.sm;
|
|
40
|
+
var store = ctx.splitStore;
|
|
41
|
+
|
|
42
|
+
function validateVendor(vendor) {
|
|
43
|
+
var installed = sm.installedVendors || [];
|
|
44
|
+
if (!vendor || installed.indexOf(vendor) === -1) throw new Error("vendor is not installed: " + (vendor || "unknown"));
|
|
45
|
+
return vendor;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// An explicitly requested model must exist in the server's own catalog for
|
|
49
|
+
// that vendor, matched through the repo's canonical matcher so an alias, an
|
|
50
|
+
// id or a resolvedModel all count. Caller text is never taken on trust: if
|
|
51
|
+
// the catalog is not populated we cannot confirm the model, so an explicit
|
|
52
|
+
// request fails closed rather than being forwarded blindly.
|
|
53
|
+
function validateModel(vendor, model) {
|
|
54
|
+
var requested = typeof model === "string" ? model.trim() : "";
|
|
55
|
+
if (!requested) return null;
|
|
56
|
+
var catalog = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
|
|
57
|
+
if (catalog.length === 0) {
|
|
58
|
+
throw new Error("no models are available yet for vendor \"" + vendor +
|
|
59
|
+
"\", so model \"" + requested + "\" cannot be confirmed");
|
|
60
|
+
}
|
|
61
|
+
for (var i = 0; i < catalog.length; i++) {
|
|
62
|
+
if (models.modelEntryMatches(catalog[i], requested)) return requested;
|
|
63
|
+
}
|
|
64
|
+
throw new Error("model is not available for vendor \"" + vendor + "\": " + requested);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateEffort(vendor, effort) {
|
|
68
|
+
var requested = typeof effort === "string" ? effort.trim() : "";
|
|
69
|
+
var fallback = (sm.currentEffortByVendor && sm.currentEffortByVendor[vendor]) || sm.currentEffort || "medium";
|
|
70
|
+
var clamped = yoke.clampEffort(vendor, requested || fallback) || null;
|
|
71
|
+
if (requested && !clamped) {
|
|
72
|
+
throw new Error("reasoning effort is not supported for vendor \"" + vendor + "\": " + requested);
|
|
73
|
+
}
|
|
74
|
+
return clamped;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Resolve and validate one session runtime request without creating
|
|
78
|
+
// anything. Throws on the first unusable field.
|
|
79
|
+
function preflightRuntime(spec, defaultVendor) {
|
|
80
|
+
var request = spec || {};
|
|
81
|
+
var vendor = validateVendor((typeof request.vendor === "string" && request.vendor.trim()) || defaultVendor);
|
|
82
|
+
return {
|
|
83
|
+
vendor: vendor,
|
|
84
|
+
model: validateModel(vendor, request.model),
|
|
85
|
+
effort: validateEffort(vendor, request.effort),
|
|
86
|
+
explicitEffort: !!(typeof request.effort === "string" && request.effort.trim()),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The default Worker vendor for an accepted proposal or replacement prefers
|
|
91
|
+
// a different engine from the Driver's own, so the pair does not double up.
|
|
92
|
+
function defaultWorkerVendorFor(driver) {
|
|
93
|
+
var installed = sm.installedVendors || [];
|
|
94
|
+
if (installed.length === 0) throw new Error("no coding agent is installed for a Split Worker session");
|
|
95
|
+
if (installed.indexOf("codex") !== -1 && driver.vendor !== "codex") return "codex";
|
|
96
|
+
for (var i = 0; i < installed.length; i++) {
|
|
97
|
+
if (installed[i] !== driver.vendor) return installed[i];
|
|
98
|
+
}
|
|
99
|
+
return installed[0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Side-effect-free check that this exact Driver could create this exact
|
|
103
|
+
// Worker right now. Used by replacement before it touches anything.
|
|
104
|
+
function preflightWorkerForDriver(driver, args) {
|
|
105
|
+
if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
|
|
106
|
+
if (!driver) throw new Error("driver session not found");
|
|
107
|
+
var verdict = driverEligibility.evaluateDriverSession(driver, sm);
|
|
108
|
+
if (!verdict.ok) throw new Error(verdict.error);
|
|
109
|
+
var options = args || {};
|
|
110
|
+
var vendor = typeof options.workerVendor === "string" ? options.workerVendor.trim() : "";
|
|
111
|
+
return preflightRuntime({
|
|
112
|
+
vendor: vendor || defaultWorkerVendorFor(driver),
|
|
113
|
+
model: options.workerModel,
|
|
114
|
+
effort: options.workerEffort,
|
|
115
|
+
}, null);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function createPairRecord(ws, msg) {
|
|
119
|
+
if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
|
|
120
|
+
var driverSpec = msg.driver || {};
|
|
121
|
+
var workerSpec = msg.worker || {};
|
|
122
|
+
var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
|
|
123
|
+
|
|
124
|
+
// --- Validate everything first; create nothing yet. ---
|
|
125
|
+
|
|
126
|
+
var workerRuntime = preflightRuntime(workerSpec, sm.lastVendor || "codex");
|
|
127
|
+
|
|
128
|
+
var existingDriver = null;
|
|
129
|
+
var driverRuntime = null;
|
|
130
|
+
var groupName;
|
|
131
|
+
if (Number.isInteger(driverSpec.sessionId)) {
|
|
132
|
+
// "Add Split Worker" on an existing session: the current session becomes
|
|
133
|
+
// the Driver with its full conversation context intact.
|
|
134
|
+
existingDriver = sm.sessions.get(driverSpec.sessionId);
|
|
135
|
+
if (!existingDriver) throw new Error("driver session not found");
|
|
136
|
+
if ((existingDriver.ownerId || null) !== ownerId) throw new Error("driver session access denied");
|
|
137
|
+
if (store.groupForMember(existingDriver.localId)) throw new Error("this session is already in a split group");
|
|
138
|
+
var existingVerdict = driverEligibility.evaluateDriverSession(existingDriver, sm);
|
|
139
|
+
if (!existingVerdict.ok) throw new Error(existingVerdict.error);
|
|
140
|
+
groupName = undefined; // auto name from member titles
|
|
141
|
+
} else {
|
|
142
|
+
driverRuntime = preflightRuntime(driverSpec, "claude");
|
|
143
|
+
groupName = "Agent pair";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Everything is valid; now create. ---
|
|
147
|
+
|
|
148
|
+
var driver = existingDriver;
|
|
149
|
+
var createdDriver = null;
|
|
150
|
+
if (!driver) {
|
|
151
|
+
driver = sm.createSessionRaw({
|
|
152
|
+
ownerId: ownerId,
|
|
153
|
+
vendor: driverRuntime.vendor,
|
|
154
|
+
model: driverRuntime.model,
|
|
155
|
+
effort: driverRuntime.effort,
|
|
156
|
+
});
|
|
157
|
+
createdDriver = driver;
|
|
158
|
+
driver.title = "Driver · " + ((yoke.getVendorInfo(driverRuntime.vendor) || {}).displayName || driverRuntime.vendor);
|
|
159
|
+
if (driverRuntime.explicitEffort) driver.loopSettings = { effort: driverRuntime.effort };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var worker = sm.createSessionRaw({
|
|
163
|
+
ownerId: ownerId,
|
|
164
|
+
vendor: workerRuntime.vendor,
|
|
165
|
+
model: workerRuntime.model,
|
|
166
|
+
effort: workerRuntime.effort,
|
|
167
|
+
});
|
|
168
|
+
worker.title = "Split Worker · " + ((yoke.getVendorInfo(workerRuntime.vendor) || {}).displayName || workerRuntime.vendor);
|
|
169
|
+
if (workerRuntime.explicitEffort) worker.loopSettings = { effort: workerRuntime.effort };
|
|
170
|
+
|
|
171
|
+
var result;
|
|
172
|
+
try {
|
|
173
|
+
// Creation provenance is durable and independent from the active split
|
|
174
|
+
// group. Persist both ends before the group's broadcast so the Worker
|
|
175
|
+
// never flashes as unrelated and survives a restart with its parentage.
|
|
176
|
+
sessionProvenance.markWorker(driver, worker, sm.sessions);
|
|
177
|
+
if (typeof sm.saveSessionFile === "function") {
|
|
178
|
+
sm.saveSessionFile(driver);
|
|
179
|
+
sm.saveSessionFile(worker);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
result = store.create(ws, {
|
|
183
|
+
members: [driver.localId, worker.localId],
|
|
184
|
+
pair: { driverId: driver.localId, workerId: worker.localId },
|
|
185
|
+
name: groupName,
|
|
186
|
+
});
|
|
187
|
+
if (!result.ok) throw new Error(result.error);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
// Any failure after session creation must remove exactly the sessions
|
|
190
|
+
// made by this call. An existing Driver is never removed.
|
|
191
|
+
removeCreated(worker);
|
|
192
|
+
removeCreated(createdDriver);
|
|
193
|
+
throw e;
|
|
194
|
+
}
|
|
195
|
+
sm.broadcastSessionList();
|
|
196
|
+
return { driver: driver, worker: worker, group: result.group };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function removeCreated(session) {
|
|
200
|
+
if (!session) return;
|
|
201
|
+
if (typeof sm.deleteSessionQuiet === "function") {
|
|
202
|
+
try { sm.deleteSessionQuiet(session.localId); return; } catch (e) {}
|
|
203
|
+
}
|
|
204
|
+
try { sm.sessions.delete(session.localId); } catch (e) {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createWorkerForDriver(driver, args) {
|
|
208
|
+
// Validates the Driver's capability and the whole Worker runtime before
|
|
209
|
+
// any session is made, so a rejection creates nothing.
|
|
210
|
+
var runtime = preflightWorkerForDriver(driver, args);
|
|
211
|
+
var ws = {
|
|
212
|
+
_clayActiveSession: driver.localId,
|
|
213
|
+
_clayUser: driver.ownerId ? { id: driver.ownerId } : null,
|
|
214
|
+
};
|
|
215
|
+
var created = createPairRecord(ws, {
|
|
216
|
+
driver: { sessionId: driver.localId },
|
|
217
|
+
worker: {
|
|
218
|
+
vendor: runtime.vendor,
|
|
219
|
+
model: runtime.model || "",
|
|
220
|
+
effort: runtime.explicitEffort ? (args && args.workerEffort) || "" : "",
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
sm.sendToSession(driver, { type: "pair_session_created", ok: true, group: created.group });
|
|
224
|
+
return created;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function createPair(ws, msg) {
|
|
228
|
+
try {
|
|
229
|
+
var created = createPairRecord(ws, msg);
|
|
230
|
+
ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
|
|
231
|
+
} catch (e) {
|
|
232
|
+
ctx.sendTo(ws, { type: "pair_session_created", ok: false, error: e.message || String(e) });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
createPair: createPair,
|
|
238
|
+
createPairRecord: createPairRecord,
|
|
239
|
+
createWorkerForDriver: createWorkerForDriver,
|
|
240
|
+
preflightRuntime: preflightRuntime,
|
|
241
|
+
preflightWorkerForDriver: preflightWorkerForDriver,
|
|
242
|
+
validateModel: validateModel,
|
|
243
|
+
validateVendor: validateVendor,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = { attachPairFactory: attachPairFactory };
|
|
@@ -2,26 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
var buildShape = require("./session-spawn-mcp-server").buildShape;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// `options.lifecycle` adds the autonomous management tools. They are omitted
|
|
6
|
+
// for a plain side-by-side split, which has no Driver role to exercise them.
|
|
7
|
+
function getToolDefs(handlers, options) {
|
|
8
|
+
var lifecycle = !!(options && options.lifecycle);
|
|
9
|
+
var defs = [
|
|
7
10
|
{
|
|
8
11
|
name: "send_to_partner",
|
|
9
|
-
description: "Delegate one concrete task to the visible Split Worker.
|
|
12
|
+
description: "Delegate one concrete follow-up task to the existing visible Split Worker. This tool is exposed only after the user has configured and accepted an exact pair. A completed turn does not end the Split Worker session: reuse the same Split Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
|
|
10
13
|
inputSchema: buildShape({
|
|
11
14
|
message: { type: "string", description: "The complete task or question for the partner." },
|
|
12
15
|
wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
|
|
13
16
|
timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
|
|
14
|
-
|
|
15
|
-
workerModel: { type: "string", description: "Optional Split Worker model to use only when creating a new pair." },
|
|
16
|
-
workerEffort: { type: "string", description: "Optional Split Worker reasoning effort to use only when creating a new pair." },
|
|
17
|
+
operationId: { type: "string", description: "Optional stable id for this delegation. Reusing it within the same human turn returns the original operation instead of sending twice." },
|
|
17
18
|
}, ["message"]),
|
|
18
19
|
handler: function (args) { return handlers.send(args || {}); },
|
|
19
20
|
},
|
|
20
21
|
{
|
|
21
22
|
name: "read_partner",
|
|
22
|
-
description: "Read the
|
|
23
|
+
description: "Read the Split Worker's current status and recent conversation turns, plus the same bounded capacity report partner_status returns. Use this for an interim check after a non-waiting delegation or timeout; completed results are pushed automatically.",
|
|
23
24
|
inputSchema: buildShape({
|
|
24
|
-
lastTurns: { type: "number", description: "Number of recent user-message-delimited turns, from
|
|
25
|
+
lastTurns: { type: "number", description: "Number of recent user-message-delimited turns, from 0 to 5. Use 0 for status only. Defaults to 1." },
|
|
25
26
|
}),
|
|
26
27
|
handler: function (args) { return handlers.read(args || {}); },
|
|
27
28
|
},
|
|
@@ -38,6 +39,43 @@ function getToolDefs(handlers) {
|
|
|
38
39
|
handler: function (args) { return handlers.close(args || {}); },
|
|
39
40
|
},
|
|
40
41
|
];
|
|
42
|
+
if (!lifecycle) return defs;
|
|
43
|
+
return defs.concat([
|
|
44
|
+
{
|
|
45
|
+
name: "partner_status",
|
|
46
|
+
description: "Bounded capacity and continuity report for your Split Worker, for deciding reuse against replacement: context tokens used and the ratio of its window, current task and activity, vendor/model/effort, history size and idle time, whether it is safe to replace right now, and your own recorded results for earlier Worker generations. Returns no transcript. Prefer this over reading turns when you only need to decide.",
|
|
47
|
+
inputSchema: buildShape({}),
|
|
48
|
+
handler: function (args) { return handlers.status(args || {}); },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "replace_partner",
|
|
52
|
+
description: "Propose replacing the current Split Worker with a fresh runtime. This non-mutating call shows a user-controlled vendor, model, and effort card. Only acceptance performs the transactional replacement and delegates the supplied task once; decline keeps the existing pair and resumes the Driver. The previous Worker's conversation remains browsable. An active Worker requires interrupt true.",
|
|
53
|
+
inputSchema: buildShape({
|
|
54
|
+
interrupt: { type: "boolean", description: "Stop the Worker first if it is mid-turn or holding a delegated task. Required to replace an active Worker." },
|
|
55
|
+
message: { type: "string", description: "Task to deliver exactly once after the user accepts the replacement." },
|
|
56
|
+
workerVendor: { type: "string", description: "Optional vendor for the new Worker. Must be installed and available." },
|
|
57
|
+
workerModel: { type: "string", description: "Optional model for the new Worker. Must be offered by that vendor." },
|
|
58
|
+
workerEffort: { type: "string", description: "Optional reasoning effort for the new Worker." },
|
|
59
|
+
recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended replacement vendor, model, and effort fit the next task." },
|
|
60
|
+
evaluation: {
|
|
61
|
+
type: "object",
|
|
62
|
+
description: "Optional bounded assessment of the Worker being replaced, recorded against that exact generation.",
|
|
63
|
+
},
|
|
64
|
+
operationId: { type: "string", description: "Optional stable id for this replacement. Reusing it within the same human turn returns the original operation instead of replacing twice." },
|
|
65
|
+
}, ["message", "recommendationRationale"]),
|
|
66
|
+
handler: function (args) { return handlers.replace(args || {}); },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "record_partner_evaluation",
|
|
70
|
+
description: "Record a bounded assessment of one Split Worker generation so your later model choices can use observed outcomes instead of re-reading transcripts. Records your judgement alongside the objective signals Clay already measured (turns, errors, interruption, context used). This is scoped to your own pair and forms no global model ranking.",
|
|
71
|
+
inputSchema: buildShape({
|
|
72
|
+
outcome: { type: "string", description: 'One of "succeeded", "partial", "failed", "abandoned".' },
|
|
73
|
+
note: { type: "string", description: "Optional short reason, up to 400 characters." },
|
|
74
|
+
generation: { type: "number", description: "Optional generation number. Defaults to the current Worker." },
|
|
75
|
+
}, ["outcome"]),
|
|
76
|
+
handler: function (args) { return handlers.evaluate(args || {}); },
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
41
79
|
}
|
|
42
80
|
|
|
43
81
|
module.exports = { getToolDefs: getToolDefs };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Driver-facing system prompt text for the visible Driver/Split Worker pair.
|
|
2
|
+
//
|
|
3
|
+
// Pure data, extracted from project-session-pair.js so that module stays under
|
|
4
|
+
// the size limit and so the guidance can be reviewed as prose.
|
|
5
|
+
|
|
6
|
+
var DRIVER_DELEGATION = [
|
|
7
|
+
"Your management objective: protect your own context, keep the Split Worker compact, and put execution where",
|
|
8
|
+
"it runs best. Delegate implementation-heavy work rather than doing it here. Treat work spanning multiple modules,",
|
|
9
|
+
"crossing client/server/data boundaries, requiring a migration, or containing independent investigation and",
|
|
10
|
+
"execution as implementation-heavy. Your own capability is not a reason to retain that execution: preserve",
|
|
11
|
+
"the Driver for decomposition, runtime selection, judgment, review, and integration unless the user explicitly",
|
|
12
|
+
"asks you to work directly or the work must remain sequential in this session.",
|
|
13
|
+
].join(" ").replace(/ {2,}/g, " ");
|
|
14
|
+
|
|
15
|
+
var DRIVER_CORE = [
|
|
16
|
+
"You are the Driver of a visible Driver/Split Worker pair, and you manage that Split Worker yourself.",
|
|
17
|
+
"The tools send_to_partner, read_partner, partner_status, replace_partner, interrupt_partner, close_partner,",
|
|
18
|
+
"and record_partner_evaluation are provided directly to you.",
|
|
19
|
+
"",
|
|
20
|
+
"Reuse the existing Split Worker",
|
|
21
|
+
"only when its accumulated context genuinely helps the next task; when it is context-bloated, stale, or",
|
|
22
|
+
"working on something unrelated, replace it instead of carrying that cost forward. Call partner_status to",
|
|
23
|
+
"decide: it reports context tokens used and the ratio of its window, current activity, vendor/model/effort,",
|
|
24
|
+
"history size and idle time, whether replacing is safe right now, and the results you recorded for earlier",
|
|
25
|
+
"Worker generations. It returns no transcript, so it is cheap to consult. Never tell the user that you will",
|
|
26
|
+
"reuse the current Worker before checking partner_status. If you previously expected reuse but the status leads",
|
|
27
|
+
"you to replace it, explicitly say that the decision changed and give the reason.",
|
|
28
|
+
"",
|
|
29
|
+
"Use send_to_partner seamlessly for follow-up work in the existing pair. Runtime creation and replacement are",
|
|
30
|
+
"recorded through a configuration card before anything mutates. replace_partner always posts that card. When",
|
|
31
|
+
"this Driver is in full-access mode, Clay may auto-accept your exact server-validated recommendation; otherwise",
|
|
32
|
+
"the selected vendor, model, and effort remain pending until the user explicitly accepts them.",
|
|
33
|
+
"The replaced Worker keeps its conversation, so nothing is lost. Replacing an actively running Worker requires",
|
|
34
|
+
"interrupt true, which stops it first only after acceptance. A human Stop",
|
|
35
|
+
"is authoritative: do not retry, send more work, or replace the Worker in the same turn. Clay blocks those",
|
|
36
|
+
"actions until the human sends a new Driver message. Use close_partner when they ask to close the pane.",
|
|
37
|
+
"",
|
|
38
|
+
"Recommend a Worker vendor, model, and effort from what is actually installed and offered, and include a concise",
|
|
39
|
+
"rationale explaining why all three fit the task. The card remains visible as an audit trail even when full access",
|
|
40
|
+
"auto-accepts it; an unavailable choice is never auto-accepted. After a Worker generation finishes or is",
|
|
41
|
+
"replaced, call record_partner_evaluation with succeeded, partial, failed, or abandoned and a short reason.",
|
|
42
|
+
"Clay stores that against that exact generation alongside what it measured itself, and partner_status hands it",
|
|
43
|
+
"back, so your next model choice can use observed results instead of guesswork. This is your own record for",
|
|
44
|
+
"this pair, not a general ranking of models.",
|
|
45
|
+
"",
|
|
46
|
+
"Internal Sub-agents are a distinct execution mechanism, not a lexical category; use them only when the user",
|
|
47
|
+
"clearly intends internal or background parallel delegation rather than the visible paired session. When",
|
|
48
|
+
"ambiguous and a visible pair exists, prefer the visible Split Worker. If review or user feedback requires",
|
|
49
|
+
"corrections to the Split Worker's implementation, delegate a follow-up turn to that Split Worker instead of",
|
|
50
|
+
"editing its files yourself. If a non-waiting delegation finishes later, Clay pushes the result back and",
|
|
51
|
+
"resumes you automatically. You also decide the Split Worker's tool-permission requests when they arise;",
|
|
52
|
+
"approve only what falls inside the task the user authorized. Integrate and verify the final outcome. Do not",
|
|
53
|
+
"search the project for implementations of these tools, and do not delegate work that must be performed",
|
|
54
|
+
"sequentially in this same session.",
|
|
55
|
+
].join(" ").replace(/ {2,}/g, " ");
|
|
56
|
+
|
|
57
|
+
var DRIVER = DRIVER_CORE + " " + DRIVER_DELEGATION;
|
|
58
|
+
|
|
59
|
+
var UNPAIRED = [
|
|
60
|
+
"Infer the user's intended target from conversational and UI context. References to a user-visible paired or",
|
|
61
|
+
"split pane, its session, its activity or status, or a collaborator the user wants opened resolve to Clay's",
|
|
62
|
+
"Split Worker and its partner tools. Internal Sub-agents are a distinct execution mechanism, not a lexical",
|
|
63
|
+
"category; use them only when the user clearly intends internal or background parallel delegation rather than a",
|
|
64
|
+
"visible paired session. When ambiguity would materially change where work runs, ask a concise clarification",
|
|
65
|
+
"instead of guessing. Treat implementation as heavy when it spans multiple modules, crosses client/server/data",
|
|
66
|
+
"boundaries, requires a migration, or contains independent investigation and execution. In those cases, call",
|
|
67
|
+
"propose_worker before substantial execution unless the user explicitly asks you to work directly or the work",
|
|
68
|
+
"must remain sequential in this session. Your own capability is not a reason to skip delegation: preserve the",
|
|
69
|
+
"Driver for decomposition, runtime selection, judgment, review, and integration. Give an exact runtime recommendation",
|
|
70
|
+
"and concise rationale. Clay records and shows the card before the visible pair is accepted or auto-accepted. Do not use a background",
|
|
71
|
+
"Sub-agent as a substitute for a visible Split Worker.",
|
|
72
|
+
].join(" ").replace(/ {2,}/g, " ");
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
DRIVER: DRIVER,
|
|
76
|
+
DRIVER_CORE: DRIVER_CORE,
|
|
77
|
+
DRIVER_DELEGATION: DRIVER_DELEGATION,
|
|
78
|
+
UNPAIRED: UNPAIRED,
|
|
79
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Per-human-turn safety policy for Driver/Split Worker orchestration.
|
|
2
|
+
//
|
|
3
|
+
// A human Stop is a cancellation barrier, not merely an AbortController
|
|
4
|
+
// signal. It blocks delegation and Worker replacement until the next ordinary
|
|
5
|
+
// human message reaches the Driver. Internal result and permission messages do
|
|
6
|
+
// not clear it. Creation budgets and operation ids also make retries bounded
|
|
7
|
+
// and idempotent even when no Stop occurred.
|
|
8
|
+
|
|
9
|
+
var MAX_CREATIONS_PER_TURN = 2;
|
|
10
|
+
var MAX_REPLACEMENTS_PER_TURN = 1;
|
|
11
|
+
var MAX_OPERATION_ID_CHARS = 120;
|
|
12
|
+
|
|
13
|
+
function attachPairTurnControl(ctx) {
|
|
14
|
+
var sm = ctx.sm;
|
|
15
|
+
var store = ctx.splitStore;
|
|
16
|
+
|
|
17
|
+
function stateFor(driver) {
|
|
18
|
+
if (!driver._pairTurnControl) {
|
|
19
|
+
driver._pairTurnControl = {
|
|
20
|
+
serial: 0,
|
|
21
|
+
humanStopped: false,
|
|
22
|
+
stoppedAt: null,
|
|
23
|
+
stoppedWorkerId: null,
|
|
24
|
+
creations: 0,
|
|
25
|
+
replacements: 0,
|
|
26
|
+
operations: Object.create(null),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return driver._pairTurnControl;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function liveSession(session) {
|
|
33
|
+
return !!(session && sm.sessions.get(session.localId) === session);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rolesFor(session) {
|
|
37
|
+
if (!liveSession(session)) return null;
|
|
38
|
+
var group = store.groupForMember(session.localId);
|
|
39
|
+
if (!group || !group.pair) return null;
|
|
40
|
+
var driver = sm.sessions.get(group.pair.driverId);
|
|
41
|
+
var worker = sm.sessions.get(group.pair.workerId);
|
|
42
|
+
if (!liveSession(driver) || !liveSession(worker)) return null;
|
|
43
|
+
if ((driver.ownerId || null) !== (worker.ownerId || null)) return null;
|
|
44
|
+
return { group: group, driver: driver, worker: worker };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function beginHumanTurn(session) {
|
|
48
|
+
if (!liveSession(session)) return false;
|
|
49
|
+
var roles = rolesFor(session);
|
|
50
|
+
var driver = roles ? roles.driver : session;
|
|
51
|
+
if (roles && roles.driver !== session) return false;
|
|
52
|
+
var state = stateFor(driver);
|
|
53
|
+
state.serial += 1;
|
|
54
|
+
state.humanStopped = false;
|
|
55
|
+
state.stoppedAt = null;
|
|
56
|
+
state.stoppedWorkerId = null;
|
|
57
|
+
state.creations = 0;
|
|
58
|
+
state.replacements = 0;
|
|
59
|
+
state.operations = Object.create(null);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function markHumanStop(session) {
|
|
64
|
+
var roles = rolesFor(session);
|
|
65
|
+
if (!roles) return null;
|
|
66
|
+
var state = stateFor(roles.driver);
|
|
67
|
+
state.humanStopped = true;
|
|
68
|
+
state.stoppedAt = Date.now();
|
|
69
|
+
state.stoppedWorkerId = roles.worker.localId;
|
|
70
|
+
return roles;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function blockedReason(driver) {
|
|
74
|
+
if (!liveSession(driver)) return "this Driver session is no longer live";
|
|
75
|
+
var state = stateFor(driver);
|
|
76
|
+
if (!state.humanStopped) return null;
|
|
77
|
+
return "the human stopped this Split Worker turn; Worker actions are blocked until the human sends a new message to the Driver";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertWorkerAction(driver) {
|
|
81
|
+
var reason = blockedReason(driver);
|
|
82
|
+
if (reason) throw new Error(reason);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function reserveCreation(driver, kind) {
|
|
86
|
+
assertWorkerAction(driver);
|
|
87
|
+
var state = stateFor(driver);
|
|
88
|
+
if (kind === "replace" && state.replacements >= MAX_REPLACEMENTS_PER_TURN) {
|
|
89
|
+
throw new Error("the Split Worker replacement limit for this human turn has been reached; wait for a new human message before replacing it again");
|
|
90
|
+
}
|
|
91
|
+
if (state.creations >= MAX_CREATIONS_PER_TURN) {
|
|
92
|
+
throw new Error("the Split Worker creation limit for this human turn has been reached; wait for a new human message before creating another Worker");
|
|
93
|
+
}
|
|
94
|
+
state.creations += 1;
|
|
95
|
+
if (kind === "replace") state.replacements += 1;
|
|
96
|
+
return { driver: driver, kind: kind, active: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function releaseCreation(ticket) {
|
|
100
|
+
if (!ticket || !ticket.active || !liveSession(ticket.driver)) return;
|
|
101
|
+
ticket.active = false;
|
|
102
|
+
var state = stateFor(ticket.driver);
|
|
103
|
+
state.creations = Math.max(0, state.creations - 1);
|
|
104
|
+
if (ticket.kind === "replace") state.replacements = Math.max(0, state.replacements - 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function operationId(value) {
|
|
108
|
+
if (typeof value !== "string") return "";
|
|
109
|
+
var clean = value.trim();
|
|
110
|
+
if (!clean || clean.length > MAX_OPERATION_ID_CHARS || !/^[A-Za-z0-9._:-]+$/.test(clean)) return "";
|
|
111
|
+
return clean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function runOperation(driver, kind, rawId, fn) {
|
|
115
|
+
var id = operationId(rawId);
|
|
116
|
+
if (!id) return Promise.resolve().then(fn);
|
|
117
|
+
var state = stateFor(driver);
|
|
118
|
+
var key = kind + ":" + id;
|
|
119
|
+
if (state.operations[key]) return state.operations[key];
|
|
120
|
+
var promise = Promise.resolve().then(fn);
|
|
121
|
+
state.operations[key] = promise;
|
|
122
|
+
return promise;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function status(driver) {
|
|
126
|
+
var state = stateFor(driver);
|
|
127
|
+
return {
|
|
128
|
+
humanStopped: state.humanStopped,
|
|
129
|
+
stoppedAt: state.stoppedAt,
|
|
130
|
+
stoppedWorkerId: state.stoppedWorkerId,
|
|
131
|
+
turnSerial: state.serial,
|
|
132
|
+
creationsThisTurn: state.creations,
|
|
133
|
+
replacementsThisTurn: state.replacements,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
assertWorkerAction: assertWorkerAction,
|
|
139
|
+
beginHumanTurn: beginHumanTurn,
|
|
140
|
+
blockedReason: blockedReason,
|
|
141
|
+
markHumanStop: markHumanStop,
|
|
142
|
+
releaseCreation: releaseCreation,
|
|
143
|
+
reserveCreation: reserveCreation,
|
|
144
|
+
runOperation: runOperation,
|
|
145
|
+
status: status,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
MAX_CREATIONS_PER_TURN: MAX_CREATIONS_PER_TURN,
|
|
151
|
+
MAX_REPLACEMENTS_PER_TURN: MAX_REPLACEMENTS_PER_TURN,
|
|
152
|
+
attachPairTurnControl: attachPairTurnControl,
|
|
153
|
+
};
|