clay-server 4.0.0-beta.9 → 4.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +71 -16
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +191 -6
- package/lib/public/app.js +64 -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 +45 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-message-router.js +1 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/search-clay-chat.js +47 -6
- package/lib/public/modules/search-clay-controls.js +86 -0
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +136 -47
- package/lib/sdk-message-processor.js +36 -7
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +34 -1
- package/lib/server-home-chat.js +62 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +44 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
var pairMcp = require("./session-pair-mcp-server");
|
|
2
|
+
var pairPrompts = require("./session-pair-prompts");
|
|
3
|
+
var { attachPairFactory } = require("./session-pair-factory");
|
|
4
|
+
var { attachPairLifecycle } = require("./project-pair-lifecycle");
|
|
5
|
+
var { attachPairTurnControl } = require("./session-pair-turn-control");
|
|
6
|
+
var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration");
|
|
7
|
+
var { attachWorkerPermission } = require("./project-worker-permission");
|
|
2
8
|
var { attachWorkerProposal } = require("./project-worker-proposal");
|
|
3
|
-
var yoke = require("./yoke");
|
|
4
|
-
|
|
5
9
|
var MAX_RESPONSE_CHARS = 30000;
|
|
6
|
-
|
|
7
|
-
function toolResult(value) {
|
|
8
|
-
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
9
|
-
}
|
|
10
|
-
|
|
10
|
+
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
|
|
11
11
|
function toolError(err) {
|
|
12
12
|
return Promise.resolve({
|
|
13
13
|
content: [{ type: "text", text: "Error: " + (err.message || err) }],
|
|
14
14
|
isError: true,
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
-
|
|
18
17
|
function responseText(history, fromIndex) {
|
|
19
18
|
var text = "";
|
|
20
19
|
for (var i = Math.max(0, fromIndex || 0); i < history.length; i++) {
|
|
@@ -59,11 +58,17 @@ function attachSessionPair(ctx) {
|
|
|
59
58
|
var sm = ctx.sm;
|
|
60
59
|
var store = ctx.splitStore;
|
|
61
60
|
var workerProposal;
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
var workerPermission;
|
|
62
|
+
var lifecycle;
|
|
63
|
+
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
|
|
64
|
+
function groupAndPartner(caller, missingPairMessage) {
|
|
64
65
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
66
|
+
// Captured handlers must remain bound to the exact live Session object.
|
|
67
|
+
if (sm.sessions.get(caller.localId) !== caller) {
|
|
68
|
+
throw new Error("this session is no longer live; the partner tools are bound to an exact session");
|
|
69
|
+
}
|
|
65
70
|
var group = store.groupForMember(caller.localId);
|
|
66
|
-
if (!group) throw new Error("this session is not in a split group");
|
|
71
|
+
if (!group) throw new Error(missingPairMessage || "this session is not in a split group");
|
|
67
72
|
if (group.pair && group.pair.driverId !== caller.localId) {
|
|
68
73
|
throw new Error("only the configured Driver can direct this pair");
|
|
69
74
|
}
|
|
@@ -93,27 +98,19 @@ function attachSessionPair(ctx) {
|
|
|
93
98
|
broadcastDelegation(group, caller, partner, false);
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
function
|
|
97
|
-
if (!
|
|
98
|
-
|
|
99
|
-
var
|
|
100
|
-
var response = token.response || "";
|
|
101
|
-
var text;
|
|
102
|
-
if (token.interrupted) {
|
|
103
|
-
text = "[Split Worker execution interrupted] The user interrupted the Split Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
|
|
104
|
-
} else {
|
|
105
|
-
text = "A Split Worker task delegated through send_to_partner has finished.\n\n" +
|
|
106
|
-
"Original task:\n" + token.message + "\n\n" +
|
|
107
|
-
(failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) +
|
|
108
|
-
"\n\nReview the result, verify it as needed, and continue the task.";
|
|
109
|
-
}
|
|
110
|
-
sm.sendAndRecord(caller, {
|
|
101
|
+
function resumeDriverWithMessage(caller, text, meta) {
|
|
102
|
+
if (!caller || caller.destroying) return false;
|
|
103
|
+
if (turnControl.blockedReason(caller)) return false;
|
|
104
|
+
var record = {
|
|
111
105
|
type: "user_message",
|
|
112
106
|
text: text,
|
|
113
107
|
_internal: true,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
};
|
|
109
|
+
if (meta) {
|
|
110
|
+
var keys = Object.keys(meta);
|
|
111
|
+
for (var i = 0; i < keys.length; i++) record[keys[i]] = meta[keys[i]];
|
|
112
|
+
}
|
|
113
|
+
sm.sendAndRecord(caller, record);
|
|
117
114
|
caller.lastActivity = Date.now();
|
|
118
115
|
var sdk = ctx.getSdk();
|
|
119
116
|
if (!sdk) {
|
|
@@ -138,6 +135,33 @@ function attachSessionPair(ctx) {
|
|
|
138
135
|
return true;
|
|
139
136
|
}
|
|
140
137
|
|
|
138
|
+
function resumeDriverWithResult(caller, partner, token) {
|
|
139
|
+
if (!token.detached || token.delivered || caller.destroying) return false;
|
|
140
|
+
token.delivered = true;
|
|
141
|
+
var failure = token.failure || null;
|
|
142
|
+
var response = token.response || "";
|
|
143
|
+
var text;
|
|
144
|
+
if (token.interrupted) {
|
|
145
|
+
text = "[Split Worker execution interrupted] The user interrupted the Split Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
|
|
146
|
+
} else {
|
|
147
|
+
text = "A Split Worker task delegated through send_to_partner has finished.\n\n" +
|
|
148
|
+
"Original task:\n" + token.message + "\n\n" +
|
|
149
|
+
(failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) +
|
|
150
|
+
"\n\nReview the result, verify it as needed, and continue the task.";
|
|
151
|
+
}
|
|
152
|
+
return resumeDriverWithMessage(caller, text, {
|
|
153
|
+
partnerResult: true,
|
|
154
|
+
partnerSessionId: partner.localId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requestDetach(worker) {
|
|
159
|
+
var token = worker && worker._pairDelegation;
|
|
160
|
+
if (!token || token.detached || token.detachRequested) return false;
|
|
161
|
+
token.detachRequested = true;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
141
165
|
function handleTurnDone(partner) {
|
|
142
166
|
var token = partner && partner._pairDelegation;
|
|
143
167
|
if (!token) return false;
|
|
@@ -179,7 +203,8 @@ function attachSessionPair(ctx) {
|
|
|
179
203
|
});
|
|
180
204
|
return;
|
|
181
205
|
}
|
|
182
|
-
|
|
206
|
+
// Timeouts and permission requests both detach into the push-back path.
|
|
207
|
+
if (Date.now() >= deadline || token.detachRequested) {
|
|
183
208
|
clearInterval(timer);
|
|
184
209
|
token.detached = true;
|
|
185
210
|
monitorPartner(group, caller, partner, token);
|
|
@@ -203,13 +228,11 @@ function attachSessionPair(ctx) {
|
|
|
203
228
|
async function sendToPartner(args, caller) {
|
|
204
229
|
try {
|
|
205
230
|
if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
|
|
231
|
+
var resolved = groupAndPartner(caller,
|
|
232
|
+
"send_to_partner requires an exact existing Driver/Split Worker pair; call propose_worker while unpaired");
|
|
206
233
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
207
234
|
if (!message) throw new Error("message is required");
|
|
208
|
-
|
|
209
|
-
if (caller && !store.groupForMember(caller.localId)) {
|
|
210
|
-
created = createWorkerForDriver(caller, args);
|
|
211
|
-
}
|
|
212
|
-
var resolved = groupAndPartner(caller);
|
|
235
|
+
turnControl.assertWorkerAction(caller);
|
|
213
236
|
var wait = args.wait !== false;
|
|
214
237
|
var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
|
|
215
238
|
timeout = Math.max(1, Math.min(900, timeout));
|
|
@@ -247,13 +270,18 @@ function attachSessionPair(ctx) {
|
|
|
247
270
|
if (!wait) {
|
|
248
271
|
token.detached = true;
|
|
249
272
|
monitorPartner(resolved.group, caller, partner, token);
|
|
250
|
-
return toolResult({ status: "running", partnerId: partner.localId,
|
|
273
|
+
return toolResult({ status: "running", partnerId: partner.localId, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
|
|
251
274
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
275
|
+
if (!partner.isProcessing && !partner._queryStarting) {
|
|
276
|
+
finishDelegation(resolved.group, caller, partner, token);
|
|
277
|
+
var immediateFailure = errorSince(partner.history || [], token.startIndex);
|
|
278
|
+
return toolResult({
|
|
279
|
+
status: immediateFailure ? "error" : "complete",
|
|
280
|
+
response: responseText(partner.history || [], token.startIndex),
|
|
281
|
+
error: immediateFailure || undefined,
|
|
282
|
+
});
|
|
256
283
|
}
|
|
284
|
+
var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
|
|
257
285
|
return toolResult(completed);
|
|
258
286
|
} catch (e) {
|
|
259
287
|
if (caller) {
|
|
@@ -274,13 +302,15 @@ function attachSessionPair(ctx) {
|
|
|
274
302
|
try {
|
|
275
303
|
var resolved = groupAndPartner(caller);
|
|
276
304
|
var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
|
|
277
|
-
count = Math.max(
|
|
278
|
-
|
|
305
|
+
count = Math.max(0, Math.min(5, count));
|
|
306
|
+
var payload = {
|
|
279
307
|
status: resolved.partner.isProcessing ? "running" : (resolved.partner._lastTurnInterrupted ? "interrupted" : "idle"),
|
|
280
308
|
partnerId: resolved.partner.localId,
|
|
281
309
|
title: resolved.partner.title || "New Session",
|
|
282
|
-
turns: recentTurns(resolved.partner, count),
|
|
283
|
-
}
|
|
310
|
+
turns: count > 0 ? recentTurns(resolved.partner, count) : [],
|
|
311
|
+
};
|
|
312
|
+
payload.capacity = lifecycle.optionalStatus(caller);
|
|
313
|
+
return toolResult(payload);
|
|
284
314
|
} catch (e) {
|
|
285
315
|
return toolError(e);
|
|
286
316
|
}
|
|
@@ -311,6 +341,7 @@ function attachSessionPair(ctx) {
|
|
|
311
341
|
if (partner.abortController) partner.abortController.abort();
|
|
312
342
|
}
|
|
313
343
|
if (partner._pairDelegation) finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
|
|
344
|
+
workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
|
|
314
345
|
var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
|
|
315
346
|
var result = store.dissolve(ws, { id: resolved.group.id });
|
|
316
347
|
if (!result.ok) throw new Error(result.error || "could not close the Worker pair");
|
|
@@ -320,115 +351,85 @@ function attachSessionPair(ctx) {
|
|
|
320
351
|
}
|
|
321
352
|
}
|
|
322
353
|
|
|
354
|
+
function handleHumanStop(session) {
|
|
355
|
+
var roles = turnControl.markHumanStop(session);
|
|
356
|
+
if (!roles) return false;
|
|
357
|
+
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
323
361
|
function getToolDefs(boundSession) {
|
|
324
362
|
if (!boundSession) return pairMcp.getToolDefs({
|
|
325
363
|
send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
|
|
326
364
|
read: function () { return toolError(new Error("read_partner requires a session-bound tool server")); },
|
|
327
365
|
interrupt: function () { return toolError(new Error("interrupt_partner requires a session-bound tool server")); },
|
|
328
366
|
close: function () { return toolError(new Error("close_partner requires a session-bound tool server")); },
|
|
367
|
+
status: function () { return toolError(new Error("partner_status requires a session-bound tool server")); },
|
|
368
|
+
replace: function () { return toolError(new Error("replace_partner requires a session-bound tool server")); },
|
|
369
|
+
evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
|
|
329
370
|
});
|
|
330
|
-
// Mount whenever we have a bound session: MCP servers are fixed for the
|
|
331
|
-
// lifetime of a query, and Claude queries live across turns, so gating on
|
|
332
|
-
// group membership HERE would permanently hide the tools from a session
|
|
333
|
-
// whose split is created mid-conversation (the ad-hoc flow). Handlers
|
|
334
|
-
// re-resolve the group on every call, so an ungrouped session gets a
|
|
335
|
-
// clear error, and a session that gains a partner later just works.
|
|
336
|
-
// The one structural exclusion: a session that is ALREADY a configured
|
|
337
|
-
// pair worker at query start never sees the tools.
|
|
338
371
|
var group = store.groupForMember(boundSession.localId);
|
|
339
372
|
if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
|
|
373
|
+
var adHocSplit = !!(group && !group.pair);
|
|
374
|
+
if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
|
|
375
|
+
var persistentCatalog = boundSession.vendor === "codex" && (!group || !!group.pair);
|
|
376
|
+
if (!group && !persistentCatalog) {
|
|
377
|
+
return workerProposal.getToolDefs(boundSession)
|
|
378
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
|
|
379
|
+
}
|
|
380
|
+
var lifecycleHandlers = lifecycle.toolHandlers(boundSession);
|
|
340
381
|
var tools = pairMcp.getToolDefs({
|
|
341
|
-
send: function (args) {
|
|
382
|
+
send: function (args) {
|
|
383
|
+
return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
|
|
384
|
+
return sendToPartner(args || {}, boundSession);
|
|
385
|
+
});
|
|
386
|
+
},
|
|
342
387
|
read: function (args) { return readPartner(args, boundSession); },
|
|
343
388
|
interrupt: function (args) { return interruptPartner(args, boundSession); },
|
|
344
389
|
close: function (args) { return closePartner(args, boundSession); },
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
var driverSpec = msg.driver || {};
|
|
358
|
-
var workerSpec = msg.worker || {};
|
|
359
|
-
var workerVendor = validateVendor(workerSpec.vendor || sm.lastVendor || "codex");
|
|
360
|
-
var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
|
|
361
|
-
var driver;
|
|
362
|
-
var groupName;
|
|
363
|
-
if (Number.isInteger(driverSpec.sessionId)) {
|
|
364
|
-
// "Add Split Worker" on an existing session: the current session becomes
|
|
365
|
-
// the Driver with its full conversation context intact.
|
|
366
|
-
driver = sm.sessions.get(driverSpec.sessionId);
|
|
367
|
-
if (!driver) throw new Error("driver session not found");
|
|
368
|
-
if ((driver.ownerId || null) !== ownerId) throw new Error("driver session access denied");
|
|
369
|
-
if (store.groupForMember(driver.localId)) throw new Error("this session is already in a split group");
|
|
370
|
-
groupName = undefined; // auto name from member titles
|
|
371
|
-
} else {
|
|
372
|
-
var driverVendor = validateVendor(driverSpec.vendor || "claude");
|
|
373
|
-
var driverEffort = yoke.clampEffort(driverVendor, driverSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[driverVendor]) || sm.currentEffort || "medium") || null;
|
|
374
|
-
driver = sm.createSessionRaw({ ownerId: ownerId, vendor: driverVendor, model: driverSpec.model || null, effort: driverEffort });
|
|
375
|
-
driver.title = "Driver · " + ((yoke.getVendorInfo(driverVendor) || {}).displayName || driverVendor);
|
|
376
|
-
if (driverSpec.effort) driver.loopSettings = { effort: driverSpec.effort };
|
|
377
|
-
groupName = "Agent pair";
|
|
390
|
+
status: lifecycleHandlers.status,
|
|
391
|
+
replace: function (args) {
|
|
392
|
+
return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
|
|
393
|
+
return workerProposal.proposeReplacement(args || {}, boundSession);
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
evaluate: lifecycleHandlers.evaluate,
|
|
397
|
+
}, { lifecycle: persistentCatalog || !adHocSplit });
|
|
398
|
+
if (persistentCatalog) {
|
|
399
|
+
return workerProposal.getToolDefs(boundSession, { persistent: true })
|
|
400
|
+
.concat(tools)
|
|
401
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
|
|
378
402
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
worker.title = "Split Worker · " + ((yoke.getVendorInfo(workerVendor) || {}).displayName || workerVendor);
|
|
382
|
-
if (workerSpec.effort) worker.loopSettings = { effort: workerSpec.effort };
|
|
383
|
-
var result = store.create(ws, {
|
|
384
|
-
members: [driver.localId, worker.localId],
|
|
385
|
-
pair: { driverId: driver.localId, workerId: worker.localId },
|
|
386
|
-
name: groupName,
|
|
387
|
-
});
|
|
388
|
-
if (!result.ok) throw new Error(result.error);
|
|
389
|
-
sm.broadcastSessionList();
|
|
390
|
-
return { driver: driver, worker: worker, group: result.group };
|
|
403
|
+
return tools
|
|
404
|
+
.concat(workerPermission.getToolDefs(boundSession, { dormantDriver: false }));
|
|
391
405
|
}
|
|
392
406
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (vendor) validateVendor(vendor);
|
|
399
|
-
if (!vendor) {
|
|
400
|
-
if (installed.indexOf("codex") !== -1 && driver.vendor !== "codex") vendor = "codex";
|
|
401
|
-
else {
|
|
402
|
-
vendor = installed[0];
|
|
403
|
-
for (var i = 0; i < installed.length; i++) {
|
|
404
|
-
if (installed[i] !== driver.vendor) { vendor = installed[i]; break; }
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
var ws = {
|
|
409
|
-
_clayActiveSession: driver.localId,
|
|
410
|
-
_clayUser: driver.ownerId ? { id: driver.ownerId } : null,
|
|
411
|
-
};
|
|
412
|
-
var created = createPairRecord(ws, {
|
|
413
|
-
driver: { sessionId: driver.localId },
|
|
414
|
-
worker: {
|
|
415
|
-
vendor: vendor,
|
|
416
|
-
model: typeof args.workerModel === "string" ? args.workerModel : "",
|
|
417
|
-
effort: typeof args.workerEffort === "string" ? args.workerEffort : "",
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
sm.sendToSession(driver, { type: "pair_session_created", ok: true, group: created.group });
|
|
421
|
-
return created;
|
|
422
|
-
}
|
|
407
|
+
var factory = attachPairFactory(ctx);
|
|
408
|
+
var createPairRecord = factory.createPairRecord;
|
|
409
|
+
var createWorkerForDriver = factory.createWorkerForDriver;
|
|
410
|
+
var createPair = factory.createPair;
|
|
411
|
+
var preflightWorkerForDriver = factory.preflightWorkerForDriver;
|
|
423
412
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
413
|
+
lifecycle = attachPairLifecycle({
|
|
414
|
+
sm: sm,
|
|
415
|
+
splitStore: store,
|
|
416
|
+
createWorkerForDriver: function (driver, args) { return createWorkerForDriver(driver, args); },
|
|
417
|
+
preflightWorkerForDriver: function (driver, args) { return preflightWorkerForDriver(driver, args); },
|
|
418
|
+
sendToPartner: function (args, caller) { return sendToPartner(args, caller); },
|
|
419
|
+
finishDelegation: finishDelegation,
|
|
420
|
+
cancelWorkerPermissions: function (worker, reason) {
|
|
421
|
+
return workerPermission.cancelForSession(worker, reason);
|
|
422
|
+
},
|
|
423
|
+
turnControl: turnControl,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
workerPermission = attachWorkerPermission({
|
|
427
|
+
sm: sm,
|
|
428
|
+
splitStore: store,
|
|
429
|
+
onProcessingChanged: ctx.onProcessingChanged,
|
|
430
|
+
resumeDriverWithMessage: resumeDriverWithMessage,
|
|
431
|
+
requestDetach: requestDetach,
|
|
432
|
+
});
|
|
432
433
|
|
|
433
434
|
workerProposal = attachWorkerProposal({
|
|
434
435
|
sm: sm,
|
|
@@ -440,8 +441,11 @@ function attachSessionPair(ctx) {
|
|
|
440
441
|
getLinuxUserForSession: ctx.getLinuxUserForSession,
|
|
441
442
|
onProcessingChanged: ctx.onProcessingChanged,
|
|
442
443
|
adapters: ctx.adapters,
|
|
444
|
+
dangerouslySkipPermissions: ctx.dangerouslySkipPermissions,
|
|
443
445
|
createPairRecord: createPairRecord,
|
|
446
|
+
recordGenerationStart: function (driver, worker) { return lifecycle.recordGenerationStart(driver, worker); },
|
|
444
447
|
sendToPartner: sendToPartner,
|
|
448
|
+
replacePartner: function (args, session) { return lifecycle.replacePartner(args, session); },
|
|
445
449
|
});
|
|
446
450
|
|
|
447
451
|
function handleMessage(ws, msg) {
|
|
@@ -468,18 +472,27 @@ function attachSessionPair(ctx) {
|
|
|
468
472
|
var group = store.groupForMember(session.localId);
|
|
469
473
|
var pairPrompt = "";
|
|
470
474
|
if (group && group.pair && group.pair.driverId === session.localId) {
|
|
471
|
-
pairPrompt =
|
|
472
|
-
|
|
473
|
-
|
|
475
|
+
pairPrompt = pairPrompts.DRIVER_CORE;
|
|
476
|
+
if (driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
477
|
+
pairPrompt += " " + pairPrompts.DRIVER_DELEGATION;
|
|
478
|
+
}
|
|
479
|
+
} else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, sm) &&
|
|
480
|
+
driverOrchestration.isHighTierDriverSession(session, sm)) {
|
|
481
|
+
pairPrompt = pairPrompts.UNPAIRED + " " + workerProposal.getSystemPrompt(session);
|
|
474
482
|
}
|
|
475
|
-
return
|
|
483
|
+
return pairPrompt;
|
|
476
484
|
}
|
|
477
485
|
|
|
478
|
-
return {
|
|
486
|
+
return {
|
|
487
|
+
beginHumanTurn: turnControl.beginHumanTurn,
|
|
488
|
+
getToolDefs: getToolDefs,
|
|
489
|
+
handleHumanStop: handleHumanStop,
|
|
490
|
+
handleMessage: handleMessage,
|
|
491
|
+
handleTurnDone: handleTurnDone,
|
|
492
|
+
getSystemPrompt: getSystemPrompt,
|
|
493
|
+
respondToWorkerProposal: workerProposal.respondToProposal,
|
|
494
|
+
workerPermission: workerPermission,
|
|
495
|
+
};
|
|
479
496
|
}
|
|
480
497
|
|
|
481
|
-
module.exports = {
|
|
482
|
-
attachSessionPair: attachSessionPair,
|
|
483
|
-
responseText: responseText,
|
|
484
|
-
recentTurns: recentTurns,
|
|
485
|
-
};
|
|
498
|
+
module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
|
|
@@ -198,7 +198,7 @@ function attachSessionSpawn(ctx) {
|
|
|
198
198
|
var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
|
|
199
199
|
var session = create.call(sm, {
|
|
200
200
|
ownerId: parent.ownerId || null,
|
|
201
|
-
sessionVisibility:
|
|
201
|
+
sessionVisibility: "private",
|
|
202
202
|
vendor: vendor,
|
|
203
203
|
});
|
|
204
204
|
session.spawn = { parentId: parent.localId, index: index, batchId: batchId };
|
package/lib/project-sessions.js
CHANGED
|
@@ -3,7 +3,9 @@ var path = require("path");
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var { execFileSync } = require("child_process");
|
|
5
5
|
var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
|
|
6
|
+
var updateSnooze = require("./update-snooze");
|
|
6
7
|
var yoke = require("./yoke");
|
|
8
|
+
var sessionVisibility = require("./session-visibility");
|
|
7
9
|
|
|
8
10
|
function vendorSupportsTui(vendor) {
|
|
9
11
|
var info = yoke.getVendorInfo(vendor);
|
|
@@ -71,6 +73,9 @@ function attachSessions(ctx) {
|
|
|
71
73
|
var send = ctx.send;
|
|
72
74
|
var sendTo = ctx.sendTo;
|
|
73
75
|
var sendToAdmins = ctx.sendToAdmins;
|
|
76
|
+
var broadcastUpdateAvailable = ctx.broadcastUpdateAvailable || function () {};
|
|
77
|
+
var broadcastUpdateStarted = ctx.broadcastUpdateStarted || function () {};
|
|
78
|
+
var sendToUpdateUser = ctx.sendToUpdateUser || function () {};
|
|
74
79
|
var sendToSession = ctx.sendToSession;
|
|
75
80
|
var sendToSessionOthers = ctx.sendToSessionOthers;
|
|
76
81
|
var opts = ctx.opts;
|
|
@@ -82,6 +87,13 @@ function attachSessions(ctx) {
|
|
|
82
87
|
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
83
88
|
var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
|
|
84
89
|
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
90
|
+
function stopSession(session) {
|
|
91
|
+
if (!session || (!session.isProcessing && !session._queryStarting)) return false;
|
|
92
|
+
if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
|
|
93
|
+
session.taskStopRequested = true;
|
|
94
|
+
if (session.abortController) session.abortController.abort();
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
85
97
|
var hydrateImageRefs = ctx.hydrateImageRefs;
|
|
86
98
|
var onProcessingChanged = ctx.onProcessingChanged;
|
|
87
99
|
var broadcastPresence = ctx.broadcastPresence;
|
|
@@ -492,7 +504,7 @@ function attachSessions(ctx) {
|
|
|
492
504
|
sm.sweepBlankSessions();
|
|
493
505
|
var sessionOpts = {};
|
|
494
506
|
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
495
|
-
|
|
507
|
+
sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
|
|
496
508
|
if (msg.vendor) sessionOpts.vendor = msg.vendor;
|
|
497
509
|
// Mode resolution: vendors without a TUI session mode are always GUI.
|
|
498
510
|
// TUI-capable sessions honor the explicit msg.mode if provided, otherwise
|
|
@@ -577,7 +589,8 @@ function attachSessions(ctx) {
|
|
|
577
589
|
if (reusable) {
|
|
578
590
|
if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
|
|
579
591
|
if (!reusable.effort) reusable.effort = sessionOpts.effort;
|
|
580
|
-
|
|
592
|
+
sm.setSessionVisibility(reusable.localId, sessionOpts.sessionVisibility);
|
|
593
|
+
sessionVisibility.revokeViewers(ctx, reusable);
|
|
581
594
|
sm.switchSession(reusable.localId, ws);
|
|
582
595
|
newSess = reusable;
|
|
583
596
|
} else {
|
|
@@ -605,9 +618,7 @@ function attachSessions(ctx) {
|
|
|
605
618
|
}
|
|
606
619
|
|
|
607
620
|
if (msg.type === "set_session_visibility") {
|
|
608
|
-
|
|
609
|
-
sm.setSessionVisibility(msg.sessionId, msg.visibility);
|
|
610
|
-
}
|
|
621
|
+
sessionVisibility.handleChange(ctx, ws, msg);
|
|
611
622
|
return true;
|
|
612
623
|
}
|
|
613
624
|
|
|
@@ -897,25 +908,30 @@ function attachSessions(ctx) {
|
|
|
897
908
|
}
|
|
898
909
|
|
|
899
910
|
if (msg.type === "set_update_channel") {
|
|
900
|
-
if (
|
|
911
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
901
912
|
var newChannel = msg.channel === "beta" ? "beta" : "stable";
|
|
902
913
|
setUpdateChannel(newChannel);
|
|
903
914
|
setLatestVersion(null);
|
|
904
915
|
if (typeof opts.onSetUpdateChannel === "function") {
|
|
905
916
|
opts.onSetUpdateChannel(newChannel);
|
|
906
917
|
}
|
|
907
|
-
// Re-fetch with new channel and broadcast to admin clients
|
|
918
|
+
// Re-fetch with new channel and broadcast to admin clients, honouring
|
|
919
|
+
// any snooze each of them has on the version that comes back.
|
|
908
920
|
fetchVersion(newChannel).then(function (v) {
|
|
909
921
|
if (v && isNewer(v, currentVersion)) {
|
|
910
922
|
setLatestVersion(v);
|
|
911
|
-
|
|
923
|
+
broadcastUpdateAvailable(v);
|
|
912
924
|
}
|
|
913
925
|
}).catch(function () {});
|
|
914
926
|
return true;
|
|
915
927
|
}
|
|
916
928
|
|
|
917
929
|
if (msg.type === "check_update") {
|
|
918
|
-
if (
|
|
930
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
931
|
+
// A manual check is the user deliberately re-engaging, so it answers
|
|
932
|
+
// truthfully and ends any snooze rather than reporting an update it
|
|
933
|
+
// would then refuse to show.
|
|
934
|
+
updateSnooze.clearSnooze(updateSnooze.userKeyFor(ws, usersModule));
|
|
919
935
|
var updateChannel = getUpdateChannel();
|
|
920
936
|
fetchVersion(updateChannel).then(function (v) {
|
|
921
937
|
if (v && isNewer(v, currentVersion)) {
|
|
@@ -928,9 +944,40 @@ function attachSessions(ctx) {
|
|
|
928
944
|
return true;
|
|
929
945
|
}
|
|
930
946
|
|
|
947
|
+
if (msg.type === "update_snooze") {
|
|
948
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
949
|
+
var snoozeKey = updateSnooze.userKeyFor(ws, usersModule);
|
|
950
|
+
// The target is always the server's own latest version. A version named
|
|
951
|
+
// in the payload is ignored, as are any timestamps or user ids.
|
|
952
|
+
var snoozeResult = updateSnooze.snooze(
|
|
953
|
+
snoozeKey,
|
|
954
|
+
getLatestVersion(),
|
|
955
|
+
msg.duration,
|
|
956
|
+
Date.now(),
|
|
957
|
+
msg.tzOffsetMinutes
|
|
958
|
+
);
|
|
959
|
+
if (!snoozeResult.ok) {
|
|
960
|
+
sendTo(ws, { type: "update_snoozed", ok: false, error: snoozeResult.error });
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
// Every live connection of this user, so a second device stops showing
|
|
964
|
+
// a banner the user has already dealt with.
|
|
965
|
+
sendToUpdateUser(snoozeKey, {
|
|
966
|
+
type: "update_snoozed",
|
|
967
|
+
ok: true,
|
|
968
|
+
version: snoozeResult.version,
|
|
969
|
+
until: snoozeResult.until,
|
|
970
|
+
duration: snoozeResult.duration,
|
|
971
|
+
});
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
|
|
931
975
|
if (msg.type === "update_now") {
|
|
932
|
-
if (
|
|
933
|
-
|
|
976
|
+
if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
|
|
977
|
+
// Installing makes any snooze moot; drop it so the record does not
|
|
978
|
+
// outlive the version it referred to.
|
|
979
|
+
updateSnooze.clearSnooze(updateSnooze.userKeyFor(ws, usersModule));
|
|
980
|
+
broadcastUpdateStarted(getLatestVersion());
|
|
934
981
|
var _ipc = require("./ipc");
|
|
935
982
|
var _config = require("./config");
|
|
936
983
|
_ipc.sendIPCCommand(_config.socketPath(), { cmd: "update" });
|
|
@@ -964,10 +1011,7 @@ function attachSessions(ctx) {
|
|
|
964
1011
|
|
|
965
1012
|
if (msg.type === "stop") {
|
|
966
1013
|
var session = getSessionForWs(ws);
|
|
967
|
-
|
|
968
|
-
session.taskStopRequested = true;
|
|
969
|
-
if (session.abortController) session.abortController.abort();
|
|
970
|
-
}
|
|
1014
|
+
stopSession(session);
|
|
971
1015
|
return true;
|
|
972
1016
|
}
|
|
973
1017
|
|
|
@@ -1736,8 +1780,12 @@ function attachSessions(ctx) {
|
|
|
1736
1780
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid branch name" });
|
|
1737
1781
|
return true;
|
|
1738
1782
|
}
|
|
1783
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(wtDirName)) {
|
|
1784
|
+
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid worktree directory name" });
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1739
1787
|
if (typeof onCreateWorktree === "function") {
|
|
1740
|
-
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase);
|
|
1788
|
+
var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase, ws._clayUser);
|
|
1741
1789
|
sendTo(ws, { type: "create_worktree_result", ok: wtResult.ok, slug: wtResult.slug, error: wtResult.error });
|
|
1742
1790
|
} else {
|
|
1743
1791
|
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Not supported" });
|
|
@@ -2026,6 +2074,13 @@ function attachSessions(ctx) {
|
|
|
2026
2074
|
handleSessionsMessage: handleSessionsMessage,
|
|
2027
2075
|
resolveSessionForView: resolveSessionForView,
|
|
2028
2076
|
respondToAskUser: respondToAskUser,
|
|
2077
|
+
respondToHomePermission: function (ws, msg, session) {
|
|
2078
|
+
if (!session || !msg || !session.pendingPermissions || !session.pendingPermissions[msg.requestId]) return false;
|
|
2079
|
+
var indexedSessionId = sm.permissionRequestIndex[msg.requestId];
|
|
2080
|
+
if (indexedSessionId !== session.localId || sm.sessions.get(indexedSessionId) !== session) return false;
|
|
2081
|
+
return handleSessionsMessage(ws, { type: "permission_response", requestId: msg.requestId, decision: msg.decision });
|
|
2082
|
+
},
|
|
2083
|
+
stopHomeSession: stopSession,
|
|
2029
2084
|
};
|
|
2030
2085
|
}
|
|
2031
2086
|
|