@agentshouse/kit 0.1.0-alpha.2 → 0.1.0-alpha.20
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/README.md +270 -26
- package/bin/connect-linux.sh +85 -13
- package/bin/kit.js +23 -3
- package/bootstrap.json +17 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +0 -13
- package/src/agents.js +63 -8
- package/src/binding.js +41 -0
- package/src/bridge.js +1 -1
- package/src/contract.js +21 -3
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +345 -73
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +88 -0
- package/src/launcher.js +9 -3
- package/src/local-files.js +3 -11
- package/src/login.js +2 -1
- package/src/mcp.js +13 -0
- package/src/mount.js +13 -21
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +7 -2
- package/src/source.js +44 -30
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +4 -0
- package/src/sync.js +15 -10
- package/src/telegram.js +29 -11
- package/src/worker.js +519 -116
- package/src/working-copy.js +1066 -0
package/src/worker.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
installedAgentCommand,
|
|
8
|
+
readAgentState,
|
|
9
|
+
reconcileAgentKinds,
|
|
10
|
+
} from "./agents.js";
|
|
6
11
|
import { readBindings, removeBinding, writeBinding } from "./binding.js";
|
|
7
12
|
import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
|
|
8
|
-
import { openBridge } from "./bridge.js";
|
|
13
|
+
import { bridgedFrame, openBridge } from "./bridge.js";
|
|
9
14
|
import { assistantBlocks } from "./blocks.js";
|
|
10
15
|
import { retryConversationFiles } from "./conversation-originals.js";
|
|
11
16
|
import { openAssistantDrafts } from "./draft.js";
|
|
12
|
-
import {
|
|
17
|
+
import { localRequest, reconcileInbound } from "./inbound.js";
|
|
13
18
|
import { localFiles } from "./local-files.js";
|
|
19
|
+
import { answerText, answerYaml } from "./mcp.js";
|
|
14
20
|
import { inheritedEnvironment } from "./command.js";
|
|
21
|
+
import { watchKitWork } from "./kit-work-stream.js";
|
|
22
|
+
import { applyWorkingCopies, ownerCaller, pushWorkingCopy, recordIndexChange, workingCopyPaths } from "./working-copy.js";
|
|
15
23
|
import { readConfiguration } from "./configuration.js";
|
|
16
24
|
import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
17
25
|
import {
|
|
@@ -25,13 +33,14 @@ import {
|
|
|
25
33
|
WORKER_HEARTBEAT_MS,
|
|
26
34
|
WORKER_INHERITED_ENVIRONMENT,
|
|
27
35
|
WORKER_SUPERVISION_POLL_MS,
|
|
36
|
+
WORKING_COPY_INDEX_METHOD,
|
|
37
|
+
WORKING_COPY_PUSH_METHOD,
|
|
28
38
|
} from "./contract.js";
|
|
29
|
-
import { EpochReportRequired, houseFailed, KitRefusal } from "./refusal.js";
|
|
39
|
+
import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
|
|
30
40
|
import {
|
|
31
41
|
acceptAssistantText,
|
|
32
42
|
acquireCommands,
|
|
33
43
|
acquireSetup,
|
|
34
|
-
reportAgentCapabilities,
|
|
35
44
|
acquireOneRun,
|
|
36
45
|
acquireOneResumption,
|
|
37
46
|
admitInteraction,
|
|
@@ -40,12 +49,13 @@ import {
|
|
|
40
49
|
rejectResumption,
|
|
41
50
|
callHouseMcp,
|
|
42
51
|
commitRun,
|
|
43
|
-
|
|
52
|
+
controlSleep,
|
|
44
53
|
maintainControl,
|
|
45
54
|
mutatingCall,
|
|
46
55
|
observeSessionStart,
|
|
47
56
|
observeSessionStop,
|
|
48
57
|
openSession,
|
|
58
|
+
readRunRequest,
|
|
49
59
|
replaceSessionAuthority,
|
|
50
60
|
reportContinuation,
|
|
51
61
|
reportInterruptOutcome,
|
|
@@ -53,13 +63,17 @@ import {
|
|
|
53
63
|
toolMutations,
|
|
54
64
|
reportRunOutcome,
|
|
55
65
|
reportSessionHeartbeat,
|
|
66
|
+
releaseControl,
|
|
56
67
|
reportSessions,
|
|
68
|
+
serveResident,
|
|
57
69
|
sessionAuthority,
|
|
58
70
|
streamAssistantDraft,
|
|
71
|
+
updateDrain,
|
|
59
72
|
} from "./house.js";
|
|
60
73
|
import { operationId } from "./operation.js";
|
|
61
|
-
import { runSetup } from "./setup.js";
|
|
74
|
+
import { runSetup, signInWantedAgent } from "./setup.js";
|
|
62
75
|
import { readSettlements, removeSettlement, writeSettlement } from "./settlement.js";
|
|
76
|
+
import { writeText } from "./state.js";
|
|
63
77
|
import { failedReport, terminalReport } from "./terminal.js";
|
|
64
78
|
import {
|
|
65
79
|
createSession,
|
|
@@ -71,47 +85,31 @@ import {
|
|
|
71
85
|
} from "./tmux.js";
|
|
72
86
|
|
|
73
87
|
const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
|
|
88
|
+
const RESIDENT_INTERVAL_MS = 30_000;
|
|
74
89
|
|
|
75
90
|
async function dispatchSeal(context, bound) {
|
|
76
91
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return { accepted, failure: null, unresolved: false, refused: null };
|
|
92
|
+
await acceptAssistantText(context.session, bound.session, bound.sealed);
|
|
93
|
+
bound.stopped.textAccepted = true;
|
|
80
94
|
} catch (refusal) {
|
|
81
95
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
82
96
|
const named = { code: refusal.code, detail: refusal.message };
|
|
83
|
-
if (undecided(refusal)) return
|
|
84
|
-
|
|
85
|
-
return { accepted: null, failure: named, unresolved: false, refused: null };
|
|
97
|
+
if (undecided(context, refusal)) return named;
|
|
98
|
+
bound.stopped.failure ??= named;
|
|
86
99
|
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function resolveSeal(context, bound) {
|
|
90
100
|
bound.sealed = null;
|
|
91
101
|
await writeBinding(context.directory, bound);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
async function sealAssistantText(context, bound, said) {
|
|
95
|
-
const blocks = assistantBlocks(said.join("\n\n"));
|
|
96
|
-
if (blocks === null) return { accepted: null, failure: null, unresolved: false, refused: null };
|
|
97
|
-
bound.sealed = { operation: operationId(), blocks };
|
|
98
|
-
await writeBinding(context.directory, bound);
|
|
99
|
-
return dispatchSeal(context, bound);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function recoverAssistantSeal(context, binding) {
|
|
103
|
-
if (binding.sealed === null) return { unresolved: false, refused: null };
|
|
104
|
-
const sealed = await dispatchSeal(context, binding);
|
|
105
|
-
return { unresolved: sealed.unresolved, refused: sealed.refused };
|
|
102
|
+
return null;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
function unresolvedRun(bound, refused = null) {
|
|
109
106
|
return { run: bound.run, session: bound.session, refused };
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
function undecided(refusal) {
|
|
109
|
+
function undecided(context, refusal) {
|
|
113
110
|
return (
|
|
114
111
|
houseFailed(refusal) ||
|
|
112
|
+
context.session.refused === true ||
|
|
115
113
|
(refusal instanceof KitRefusal &&
|
|
116
114
|
["kit_command_cancelled", "kit_session_token_rejected"].includes(refusal.code))
|
|
117
115
|
);
|
|
@@ -121,7 +119,9 @@ async function pendingCommands(context, kinds = ["stop"]) {
|
|
|
121
119
|
try {
|
|
122
120
|
return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
|
|
123
121
|
} catch (refusal) {
|
|
124
|
-
if (houseFailed(refusal)
|
|
122
|
+
if (houseFailed(refusal) || context.session.refused === true) {
|
|
123
|
+
return { stops: [], answers: [], interrupts: [] };
|
|
124
|
+
}
|
|
125
125
|
throw refusal;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -182,6 +182,7 @@ async function supervise(context, bound, bridge) {
|
|
|
182
182
|
while (live) {
|
|
183
183
|
await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
|
|
184
184
|
if (!live) return null;
|
|
185
|
+
await context.update?.announce();
|
|
185
186
|
if (Date.now() >= commanded) {
|
|
186
187
|
commanded = Date.now() + WORKER_COMMAND_POLL_MS;
|
|
187
188
|
const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
|
|
@@ -223,7 +224,7 @@ async function completeOwnerStop(context, bound, requestedAt) {
|
|
|
223
224
|
try {
|
|
224
225
|
await observeSessionStop(context.session, bound.session);
|
|
225
226
|
} catch (refusal) {
|
|
226
|
-
if (
|
|
227
|
+
if (undecided(context, refusal)) return { destroyed, confirmed: false, failure: null };
|
|
227
228
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
228
229
|
failure = { code: refusal.code, detail: refusal.message };
|
|
229
230
|
}
|
|
@@ -237,7 +238,7 @@ async function settleReported(context, bound) {
|
|
|
237
238
|
await reportRunOutcome(context.session, bound.run, bound.reported);
|
|
238
239
|
return { settled: true, failure: null };
|
|
239
240
|
} catch (refusal) {
|
|
240
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
241
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
241
242
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
242
243
|
if (refusal.code === "worker_run_settled") return { settled: true, failure: null };
|
|
243
244
|
return { settled: false, failure: { code: refusal.code, detail: refusal.message } };
|
|
@@ -272,6 +273,54 @@ function endOnEpochReport(ended, act) {
|
|
|
272
273
|
};
|
|
273
274
|
}
|
|
274
275
|
|
|
276
|
+
function refusedWith(text) {
|
|
277
|
+
const conflicts = answerYaml(text.slice(text.indexOf("\n") + 1))?.conflicts;
|
|
278
|
+
const current = Array.isArray(conflicts)
|
|
279
|
+
? conflicts.map(({ path, state, revision }) =>
|
|
280
|
+
state === "absent" ? { path, revision: null, content: null } : { path, revision },
|
|
281
|
+
)
|
|
282
|
+
: null;
|
|
283
|
+
return { refused: { code: text.split(":")[0].trim(), text, current } };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function acceptedWith(text) {
|
|
287
|
+
const written = new Map();
|
|
288
|
+
for (const { path, op, revision, from } of answerYaml(text).results) {
|
|
289
|
+
if (from !== undefined) written.set(from, { path: from, revision: null, content: null });
|
|
290
|
+
written.set(path, op === "remove" ? { path, revision: null, content: null } : { path, revision });
|
|
291
|
+
}
|
|
292
|
+
return { refused: null, written: [...written.values()] };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function sessionPush(context, bound, authority, frame, signal) {
|
|
296
|
+
const edit = async (body, sent) => {
|
|
297
|
+
const answer = await callHouseMcp(
|
|
298
|
+
context.session,
|
|
299
|
+
await sessionAuthority(context.session, bound.session, authority, sent),
|
|
300
|
+
bridgedFrame(
|
|
301
|
+
{ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "edit", arguments: { changes: body.changes } } },
|
|
302
|
+
body.operation_id,
|
|
303
|
+
),
|
|
304
|
+
sent,
|
|
305
|
+
);
|
|
306
|
+
if (answer.error !== undefined) return refusedWith(answer.error.message ?? "kit_house_refused");
|
|
307
|
+
const text = answerText(answer.result);
|
|
308
|
+
return answer.result?.isError === true ? refusedWith(text) : acceptedWith(text);
|
|
309
|
+
};
|
|
310
|
+
let outcome;
|
|
311
|
+
try {
|
|
312
|
+
outcome = await pushWorkingCopy(context, frame.params, { binding: { session: bound.session }, submit: edit }, signal);
|
|
313
|
+
} catch (refusal) {
|
|
314
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
315
|
+
outcome = { refused: true, text: refusal.message };
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
jsonrpc: "2.0",
|
|
319
|
+
id: frame.id,
|
|
320
|
+
result: { content: [{ type: "text", text: outcome.text }], ...(outcome.refused ? { isError: true } : {}) },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
275
324
|
async function openRunBridge(context, bound, authority, start) {
|
|
276
325
|
const ended = new AbortController();
|
|
277
326
|
const handed = (act) => endOnEpochReport(ended, act);
|
|
@@ -293,14 +342,17 @@ async function openRunBridge(context, bound, authority, start) {
|
|
|
293
342
|
),
|
|
294
343
|
secret: (interaction, ceremony) => awaitSecretInput(context.session, interaction, ceremony),
|
|
295
344
|
settle: handed((interaction, outcome) => settleInteraction(context, interaction, outcome)),
|
|
296
|
-
forward: handed((frame, signal) =>
|
|
297
|
-
|
|
298
|
-
|
|
345
|
+
forward: handed((frame, signal) => frame.method === WORKING_COPY_PUSH_METHOD
|
|
346
|
+
? sessionPush(context, bound, authority, frame, signal)
|
|
347
|
+
: frame.method === WORKING_COPY_INDEX_METHOD
|
|
348
|
+
? recordIndexChange(context, frame.params, signal).then(() => ({ jsonrpc: "2.0", id: frame.id, result: {} }))
|
|
349
|
+
: localFiles(async (sent) =>
|
|
350
|
+
callHouseMcp(
|
|
299
351
|
context.session,
|
|
300
352
|
await sessionAuthority(context.session, bound.session, authority, signal),
|
|
301
353
|
sent,
|
|
302
354
|
signal,
|
|
303
|
-
)
|
|
355
|
+
), context.session.dependencies, signal)(frame)),
|
|
304
356
|
});
|
|
305
357
|
return { bridge, drafts, ended };
|
|
306
358
|
}
|
|
@@ -371,50 +423,65 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
371
423
|
held !== null &&
|
|
372
424
|
stopped?.providerContinuation === true &&
|
|
373
425
|
stopped.providerSessionId !== null;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
426
|
+
if (continuable) {
|
|
427
|
+
await writeContinuation(context.directory, { ...held, providerSessionId: stopped.providerSessionId });
|
|
428
|
+
}
|
|
429
|
+
const turnCompleted = stopped?.stopOutcome === "completed";
|
|
430
|
+
const blocks = turnCompleted ? assistantBlocks(outcome.assistant.join("\n\n")) : null;
|
|
431
|
+
bound.sealed = blocks === null ? null : { operation: operationId(), blocks };
|
|
432
|
+
bound.stopped = {
|
|
433
|
+
turnCompleted,
|
|
434
|
+
capacityExhausted,
|
|
435
|
+
providerRefused,
|
|
436
|
+
continuable,
|
|
437
|
+
failure: outcome.failure,
|
|
438
|
+
textAccepted: false,
|
|
439
|
+
};
|
|
440
|
+
await writeBinding(context.directory, bound);
|
|
441
|
+
return { ...outcome, ...(await settleStopped(context, bound)) };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function stoppedReport(bound) {
|
|
445
|
+
if (bound.stopped === null || bound.stopped === undefined) return null;
|
|
446
|
+
return terminalReport({ ...bound.stopped, conversation: bound.conversation === true });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function settleStopped(context, bound) {
|
|
450
|
+
const { stopped } = bound;
|
|
451
|
+
const unresolved = (refused) => ({ failure: stopped.failure, refused, settled: false });
|
|
452
|
+
if (bound.sealed !== null) {
|
|
453
|
+
const refused = await dispatchSeal(context, bound);
|
|
454
|
+
if (refused !== null) return unresolved(refused);
|
|
455
|
+
}
|
|
456
|
+
let failure = stopped.failure;
|
|
381
457
|
try {
|
|
382
|
-
await observeSessionStop(context.session, bound.session,
|
|
458
|
+
await observeSessionStop(context.session, bound.session, stopped.textAccepted);
|
|
383
459
|
} catch (refusal) {
|
|
460
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
384
461
|
if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
|
|
385
|
-
|
|
462
|
+
failure ??= { code: refusal.code, detail: refusal.message };
|
|
386
463
|
await observeSessionStop(context.session, bound.session);
|
|
387
464
|
}
|
|
388
|
-
const report =
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
continuable,
|
|
395
|
-
});
|
|
396
|
-
const retained = report === null;
|
|
397
|
-
if (retained) {
|
|
398
|
-
await writeContinuation(context.directory, {
|
|
399
|
-
...held,
|
|
400
|
-
providerSessionId: stopped.providerSessionId,
|
|
465
|
+
const report = stoppedReport(bound);
|
|
466
|
+
if (report !== null) await removeContinuation(context.directory, bound.run);
|
|
467
|
+
try {
|
|
468
|
+
await reportContinuation(context.session, bound.session, {
|
|
469
|
+
providerContinuation: report === null,
|
|
470
|
+
providerCapacityExhausted: stopped.capacityExhausted,
|
|
401
471
|
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
407
|
-
if (retained) {
|
|
472
|
+
} catch (refusal) {
|
|
473
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
474
|
+
if (!(refusal instanceof KitRefusal) || refusal.code !== "worker_run_settled") throw refusal;
|
|
475
|
+
await removeContinuation(context.directory, bound.run);
|
|
408
476
|
await removeBinding(context.directory, bound.tmux);
|
|
409
|
-
return
|
|
477
|
+
return { failure, refused: null, settled: true };
|
|
478
|
+
}
|
|
479
|
+
if (report === null) {
|
|
480
|
+
await removeBinding(context.directory, bound.tmux);
|
|
481
|
+
return { failure, refused: null, settled: true };
|
|
410
482
|
}
|
|
411
483
|
const settled = await reportTerminalState(context, bound, report);
|
|
412
|
-
return {
|
|
413
|
-
...outcome,
|
|
414
|
-
reported: settled.reported,
|
|
415
|
-
refused: settled.failure,
|
|
416
|
-
settled: settled.settled,
|
|
417
|
-
};
|
|
484
|
+
return { failure, reported: settled.reported, refused: settled.failure, settled: settled.settled };
|
|
418
485
|
}
|
|
419
486
|
|
|
420
487
|
function assignmentRoute(context, assignment) {
|
|
@@ -425,6 +492,11 @@ function assignmentRoute(context, assignment) {
|
|
|
425
492
|
return assignment.route;
|
|
426
493
|
}
|
|
427
494
|
|
|
495
|
+
async function appliedWorkingCopies(context) {
|
|
496
|
+
await context.workingCopiesApplied?.();
|
|
497
|
+
return workingCopyPaths(context.directory);
|
|
498
|
+
}
|
|
499
|
+
|
|
428
500
|
async function prepareAssignment(context, assignment, resumed) {
|
|
429
501
|
const route = assignmentRoute(context, assignment);
|
|
430
502
|
const command = await installedAgentCommand(context.directory, route.adapter.kind);
|
|
@@ -450,8 +522,9 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
450
522
|
reported: null,
|
|
451
523
|
};
|
|
452
524
|
const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
|
|
453
|
-
const
|
|
525
|
+
const start = {
|
|
454
526
|
working_directory: assignment.workingDirectory,
|
|
527
|
+
working_copies: await appliedWorkingCopies(context),
|
|
455
528
|
execution_authority: context.configuration.executionAuthority,
|
|
456
529
|
adapter: {
|
|
457
530
|
kind: route.adapter.kind,
|
|
@@ -461,10 +534,16 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
461
534
|
effort: route.effort,
|
|
462
535
|
environment: {},
|
|
463
536
|
},
|
|
464
|
-
request:
|
|
537
|
+
request: null,
|
|
465
538
|
provider_session: assignment.providerSessionId ?? null,
|
|
466
|
-
}
|
|
467
|
-
|
|
539
|
+
};
|
|
540
|
+
const opened = await openRunBridge(context, bound, authority, start);
|
|
541
|
+
const request = async () => {
|
|
542
|
+
const token = await sessionAuthority(context.session, bound.session, authority);
|
|
543
|
+
start.request = await localRequest(context.directory, bound.run,
|
|
544
|
+
await readRunRequest(context.session, bound.run, token));
|
|
545
|
+
};
|
|
546
|
+
return { bound, opened, request };
|
|
468
547
|
}
|
|
469
548
|
|
|
470
549
|
async function refuseContinuation(context, command, failure) {
|
|
@@ -502,14 +581,15 @@ async function superviseAssignment(context, assignment, resumed = false) {
|
|
|
502
581
|
detail: refusal instanceof KitRefusal ? refusal.message : null,
|
|
503
582
|
};
|
|
504
583
|
if (resumed) return refuseContinuation(context, assignment, failure);
|
|
505
|
-
if (undecided(refusal)) throw refusal;
|
|
584
|
+
if (undecided(context, refusal)) throw refusal;
|
|
506
585
|
return refuseAssignment(context, assignment, failure);
|
|
507
586
|
}
|
|
508
|
-
const { bound, opened } = prepared;
|
|
587
|
+
const { bound, opened, request } = prepared;
|
|
509
588
|
return superviseBound(context, bound, opened, async (spawn) => {
|
|
510
589
|
if (!resumed) await commitRun(context.session, assignment.run);
|
|
511
590
|
spawn.committed = true;
|
|
512
591
|
await writeBinding(context.directory, bound);
|
|
592
|
+
await request();
|
|
513
593
|
await createSession(context.runtime, {
|
|
514
594
|
session: bound.tmux,
|
|
515
595
|
workingDirectory: assignment.workingDirectory,
|
|
@@ -531,7 +611,7 @@ async function superviseResumption(context, command) {
|
|
|
531
611
|
}
|
|
532
612
|
return superviseAssignment(context, {
|
|
533
613
|
...command, agentId: held.agentId, workingDirectory: held.workingDirectory,
|
|
534
|
-
conversation: held.conversation, providerSessionId: held.providerSessionId,
|
|
614
|
+
conversation: held.conversation, providerSessionId: held.providerSessionId,
|
|
535
615
|
}, true);
|
|
536
616
|
}
|
|
537
617
|
|
|
@@ -551,7 +631,7 @@ async function settleAbsentSession(context, binding) {
|
|
|
551
631
|
providerCapacityExhausted: false,
|
|
552
632
|
});
|
|
553
633
|
} catch (refusal) {
|
|
554
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
634
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
555
635
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
556
636
|
}
|
|
557
637
|
await removeContinuation(context.directory, binding.run);
|
|
@@ -567,7 +647,7 @@ async function confirmRestorable(context, binding) {
|
|
|
567
647
|
try {
|
|
568
648
|
authority = await replaceSessionAuthority(context.session, binding.session);
|
|
569
649
|
} catch (refusal) {
|
|
570
|
-
if (undecided(refusal)) return { authority: null, unresolved: true };
|
|
650
|
+
if (undecided(context, refusal)) return { authority: null, unresolved: true };
|
|
571
651
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
572
652
|
await destroySession(context.runtime, binding.tmux);
|
|
573
653
|
await removeBinding(context.directory, binding.tmux);
|
|
@@ -589,17 +669,17 @@ async function reconcile(context) {
|
|
|
589
669
|
const commands = (await pendingCommands(context)).stops;
|
|
590
670
|
const carried = [];
|
|
591
671
|
for (const binding of bindings) {
|
|
592
|
-
const resealed = await recoverAssistantSeal(context, binding);
|
|
593
|
-
if (resealed.unresolved) {
|
|
594
|
-
unresolved.push(unresolvedRun(binding, resealed.refused));
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
672
|
if (binding.reported !== null) {
|
|
598
673
|
const settled = await settleReported(context, binding);
|
|
599
674
|
if (settled.settled) await removeBinding(context.directory, binding.tmux);
|
|
600
675
|
else unresolved.push(unresolvedRun(binding, settled.failure));
|
|
601
676
|
continue;
|
|
602
677
|
}
|
|
678
|
+
if (binding.stopped !== null) {
|
|
679
|
+
const settled = await settleStopped(context, binding);
|
|
680
|
+
if (!settled.settled) unresolved.push(unresolvedRun(binding, settled.refused));
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
603
683
|
const command = commands.find((pending) => pending.run === binding.run) ?? null;
|
|
604
684
|
const requestedAt = binding.ownerStop ?? command?.requestedAt ?? null;
|
|
605
685
|
if (requestedAt === null) {
|
|
@@ -639,7 +719,7 @@ async function reportHeldSessions(context) {
|
|
|
639
719
|
const continued = (await readContinuationRuns(context.directory)).map((run) => ({ run, reported: null }));
|
|
640
720
|
const held = new Map();
|
|
641
721
|
for (const entry of [...settlements, ...bindings, ...continued]) {
|
|
642
|
-
held.set(entry.run, held.get(entry.run) ?? entry.reported);
|
|
722
|
+
held.set(entry.run, held.get(entry.run) ?? entry.reported ?? stoppedReport(entry));
|
|
643
723
|
}
|
|
644
724
|
const reported = await reportSessions(
|
|
645
725
|
context.session,
|
|
@@ -666,39 +746,54 @@ async function afterEpochReport(context, pass) {
|
|
|
666
746
|
return pass();
|
|
667
747
|
}
|
|
668
748
|
|
|
669
|
-
|
|
670
|
-
|
|
749
|
+
function unlessDraining(context, act) {
|
|
750
|
+
return context.update?.requested ? null : act();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function workerPass(context, dependencies, baseConfiguration) {
|
|
754
|
+
const { session, signal } = context;
|
|
671
755
|
const reconciled = await reconcile(context);
|
|
672
|
-
if (reconciled.run !== null || reconciled.unresolved.length > 0)
|
|
673
|
-
|
|
674
|
-
|
|
756
|
+
if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
|
|
757
|
+
return reconciled;
|
|
758
|
+
}
|
|
759
|
+
context.busy = false;
|
|
760
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, baseConfiguration);
|
|
761
|
+
context.startLogins?.(needsLogin);
|
|
762
|
+
context.busy = true;
|
|
763
|
+
const { configuration } = context;
|
|
764
|
+
context.inbound = await unlessDraining(context, () => reconcileInbound(context.directory, session));
|
|
765
|
+
const setup = await unlessDraining(context, () => acquireSetup(session, signal));
|
|
675
766
|
if (setup !== null) {
|
|
676
767
|
context.setup = await runSetup(
|
|
677
|
-
{
|
|
768
|
+
{
|
|
769
|
+
session,
|
|
770
|
+
configuration,
|
|
771
|
+
directory: context.directory,
|
|
772
|
+
dependencies,
|
|
773
|
+
signal,
|
|
774
|
+
waitAgentLogin: context.waitAgentLogin,
|
|
775
|
+
},
|
|
678
776
|
setup.request,
|
|
679
777
|
);
|
|
778
|
+
await context.recordSetupLogins?.(context.setup);
|
|
779
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
780
|
+
context.startLogins?.(needsLogin);
|
|
680
781
|
}
|
|
681
|
-
|
|
682
|
-
{ directory: context.directory, at: dependencies.now?.() },
|
|
683
|
-
configuration,
|
|
684
|
-
dependencies,
|
|
685
|
-
);
|
|
686
|
-
const readiness = await agentReadiness({ directory: context.directory }, configuration, dependencies);
|
|
687
|
-
await reportAgentCapabilities(session, readiness.agents);
|
|
688
|
-
if (readiness.refused !== null) throw readiness.refused;
|
|
689
|
-
const resumption = await acquireOneResumption(session);
|
|
782
|
+
const resumption = await unlessDraining(context, () => acquireOneResumption(session));
|
|
690
783
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
691
|
-
const assignment = await acquireOneRun(session);
|
|
784
|
+
const assignment = await unlessDraining(context, () => acquireOneRun(session));
|
|
692
785
|
if (assignment === null) return reconciled;
|
|
693
786
|
return { ...reconciled, run: await superviseAssignment(context, assignment) };
|
|
694
787
|
}
|
|
695
788
|
|
|
696
|
-
async function workerContext(options, dependencies) {
|
|
789
|
+
async function workerContext(options, dependencies, resident = false) {
|
|
697
790
|
const configuration = await readConfiguration(options.configuration);
|
|
698
791
|
const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
|
|
699
792
|
const launcher = dependencies.launcher ?? [process.execPath, LAUNCHER_MAIN];
|
|
700
793
|
const tmux = await readyTmux(runtime);
|
|
701
|
-
const session =
|
|
794
|
+
const session = resident
|
|
795
|
+
? await residentSession(options, dependencies)
|
|
796
|
+
: await openSession({ ...options, acquire: true }, dependencies);
|
|
702
797
|
const context = {
|
|
703
798
|
configuration,
|
|
704
799
|
runtime,
|
|
@@ -709,21 +804,66 @@ async function workerContext(options, dependencies) {
|
|
|
709
804
|
directory: options.directory,
|
|
710
805
|
files: { originals: [], failures: [] },
|
|
711
806
|
epochReport: null,
|
|
807
|
+
agentRefresh: Promise.resolve(),
|
|
808
|
+
configurationPath: options.configuration,
|
|
809
|
+
busy: false,
|
|
810
|
+
deferredRemoval: false,
|
|
712
811
|
};
|
|
713
812
|
return { context, tmux };
|
|
714
813
|
}
|
|
715
814
|
|
|
815
|
+
async function refreshAgentCapabilities(context, dependencies, signal, configuration, pointer = false) {
|
|
816
|
+
const base = configuration ?? (await readConfiguration(context.configurationPath));
|
|
817
|
+
const busy = context.busy || (await readBindings(context.directory)).length > 0;
|
|
818
|
+
const result = await reconcileAgentKinds(
|
|
819
|
+
{ directory: context.directory },
|
|
820
|
+
context.session,
|
|
821
|
+
base,
|
|
822
|
+
dependencies,
|
|
823
|
+
signal,
|
|
824
|
+
{
|
|
825
|
+
pointer,
|
|
826
|
+
busy,
|
|
827
|
+
lastDesired: context.lastDesired,
|
|
828
|
+
onDesired: (wanted) => context.cancelLogins?.(wanted),
|
|
829
|
+
},
|
|
830
|
+
);
|
|
831
|
+
context.deferredRemoval = result.retained.length > 0;
|
|
832
|
+
context.configuration = result.selected;
|
|
833
|
+
context.agents = result.updated;
|
|
834
|
+
context.lastDesired = [...result.selected.agents];
|
|
835
|
+
if (result.refused !== null) throw result.refused;
|
|
836
|
+
return result.reported.filter(
|
|
837
|
+
(agent) =>
|
|
838
|
+
result.selected.agents.includes(agent.kind) && agent.release !== null && !agent.signed_in,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function queueAgentRefresh(context, dependencies, signal, configuration, pointer = false) {
|
|
843
|
+
const refreshed = context.agentRefresh.then(() =>
|
|
844
|
+
refreshAgentCapabilities(context, dependencies, signal, configuration, pointer),
|
|
845
|
+
);
|
|
846
|
+
context.agentRefresh = refreshed.catch(() => {});
|
|
847
|
+
return refreshed;
|
|
848
|
+
}
|
|
849
|
+
|
|
716
850
|
async function controlledPass(context, dependencies, configuration, signal) {
|
|
717
|
-
context.configuration = { ...configuration, agents: await desiredAgents(context.session, signal) };
|
|
718
851
|
context.signal = signal;
|
|
719
852
|
context.session.signal = signal;
|
|
720
|
-
|
|
853
|
+
context.busy = true;
|
|
854
|
+
try {
|
|
855
|
+
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration));
|
|
856
|
+
} finally {
|
|
857
|
+
context.busy = false;
|
|
858
|
+
if (context.deferredRemoval) context.onIdle?.();
|
|
859
|
+
}
|
|
721
860
|
}
|
|
722
861
|
|
|
723
862
|
export async function runWorkers(options, dependencies = {}) {
|
|
724
863
|
const { context, tmux } = await workerContext(options, dependencies);
|
|
725
864
|
const { session, runtime } = context;
|
|
726
865
|
const supervised = await maintainControl(session, async (signal) => {
|
|
866
|
+
await applySavedWorkingCopies(context, signal);
|
|
727
867
|
return controlledPass(context, dependencies, context.configuration, signal);
|
|
728
868
|
}, session.signal);
|
|
729
869
|
return {
|
|
@@ -740,17 +880,280 @@ export async function runWorkers(options, dependencies = {}) {
|
|
|
740
880
|
};
|
|
741
881
|
}
|
|
742
882
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
883
|
+
function reported(line) {
|
|
884
|
+
process.stderr.write(`kit: ${line}\n`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function applySavedWorkingCopies(context, signal) {
|
|
888
|
+
const applied = await applyWorkingCopies(context, signal);
|
|
889
|
+
for (const refused of applied.refused) {
|
|
890
|
+
reported(`working copy ${refused.path} refused: ${refused.code}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function coalescedTask(signal, fail, work) {
|
|
895
|
+
let pending = false;
|
|
896
|
+
let task = null;
|
|
897
|
+
const trigger = () => {
|
|
898
|
+
pending = true;
|
|
899
|
+
if (task !== null) return;
|
|
900
|
+
task = (async () => {
|
|
901
|
+
while (pending && !signal.aborted) {
|
|
902
|
+
pending = false;
|
|
903
|
+
await work();
|
|
904
|
+
}
|
|
905
|
+
})()
|
|
906
|
+
.catch((error) => {
|
|
907
|
+
if (signal.aborted) return;
|
|
908
|
+
fail(error);
|
|
909
|
+
})
|
|
910
|
+
.finally(() => {
|
|
911
|
+
task = null;
|
|
912
|
+
if (pending && !signal.aborted) trigger();
|
|
913
|
+
});
|
|
914
|
+
};
|
|
915
|
+
return { trigger, idle: () => task === null, settled: () => task };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function residentWake() {
|
|
919
|
+
let pending = false;
|
|
920
|
+
let release = null;
|
|
921
|
+
return {
|
|
922
|
+
notify() {
|
|
923
|
+
pending = true;
|
|
924
|
+
release?.();
|
|
925
|
+
},
|
|
926
|
+
async wait(milliseconds, signal) {
|
|
927
|
+
if (pending) {
|
|
928
|
+
pending = false;
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const pause = new AbortController();
|
|
749
932
|
try {
|
|
750
|
-
await
|
|
933
|
+
await Promise.race([
|
|
934
|
+
new Promise((resolve) => {
|
|
935
|
+
release = resolve;
|
|
936
|
+
}),
|
|
937
|
+
delay(milliseconds, undefined, { signal: AbortSignal.any([signal, pause.signal]) }),
|
|
938
|
+
]);
|
|
751
939
|
} catch (error) {
|
|
752
940
|
if (!signal.aborted) throw error;
|
|
941
|
+
} finally {
|
|
942
|
+
release = null;
|
|
943
|
+
pending = false;
|
|
944
|
+
pause.abort();
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function residentUpdate(context, wake) {
|
|
951
|
+
let requested = existsSync(updateDrain(context.directory));
|
|
952
|
+
let ready = null;
|
|
953
|
+
const waiting = [];
|
|
954
|
+
return {
|
|
955
|
+
get requested() {
|
|
956
|
+
return requested;
|
|
957
|
+
},
|
|
958
|
+
async request(serving) {
|
|
959
|
+
if (!requested) {
|
|
960
|
+
requested = true;
|
|
961
|
+
await writeText(updateDrain(context.directory), "");
|
|
753
962
|
}
|
|
963
|
+
wake.notify();
|
|
964
|
+
const sessions = await heldSessions(context);
|
|
965
|
+
if (ready !== null) return ready;
|
|
966
|
+
if (serving) return { ready: false, sessions, serving };
|
|
967
|
+
if (sessions.length > 0) return { ready: false, sessions };
|
|
968
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
969
|
+
},
|
|
970
|
+
answer(answer) {
|
|
971
|
+
for (const resolve of waiting.splice(0)) resolve(answer);
|
|
972
|
+
},
|
|
973
|
+
async announce() {
|
|
974
|
+
if (waiting.length === 0) return;
|
|
975
|
+
const sessions = await heldSessions(context);
|
|
976
|
+
if (sessions.length > 0) this.answer({ ready: false, sessions });
|
|
977
|
+
},
|
|
978
|
+
quiesce() {
|
|
979
|
+
ready = { ready: true, sessions: [] };
|
|
980
|
+
this.answer(ready);
|
|
981
|
+
},
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function heldSessions(context) {
|
|
986
|
+
const bindings = await readBindings(context.directory);
|
|
987
|
+
if (!context.session.refused) {
|
|
988
|
+
return [...bindings, ...(await readSettlements(context.directory))].map((held) => held.run);
|
|
989
|
+
}
|
|
990
|
+
const live = [];
|
|
991
|
+
for (const binding of bindings) {
|
|
992
|
+
if (binding.stopped === null && (await sessionPresent(context.runtime, binding.tmux))) {
|
|
993
|
+
live.push(binding.run);
|
|
754
994
|
}
|
|
755
|
-
}
|
|
995
|
+
}
|
|
996
|
+
return live;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function residentSession(options, dependencies) {
|
|
1000
|
+
for (;;) {
|
|
1001
|
+
try {
|
|
1002
|
+
return await openSession({ ...options, resident: true }, dependencies);
|
|
1003
|
+
} catch (refusal) {
|
|
1004
|
+
if (refusal?.code !== "environment_control_lease_active") throw refusal;
|
|
1005
|
+
await controlSleep(dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS, options.signal);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
export async function runResident(options, dependencies = {}) {
|
|
1011
|
+
const { context } = await workerContext(options, dependencies, true);
|
|
1012
|
+
const { session } = context;
|
|
1013
|
+
const interval = dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS;
|
|
1014
|
+
const wake = residentWake();
|
|
1015
|
+
const update = residentUpdate(context, wake);
|
|
1016
|
+
context.update = update;
|
|
1017
|
+
const served = await serveResident(
|
|
1018
|
+
session,
|
|
1019
|
+
context.directory,
|
|
1020
|
+
options.signal,
|
|
1021
|
+
(serving) => update.request(serving),
|
|
1022
|
+
(request, signal) =>
|
|
1023
|
+
pushWorkingCopy({ directory: context.directory, session }, request, ownerCaller(session), signal).catch(
|
|
1024
|
+
(error) => ({ refused: true, text: formatCommandError(error) }),
|
|
1025
|
+
),
|
|
1026
|
+
);
|
|
1027
|
+
const serve = async (signal) => {
|
|
1028
|
+
context.busy = true;
|
|
1029
|
+
const watcher = new AbortController();
|
|
1030
|
+
let failure = null;
|
|
1031
|
+
const fail = (error) => {
|
|
1032
|
+
if (signal.aborted || session.refused) return;
|
|
1033
|
+
failure = error;
|
|
1034
|
+
wake.notify();
|
|
1035
|
+
};
|
|
1036
|
+
const logins = new Map();
|
|
1037
|
+
const attemptedLogins = new Map();
|
|
1038
|
+
context.cancelLogins = (wanted) => {
|
|
1039
|
+
for (const [kind, running] of logins) {
|
|
1040
|
+
if (!wanted.includes(kind)) running.controller.abort();
|
|
1041
|
+
}
|
|
1042
|
+
for (const kind of attemptedLogins.keys()) {
|
|
1043
|
+
if (!wanted.includes(kind)) attemptedLogins.delete(kind);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
context.recordSetupLogins = async (modules) => {
|
|
1047
|
+
for (const module of modules) {
|
|
1048
|
+
if (module.category !== "agent" || module.status !== "needs_attention") continue;
|
|
1049
|
+
const release = (await readAgentState(context.directory, module.name)).release;
|
|
1050
|
+
if (release !== null) attemptedLogins.set(module.name, release);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
context.startLogins = (agents) => {
|
|
1054
|
+
if (update.requested) return;
|
|
1055
|
+
for (const agent of agents) {
|
|
1056
|
+
const { kind, release } = agent;
|
|
1057
|
+
if (logins.has(kind) || attemptedLogins.get(kind) === release) continue;
|
|
1058
|
+
const controller = new AbortController();
|
|
1059
|
+
const loginSignal = AbortSignal.any([signal, controller.signal]);
|
|
1060
|
+
const task = (async () => {
|
|
1061
|
+
if ((await acquireSetup(context.session, loginSignal)) !== null) return;
|
|
1062
|
+
attemptedLogins.set(kind, release);
|
|
1063
|
+
await signInWantedAgent({ ...context, dependencies, signal: loginSignal }, kind);
|
|
1064
|
+
await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
1065
|
+
})()
|
|
1066
|
+
.catch((error) => {
|
|
1067
|
+
if (loginSignal.aborted) return;
|
|
1068
|
+
fail(error);
|
|
1069
|
+
})
|
|
1070
|
+
.finally(() => logins.delete(kind));
|
|
1071
|
+
logins.set(kind, { controller, task });
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
context.waitAgentLogin = async (kind) => {
|
|
1075
|
+
await logins.get(kind)?.task;
|
|
1076
|
+
};
|
|
1077
|
+
const agentRefresh = coalescedTask(signal, fail, async () => {
|
|
1078
|
+
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
1079
|
+
context.startLogins(needsLogin);
|
|
1080
|
+
});
|
|
1081
|
+
const copies = coalescedTask(
|
|
1082
|
+
signal,
|
|
1083
|
+
(error) => reported(`working copies unavailable: ${error?.message ?? error}`),
|
|
1084
|
+
() => applySavedWorkingCopies(context, signal),
|
|
1085
|
+
);
|
|
1086
|
+
const pointer = () => {
|
|
1087
|
+
if (!update.requested) agentRefresh.trigger();
|
|
1088
|
+
};
|
|
1089
|
+
const available = (subject) => {
|
|
1090
|
+
if (update.requested) return;
|
|
1091
|
+
if (subject !== "working_copy") agentRefresh.trigger();
|
|
1092
|
+
if (subject !== "agents") copies.trigger();
|
|
1093
|
+
};
|
|
1094
|
+
context.onIdle = pointer;
|
|
1095
|
+
context.workingCopiesApplied = copies.settled;
|
|
1096
|
+
if (!update.requested) copies.trigger();
|
|
1097
|
+
const watching = (dependencies.watchKitWork ?? watchKitWork)(
|
|
1098
|
+
context.session,
|
|
1099
|
+
AbortSignal.any([signal, watcher.signal]),
|
|
1100
|
+
available,
|
|
1101
|
+
dependencies,
|
|
1102
|
+
).catch(fail);
|
|
1103
|
+
try {
|
|
1104
|
+
while (!signal.aborted) {
|
|
1105
|
+
if (failure !== null) throw failure;
|
|
1106
|
+
if (!session.refused) {
|
|
1107
|
+
const configuration = await readConfiguration(options.configuration);
|
|
1108
|
+
await controlledPass(context, dependencies, configuration, signal).catch((error) => {
|
|
1109
|
+
if (!session.refused) throw error;
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (session.refused || update.requested) {
|
|
1113
|
+
const sessions = await heldSessions(context);
|
|
1114
|
+
if (sessions.length > 0) update.answer({ ready: false, sessions });
|
|
1115
|
+
else if (
|
|
1116
|
+
session.refused ||
|
|
1117
|
+
(!served.serving() && logins.size === 0 && agentRefresh.idle() && copies.idle())
|
|
1118
|
+
) {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
await wake.wait(interval, signal);
|
|
1123
|
+
}
|
|
1124
|
+
} finally {
|
|
1125
|
+
watcher.abort();
|
|
1126
|
+
await watching;
|
|
1127
|
+
await agentRefresh.settled();
|
|
1128
|
+
await copies.settled();
|
|
1129
|
+
for (const running of logins.values()) running.controller.abort();
|
|
1130
|
+
await Promise.all([...logins.values()].map((running) => running.task));
|
|
1131
|
+
await context.agentRefresh;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
try {
|
|
1135
|
+
for (;;) {
|
|
1136
|
+
try {
|
|
1137
|
+
await maintainControl(session, serve, options.signal, { release: false });
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
await releaseControl(session);
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
if (session.refused) break;
|
|
1143
|
+
try {
|
|
1144
|
+
await releaseControl(session, { required: true });
|
|
1145
|
+
break;
|
|
1146
|
+
} catch (refusal) {
|
|
1147
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
1148
|
+
if (session.refused) break;
|
|
1149
|
+
update.answer({ ready: false, sessions: [], refused: refusal.code });
|
|
1150
|
+
}
|
|
1151
|
+
await wake.wait(interval, options.signal);
|
|
1152
|
+
}
|
|
1153
|
+
update.quiesce();
|
|
1154
|
+
while (!options.signal.aborted) await wake.wait(interval, options.signal);
|
|
1155
|
+
throw new KitRefusal("kit_command_cancelled");
|
|
1156
|
+
} finally {
|
|
1157
|
+
await served.close();
|
|
1158
|
+
}
|
|
756
1159
|
}
|