clay-server 4.3.0-beta.3 → 4.3.0-beta.5
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/project-connection.js +2 -0
- package/lib/project-pair-result-capture.js +86 -0
- package/lib/project-pair-result-delivery.js +139 -0
- package/lib/project-pair-result-outbox.js +331 -0
- package/lib/project-pair-result-pipeline.js +16 -0
- package/lib/project-pair-result-recovery.js +225 -0
- package/lib/project-pair-task-control.js +6 -0
- package/lib/project-session-pair.js +37 -33
- package/lib/project.js +23 -1
- package/lib/public/app.js +1 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/overlays.css +22 -21
- package/lib/public/css/pane.css +16 -12
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-messages.js +7 -0
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/permission-control.js +5 -5
- package/lib/session-pair-mcp-server.js +2 -2
- package/lib/session-pair-prompts.js +7 -1
- package/lib/sessions.js +33 -2
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
function attachPairResultRecovery(ctx) {
|
|
4
|
+
var scheduled = Object.create(null);
|
|
5
|
+
var runtimeStatus = Object.create(null);
|
|
6
|
+
|
|
7
|
+
function opaqueId(key) {
|
|
8
|
+
return crypto.createHash("sha256").update(String(key)).digest("hex").slice(0, 24);
|
|
9
|
+
}
|
|
10
|
+
function ownerId(session) { return session && session.ownerId || null; }
|
|
11
|
+
function findOrigin(originId) {
|
|
12
|
+
var found = null, duplicate = false;
|
|
13
|
+
ctx.sm.sessions.forEach(function (session) {
|
|
14
|
+
if (!session || session.sessionOriginId !== originId) return;
|
|
15
|
+
if (found) duplicate = true;
|
|
16
|
+
else found = session;
|
|
17
|
+
});
|
|
18
|
+
return duplicate ? null : found;
|
|
19
|
+
}
|
|
20
|
+
function resolve(record) {
|
|
21
|
+
if (!record || record.projectSlug !== ctx.projectSlug) return { ok: false, code: "project", reason: "This completed result belongs to a different project." };
|
|
22
|
+
var driver = findOrigin(record.driverOriginId), worker = findOrigin(record.workerOriginId);
|
|
23
|
+
if (!driver || !worker) return { ok: false, code: "session", reason: "The original Driver or Split Worker session is unavailable." };
|
|
24
|
+
if (ownerId(driver) !== (record.ownerId || null) || ownerId(worker) !== (record.ownerId || null)) return { ok: false, code: "owner", reason: "The original session owner no longer matches." };
|
|
25
|
+
var group = ctx.store.groupForMember(driver.localId);
|
|
26
|
+
if (!group || ctx.store.groupForMember(worker.localId) !== group || !group.pair ||
|
|
27
|
+
group.pair.driverId !== driver.localId || group.pair.workerId !== worker.localId) {
|
|
28
|
+
return { ok: false, code: "pair", reason: "The original Driver and Split Worker pair is no longer active." };
|
|
29
|
+
}
|
|
30
|
+
var provenance = worker.sessionProvenance;
|
|
31
|
+
if (!provenance || provenance.kind !== "worker" || provenance.parentSessionOriginId !== record.driverOriginId || provenance.generation !== record.generation) {
|
|
32
|
+
return { ok: false, code: "generation", reason: "The Split Worker generation no longer matches this completed result." };
|
|
33
|
+
}
|
|
34
|
+
if (worker._pairDelegation && (worker._pairDelegation.outboxKey !== record.key || worker._pairDelegation.taskId !== record.taskId)) {
|
|
35
|
+
return { ok: false, code: "active_task", reason: "A newer Split Worker task is active, so this result cannot be restored automatically." };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true, driver: driver, worker: worker, group: group };
|
|
38
|
+
}
|
|
39
|
+
function tokenFor(record, resolved) {
|
|
40
|
+
var token = resolved.worker._pairDelegation;
|
|
41
|
+
if (token && token.outboxKey === record.key) {
|
|
42
|
+
if (Number(record.manualRetryEpoch || 0) > Number(token._manualRetryEpochApplied || 0)) {
|
|
43
|
+
if (token._deliveryTimer && token._deliveryTimer.timer) clearTimeout(token._deliveryTimer.timer);
|
|
44
|
+
delete token._deliveryTimer; delete token._deliveryUncertain; delete token._deliveryAccepted;
|
|
45
|
+
delete token._deliveryStartupAttemptId;
|
|
46
|
+
token._manualRetryEpochApplied = Number(record.manualRetryEpoch || 0);
|
|
47
|
+
}
|
|
48
|
+
return token;
|
|
49
|
+
}
|
|
50
|
+
token = {
|
|
51
|
+
outboxKey: record.key, taskId: record.taskId, generation: record.generation,
|
|
52
|
+
from: resolved.driver.localId, groupId: resolved.group.id,
|
|
53
|
+
startIndex: Number.isInteger(record.historyStartIndex) ? record.historyStartIndex : resolved.worker.history.length,
|
|
54
|
+
message: record.message || "", wait: record.deliveryRoute === "blocking",
|
|
55
|
+
detached: record.deliveryRoute === "callback", detachRequested: record.deliveryRoute === "callback",
|
|
56
|
+
response: record.outcome && record.outcome.response || "",
|
|
57
|
+
failure: record.outcome && record.outcome.error || null,
|
|
58
|
+
interrupted: !!(record.outcome && record.outcome.status === "interrupted"),
|
|
59
|
+
_captureComplete: true,
|
|
60
|
+
_manualRetryEpochApplied: Number(record.manualRetryEpoch || 0),
|
|
61
|
+
};
|
|
62
|
+
resolved.worker._pairDelegation = token;
|
|
63
|
+
resolved.worker._delegatedBy = resolved.driver.localId;
|
|
64
|
+
return token;
|
|
65
|
+
}
|
|
66
|
+
function block(record, reason, code) {
|
|
67
|
+
if (record.deliveryState === "accepted" && record.finalized) return;
|
|
68
|
+
var saved = ctx.outbox.markBlocked(record.key, reason, code);
|
|
69
|
+
if (saved.ok) changed();
|
|
70
|
+
}
|
|
71
|
+
function process(key, allowDelivery) {
|
|
72
|
+
delete scheduled[key];
|
|
73
|
+
var record = ctx.outbox.get(key);
|
|
74
|
+
if (!record || !record.outcome) return { ok: false, deferred: true };
|
|
75
|
+
if (record.finalized) return { ok: true, finalized: true };
|
|
76
|
+
if (record.blockedCode === "human_stop") return { ok: false, blocked: true };
|
|
77
|
+
var resolved = resolve(record);
|
|
78
|
+
if (!resolved.ok) {
|
|
79
|
+
block(record, resolved.reason, resolved.code);
|
|
80
|
+
return { ok: false, blocked: true };
|
|
81
|
+
}
|
|
82
|
+
var stopReason = typeof ctx.blockedReason === "function" ? ctx.blockedReason(resolved.driver) : null;
|
|
83
|
+
if (stopReason) {
|
|
84
|
+
block(record, stopReason, "human_stop");
|
|
85
|
+
return { ok: false, blocked: true };
|
|
86
|
+
}
|
|
87
|
+
if (record.finalizationState === "failed") return { ok: false, blocked: true };
|
|
88
|
+
var token = tokenFor(record, resolved);
|
|
89
|
+
if (record.deliveryRoute === "blocking" || record.deliveryState === "accepted") {
|
|
90
|
+
var finished = ctx.resultCapture.finish(resolved.driver, resolved.worker, token, record.deliveryState === "accepted");
|
|
91
|
+
changed(resolved.driver);
|
|
92
|
+
return { ok: finished !== false, finalized: finished !== false };
|
|
93
|
+
}
|
|
94
|
+
if (record.deliveryState === "uncertain") return { ok: false, uncertain: true };
|
|
95
|
+
if (!allowDelivery) return { ok: false, deferred: true };
|
|
96
|
+
var result = ctx.wake(resolved.driver, resolved.worker, token);
|
|
97
|
+
changed(resolved.driver);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function schedule(key) {
|
|
101
|
+
if (scheduled[key]) return;
|
|
102
|
+
scheduled[key] = true;
|
|
103
|
+
setImmediate(function () { process(key, true); });
|
|
104
|
+
}
|
|
105
|
+
function healthyLiveDispatch(record) {
|
|
106
|
+
if (!record || record.outcome || record.state === "blocked") return false;
|
|
107
|
+
var resolved = resolve(record);
|
|
108
|
+
if (!resolved.ok) return false;
|
|
109
|
+
var token = resolved.worker._pairDelegation;
|
|
110
|
+
return !!(token && token.outboxKey === record.key && token.taskId === record.taskId && token.generation === record.generation &&
|
|
111
|
+
token.from === resolved.driver.localId && token.groupId === resolved.group.id && !token._capturePending && !token._captureHookError &&
|
|
112
|
+
(resolved.worker.isProcessing || resolved.worker._queryStarting));
|
|
113
|
+
}
|
|
114
|
+
function recordsFor(session) {
|
|
115
|
+
if (!session) return [];
|
|
116
|
+
var originId = session.sessionOriginId, currentOwner = ownerId(session);
|
|
117
|
+
return ctx.outbox.list().map(function (record) {
|
|
118
|
+
return runtimeStatus[record.key] ? Object.assign({}, record, runtimeStatus[record.key]) : record;
|
|
119
|
+
}).filter(function (record) {
|
|
120
|
+
if (record.projectSlug !== ctx.projectSlug || record.driverOriginId !== originId || (record.ownerId || null) !== currentOwner) return false;
|
|
121
|
+
if (healthyLiveDispatch(record)) return false;
|
|
122
|
+
if (record.finalized) return false;
|
|
123
|
+
if (record.deliveryState === "accepted") return record.state === "blocked" || record.finalizationState === "failed";
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function publicItem(record) {
|
|
128
|
+
var completed = !!record.outcome;
|
|
129
|
+
var finalizationFailed = record.finalizationState === "failed";
|
|
130
|
+
var state = record.state === "blocked" || finalizationFailed ? "blocked" : record.deliveryState;
|
|
131
|
+
if (state === "attempting") state = "uncertain";
|
|
132
|
+
if (!completed && state !== "blocked") state = "uncertain";
|
|
133
|
+
var exhausted = state === "pending" && Number(record.deliveryAttemptCount || 0) >= ctx.outbox.maxDeliveryAttempts;
|
|
134
|
+
return {
|
|
135
|
+
id: opaqueId(record.key), state: exhausted ? "pending" : state,
|
|
136
|
+
message: record.blockedReason || record.finalizationError || (!completed ? "The Split Worker task was retained, but no durable completed result is available yet." :
|
|
137
|
+
(state === "uncertain" ? "Delivery could not be confirmed. The Driver may already have received this result." :
|
|
138
|
+
(exhausted ? "Automatic delivery could not complete. The finished result is retained." : "The finished Split Worker result is waiting to be delivered."))),
|
|
139
|
+
response: completed ? record.outcome.response || "" : "", error: completed ? record.outcome.error || null : null,
|
|
140
|
+
hasCompletedResult: completed,
|
|
141
|
+
canRetry: completed && !finalizationFailed && record.blockedCode !== "human_stop" && Number(record.manualRetryCount || 0) < 1 && (state === "uncertain" || exhausted),
|
|
142
|
+
confirmDuplicate: completed && state === "uncertain",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function canRead(ws, session) {
|
|
146
|
+
if (!ws || !session || ws._clayActiveSession !== session.localId) return false;
|
|
147
|
+
if (ownerId(session)) return !!(ws._clayUser && String(ws._clayUser.id) === String(ownerId(session)));
|
|
148
|
+
return !ws._clayUser || !ctx.isMultiUser();
|
|
149
|
+
}
|
|
150
|
+
function sendState(session, ws, sendFn) {
|
|
151
|
+
if (!canRead(ws, session)) return false;
|
|
152
|
+
var items = recordsFor(session).map(publicItem);
|
|
153
|
+
(sendFn || ctx.sendTo.bind(null, ws))({ type: "pair_result_status", sessionId: session.localId, items: items });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
function changed(driver) {
|
|
157
|
+
var targets = typeof ctx.getClients === "function" ? ctx.getClients() : [];
|
|
158
|
+
for (var ws of targets) {
|
|
159
|
+
var session = driver || ctx.sm.sessions.get(ws._clayActiveSession);
|
|
160
|
+
if (session) sendState(session, ws);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function stateChanged(driver, record) {
|
|
164
|
+
if (record && record.key && record.persistenceError) runtimeStatus[record.key] = {
|
|
165
|
+
finalizationState: record.finalizationState, finalizationError: record.finalizationError,
|
|
166
|
+
};
|
|
167
|
+
if (record && record.key && record.finalized) delete runtimeStatus[record.key];
|
|
168
|
+
changed(driver);
|
|
169
|
+
if (record && !record.finalized && record.finalizationState !== "failed" && (record.deliveryState === "accepted" || record.deliveryRoute === "blocking")) schedule(record.key);
|
|
170
|
+
}
|
|
171
|
+
function available(session) {
|
|
172
|
+
if (!session) return;
|
|
173
|
+
var origin = session.sessionOriginId;
|
|
174
|
+
var records = ctx.outbox.list();
|
|
175
|
+
for (var i = 0; i < records.length; i++) {
|
|
176
|
+
if (records[i].driverOriginId === origin && records[i].outcome && records[i].deliveryState === "pending" && records[i].state !== "blocked") schedule(records[i].key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function reconcile() {
|
|
180
|
+
var records = ctx.outbox.list();
|
|
181
|
+
for (var i = 0; i < records.length; i++) {
|
|
182
|
+
var record = records[i];
|
|
183
|
+
if (!record.outcome) continue;
|
|
184
|
+
if (record.deliveryState === "attempting") {
|
|
185
|
+
var uncertain = ctx.outbox.markRestartUncertain(record.key);
|
|
186
|
+
if (uncertain.ok && uncertain.record) record = uncertain.record;
|
|
187
|
+
}
|
|
188
|
+
process(record.key, false);
|
|
189
|
+
record = ctx.outbox.get(record.key);
|
|
190
|
+
if (record && record.deliveryState === "pending" && record.state !== "blocked" && record.finalizationState !== "failed") schedule(record.key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function beginHumanTurn(session) {
|
|
194
|
+
var records = recordsFor(session);
|
|
195
|
+
for (var i = 0; i < records.length; i++) {
|
|
196
|
+
if (records[i].blockedCode !== "human_stop") continue;
|
|
197
|
+
var saved = ctx.outbox.authorizeHumanTurn(records[i].key);
|
|
198
|
+
if (saved.ok && saved.record.outcome && (saved.record.deliveryState === "pending" || saved.record.deliveryState === "accepted")) schedule(records[i].key);
|
|
199
|
+
}
|
|
200
|
+
changed(session);
|
|
201
|
+
}
|
|
202
|
+
function handleMessage(ws, msg) {
|
|
203
|
+
if (!msg || (msg.type !== "pair_result_state_request" && msg.type !== "pair_result_retry")) return false;
|
|
204
|
+
var session = ctx.sm.sessions.get(ws._clayActiveSession);
|
|
205
|
+
if (!canRead(ws, session)) return true;
|
|
206
|
+
if (msg.sessionId != null && msg.sessionId !== session.localId) return true;
|
|
207
|
+
if (msg.projectSlug && msg.projectSlug !== ctx.projectSlug) return true;
|
|
208
|
+
if (msg.type === "pair_result_state_request") { sendState(session, ws); return true; }
|
|
209
|
+
var records = recordsFor(session), record = null;
|
|
210
|
+
for (var i = 0; i < records.length; i++) if (opaqueId(records[i].key) === msg.id) record = records[i];
|
|
211
|
+
if (!record) { sendState(session, ws); return true; }
|
|
212
|
+
if (record.deliveryState === "uncertain" && msg.confirmDuplicate !== true) { sendState(session, ws); return true; }
|
|
213
|
+
var granted = ctx.outbox.grantManualRetry(record.key);
|
|
214
|
+
if (granted.ok) schedule(record.key);
|
|
215
|
+
changed(session);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (ctx.sm && typeof ctx.sm.addOnSessionViewed === "function") {
|
|
219
|
+
ctx.sm.addOnSessionViewed(function (session, ws, sendFn) { available(session); sendState(session, ws, sendFn); });
|
|
220
|
+
}
|
|
221
|
+
return { available: available, beginHumanTurn: beginHumanTurn, changed: changed, stateChanged: stateChanged, handleMessage: handleMessage,
|
|
222
|
+
reconcile: reconcile, sendState: sendState, process: process };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = { attachPairResultRecovery: attachPairResultRecovery };
|
|
@@ -228,6 +228,12 @@ function attachPairTaskControl(ctx) {
|
|
|
228
228
|
token.outcomeEnvelope = envelope;
|
|
229
229
|
worker._lastPairOutcome = envelope;
|
|
230
230
|
if (finalStatus === "interrupted" || finalStatus === "partial") worker._lastInterruptedPairTask = { taskId: token.taskId, message: token.message, generation: token.generation };
|
|
231
|
+
if (ctx.sm.sendAndRecord) {
|
|
232
|
+
try { ctx.sm.sendAndRecord(worker, { type: "pair_task_completed", _internal: true,
|
|
233
|
+
ownerId: caller.ownerId || null, driverOriginId: caller.sessionOriginId || null,
|
|
234
|
+
workerOriginId: worker.sessionOriginId || null, taskId: token.taskId, generation: token.generation,
|
|
235
|
+
outcome: envelope }); } catch (evidenceError) { token._completionEvidenceError = evidenceError.message || String(evidenceError); }
|
|
236
|
+
}
|
|
231
237
|
if (ctx.sm.sendToSession) ctx.sm.sendToSession(caller, { type: "partner_task_completed", outcome: envelope });
|
|
232
238
|
return envelope;
|
|
233
239
|
}
|
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts"); var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"),
|
|
2
|
-
{ attachPairTurnControl } = require("./session-pair-turn-control");
|
|
1
|
+
var pairMcp = require("./session-pair-mcp-server"), pairPrompts = require("./session-pair-prompts"); var { attachPairFactory } = require("./session-pair-factory"), { attachPairLifecycle } = require("./project-pair-lifecycle"), { attachPairTurnControl } = require("./session-pair-turn-control");
|
|
3
2
|
var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration"), { attachWorkerPermission } = require("./project-worker-permission"), { attachWorkerProposal } = require("./project-worker-proposal");
|
|
4
3
|
var { attachPairMessage } = require("./project-pair-message"), { attachPairTaskControl } = require("./project-pair-task-control"), stopAutonomous = require("./project-pair-autonomous-stop").stopAutonomousWork, stopOwned = require("./project-pair-owned-stop").stopOwnedPair;
|
|
5
4
|
var pairHistory = require("./session-pair-history"), responseText = pairHistory.responseText, errorSince = pairHistory.errorSince, recentTurns = pairHistory.recentTurns, operationFingerprint = require("./session-pair-operation").fingerprint;
|
|
6
|
-
var beginPairQuery = require("./session-pair-query-start").beginPairQuery;
|
|
7
|
-
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
|
|
8
|
-
function toolError(err) { return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true }); }
|
|
5
|
+
var beginPairQuery = require("./session-pair-query-start").beginPairQuery, attachPairResultPipeline = require("./project-pair-result-pipeline").attachPairResultPipeline;
|
|
6
|
+
function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); } function toolError(err) { return Promise.resolve({ content: [{ type: "text", text: "Error: " + (err.message || err) }], isError: true }); }
|
|
9
7
|
function attachSessionPair(ctx) {
|
|
10
|
-
var sm = ctx.sm;
|
|
11
|
-
var
|
|
12
|
-
var workerProposal;
|
|
13
|
-
var workerPermission;
|
|
14
|
-
var lifecycle;
|
|
15
|
-
var taskControl;
|
|
16
|
-
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
|
|
8
|
+
var sm = ctx.sm, store = ctx.splitStore, workerProposal, workerPermission, lifecycle, taskControl;
|
|
9
|
+
var turnControl = attachPairTurnControl({ sm: sm, splitStore: store }), resultCapture;
|
|
17
10
|
function groupAndPartner(caller, missingPairMessage) {
|
|
18
11
|
if (!caller) throw new Error("partner tools require a session-bound tool server");
|
|
19
12
|
if (sm.sessions.get(caller.localId) !== caller) {
|
|
@@ -119,12 +112,14 @@ function attachSessionPair(ctx) {
|
|
|
119
112
|
function requestDetach(worker) {
|
|
120
113
|
var token = worker && worker._pairDelegation;
|
|
121
114
|
if (!token || token.detached || token.detachRequested) return false;
|
|
122
|
-
token.
|
|
115
|
+
if (token.outboxKey && ctx.resultOutbox && !ctx.resultOutbox.setRoute(token.outboxKey, "callback").ok) return false;
|
|
116
|
+
token.wait = false; token.detachRequested = true;
|
|
123
117
|
return true;
|
|
124
118
|
}
|
|
125
119
|
function handleTurnDone(partner) {
|
|
126
120
|
var token = partner && partner._pairDelegation;
|
|
127
121
|
if (!token) return false;
|
|
122
|
+
if (token._capturePending || token._captureComplete) return false;
|
|
128
123
|
var group = store.groupForMember(partner.localId);
|
|
129
124
|
if (!group || group.id !== token.groupId || group.members.indexOf(token.from) === -1) return false;
|
|
130
125
|
var caller = sm.sessions.get(token.from);
|
|
@@ -137,12 +132,8 @@ function attachSessionPair(ctx) {
|
|
|
137
132
|
token.failure = errorSince(partner.history || [], token.startIndex);
|
|
138
133
|
token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
|
|
139
134
|
if (token.interrupted) taskControl.ensureSystemInterruption(partner, token);
|
|
140
|
-
taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
|
|
141
|
-
|
|
142
|
-
if (typeof ctx.onPartnerResult === "function") ctx.onPartnerResult(caller, partner, token);
|
|
143
|
-
var resumed = resumeDriverWithResult(caller, partner, token);
|
|
144
|
-
taskControl.drain(caller, partner);
|
|
145
|
-
return resumed;
|
|
135
|
+
var outcome = taskControl.complete(partner, caller, token, token.failure ? "failed" : (token.interrupted ? "interrupted" : "completed"), token.response, token.failure);
|
|
136
|
+
return resultCapture.capture(caller, partner, token, outcome);
|
|
146
137
|
}
|
|
147
138
|
function waitForPartner(group, caller, partner, token, timeoutSeconds) {
|
|
148
139
|
return new Promise(function (resolve, reject) {
|
|
@@ -157,28 +148,33 @@ function attachSessionPair(ctx) {
|
|
|
157
148
|
}
|
|
158
149
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
159
150
|
clearInterval(timer);
|
|
160
|
-
finishDelegation(group, caller, partner, token);
|
|
161
151
|
var failure = errorSince(partner.history || [], token.startIndex);
|
|
162
152
|
var interrupted = !failure && !!partner._lastTurnInterrupted;
|
|
163
153
|
var response = responseText(partner.history || [], token.startIndex);
|
|
164
154
|
if (interrupted) taskControl.ensureSystemInterruption(partner, token);
|
|
165
155
|
var outcome = taskControl.complete(partner, caller, token, failure ? "failed" : (interrupted ? "interrupted" : "completed"), response, failure);
|
|
156
|
+
resultCapture.capture(caller, partner, token, outcome);
|
|
166
157
|
resolve({
|
|
167
|
-
status: failure ? "error" : (outcome.status === "completed" ? "complete" : outcome.status),
|
|
158
|
+
status: token._capturePending ? "capture_pending" : (failure ? "error" : (outcome.status === "completed" ? "complete" : outcome.status)),
|
|
168
159
|
taskId: token.taskId,
|
|
169
160
|
generation: token.generation,
|
|
170
161
|
response: response,
|
|
171
162
|
error: failure || undefined,
|
|
172
163
|
outcome: outcome,
|
|
173
164
|
});
|
|
174
|
-
taskControl.drain(caller, partner);
|
|
175
165
|
return;
|
|
176
166
|
}
|
|
177
167
|
if (Date.now() >= deadline || token.detachRequested) {
|
|
178
168
|
clearInterval(timer);
|
|
179
|
-
token.
|
|
169
|
+
var routed = token.outboxKey && ctx.resultOutbox ? ctx.resultOutbox.setRoute(token.outboxKey, "callback") : { ok: true };
|
|
170
|
+
if (!routed.ok) {
|
|
171
|
+
monitorPartner(group, caller, partner, token);
|
|
172
|
+
resolve({ status: "capture_pending", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), error: routed.error });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
token.wait = false; token.detached = true;
|
|
180
176
|
monitorPartner(group, caller, partner, token);
|
|
181
|
-
resolve({ status: "running", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for
|
|
177
|
+
resolve({ status: "running", taskId: token.taskId, generation: token.generation, response: responseText(partner.history || [], token.startIndex), hint: "The completed result will be pushed back automatically. Use read_partner only for a requested status, concrete failure or stall diagnosis, or capacity/queue decision; do not poll routinely." });
|
|
182
178
|
}
|
|
183
179
|
}, 500);
|
|
184
180
|
});
|
|
@@ -201,14 +197,20 @@ function attachSessionPair(ctx) {
|
|
|
201
197
|
var message = typeof args.message === "string" ? args.message.trim() : "";
|
|
202
198
|
if (!message) throw new Error("message is required");
|
|
203
199
|
turnControl.assertWorkerAction(caller);
|
|
204
|
-
var wait = args.wait
|
|
200
|
+
var wait = args.wait === true;
|
|
205
201
|
var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
|
|
206
202
|
timeout = Math.max(1, Math.min(900, timeout));
|
|
207
203
|
var partner = resolved.partner;
|
|
208
204
|
if (partner._pairDelegation) throw new Error("the partner is already handling a delegated task");
|
|
209
205
|
delete partner._pairInterruption; delete partner._lastInterruptedPairTask;
|
|
210
206
|
var task = taskControl.begin(partner, caller, message, args.taskId);
|
|
211
|
-
var token = Object.assign(task, { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length });
|
|
207
|
+
var token = Object.assign(task, { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length, wait: wait });
|
|
208
|
+
var started = resultCapture.begin(caller, partner, token, message, ctx.projectSlug);
|
|
209
|
+
if (!started.ok) {
|
|
210
|
+
token._retainOnError = true; partner._pairDelegation = token; partner._delegatedBy = caller.localId;
|
|
211
|
+
sm.sendAndRecord(caller, { type: "error", text: "The Split Worker task was not dispatched because its durable completion record could not be created: " + started.error });
|
|
212
|
+
return toolError(new Error(started.error));
|
|
213
|
+
}
|
|
212
214
|
partner._pairDelegation = token;
|
|
213
215
|
partner._delegatedBy = caller.localId;
|
|
214
216
|
sm.sendAndRecord(partner, {
|
|
@@ -243,14 +245,14 @@ function attachSessionPair(ctx) {
|
|
|
243
245
|
if (!wait) {
|
|
244
246
|
token.detached = true;
|
|
245
247
|
monitorPartner(resolved.group, caller, partner, token);
|
|
246
|
-
return toolResult({ status: "running", partnerId: partner.localId, taskId: token.taskId, generation: token.generation, hint: "The completed result will be pushed back automatically. Use read_partner only for
|
|
248
|
+
return toolResult({ status: "running", partnerId: partner.localId, taskId: token.taskId, generation: token.generation, hint: "The completed result will be pushed back automatically. Use read_partner only for a requested status, concrete failure or stall diagnosis, or capacity/queue decision; do not poll routinely." });
|
|
247
249
|
}
|
|
248
250
|
if (!partner.isProcessing && !partner._queryStarting) {
|
|
249
|
-
finishDelegation(resolved.group, caller, partner, token);
|
|
250
251
|
var immediateFailure = errorSince(partner.history || [], token.startIndex);
|
|
251
252
|
var immediateResponse = responseText(partner.history || [], token.startIndex);
|
|
252
253
|
var immediateOutcome = taskControl.complete(partner, caller, token, immediateFailure ? "failed" : "completed", immediateResponse, immediateFailure);
|
|
253
|
-
|
|
254
|
+
var immediateCapture = resultCapture.capture(caller, partner, token, immediateOutcome);
|
|
255
|
+
if (!immediateCapture && token._capturePending) return toolError(new Error("The completed Split Worker result could not be durably captured yet."));
|
|
254
256
|
return toolResult({
|
|
255
257
|
status: immediateFailure ? "error" : (immediateOutcome.status === "completed" ? "complete" : immediateOutcome.status),
|
|
256
258
|
taskId: token.taskId,
|
|
@@ -268,7 +270,7 @@ function attachSessionPair(ctx) {
|
|
|
268
270
|
if (group) {
|
|
269
271
|
var partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
|
|
270
272
|
var partner = sm.sessions.get(partnerId);
|
|
271
|
-
if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing) {
|
|
273
|
+
if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing && !partner._pairDelegation._retainOnError) {
|
|
272
274
|
finishDelegation(group, caller, partner, partner._pairDelegation);
|
|
273
275
|
}
|
|
274
276
|
}
|
|
@@ -334,6 +336,7 @@ function attachSessionPair(ctx) {
|
|
|
334
336
|
function handleHumanStop(session) {
|
|
335
337
|
var roles = turnControl.markHumanStop(session);
|
|
336
338
|
if (!roles) return false;
|
|
339
|
+
if (roles.worker._pairDelegation && roles.worker._pairDelegation.outboxKey && ctx.resultOutbox) ctx.resultOutbox.markBlocked(roles.worker._pairDelegation.outboxKey, "The human stopped this Split Worker turn.", "human_stop");
|
|
337
340
|
taskControl.markInterruption(roles.worker, "user", "The human stopped this Split Worker turn.");
|
|
338
341
|
workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
|
|
339
342
|
return true;
|
|
@@ -487,8 +490,9 @@ function attachSessionPair(ctx) {
|
|
|
487
490
|
}
|
|
488
491
|
return pairPrompt;
|
|
489
492
|
}
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
var resultPipeline = attachPairResultPipeline({ sm: sm, store: store, resultOutbox: ctx.resultOutbox, getSdk: ctx.getSdk, getLinuxUserForSession: ctx.getLinuxUserForSession, acceptanceTimeoutMs: ctx.acceptanceTimeoutMs, finishDelegation: finishDelegation, resumeDriverWithResult: resumeDriverWithResult, onPartnerResult: ctx.onPartnerResult, onStateChange: ctx.onResultStateChange, blockedReason: turnControl.blockedReason, drain: function (caller, partner) { return taskControl.drain(caller, partner); } });
|
|
494
|
+
resultCapture = resultPipeline.capture; var resultDelivery = resultPipeline.delivery;
|
|
495
|
+
return { beginHumanTurn: beginHumanTurn, getToolDefs: getToolDefs, handleHumanStop: handleHumanStop, stopOwnedPair: stopOwnedPair, stopAutonomousWork: stopAutonomousWork, stopBarrier: stopBarrier, handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt,
|
|
492
496
|
respondToWorkerProposal: workerProposal.respondToProposal, workerPermission: workerPermission, createOwnedPair: createOwnedPair, removeOwnedPair: removeOwnedPair, preflightRuntime: preflightRuntime,
|
|
493
|
-
sendToPartner: sendToPartner, rolesFor: rolesFor };
|
|
497
|
+
sendToPartner: sendToPartner, rolesFor: rolesFor, resultCapture: resultCapture, resultOutbox: ctx.resultOutbox, wakePartnerResult: function (caller, partner, token) { return resultDelivery.wake(caller, partner, token); }, getResultOutboxStatus: function () { return ctx.resultOutbox ? ctx.resultOutbox.status() : { records: [], persistenceError: null }; } };
|
|
494
498
|
} module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
|
package/lib/project.js
CHANGED
|
@@ -55,6 +55,8 @@ var { attachDelegatedFollowUp } = require("./project-delegated-follow-up");
|
|
|
55
55
|
var { attachCapsuleTurn } = require("./project-capsule-turn");
|
|
56
56
|
var workspaceSessionRef = require("./workspace-query-service").sessionRef;
|
|
57
57
|
var { attachSessionPair } = require("./project-session-pair");
|
|
58
|
+
var { attachPairResultOutbox } = require("./project-pair-result-outbox");
|
|
59
|
+
var { attachPairResultRecovery } = require("./project-pair-result-recovery");
|
|
58
60
|
var { attachSessionHandoff } = require("./project-session-handoff");
|
|
59
61
|
var { attachDriverContinuation } = require("./project-driver-continuation");
|
|
60
62
|
var driverContinuationAccess = require("./driver-continuation-access");
|
|
@@ -819,6 +821,9 @@ function createProjectContext(opts) {
|
|
|
819
821
|
|
|
820
822
|
// The SDK bridge is created after local MCP servers. Session spawning uses
|
|
821
823
|
// a getter so tool handlers see the initialized bridge when they run.
|
|
824
|
+
var _pairResultOutbox = attachPairResultOutbox({ storageDir: sm.sessionsDir, projectSlug: slug });
|
|
825
|
+
_pairResultOutbox.recoverPending(sm.sessions);
|
|
826
|
+
var _pairResultRecovery = null;
|
|
822
827
|
var _sessionPair = attachSessionPair({
|
|
823
828
|
sm: sm,
|
|
824
829
|
isMate: isMate,
|
|
@@ -837,12 +842,21 @@ function createProjectContext(opts) {
|
|
|
837
842
|
usersModule: usersModule,
|
|
838
843
|
getLinuxUserForSession: getLinuxUserForSession,
|
|
839
844
|
onProcessingChanged: onProcessingChanged,
|
|
845
|
+
projectSlug: slug,
|
|
846
|
+
resultOutbox: _pairResultOutbox,
|
|
847
|
+
onResultStateChange: function (driver, record) { if (_pairResultRecovery) _pairResultRecovery.stateChanged(driver, record); },
|
|
840
848
|
adapters: adapters,
|
|
841
849
|
onPartnerResult: function (driver, worker, token) {
|
|
842
850
|
if (_scheduledTaskExecution) _scheduledTaskExecution.onPartnerResult(driver, worker, token);
|
|
843
851
|
if (_autonomousRun) _autonomousRun.onPartnerResult(driver, worker, token);
|
|
844
852
|
},
|
|
845
853
|
});
|
|
854
|
+
_pairResultRecovery = attachPairResultRecovery({
|
|
855
|
+
sm: sm, store: _splitGroups.store, outbox: _pairResultOutbox, resultCapture: _sessionPair.resultCapture,
|
|
856
|
+
wake: _sessionPair.wakePartnerResult, projectSlug: slug, sendTo: sendTo,
|
|
857
|
+
blockedReason: _sessionPair.stopBarrier,
|
|
858
|
+
getClients: function () { return clients; }, isMultiUser: usersModule.isMultiUser,
|
|
859
|
+
});
|
|
846
860
|
var _sessionSpawn = attachSessionSpawn({
|
|
847
861
|
cwd: cwd,
|
|
848
862
|
sm: sm,
|
|
@@ -1277,6 +1291,7 @@ function createProjectContext(opts) {
|
|
|
1277
1291
|
},
|
|
1278
1292
|
onProcessingChanged: onProcessingChanged,
|
|
1279
1293
|
onSessionAvailable: function (session) {
|
|
1294
|
+
if (_pairResultRecovery) _pairResultRecovery.available(session);
|
|
1280
1295
|
if (_scheduledMessages) _scheduledMessages.notify();
|
|
1281
1296
|
if (_userMessage && session) setImmediate(function () { _userMessage.consumePendingMessage(session); });
|
|
1282
1297
|
},
|
|
@@ -1741,6 +1756,7 @@ function createProjectContext(opts) {
|
|
|
1741
1756
|
if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
|
|
1742
1757
|
|
|
1743
1758
|
// --- Sessions, config, project mgmt (delegated to project-sessions.js) ---
|
|
1759
|
+
if (_pairResultRecovery.handleMessage(ws, msg)) return;
|
|
1744
1760
|
if (_sessionPair.handleMessage(ws, msg)) return;
|
|
1745
1761
|
if (_sessionHandoff.handleMessage(ws, msg)) return;
|
|
1746
1762
|
if (_driverContinuation.handleMessage(ws, msg)) return;
|
|
@@ -2016,6 +2032,7 @@ function createProjectContext(opts) {
|
|
|
2016
2032
|
serverAuthToken: serverAuthToken,
|
|
2017
2033
|
resolveDefaultAi: opts.resolveDefaultAi,
|
|
2018
2034
|
});
|
|
2035
|
+
_pairResultRecovery.reconcile();
|
|
2019
2036
|
var _toolLlmRuntime = attachProjectToolLlmRuntime({
|
|
2020
2037
|
users: usersModule,
|
|
2021
2038
|
osUsers: osUsers,
|
|
@@ -2066,7 +2083,11 @@ function createProjectContext(opts) {
|
|
|
2066
2083
|
if (isMate || !_sessionPair.workerPermission) return false;
|
|
2067
2084
|
return _sessionPair.workerPermission.isDriverOperated(session);
|
|
2068
2085
|
},
|
|
2069
|
-
beginHumanPairTurn: function (session) {
|
|
2086
|
+
beginHumanPairTurn: function (session) {
|
|
2087
|
+
var resumed = _sessionPair.beginHumanTurn(session);
|
|
2088
|
+
if (resumed) _pairResultRecovery.beginHumanTurn(session);
|
|
2089
|
+
return resumed;
|
|
2090
|
+
},
|
|
2070
2091
|
authorizePendingDispatch: function (session, actor, item) {
|
|
2071
2092
|
if (!session || sm.sessions.get(session.localId) !== session || !item || item.projectSlug !== slug) return false;
|
|
2072
2093
|
if (_scheduledMessages && !_scheduledMessages.authorizePending(item, session)) return false;
|
|
@@ -2499,6 +2520,7 @@ function createProjectContext(opts) {
|
|
|
2499
2520
|
_email: _email,
|
|
2500
2521
|
restoreDebateState: restoreDebateState,
|
|
2501
2522
|
scheduledMessages: _scheduledMessages,
|
|
2523
|
+
pairResultRecovery: _pairResultRecovery,
|
|
2502
2524
|
sendCursorSharingState: opts.sendCursorSharingState,
|
|
2503
2525
|
stopFileWatch: stopFileWatch,
|
|
2504
2526
|
stopAllDirWatches: stopAllDirWatches,
|
package/lib/public/app.js
CHANGED
|
@@ -1387,6 +1387,18 @@ pre.mermaid-error {
|
|
|
1387
1387
|
.plan-banner.plan-exit { color: var(--success); }
|
|
1388
1388
|
.plan-banner.plan-exit .plan-banner-icon { color: var(--success); }
|
|
1389
1389
|
|
|
1390
|
+
.pair-result-status { display: grid; gap: 8px; padding: 10px 16px 0; }
|
|
1391
|
+
.pair-result-status-card { border: 1px solid var(--border); border-radius: 10px; background: var(--bg-alt); padding: 10px 12px; color: var(--text-secondary); font-size: 12px; }
|
|
1392
|
+
.pair-result-status-card > strong, .pair-result-status-card > span { display: block; }
|
|
1393
|
+
.pair-result-status-card > strong { color: var(--text); margin-bottom: 3px; }
|
|
1394
|
+
.pair-result-status-card details { margin-top: 7px; }
|
|
1395
|
+
.pair-result-status-card summary { cursor: pointer; color: var(--accent); }
|
|
1396
|
+
.pair-result-status-card pre { max-height: 180px; overflow: auto; white-space: pre-wrap; font: inherit; }
|
|
1397
|
+
.pair-result-status-card button { margin-top: 8px; }
|
|
1398
|
+
.pair-result-status-retry:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1399
|
+
.pair-result-status-retry:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
1400
|
+
.pair-result-status-uncertain, .pair-result-status-blocked { border-color: color-mix(in srgb, var(--warning) 45%, var(--border)); }
|
|
1401
|
+
|
|
1390
1402
|
/* --- Plan card --- */
|
|
1391
1403
|
.plan-card {
|
|
1392
1404
|
max-width: var(--content-width);
|
|
@@ -518,7 +518,7 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
|
|
|
518
518
|
.popover-copy:hover { color: var(--text); border-color: var(--text-dimmer); }
|
|
519
519
|
.popover-copy.copied { color: var(--success); border-color: var(--success); }
|
|
520
520
|
|
|
521
|
-
/*
|
|
521
|
+
/* Quiet native toolbar control for session permission mode. */
|
|
522
522
|
.session-permission-control {
|
|
523
523
|
display: inline-flex;
|
|
524
524
|
align-items: center;
|
|
@@ -535,35 +535,40 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
|
|
|
535
535
|
flex: 0 0 auto;
|
|
536
536
|
gap: 1px;
|
|
537
537
|
padding: 2px;
|
|
538
|
-
border: 1px solid color-mix(in srgb, var(--border
|
|
539
|
-
border-radius:
|
|
540
|
-
background: color-mix(in srgb, var(--
|
|
541
|
-
box-shadow: inset 0 1px 0 rgba(255,255,255,.04), 0 2px 8px rgba(0,0,0,.12);
|
|
538
|
+
border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
|
|
539
|
+
border-radius: 8px;
|
|
540
|
+
background: color-mix(in srgb, var(--bg-alt) 72%, transparent);
|
|
542
541
|
}
|
|
543
542
|
.session-permission-segmented button {
|
|
544
543
|
position: relative;
|
|
545
|
-
flex: 0 0
|
|
544
|
+
flex: 0 0 48px;
|
|
545
|
+
width: 48px;
|
|
546
|
+
height: 22px;
|
|
546
547
|
border: 0;
|
|
547
548
|
border-radius: 7px;
|
|
548
|
-
padding: 4px
|
|
549
|
-
color: var(--text-
|
|
549
|
+
padding: 3px 4px;
|
|
550
|
+
color: var(--text-secondary);
|
|
550
551
|
background: transparent;
|
|
551
552
|
font: inherit;
|
|
553
|
+
font-weight: 500;
|
|
552
554
|
font-size: 11px;
|
|
553
555
|
line-height: 1;
|
|
554
556
|
white-space: nowrap;
|
|
555
557
|
cursor: pointer;
|
|
556
|
-
transition: color .15s ease, background .15s ease, box-shadow .15s ease;
|
|
557
|
-
}
|
|
558
|
-
.session-permission-segmented button:hover:not(:disabled) { color: var(--text
|
|
559
|
-
.session-permission-segmented button:focus-visible { outline: 2px solid var(--accent
|
|
560
|
-
.session-permission-segmented button.active { background: var(--
|
|
561
|
-
.session-permission-segmented button
|
|
558
|
+
transition: color .15s ease, background .15s ease, box-shadow .15s ease, border-color .15s ease;
|
|
559
|
+
}
|
|
560
|
+
.session-permission-segmented button:hover:not(:disabled) { color: var(--text); background: color-mix(in srgb, var(--bg) 76%, transparent); }
|
|
561
|
+
.session-permission-segmented button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; z-index: 1; }
|
|
562
|
+
.session-permission-segmented button.active { border: 1px solid var(--border); background: var(--input-bg); color: var(--text); font-weight: var(--font-weight-heading, 600); box-shadow: 0 1px 2px rgba(var(--shadow-rgb), .12); }
|
|
563
|
+
:root.light-theme .session-permission-segmented button.active { background: var(--bg); }
|
|
564
|
+
.session-permission-segmented button:disabled { cursor: not-allowed; }
|
|
565
|
+
.session-permission-control:not(.permission-pending) .session-permission-segmented button:disabled { color: var(--text-muted); opacity: .65; }
|
|
562
566
|
.session-permission-control.permission-pending .session-permission-segmented button:disabled { cursor: wait; }
|
|
563
|
-
.session-permission-segmented button.pending { color:
|
|
564
|
-
.permission-
|
|
567
|
+
.session-permission-segmented button.pending { color: var(--text); background: color-mix(in srgb, var(--bg) 42%, transparent); }
|
|
568
|
+
.session-permission-segmented button.pending .permission-label { visibility: hidden; }
|
|
569
|
+
.permission-spinner { position: absolute; inset: 50% auto auto 50%; display: block; visibility: hidden; width: 10px; height: 10px; transform: translate(-50%, -50%); border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; }
|
|
565
570
|
.session-permission-segmented button.pending .permission-spinner { visibility: visible; animation: permission-spin .7s linear infinite; }
|
|
566
|
-
@keyframes permission-spin { to { transform: rotate(360deg); } }
|
|
571
|
+
@keyframes permission-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
|
|
567
572
|
.session-permission-status {
|
|
568
573
|
flex: 0 1 auto;
|
|
569
574
|
min-width: 0;
|
|
@@ -592,12 +597,8 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
|
|
|
592
597
|
}
|
|
593
598
|
.permission-runtime-fallback .session-permission-status { color: var(--warning, #d49a55); }
|
|
594
599
|
.permission-globally-forced .session-permission-status { color: var(--warning, #d49a55); }
|
|
595
|
-
.permission-label-short { display: none; }
|
|
596
600
|
@media (max-width: 760px) {
|
|
597
|
-
.permission-label-long { display: none; }
|
|
598
|
-
.permission-label-short { display: inline; }
|
|
599
601
|
.session-permission-control { column-gap: 4px; }
|
|
600
|
-
.session-permission-segmented button { padding-inline: 7px; }
|
|
601
602
|
.session-permission-status {
|
|
602
603
|
max-width: 132px;
|
|
603
604
|
font-size: 9px;
|