@agentshouse/kit 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +270 -26
- package/bin/connect-linux.sh +85 -13
- package/bin/kit.js +23 -3
- package/bootstrap.json +17 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +0 -13
- package/src/agents.js +63 -8
- package/src/binding.js +41 -0
- package/src/bridge.js +1 -1
- package/src/contract.js +21 -3
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +345 -73
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +88 -0
- package/src/launcher.js +9 -3
- package/src/local-files.js +3 -11
- package/src/login.js +2 -1
- package/src/mcp.js +13 -0
- package/src/mount.js +13 -21
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +7 -2
- package/src/source.js +44 -30
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +4 -0
- package/src/sync.js +15 -10
- package/src/telegram.js +29 -11
- package/src/worker.js +519 -116
- package/src/working-copy.js +1066 -0
package/src/launcher.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { commandHouseInstructions
|
|
2
|
+
import { commandHouseInstructions } from "./adapter-mcp.js";
|
|
3
3
|
import {
|
|
4
4
|
compiledContract,
|
|
5
5
|
initializeCommandAdapter,
|
|
@@ -129,6 +129,12 @@ async function configureSession(acp, contract, sessionId, adapter) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function workingCopyContext(copies) {
|
|
133
|
+
if (!Array.isArray(copies) || copies.length === 0) return [];
|
|
134
|
+
const lines = copies.map((copy) => `${copy.path}: ${copy.repository}`);
|
|
135
|
+
return [{ type: "text", text: ["Local Git working copies of House paths:", ...lines].join("\n") }];
|
|
136
|
+
}
|
|
137
|
+
|
|
132
138
|
async function runTurn(channel, start, bridge, turn) {
|
|
133
139
|
const contract = compiledContract(start.adapter?.kind);
|
|
134
140
|
const adapter = startCommandAdapter(
|
|
@@ -185,14 +191,14 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
185
191
|
loading = false;
|
|
186
192
|
}
|
|
187
193
|
await configureSession(acp, contract, providerSessionId, start.adapter);
|
|
188
|
-
const request = start.request === null ? await commandHouseRequest(bridge) : start.request;
|
|
189
194
|
turn.adapterSession = true;
|
|
190
195
|
turn.cancel = () => acp.cancel({ sessionId: providerSessionId });
|
|
191
196
|
const prompted = await acp.request("session/prompt", {
|
|
192
197
|
sessionId: providerSessionId,
|
|
193
198
|
prompt: [
|
|
194
|
-
{ type: "text", text: JSON.stringify(request) },
|
|
199
|
+
{ type: "text", text: JSON.stringify(start.request) },
|
|
195
200
|
{ type: "text", text: instructions },
|
|
201
|
+
...workingCopyContext(start.working_copies),
|
|
196
202
|
],
|
|
197
203
|
});
|
|
198
204
|
await new Promise((resolve) => setImmediate(resolve));
|
package/src/local-files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { stringify } from "yaml";
|
|
3
3
|
import { AGENT_OPERATION_META, ATTACHMENT_FILE_META } from "./contract.js";
|
|
4
|
+
import { answerText, answerYaml } from "./mcp.js";
|
|
4
5
|
import { derivedOperationId } from "./operation.js";
|
|
5
6
|
import { localAttachment, sendLocalConversationFile, uploadLocalAttachment } from "./original.js";
|
|
6
7
|
import { KitRefusal } from "./refusal.js";
|
|
@@ -12,18 +13,9 @@ class HouseRefused extends Error {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function parsed(text) {
|
|
16
|
-
try {
|
|
17
|
-
return parse(text);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
function answerOf(frame) {
|
|
24
17
|
if (frame.error !== undefined || frame.result?.isError === true) throw new HouseRefused(frame);
|
|
25
|
-
const
|
|
26
|
-
const answer = parsed(text);
|
|
18
|
+
const answer = answerYaml(answerText(frame.result));
|
|
27
19
|
if (answer === null || typeof answer !== "object") throw new KitRefusal("kit_house_answer_unreadable");
|
|
28
20
|
return answer;
|
|
29
21
|
}
|
package/src/login.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
8
8
|
import { stdin, stdout } from "node:process";
|
|
9
9
|
import { KIT_CREDENTIAL } from "./contract.js";
|
|
10
10
|
import { launchBase, prepareLaunchBase } from "./directory.js";
|
|
11
|
-
import { houseRequest } from "./house.js";
|
|
11
|
+
import { houseRequest, refuseDrained } from "./house.js";
|
|
12
12
|
import { KitRefusal } from "./refusal.js";
|
|
13
13
|
import { writeText } from "./state.js";
|
|
14
14
|
|
|
@@ -260,6 +260,7 @@ export async function openBrowser(url) {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
export async function connectHouse(options, dependencies = {}) {
|
|
263
|
+
refuseDrained(options.directory);
|
|
263
264
|
await prepareLaunchBase(launchBase(dependencies));
|
|
264
265
|
if (options.presentation === "manual") return login(options, dependencies);
|
|
265
266
|
const callback = await loopback();
|
package/src/mcp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { parse } from "yaml";
|
|
2
3
|
import { bridgePeer } from "./peer.js";
|
|
3
4
|
import { KitRefusal } from "./refusal.js";
|
|
4
5
|
|
|
@@ -69,3 +70,15 @@ export async function bridgeCall(bridge, request, signal = undefined) {
|
|
|
69
70
|
await peer.end();
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
|
|
74
|
+
export function answerText(result) {
|
|
75
|
+
return (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function answerYaml(text) {
|
|
79
|
+
try {
|
|
80
|
+
return parse(text);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/mount.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { apply, links, parse, serialize } from "@agentshouse/mdmodel";
|
|
4
|
+
import { apply, links, parse, retarget, serialize } from "@agentshouse/mdmodel";
|
|
5
5
|
import {
|
|
6
6
|
KIT_DOOR_BATCH_PATH,
|
|
7
7
|
KIT_DOOR_BOOTSTRAP_PATH,
|
|
@@ -84,7 +84,6 @@ export function mountTarget(subtree) {
|
|
|
84
84
|
refuse("kit_mount_subtree_invalid", subtree);
|
|
85
85
|
}
|
|
86
86
|
return {
|
|
87
|
-
kind: own ? "private" : "room",
|
|
88
87
|
handle: own ? null : segments[1],
|
|
89
88
|
root: own ? "/private" : `/rooms/${segments[1]}`,
|
|
90
89
|
subtree,
|
|
@@ -107,10 +106,11 @@ function discoveryOf(value) {
|
|
|
107
106
|
|
|
108
107
|
function authorityFor(discovery, target) {
|
|
109
108
|
const found = discovery.authorities.find((entry) =>
|
|
110
|
-
|
|
109
|
+
entry.kind === "room" && entry.room_handle === target.handle,
|
|
111
110
|
);
|
|
112
111
|
if (found === undefined) refuse("kit_mount_authority_unknown", target.root);
|
|
113
112
|
if (
|
|
113
|
+
typeof found.room_ref !== "string" ||
|
|
114
114
|
!Array.isArray(found.protected_paths) ||
|
|
115
115
|
found.operations === null ||
|
|
116
116
|
typeof found.operations !== "object" ||
|
|
@@ -119,8 +119,8 @@ function authorityFor(discovery, target) {
|
|
|
119
119
|
refuse("kit_mount_rejected", "discover");
|
|
120
120
|
}
|
|
121
121
|
return {
|
|
122
|
-
kind:
|
|
123
|
-
authority: found.
|
|
122
|
+
kind: "room",
|
|
123
|
+
authority: found.room_ref,
|
|
124
124
|
path: target.root,
|
|
125
125
|
protected_paths: found.protected_paths,
|
|
126
126
|
operations: found.operations,
|
|
@@ -128,16 +128,11 @@ function authorityFor(discovery, target) {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
function hrefSuffix(href) {
|
|
132
|
-
const match = /[?#]/.exec(href);
|
|
133
|
-
return match === null ? "" : href.slice(match.index);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
131
|
function replaceLink(document, href, destination) {
|
|
137
132
|
const marker = `house-kit-link:${operationId()}`;
|
|
138
133
|
const located = apply(document, { kind: "link", href, to: marker });
|
|
139
134
|
if (located.outcome !== "applied") refuse("kit_mount_link_unwritable", href);
|
|
140
|
-
const cleared = parse(serialize(located.document).replaceAll(
|
|
135
|
+
const cleared = parse(serialize(located.document).replaceAll(retarget(href, marker), marker));
|
|
141
136
|
if (!cleared.ok) refuse("kit_mount_link_unwritable", href);
|
|
142
137
|
const changed = apply(cleared.document, { kind: "link", href: marker, to: destination });
|
|
143
138
|
if (changed.outcome !== "applied") refuse("kit_mount_link_unwritable", href);
|
|
@@ -178,11 +173,8 @@ export function rewriteAttachmentLinks(held, mintVersion) {
|
|
|
178
173
|
refuse("kit_migrator_referrer_undeclared", `${relative} -> ${path}`);
|
|
179
174
|
}
|
|
180
175
|
const version = mintVersion(path);
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
href: link.href,
|
|
184
|
-
to: `${PENDING_LINK_SCHEME}:${version}`,
|
|
185
|
-
});
|
|
176
|
+
const pending = `${PENDING_LINK_SCHEME}:${version}`;
|
|
177
|
+
const applied = apply(document, { kind: "link", href: link.href, to: pending });
|
|
186
178
|
if (applied.outcome !== "applied") refuse("kit_mount_link_unwritable", link.href);
|
|
187
179
|
document = applied.document;
|
|
188
180
|
let save = saves.find((entry) => entry.path === path);
|
|
@@ -191,8 +183,8 @@ export function rewriteAttachmentLinks(held, mintVersion) {
|
|
|
191
183
|
saves.push(save);
|
|
192
184
|
}
|
|
193
185
|
if (!save.documents.includes(relative)) save.documents.push(relative);
|
|
194
|
-
const suffix =
|
|
195
|
-
const href =
|
|
186
|
+
const suffix = retarget(link.href, "");
|
|
187
|
+
const href = retarget(link.href, pending);
|
|
196
188
|
if (!save.hrefs.some((entry) => entry.document === relative && entry.href === href)) {
|
|
197
189
|
save.hrefs.push({ document: relative, href, suffix });
|
|
198
190
|
}
|
|
@@ -326,7 +318,7 @@ function pendingTransfer(held, save) {
|
|
|
326
318
|
};
|
|
327
319
|
}
|
|
328
320
|
|
|
329
|
-
async function submitAttachments(session, held,
|
|
321
|
+
async function submitAttachments(session, held, authority, plan, signal) {
|
|
330
322
|
const transfers = [];
|
|
331
323
|
for (const save of plan.saves) {
|
|
332
324
|
const local = held.attachments.get(save.path);
|
|
@@ -341,7 +333,7 @@ async function submitAttachments(session, held, target, authority, plan, signal)
|
|
|
341
333
|
version: save.version,
|
|
342
334
|
name: declared.name,
|
|
343
335
|
mediaType: declared.mediaType ?? undefined,
|
|
344
|
-
room:
|
|
336
|
+
room: authority.authority,
|
|
345
337
|
body,
|
|
346
338
|
signal,
|
|
347
339
|
});
|
|
@@ -471,7 +463,7 @@ export async function mountPackage(held, options, dependencies = {}) {
|
|
|
471
463
|
};
|
|
472
464
|
const localReport = join(held.root, "MIGRATION.json");
|
|
473
465
|
await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
|
|
474
|
-
result.transfers = await submitAttachments(session, held,
|
|
466
|
+
result.transfers = await submitAttachments(session, held, authority, plan, signal);
|
|
475
467
|
|
|
476
468
|
const report = `${target.subtree}/${MOUNT_REPORT_NAME}`;
|
|
477
469
|
await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
|
package/src/repository.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { constants } from "node:fs";
|
|
4
3
|
import { open, readlink, realpath } from "node:fs/promises";
|
|
5
4
|
import { join } from "node:path";
|
|
5
|
+
import { runGit } from "./git.js";
|
|
6
6
|
import { deliverPublicationChunk, maintainControl, openPublicationManifest, openSession } from "./house.js";
|
|
7
7
|
import { KitRefusal } from "./refusal.js";
|
|
8
8
|
import { readRepositoryState, writeRepositoryState } from "./state.js";
|
|
@@ -258,69 +258,11 @@ async function* chunkArchive(checkout, entries, signal) {
|
|
|
258
258
|
yield Buffer.alloc(BLOCK_BYTES * 2);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
function runGit(checkout, args, dependencies, capture, signal) {
|
|
262
|
-
active(signal);
|
|
263
|
-
const launcher = dependencies.spawn ?? spawn;
|
|
264
|
-
return new Promise((resolve, reject) => {
|
|
265
|
-
let child;
|
|
266
|
-
try {
|
|
267
|
-
child = launcher("git", args, {
|
|
268
|
-
cwd: checkout,
|
|
269
|
-
env: process.env,
|
|
270
|
-
stdio: capture ? ["ignore", "pipe", "inherit"] : "inherit",
|
|
271
|
-
signal,
|
|
272
|
-
killSignal: "SIGKILL",
|
|
273
|
-
});
|
|
274
|
-
} catch (error) {
|
|
275
|
-
reject(
|
|
276
|
-
signal?.aborted === true || error?.name === "AbortError"
|
|
277
|
-
? new KitRefusal("kit_command_cancelled")
|
|
278
|
-
: new KitRefusal("kit_git_unavailable"),
|
|
279
|
-
);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
const output = [];
|
|
283
|
-
let settled = false;
|
|
284
|
-
const finish = (operation) => {
|
|
285
|
-
if (settled) return;
|
|
286
|
-
settled = true;
|
|
287
|
-
operation();
|
|
288
|
-
};
|
|
289
|
-
child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
290
|
-
child.once("error", (error) =>
|
|
291
|
-
finish(() =>
|
|
292
|
-
reject(
|
|
293
|
-
signal?.aborted === true || error?.name === "AbortError"
|
|
294
|
-
? new KitRefusal("kit_command_cancelled")
|
|
295
|
-
: new KitRefusal("kit_git_unavailable"),
|
|
296
|
-
),
|
|
297
|
-
),
|
|
298
|
-
);
|
|
299
|
-
child.once("close", (status) => {
|
|
300
|
-
finish(() => {
|
|
301
|
-
if (signal?.aborted === true) {
|
|
302
|
-
reject(new KitRefusal("kit_command_cancelled"));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
if (status !== 0) {
|
|
306
|
-
reject(
|
|
307
|
-
new KitRefusal(args[0] === "pull" ? "kit_git_pull_failed" : "kit_git_selection_failed"),
|
|
308
|
-
);
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
resolve(Buffer.concat(output));
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
261
|
async function selectedPaths(checkout, dependencies, signal) {
|
|
318
262
|
const output = await runGit(
|
|
319
263
|
checkout,
|
|
320
264
|
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
321
|
-
dependencies,
|
|
322
|
-
true,
|
|
323
|
-
signal,
|
|
265
|
+
{ dependencies, capture: true, signal, failure: "kit_git_selection_failed" },
|
|
324
266
|
);
|
|
325
267
|
const paths = [];
|
|
326
268
|
let offset = 0;
|
|
@@ -469,7 +411,7 @@ export async function captureRepository(options, dependencies = {}) {
|
|
|
469
411
|
session,
|
|
470
412
|
async (signal) => {
|
|
471
413
|
const state = stateOf(await readRepositoryState(options.directory, key), identity);
|
|
472
|
-
await runGit(identity.checkout, ["pull"], dependencies,
|
|
414
|
+
await runGit(identity.checkout, ["pull"], { dependencies, signal, failure: "kit_git_pull_failed" });
|
|
473
415
|
const now = dependencies.now ?? (() => new Date());
|
|
474
416
|
let sourceRef = state.source_ref;
|
|
475
417
|
let publication = null;
|
package/src/setup.js
CHANGED
|
@@ -114,14 +114,14 @@ async function signedIn(module) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
async function setUpModule(context, module) {
|
|
117
|
+
async function setUpModule(context, module, install = true) {
|
|
118
118
|
const summary = (status, reason = null) => ({
|
|
119
119
|
category: module.category,
|
|
120
120
|
name: module.name,
|
|
121
121
|
status,
|
|
122
122
|
reason,
|
|
123
123
|
});
|
|
124
|
-
if (module.install !== null) {
|
|
124
|
+
if (install && module.install !== null) {
|
|
125
125
|
try {
|
|
126
126
|
await module.install();
|
|
127
127
|
} catch (error) {
|
|
@@ -144,10 +144,15 @@ async function setUpModule(context, module) {
|
|
|
144
144
|
return summary("ready");
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export function signInWantedAgent(context, kind) {
|
|
148
|
+
return setUpModule(context, agentModule(context, kind), false);
|
|
149
|
+
}
|
|
150
|
+
|
|
147
151
|
export async function runSetup(context, request) {
|
|
148
152
|
await prepareLaunchBase(launchBase(context.dependencies));
|
|
149
153
|
const modules = [];
|
|
150
154
|
for (const kind of context.configuration.agents) {
|
|
155
|
+
await context.waitAgentLogin?.(kind);
|
|
151
156
|
modules.push(await setUpModule(context, agentModule(context, kind)));
|
|
152
157
|
}
|
|
153
158
|
for (const instance of context.configuration.instances) {
|
package/src/source.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
newCheckpoint,
|
|
13
13
|
stallOf,
|
|
14
14
|
} from "./delivery.js";
|
|
15
|
-
import { captureBatch, openSession } from "./house.js";
|
|
15
|
+
import { captureBatch, maintainControl, openSession } from "./house.js";
|
|
16
16
|
import { googlePage } from "./google/source.js";
|
|
17
17
|
import { houseAnswered, houseAnsweredFailure, houseFailed, KitRefusal } from "./refusal.js";
|
|
18
18
|
import { recordAcceptedOriginals, sameReference, saveSourceOriginals } from "./source-originals.js";
|
|
@@ -204,18 +204,24 @@ export async function captureSource(options, dependencies = {}) {
|
|
|
204
204
|
const googleOAuthClient =
|
|
205
205
|
instance.preset === "google" ? dependencies.googleOAuthClient() : null;
|
|
206
206
|
const session = await openSession(options, dependencies);
|
|
207
|
-
|
|
208
|
-
instance,
|
|
207
|
+
return maintainControl(
|
|
209
208
|
session,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
async (signal) => {
|
|
210
|
+
const context = {
|
|
211
|
+
instance,
|
|
212
|
+
session,
|
|
213
|
+
directory: options.directory,
|
|
214
|
+
signal,
|
|
215
|
+
now: session.now,
|
|
216
|
+
dependencies,
|
|
217
|
+
googleOAuthClient,
|
|
218
|
+
};
|
|
219
|
+
const delivered = [];
|
|
220
|
+
for (const service of services) delivered.push(await deliverService(context, service));
|
|
221
|
+
return { instance: instance.id, services: delivered };
|
|
222
|
+
},
|
|
223
|
+
options.signal,
|
|
224
|
+
);
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
export async function sourceStatus(options) {
|
|
@@ -255,32 +261,40 @@ export async function retrySourceOriginals(options, dependencies = {}) {
|
|
|
255
261
|
const configuration = await readConfiguration(options.configuration);
|
|
256
262
|
const instance = sourceInstance(configuration, options.instance);
|
|
257
263
|
const services = declaredServices(instance, options.service ?? null);
|
|
258
|
-
const context = { instance, directory: options.directory, signal: options.signal };
|
|
259
264
|
const outstanding = [];
|
|
260
265
|
for (const service of services) {
|
|
261
266
|
outstanding.push((await readSourceOriginals(options.directory, instance.id, service.service)).length);
|
|
262
267
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
268
|
+
const retry = async (context) => {
|
|
269
|
+
const retried = [];
|
|
270
|
+
for (const [position, service] of services.entries()) {
|
|
271
|
+
retried.push({
|
|
272
|
+
service: service.service,
|
|
273
|
+
originals:
|
|
274
|
+
outstanding[position] === 0
|
|
275
|
+
? []
|
|
276
|
+
: await saveSourceOriginals(context, serviceBinding(context, service)),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return { instance: instance.id, services: retried };
|
|
280
|
+
};
|
|
281
|
+
const context = { instance, directory: options.directory, signal: options.signal };
|
|
282
|
+
if (outstanding.every((count) => count === 0)) return retry(context);
|
|
283
|
+
const session = await openSession(options, dependencies);
|
|
284
|
+
return maintainControl(session, (signal) => retry({ ...context, session, signal }), options.signal);
|
|
275
285
|
}
|
|
276
286
|
|
|
277
287
|
export async function resetSource(options, dependencies = {}) {
|
|
278
288
|
const configuration = await readConfiguration(options.configuration);
|
|
279
289
|
const instance = sourceInstance(configuration, options.instance);
|
|
280
290
|
declaredServices(instance, options.service);
|
|
281
|
-
await openSession(options, dependencies);
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
const session = await openSession(options, dependencies);
|
|
292
|
+
return maintainControl(
|
|
293
|
+
session,
|
|
294
|
+
async () => ({
|
|
295
|
+
instance: instance.id,
|
|
296
|
+
services: await resetCheckpoints(options.directory, instance.id, options.service),
|
|
297
|
+
}),
|
|
298
|
+
options.signal,
|
|
299
|
+
);
|
|
286
300
|
}
|
package/src/sync-library.js
CHANGED
|
@@ -100,13 +100,12 @@ function recordedMap(authority) {
|
|
|
100
100
|
return new Map(authority.files.map((file) => [file.path, file]));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function structuredOrReplace(root, relative, recorded, content) {
|
|
103
|
+
export function structuredOrReplace(root, relative, recorded, content) {
|
|
104
104
|
const path = housePath(root, relative);
|
|
105
|
+
const whole = [{ op: "replace", path, base: recorded.revision, content }];
|
|
105
106
|
const before = parse(recorded.content, relative);
|
|
106
107
|
const after = parse(content, relative);
|
|
107
|
-
if (!before.ok || !after.ok)
|
|
108
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
109
|
-
}
|
|
108
|
+
if (!before.ok || !after.ok) return whole;
|
|
110
109
|
const ops = [];
|
|
111
110
|
if (before.document.preamble !== after.document.preamble) {
|
|
112
111
|
ops.push({
|
|
@@ -124,9 +123,7 @@ function structuredOrReplace(root, relative, recorded, content) {
|
|
|
124
123
|
if (JSON.stringify(before.document.fields[name]) === JSON.stringify(after.document.fields[name])) {
|
|
125
124
|
continue;
|
|
126
125
|
}
|
|
127
|
-
if (after.document.fields[name] === undefined)
|
|
128
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
129
|
-
}
|
|
126
|
+
if (after.document.fields[name] === undefined) return whole;
|
|
130
127
|
const change = { op: "set_field", path, field: name, value: after.document.fields[name] };
|
|
131
128
|
if (before.document.fields[name] !== undefined) change.base = before.document.fields[name];
|
|
132
129
|
ops.push(change);
|
|
@@ -139,12 +136,11 @@ function structuredOrReplace(root, relative, recorded, content) {
|
|
|
139
136
|
const prior = before.document.sections[name];
|
|
140
137
|
const next = after.document.sections[name];
|
|
141
138
|
if (prior === next) continue;
|
|
142
|
-
if (prior === undefined || next === undefined)
|
|
143
|
-
return [{ op: "replace", path, base: recorded.revision, content }];
|
|
144
|
-
}
|
|
139
|
+
if (prior === undefined || next === undefined) return whole;
|
|
145
140
|
ops.push({ op: "replace_section", path, section: name, base: prior, content: next });
|
|
146
141
|
}
|
|
147
|
-
|
|
142
|
+
if (ops.length === 0) return whole;
|
|
143
|
+
return applyChanges({ [relative]: recorded.content }, ops, root)[relative] === content ? ops : whole;
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
export function deriveChanges(state, authority, localFiles) {
|
package/src/sync-state.js
CHANGED
|
@@ -26,6 +26,9 @@ function syncFile(directory, key, kind = "") {
|
|
|
26
26
|
function recordedAuthority(authority) {
|
|
27
27
|
const operations = authority?.operations;
|
|
28
28
|
return (
|
|
29
|
+
authority?.kind === "room" &&
|
|
30
|
+
typeof authority.authority === "string" &&
|
|
31
|
+
typeof authority.path === "string" &&
|
|
29
32
|
typeof authority?.log_epoch === "string" &&
|
|
30
33
|
operations !== null &&
|
|
31
34
|
typeof operations === "object" &&
|
|
@@ -43,6 +46,7 @@ export async function readReplicaState(directory, replica) {
|
|
|
43
46
|
}
|
|
44
47
|
const state = await readNote(syncFile(directory, replicaKey(canonical)), null);
|
|
45
48
|
if (state === null) return null;
|
|
49
|
+
if (state.version !== EXPLICIT_SYNC_VERSION) return null;
|
|
46
50
|
if (!Array.isArray(state.authorities) || !state.authorities.every(recordedAuthority)) return null;
|
|
47
51
|
return state;
|
|
48
52
|
}
|
package/src/sync.js
CHANGED
|
@@ -41,12 +41,12 @@ function sessionDependencies(options, dependencies) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function withSession(options, dependencies, operation) {
|
|
44
|
+
export async function withSession(options, dependencies, operation) {
|
|
45
45
|
const session = await openSession(options, sessionDependencies(options, dependencies));
|
|
46
46
|
return maintainControl(session, (signal) => operation(session, signal), options.signal);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function door(session, path, body, signal) {
|
|
49
|
+
export async function door(session, path, body, signal) {
|
|
50
50
|
return houseRequest(
|
|
51
51
|
session.house,
|
|
52
52
|
await heldControl(session, signal),
|
|
@@ -83,7 +83,7 @@ function changePosition(value, declaration) {
|
|
|
83
83
|
return { position: value.position, log_epoch: value.log_epoch };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function discoveryOf(value) {
|
|
86
|
+
export function discoveryOf(value) {
|
|
87
87
|
if (typeof value?.md_model_version !== "string" || !Array.isArray(value.authorities)) {
|
|
88
88
|
throw new KitRefusal("kit_sync_rejected", "discover");
|
|
89
89
|
}
|
|
@@ -93,7 +93,7 @@ function discoveryOf(value) {
|
|
|
93
93
|
return value;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
async function bootstrapBundle(session, prefixes, signal) {
|
|
96
|
+
export async function bootstrapBundle(session, prefixes, signal) {
|
|
97
97
|
const answer = await door(session, KIT_DOOR_BOOTSTRAP_PATH, { prefixes }, signal);
|
|
98
98
|
if (typeof answer?.transfer?.url === "string") {
|
|
99
99
|
const granted = await houseGetJson(answer.transfer.url, session.dependencies, signal);
|
|
@@ -105,9 +105,6 @@ async function bootstrapBundle(session, prefixes, signal) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function matchedDiscovery(discovery, scope) {
|
|
108
|
-
if (scope.manifest?.scope === "private") {
|
|
109
|
-
return discovery.authorities.find((entry) => entry.kind === "private");
|
|
110
|
-
}
|
|
111
108
|
return discovery.authorities.find(
|
|
112
109
|
(entry) => entry.kind === "room" && entry.room_ref === scope.manifest?.anchor,
|
|
113
110
|
);
|
|
@@ -173,11 +170,17 @@ function operationsOf(value) {
|
|
|
173
170
|
function authorityRecord(scope, discovery, files) {
|
|
174
171
|
const found = matchedDiscovery(discovery, scope);
|
|
175
172
|
const operations = found === undefined ? null : operationsOf(found.operations);
|
|
176
|
-
if (
|
|
173
|
+
if (
|
|
174
|
+
scope.manifest?.scope !== "room" ||
|
|
175
|
+
typeof scope.manifest.anchor !== "string" ||
|
|
176
|
+
found === undefined ||
|
|
177
|
+
!stringList(found.protected_paths) ||
|
|
178
|
+
operations === null
|
|
179
|
+
) {
|
|
177
180
|
throw new KitRefusal("kit_bootstrap_rejected");
|
|
178
181
|
}
|
|
179
182
|
return {
|
|
180
|
-
kind:
|
|
183
|
+
kind: "room",
|
|
181
184
|
authority: scope.manifest.anchor,
|
|
182
185
|
room_handle: scope.manifest.room_handle,
|
|
183
186
|
path: scope.manifest.path,
|
|
@@ -192,9 +195,10 @@ function authorityRecord(scope, discovery, files) {
|
|
|
192
195
|
async function materialize(options, discovery, bundle, signal, previous = null) {
|
|
193
196
|
const authorities = [];
|
|
194
197
|
for (const scope of bundle.scopes) {
|
|
198
|
+
const record = authorityRecord(scope, discovery, []);
|
|
195
199
|
const held = previous?.authorities.find((entry) => entry.authority === scope.manifest?.anchor);
|
|
196
200
|
const files = await replaceScope(options.replica, held, scope, signal);
|
|
197
|
-
authorities.push(
|
|
201
|
+
authorities.push({ ...record, files });
|
|
198
202
|
}
|
|
199
203
|
const state = {
|
|
200
204
|
version: EXPLICIT_SYNC_VERSION,
|
|
@@ -409,6 +413,7 @@ export async function bootstrap(options, dependencies = {}) {
|
|
|
409
413
|
const canonical = await canonicalReplica(options.replica);
|
|
410
414
|
const existing = await readReplicaState(options.directory, canonical);
|
|
411
415
|
if (existing !== null) throw new KitRefusal("kit_sync_already_bootstrapped", canonical);
|
|
416
|
+
await clearPending(options.directory, canonical);
|
|
412
417
|
return withSession(options, dependencies, async (session, signal) => {
|
|
413
418
|
const discovery = discoveryOf(
|
|
414
419
|
await door(session, KIT_DOOR_DISCOVER_PATH, { prefixes: options.prefixes }, signal),
|