@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 +22 -4
- package/bin/connect-linux.sh +3 -1
- package/bin/kit.js +29 -12
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/bytes.js +7 -5
- package/src/original.js +4 -4
- package/src/sync-state.js +12 -3
- package/src/sync.js +21 -11
- package/src/telegram.js +9 -3
- package/src/working-copy.js +5 -5
- package/src/workspace.js +40 -32
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.
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
package/bin/connect-linux.sh
CHANGED
|
@@ -44,7 +44,9 @@ login_kit() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
start_resident() {
|
|
47
|
-
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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({
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
-
|
|
62
|
-
|
|
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"
|
|
137
|
-
|
|
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",
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
156
|
+
if (!kept.has(file.path)) await removeReplicaFile(replica, previous.path, file.path);
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
|
-
return
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
1124
|
+
refusedPass(gateway, { activity: "drafts", item: null, code: error?.code ?? "kit_stream_failed" });
|
|
1119
1125
|
});
|
|
1120
1126
|
try {
|
|
1121
1127
|
await serveActivities(gateway, stopping);
|
package/src/working-copy.js
CHANGED
|
@@ -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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
485
|
-
|
|
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
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
}
|