clay-server 4.0.1-beta.1 → 4.1.0-beta.2

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.
Files changed (46) hide show
  1. package/lib/daemon-project-access.js +118 -0
  2. package/lib/daemon-projects.js +19 -5
  3. package/lib/daemon.js +19 -18
  4. package/lib/project-logs-access-scope.js +94 -0
  5. package/lib/project-logs-query.js +3 -0
  6. package/lib/project-logs-service.js +86 -84
  7. package/lib/project-logs-store.js +5 -3
  8. package/lib/project-pair-lifecycle.js +140 -102
  9. package/lib/project-pair-message.js +50 -0
  10. package/lib/project-pair-replacement-state.js +72 -0
  11. package/lib/project-pair-task-control.js +264 -0
  12. package/lib/project-pair-usage.js +76 -0
  13. package/lib/project-session-pair.js +115 -116
  14. package/lib/project-sessions.js +25 -1
  15. package/lib/project-worker-proposal.js +69 -58
  16. package/lib/project.js +21 -4
  17. package/lib/public/css/icon-strip.css +40 -0
  18. package/lib/public/modules/app-messages.js +9 -0
  19. package/lib/public/modules/project-access.js +141 -0
  20. package/lib/public/modules/project-grouping.js +22 -0
  21. package/lib/public/modules/sidebar-projects.js +14 -171
  22. package/lib/public/modules/worker-proposal.js +4 -0
  23. package/lib/sdk-bridge.js +20 -0
  24. package/lib/server-admin.js +3 -3
  25. package/lib/server-global-ws.js +10 -0
  26. package/lib/server-mates.js +4 -3
  27. package/lib/server-palette.js +17 -14
  28. package/lib/server.js +184 -102
  29. package/lib/session-pair-factory.js +3 -0
  30. package/lib/session-pair-history.js +40 -0
  31. package/lib/session-pair-mcp-server.js +19 -2
  32. package/lib/session-pair-operation.js +11 -0
  33. package/lib/session-pair-prompts.js +24 -2
  34. package/lib/session-pair-turn-control.js +8 -3
  35. package/lib/session-provenance.js +1 -0
  36. package/lib/session-spawn-mcp-server.js +2 -1
  37. package/lib/sessions.js +2 -0
  38. package/lib/worker-proposal-control.js +69 -0
  39. package/lib/worker-proposal-decision.js +7 -0
  40. package/lib/worker-runtime-catalog.js +52 -0
  41. package/lib/workspace-query-access.js +3 -2
  42. package/lib/workspace-query-service.js +1 -1
  43. package/lib/worktree.js +8 -4
  44. package/lib/ws-schema.js +1 -0
  45. package/lib/yoke/adapters/codex.js +25 -7
  46. package/package.json +1 -1
package/lib/sessions.js CHANGED
@@ -186,6 +186,7 @@ function createSessionManager(opts) {
186
186
  var provenanceMeta = sessionProvenance.metadata(session);
187
187
  metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
188
188
  if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
189
+ if (Array.isArray(session._workerGenerations)) metaObj.workerGenerations = session._workerGenerations;
189
190
  var meta = JSON.stringify(metaObj);
190
191
  var lines = [meta];
191
192
  for (var i = 0; i < session.history.length; i++) {
@@ -308,6 +309,7 @@ function createSessionManager(opts) {
308
309
  if (m.ownerId) session.ownerId = m.ownerId;
309
310
  session.sessionOriginId = m.sessionOriginId || null;
310
311
  session._provenanceMigrationNeeded = sessionProvenance.restore(m, session);
312
+ if (Array.isArray(m.workerGenerations)) session._workerGenerations = m.workerGenerations;
311
313
  // Born-TUI session: PTY is gone after restart, but the cliSessionId
312
314
  // is still resumable via `claude --resume <id>`. We mark the mode
313
315
  // here so it shows up in the sidebar with the right icon; the
@@ -0,0 +1,69 @@
1
+ var buildShape = require("./session-spawn-mcp-server").buildShape;
2
+
3
+ function findProposal(session, proposalId) {
4
+ var history = (session && session.history) || [];
5
+ for (var i = history.length - 1; i >= 0; i--) {
6
+ if (history[i] && history[i].type === "worker_proposal" && history[i].proposalId === proposalId) return history[i];
7
+ }
8
+ return null;
9
+ }
10
+
11
+ function pendingProposal(session) {
12
+ var history = (session && session.history) || [];
13
+ for (var i = history.length - 1; i >= 0; i--) {
14
+ var item = history[i];
15
+ if (!item || item.type !== "worker_proposal") continue;
16
+ if (item.status === "pending" || item.status === "starting") return item;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function projection(proposal) {
22
+ if (!proposal) return { status: "none", proposal: null };
23
+ return {
24
+ status: proposal.status,
25
+ proposal: {
26
+ proposalId: proposal.proposalId,
27
+ action: proposal.action === "replace" ? "replace" : "create",
28
+ summary: proposal.summary || "",
29
+ recommendedVendor: proposal.recommendedVendor || null,
30
+ recommendedModel: proposal.recommendedModel || null,
31
+ recommendedEffort: proposal.recommendedEffort || null,
32
+ status: proposal.status,
33
+ pending: proposal.status === "pending",
34
+ decisionRequired: proposal.status === "pending",
35
+ transactionId: proposal.transactionId || null,
36
+ sourceWorkerId: proposal.sourceWorkerId || null,
37
+ createdAt: proposal.createdAt || null,
38
+ updatedAt: proposal.updatedAt || null,
39
+ },
40
+ };
41
+ }
42
+
43
+ function getToolDefs(handlers) {
44
+ return [{
45
+ name: "inspect_worker_proposal",
46
+ description: "Inspect the exact unresolved Split Worker configuration proposal for this Driver. Returns none when no user decision is pending.",
47
+ inputSchema: buildShape({}),
48
+ handler: function () { return handlers.inspect(); },
49
+ }, {
50
+ name: "cancel_worker_proposal",
51
+ description: "Cancel one exact pending Split Worker configuration proposal. A proposal that is already accepted, starting, superseded, declined, or otherwise resolved cannot be cancelled.",
52
+ inputSchema: buildShape({
53
+ proposalId: { type: "string", description: "Exact proposal id returned by propose_worker, replace_partner, or inspect_worker_proposal." },
54
+ }, ["proposalId"]),
55
+ handler: function (args) { return handlers.cancel(args || {}); },
56
+ }, {
57
+ name: "worker_runtime_catalog",
58
+ description: "List server-observed executable Split Worker vendor, model, and effort combinations, including approval mode and temporary unavailability reasons.",
59
+ inputSchema: buildShape({}),
60
+ handler: function () { return handlers.catalog(); },
61
+ }];
62
+ }
63
+
64
+ module.exports = {
65
+ findProposal: findProposal,
66
+ getToolDefs: getToolDefs,
67
+ pendingProposal: pendingProposal,
68
+ projection: projection,
69
+ };
@@ -0,0 +1,7 @@
1
+ // An accepted execution record is not an unanswered configuration decision.
2
+ // Execution interruption and replacement safety belong to the pair lifecycle.
3
+ var pendingProposal = require("./worker-proposal-control").pendingProposal;
4
+
5
+ function hasPendingProposal(session) { return !!pendingProposal(session); }
6
+
7
+ module.exports = { hasPendingProposal: hasPendingProposal };
@@ -0,0 +1,52 @@
1
+ var yoke = require("./yoke");
2
+
3
+ function modelValue(entry) { return typeof entry === "string" ? entry : entry && (entry.value || entry.id) || ""; }
4
+
5
+ function build(options, fullAccess, preflight) {
6
+ var vendors = [];
7
+ var installed = options.installedVendors || [];
8
+ for (var i = 0; i < installed.length; i++) {
9
+ var vendor = installed[i];
10
+ var models = options.modelsByVendor[vendor] || [];
11
+ var capability = options.capabilitiesByVendor[vendor] || {};
12
+ var vendorInfo = yoke.getVendorInfo(vendor) || {};
13
+ var combinations = [];
14
+ var readyObserved = Array.isArray(options.availableVendors);
15
+ var adapterReady = !readyObserved || options.availableVendors.indexOf(vendor) !== -1;
16
+ for (var j = 0; j < models.length; j++) {
17
+ var model = modelValue(models[j]);
18
+ if (!model) continue;
19
+ var levels = models[j] && models[j].supportedEffortLevels;
20
+ if (!Array.isArray(levels) || levels.length === 0) levels = vendorInfo.effortLevels || [];
21
+ if (capability.effort === false || levels.length === 0) levels = [null];
22
+ for (var k = 0; k < levels.length; k++) {
23
+ try {
24
+ if (preflight) preflight(vendor, model, levels[k]);
25
+ if (adapterReady) combinations.push({ model: model, effort: levels[k] });
26
+ } catch (err) {}
27
+ }
28
+ }
29
+ vendors.push({
30
+ vendor: vendor,
31
+ status: combinations.length ? "available" : "temporarily_unavailable",
32
+ unavailableReason: combinations.length ? null : (!adapterReady ? "The runtime adapter is not currently available." : (options.unavailableReasons && options.unavailableReasons[vendor] || "No executable model and effort combination passed server preflight.")),
33
+ evidence: {
34
+ installation: "observed",
35
+ adapter: readyObserved ? (adapterReady ? "ready" : "unavailable") : "unknown",
36
+ authentication: "unknown",
37
+ authenticationReason: "Authentication is confirmed by the vendor runtime when a task starts; catalog discovery alone is not proof of credentials.",
38
+ },
39
+ combinations: combinations,
40
+ });
41
+ }
42
+ return {
43
+ status: "ok",
44
+ observedAt: Date.now(),
45
+ vendors: vendors,
46
+ approval: fullAccess
47
+ ? { mode: "conditional_auto_accept", reason: "Only an exact server-validated Driver recommendation may auto-accept." }
48
+ : { mode: "user_required", reason: "Runtime creation and replacement require the visible configuration decision." },
49
+ };
50
+ }
51
+
52
+ module.exports = { build: build };
@@ -77,10 +77,11 @@ function attachWorkspaceQueryAccess(deps) {
77
77
 
78
78
  // `accessible` decides whether the container is visible at all; `owned`
79
79
  // drives the separate rule that an accessible container holding no session of
80
- // the caller's own is omitted. Worktrees are never surfaced directly.
80
+ // the caller's own is omitted. Exact worktree grants are resolved from their
81
+ // own authoritative access record rather than inferred from the parent.
81
82
  function evaluate(principal, status) {
82
83
  var denied = { owned: false, accessible: false };
83
- if (!principal || !status || status.isWorktree) return denied;
84
+ if (!principal || !status) return denied;
84
85
  if (typeof isMultiUser !== "function" || !isMultiUser()) {
85
86
  var owned = ownsSingleUser(principal, status);
86
87
  return { owned: owned, accessible: owned };
@@ -181,7 +181,7 @@ function attachWorkspaceQueryService(ctx) {
181
181
  var projects = [];
182
182
  getProjects().forEach(function (project, slug) {
183
183
  var status = project.getStatus();
184
- if (!status || status.isWorktree) return;
184
+ if (!status) return;
185
185
  // Authorized here rather than at bind time, so an already-created tool
186
186
  // handler loses a project the moment its access record changes.
187
187
  var verdict = projectAccess.evaluate(principal, status);
package/lib/worktree.js CHANGED
@@ -55,7 +55,7 @@ function worktreePath(projectPath, dirName) {
55
55
  // Scan worktrees for a given project path
56
56
  // Returns array of { path, branch, bare, detached, external }
57
57
  // external = true when Git registered the worktree outside the main project folder
58
- function scanWorktrees(projectPath, osUserInfo) {
58
+ function scanWorktreesResult(projectPath, osUserInfo) {
59
59
  var resolvedParent = path.resolve(projectPath);
60
60
  try { resolvedParent = fs.realpathSync(resolvedParent); } catch (e) {}
61
61
  try {
@@ -76,12 +76,16 @@ function scanWorktrees(projectPath, osUserInfo) {
76
76
  wt.dirName = path.basename(wt.path);
77
77
  results.push(wt);
78
78
  }
79
- return results;
79
+ return { ok: true, worktrees: results };
80
80
  } catch (e) {
81
- return [];
81
+ return { ok: false, worktrees: [], error: e.message || "Failed to scan worktrees" };
82
82
  }
83
83
  }
84
84
 
85
+ function scanWorktrees(projectPath, osUserInfo) {
86
+ return scanWorktreesResult(projectPath, osUserInfo).worktrees;
87
+ }
88
+
85
89
  // Create a new worktree inside the parent project directory
86
90
  // Returns { ok, path, error }
87
91
  function createWorktree(projectPath, branchName, dirName, baseBranch, osUserInfo) {
@@ -127,4 +131,4 @@ function removeWorktree(projectPath, worktreeDirName, osUserInfo) {
127
131
  }
128
132
  }
129
133
 
130
- module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree, isPathInside: isPathInside };
134
+ module.exports = { scanWorktrees: scanWorktrees, scanWorktreesResult: scanWorktreesResult, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree, isPathInside: isPathInside };
package/lib/ws-schema.js CHANGED
@@ -72,6 +72,7 @@ var schema = {
72
72
  "home_project_assignment_proposal": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Home-projected exact-session assignment approval card" },
73
73
  "home_project_assignment_status": { direction: "s2c", handler: "lib/public/modules/app-message-router.js", description: "Home-projected exact-session assignment lifecycle" },
74
74
  "split_delegation": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "A pair delegation started or ended (drives the split-view flow indicator)" },
75
+ "partner_task_completed": { direction: "s2c", handler: "lib/public/modules/app-messages.js", description: "Structured completion event for one correlated Split Worker task" },
75
76
 
76
77
  // -----------------------------------------------------------------------
77
78
  // History replay
@@ -193,7 +193,8 @@ function flattenEvent(notification, state) {
193
193
  if (turnStatus === "interrupted" || state.aborted) {
194
194
  events.push({ yokeType: "interrupted" });
195
195
  }
196
- var inputTokens = state.lastInputTokens || (usage ? (usage.input_tokens || 0) + (usage.cached_input_tokens || 0) : 0);
196
+ var inputTokens = state.lastInputTokens !== null && state.lastInputTokens !== undefined
197
+ ? state.lastInputTokens : (usage ? (usage.input_tokens || 0) : 0);
197
198
  var outputTokens = (usage ? (usage.output_tokens || 0) : 0) || state.lastOutputTokens || 0;
198
199
  var cachedTokens = (usage ? (usage.cached_input_tokens || 0) : 0) || state.lastCachedTokens || 0;
199
200
  var hasTokenData = inputTokens > 0 || outputTokens > 0;
@@ -217,7 +218,7 @@ function flattenEvent(notification, state) {
217
218
  } : null,
218
219
  modelUsage: resultModelUsage,
219
220
  sessionId: state.threadId || null,
220
- lastStreamInputTokens: state.lastInputTokens || null,
221
+ lastStreamInputTokens: null,
221
222
  });
222
223
  state.lastInputTokens = null;
223
224
  state.lastCachedTokens = null;
@@ -675,15 +676,28 @@ function flattenEvent(notification, state) {
675
676
  // context gauge must use the LAST turn, not the running total: each turn's
676
677
  // input already contains the whole conversation, so summing turns
677
678
  // overstates occupancy (a 5k + 8k thread reads as 13k of an 8k context).
678
- // `total` is only a fallback for servers that omit lastTurn.
679
+ // A missing lastTurn is not evidence of current context occupancy. The
680
+ // protocol's total is cumulative across the thread and must never be used
681
+ // as the context gauge.
679
682
  if (method === "thread/tokenUsage/updated") {
680
683
  var tu = params.tokenUsage;
681
684
  if (tu) {
682
- var turnUsage = tu.lastTurn || tu.total;
685
+ var turnUsage = tu.lastTurn;
683
686
  if (turnUsage) {
684
- state.lastInputTokens = (turnUsage.inputTokens || 0) + (turnUsage.cachedInputTokens || 0);
687
+ var candidateInput = typeof turnUsage.inputTokens === "number" ? turnUsage.inputTokens : null;
688
+ var currentWindow = typeof tu.modelContextWindow === "number" ? tu.modelContextWindow : null;
689
+ state.lastInputTokens = candidateInput !== null && (currentWindow === null || candidateInput <= currentWindow) ? candidateInput : null;
685
690
  state.lastCachedTokens = turnUsage.cachedInputTokens || 0;
686
691
  state.lastOutputTokens = turnUsage.outputTokens || 0;
692
+ state.currentContextUsage = state.lastInputTokens !== null &&
693
+ (currentWindow === null || state.lastInputTokens <= currentWindow)
694
+ ? { input_tokens: state.lastInputTokens, contextWindow: currentWindow }
695
+ : { input_tokens: null, contextWindow: currentWindow };
696
+ } else {
697
+ state.lastInputTokens = null;
698
+ state.lastCachedTokens = null;
699
+ state.lastOutputTokens = null;
700
+ state.currentContextUsage = null;
687
701
  }
688
702
  if (tu.modelContextWindow) state.modelContextWindow = tu.modelContextWindow;
689
703
  }
@@ -734,6 +748,7 @@ function createEventState(model) {
734
748
  lastInputTokens: null,
735
749
  lastCachedTokens: null,
736
750
  lastOutputTokens: null,
751
+ currentContextUsage: null,
737
752
  modelContextWindow: null,
738
753
  done: false,
739
754
  aborted: false,
@@ -1107,6 +1122,9 @@ function createCodexQueryHandle(appServer, queryOpts) {
1107
1122
 
1108
1123
  var threadResult;
1109
1124
  if (queryOpts.resumeSessionId) {
1125
+ // ThreadResumeParams in the installed app-server schema has no
1126
+ // dynamicTools field. New tools therefore require a new Driver/Worker
1127
+ // thread; resumed threads can only refresh handlers for persisted tools.
1110
1128
  threadResult = await appServer.send("thread/resume", {
1111
1129
  threadId: queryOpts.resumeSessionId,
1112
1130
  model: threadParams.model,
@@ -1269,7 +1287,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
1269
1287
  },
1270
1288
 
1271
1289
  getContextUsage: function() {
1272
- return Promise.resolve(state.lastUsage);
1290
+ return Promise.resolve(state.currentContextUsage);
1273
1291
  },
1274
1292
 
1275
1293
  abort: function() {
@@ -1342,8 +1360,8 @@ function createCodexAdapter(opts) {
1342
1360
  // listing them would only surface a runtime failure in the model picker.
1343
1361
  var CODEX_MODELS = [
1344
1362
  "gpt-6-astra",
1345
- "gpt-5.6-terra",
1346
1363
  "gpt-5.6-sol",
1364
+ "gpt-5.6-terra",
1347
1365
  "gpt-5.6-luna",
1348
1366
  "gpt-5.5",
1349
1367
  "gpt-5.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.0.1-beta.1",
3
+ "version": "4.1.0-beta.2",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",