clay-server 4.0.0-beta.2 → 4.0.0-beta.21
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/background-task-timing.js +105 -0
- 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 +14 -1
- 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-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-vendor-login.js +20 -3
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +178 -6
- package/lib/public/app.js +58 -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 +163 -28
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- 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 +34 -1
- package/lib/public/index.html +45 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +109 -4
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +84 -7
- 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 +46 -4
- package/lib/public/modules/app-projects.js +13 -8
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/background-tasks-ui.js +139 -16
- 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-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-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-view.js +5 -2
- 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/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 +21 -1
- 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 -12
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +132 -46
- package/lib/sdk-message-processor.js +60 -14
- 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 +124 -18
- 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 +39 -0
- package/lib/sessions.js +61 -11
- 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 +2 -2
- 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 +81 -4
- package/lib/yoke/adapters/codex.js +63 -15
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +3 -3
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
var PROVENANCE_VERSION = 1;
|
|
4
|
+
var MAX_ORIGIN_LENGTH = 128;
|
|
5
|
+
|
|
6
|
+
function safeOriginId(value) {
|
|
7
|
+
if (typeof value !== "string") return null;
|
|
8
|
+
var trimmed = value.trim();
|
|
9
|
+
if (!trimmed || trimmed.length > MAX_ORIGIN_LENGTH) return null;
|
|
10
|
+
return trimmed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ensureOrigin(session) {
|
|
14
|
+
if (!session) return null;
|
|
15
|
+
var existing = safeOriginId(session.sessionOriginId);
|
|
16
|
+
if (existing) {
|
|
17
|
+
session.sessionOriginId = existing;
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
session.sessionOriginId = crypto.randomUUID();
|
|
21
|
+
return session.sessionOriginId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isWorker(session) {
|
|
25
|
+
return !!(session && session.sessionProvenance && session.sessionProvenance.kind === "worker");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nextWorkerGeneration(parentOriginId, sessions) {
|
|
29
|
+
var next = 1;
|
|
30
|
+
sessions.forEach(function (session) {
|
|
31
|
+
var provenance = session && session.sessionProvenance;
|
|
32
|
+
if (!provenance || provenance.kind !== "worker" || provenance.parentSessionOriginId !== parentOriginId) return;
|
|
33
|
+
var generation = Number.isInteger(provenance.generation) && provenance.generation > 0 ? provenance.generation : 0;
|
|
34
|
+
if (generation >= next) next = generation + 1;
|
|
35
|
+
});
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function markWorker(driver, worker, sessions) {
|
|
40
|
+
if (!driver || !worker || driver === worker) throw new Error("Split Worker provenance requires distinct sessions");
|
|
41
|
+
if ((driver.ownerId || null) !== (worker.ownerId || null)) throw new Error("Split Worker provenance requires the same owner");
|
|
42
|
+
if (isWorker(driver)) throw new Error("A Split Worker cannot become a Driver");
|
|
43
|
+
var parentOriginId = ensureOrigin(driver);
|
|
44
|
+
ensureOrigin(worker);
|
|
45
|
+
worker.sessionProvenance = {
|
|
46
|
+
version: PROVENANCE_VERSION,
|
|
47
|
+
kind: "worker",
|
|
48
|
+
parentSessionOriginId: parentOriginId,
|
|
49
|
+
generation: nextWorkerGeneration(parentOriginId, sessions),
|
|
50
|
+
createdVia: "split-worker",
|
|
51
|
+
createdAt: Date.now(),
|
|
52
|
+
};
|
|
53
|
+
return worker.sessionProvenance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function restore(meta, session) {
|
|
57
|
+
var storedOriginId = safeOriginId(meta && meta.sessionOriginId);
|
|
58
|
+
ensureOrigin(session);
|
|
59
|
+
var stored = meta && meta.sessionProvenance;
|
|
60
|
+
if (!stored || stored.kind !== "worker") return storedOriginId !== session.sessionOriginId;
|
|
61
|
+
var parentOriginId = safeOriginId(stored.parentSessionOriginId);
|
|
62
|
+
// A corrupt Worker record remains a Worker. It is projected as orphaned
|
|
63
|
+
// instead of silently gaining top-level Driver authority.
|
|
64
|
+
session.sessionProvenance = {
|
|
65
|
+
version: PROVENANCE_VERSION,
|
|
66
|
+
kind: "worker",
|
|
67
|
+
parentSessionOriginId: parentOriginId,
|
|
68
|
+
generation: Number.isInteger(stored.generation) && stored.generation > 0 ? stored.generation : null,
|
|
69
|
+
createdVia: stored.createdVia === "split-worker" ? "split-worker" : null,
|
|
70
|
+
createdAt: typeof stored.createdAt === "number" && isFinite(stored.createdAt) ? stored.createdAt : null,
|
|
71
|
+
};
|
|
72
|
+
return storedOriginId !== session.sessionOriginId || JSON.stringify(stored) !== JSON.stringify(session.sessionProvenance);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function metadata(session) {
|
|
76
|
+
var result = { sessionOriginId: ensureOrigin(session) };
|
|
77
|
+
if (isWorker(session)) result.sessionProvenance = Object.assign({}, session.sessionProvenance);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hierarchyFor(sessions) {
|
|
82
|
+
var originMap = {};
|
|
83
|
+
var duplicateOrigins = {};
|
|
84
|
+
for (var i = 0; i < sessions.length; i++) {
|
|
85
|
+
var originId = safeOriginId(sessions[i] && sessions[i].sessionOriginId);
|
|
86
|
+
if (!originId) continue;
|
|
87
|
+
if (originMap[originId]) duplicateOrigins[originId] = true;
|
|
88
|
+
else originMap[originId] = sessions[i];
|
|
89
|
+
}
|
|
90
|
+
var result = {};
|
|
91
|
+
for (var j = 0; j < sessions.length; j++) {
|
|
92
|
+
var session = sessions[j];
|
|
93
|
+
if (!isWorker(session)) {
|
|
94
|
+
result[session.localId] = { role: "driver", parentSessionId: null, parentAvailable: false, generation: null };
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
var provenance = session.sessionProvenance;
|
|
98
|
+
var parentOriginId = safeOriginId(provenance.parentSessionOriginId);
|
|
99
|
+
var parent = duplicateOrigins[parentOriginId] ? null : originMap[parentOriginId];
|
|
100
|
+
var sameOwner = parent && (parent.ownerId || null) === (session.ownerId || null);
|
|
101
|
+
var validParent = sameOwner && !isWorker(parent) ? parent : null;
|
|
102
|
+
result[session.localId] = {
|
|
103
|
+
role: "worker",
|
|
104
|
+
parentSessionId: validParent ? validParent.localId : null,
|
|
105
|
+
parentAvailable: !!validParent,
|
|
106
|
+
generation: Number.isInteger(provenance.generation) && provenance.generation > 0 ? provenance.generation : null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
ensureOrigin: ensureOrigin,
|
|
114
|
+
hierarchyFor: hierarchyFor,
|
|
115
|
+
isWorker: isWorker,
|
|
116
|
+
markWorker: markWorker,
|
|
117
|
+
metadata: metadata,
|
|
118
|
+
restore: restore,
|
|
119
|
+
};
|
|
@@ -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." },
|
|
@@ -280,7 +280,10 @@ function attachSplitGroups(ctx) {
|
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
function sendState(ws) {
|
|
283
|
-
if (!ws
|
|
283
|
+
if (!ws) return;
|
|
284
|
+
// Pane clients need the same owner-scoped pair roles as the parent shell.
|
|
285
|
+
// The Worker composer lock resolves its role from this projection; hiding
|
|
286
|
+
// the projection from panes leaves a configured Worker looking editable.
|
|
284
287
|
ctx.sendTo(ws, { type: "split_groups", groups: store.listFor(ws) });
|
|
285
288
|
}
|
|
286
289
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Session title policy shared by query startup and completed-turn processing.
|
|
2
|
+
// Initial titles are immediate, provisional UI labels. Model-generated titles
|
|
3
|
+
// replace them after enough conversation exists to describe the actual topic.
|
|
4
|
+
|
|
5
|
+
var DEFAULT_GENERATION_TURN = 2;
|
|
6
|
+
var HOME_GENERATION_TURN = 1;
|
|
7
|
+
var MAX_PROVISIONAL_CHARS = 60;
|
|
8
|
+
var LEGACY_SEARCH_TITLE = "This conversation began in Clay Studio's global search.";
|
|
9
|
+
|
|
10
|
+
function cleanText(value) {
|
|
11
|
+
if (typeof value !== "string") return "";
|
|
12
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function firstRecordedUserMessage(session) {
|
|
16
|
+
var history = session && Array.isArray(session.history) ? session.history : [];
|
|
17
|
+
for (var i = 0; i < history.length; i++) {
|
|
18
|
+
if (history[i] && history[i].type === "user_message") {
|
|
19
|
+
var text = cleanText(history[i].text);
|
|
20
|
+
if (text) return text;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function selectInitialTitle(session, transportText) {
|
|
27
|
+
var existing = cleanText(session && session.title);
|
|
28
|
+
if (existing) return { title: existing, provisional: false };
|
|
29
|
+
|
|
30
|
+
// Prefer the visible message recorded in history. Query transports may wrap
|
|
31
|
+
// it in identical provider instructions, as global Ask Clay does.
|
|
32
|
+
var source = firstRecordedUserMessage(session) || cleanText(transportText);
|
|
33
|
+
if (source.length < 10) return { title: null, provisional: false };
|
|
34
|
+
return {
|
|
35
|
+
title: source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source,
|
|
36
|
+
provisional: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generationTurn(isMate) {
|
|
41
|
+
return isMate === true ? HOME_GENERATION_TURN : DEFAULT_GENERATION_TURN;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function repairLegacySearchTitle(session) {
|
|
45
|
+
var title = cleanText(session && session.title);
|
|
46
|
+
if (!session || title.indexOf(LEGACY_SEARCH_TITLE) !== 0) return false;
|
|
47
|
+
var source = firstRecordedUserMessage(session);
|
|
48
|
+
if (!source) return false;
|
|
49
|
+
session.title = source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source;
|
|
50
|
+
session.titleAutoGenerated = false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
DEFAULT_GENERATION_TURN: DEFAULT_GENERATION_TURN,
|
|
56
|
+
HOME_GENERATION_TURN: HOME_GENERATION_TURN,
|
|
57
|
+
selectInitialTitle: selectInitialTitle,
|
|
58
|
+
generationTurn: generationTurn,
|
|
59
|
+
repairLegacySearchTitle: repairLegacySearchTitle,
|
|
60
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function normalize(value) {
|
|
2
|
+
return value === "shared" ? "shared" : "private";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function restore(meta) {
|
|
6
|
+
// Older releases persisted the shared default without recording intent.
|
|
7
|
+
return meta.sessionVisibilityExplicit === true ? normalize(meta.sessionVisibility) : "private";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function handleChange(ctx, ws, msg) {
|
|
11
|
+
if (typeof msg.sessionId !== "number" || (msg.visibility !== "shared" && msg.visibility !== "private")) return;
|
|
12
|
+
var session = ctx.sm.sessions.get(msg.sessionId);
|
|
13
|
+
if (!session) return;
|
|
14
|
+
if (ctx.usersModule.isMultiUser()) {
|
|
15
|
+
var user = ws._clayUser;
|
|
16
|
+
var project = ctx.getProjectAccess ? ctx.getProjectAccess() : { visibility: "public" };
|
|
17
|
+
if (!user || !session.ownerId || session.ownerId !== user.id ||
|
|
18
|
+
!ctx.usersModule.canAccessSession(user.id, session, project)) return;
|
|
19
|
+
} else if (session.ownerId) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
ctx.sm.setSessionVisibility(msg.sessionId, msg.visibility);
|
|
23
|
+
revokeViewers(ctx, session);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function revokeViewers(ctx, session) {
|
|
27
|
+
if (ctx.usersModule.isMultiUser() && session.sessionVisibility === "private") {
|
|
28
|
+
ctx.clients.forEach(function(client) {
|
|
29
|
+
if (client._clayActiveSession !== session.localId) return;
|
|
30
|
+
if (client._clayUser && client._clayUser.id === session.ownerId) return;
|
|
31
|
+
// Clear the server-side selection before reconnecting so no further
|
|
32
|
+
// session events or input can cross the revoked access boundary.
|
|
33
|
+
client._clayActiveSession = null;
|
|
34
|
+
if (client.readyState === 1) client.close(1008, "Session access changed");
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { normalize: normalize, restore: restore, handleChange: handleChange, revokeViewers: revokeViewers };
|