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
package/lib/sdk-bridge.js
CHANGED
|
@@ -12,6 +12,7 @@ var { createMessageQueue } = require("./sdk-message-queue");
|
|
|
12
12
|
var { attachMessageProcessor } = require("./sdk-message-processor");
|
|
13
13
|
var homeDebateToolPolicy = require("./home-debate-tool-policy");
|
|
14
14
|
var yoke = require("./yoke");
|
|
15
|
+
var sessionTitlePolicy = require("./session-title-policy");
|
|
15
16
|
|
|
16
17
|
// Extract serializable tool descriptors from MCP server instances.
|
|
17
18
|
// Used for IPC to worker processes (McpSdkServerConfigWithInstance is not serializable).
|
|
@@ -128,6 +129,9 @@ function createSDKBridge(opts) {
|
|
|
128
129
|
var clayTls = opts.clayTls || false;
|
|
129
130
|
var clayAuthToken = opts.clayAuthToken || null;
|
|
130
131
|
var getSessionToolDefs = opts.getSessionToolDefs || function () { return []; };
|
|
132
|
+
// Split Worker permission routing. A getter because the pair module is
|
|
133
|
+
// constructed before this bridge exists.
|
|
134
|
+
var getWorkerPermissionRouter = opts.getWorkerPermissionRouter || function () { return null; };
|
|
131
135
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
132
136
|
var _cachedFreshAuthState = null;
|
|
133
137
|
var _cachedFreshAuthAt = 0;
|
|
@@ -601,27 +605,77 @@ function createSDKBridge(opts) {
|
|
|
601
605
|
return { behavior: "allow", updatedInput: input };
|
|
602
606
|
}
|
|
603
607
|
|
|
604
|
-
// Auto-approve
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
608
|
+
// Auto-approve the non-mutating configuration proposal and the visible
|
|
609
|
+
// Driver/Split Worker pair tools, including the Driver's answer to a Worker
|
|
610
|
+
// permission request. Creation and replacement still require a separate
|
|
611
|
+
// human decision in the proposal card; skip-permissions is never treated as
|
|
612
|
+
// that product choice. Every handler is confined to the caller's exact
|
|
613
|
+
// session/pair and re-checks eligibility, ownership and live identity.
|
|
614
|
+
//
|
|
615
|
+
// spawn_sessions is deliberately absent: it creates arbitrary sessions.
|
|
616
|
+
//
|
|
617
|
+
// Only the two name forms the bridge can genuinely emit are accepted:
|
|
618
|
+
//
|
|
619
|
+
// mcp__clay-sessions__<tool> every MCP-mounted vendor. The server
|
|
620
|
+
// segment is matched exactly, so a tool of
|
|
621
|
+
// the same name on another MCP server is
|
|
622
|
+
// never auto-approved.
|
|
623
|
+
// <tool> a Codex dynamic tool. Codex canonicalizes
|
|
624
|
+
// send_to_partner and read_partner to the MCP
|
|
625
|
+
// form itself, so only the remaining names
|
|
626
|
+
// can legitimately arrive bare.
|
|
627
|
+
var PAIR_MCP_PREFIX = "mcp__clay-sessions__";
|
|
628
|
+
var pairMcpTools = {
|
|
629
|
+
propose_worker: true,
|
|
630
|
+
send_to_partner: true,
|
|
631
|
+
read_partner: true,
|
|
632
|
+
partner_status: true,
|
|
633
|
+
replace_partner: true,
|
|
634
|
+
interrupt_partner: true,
|
|
635
|
+
close_partner: true,
|
|
636
|
+
record_partner_evaluation: true,
|
|
637
|
+
respond_to_worker_permission: true,
|
|
638
|
+
};
|
|
639
|
+
var pairDynamicTools = {
|
|
640
|
+
propose_worker: true,
|
|
641
|
+
partner_status: true,
|
|
642
|
+
replace_partner: true,
|
|
643
|
+
interrupt_partner: true,
|
|
644
|
+
close_partner: true,
|
|
645
|
+
record_partner_evaluation: true,
|
|
646
|
+
respond_to_worker_permission: true,
|
|
647
|
+
};
|
|
648
|
+
if (toolName.indexOf("mcp__") === 0) {
|
|
649
|
+
if (toolName.indexOf(PAIR_MCP_PREFIX) === 0
|
|
650
|
+
&& pairMcpTools[toolName.slice(PAIR_MCP_PREFIX.length)]) {
|
|
614
651
|
return { behavior: "allow", updatedInput: input };
|
|
615
652
|
}
|
|
653
|
+
} else if (pairDynamicTools[toolName]) {
|
|
654
|
+
return { behavior: "allow", updatedInput: input };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Sticky-note operations are immediately visible and reversible on the
|
|
658
|
+
// shared board. The exact session-bound handlers still enforce ownership,
|
|
659
|
+
// lifecycle rules, and input validation, so a provider prompt adds no
|
|
660
|
+
// authority. Keep this confined to the exact Clay Notes namespace.
|
|
661
|
+
var NOTES_MCP_PREFIX = "mcp__clay-notes__";
|
|
662
|
+
var notesTools = {
|
|
663
|
+
list_notes: true,
|
|
664
|
+
write_note: true,
|
|
665
|
+
close_note: true,
|
|
666
|
+
reopen_note: true,
|
|
667
|
+
remove_note: true,
|
|
668
|
+
};
|
|
669
|
+
if (toolName.indexOf(NOTES_MCP_PREFIX) === 0
|
|
670
|
+
&& notesTools[toolName.slice(NOTES_MCP_PREFIX.length)]) {
|
|
671
|
+
return { behavior: "allow", updatedInput: input };
|
|
616
672
|
}
|
|
617
673
|
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|| toolName.indexOf("clay-notes__") !== -1;
|
|
624
|
-
if (isClayNotesTool && (notesToolName === "list_notes" || notesToolName === "write_note")) {
|
|
674
|
+
// Project Logs are exact-session, project-bound tools whose authority is
|
|
675
|
+
// enforced again by the server binding. Canonical changes are append-only
|
|
676
|
+
// revisions, and the surface exposes no destructive delete operation, so
|
|
677
|
+
// a second provider permission prompt adds no additional safety.
|
|
678
|
+
if (toolName.indexOf("mcp__clay-logs__") === 0) {
|
|
625
679
|
return { behavior: "allow", updatedInput: input };
|
|
626
680
|
}
|
|
627
681
|
|
|
@@ -720,6 +774,19 @@ function createSDKBridge(opts) {
|
|
|
720
774
|
// Full-auto mode: auto-approve everything except AskUserQuestion
|
|
721
775
|
// (which still needs to go through the user interaction flow).
|
|
722
776
|
var clayPermissionMode = session.permissionMode || sm.currentPermissionMode;
|
|
777
|
+
// A configured Split Worker is Driver-operated, so it runs under its
|
|
778
|
+
// Driver's permission mode, re-resolved from the live pair on every single
|
|
779
|
+
// decision. Nothing is copied at creation and the Worker's own persisted
|
|
780
|
+
// setting is never written, so toggling skip permissions on the Driver
|
|
781
|
+
// applies to the Worker's next call in either direction, mid-turn included.
|
|
782
|
+
var _wpMode = getWorkerPermissionRouter();
|
|
783
|
+
if (_wpMode && typeof _wpMode.inheritedPermissionMode === "function") {
|
|
784
|
+
var inherited = _wpMode.inheritedPermissionMode(session);
|
|
785
|
+
if (inherited) clayPermissionMode = inherited;
|
|
786
|
+
}
|
|
787
|
+
// AskUserQuestion is genuine user input, not an authorization, so it is
|
|
788
|
+
// excluded here exactly as it always was: a Driver in skip mode still
|
|
789
|
+
// cannot silently answer a question addressed to the person.
|
|
723
790
|
if (clayPermissionMode === "bypassPermissions" && toolName !== "AskUserQuestion") {
|
|
724
791
|
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
725
792
|
}
|
|
@@ -747,6 +814,22 @@ function createSDKBridge(opts) {
|
|
|
747
814
|
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
748
815
|
}
|
|
749
816
|
|
|
817
|
+
// A visible Split Worker's remaining prompts belong to its paired Driver,
|
|
818
|
+
// not to the Worker pane. This sits below every auto-decision above, so
|
|
819
|
+
// skip permissions, the whitelist, allowedTools and the loop/debate
|
|
820
|
+
// policies all still resolve first and never wait on a Driver.
|
|
821
|
+
var _wp = getWorkerPermissionRouter();
|
|
822
|
+
if (_wp) {
|
|
823
|
+
var routed = _wp.routeIfWorker(session, {
|
|
824
|
+
toolName: toolName,
|
|
825
|
+
input: input,
|
|
826
|
+
toolUseId: opts.toolUseID,
|
|
827
|
+
decisionReason: opts.decisionReason || "",
|
|
828
|
+
signal: opts.signal,
|
|
829
|
+
});
|
|
830
|
+
if (routed) return routed;
|
|
831
|
+
}
|
|
832
|
+
|
|
750
833
|
// Regular tool permission request: send to client and wait
|
|
751
834
|
return new Promise(function(resolve) {
|
|
752
835
|
var requestId = crypto.randomUUID();
|
|
@@ -1540,25 +1623,33 @@ function createSDKBridge(opts) {
|
|
|
1540
1623
|
if (selectedUserInputMode === "fallback") {
|
|
1541
1624
|
sessionToolDefs = sessionToolDefs.concat(yoke.userInput.fallbackToolDefs(sessionUserInputHandler));
|
|
1542
1625
|
}
|
|
1626
|
+
function findSessionToolDef(toolName) {
|
|
1627
|
+
// Codex restores a thread's original dynamic-tool catalog on resume, but
|
|
1628
|
+
// pair state can change after this query starts. Resolve live session
|
|
1629
|
+
// handlers first so the dispatcher cannot lag behind that catalog. Keep
|
|
1630
|
+
// the query snapshot as a fallback for query-bound tools such as the
|
|
1631
|
+
// structured-input fallback, whose handler exists only for this turn.
|
|
1632
|
+
var liveToolDefs = getSessionToolDefs(session) || [];
|
|
1633
|
+
for (var liveIndex = 0; liveIndex < liveToolDefs.length; liveIndex++) {
|
|
1634
|
+
if (liveToolDefs[liveIndex].name === toolName) return liveToolDefs[liveIndex];
|
|
1635
|
+
}
|
|
1636
|
+
for (var snapshotIndex = 0; snapshotIndex < sessionToolDefs.length; snapshotIndex++) {
|
|
1637
|
+
if (sessionToolDefs[snapshotIndex].name === toolName) return sessionToolDefs[snapshotIndex];
|
|
1638
|
+
}
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1543
1641
|
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1642
|
+
// Give fresh queries an immediate provisional label. The visible user
|
|
1643
|
+
// message in history wins over `text`, which may contain an internal
|
|
1644
|
+
// provider wrapper rather than what the person typed.
|
|
1547
1645
|
// Only applied to NEW sessions (no cliSessionId yet) — when resuming,
|
|
1548
1646
|
// the SDK ignores Options.title in favor of the persisted title.
|
|
1549
1647
|
var initialTitle = null;
|
|
1648
|
+
var initialTitleProvisional = false;
|
|
1550
1649
|
if (!session.cliSessionId && !session.titleManuallySet && !session.titleAutoGenerated) {
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
} else if (typeof text === "string") {
|
|
1555
|
-
// Derive a quick first-line snippet from the user's first message.
|
|
1556
|
-
// Skip if too short to be meaningful — fall back to autoGenerateTitle.
|
|
1557
|
-
var firstLine = text.replace(/\s+/g, " ").trim();
|
|
1558
|
-
if (firstLine.length >= 10) {
|
|
1559
|
-
initialTitle = firstLine.length > 60 ? firstLine.substring(0, 60) : firstLine;
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1650
|
+
var initialTitleChoice = sessionTitlePolicy.selectInitialTitle(session, text);
|
|
1651
|
+
initialTitle = initialTitleChoice.title;
|
|
1652
|
+
initialTitleProvisional = initialTitleChoice.provisional;
|
|
1562
1653
|
}
|
|
1563
1654
|
|
|
1564
1655
|
var queryOpts = {
|
|
@@ -1587,10 +1678,9 @@ function createSDKBridge(opts) {
|
|
|
1587
1678
|
toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
|
|
1588
1679
|
dynamicTools: sessionToolDefs,
|
|
1589
1680
|
callDynamicTool: function (toolName, args) {
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
}
|
|
1681
|
+
var toolDef = findSessionToolDef(toolName);
|
|
1682
|
+
if (toolDef && typeof toolDef.handler === "function") {
|
|
1683
|
+
return toolDef.handler(args || {});
|
|
1594
1684
|
}
|
|
1595
1685
|
return Promise.reject(new Error("Session tool not found: " + toolName));
|
|
1596
1686
|
},
|
|
@@ -1598,12 +1688,8 @@ function createSDKBridge(opts) {
|
|
|
1598
1688
|
abortController: linuxUser ? undefined : session.abortController,
|
|
1599
1689
|
canUseTool: function(toolName, input, toolOpts) {
|
|
1600
1690
|
var permissionName = toolName;
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
permissionName = sessionToolDefs[pti].permissionName;
|
|
1604
|
-
break;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1691
|
+
var permissionToolDef = findSessionToolDef(toolName);
|
|
1692
|
+
if (permissionToolDef && permissionToolDef.permissionName) permissionName = permissionToolDef.permissionName;
|
|
1607
1693
|
return handleCanUseTool(session, permissionName, input, toolOpts);
|
|
1608
1694
|
},
|
|
1609
1695
|
onUserInputRequest: function(request, respond) {
|
|
@@ -1653,16 +1739,16 @@ function createSDKBridge(opts) {
|
|
|
1653
1739
|
try {
|
|
1654
1740
|
handle = await sessionAdapter.createQuery(queryOpts);
|
|
1655
1741
|
console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
|
|
1656
|
-
// SDK accepted the explicit title
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1742
|
+
// SDK accepted the explicit title. Provisional labels remain eligible
|
|
1743
|
+
// for semantic generation after the first Home exchange (or the normal
|
|
1744
|
+
// project threshold); seeded titles remain fixed.
|
|
1659
1745
|
if (initialTitle && !session.title) {
|
|
1660
1746
|
session.title = initialTitle;
|
|
1661
|
-
session.titleAutoGenerated =
|
|
1747
|
+
session.titleAutoGenerated = !initialTitleProvisional;
|
|
1662
1748
|
sm.saveSessionFile(session);
|
|
1663
1749
|
sm.broadcastSessionList();
|
|
1664
1750
|
} else if (initialTitle && session.title === initialTitle) {
|
|
1665
|
-
session.titleAutoGenerated =
|
|
1751
|
+
session.titleAutoGenerated = !initialTitleProvisional;
|
|
1666
1752
|
sm.saveSessionFile(session);
|
|
1667
1753
|
}
|
|
1668
1754
|
} catch (e) {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
var usersModule = require("./users");
|
|
2
2
|
var yoke = require("./yoke");
|
|
3
|
+
var backgroundTaskTiming = require("./background-task-timing");
|
|
4
|
+
var sessionTitlePolicy = require("./session-title-policy");
|
|
5
|
+
|
|
6
|
+
var OVERLOAD_RETRY_LIMIT = 3;
|
|
3
7
|
|
|
4
8
|
function attachMessageProcessor(ctx) {
|
|
5
9
|
var sm = ctx.sm;
|
|
@@ -22,8 +26,6 @@ function attachMessageProcessor(ctx) {
|
|
|
22
26
|
var saveImageFile = ctx.saveImageFile;
|
|
23
27
|
var getSessionLinuxUser = ctx.getSessionLinuxUser || function () { return null; };
|
|
24
28
|
|
|
25
|
-
var AUTO_TITLE_TURN_THRESHOLD = 2;
|
|
26
|
-
|
|
27
29
|
function getMateIdForNotification() {
|
|
28
30
|
if (!isMate) return null;
|
|
29
31
|
if (typeof slug === "string" && slug.indexOf("mate-") === 0) {
|
|
@@ -60,6 +62,8 @@ function attachMessageProcessor(ctx) {
|
|
|
60
62
|
if (session._lastAuthEmit && (now - session._lastAuthEmit) < 5000) return;
|
|
61
63
|
session._lastAuthEmit = now;
|
|
62
64
|
|
|
65
|
+
var vendor = session.vendor || "claude";
|
|
66
|
+
|
|
63
67
|
var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
|
|
64
68
|
var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
|
|
65
69
|
var canAutoLogin = !usersModule.isMultiUser()
|
|
@@ -69,10 +73,13 @@ function attachMessageProcessor(ctx) {
|
|
|
69
73
|
var authTitle = ((vendorInfo && vendorInfo.displayName) || "Claude Code") + " is not logged in.";
|
|
70
74
|
var loginCommand = (vendorInfo && vendorInfo.loginCommand) || "claude login";
|
|
71
75
|
var _nmLogin = getNotificationsModule();
|
|
72
|
-
|
|
76
|
+
// Authentication recovery is transient UI state, not transcript content.
|
|
77
|
+
// Recording it makes history replay look like a fresh auth failure and
|
|
78
|
+
// re-opens the login terminal after a successful sign-in.
|
|
79
|
+
sendToSession(session, {
|
|
73
80
|
type: "auth_required",
|
|
74
81
|
text: authTitle,
|
|
75
|
-
vendor:
|
|
82
|
+
vendor: vendor,
|
|
76
83
|
loginCommand: loginCommand,
|
|
77
84
|
linuxUser: authLinuxUser,
|
|
78
85
|
canAutoLogin: canAutoLogin,
|
|
@@ -84,7 +91,7 @@ function attachMessageProcessor(ctx) {
|
|
|
84
91
|
slug: slug,
|
|
85
92
|
sessionId: session.localId,
|
|
86
93
|
ownerId: session.ownerId || null,
|
|
87
|
-
vendor:
|
|
94
|
+
vendor: vendor,
|
|
88
95
|
loginCommand: loginCommand,
|
|
89
96
|
linuxUser: authLinuxUser,
|
|
90
97
|
canAutoLogin: canAutoLogin,
|
|
@@ -192,7 +199,9 @@ function attachMessageProcessor(ctx) {
|
|
|
192
199
|
|
|
193
200
|
// Cache slash_commands and model from CLI init message
|
|
194
201
|
if (parsed.yokeType === "init") {
|
|
195
|
-
|
|
202
|
+
// Ambient housekeeping never drove the sidebar badge, so a reset that
|
|
203
|
+
// only clears ambient watchers must not rebroadcast the session list.
|
|
204
|
+
var previousBackgroundTaskCount = backgroundTaskTiming.countUserTasks(session.activeBackgroundTasks);
|
|
196
205
|
session.activeBackgroundTasks = [];
|
|
197
206
|
sendToSession(session, { type: "active_background_tasks", tasks: [] });
|
|
198
207
|
if (previousBackgroundTaskCount !== 0) sm.broadcastSessionList();
|
|
@@ -480,6 +489,8 @@ function attachMessageProcessor(ctx) {
|
|
|
480
489
|
}
|
|
481
490
|
|
|
482
491
|
} else if (parsed.yokeType === "result") {
|
|
492
|
+
var overloadAbortRequested = !!session._overloadAbortRequested;
|
|
493
|
+
session._overloadAbortRequested = false;
|
|
483
494
|
session.blocks = {};
|
|
484
495
|
session.sentToolResults = {};
|
|
485
496
|
session.pendingPermissions = {};
|
|
@@ -525,7 +536,9 @@ function attachMessageProcessor(ctx) {
|
|
|
525
536
|
session._awaitingTurnResult = false;
|
|
526
537
|
session._queuedTurnCount = 0;
|
|
527
538
|
onProcessingChanged();
|
|
528
|
-
|
|
539
|
+
if (!overloadAbortRequested) {
|
|
540
|
+
sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
|
|
541
|
+
}
|
|
529
542
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
530
543
|
sm.broadcastSessionList();
|
|
531
544
|
return;
|
|
@@ -617,7 +630,7 @@ function attachMessageProcessor(ctx) {
|
|
|
617
630
|
sm.broadcastSessionList();
|
|
618
631
|
|
|
619
632
|
// Auto-generate title after N turns (skip if loop or already auto-generated)
|
|
620
|
-
if (session.turnCount ===
|
|
633
|
+
if (session.turnCount === sessionTitlePolicy.generationTurn(isMate)
|
|
621
634
|
&& !session.titleAutoGenerated
|
|
622
635
|
&& !session.titleManuallySet
|
|
623
636
|
&& !session.loop
|
|
@@ -686,11 +699,20 @@ function attachMessageProcessor(ctx) {
|
|
|
686
699
|
}
|
|
687
700
|
|
|
688
701
|
} else if (parsed.yokeType === "background_tasks_changed") {
|
|
689
|
-
var
|
|
690
|
-
|
|
702
|
+
var previousBackgroundTasks = session.activeBackgroundTasks || [];
|
|
703
|
+
// Count user work only: an ambient watcher starting or stopping must not
|
|
704
|
+
// rebroadcast the session list and flicker every client's sidebar badge.
|
|
705
|
+
var previousBackgroundTaskCount = backgroundTaskTiming.countUserTasks(previousBackgroundTasks);
|
|
706
|
+
// Vendors re-send the whole list on every change, so carry each task's
|
|
707
|
+
// first-seen time forward; otherwise the composer's elapsed timer would
|
|
708
|
+
// restart whenever any other task starts or finishes.
|
|
709
|
+
var activeBackgroundTasks = backgroundTaskTiming.mergeStartTimes(
|
|
710
|
+
previousBackgroundTasks,
|
|
711
|
+
Array.isArray(parsed.tasks) ? parsed.tasks : []
|
|
712
|
+
);
|
|
691
713
|
session.activeBackgroundTasks = activeBackgroundTasks;
|
|
692
714
|
sendToSession(session, { type: "active_background_tasks", tasks: activeBackgroundTasks });
|
|
693
|
-
if (previousBackgroundTaskCount !== activeBackgroundTasks
|
|
715
|
+
if (previousBackgroundTaskCount !== backgroundTaskTiming.countUserTasks(activeBackgroundTasks)) {
|
|
694
716
|
sm.broadcastSessionList();
|
|
695
717
|
}
|
|
696
718
|
|
|
@@ -803,9 +825,33 @@ function attachMessageProcessor(ctx) {
|
|
|
803
825
|
}
|
|
804
826
|
|
|
805
827
|
} else if (parsed.yokeType === "api_retry") {
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
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
|
+
}
|
|
809
855
|
|
|
810
856
|
} else if (parsed.yokeType === "commands_changed") {
|
|
811
857
|
// Mid-session command list change: rebuild the union with skills (same as
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function attachExperimentalSettings(ctx) {
|
|
2
|
+
var users = ctx.users;
|
|
3
|
+
var getMultiUserFromReq = ctx.getMultiUserFromReq;
|
|
4
|
+
var opts = ctx.opts;
|
|
5
|
+
|
|
6
|
+
function handleRequest(req, res, fullUrl) {
|
|
7
|
+
if (req.method !== "PUT" || fullUrl !== "/api/user/capsules-enabled") return false;
|
|
8
|
+
var isMultiUser = users.isMultiUser();
|
|
9
|
+
var mu = getMultiUserFromReq(req);
|
|
10
|
+
if (isMultiUser && !mu) {
|
|
11
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
12
|
+
res.end('{"error":"unauthorized"}');
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
var body = "";
|
|
16
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
17
|
+
req.on("end", function () {
|
|
18
|
+
try {
|
|
19
|
+
var data = JSON.parse(body);
|
|
20
|
+
var result;
|
|
21
|
+
var userId = "default";
|
|
22
|
+
if (isMultiUser) {
|
|
23
|
+
userId = mu.id;
|
|
24
|
+
result = users.setCapsulesEnabled(userId, data.enabled === true);
|
|
25
|
+
} else if (typeof opts.onSetCapsulesEnabled === "function") {
|
|
26
|
+
result = opts.onSetCapsulesEnabled(data.enabled === true);
|
|
27
|
+
} else {
|
|
28
|
+
result = { ok: true, capsulesEnabled: false };
|
|
29
|
+
}
|
|
30
|
+
if (result.error) {
|
|
31
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
32
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof ctx.onCapsulesPreferenceChanged === "function") ctx.onCapsulesPreferenceChanged(userId);
|
|
36
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
37
|
+
res.end(JSON.stringify({ ok: true, capsulesEnabled: result.capsulesEnabled === true }));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
40
|
+
res.end('{"error":"Invalid request"}');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { handleRequest: handleRequest };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { attachExperimentalSettings: attachExperimentalSettings };
|
package/lib/server-global-ws.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Bound to the slug-less `/ws` endpoint by lib/server.js. Only handles the
|
|
8
8
|
// small set of messages needed to bootstrap into a project context:
|
|
9
9
|
// - ping -> pong keep-alive
|
|
10
|
+
// - Home and Mate -> the same user-scoped handlers as project sockets
|
|
10
11
|
// - browse_dir -> directory picker for the add-project modal
|
|
11
12
|
// - add_project -> register an existing directory
|
|
12
13
|
// - create_project -> make a new empty project
|
|
@@ -37,6 +38,8 @@ function attachGlobalWs(opts) {
|
|
|
37
38
|
var onAddProject = opts.onAddProject;
|
|
38
39
|
var onCreateProject = opts.onCreateProject;
|
|
39
40
|
var onCloneProject = opts.onCloneProject;
|
|
41
|
+
var onHomePreferenceMessage = opts.onHomePreferenceMessage;
|
|
42
|
+
var onAppMessage = opts.onAppMessage;
|
|
40
43
|
|
|
41
44
|
function handleMessage(ws, msg) {
|
|
42
45
|
if (!msg || typeof msg !== "object") return;
|
|
@@ -46,6 +49,14 @@ function attachGlobalWs(opts) {
|
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
// A user does not need an ordinary project before Home becomes useful.
|
|
53
|
+
// Route Mate discovery, Home chat/debates, and tools through the same
|
|
54
|
+
// handlers used by project sockets. This also lets mate_list repair an
|
|
55
|
+
// existing account whose built-ins were missing from its final storage.
|
|
56
|
+
if (typeof onAppMessage === "function" && onAppMessage(ws, msg)) return;
|
|
57
|
+
|
|
58
|
+
if (typeof onHomePreferenceMessage === "function" && onHomePreferenceMessage(ws, msg)) return;
|
|
59
|
+
|
|
49
60
|
// --- Directory picker for the add-project modal ---
|
|
50
61
|
if (msg.type === "browse_dir") {
|
|
51
62
|
var rawPath = (msg.path || "").replace(/^~/, config.REAL_HOME);
|
|
@@ -134,9 +134,24 @@ function historyToHomeChat(history, debateSetupMode, mateCreationMode) {
|
|
|
134
134
|
applyDebateHistoryEvent(messages, event);
|
|
135
135
|
continue;
|
|
136
136
|
}
|
|
137
|
+
if (event.type === "capsule_turn") {
|
|
138
|
+
// A Capsule engagement delivery. The transcript shows a short system
|
|
139
|
+
// note; the full delivery prompt is model-facing, not conversation.
|
|
140
|
+
flushAssistant();
|
|
141
|
+
messages.push({
|
|
142
|
+
role: "capsule_turn",
|
|
143
|
+
text: (event.toolName || event.toolId || "Capsule") + (event.kind === "start" ? ": a new game started." : ": your Mate is taking its turn."),
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
137
147
|
if (event.type === "user_message" && event.text && event.askUserAnswer !== true && !interviewMode) {
|
|
138
148
|
flushAssistant();
|
|
139
149
|
messages.push({ role: "user", text: event.text });
|
|
150
|
+
} else if (!interviewMode && (event.type === "tool_start" || event.type === "tool_executing" || event.type === "tool_result")) {
|
|
151
|
+
// A tool call ends the current spoken burst. Without this flush the
|
|
152
|
+
// text on both sides of the call concatenates into one run-on bubble,
|
|
153
|
+
// which is exactly how a Capsule game transcript turns into a wall.
|
|
154
|
+
flushAssistant();
|
|
140
155
|
} else if (event.type === "delta" && typeof event.text === "string" && !interviewMode) {
|
|
141
156
|
pending += event.text;
|
|
142
157
|
} else if (event.type === "result" || event.type === "done") {
|
|
@@ -185,13 +200,17 @@ function historyToHomeChat(history, debateSetupMode, mateCreationMode) {
|
|
|
185
200
|
return messages;
|
|
186
201
|
}
|
|
187
202
|
|
|
203
|
+
// The authoritative text for the bubble being finalized: only the deltas
|
|
204
|
+
// since the last conversational boundary. Tool calls and Capsule turn
|
|
205
|
+
// deliveries are boundaries too, or a session that is never driven by typed
|
|
206
|
+
// user messages (a Capsule game) would collapse into one run-on bubble.
|
|
188
207
|
function latestAssistantText(history) {
|
|
189
208
|
var text = "";
|
|
190
209
|
for (var i = (history || []).length - 1; i >= 0; i--) {
|
|
191
210
|
var event = history[i];
|
|
192
211
|
if (!event) continue;
|
|
193
212
|
if (event.type === "delta" && typeof event.text === "string") text = event.text + text;
|
|
194
|
-
if (event.type === "user_message" || event.type === "
|
|
213
|
+
if (event.type === "user_message" || event.type === "capsule_turn" || event.type === "tool_start" || event.type === "tool_executing" || event.type === "tool_result" || event.type === "debate_proposal" || event.type === "debate_proposal_resolved" || event.type === "project_assignment_proposal" || event.type === "project_assignment_status") break;
|
|
195
214
|
}
|
|
196
215
|
return text;
|
|
197
216
|
}
|
|
@@ -209,6 +228,9 @@ function searchToolLabel(name, input) {
|
|
|
209
228
|
if (name === "list_project_sessions") return "Reviewing project conversations";
|
|
210
229
|
if (name === "list_projects") return "Reviewing your projects";
|
|
211
230
|
if (name === "search_project_history") return query ? "Searching a project for \u201c" + query + "\u201d" : "Searching a project";
|
|
231
|
+
if (name === "search_project_logs") return query ? "Searching project logs for \u201c" + query + "\u201d" : "Searching project logs";
|
|
232
|
+
if (name === "list_project_logs") return "Reviewing project logs";
|
|
233
|
+
if (name === "read_project_log" || name === "read_project_log_revision" || name === "project_log_history") return "Reading a project log";
|
|
212
234
|
return "Using " + (cleanActivityText(name, 64) || "a workspace tool");
|
|
213
235
|
}
|
|
214
236
|
|
|
@@ -281,6 +303,14 @@ function transformEvent(event, mateId, session, requestId, stableSessionId) {
|
|
|
281
303
|
if (session && session.mateCreationMode === true && event.type === "tool_executing" && event.name === "AskUserQuestion" && event.id && event.input) return Object.assign({ type: "home_mate_creation_question", toolId: event.id, questions: homeDebateQuestions(event.input) }, metadata);
|
|
282
304
|
if (session && session.mateCreationMode === true && event.type === "ask_user_answered" && event.toolId) return Object.assign({ type: "home_mate_creation_question_resolved", toolId: event.toolId, status: "answered", answers: event.answers || null }, metadata);
|
|
283
305
|
if (session && session.mateCreationMode === true && event.type === "home_mate_creation_question_expired" && event.toolId) return Object.assign({ type: "home_mate_creation_question_resolved", toolId: event.toolId, status: "expired", error: event.error || "This question expired. Ask Clay to repeat it." }, metadata);
|
|
306
|
+
if ((event.type === "tool_start" || event.type === "tool_executing") && event.name !== "AskUserQuestion") {
|
|
307
|
+
// A tool call ends the current spoken burst: the client seals the bubble
|
|
308
|
+
// it has streamed so far, and later text starts a fresh one. Without this
|
|
309
|
+
// a session driven by tool play (a Capsule game) streams into one
|
|
310
|
+
// ever-growing run-on bubble. Last so every special surface above (search
|
|
311
|
+
// activity, debates, interviews, AskUserQuestion) keeps precedence.
|
|
312
|
+
return Object.assign({ type: "home_mate_segment" }, metadata);
|
|
313
|
+
}
|
|
284
314
|
return null;
|
|
285
315
|
}
|
|
286
316
|
|