@agentshouse/kit 0.1.0-alpha.29 → 0.1.0-alpha.30

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 CHANGED
@@ -12,7 +12,7 @@ The Skill set Kit installs is Matt Pocock's
12
12
  On Linux `amd64` or `arm64`, run the published, versioned bootstrap once:
13
13
 
14
14
  ```sh
15
- bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.29/connect-linux.sh)
15
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.30/connect-linux.sh)
16
16
  ```
17
17
 
18
18
  The bootstrap first establishes Docker Engine from what it observes on the
@@ -42,7 +42,11 @@ Kit home and `/agents/house`, enrolls through House Login in the host browser,
42
42
  and starts the container with Docker's `unless-stopped` lifecycle. Pass
43
43
  `--workspace /absolute/path` for one other workspace root or `--manual` for
44
44
  the existing URL and one-time-code login route. `--house <origin>` selects
45
- another House and is retained in Kit home. A repeated invocation reuses the
45
+ another House and is retained in Kit home. A local loopback House,
46
+ `http://127.0.0.1:<port>` or `http://localhost:<port>`, runs the resident
47
+ container on the host network, as the one-shot login, credential check and
48
+ apply containers always run, so the resident reaches the House the host serves;
49
+ an HTTPS House leaves it on Docker's own network. A repeated invocation reuses the
46
50
  same container, credential and Environment. Every invocation that is not a
47
51
  forwarded command also sets `skills` in Kit's configuration: `--no-skills`
48
52
  declines [the Skill set](#the-skill-set) and an invocation without it accepts
@@ -1377,7 +1381,8 @@ copy's recorded position through `POST /kit/door/enumerate`. A changed, new or
1377
1381
  removed authored file becomes a commit on `refs/house/received`, its House
1378
1382
  revision kept in Kit home; changed, new or removed read-only content, such as
1379
1383
  Capture with its provenance sidecar or a House projection, is written into or
1380
- removed from the ignored local tree, and `.gitignore` and `.ignore` follow it.
1384
+ removed from the ignored local tree, with each directory a removal leaves empty,
1385
+ and `.gitignore` and `.ignore` follow it.
1381
1386
  Delivery never touches the branch, the index or a tracked file, and a push
1382
1387
  waiting for its outcome or its local integration keeps its record. A push takes
1383
1388
  each file's revision from the newest House state in its own base's history that
@@ -1524,7 +1529,10 @@ a Room the User can only read answers `bootstrap`, `pull`, and `rebootstrap`
1524
1529
  with `operation_denied`. Generated `_provenance` files delivered beside authored
1525
1530
  Library paths are House metadata, not authored Library documents. `pull` presents
1526
1531
  each recorded position with its log epoch, applies each enumerated path's whole
1527
- content or removal, and advances those positions. A position older than House's
1532
+ content or removal, and advances those positions. A removal, pulled or accepted
1533
+ from `submit`, also removes each directory it leaves empty up to the authority,
1534
+ so a later file can take that path; a directory still holding a file, such as a
1535
+ local draft, stays. A position older than House's
1528
1536
  declared retention, or one of a log epoch before House's database was restored,
1529
1537
  is reported as `position_expired`; only `rebootstrap` rebuilds the directory.
1530
1538
 
@@ -1658,6 +1666,13 @@ House authorizes: a managed workspace House no longer authorizes to run is
1658
1666
  stopped, and a container this Kit did not create is never adopted, started or
1659
1667
  removed.
1660
1668
 
1669
+ A pass House does not answer, such as one during a House restart, ends that
1670
+ pass only, and the host tries again on the next one; a pass House refuses ends
1671
+ the host. Without `--passes` the host serves until SIGINT or SIGTERM. It
1672
+ asks House to release its control lease when it ends, after its passes, a
1673
+ refusal, SIGINT or SIGTERM; once House has released it, a restarted host is
1674
+ admitted at once.
1675
+
1661
1676
  Ceilings are shared ceilings. The ceilings configured across one host may exceed
1662
1677
  that host, nothing is reserved for any workspace, and no workspace is promised
1663
1678
  its maximum while another one runs.
@@ -1766,6 +1781,9 @@ so a restarted gateway never leaves a delivery attempted forever. A Telegram
1766
1781
  the delay Telegram supplied has passed. `--passes <count>` bounds each activity
1767
1782
  to that many passes and is what tests and one-shot runs use; without it the
1768
1783
  command is resident and stops on SIGINT or SIGTERM, printing what it settled.
1784
+ Each refused pass is printed as it happens, naming its activity, item and
1785
+ refusal code, so a gateway refused on every pass says so while it runs; the
1786
+ summary printed when it stops still lists each of them.
1769
1787
 
1770
1788
  Release the bot:
1771
1789
 
@@ -44,7 +44,9 @@ login_kit() {
44
44
  }
45
45
 
46
46
  start_resident() {
47
- docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident >/dev/null
47
+ local network=()
48
+ [[ "$HOUSE" == https://* ]] || network=(--network host)
49
+ docker run -d --name "$NAME" --restart unless-stopped "${network[@]}" --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident >/dev/null
48
50
  }
49
51
 
50
52
  docker() {
package/bin/kit.js CHANGED
@@ -862,12 +862,23 @@ async function runWorkspaceHost(args, directory) {
862
862
  throw new Error(WORKSPACE_USAGE);
863
863
  }
864
864
  const house = await storedHouse(directory);
865
- const served = await serveWorkspaces({
866
- house,
867
- directory,
868
- configuration: values["--config"] ?? join(directory, "kit.json"),
869
- passes,
870
- });
865
+ const cancellation = new AbortController();
866
+ const cancel = () => cancellation.abort();
867
+ process.once("SIGINT", cancel);
868
+ process.once("SIGTERM", cancel);
869
+ let served;
870
+ try {
871
+ served = await serveWorkspaces({
872
+ house,
873
+ directory,
874
+ configuration: values["--config"] ?? join(directory, "kit.json"),
875
+ passes,
876
+ signal: cancellation.signal,
877
+ });
878
+ } finally {
879
+ process.removeListener("SIGINT", cancel);
880
+ process.removeListener("SIGTERM", cancel);
881
+ }
871
882
  for (const name of served.reconciled) {
872
883
  write(`Stopped ${name}; House authorizes no running workspace for it.`);
873
884
  }
@@ -934,7 +945,13 @@ async function runTelegramServe(args, directory) {
934
945
  process.once("SIGTERM", cancel);
935
946
  let served;
936
947
  try {
937
- served = await serveTelegramGateway({ house, directory, passes, signal: cancellation.signal });
948
+ served = await serveTelegramGateway({
949
+ house,
950
+ directory,
951
+ passes,
952
+ signal: cancellation.signal,
953
+ refused: (failure) => write(refusedPassLine(failure)),
954
+ });
938
955
  } finally {
939
956
  process.removeListener("SIGINT", cancel);
940
957
  process.removeListener("SIGTERM", cancel);
@@ -954,14 +971,14 @@ async function runTelegramServe(args, directory) {
954
971
  `File send ${settled.upload} settled ${settled.outcome}${settled.reason === null ? "" : ` (${settled.reason})`}.`,
955
972
  );
956
973
  }
957
- for (const failure of served.failures) {
958
- write(
959
- `The ${failure.activity} pass${failure.item === null ? "" : ` for ${failure.item}`} could not finish (${failure.code}).`,
960
- );
961
- }
974
+ for (const failure of served.failures) write(refusedPassLine(failure));
962
975
  if (served.failures.length > 0) process.exitCode = 1;
963
976
  }
964
977
 
978
+ function refusedPassLine(failure) {
979
+ return `The ${failure.activity} pass${failure.item === null ? "" : ` for ${failure.item}`} could not finish (${failure.code}).`;
980
+ }
981
+
965
982
  async function main() {
966
983
  const [command, ...args] = process.argv.slice(2);
967
984
  const directory = process.env.HOUSE_KIT_HOME ?? join(homedir(), ".house-kit");
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.29",
3
+ "version": "0.1.0-alpha.30",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.29/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.30/connect-linux.sh)",
7
7
  "asset": {
8
8
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.29/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.30/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.29",
3
+ "version": "0.1.0-alpha.30",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/bytes.js CHANGED
@@ -43,7 +43,7 @@ export async function transferBytes(request, dependencies = {}) {
43
43
  if (typeof request.url !== "string" || request.url.length === 0) {
44
44
  throw new KitRefusal("kit_byte_url_invalid");
45
45
  }
46
- if (typeof request.operation !== "string" || request.operation.length === 0) {
46
+ if (request.method === "POST" && (typeof request.operation !== "string" || request.operation.length === 0)) {
47
47
  throw new KitRefusal("kit_byte_operation_invalid");
48
48
  }
49
49
  const framed = request.framed === true;
@@ -57,10 +57,12 @@ export async function transferBytes(request, dependencies = {}) {
57
57
  const fetcher = dependencies.fetch ?? fetch;
58
58
  const headers = framed
59
59
  ? { "content-type": "text/plain; charset=utf-8" }
60
- : {
61
- [BYTE_GRANT_OPERATION_HEADER]: request.operation,
62
- ...(request.method === "POST" ? { "content-type": "application/octet-stream" } : {}),
63
- };
60
+ : request.method === "POST"
61
+ ? {
62
+ [BYTE_GRANT_OPERATION_HEADER]: request.operation,
63
+ "content-type": "application/octet-stream",
64
+ }
65
+ : {};
64
66
  let response;
65
67
  try {
66
68
  response = await fetcher(request.url, {
package/src/original.js CHANGED
@@ -133,9 +133,9 @@ function outcomeOf(answer) {
133
133
 
134
134
  function grantOf(value, kind) {
135
135
  if (value === undefined) return null;
136
- if (typeof value?.url !== "string" || typeof value?.operation !== "string") {
137
- refuse("kit_original_grant_rejected", kind);
138
- }
136
+ if (typeof value?.url !== "string") refuse("kit_original_grant_rejected", kind);
137
+ if (kind === "download") return { url: value.url };
138
+ if (typeof value.operation !== "string") refuse("kit_original_grant_rejected", kind);
139
139
  return { url: value.url, operation: value.operation };
140
140
  }
141
141
 
@@ -148,7 +148,7 @@ function sendBytes(dependencies, grant, body, signal) {
148
148
 
149
149
  async function receiveBytes(session, grant, signal) {
150
150
  const transferred = await transferBytes(
151
- { url: grant.url, method: "GET", operation: grant.operation, signal },
151
+ { url: grant.url, method: "GET", signal },
152
152
  session.dependencies,
153
153
  );
154
154
  return transferred.bytes;
package/src/sync-state.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdir, readFile, readdir, realpath, rm } from "node:fs/promises";
3
- import { join, relative, resolve } from "node:path";
2
+ import { mkdir, readFile, readdir, realpath, rm, rmdir } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
4
  import { EXPLICIT_SYNC_VERSION } from "./contract.js";
5
5
  import { KitRefusal } from "./refusal.js";
6
6
  import { readNote, writeNote, writeText } from "./state.js";
@@ -85,12 +85,21 @@ export async function writeReplicaFile(path, content, signal) {
85
85
  return writeText(path, content, signal, 0o644);
86
86
  }
87
87
 
88
- export async function removeReplicaFile(path) {
88
+ export async function removeReplicaFile(replica, rootPath, file) {
89
+ const root = locate(replica, rootPath);
90
+ const path = locate(replica, rootPath, file);
89
91
  try {
90
92
  await rm(path);
91
93
  } catch (error) {
92
94
  if (error.code !== "ENOENT") throw new KitRefusal("kit_state_unreadable", path);
93
95
  }
96
+ for (let directory = dirname(path); directory.startsWith(`${root}/`); directory = dirname(directory)) {
97
+ try {
98
+ await rmdir(directory);
99
+ } catch (error) {
100
+ if (error.code !== "ENOENT") return;
101
+ }
102
+ }
94
103
  }
95
104
 
96
105
  export async function listFiles(root) {
package/src/sync.js CHANGED
@@ -110,7 +110,7 @@ function matchedDiscovery(discovery, scope) {
110
110
  );
111
111
  }
112
112
 
113
- async function writeScope(replica, scope, signal) {
113
+ function scopeContents(replica, scope) {
114
114
  if (
115
115
  typeof scope?.manifest?.path !== "string" ||
116
116
  typeof scope.manifest.position !== "string" ||
@@ -121,11 +121,18 @@ async function writeScope(replica, scope, signal) {
121
121
  if (!Array.isArray(scope.contents) || !Array.isArray(scope.manifest.files)) {
122
122
  throw new KitRefusal("kit_bootstrap_rejected");
123
123
  }
124
- const files = [];
125
124
  for (const entry of scope.contents) {
126
125
  if (typeof entry?.path !== "string" || typeof entry.content !== "string") {
127
126
  throw new KitRefusal("kit_bootstrap_rejected");
128
127
  }
128
+ locate(replica, scope.manifest.path, entry.path);
129
+ }
130
+ return scope.contents;
131
+ }
132
+
133
+ async function writeScope(replica, scope, signal) {
134
+ const files = [];
135
+ for (const entry of scopeContents(replica, scope)) {
129
136
  const meta = scope.manifest.files.find((file) => file.path === entry.path);
130
137
  await writeReplicaFile(locate(replica, scope.manifest.path, entry.path), entry.content, signal);
131
138
  files.push({
@@ -139,18 +146,21 @@ async function writeScope(replica, scope, signal) {
139
146
  }
140
147
 
141
148
  async function replaceScope(replica, previous, scope, signal) {
142
- const next = await writeScope(replica, scope, signal);
143
- const kept = new Set(next.map((file) => file.path));
149
+ const kept = new Set(scopeContents(replica, scope).map((entry) => entry.path));
144
150
  const root = locate(replica, scope.manifest.path);
145
151
  for (const path of await listFiles(root)) {
146
- if (!kept.has(path)) await removeReplicaFile(locate(replica, scope.manifest.path, path));
152
+ if (!kept.has(path)) await removeReplicaFile(replica, scope.manifest.path, path);
147
153
  }
148
154
  if (previous !== undefined) {
149
155
  for (const file of previous.files) {
150
- if (!kept.has(file.path)) await removeReplicaFile(locate(replica, previous.path, file.path));
156
+ if (!kept.has(file.path)) await removeReplicaFile(replica, previous.path, file.path);
151
157
  }
152
158
  }
153
- return next;
159
+ return writeScope(replica, scope, signal);
160
+ }
161
+
162
+ function removalsFirst(entries, removal) {
163
+ return [...entries.filter(removal), ...entries.filter((entry) => !removal(entry))];
154
164
  }
155
165
 
156
166
  function stringList(value) {
@@ -245,10 +255,10 @@ async function applyEnumeration(options, state, enumeration, signal) {
245
255
  if (authority === undefined) throw new KitRefusal("kit_sync_rejected", view.authority);
246
256
  const reached = changePosition(view, "enumerate");
247
257
  const recorded = new Map(authority.files.map((file) => [file.path, file]));
248
- for (const change of view.changes ?? []) {
258
+ for (const change of removalsFirst(view.changes ?? [], (change) => change?.removed === true)) {
249
259
  if (typeof change?.path !== "string") throw new KitRefusal("kit_sync_rejected", "enumerate");
250
260
  if (change.removed === true) {
251
- await removeReplicaFile(locate(options.replica, authority.path, change.path));
261
+ await removeReplicaFile(options.replica, authority.path, change.path);
252
262
  recorded.delete(change.path);
253
263
  continue;
254
264
  }
@@ -305,7 +315,7 @@ async function acceptBatch(options, state, outcome, signal, pending = null) {
305
315
  const reached = changePosition(outcome, "batch");
306
316
  const recorded = new Map(authority.files.map((file) => [file.path, file]));
307
317
  const predicted = predictedReplica(authority, pending);
308
- for (const file of outcome.contents ?? []) {
318
+ for (const file of removalsFirst(outcome.contents ?? [], (file) => file?.content === null)) {
309
319
  const relative = relativeTo(authority.path, file.path);
310
320
  const destination = locate(options.replica, authority.path, relative);
311
321
  const local = await replicaText(destination);
@@ -319,7 +329,7 @@ async function acceptBatch(options, state, outcome, signal, pending = null) {
319
329
  local !== accepted &&
320
330
  local !== expected;
321
331
  if (accepted === null) {
322
- if (!preserve) await removeReplicaFile(destination);
332
+ if (!preserve) await removeReplicaFile(options.replica, authority.path, relative);
323
333
  recorded.delete(relative);
324
334
  continue;
325
335
  }
package/src/telegram.js CHANGED
@@ -736,7 +736,7 @@ async function relayInboundFiles(gateway) {
736
736
  gateway.summary.relayed.push({ attachment: file.id, bytes: bytes.length });
737
737
  } catch (error) {
738
738
  if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
739
- gateway.summary.failures.push({ activity: "inbound", item: file.id, code: error.code });
739
+ refusedPass(gateway, { activity: "inbound", item: file.id, code: error.code });
740
740
  }
741
741
  }
742
742
  }
@@ -914,12 +914,17 @@ async function drainOutboundQueue(gateway) {
914
914
  }
915
915
  }
916
916
 
917
+ function refusedPass(gateway, failure) {
918
+ gateway.summary.failures.push(failure);
919
+ gateway.refused(failure);
920
+ }
921
+
917
922
  async function onePass(gateway, activity, work) {
918
923
  try {
919
924
  await work(gateway);
920
925
  } catch (error) {
921
926
  if (cancelled(error) || !(error instanceof KitRefusal) || error.code === "kit_update_draining") throw error;
922
- gateway.summary.failures.push({ activity, item: null, code: error.code });
927
+ refusedPass(gateway, { activity, item: null, code: error.code });
923
928
  }
924
929
  }
925
930
 
@@ -1022,6 +1027,7 @@ function gatewayContext(session, options, signal, summary = emptySummary()) {
1022
1027
  passes: options.passes ?? Number.POSITIVE_INFINITY,
1023
1028
  draining: false,
1024
1029
  summary,
1030
+ refused: options.refused,
1025
1031
  };
1026
1032
  }
1027
1033
 
@@ -1115,7 +1121,7 @@ export async function serveTelegramGateway(options, dependencies = {}) {
1115
1121
  gateway.live.heard,
1116
1122
  ).catch((error) => {
1117
1123
  if (cancelled(error)) return;
1118
- summary.failures.push({ activity: "drafts", item: null, code: error?.code ?? "kit_stream_failed" });
1124
+ refusedPass(gateway, { activity: "drafts", item: null, code: error?.code ?? "kit_stream_failed" });
1119
1125
  });
1120
1126
  try {
1121
1127
  await serveActivities(gateway, stopping);
@@ -266,6 +266,11 @@ async function trackedPaths(repository, signal) {
266
266
  async function replaceReadOnly(repository, held, written, removed, signal) {
267
267
  const tracked = await trackedPaths(repository, signal);
268
268
  const holding = new Set(held);
269
+ for (const path of removed) {
270
+ if (!held.has(path)) continue;
271
+ if (!tracked.has(path)) await removeReplicaFile(repository, "", path);
272
+ holding.delete(path);
273
+ }
269
274
  for (const [path, content] of written) {
270
275
  if (tracked.has(path)) continue;
271
276
  const target = locate(repository, "", path);
@@ -273,11 +278,6 @@ async function replaceReadOnly(repository, held, written, removed, signal) {
273
278
  await writeReplicaFile(target, content, signal);
274
279
  holding.add(path);
275
280
  }
276
- for (const path of removed) {
277
- if (!held.has(path)) continue;
278
- if (!tracked.has(path)) await removeReplicaFile(locate(repository, "", path));
279
- holding.delete(path);
280
- }
281
281
  return [...holding];
282
282
  }
283
283
 
package/src/workspace.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  releaseControl,
29
29
  reportWorkspaceOutcome,
30
30
  } from "./house.js";
31
- import { KitRefusal } from "./refusal.js";
31
+ import { houseFailed, KitRefusal } from "./refusal.js";
32
32
 
33
33
  const REQUIRED_CONTROLLERS = ["cpu", "memory"];
34
34
  const MEMINFO = "/proc/meminfo";
@@ -461,40 +461,48 @@ export async function serveWorkspaces(options, dependencies = {}) {
461
461
  const withdrawn = [];
462
462
  let reconciled = [];
463
463
  const passes = options.passes ?? Number.POSITIVE_INFINITY;
464
- for (let pass = 0; pass < passes; pass += 1) {
465
- const work = await maintainControl(
466
- session,
467
- async (signal) => {
468
- const acquired = await acquireWorkspaces(session, WORKSPACE_ACQUIRE_MAX, signal);
469
- reconciled = await reconcileWorkspaces(host, acquired.workspaces);
470
- for (const operation of acquired.operations) {
471
- let report;
472
- try {
473
- report = await performOperation(host, settings, operation);
474
- } catch (error) {
475
- if (!(error instanceof KitRefusal)) throw error;
476
- report = { outcome: "failed", refusal: error.message };
477
- }
478
- try {
479
- await reportWorkspaceOutcome(session, operation.operation, report, signal);
480
- } catch (error) {
481
- if (!(error instanceof KitRefusal) || error.code !== WORKSPACE_OPERATION_WITHDRAWN) {
482
- throw error;
464
+ try {
465
+ for (let pass = 0; pass < passes; pass += 1) {
466
+ const work = await maintainControl(
467
+ session,
468
+ async (signal) => {
469
+ const acquired = await acquireWorkspaces(session, WORKSPACE_ACQUIRE_MAX, signal);
470
+ reconciled = await reconcileWorkspaces(host, acquired.workspaces);
471
+ for (const operation of acquired.operations) {
472
+ let report;
473
+ try {
474
+ report = await performOperation(host, settings, operation);
475
+ } catch (error) {
476
+ if (!(error instanceof KitRefusal)) throw error;
477
+ report = { outcome: "failed", refusal: error.message };
483
478
  }
484
- withdrawn.push({ operation: operation.operation, kind: operation.kind });
485
- continue;
479
+ try {
480
+ await reportWorkspaceOutcome(session, operation.operation, report, signal);
481
+ } catch (error) {
482
+ if (!(error instanceof KitRefusal) || error.code !== WORKSPACE_OPERATION_WITHDRAWN) {
483
+ throw error;
484
+ }
485
+ withdrawn.push({ operation: operation.operation, kind: operation.kind });
486
+ continue;
487
+ }
488
+ settled.push({ operation: operation.operation, kind: operation.kind, ...report });
486
489
  }
487
- settled.push({ operation: operation.operation, kind: operation.kind, ...report });
488
- }
489
- return acquired;
490
- },
491
- options.signal,
492
- { release: false },
493
- );
494
- if (pass + 1 < passes && work.operations.length === 0) {
495
- await host.wait(WORKSPACE_POLL_MS);
490
+ return acquired;
491
+ },
492
+ options.signal,
493
+ { release: false },
494
+ ).catch((error) => {
495
+ if (!houseFailed(error)) throw error;
496
+ return null;
497
+ });
498
+ if (pass + 1 < passes && (work === null || work.operations.length === 0)) {
499
+ await host.wait(WORKSPACE_POLL_MS);
500
+ }
496
501
  }
502
+ } catch (error) {
503
+ if (!(error instanceof KitRefusal) || error.code !== "kit_command_cancelled") throw error;
504
+ } finally {
505
+ await releaseControl(session);
497
506
  }
498
- await releaseControl(session);
499
507
  return { settled, reconciled, withdrawn };
500
508
  }