@agentshouse/kit 0.1.0-alpha.20 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -7
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/contract.js +1 -1
- package/src/worker.js +20 -10
- package/src/working-copy.js +177 -33
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
|
|
|
10
10
|
versioned bootstrap once:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
13
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -1261,9 +1261,35 @@ committed.
|
|
|
1261
1261
|
Local edits, staging and commits change nothing in House until a commit is
|
|
1262
1262
|
pushed. `refs/house/received` records the House content last received; each
|
|
1263
1263
|
commit Kit wrote there, and each commit it acknowledged for a push, is a House
|
|
1264
|
-
state. The House revision of every file content Kit received, keyed by path
|
|
1265
|
-
Git blob, stays in Kit home under
|
|
1266
|
-
repository. A local commit is a local tree, never a House revision.
|
|
1264
|
+
state. The House revision of every file content Kit received, keyed by path,
|
|
1265
|
+
Git blob and the House state that received it, stays in Kit home under
|
|
1266
|
+
`state/working-copies/`, outside the repository. A local commit is a local tree, never a House revision.
|
|
1267
|
+
|
|
1268
|
+
The resident Kit keeps every active copy current. On each pass, and whenever
|
|
1269
|
+
it reads the saved choice, one `POST /kit/door/discover` tells it where each
|
|
1270
|
+
copied Room stands, and for a Room that moved on it reads the changes after the
|
|
1271
|
+
copy's recorded position through `POST /kit/door/enumerate`. A changed, new or
|
|
1272
|
+
removed authored file becomes a commit on `refs/house/received`, its House
|
|
1273
|
+
revision kept in Kit home; changed, new or removed read-only content, such as
|
|
1274
|
+
Capture with its provenance sidecar or a House projection, is written into or
|
|
1275
|
+
removed from the ignored local tree, and `.gitignore` and `.ignore` follow it.
|
|
1276
|
+
Delivery never touches the branch, the index or a tracked file, and a push
|
|
1277
|
+
waiting for its outcome or its local integration keeps its record. A push takes
|
|
1278
|
+
each file's revision from the newest House state in its own base's history that
|
|
1279
|
+
received that content, so a local commit keeps the base it was made on even when
|
|
1280
|
+
House returns to the same text under a newer revision. A file new to the copy is tracked when a write scope
|
|
1281
|
+
House declares for the Room covers it more closely than a declared protected
|
|
1282
|
+
path, and a local file already sitting where new read-only content arrives
|
|
1283
|
+
stays yours: Kit neither overwrites nor later removes it. When the write scopes
|
|
1284
|
+
or protected paths House declares for the Room change, Kit reads the Room again
|
|
1285
|
+
as a reselection does. A position older than House
|
|
1286
|
+
retains, or from before House's database was restored, makes Kit read the Room
|
|
1287
|
+
again as a reselection does; while House's replica has not reached the position
|
|
1288
|
+
the copy waits for the next pass. A Room House no longer answers under its
|
|
1289
|
+
recorded handle makes Kit read the saved choice again, which follows a handle
|
|
1290
|
+
change or marks the copy inactive, and a Room that lost sync is named on
|
|
1291
|
+
standard error as `operation_denied` and keeps what it holds. `kit worker run`
|
|
1292
|
+
delivers once, after reading the saved choice.
|
|
1267
1293
|
|
|
1268
1294
|
`house git push [commit]`, run anywhere inside a copy or one of its linked
|
|
1269
1295
|
worktrees, submits one commit, `HEAD` by default, resolved once. Kit compares
|
|
@@ -1319,7 +1345,10 @@ commits, and pushes again. After acceptance Kit records House's result for the
|
|
|
1319
1345
|
pushed paths, the contents an owner push receives or each accepted revision
|
|
1320
1346
|
read by name, in a commit that covers the pushed one and in
|
|
1321
1347
|
`refs/house/received`; a read-only path House regenerates, such as the Wiki
|
|
1322
|
-
index, stays out of Git.
|
|
1348
|
+
index, stays out of Git. The copy's recorded position goes back to where it
|
|
1349
|
+
stood when the push was sent, so the next delivery brings again anything House
|
|
1350
|
+
changed after the push's result, including what delivery received while the
|
|
1351
|
+
push awaited its answer. Kit then
|
|
1323
1352
|
fast-forwards a clean branch still at the pushed commit, or rebases only its
|
|
1324
1353
|
later commits. A result not yet readable, uncommitted tracked changes,
|
|
1325
1354
|
a moved branch or a rebase conflict leave the branch as it was, with no stash or
|
|
@@ -1335,8 +1364,8 @@ Room again: if House holds the pushed content the push finishes as accepted,
|
|
|
1335
1364
|
otherwise it is dropped and the next push submits it as a new operation.
|
|
1336
1365
|
|
|
1337
1366
|
Deselecting a Room, or losing read access to it, marks its copy inactive:
|
|
1338
|
-
launches omit it, a new push is refused as
|
|
1339
|
-
nothing local is deleted; a push already sent still settles and integrates. Selecting it again reads the Room
|
|
1367
|
+
launches omit it, delivery stops, a new push is refused as
|
|
1368
|
+
`kit_working_copy_inactive`, and nothing local is deleted; a push already sent still settles and integrates. Selecting it again reads the Room
|
|
1340
1369
|
again, advances `refs/house/received`, and replaces only the read-only content
|
|
1341
1370
|
Kit wrote, never the branch, the index or a tracked file. A Room the owner may
|
|
1342
1371
|
read without sync is refused as `operation_denied` and stays uncopied, while its
|
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.21",
|
|
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.21/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.21/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
package/src/contract.js
CHANGED
|
@@ -297,7 +297,7 @@ export const EXPLICIT_SYNC_VERSION = "explicit-sync/2";
|
|
|
297
297
|
export const KIT_DOOR_UNMOUNT_PATH = "/kit/door/unmount";
|
|
298
298
|
|
|
299
299
|
export const KIT_WORKING_COPY_SELECTION_PATH = "/kit/working-copy/selection";
|
|
300
|
-
export const WORKING_COPY_VERSION = "working-copy-state/
|
|
300
|
+
export const WORKING_COPY_VERSION = "working-copy-state/3";
|
|
301
301
|
export const WORKING_COPIES_HOME = "working-copies";
|
|
302
302
|
export const WORKING_COPY_ROOM_HOMES = ["capture", "library", "collaboration"];
|
|
303
303
|
export const WORKING_COPY_PRIVATE_HOMES = ["library", "collaboration", "rooms", "agents", "house"];
|
package/src/worker.js
CHANGED
|
@@ -19,7 +19,14 @@ import { localFiles } from "./local-files.js";
|
|
|
19
19
|
import { answerText, answerYaml } from "./mcp.js";
|
|
20
20
|
import { inheritedEnvironment } from "./command.js";
|
|
21
21
|
import { watchKitWork } from "./kit-work-stream.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
applyWorkingCopies,
|
|
24
|
+
deliverWorkingCopies,
|
|
25
|
+
ownerCaller,
|
|
26
|
+
pushWorkingCopy,
|
|
27
|
+
recordIndexChange,
|
|
28
|
+
workingCopyPaths,
|
|
29
|
+
} from "./working-copy.js";
|
|
23
30
|
import { readConfiguration } from "./configuration.js";
|
|
24
31
|
import { launchBase, readyLaunchDirectory } from "./directory.js";
|
|
25
32
|
import {
|
|
@@ -863,7 +870,7 @@ export async function runWorkers(options, dependencies = {}) {
|
|
|
863
870
|
const { context, tmux } = await workerContext(options, dependencies);
|
|
864
871
|
const { session, runtime } = context;
|
|
865
872
|
const supervised = await maintainControl(session, async (signal) => {
|
|
866
|
-
await
|
|
873
|
+
await reportedWorkingCopies(applyWorkingCopies, context, signal);
|
|
867
874
|
return controlledPass(context, dependencies, context.configuration, signal);
|
|
868
875
|
}, session.signal);
|
|
869
876
|
return {
|
|
@@ -884,9 +891,8 @@ function reported(line) {
|
|
|
884
891
|
process.stderr.write(`kit: ${line}\n`);
|
|
885
892
|
}
|
|
886
893
|
|
|
887
|
-
async function
|
|
888
|
-
const
|
|
889
|
-
for (const refused of applied.refused) {
|
|
894
|
+
async function reportedWorkingCopies(act, context, signal) {
|
|
895
|
+
for (const refused of (await act(context, signal)).refused) {
|
|
890
896
|
reported(`working copy ${refused.path} refused: ${refused.code}`);
|
|
891
897
|
}
|
|
892
898
|
}
|
|
@@ -1078,10 +1084,12 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1078
1084
|
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
1079
1085
|
context.startLogins(needsLogin);
|
|
1080
1086
|
});
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
(
|
|
1084
|
-
|
|
1087
|
+
const unavailable = (error) => reported(`working copies unavailable: ${error?.message ?? error}`);
|
|
1088
|
+
const copies = coalescedTask(signal, unavailable, () =>
|
|
1089
|
+
reportedWorkingCopies(applyWorkingCopies, context, signal),
|
|
1090
|
+
);
|
|
1091
|
+
const deliveries = coalescedTask(signal, unavailable, () =>
|
|
1092
|
+
reportedWorkingCopies(deliverWorkingCopies, context, signal),
|
|
1085
1093
|
);
|
|
1086
1094
|
const pointer = () => {
|
|
1087
1095
|
if (!update.requested) agentRefresh.trigger();
|
|
@@ -1114,18 +1122,20 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1114
1122
|
if (sessions.length > 0) update.answer({ ready: false, sessions });
|
|
1115
1123
|
else if (
|
|
1116
1124
|
session.refused ||
|
|
1117
|
-
(!served.serving() && logins.size === 0 && agentRefresh.idle() && copies.idle())
|
|
1125
|
+
(!served.serving() && logins.size === 0 && agentRefresh.idle() && copies.idle() && deliveries.idle())
|
|
1118
1126
|
) {
|
|
1119
1127
|
return;
|
|
1120
1128
|
}
|
|
1121
1129
|
}
|
|
1122
1130
|
await wake.wait(interval, signal);
|
|
1131
|
+
if (!update.requested && !session.refused) deliveries.trigger();
|
|
1123
1132
|
}
|
|
1124
1133
|
} finally {
|
|
1125
1134
|
watcher.abort();
|
|
1126
1135
|
await watching;
|
|
1127
1136
|
await agentRefresh.settled();
|
|
1128
1137
|
await copies.settled();
|
|
1138
|
+
await deliveries.settled();
|
|
1129
1139
|
for (const running of logins.values()) running.controller.abort();
|
|
1130
1140
|
await Promise.all([...logins.values()].map((running) => running.task));
|
|
1131
1141
|
await context.agentRefresh;
|
package/src/working-copy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readdir, realpath, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readdir, readFile, realpath, rename, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { readBindings } from "./binding.js";
|
|
5
5
|
import {
|
|
@@ -21,7 +21,7 @@ import { operationId } from "./operation.js";
|
|
|
21
21
|
import { formatCommandError, houseAnswered, houseFailed, KitRefusal } from "./refusal.js";
|
|
22
22
|
import { readNote, writeNote, writeText } from "./state.js";
|
|
23
23
|
import { bootstrapBundle, discoveryOf, door, withSession } from "./sync.js";
|
|
24
|
-
import { sidecarPath, structuredOrReplace } from "./sync-library.js";
|
|
24
|
+
import { isGeneratedProvenance, sidecarPath, structuredOrReplace } from "./sync-library.js";
|
|
25
25
|
import { locate, removeReplicaFile, writeReplicaFile } from "./sync-state.js";
|
|
26
26
|
|
|
27
27
|
const COMMITTER_NAME = "House Kit";
|
|
@@ -134,6 +134,7 @@ async function readScope(session, scope, signal) {
|
|
|
134
134
|
if (
|
|
135
135
|
authority === undefined ||
|
|
136
136
|
!Array.isArray(authority.protected_paths) ||
|
|
137
|
+
!Array.isArray(authority.operations?.write) ||
|
|
137
138
|
(scope.kind === "room" && view.manifest.anchor !== scope.ref)
|
|
138
139
|
) {
|
|
139
140
|
throw rejected("discover");
|
|
@@ -142,11 +143,16 @@ async function readScope(session, scope, signal) {
|
|
|
142
143
|
anchor: view.manifest.anchor,
|
|
143
144
|
position: view.manifest.position,
|
|
144
145
|
logEpoch: view.manifest.log_epoch,
|
|
145
|
-
protectedPaths: authority.protected_paths
|
|
146
|
+
protectedPaths: authority.protected_paths,
|
|
147
|
+
declared: declaration(authority),
|
|
146
148
|
files,
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
function declaration(authority) {
|
|
153
|
+
return { write: authority.operations.write, protected: authority.protected_paths };
|
|
154
|
+
}
|
|
155
|
+
|
|
150
156
|
function importData(text) {
|
|
151
157
|
const bytes = Buffer.from(text, "utf8");
|
|
152
158
|
return [Buffer.from(`data ${bytes.length}\n`), bytes, Buffer.from("\n")];
|
|
@@ -210,16 +216,28 @@ async function commitOf(repository, name, signal) {
|
|
|
210
216
|
return commit.toString("utf8").trim();
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
async function remembered(repository, revisions, commit, files, signal) {
|
|
219
|
+
async function remembered(repository, revisions, commit, files, signal, acknowledged = false) {
|
|
214
220
|
const tree = await treeOf(repository, commit, signal);
|
|
215
221
|
const next = { ...revisions };
|
|
216
222
|
for (const { path, revision } of files) {
|
|
217
223
|
const entry = tree.get(path);
|
|
218
|
-
if (entry
|
|
224
|
+
if (entry === undefined || revision === null) continue;
|
|
225
|
+
const seen = next[path]?.[entry.object] ?? [];
|
|
226
|
+
if (acknowledged || seen.at(-1)?.revision !== revision) {
|
|
227
|
+
next[path] = { ...next[path], [entry.object]: [...seen, { commit, revision }] };
|
|
228
|
+
}
|
|
219
229
|
}
|
|
220
230
|
return next;
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
async function revisionAt(copy, path, entry, base, signal) {
|
|
234
|
+
const seen = entry === undefined ? [] : (copy.revisions[path]?.[entry.object] ?? []);
|
|
235
|
+
for (const known of seen.toReversed()) {
|
|
236
|
+
if (seen.length === 1 || (await contains(copy.repository, known.commit, base, signal))) return known.revision;
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
223
241
|
function readOnlyContents(read) {
|
|
224
242
|
const contents = new Map();
|
|
225
243
|
for (const file of read.files) {
|
|
@@ -244,19 +262,22 @@ async function trackedPaths(repository, signal) {
|
|
|
244
262
|
return new Set(listed.toString("utf8").split("\0").filter((path) => path !== ""));
|
|
245
263
|
}
|
|
246
264
|
|
|
247
|
-
async function replaceReadOnly(repository,
|
|
265
|
+
async function replaceReadOnly(repository, held, written, removed, signal) {
|
|
248
266
|
const tracked = await trackedPaths(repository, signal);
|
|
249
|
-
const
|
|
250
|
-
for (const [path, content] of
|
|
267
|
+
const holding = new Set(held);
|
|
268
|
+
for (const [path, content] of written) {
|
|
251
269
|
if (tracked.has(path)) continue;
|
|
252
270
|
const target = locate(repository, "", path);
|
|
253
271
|
if (!held.has(path) && (await present(target))) continue;
|
|
254
272
|
await writeReplicaFile(target, content, signal);
|
|
273
|
+
holding.add(path);
|
|
255
274
|
}
|
|
256
|
-
for (const path of
|
|
257
|
-
if (
|
|
258
|
-
await removeReplicaFile(locate(repository, "", path));
|
|
275
|
+
for (const path of removed) {
|
|
276
|
+
if (!held.has(path)) continue;
|
|
277
|
+
if (!tracked.has(path)) await removeReplicaFile(locate(repository, "", path));
|
|
278
|
+
holding.delete(path);
|
|
259
279
|
}
|
|
280
|
+
return [...holding];
|
|
260
281
|
}
|
|
261
282
|
|
|
262
283
|
function covers(path, prefix) {
|
|
@@ -304,17 +325,16 @@ async function writeIgnoreRules(repository, read, signal) {
|
|
|
304
325
|
...[...house, ...granted].map((pattern) => `!${pattern}`),
|
|
305
326
|
"",
|
|
306
327
|
];
|
|
307
|
-
await
|
|
308
|
-
await
|
|
328
|
+
await ruled(join(repository, ".gitignore"), gitignore.join("\n"), signal);
|
|
329
|
+
await ruled(join(repository, ".ignore"), ignore.join("\n"), signal);
|
|
309
330
|
}
|
|
310
331
|
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
};
|
|
332
|
+
async function ruled(path, rules, signal) {
|
|
333
|
+
if ((await readFile(path, "utf8").catch(() => null)) !== rules) await writeText(path, rules, signal, 0o644);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function manifest(read, commit, readOnly) {
|
|
337
|
+
return { commit, position: read.position, log_epoch: read.logEpoch, read_only: readOnly, declared: read.declared };
|
|
318
338
|
}
|
|
319
339
|
|
|
320
340
|
function copyPath(directory, copy) {
|
|
@@ -376,6 +396,7 @@ async function materialize(context, scope, signal) {
|
|
|
376
396
|
const imported = received(scope, read, []);
|
|
377
397
|
let commit;
|
|
378
398
|
let revisions;
|
|
399
|
+
let readOnly;
|
|
379
400
|
try {
|
|
380
401
|
await runGit(staging, ["init", "-q", "-b", WORKING_COPY_BRANCH], { signal, failure: GIT_FAILED });
|
|
381
402
|
commit = (await importReceived(staging, imported, signal)).received;
|
|
@@ -385,7 +406,7 @@ async function materialize(context, scope, signal) {
|
|
|
385
406
|
failure: GIT_FAILED,
|
|
386
407
|
});
|
|
387
408
|
await runGit(staging, ["read-tree", "-m", "-u", "HEAD"], { signal, failure: GIT_FAILED });
|
|
388
|
-
await replaceReadOnly(staging,
|
|
409
|
+
readOnly = await replaceReadOnly(staging, new Set(), readOnlyContents(read), [], signal);
|
|
389
410
|
await writeIgnoreRules(staging, read, signal);
|
|
390
411
|
await rename(staging, repository);
|
|
391
412
|
} catch (error) {
|
|
@@ -399,7 +420,7 @@ async function materialize(context, scope, signal) {
|
|
|
399
420
|
room_handle: scope.handle,
|
|
400
421
|
repository,
|
|
401
422
|
active: true,
|
|
402
|
-
received: manifest(read, commit),
|
|
423
|
+
received: manifest(read, commit, readOnly),
|
|
403
424
|
revisions,
|
|
404
425
|
});
|
|
405
426
|
}
|
|
@@ -409,11 +430,14 @@ async function receive(context, copy, scope, signal) {
|
|
|
409
430
|
if (read.anchor !== copy.room_ref) throw rejected("bootstrap");
|
|
410
431
|
const imported = received(scope, read, [copy.received.commit]);
|
|
411
432
|
const commit = (await importReceived(copy.repository, imported, signal)).received;
|
|
412
|
-
|
|
433
|
+
const next = readOnlyContents(read);
|
|
434
|
+
const held = copy.received.read_only;
|
|
435
|
+
const removed = held.filter((path) => !next.has(path));
|
|
436
|
+
const readOnly = await replaceReadOnly(copy.repository, new Set(held), next, removed, signal);
|
|
413
437
|
await writeIgnoreRules(copy.repository, read, signal);
|
|
414
438
|
return {
|
|
415
439
|
...copy,
|
|
416
|
-
received: manifest(read, commit),
|
|
440
|
+
received: manifest(read, commit, readOnly),
|
|
417
441
|
revisions: await remembered(copy.repository, copy.revisions, commit, imported.files, signal),
|
|
418
442
|
};
|
|
419
443
|
}
|
|
@@ -432,6 +456,117 @@ function scopeRefused(error) {
|
|
|
432
456
|
return error instanceof KitRefusal && (houseAnswered(error) || error.code.startsWith("kit_working_copy_"));
|
|
433
457
|
}
|
|
434
458
|
|
|
459
|
+
function declaredWritable(path, authority) {
|
|
460
|
+
const deepest = (scopes) =>
|
|
461
|
+
Math.max(-1, ...scopes.filter((scope) => scope === "" || covers(path, scope)).map((scope) => scope.length));
|
|
462
|
+
return deepest(authority.operations.write) > deepest(authority.protected_paths);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function incorporated(copy, view, authority, signal) {
|
|
466
|
+
const tree = await treeOf(copy.repository, copy.received.commit, signal);
|
|
467
|
+
const held = new Set(copy.received.read_only);
|
|
468
|
+
const files = [];
|
|
469
|
+
const written = new Map();
|
|
470
|
+
const removed = [];
|
|
471
|
+
for (const change of view.changes) {
|
|
472
|
+
const gone = change.removed === true;
|
|
473
|
+
if (tree.has(change.path) || (!gone && !held.has(change.path) && declaredWritable(change.path, authority))) {
|
|
474
|
+
files.push({ path: change.path, revision: gone ? null : change.revision, content: gone ? null : change.content });
|
|
475
|
+
} else if (gone) {
|
|
476
|
+
removed.push(change.path, sidecarPath(change.path));
|
|
477
|
+
} else {
|
|
478
|
+
written.set(change.path, change.content);
|
|
479
|
+
if (typeof change.provenance === "string") written.set(sidecarPath(change.path), change.provenance);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const message = `House ${housePath(scopeOf(copy))} at position ${view.position}`;
|
|
483
|
+
const commit =
|
|
484
|
+
files.length === 0
|
|
485
|
+
? copy.received.commit
|
|
486
|
+
: (await importReceived(copy.repository, { message, parents: [copy.received.commit], files }, signal)).received;
|
|
487
|
+
const readOnly = await replaceReadOnly(copy.repository, held, written, removed, signal);
|
|
488
|
+
if (view.changes.length > 0) {
|
|
489
|
+
const listed = [
|
|
490
|
+
...[...(await treeOf(copy.repository, commit, signal)).keys()].map((path) => ({ path, writable: true })),
|
|
491
|
+
...readOnly.filter((path) => !isGeneratedProvenance(path)).map((path) => ({ path, writable: false })),
|
|
492
|
+
].sort((left, right) => (left.path < right.path ? -1 : 1));
|
|
493
|
+
await writeIgnoreRules(copy.repository, { files: listed, protectedPaths: authority.protected_paths }, signal);
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
...copy,
|
|
497
|
+
received: { ...copy.received, commit, position: view.position, log_epoch: view.log_epoch, read_only: readOnly },
|
|
498
|
+
revisions: await remembered(copy.repository, copy.revisions, commit, files, signal),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function deliver(context, path, authority, signal) {
|
|
503
|
+
if (
|
|
504
|
+
typeof authority.position !== "string" ||
|
|
505
|
+
typeof authority.log_epoch !== "string" ||
|
|
506
|
+
!Array.isArray(authority.protected_paths) ||
|
|
507
|
+
!Array.isArray(authority.operations?.write)
|
|
508
|
+
) {
|
|
509
|
+
throw rejected("discover");
|
|
510
|
+
}
|
|
511
|
+
let copy = await readCopy(path);
|
|
512
|
+
if (copy?.active && JSON.stringify(copy.received.declared) !== JSON.stringify(declaration(authority))) {
|
|
513
|
+
await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
while (
|
|
517
|
+
copy?.active &&
|
|
518
|
+
(copy.received.log_epoch !== authority.log_epoch || BigInt(copy.received.position) < BigInt(authority.position))
|
|
519
|
+
) {
|
|
520
|
+
const since = { position: copy.received.position, log_epoch: copy.received.log_epoch };
|
|
521
|
+
let view;
|
|
522
|
+
try {
|
|
523
|
+
view = await enumerated(context, copy, since, signal);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (error.code === "replica_behind") return;
|
|
526
|
+
if (error.code !== "position_expired") throw error;
|
|
527
|
+
await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (view.position === since.position) return;
|
|
531
|
+
copy = await incorporated(copy, view, authority, signal);
|
|
532
|
+
await writeCopy(context.directory, copy, signal);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function delivering(context, signal) {
|
|
537
|
+
const copies = (await heldCopies(context.directory)).filter((copy) => copy.active);
|
|
538
|
+
if (copies.length === 0) return { refused: [], lost: false };
|
|
539
|
+
const prefixes = copies.flatMap((copy) => selectedPrefixes(scopeOf(copy)));
|
|
540
|
+
const discovery = discoveryOf(await door(context.session, KIT_DOOR_DISCOVER_PATH, { prefixes }, signal));
|
|
541
|
+
const refused = [];
|
|
542
|
+
let lost = false;
|
|
543
|
+
for (const copy of copies) {
|
|
544
|
+
const at = housePath(scopeOf(copy));
|
|
545
|
+
const authority = discovery.authorities.find((entry) => entry?.room_ref === copy.room_ref);
|
|
546
|
+
if (authority === undefined) {
|
|
547
|
+
lost = true;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (authority.managed_replication !== "admitted") {
|
|
551
|
+
refused.push({ path: at, code: "operation_denied" });
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const path = copyPath(context.directory, copy);
|
|
555
|
+
try {
|
|
556
|
+
await exclusively(path, () => deliver(context, path, authority, signal));
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (!scopeRefused(error)) throw error;
|
|
559
|
+
refused.push({ path: at, code: error.code });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return { refused, lost };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export async function deliverWorkingCopies(context, signal) {
|
|
566
|
+
const delivered = await delivering(context, signal);
|
|
567
|
+
return delivered.lost ? applyWorkingCopies(context, signal) : { refused: delivered.refused };
|
|
568
|
+
}
|
|
569
|
+
|
|
435
570
|
export async function applyWorkingCopies(context, signal) {
|
|
436
571
|
const selected = (await savedSelection(context.session, signal)).filter(
|
|
437
572
|
(scope) => scope.kind === "private" || scope.handle !== null,
|
|
@@ -461,7 +596,7 @@ export async function applyWorkingCopies(context, signal) {
|
|
|
461
596
|
refused.push({ path: housePath(scope), code: error.code });
|
|
462
597
|
}
|
|
463
598
|
}
|
|
464
|
-
return { refused };
|
|
599
|
+
return { refused: [...refused, ...(await delivering(context, signal)).refused] };
|
|
465
600
|
}
|
|
466
601
|
|
|
467
602
|
export async function workingCopyPaths(directory) {
|
|
@@ -694,8 +829,8 @@ function renamed(chains, reached, before, after) {
|
|
|
694
829
|
return new Map(pairs.filter(([from, to]) => pairs.filter((pair) => pair[0] === from || pair[1] === to).length === 1));
|
|
695
830
|
}
|
|
696
831
|
|
|
697
|
-
async function relocated(copy, root, [from, to], prior, next, signal) {
|
|
698
|
-
const base = copy
|
|
832
|
+
async function relocated(copy, root, [from, to], [prior, next], house, signal) {
|
|
833
|
+
const base = await revisionAt(copy, from, prior, house, signal);
|
|
699
834
|
const rename = { op: "rename", path: `${root}/${from}`, to: `${root}/${to}`, base };
|
|
700
835
|
if (prior.object === next.object) return [rename];
|
|
701
836
|
return [rename, { op: "replace", path: `${root}/${to}`, base, content: await blobText(copy.repository, next, to, signal) }];
|
|
@@ -713,13 +848,13 @@ async function derived(copy, base, commit, signal) {
|
|
|
713
848
|
const prior = before.get(path);
|
|
714
849
|
if (moved.has(path)) {
|
|
715
850
|
const to = moved.get(path);
|
|
716
|
-
changes.push(...(await relocated(copy, root, [path, to], prior, after.get(to), signal)));
|
|
851
|
+
changes.push(...(await relocated(copy, root, [path, to], [prior, after.get(to)], base, signal)));
|
|
717
852
|
continue;
|
|
718
853
|
}
|
|
719
854
|
const next = after.get(path);
|
|
720
855
|
if (prior?.object === next?.object) continue;
|
|
721
856
|
const target = `${root}/${path}`;
|
|
722
|
-
const revision = copy
|
|
857
|
+
const revision = await revisionAt(copy, path, prior, base, signal);
|
|
723
858
|
if (next === undefined) {
|
|
724
859
|
changes.push({ op: "remove", path: target, base: revision });
|
|
725
860
|
continue;
|
|
@@ -761,8 +896,15 @@ async function recorded(copy, files, message, acknowledging, signal) {
|
|
|
761
896
|
return {
|
|
762
897
|
copy: {
|
|
763
898
|
...copy,
|
|
764
|
-
received: { ...copy.received, commit: imported.received },
|
|
765
|
-
revisions: await remembered(
|
|
899
|
+
received: { ...copy.received, commit: imported.received, ...(acknowledging === null ? {} : copy.push.from) },
|
|
900
|
+
revisions: await remembered(
|
|
901
|
+
copy.repository,
|
|
902
|
+
copy.revisions,
|
|
903
|
+
imported.acknowledged ?? imported.received,
|
|
904
|
+
files,
|
|
905
|
+
signal,
|
|
906
|
+
imported.acknowledged !== null,
|
|
907
|
+
),
|
|
766
908
|
},
|
|
767
909
|
acknowledged: imported.acknowledged,
|
|
768
910
|
};
|
|
@@ -952,11 +1094,12 @@ async function expired(context, copy, text, signal) {
|
|
|
952
1094
|
const entry = house.get(path);
|
|
953
1095
|
written.push({
|
|
954
1096
|
path: `${root}/${path}`,
|
|
955
|
-
revision: entry === undefined ? null : confirmed
|
|
1097
|
+
revision: entry === undefined ? null : await revisionAt(confirmed, path, entry, confirmed.received.commit, signal),
|
|
956
1098
|
content: await blobText(copy.repository, entry, path, signal),
|
|
957
1099
|
});
|
|
958
1100
|
}
|
|
959
|
-
const
|
|
1101
|
+
const from = { position: confirmed.received.position, log_epoch: confirmed.received.log_epoch };
|
|
1102
|
+
const accepted = { ...confirmed, push: { ...push, state: "accepted", written, from } };
|
|
960
1103
|
await writeCopy(context.directory, accepted, signal);
|
|
961
1104
|
return finish(context, accepted, signal);
|
|
962
1105
|
}
|
|
@@ -1030,6 +1173,7 @@ async function pushed(context, path, at, selector, caller, signal) {
|
|
|
1030
1173
|
operation_id: operationId(),
|
|
1031
1174
|
changes,
|
|
1032
1175
|
commit,
|
|
1176
|
+
from: { position: copy.received.position, log_epoch: copy.received.log_epoch },
|
|
1033
1177
|
worktree: at.worktree,
|
|
1034
1178
|
branch,
|
|
1035
1179
|
...(since === null ? {} : { since }),
|