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