@agentshouse/kit 0.1.0-alpha.27 → 0.1.0-alpha.29
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 +136 -49
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/binding.js +11 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +23 -13
- package/src/contract.js +6 -9
- package/src/draft.js +4 -82
- package/src/house.js +63 -58
- package/src/kit-work-stream.js +57 -27
- package/src/launcher.js +21 -6
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/tasks.js +21 -18
- package/src/telegram-view.js +214 -0
- package/src/telegram.js +124 -53
- package/src/turn-frames.js +69 -0
- package/src/worker.js +144 -91
- package/src/working-copy.js +18 -9
package/src/worker.js
CHANGED
|
@@ -15,13 +15,13 @@ import { bridgedFrame, openBridge } from "./bridge.js";
|
|
|
15
15
|
import { transferBytes } from "./bytes.js";
|
|
16
16
|
import { assistantBlocks } from "./blocks.js";
|
|
17
17
|
import { retryConversationFiles } from "./conversation-originals.js";
|
|
18
|
-
import { openAssistantDrafts } from "./draft.js";
|
|
19
18
|
import { localRequest, reconcileInbound } from "./inbound.js";
|
|
20
19
|
import { localFiles } from "./local-files.js";
|
|
21
20
|
import { answerText, answerYaml, editRefusal } from "./mcp.js";
|
|
22
21
|
import { inheritedEnvironment } from "./command.js";
|
|
23
22
|
import { sourceEntries } from "./google/entries.js";
|
|
24
23
|
import { watchKitWork } from "./kit-work-stream.js";
|
|
24
|
+
import { finalPlan } from "./plan.js";
|
|
25
25
|
import { coalescedTask, residentWake } from "./tasks.js";
|
|
26
26
|
import {
|
|
27
27
|
applyWorkingCopies,
|
|
@@ -40,10 +40,8 @@ import {
|
|
|
40
40
|
MCP_REQUEST_BODY_MAX_BYTES,
|
|
41
41
|
PROVIDER_CONTINUATION_UNAVAILABLE,
|
|
42
42
|
WORKER_COMMAND_BATCH_MAX,
|
|
43
|
-
WORKER_COMMAND_POLL_MS,
|
|
44
43
|
WORKER_EXIT_GRACE_MS,
|
|
45
44
|
WORKER_EXIT_POLL_MS,
|
|
46
|
-
WORKER_HEARTBEAT_MS,
|
|
47
45
|
WORKER_INHERITED_ENVIRONMENT,
|
|
48
46
|
WORKER_SUPERVISION_POLL_MS,
|
|
49
47
|
WORKING_COPY_INDEX_METHOD,
|
|
@@ -58,7 +56,6 @@ import {
|
|
|
58
56
|
acquireOneRun,
|
|
59
57
|
acquireOneResumption,
|
|
60
58
|
admitInteraction,
|
|
61
|
-
admitPlanSnapshot,
|
|
62
59
|
awaitSecretInput,
|
|
63
60
|
rejectResumption,
|
|
64
61
|
callHouseMcp,
|
|
@@ -76,12 +73,10 @@ import {
|
|
|
76
73
|
reportInteractionOutcome,
|
|
77
74
|
toolMutations,
|
|
78
75
|
reportRunOutcome,
|
|
79
|
-
reportSessionHeartbeat,
|
|
80
76
|
releaseControl,
|
|
81
77
|
reportSessions,
|
|
82
78
|
serveResident,
|
|
83
79
|
sessionAuthority,
|
|
84
|
-
streamAssistantDraft,
|
|
85
80
|
updateDrain,
|
|
86
81
|
} from "./house.js";
|
|
87
82
|
import { operationId } from "./operation.js";
|
|
@@ -90,6 +85,7 @@ import { readSettlements, removeSettlement, writeSettlement } from "./settlement
|
|
|
90
85
|
import { howWeWorkOwed, noteHowWeWork, reconcileSkillSet } from "./skill-set.js";
|
|
91
86
|
import { writeText } from "./state.js";
|
|
92
87
|
import { failedReport, terminalReport } from "./terminal.js";
|
|
88
|
+
import { kitStreamWriter, turnFrames } from "./turn-frames.js";
|
|
93
89
|
import {
|
|
94
90
|
createSession,
|
|
95
91
|
destroySession,
|
|
@@ -101,10 +97,11 @@ import {
|
|
|
101
97
|
|
|
102
98
|
const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
|
|
103
99
|
const RESIDENT_INTERVAL_MS = 30_000;
|
|
100
|
+
const PASS_REACHES = ["commands", "runs"];
|
|
104
101
|
|
|
105
102
|
async function dispatchSeal(context, bound) {
|
|
106
103
|
try {
|
|
107
|
-
await acceptAssistantText(context.session, bound.session, bound.sealed);
|
|
104
|
+
await acceptAssistantText(context.session, bound.session, bound.sealed, bound.plan);
|
|
108
105
|
bound.stopped.textAccepted = true;
|
|
109
106
|
} catch (refusal) {
|
|
110
107
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
@@ -134,9 +131,7 @@ async function pendingCommands(context, kinds = ["stop"]) {
|
|
|
134
131
|
try {
|
|
135
132
|
return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
|
|
136
133
|
} catch (refusal) {
|
|
137
|
-
if (houseFailed(refusal) || context.session.refused === true)
|
|
138
|
-
return { stops: [], answers: [], interrupts: [] };
|
|
139
|
-
}
|
|
134
|
+
if (houseFailed(refusal) || context.session.refused === true) return null;
|
|
140
135
|
throw refusal;
|
|
141
136
|
}
|
|
142
137
|
}
|
|
@@ -157,14 +152,6 @@ async function settleInterrupt(context, interrupt, outcome) {
|
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
154
|
|
|
160
|
-
async function reportHeartbeat(context, bound) {
|
|
161
|
-
try {
|
|
162
|
-
await reportSessionHeartbeat(context.session, bound.session);
|
|
163
|
-
} catch (refusal) {
|
|
164
|
-
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
155
|
function retryFiles(context) {
|
|
169
156
|
if (context.retrying !== undefined) return;
|
|
170
157
|
context.retrying = retryConversationFiles(context)
|
|
@@ -187,40 +174,32 @@ async function settleFiles(context) {
|
|
|
187
174
|
}
|
|
188
175
|
|
|
189
176
|
async function supervise(context, bound, bridge) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
|
|
199
|
-
if (!live) return null;
|
|
177
|
+
const departed = new AbortController();
|
|
178
|
+
bridge.departed.then(() => departed.abort());
|
|
179
|
+
const waiting = AbortSignal.any([context.signal, departed.signal]);
|
|
180
|
+
let owed = false;
|
|
181
|
+
for (;;) {
|
|
182
|
+
const commanded = (await context.commands.wait(WORKER_SUPERVISION_POLL_MS, waiting)) || owed;
|
|
183
|
+
if (departed.signal.aborted) return null;
|
|
184
|
+
if (context.signal.aborted) throw new KitRefusal("kit_command_cancelled");
|
|
200
185
|
await context.update?.announce();
|
|
201
|
-
if (
|
|
202
|
-
commanded = Date.now() + WORKER_COMMAND_POLL_MS;
|
|
186
|
+
if (commanded) {
|
|
203
187
|
const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
|
|
204
|
-
|
|
188
|
+
owed = polled === null && context.session.refused !== true;
|
|
189
|
+
for (const answer of polled?.answers ?? []) {
|
|
205
190
|
if (answer.session === bound.session) bridge.answered(answer);
|
|
206
191
|
}
|
|
207
|
-
for (const interrupt of polled
|
|
192
|
+
for (const interrupt of polled?.interrupts ?? []) {
|
|
208
193
|
if (interrupt.session !== bound.session || interrupt.run !== bound.run) continue;
|
|
209
194
|
if (!bridge.interrupt(interrupt)) {
|
|
210
195
|
await settleInterrupt(context, interrupt.interrupt, "nothing_to_interrupt");
|
|
211
196
|
}
|
|
212
197
|
}
|
|
213
|
-
const stop = polled
|
|
198
|
+
const stop = polled?.stops.find((command) => command.run === bound.run);
|
|
214
199
|
if (stop !== undefined) return stop;
|
|
215
200
|
}
|
|
216
|
-
if (Date.now() >= pulsed) {
|
|
217
|
-
pulsed = Date.now() + WORKER_HEARTBEAT_MS;
|
|
218
|
-
retryFiles(context);
|
|
219
|
-
await reportHeartbeat(context, bound);
|
|
220
|
-
}
|
|
221
201
|
if (!(await sessionPresent(context.runtime, bound.tmux))) return null;
|
|
222
202
|
}
|
|
223
|
-
return null;
|
|
224
203
|
}
|
|
225
204
|
|
|
226
205
|
async function grantExitGrace(runtime, handle) {
|
|
@@ -237,7 +216,7 @@ async function completeOwnerStop(context, bound, requestedAt) {
|
|
|
237
216
|
const destroyed = await destroySession(context.runtime, bound.tmux);
|
|
238
217
|
let failure = null;
|
|
239
218
|
try {
|
|
240
|
-
await observeSessionStop(context.session, bound.session);
|
|
219
|
+
await observeSessionStop(context.session, bound.session, false, bound.plan);
|
|
241
220
|
} catch (refusal) {
|
|
242
221
|
if (undecided(context, refusal)) return { destroyed, confirmed: false, failure: null };
|
|
243
222
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
@@ -250,7 +229,7 @@ async function completeOwnerStop(context, bound, requestedAt) {
|
|
|
250
229
|
|
|
251
230
|
async function settleReported(context, bound) {
|
|
252
231
|
try {
|
|
253
|
-
await reportRunOutcome(context.session, bound.run, bound.reported);
|
|
232
|
+
await reportRunOutcome(context.session, bound.run, bound.reported, bound.plan);
|
|
254
233
|
return { settled: true, failure: null };
|
|
255
234
|
} catch (refusal) {
|
|
256
235
|
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
@@ -379,25 +358,23 @@ async function sessionPush(context, bound, authority, frame, signal) {
|
|
|
379
358
|
async function openRunBridge(context, bound, authority, start) {
|
|
380
359
|
const ended = new AbortController();
|
|
381
360
|
const handed = (act) => endOnEpochReport(ended, act);
|
|
382
|
-
const
|
|
383
|
-
send: handed((blocks) => streamAssistantDraft(context.session, bound.session, blocks)),
|
|
384
|
-
});
|
|
361
|
+
const frames = turnFrames(context.stream);
|
|
385
362
|
const bridge = await openBridge({
|
|
386
363
|
socket: bound.socket,
|
|
387
364
|
nonce: bound.nonce,
|
|
388
365
|
signal: AbortSignal.any([context.signal, ended.signal]),
|
|
389
366
|
mutates: mutatingCall(await toolMutations(context.session, context.signal)),
|
|
390
367
|
alive: () => sessionPresent(context.runtime, bound.tmux),
|
|
391
|
-
draft: (
|
|
368
|
+
draft: (view) => frames.draft(view),
|
|
369
|
+
plan: (snapshot) => frames.plan(snapshot),
|
|
392
370
|
start,
|
|
393
371
|
question: handed((batch) => admitInteraction(context.session, bound.session, batch)),
|
|
394
|
-
plan: handed((snapshot) => admitPlanSnapshot(context.session, bound.session, snapshot)),
|
|
395
372
|
howWeWork: () =>
|
|
396
373
|
noteHowWeWork(context.directory).catch((error) =>
|
|
397
374
|
reported(`How-we-work note unwritable (${error.code ?? "unknown"})`),
|
|
398
375
|
),
|
|
399
|
-
interrupt: handed((interrupt, outcome) =>
|
|
400
|
-
reportInterruptOutcome(context.session, interrupt, outcome),
|
|
376
|
+
interrupt: handed((interrupt, outcome, plan) =>
|
|
377
|
+
reportInterruptOutcome(context.session, interrupt, outcome, finalPlan(plan)),
|
|
401
378
|
),
|
|
402
379
|
secret: (interaction, ceremony) => awaitSecretInput(context.session, interaction, ceremony),
|
|
403
380
|
settle: handed((interaction, outcome) => settleInteraction(context, interaction, outcome)),
|
|
@@ -413,11 +390,11 @@ async function openRunBridge(context, bound, authority, start) {
|
|
|
413
390
|
signal,
|
|
414
391
|
), context.session.dependencies, signal)(frame)),
|
|
415
392
|
});
|
|
416
|
-
return { bridge,
|
|
393
|
+
return { bridge, frames, ended };
|
|
417
394
|
}
|
|
418
395
|
|
|
419
396
|
async function superviseBound(context, bound, opened, start) {
|
|
420
|
-
const { bridge,
|
|
397
|
+
const { bridge, ended } = opened;
|
|
421
398
|
const spawn = { committed: false };
|
|
422
399
|
let unsupervised = null;
|
|
423
400
|
let destroyed = false;
|
|
@@ -431,23 +408,20 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
431
408
|
unsupervised = { code: refusal.code, detail: refusal.message };
|
|
432
409
|
} finally {
|
|
433
410
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
} finally {
|
|
440
|
-
bridge.stopRecovery();
|
|
441
|
-
if (owner === null) {
|
|
442
|
-
try {
|
|
443
|
-
destroyed = await grantExitGrace(context.runtime, bound.tmux);
|
|
444
|
-
} finally {
|
|
445
|
-
await bridge.close();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
411
|
+
retryFiles(context);
|
|
412
|
+
await settleFiles(context);
|
|
413
|
+
for (const interaction of bridge.unsettledInteractions()) {
|
|
414
|
+
await settleInteraction(context, interaction, "expired");
|
|
448
415
|
}
|
|
449
416
|
} finally {
|
|
450
|
-
|
|
417
|
+
bridge.stopRecovery();
|
|
418
|
+
if (owner === null) {
|
|
419
|
+
try {
|
|
420
|
+
destroyed = await grantExitGrace(context.runtime, bound.tmux);
|
|
421
|
+
} finally {
|
|
422
|
+
await bridge.close();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
451
425
|
}
|
|
452
426
|
}
|
|
453
427
|
ended.signal.throwIfAborted();
|
|
@@ -463,6 +437,7 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
463
437
|
destroyed,
|
|
464
438
|
stopped: null,
|
|
465
439
|
};
|
|
440
|
+
bound.plan = finalPlan(bridge.plan());
|
|
466
441
|
if (owner !== null) {
|
|
467
442
|
const finished = await completeOwnerStop(context, bound, owner.requestedAt);
|
|
468
443
|
return {
|
|
@@ -514,12 +489,12 @@ async function settleStopped(context, bound) {
|
|
|
514
489
|
}
|
|
515
490
|
let failure = stopped.failure;
|
|
516
491
|
try {
|
|
517
|
-
await observeSessionStop(context.session, bound.session, stopped.textAccepted);
|
|
492
|
+
await observeSessionStop(context.session, bound.session, stopped.textAccepted, bound.plan);
|
|
518
493
|
} catch (refusal) {
|
|
519
494
|
if (undecided(context, refusal)) return unresolved(null);
|
|
520
495
|
if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
|
|
521
496
|
failure ??= { code: refusal.code, detail: refusal.message };
|
|
522
|
-
await observeSessionStop(context.session, bound.session);
|
|
497
|
+
await observeSessionStop(context.session, bound.session, false, bound.plan);
|
|
523
498
|
}
|
|
524
499
|
const report = stoppedReport(bound);
|
|
525
500
|
if (report !== null) await removeContinuation(context.directory, bound.run);
|
|
@@ -579,6 +554,7 @@ async function prepareAssignment(context, assignment, resumed) {
|
|
|
579
554
|
ownerStop: null,
|
|
580
555
|
sealed: null,
|
|
581
556
|
reported: null,
|
|
557
|
+
plan: null,
|
|
582
558
|
};
|
|
583
559
|
const authority = { token: assignment.session.token, expiresAt: assignment.session.expiresAt };
|
|
584
560
|
const start = {
|
|
@@ -661,7 +637,7 @@ async function superviseAssignment(context, assignment, resumed = false) {
|
|
|
661
637
|
},
|
|
662
638
|
command: context.launcher,
|
|
663
639
|
});
|
|
664
|
-
await observeSessionStart(context.session, bound.session);
|
|
640
|
+
opened.frames.identify(await observeSessionStart(context.session, bound.session));
|
|
665
641
|
});
|
|
666
642
|
}
|
|
667
643
|
|
|
@@ -680,7 +656,7 @@ async function superviseRestored(context, binding, authority) {
|
|
|
680
656
|
const opened = await openRunBridge(context, binding, authority);
|
|
681
657
|
return superviseBound(context, binding, opened, async (spawn) => {
|
|
682
658
|
spawn.committed = true;
|
|
683
|
-
await observeSessionStart(context.session, binding.session);
|
|
659
|
+
opened.frames.identify(await observeSessionStart(context.session, binding.session));
|
|
684
660
|
});
|
|
685
661
|
}
|
|
686
662
|
|
|
@@ -727,7 +703,7 @@ async function reconcile(context) {
|
|
|
727
703
|
}
|
|
728
704
|
const bindings = await readBindings(context.directory);
|
|
729
705
|
if (bindings.length === 0) return { run: null, stopped, unresolved };
|
|
730
|
-
const commands = (await pendingCommands(context))
|
|
706
|
+
const commands = (await pendingCommands(context))?.stops ?? [];
|
|
731
707
|
const carried = [];
|
|
732
708
|
for (const binding of bindings) {
|
|
733
709
|
if (binding.reported !== null) {
|
|
@@ -811,7 +787,7 @@ function unlessDraining(context, act) {
|
|
|
811
787
|
return context.update?.requested ? null : act();
|
|
812
788
|
}
|
|
813
789
|
|
|
814
|
-
async function workerPass(context, dependencies, baseConfiguration) {
|
|
790
|
+
async function workerPass(context, dependencies, baseConfiguration, reach) {
|
|
815
791
|
const { session, signal } = context;
|
|
816
792
|
const reconciled = await reconcile(context);
|
|
817
793
|
if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
|
|
@@ -842,6 +818,7 @@ async function workerPass(context, dependencies, baseConfiguration) {
|
|
|
842
818
|
}
|
|
843
819
|
const resumption = await unlessDraining(context, () => acquireOneResumption(session));
|
|
844
820
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
821
|
+
if (reach !== "runs") return reconciled;
|
|
845
822
|
const assignment = await unlessDraining(context, () => acquireOneRun(session));
|
|
846
823
|
if (assignment === null) return reconciled;
|
|
847
824
|
return { ...reconciled, run: await superviseAssignment(context, assignment) };
|
|
@@ -865,6 +842,8 @@ async function workerContext(options, dependencies, resident = false) {
|
|
|
865
842
|
directory: options.directory,
|
|
866
843
|
home: dependencies.home ?? homedir(),
|
|
867
844
|
files: { originals: [], failures: [] },
|
|
845
|
+
commands: residentWake(),
|
|
846
|
+
stream: kitStreamWriter(),
|
|
868
847
|
epochReport: null,
|
|
869
848
|
agentRefresh: Promise.resolve(),
|
|
870
849
|
configurationPath: options.configuration,
|
|
@@ -909,7 +888,7 @@ function queueAgentRefresh(context, dependencies, signal, configuration, pointer
|
|
|
909
888
|
return refreshed;
|
|
910
889
|
}
|
|
911
890
|
|
|
912
|
-
async function controlledPass(context, dependencies, configuration, signal) {
|
|
891
|
+
async function controlledPass(context, dependencies, configuration, signal, reach = "runs") {
|
|
913
892
|
context.signal = signal;
|
|
914
893
|
context.session.signal = signal;
|
|
915
894
|
await reconcileSkillSet({
|
|
@@ -919,7 +898,7 @@ async function controlledPass(context, dependencies, configuration, signal) {
|
|
|
919
898
|
});
|
|
920
899
|
context.busy = true;
|
|
921
900
|
try {
|
|
922
|
-
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration));
|
|
901
|
+
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration, reach));
|
|
923
902
|
} finally {
|
|
924
903
|
context.busy = false;
|
|
925
904
|
if (context.deferredRemoval) context.onIdle?.();
|
|
@@ -930,8 +909,28 @@ export async function runWorkers(options, dependencies = {}) {
|
|
|
930
909
|
const { context, tmux } = await workerContext(options, dependencies);
|
|
931
910
|
const { session, runtime } = context;
|
|
932
911
|
const supervised = await maintainControl(session, async (signal) => {
|
|
933
|
-
|
|
934
|
-
|
|
912
|
+
const watcher = new AbortController();
|
|
913
|
+
const commanded = (subject) => {
|
|
914
|
+
if (subject === null || subject === "commands") context.commands.notify();
|
|
915
|
+
};
|
|
916
|
+
const watching = (dependencies.watchKitWork ?? watchKitWork)(
|
|
917
|
+
session,
|
|
918
|
+
AbortSignal.any([signal, watcher.signal]),
|
|
919
|
+
{
|
|
920
|
+
available: commanded,
|
|
921
|
+
changed: () => {},
|
|
922
|
+
opened: context.stream.opened,
|
|
923
|
+
closed: context.stream.closed,
|
|
924
|
+
},
|
|
925
|
+
dependencies,
|
|
926
|
+
).catch((error) => reported(`control stream ended: ${error?.code ?? error?.message ?? error}`));
|
|
927
|
+
try {
|
|
928
|
+
await reportedWorkingCopies(applyWorkingCopies, context, signal);
|
|
929
|
+
return await controlledPass(context, dependencies, context.configuration, signal);
|
|
930
|
+
} finally {
|
|
931
|
+
watcher.abort();
|
|
932
|
+
await watching;
|
|
933
|
+
}
|
|
935
934
|
}, session.signal);
|
|
936
935
|
return {
|
|
937
936
|
tmux,
|
|
@@ -952,9 +951,11 @@ function reported(line) {
|
|
|
952
951
|
}
|
|
953
952
|
|
|
954
953
|
async function reportedWorkingCopies(act, context, signal) {
|
|
955
|
-
|
|
954
|
+
const outcome = await act(context, signal);
|
|
955
|
+
for (const refused of outcome.refused) {
|
|
956
956
|
reported(`working copy ${refused.path} refused: ${refused.code}`);
|
|
957
957
|
}
|
|
958
|
+
return outcome;
|
|
958
959
|
}
|
|
959
960
|
|
|
960
961
|
function residentUpdate(context, wake) {
|
|
@@ -1036,6 +1037,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1036
1037
|
),
|
|
1037
1038
|
(source) =>
|
|
1038
1039
|
sources === null ? Promise.reject(new KitRefusal("kit_resident_unavailable")) : sources.capture(source),
|
|
1040
|
+
context.stream,
|
|
1039
1041
|
);
|
|
1040
1042
|
const serve = async (signal) => {
|
|
1041
1043
|
context.busy = true;
|
|
@@ -1092,11 +1094,18 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1092
1094
|
context.startLogins(needsLogin);
|
|
1093
1095
|
});
|
|
1094
1096
|
const unavailable = (error) => reported(`working copies unavailable: ${error?.message ?? error}`);
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
+
const changed = () => {
|
|
1098
|
+
if (!update.requested) deliveries.trigger();
|
|
1099
|
+
};
|
|
1100
|
+
const behind = (outcome) => {
|
|
1101
|
+
if (!outcome.behind) return;
|
|
1102
|
+
controlSleep(interval, signal).then(changed, () => {});
|
|
1103
|
+
};
|
|
1104
|
+
const copies = coalescedTask(signal, unavailable, async () =>
|
|
1105
|
+
behind(await reportedWorkingCopies(applyWorkingCopies, context, signal)),
|
|
1097
1106
|
);
|
|
1098
|
-
const deliveries = coalescedTask(signal, unavailable, () =>
|
|
1099
|
-
reportedWorkingCopies(deliverWorkingCopies, context, signal),
|
|
1107
|
+
const deliveries = coalescedTask(signal, unavailable, async () =>
|
|
1108
|
+
behind(await reportedWorkingCopies(deliverWorkingCopies, context, signal)),
|
|
1100
1109
|
);
|
|
1101
1110
|
sources = sourceEntries({
|
|
1102
1111
|
session: context.session,
|
|
@@ -1111,7 +1120,38 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1111
1120
|
const pointer = () => {
|
|
1112
1121
|
if (!update.requested) agentRefresh.trigger();
|
|
1113
1122
|
};
|
|
1114
|
-
|
|
1123
|
+
let passOwed = null;
|
|
1124
|
+
let deferred = null;
|
|
1125
|
+
let retrying = null;
|
|
1126
|
+
const higher = (reach, than) =>
|
|
1127
|
+
PASS_REACHES.indexOf(reach) > PASS_REACHES.indexOf(than) ? reach : than;
|
|
1128
|
+
const owe = (reach) => {
|
|
1129
|
+
passOwed = higher(reach, passOwed);
|
|
1130
|
+
wake.notify();
|
|
1131
|
+
};
|
|
1132
|
+
const defer = (reach) => {
|
|
1133
|
+
deferred = higher(reach, deferred);
|
|
1134
|
+
if (retrying !== null) return;
|
|
1135
|
+
retrying = new AbortController();
|
|
1136
|
+
controlSleep(interval, AbortSignal.any([signal, retrying.signal])).then(() => {
|
|
1137
|
+
retrying = null;
|
|
1138
|
+
const owed = deferred;
|
|
1139
|
+
deferred = null;
|
|
1140
|
+
owe(owed);
|
|
1141
|
+
}, () => {});
|
|
1142
|
+
};
|
|
1143
|
+
const settle = (reach) => {
|
|
1144
|
+
retrying?.abort();
|
|
1145
|
+
retrying = null;
|
|
1146
|
+
if (higher(deferred, reach) !== reach) owe(deferred);
|
|
1147
|
+
deferred = null;
|
|
1148
|
+
};
|
|
1149
|
+
const available = (subject) => {
|
|
1150
|
+
if (subject === null || subject === "runs") owe("runs");
|
|
1151
|
+
if (subject === null || subject === "commands") {
|
|
1152
|
+
context.commands.notify();
|
|
1153
|
+
owe("commands");
|
|
1154
|
+
}
|
|
1115
1155
|
if (update.requested) return;
|
|
1116
1156
|
if (subject === null || subject === "agents") agentRefresh.trigger();
|
|
1117
1157
|
if (subject === null || subject === "working_copy") copies.trigger();
|
|
@@ -1119,23 +1159,37 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1119
1159
|
};
|
|
1120
1160
|
context.onIdle = pointer;
|
|
1121
1161
|
context.workingCopiesApplied = copies.settled;
|
|
1122
|
-
if (!update.requested) copies.trigger();
|
|
1123
1162
|
const watching = (dependencies.watchKitWork ?? watchKitWork)(
|
|
1124
1163
|
context.session,
|
|
1125
1164
|
AbortSignal.any([signal, watcher.signal]),
|
|
1126
|
-
|
|
1165
|
+
{
|
|
1166
|
+
available,
|
|
1167
|
+
changed,
|
|
1168
|
+
opened: context.stream.opened,
|
|
1169
|
+
closed: context.stream.closed,
|
|
1170
|
+
viewed: (frame) => served.forward(frame),
|
|
1171
|
+
},
|
|
1127
1172
|
dependencies,
|
|
1128
1173
|
).catch(fail);
|
|
1129
1174
|
try {
|
|
1130
1175
|
while (!signal.aborted) {
|
|
1131
1176
|
if (failure !== null) throw failure;
|
|
1132
|
-
if (!session.refused) {
|
|
1177
|
+
if (passOwed !== null && !session.refused) {
|
|
1178
|
+
const reach = passOwed;
|
|
1179
|
+
passOwed = null;
|
|
1133
1180
|
const configuration = await readConfiguration(options.configuration);
|
|
1134
|
-
await controlledPass(context, dependencies, configuration, signal).catch(
|
|
1135
|
-
|
|
1136
|
-
|
|
1181
|
+
const passed = await controlledPass(context, dependencies, configuration, signal, reach).catch(
|
|
1182
|
+
(error) => {
|
|
1183
|
+
if (!session.refused) throw error;
|
|
1184
|
+
return null;
|
|
1185
|
+
},
|
|
1186
|
+
);
|
|
1187
|
+
if ((passed?.run ?? null) !== null) owe(reach);
|
|
1188
|
+
if ((passed?.unresolved.length ?? 0) > 0) defer(reach);
|
|
1189
|
+
else if (passed !== null) settle(reach);
|
|
1137
1190
|
}
|
|
1138
|
-
|
|
1191
|
+
const draining = session.refused || update.requested;
|
|
1192
|
+
if (draining) {
|
|
1139
1193
|
const sessions = await heldSessions(context);
|
|
1140
1194
|
if (sessions.length > 0) update.answer({ ready: false, sessions });
|
|
1141
1195
|
else if (
|
|
@@ -1150,8 +1204,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1150
1204
|
return;
|
|
1151
1205
|
}
|
|
1152
1206
|
}
|
|
1153
|
-
await wake.wait(interval, signal);
|
|
1154
|
-
if (!update.requested && !session.refused) deliveries.trigger();
|
|
1207
|
+
if (session.refused || passOwed === null) await wake.wait(draining ? interval : Infinity, signal);
|
|
1155
1208
|
}
|
|
1156
1209
|
} finally {
|
|
1157
1210
|
watcher.abort();
|
|
@@ -1185,7 +1238,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1185
1238
|
await wake.wait(interval, options.signal);
|
|
1186
1239
|
}
|
|
1187
1240
|
update.quiesce();
|
|
1188
|
-
while (!options.signal.aborted) await wake.wait(
|
|
1241
|
+
while (!options.signal.aborted) await wake.wait(Infinity, options.signal);
|
|
1189
1242
|
throw new KitRefusal("kit_command_cancelled");
|
|
1190
1243
|
} finally {
|
|
1191
1244
|
await served.close();
|
package/src/working-copy.js
CHANGED
|
@@ -497,7 +497,7 @@ async function deliver(context, path, authority, signal) {
|
|
|
497
497
|
let copy = await readCopy(path);
|
|
498
498
|
if (copy?.active && JSON.stringify(copy.received.declared) !== JSON.stringify(declaration(authority))) {
|
|
499
499
|
await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
|
|
500
|
-
return;
|
|
500
|
+
return false;
|
|
501
501
|
}
|
|
502
502
|
while (
|
|
503
503
|
copy?.active &&
|
|
@@ -508,24 +508,26 @@ async function deliver(context, path, authority, signal) {
|
|
|
508
508
|
try {
|
|
509
509
|
view = await enumerated(context, copy, since, signal);
|
|
510
510
|
} catch (error) {
|
|
511
|
-
if (error.code === "replica_behind") return;
|
|
511
|
+
if (error.code === "replica_behind") return true;
|
|
512
512
|
if (error.code !== "position_expired") throw error;
|
|
513
513
|
await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
|
|
514
|
-
return;
|
|
514
|
+
return false;
|
|
515
515
|
}
|
|
516
|
-
if (view.position === since.position) return;
|
|
516
|
+
if (view.position === since.position) return false;
|
|
517
517
|
copy = await incorporated(copy, view, authority, signal);
|
|
518
518
|
await writeCopy(context.directory, copy, signal);
|
|
519
519
|
}
|
|
520
|
+
return false;
|
|
520
521
|
}
|
|
521
522
|
|
|
522
523
|
async function delivering(context, signal) {
|
|
523
524
|
const copies = (await heldCopies(context.directory)).filter((copy) => copy.active);
|
|
524
|
-
if (copies.length === 0) return { refused: [], lost: false };
|
|
525
|
+
if (copies.length === 0) return { refused: [], lost: false, behind: false };
|
|
525
526
|
const prefixes = copies.flatMap((copy) => selectedPrefixes(scopeOf(copy)));
|
|
526
527
|
const discovery = discoveryOf(await door(context.session, KIT_DOOR_DISCOVER_PATH, { prefixes }, signal));
|
|
527
528
|
const refused = [];
|
|
528
529
|
let lost = false;
|
|
530
|
+
let behind = false;
|
|
529
531
|
for (const copy of copies) {
|
|
530
532
|
const at = housePath(scopeOf(copy));
|
|
531
533
|
const authority = discovery.authorities.find((entry) => entry?.room_ref === copy.room_ref);
|
|
@@ -539,18 +541,24 @@ async function delivering(context, signal) {
|
|
|
539
541
|
}
|
|
540
542
|
const path = copyPath(context.directory, copy);
|
|
541
543
|
try {
|
|
542
|
-
await exclusively(path, () => deliver(context, path, authority, signal));
|
|
544
|
+
if (await exclusively(path, () => deliver(context, path, authority, signal))) behind = true;
|
|
543
545
|
} catch (error) {
|
|
544
546
|
if (!scopeRefused(error)) throw error;
|
|
545
547
|
refused.push({ path: at, code: error.code });
|
|
546
548
|
}
|
|
547
549
|
}
|
|
548
|
-
return { refused, lost };
|
|
550
|
+
return { refused, lost, behind };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function deliveredOutcome(refused, behind) {
|
|
554
|
+
return behind ? { refused, behind } : { refused };
|
|
549
555
|
}
|
|
550
556
|
|
|
551
557
|
export async function deliverWorkingCopies(context, signal) {
|
|
552
558
|
const delivered = await delivering(context, signal);
|
|
553
|
-
return delivered.lost
|
|
559
|
+
return delivered.lost
|
|
560
|
+
? applyWorkingCopies(context, signal)
|
|
561
|
+
: deliveredOutcome(delivered.refused, delivered.behind);
|
|
554
562
|
}
|
|
555
563
|
|
|
556
564
|
export async function applyWorkingCopies(context, signal) {
|
|
@@ -582,7 +590,8 @@ export async function applyWorkingCopies(context, signal) {
|
|
|
582
590
|
refused.push({ path: housePath(scope), code: error.code });
|
|
583
591
|
}
|
|
584
592
|
}
|
|
585
|
-
|
|
593
|
+
const delivered = await delivering(context, signal);
|
|
594
|
+
return deliveredOutcome([...refused, ...delivered.refused], delivered.behind);
|
|
586
595
|
}
|
|
587
596
|
|
|
588
597
|
export async function workingCopyPaths(directory) {
|