@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/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 terminal = bridge.departed;
191
- let live = true;
192
- terminal.then(() => {
193
- live = false;
194
- });
195
- let commanded = Date.now() + WORKER_COMMAND_POLL_MS;
196
- let pulsed = Date.now() + WORKER_HEARTBEAT_MS;
197
- while (live) {
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 (Date.now() >= commanded) {
202
- commanded = Date.now() + WORKER_COMMAND_POLL_MS;
186
+ if (commanded) {
203
187
  const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
204
- for (const answer of polled.answers) {
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.interrupts) {
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.stops.find((command) => command.run === bound.run);
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 drafts = openAssistantDrafts({
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: (text) => drafts.update(text),
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, drafts, ended };
393
+ return { bridge, frames, ended };
417
394
  }
418
395
 
419
396
  async function superviseBound(context, bound, opened, start) {
420
- const { bridge, drafts, ended } = opened;
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
- try {
435
- await settleFiles(context);
436
- for (const interaction of bridge.unsettledInteractions()) {
437
- await settleInteraction(context, interaction, "expired");
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
- drafts.close();
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)).stops;
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
- await reportedWorkingCopies(applyWorkingCopies, context, signal);
934
- return controlledPass(context, dependencies, context.configuration, signal);
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
- for (const refused of (await act(context, signal)).refused) {
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 copies = coalescedTask(signal, unavailable, () =>
1096
- reportedWorkingCopies(applyWorkingCopies, context, signal),
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
- const available = (subject = null) => {
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
- available,
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((error) => {
1135
- if (!session.refused) throw error;
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
- if (session.refused || update.requested) {
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(interval, options.signal);
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();
@@ -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 ? applyWorkingCopies(context, signal) : { refused: delivered.refused };
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
- return { refused: [...refused, ...(await delivering(context, signal)).refused] };
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) {