@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c46722ad48058
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 +20 -19
- package/dist/acp.js +7 -97
- package/dist/agents.js +2 -1
- package/dist/api.js +3 -3
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +11 -37
- package/dist/clis.js +52 -13
- package/dist/codex-main.js +58 -0
- package/dist/conversations.js +47 -50
- package/dist/copies.js +127 -102
- package/dist/end.js +1 -1
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +10 -76
- package/dist/home.js +6 -2
- package/dist/house-main.js +40 -13
- package/dist/instructions.js +6 -4
- package/dist/login.js +75 -106
- package/dist/logout.js +14 -14
- package/dist/relay.js +12 -2
- package/dist/resident.js +21 -19
- package/dist/scope.js +203 -0
- package/dist/sign-in.js +1 -1
- package/dist/work.js +348 -0
- package/package.json +2 -2
package/dist/conversations.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { unlessAborted } from './abort.js';
|
|
6
|
-
import {
|
|
6
|
+
import { startAdapter } from './acp.js';
|
|
7
7
|
import { openBridge } from './bridge.js';
|
|
8
8
|
import { CLIS } from './clis.js';
|
|
9
9
|
import { readerFor } from './events.js';
|
|
@@ -11,12 +11,12 @@ import { placeFiles, uploadBytes } from './files.js';
|
|
|
11
11
|
import { agentBase } from './home.js';
|
|
12
12
|
import { instructions } from './instructions.js';
|
|
13
13
|
import { Parts } from './parts.js';
|
|
14
|
+
import { killMarked, Scope } from './scope.js';
|
|
14
15
|
import { holdSecretInput } from './secret-input.js';
|
|
15
16
|
import { createHowWeWork } from './skills.js';
|
|
16
17
|
import { transcribe } from './transcription.js';
|
|
18
|
+
import { workFor } from './work.js';
|
|
17
19
|
const KILLED = 'the conversation was killed';
|
|
18
|
-
// A process a conversation left running reports nothing when it exits, so the Kit looks again every five seconds while one runs.
|
|
19
|
-
const PROCESS_RECHECK_MS = 5000;
|
|
20
20
|
function causeOf(error) {
|
|
21
21
|
return error instanceof Error ? error.message : String(error);
|
|
22
22
|
}
|
|
@@ -29,9 +29,6 @@ function logged(error) {
|
|
|
29
29
|
function marker(bridge) {
|
|
30
30
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
31
31
|
}
|
|
32
|
-
function stop(bridge) {
|
|
33
|
-
killMarked(marker(bridge));
|
|
34
|
-
}
|
|
35
32
|
class Turn {
|
|
36
33
|
id = randomUUID();
|
|
37
34
|
parts;
|
|
@@ -53,7 +50,7 @@ class Conversation {
|
|
|
53
50
|
turn = null;
|
|
54
51
|
commands = null;
|
|
55
52
|
context = null;
|
|
56
|
-
|
|
53
|
+
leftovers = new Set();
|
|
57
54
|
waiting = [];
|
|
58
55
|
sending = null;
|
|
59
56
|
subagents = new Map();
|
|
@@ -86,9 +83,10 @@ export class Conversations {
|
|
|
86
83
|
carrying = 0;
|
|
87
84
|
reporting = 0;
|
|
88
85
|
watched = false;
|
|
89
|
-
recheck = null;
|
|
90
86
|
constructor(kit) {
|
|
91
87
|
this.kit = kit;
|
|
88
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
89
|
+
setInterval(() => this.look(), 2_000);
|
|
92
90
|
}
|
|
93
91
|
input(input) {
|
|
94
92
|
this.carrying++;
|
|
@@ -126,22 +124,24 @@ export class Conversations {
|
|
|
126
124
|
for (const conversation of this.conversations.values()) {
|
|
127
125
|
const turn = conversation.turn;
|
|
128
126
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
129
|
-
if (conversation.
|
|
127
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
130
128
|
return false;
|
|
131
129
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (running === null || running.baseline === null)
|
|
135
|
-
continue;
|
|
136
|
-
if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
|
|
137
|
-
continue;
|
|
138
|
-
this.recheck ??= setTimeout(() => {
|
|
139
|
-
this.recheck = null;
|
|
140
|
-
this.kit.changed();
|
|
141
|
-
}, PROCESS_RECHECK_MS);
|
|
130
|
+
const conversations = [...this.conversations.values()];
|
|
131
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
142
132
|
return false;
|
|
133
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
134
|
+
}
|
|
135
|
+
look() {
|
|
136
|
+
for (const conversation of this.conversations.values()) {
|
|
137
|
+
for (const scope of conversation.leftovers) {
|
|
138
|
+
if (!scope.empty())
|
|
139
|
+
continue;
|
|
140
|
+
scope.close();
|
|
141
|
+
conversation.leftovers.delete(scope);
|
|
142
|
+
}
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
this.kit.changed();
|
|
145
145
|
}
|
|
146
146
|
opened() {
|
|
147
147
|
this.watched = false;
|
|
@@ -295,11 +295,13 @@ export class Conversations {
|
|
|
295
295
|
}
|
|
296
296
|
halt(conversation) {
|
|
297
297
|
if (conversation.opening !== null)
|
|
298
|
-
|
|
298
|
+
killMarked(marker(conversation.opening));
|
|
299
|
+
for (const scope of conversation.leftovers)
|
|
300
|
+
scope.kill();
|
|
299
301
|
if (conversation.running === null)
|
|
300
302
|
return;
|
|
301
303
|
conversation.running.killed = true;
|
|
302
|
-
|
|
304
|
+
conversation.running.scope.kill();
|
|
303
305
|
}
|
|
304
306
|
async kill(conversation) {
|
|
305
307
|
const running = conversation.running;
|
|
@@ -350,22 +352,30 @@ export class Conversations {
|
|
|
350
352
|
throw new Error(KILLED);
|
|
351
353
|
}
|
|
352
354
|
conversation.opening = bridge;
|
|
355
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
356
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
353
357
|
try {
|
|
354
358
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
355
|
-
await CLIS[route.kind].allowHouse();
|
|
356
|
-
|
|
357
|
-
|
|
359
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
360
|
+
const adapter = await startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
361
|
+
work.read(message);
|
|
362
|
+
this.read(conversation, reader, message);
|
|
363
|
+
}, { ...bridge.env, ...(await work.open(cli)) }, undefined, scope);
|
|
358
364
|
void adapter.exited.then(() => bridge.close());
|
|
359
365
|
const agent = adapter.connection.agent;
|
|
360
366
|
const cwd = route.working_directory;
|
|
361
|
-
const _meta = CLIS[route.kind].sessionMeta;
|
|
367
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
362
368
|
const opened = sessionId === null
|
|
363
369
|
? await agent.request('session/new', { cwd, mcpServers: [], _meta })
|
|
364
370
|
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
365
371
|
reader.session = opened.sessionId;
|
|
366
|
-
|
|
372
|
+
if (sessionId === null && CLIS[route.kind].renames) {
|
|
373
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
374
|
+
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
375
|
+
}
|
|
376
|
+
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
367
377
|
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
368
|
-
|
|
378
|
+
void work.settled().then(() => scope.record());
|
|
369
379
|
conversation.running = running;
|
|
370
380
|
conversation.session = running.sessionId;
|
|
371
381
|
this.kit.send({ type: 'process', conversation_id: conversation.id, running: true });
|
|
@@ -376,7 +386,9 @@ export class Conversations {
|
|
|
376
386
|
return running.sessionId;
|
|
377
387
|
}
|
|
378
388
|
catch (error) {
|
|
379
|
-
|
|
389
|
+
scope.kill();
|
|
390
|
+
work.close();
|
|
391
|
+
conversation.leftovers.add(scope);
|
|
380
392
|
bridge.close();
|
|
381
393
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
382
394
|
}
|
|
@@ -409,17 +421,18 @@ export class Conversations {
|
|
|
409
421
|
const running = conversation.running;
|
|
410
422
|
if (running?.adapter !== adapter)
|
|
411
423
|
return;
|
|
412
|
-
this.detach(conversation);
|
|
424
|
+
this.detach(conversation, running);
|
|
413
425
|
if (conversation.turn !== null) {
|
|
414
426
|
this.end(conversation, conversation.turn, { failed: running.killed ? KILLED : cause });
|
|
415
427
|
}
|
|
416
428
|
this.next(conversation);
|
|
417
429
|
this.kit.changed();
|
|
418
430
|
}
|
|
419
|
-
detach(conversation) {
|
|
431
|
+
detach(conversation, running) {
|
|
432
|
+
running.work.close();
|
|
433
|
+
conversation.leftovers.add(running.scope);
|
|
420
434
|
conversation.running = null;
|
|
421
435
|
conversation.commands = null;
|
|
422
|
-
conversation.busy.clear();
|
|
423
436
|
for (const [id, question] of this.questions) {
|
|
424
437
|
if (question.conversation === conversation)
|
|
425
438
|
this.questions.delete(id);
|
|
@@ -471,8 +484,6 @@ export class Conversations {
|
|
|
471
484
|
turn.parts.measure(conversation.context);
|
|
472
485
|
for (const part of conversation.held.splice(0))
|
|
473
486
|
turn.parts.start(part);
|
|
474
|
-
for (const event of conversation.running?.reader.turnStarted() ?? [])
|
|
475
|
-
this.event(conversation, event);
|
|
476
487
|
this.kit.changed();
|
|
477
488
|
return turn;
|
|
478
489
|
}
|
|
@@ -506,8 +517,8 @@ export class Conversations {
|
|
|
506
517
|
}
|
|
507
518
|
else {
|
|
508
519
|
if (conversation.open === 1 && conversation.running === running) {
|
|
509
|
-
|
|
510
|
-
this.detach(conversation);
|
|
520
|
+
running.scope.kill();
|
|
521
|
+
this.detach(conversation, running);
|
|
511
522
|
}
|
|
512
523
|
this.end(conversation, turn, { failed: causeOf(error) });
|
|
513
524
|
}
|
|
@@ -592,20 +603,6 @@ export class Conversations {
|
|
|
592
603
|
case 'options':
|
|
593
604
|
this.options(conversation, event.options);
|
|
594
605
|
return;
|
|
595
|
-
case 'busy':
|
|
596
|
-
if (event.running)
|
|
597
|
-
conversation.busy.add(event.key);
|
|
598
|
-
else
|
|
599
|
-
conversation.busy.delete(event.key);
|
|
600
|
-
this.kit.changed();
|
|
601
|
-
return;
|
|
602
|
-
case 'servers': {
|
|
603
|
-
const running = conversation.running;
|
|
604
|
-
if (running?.baseline)
|
|
605
|
-
for (const found of processes(marker(running.bridge)))
|
|
606
|
-
running.baseline.add(found);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
606
|
case 'context':
|
|
610
607
|
conversation.context = { used: event.used, window: event.window };
|
|
611
608
|
conversation.turn?.parts.measure(conversation.context);
|
package/dist/copies.js
CHANGED
|
@@ -32,6 +32,22 @@ function rebaseFixes(refusal) {
|
|
|
32
32
|
function short(commit) {
|
|
33
33
|
return commit.slice(0, 12);
|
|
34
34
|
}
|
|
35
|
+
function moved(before, after) {
|
|
36
|
+
if (before.house !== after.house)
|
|
37
|
+
return [`${rootOf(after.room_handle)}: ${HOUSE_REF} is at ${short(after.house)}`];
|
|
38
|
+
return before.position === after.position ? [] : [`${rootOf(after.room_handle)}: read-only files refreshed`];
|
|
39
|
+
}
|
|
40
|
+
function expiredAuthority(error) {
|
|
41
|
+
if (!(error instanceof HouseRefusal))
|
|
42
|
+
return undefined;
|
|
43
|
+
try {
|
|
44
|
+
const { error: refused } = JSON.parse(error.text);
|
|
45
|
+
return refused?.code === 'expired' && typeof refused.authority === 'string' ? refused.authority : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
35
51
|
function covers(path, prefix) {
|
|
36
52
|
return prefix === '' || path === prefix || path.startsWith(`${prefix}/`);
|
|
37
53
|
}
|
|
@@ -152,8 +168,7 @@ export class LocalCopies {
|
|
|
152
168
|
house;
|
|
153
169
|
copies = new Map();
|
|
154
170
|
queues = new Map();
|
|
155
|
-
|
|
156
|
-
selecting = Promise.resolve();
|
|
171
|
+
answering = Promise.resolve();
|
|
157
172
|
constructor(house) {
|
|
158
173
|
this.house = house;
|
|
159
174
|
this.owner = {
|
|
@@ -185,42 +200,68 @@ export class LocalCopies {
|
|
|
185
200
|
}
|
|
186
201
|
}
|
|
187
202
|
async mapping() {
|
|
188
|
-
await this.
|
|
203
|
+
await this.answering;
|
|
189
204
|
return [...this.copies.values()]
|
|
190
205
|
.filter((copy) => copy.active)
|
|
191
206
|
.map((copy) => `${rootOf(copy.room_handle)}: ${copy.repository}`)
|
|
192
207
|
.sort();
|
|
193
208
|
}
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
async reselect() {
|
|
200
|
-
const { local_copy: chosen } = await this.house.post('/kit/local-copy/selection', {});
|
|
201
|
-
const selected = (chosen?.rooms ?? []).flatMap((room) => room.room_handle === null ? [] : [{ key: `rooms/${room.room_ref}`, handle: room.room_handle }]);
|
|
202
|
-
const keys = new Set(selected.map((scope) => scope.key));
|
|
203
|
-
const work = [];
|
|
204
|
-
for (const key of this.copies.keys()) {
|
|
205
|
-
if (keys.has(key))
|
|
206
|
-
continue;
|
|
207
|
-
work.push(this.exclusive(key, async () => {
|
|
208
|
-
const copy = this.copies.get(key);
|
|
209
|
-
if (copy.active)
|
|
210
|
-
await this.save({ ...copy, active: false });
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
for (const scope of selected)
|
|
214
|
-
work.push(this.exclusive(scope.key, () => this.apply(scope)).catch(logged));
|
|
215
|
-
await Promise.all(work);
|
|
209
|
+
async connected() {
|
|
210
|
+
const copied = await this.answered(() => this.fetched(false));
|
|
211
|
+
if (copied.refused)
|
|
212
|
+
logged(copied.text);
|
|
216
213
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
214
|
+
fetch() {
|
|
215
|
+
return this.answered(() => this.fetched(true));
|
|
216
|
+
}
|
|
217
|
+
answered(work) {
|
|
218
|
+
const answered = this.answering.then(work);
|
|
219
|
+
this.answering = answered.catch(() => undefined);
|
|
220
|
+
return answered;
|
|
221
|
+
}
|
|
222
|
+
async fetched(refreshing) {
|
|
223
|
+
const { local_copy: answer } = await this.house.post('/kit/local-copy/selection', {});
|
|
224
|
+
const synced = new Map(answer.rooms.map((room) => [`rooms/${room.room_ref}`, room.room_handle]));
|
|
225
|
+
return this.holding([...new Set([...this.copies.keys(), ...synced.keys()])].sort(), async () => {
|
|
226
|
+
const said = [];
|
|
227
|
+
let refused = false;
|
|
228
|
+
for (const copy of this.copies.values()) {
|
|
229
|
+
if (synced.has(copy.key) || !copy.active)
|
|
230
|
+
continue;
|
|
231
|
+
await this.save({ ...copy, active: false });
|
|
232
|
+
said.push(`${rootOf(copy.room_handle)}: no longer synced to this Environment; its files stay`);
|
|
233
|
+
}
|
|
234
|
+
const current = [];
|
|
235
|
+
for (const [key, handle] of synced) {
|
|
236
|
+
try {
|
|
237
|
+
const held = this.copies.get(key);
|
|
238
|
+
if (held === undefined || !(await exists(held.repository))) {
|
|
239
|
+
said.push(await this.materialize(key, handle));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const copy = held.room_handle === handle ? held : await this.save({ ...held, room_handle: handle });
|
|
243
|
+
if (copy.active)
|
|
244
|
+
current.push(copy);
|
|
245
|
+
else
|
|
246
|
+
said.push(...moved(copy, await this.save(await this.received({ ...copy, active: true }))));
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
refused = true;
|
|
250
|
+
said.push(`${rootOf(handle)}: ${causeOf(error)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (refreshing && current.length > 0) {
|
|
254
|
+
try {
|
|
255
|
+
const refreshed = await this.refreshed(current);
|
|
256
|
+
current.forEach((copy, index) => said.push(...moved(copy, refreshed[index])));
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
refused = true;
|
|
260
|
+
said.push(error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { refused, text: said.length === 0 ? 'Nothing new from House.' : said.join('\n') };
|
|
264
|
+
});
|
|
224
265
|
}
|
|
225
266
|
async observed(report) {
|
|
226
267
|
const key = await this.keyAt(dirname(report.common));
|
|
@@ -270,16 +311,9 @@ export class LocalCopies {
|
|
|
270
311
|
this.queues.set(key, queued.catch(() => undefined));
|
|
271
312
|
return queued;
|
|
272
313
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
this.delivering.add(key);
|
|
277
|
-
void this.exclusive(key, async () => {
|
|
278
|
-
this.delivering.delete(key);
|
|
279
|
-
const copy = this.copies.get(key);
|
|
280
|
-
if (copy.active)
|
|
281
|
-
await this.save(await this.caughtUp(copy));
|
|
282
|
-
}).catch(logged);
|
|
314
|
+
holding(keys, work) {
|
|
315
|
+
const [key, ...rest] = keys;
|
|
316
|
+
return key === undefined ? work() : this.exclusive(key, () => this.holding(rest, work));
|
|
283
317
|
}
|
|
284
318
|
async save(copy) {
|
|
285
319
|
const path = join(kitHome(), 'local-copies', `${copy.key}.json`);
|
|
@@ -325,46 +359,13 @@ export class LocalCopies {
|
|
|
325
359
|
}
|
|
326
360
|
return read;
|
|
327
361
|
}
|
|
328
|
-
async
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (copy.room_handle !== scope.handle)
|
|
335
|
-
copy = await this.save({ ...copy, room_handle: scope.handle });
|
|
336
|
-
if (!copy.active) {
|
|
337
|
-
await this.save(await this.received({ ...copy, active: true }));
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
const discovery = await this.house.post('/kit/door/discover', {
|
|
341
|
-
prefixes: [rootOf(copy.room_handle)],
|
|
342
|
-
});
|
|
343
|
-
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
344
|
-
if (authority === undefined)
|
|
345
|
-
return;
|
|
346
|
-
if (authority.managed_replication !== 'admitted') {
|
|
347
|
-
logged(`operation_denied: ${rootOf(copy.room_handle)} is not synchronized to this Environment`);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (JSON.stringify([authority.operations.write, authority.protected_paths]) !==
|
|
351
|
-
JSON.stringify([copy.write, copy.protected])) {
|
|
352
|
-
await this.save(await this.received(copy));
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
await this.save(await this.caughtUp(copy));
|
|
356
|
-
}
|
|
357
|
-
async materialize(scope) {
|
|
358
|
-
const read = await this.read(scope.handle);
|
|
359
|
-
if (read === null) {
|
|
360
|
-
logged(`House serves nothing of ${rootOf(scope.handle)} to copy`);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
362
|
+
async materialize(key, handle) {
|
|
363
|
+
const read = await this.read(handle);
|
|
364
|
+
if (read === null)
|
|
365
|
+
throw new Error('House serves nothing of it to copy');
|
|
363
366
|
const repository = join(localCopies(), 'rooms', read.anchor);
|
|
364
|
-
if (await exists(repository))
|
|
365
|
-
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
367
|
+
if (await exists(repository))
|
|
368
|
+
throw new Error(`${repository} already exists and House Kit leaves it as it is`);
|
|
368
369
|
const staging = join(dirname(repository), `.${randomUUID()}`);
|
|
369
370
|
await mkdir(staging, { recursive: true });
|
|
370
371
|
let house;
|
|
@@ -372,7 +373,7 @@ export class LocalCopies {
|
|
|
372
373
|
try {
|
|
373
374
|
await git(staging, ['init', '-q', '-b', BRANCH]);
|
|
374
375
|
house = await importCommits(staging, [
|
|
375
|
-
{ message: `House ${rootOf(
|
|
376
|
+
{ message: `House ${rootOf(handle)}`, from: null, files: read.writable },
|
|
376
377
|
]);
|
|
377
378
|
await git(staging, ['update-ref', `refs/heads/${BRANCH}`, house]);
|
|
378
379
|
await git(staging, ['reset', '-q', '--hard']);
|
|
@@ -385,9 +386,9 @@ export class LocalCopies {
|
|
|
385
386
|
throw error;
|
|
386
387
|
}
|
|
387
388
|
await this.save({
|
|
388
|
-
key
|
|
389
|
+
key,
|
|
389
390
|
room_ref: read.anchor,
|
|
390
|
-
room_handle:
|
|
391
|
+
room_handle: handle,
|
|
391
392
|
repository,
|
|
392
393
|
active: true,
|
|
393
394
|
house,
|
|
@@ -399,6 +400,7 @@ export class LocalCopies {
|
|
|
399
400
|
manifests: { [house]: { parent: null, files: read.revisions } },
|
|
400
401
|
moves: [],
|
|
401
402
|
});
|
|
403
|
+
return `${rootOf(handle)}: copied to ${repository}`;
|
|
402
404
|
}
|
|
403
405
|
async received(copy) {
|
|
404
406
|
const read = await this.read(copy.room_handle);
|
|
@@ -422,29 +424,52 @@ export class LocalCopies {
|
|
|
422
424
|
await ignoreRules(copy.repository, [...read.writable.keys()], next);
|
|
423
425
|
return next;
|
|
424
426
|
}
|
|
425
|
-
async
|
|
426
|
-
|
|
427
|
-
|
|
427
|
+
async refreshed(copies) {
|
|
428
|
+
const discovery = await this.house.post('/kit/door/discover', {
|
|
429
|
+
prefixes: copies.map((copy) => rootOf(copy.room_handle)),
|
|
430
|
+
});
|
|
431
|
+
const regranted = new Map();
|
|
432
|
+
for (const copy of copies) {
|
|
433
|
+
const authority = discovery.authorities.find((entry) => entry.room_ref === copy.room_ref);
|
|
434
|
+
if (authority !== undefined &&
|
|
435
|
+
JSON.stringify([authority.operations.write, authority.protected_paths]) !== JSON.stringify([copy.write, copy.protected])) {
|
|
436
|
+
regranted.set(copy.key, await this.save(await this.received(copy)));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const caught = await this.caughtUp(copies.filter((copy) => !regranted.has(copy.key)), false);
|
|
440
|
+
return copies.map((copy) => regranted.get(copy.key) ?? caught.find((held) => held.key === copy.key));
|
|
441
|
+
}
|
|
442
|
+
async caughtUp(copies, whole) {
|
|
443
|
+
const caught = new Map(copies.map((copy) => [copy.key, copy]));
|
|
444
|
+
let asking = [...copies];
|
|
445
|
+
while (asking.length > 0) {
|
|
428
446
|
let answer;
|
|
429
447
|
try {
|
|
430
448
|
answer = await this.house.post('/kit/door/enumerate', {
|
|
431
|
-
prefixes:
|
|
432
|
-
positions:
|
|
449
|
+
prefixes: asking.map((copy) => rootOf(copy.room_handle)),
|
|
450
|
+
positions: asking.map((copy) => ({ authority: copy.room_ref, position: copy.position, log_epoch: copy.log_epoch })),
|
|
433
451
|
});
|
|
434
452
|
}
|
|
435
453
|
catch (error) {
|
|
436
|
-
const
|
|
437
|
-
if (
|
|
438
|
-
return this.received(copy);
|
|
439
|
-
if (code !== 'replica_behind')
|
|
454
|
+
const expired = asking.find((copy) => copy.room_ref === expiredAuthority(error));
|
|
455
|
+
if (expired === undefined)
|
|
440
456
|
throw error;
|
|
441
|
-
|
|
457
|
+
caught.set(expired.key, await this.save(await this.received(expired)));
|
|
458
|
+
asking = asking.filter((copy) => copy !== expired);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const advanced = [];
|
|
462
|
+
for (const copy of asking) {
|
|
463
|
+
const view = answer.authorities.find((entry) => entry.authority === copy.room_ref);
|
|
464
|
+
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
465
|
+
continue;
|
|
466
|
+
const next = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
467
|
+
caught.set(copy.key, next);
|
|
468
|
+
advanced.push(next);
|
|
442
469
|
}
|
|
443
|
-
|
|
444
|
-
if (view === undefined || (view.position === copy.position && view.log_epoch === copy.log_epoch))
|
|
445
|
-
return copy;
|
|
446
|
-
copy = await this.save(await this.incorporated(copy, view.changes, view.position, view.log_epoch));
|
|
470
|
+
asking = whole ? advanced : [];
|
|
447
471
|
}
|
|
472
|
+
return copies.map((copy) => caught.get(copy.key));
|
|
448
473
|
}
|
|
449
474
|
async incorporated(copy, changes, position, logEpoch) {
|
|
450
475
|
const houseTree = await tree(copy.repository, copy.house);
|
|
@@ -598,12 +623,12 @@ export class LocalCopies {
|
|
|
598
623
|
const { upload, ...unstaged } = copy.push;
|
|
599
624
|
return this.settle(await this.save({ ...copy, push: unstaged }), caller, true);
|
|
600
625
|
}
|
|
601
|
-
if (outcome.refused.code === '
|
|
626
|
+
if (outcome.refused.code === 'expired')
|
|
602
627
|
return this.recovered(copy);
|
|
603
628
|
const rebased = rebaseFixes(outcome.refused);
|
|
604
629
|
let settled = await this.save(without(copy));
|
|
605
630
|
try {
|
|
606
|
-
settled =
|
|
631
|
+
settled = (await this.caughtUp([settled], true))[0];
|
|
607
632
|
}
|
|
608
633
|
catch {
|
|
609
634
|
return {
|
|
@@ -622,7 +647,7 @@ export class LocalCopies {
|
|
|
622
647
|
}
|
|
623
648
|
async recovered(copy) {
|
|
624
649
|
const push = copy.push;
|
|
625
|
-
const current =
|
|
650
|
+
const current = (await this.caughtUp([copy], true))[0];
|
|
626
651
|
const house = await tree(copy.repository, current.house);
|
|
627
652
|
const root = rootOf(current.room_handle);
|
|
628
653
|
if (!(await this.landed(copy.repository, push.changes, house, await tree(copy.repository, push.commit)))) {
|
|
@@ -666,7 +691,7 @@ export class LocalCopies {
|
|
|
666
691
|
const push = copy.push;
|
|
667
692
|
let written = push.written;
|
|
668
693
|
if (push.changes.some((change) => change.op === 'rename')) {
|
|
669
|
-
copy =
|
|
694
|
+
copy = (await this.caughtUp([copy], true))[0];
|
|
670
695
|
const house = await tree(copy.repository, copy.house);
|
|
671
696
|
const sent = await tree(copy.repository, push.house);
|
|
672
697
|
if (push.changes.some((change) => change.op === 'rename' && !house.has(relative(change.to)))) {
|
package/dist/end.js
CHANGED
package/dist/enrol-main.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { writeEnrolment } from './home.js';
|
|
3
3
|
const [house, environment] = process.argv.slice(2);
|
|
4
|
-
|
|
4
|
+
const relay = process.env.HOUSE_KIT_RELAY || undefined;
|
|
5
|
+
await writeEnrolment({
|
|
6
|
+
house: house,
|
|
7
|
+
...(relay === undefined ? {} : { relay }),
|
|
8
|
+
environment: environment,
|
|
9
|
+
credential: readFileSync(0, 'utf8').trim(),
|
|
10
|
+
});
|