@agentshouse/kit 0.1.0-alpha.30 → 0.1.0-alpha.31
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 +26 -4
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/sync.js +93 -20
- package/src/worker.js +14 -3
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.31/connect-linux.sh)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The bootstrap first establishes Docker Engine from what it observes on the
|
|
@@ -968,7 +968,7 @@ Within the same live Kit and supervised WorkerSession, a lost response or
|
|
|
968
968
|
infrastructure failure resends that request with the same identifier. A local
|
|
969
969
|
socket reconnect delivers the retained outcome to the process.
|
|
970
970
|
A second deliberate call gets a new identifier; read-only calls carry none.
|
|
971
|
-
House refusals pass through to the CLI without automatic resend. [R193](https://github.com/agentshouse/core/blob/main/docs/concept/implementation/
|
|
971
|
+
House refusals pass through to the CLI without automatic resend. [R193](https://github.com/agentshouse/core/blob/main/docs/concept/implementation/14-agents-profiles-sessions-and-runs.md#r193)
|
|
972
972
|
owns recovery authority, lifetime, and refusal semantics.
|
|
973
973
|
|
|
974
974
|
The launcher speaks only what the adapter contract fixes for this build. Every
|
|
@@ -1070,7 +1070,7 @@ Between turns, Kit retains the opaque provider session identifier and the Run's
|
|
|
1070
1070
|
Worker, working directory, and route references in an owner-only local record.
|
|
1071
1071
|
The record carries no transcript or Session credential and grants no authority:
|
|
1072
1072
|
House must authorize each successor WorkerSession. A reported terminal state and
|
|
1073
|
-
owner stops remove the record. [R199](https://github.com/agentshouse/core/blob/main/docs/concept/implementation/
|
|
1073
|
+
owner stops remove the record. [R199](https://github.com/agentshouse/core/blob/main/docs/concept/implementation/14-agents-profiles-sessions-and-runs.md#r199)
|
|
1074
1074
|
owns conversation continuity.
|
|
1075
1075
|
|
|
1076
1076
|
Launcher exit is the terminal process observation. The Kit closes the local
|
|
@@ -1548,7 +1548,12 @@ House alone validates the resulting Library: a batch it refuses answers with
|
|
|
1548
1548
|
House's own file path, field, rule, and version. A refusal is reported with the
|
|
1549
1549
|
current material House returned and the local directory is left for the caller.
|
|
1550
1550
|
An acceptance updates the recorded position, its log epoch, and the affected
|
|
1551
|
-
files from the returned final content.
|
|
1551
|
+
files from the returned final content. It also reads House's own provenance
|
|
1552
|
+
for those files by enumerating from the position the batch started at, and
|
|
1553
|
+
writes each served sidecar whose revision is the accepted one, so a submit,
|
|
1554
|
+
answered or recovered after a lost answer, leaves each file's `_provenance`
|
|
1555
|
+
sidecar at the revision House recorded. Kit never composes that metadata
|
|
1556
|
+
itself.
|
|
1552
1557
|
|
|
1553
1558
|
Pending submissions are written before transfer. A lost response, reconnect, or
|
|
1554
1559
|
lease renewal recovers that exact request under the same identifier. A domain
|
|
@@ -1561,6 +1566,10 @@ the Kit drops it like any other refusal, `rebootstrap` rebuilds the directory,
|
|
|
1561
1566
|
and a change still wanted is a new deliberate submit against the rebuilt bases.
|
|
1562
1567
|
Once House has answered any request for a replica `position_expired`, the
|
|
1563
1568
|
replica refuses `submit` as `position_expired` until `rebootstrap` rebuilds it.
|
|
1569
|
+
A pending submission House answers `operation_expired` is uncertain, so the
|
|
1570
|
+
Kit first pulls confirmed state. If House already holds the pending change,
|
|
1571
|
+
the Kit drops the pending copy and sends nothing more; only if it does not
|
|
1572
|
+
does the Kit send that change again under a new identifier.
|
|
1564
1573
|
`rebootstrap` drops a submission still pending against the directory it
|
|
1565
1574
|
replaces.
|
|
1566
1575
|
|
|
@@ -2099,6 +2108,19 @@ the previous control lease before starting the next isolated command. Evidence
|
|
|
2099
2108
|
is written to `tmp/delivery-evidence/migrator-pair.json` with producer revisions
|
|
2100
2109
|
and the installed package digest.
|
|
2101
2110
|
|
|
2111
|
+
Explicit sync has its own paired proof:
|
|
2112
|
+
|
|
2113
|
+
```sh
|
|
2114
|
+
HOUSE_CORE_ROOT=<core checkout> HOUSE_AUTH_ROOT=<auth checkout> \
|
|
2115
|
+
KIT_PAIR_SPECS=/kit/test/pair/sync.vitest.ts bash test/pair/run.sh
|
|
2116
|
+
```
|
|
2117
|
+
|
|
2118
|
+
It proves that an accepted `submit`, answered or recovered after a lost answer,
|
|
2119
|
+
leaves each file's `_provenance` sidecar equal to the provenance House serves,
|
|
2120
|
+
and that a pending submission House answers `operation_expired` is pulled first,
|
|
2121
|
+
then sent again under a new identifier only if it never landed. Evidence is
|
|
2122
|
+
written to `tmp/delivery-evidence/sync-pair.json`.
|
|
2123
|
+
|
|
2102
2124
|
The retained-original consumers have their own paired proofs:
|
|
2103
2125
|
|
|
2104
2126
|
```sh
|
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.31",
|
|
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.31/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.31/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
package/src/sync.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
assertWritableChanges,
|
|
14
14
|
authorityFor,
|
|
15
15
|
deriveChanges,
|
|
16
|
+
housePath,
|
|
16
17
|
OPERATIONS,
|
|
17
18
|
relativeTo,
|
|
18
19
|
resultingFiles,
|
|
@@ -248,6 +249,12 @@ function enumeratedRecord(path, content, change) {
|
|
|
248
249
|
};
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
async function writeSidecar(options, authority, recorded, path, change, signal) {
|
|
253
|
+
const sidecar = sidecarPath(path);
|
|
254
|
+
await writeReplicaFile(locate(options.replica, authority.path, sidecar), change.provenance, signal);
|
|
255
|
+
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
256
|
+
}
|
|
257
|
+
|
|
251
258
|
async function applyEnumeration(options, state, enumeration, signal) {
|
|
252
259
|
if (!Array.isArray(enumeration?.authorities)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
253
260
|
for (const view of enumeration.authorities) {
|
|
@@ -271,13 +278,7 @@ async function applyEnumeration(options, state, enumeration, signal) {
|
|
|
271
278
|
recorded.set(change.path, enumeratedRecord(change.path, change.content, change));
|
|
272
279
|
}
|
|
273
280
|
if (typeof change.provenance === "string") {
|
|
274
|
-
|
|
275
|
-
await writeReplicaFile(
|
|
276
|
-
locate(options.replica, authority.path, sidecar),
|
|
277
|
-
change.provenance,
|
|
278
|
-
signal,
|
|
279
|
-
);
|
|
280
|
-
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
281
|
+
await writeSidecar(options, authority, recorded, change.path, change, signal);
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
authority.files = [...recorded.values()];
|
|
@@ -309,12 +310,40 @@ function uncertainTransport(error) {
|
|
|
309
310
|
);
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
async function
|
|
313
|
+
async function servedProvenance(session, options, state, authority, signal) {
|
|
314
|
+
const enumeration = await replicaDoor(
|
|
315
|
+
session,
|
|
316
|
+
options,
|
|
317
|
+
state,
|
|
318
|
+
KIT_DOOR_ENUMERATE_PATH,
|
|
319
|
+
{
|
|
320
|
+
prefixes: state.prefixes.filter((prefix) => authorityFor(state, prefix) === authority),
|
|
321
|
+
positions: [
|
|
322
|
+
{ authority: authority.authority, position: authority.position, log_epoch: authority.log_epoch },
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
signal,
|
|
326
|
+
);
|
|
327
|
+
const view = Array.isArray(enumeration?.authorities)
|
|
328
|
+
? enumeration.authorities.find((entry) => entry?.authority === authority.authority)
|
|
329
|
+
: undefined;
|
|
330
|
+
if (!Array.isArray(view?.changes)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
331
|
+
return new Map(
|
|
332
|
+
view.changes
|
|
333
|
+
.filter((change) => typeof change?.provenance === "string")
|
|
334
|
+
.map((change) => [change.path, change]),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function acceptBatch(session, options, state, outcome, signal, pending = null) {
|
|
313
339
|
const authority = state.authorities.find((entry) => entry.authority === outcome.authority);
|
|
314
340
|
if (authority === undefined) throw new KitRefusal("kit_sync_rejected", outcome.authority);
|
|
315
341
|
const reached = changePosition(outcome, "batch");
|
|
316
342
|
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
317
343
|
const predicted = predictedReplica(authority, pending);
|
|
344
|
+
const provenance = (outcome.contents ?? []).some((file) => typeof file?.content === "string")
|
|
345
|
+
? await servedProvenance(session, options, state, authority, signal)
|
|
346
|
+
: new Map();
|
|
318
347
|
for (const file of removalsFirst(outcome.contents ?? [], (file) => file?.content === null)) {
|
|
319
348
|
const relative = relativeTo(authority.path, file.path);
|
|
320
349
|
const destination = locate(options.replica, authority.path, relative);
|
|
@@ -340,6 +369,10 @@ async function acceptBatch(options, state, outcome, signal, pending = null) {
|
|
|
340
369
|
sha256: "",
|
|
341
370
|
content: accepted,
|
|
342
371
|
});
|
|
372
|
+
const served = provenance.get(relative);
|
|
373
|
+
if (served !== undefined && served.revision === file.revision) {
|
|
374
|
+
await writeSidecar(options, authority, recorded, relative, served, signal);
|
|
375
|
+
}
|
|
343
376
|
}
|
|
344
377
|
authority.files = [...recorded.values()];
|
|
345
378
|
Object.assign(authority, reached);
|
|
@@ -351,16 +384,65 @@ function sameBody(left, right) {
|
|
|
351
384
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
352
385
|
}
|
|
353
386
|
|
|
387
|
+
function contentMap(authority) {
|
|
388
|
+
return Object.fromEntries(authority.files.map((file) => [file.path, file.content]));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function pullConfirmed(session, options, state, signal) {
|
|
392
|
+
const enumeration = await replicaDoor(
|
|
393
|
+
session,
|
|
394
|
+
options,
|
|
395
|
+
state,
|
|
396
|
+
KIT_DOOR_ENUMERATE_PATH,
|
|
397
|
+
{ prefixes: state.prefixes, positions: positionsOf(state) },
|
|
398
|
+
signal,
|
|
399
|
+
);
|
|
400
|
+
return applyEnumeration(options, state, enumeration, signal);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function settleExpired(session, options, state, pending, signal) {
|
|
404
|
+
const authority = authorityFor(state, pending.body.changes[0].path);
|
|
405
|
+
const predicted = predictedReplica(authority, pending);
|
|
406
|
+
const before = contentMap(authority);
|
|
407
|
+
await pullConfirmed(session, options, state, signal);
|
|
408
|
+
const confirmed = contentMap(authority);
|
|
409
|
+
const touched = [...new Set([...Object.keys(before), ...Object.keys(predicted)])]
|
|
410
|
+
.filter((path) => before[path] !== predicted[path])
|
|
411
|
+
.sort();
|
|
412
|
+
const landed = touched.length > 0 && touched.every((path) => confirmed[path] === predicted[path]);
|
|
413
|
+
await clearPending(options.directory, state.directory);
|
|
414
|
+
if (!landed) {
|
|
415
|
+
return { pending, accepted: await sendBatch(session, options, state, pending.body.changes, signal) };
|
|
416
|
+
}
|
|
417
|
+
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
418
|
+
return {
|
|
419
|
+
pending,
|
|
420
|
+
accepted: {
|
|
421
|
+
authority: authority.authority,
|
|
422
|
+
position: authority.position,
|
|
423
|
+
log_epoch: authority.log_epoch,
|
|
424
|
+
contents: touched.map((path) => ({
|
|
425
|
+
path: housePath(authority.path, path),
|
|
426
|
+
content: recorded.get(path)?.content ?? null,
|
|
427
|
+
revision: recorded.get(path)?.revision ?? null,
|
|
428
|
+
})),
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
354
433
|
async function recoverPending(session, options, state, signal) {
|
|
355
434
|
const pending = await readPending(options.directory, state.directory);
|
|
356
435
|
if (pending === null) return null;
|
|
357
436
|
try {
|
|
358
437
|
const outcome = await replicaDoor(session, options, state, pending.path, pending.body, signal);
|
|
359
|
-
const accepted = await acceptBatch(options, state, outcome, signal, pending);
|
|
438
|
+
const accepted = await acceptBatch(session, options, state, outcome, signal, pending);
|
|
360
439
|
await clearPending(options.directory, state.directory);
|
|
361
440
|
return { pending, accepted };
|
|
362
441
|
} catch (error) {
|
|
363
442
|
if (uncertainTransport(error)) throw error;
|
|
443
|
+
if (error.code === "operation_expired") {
|
|
444
|
+
return settleExpired(session, options, state, pending, signal);
|
|
445
|
+
}
|
|
364
446
|
await clearPending(options.directory, state.directory);
|
|
365
447
|
throw error;
|
|
366
448
|
}
|
|
@@ -396,7 +478,7 @@ async function sendBatch(session, options, state, changes, signal) {
|
|
|
396
478
|
);
|
|
397
479
|
try {
|
|
398
480
|
const outcome = await replicaDoor(session, options, state, KIT_DOOR_BATCH_PATH, body, signal);
|
|
399
|
-
last = await acceptBatch(options, state, outcome, signal);
|
|
481
|
+
last = await acceptBatch(session, options, state, outcome, signal);
|
|
400
482
|
await clearPending(options.directory, state.directory);
|
|
401
483
|
} catch (error) {
|
|
402
484
|
if (uncertainTransport(error)) throw error;
|
|
@@ -441,16 +523,7 @@ export async function bootstrap(options, dependencies = {}) {
|
|
|
441
523
|
|
|
442
524
|
export async function pull(options, dependencies = {}) {
|
|
443
525
|
return withSession(options, dependencies, async (session, signal) => {
|
|
444
|
-
const
|
|
445
|
-
const enumeration = await replicaDoor(
|
|
446
|
-
session,
|
|
447
|
-
options,
|
|
448
|
-
state,
|
|
449
|
-
KIT_DOOR_ENUMERATE_PATH,
|
|
450
|
-
{ prefixes: state.prefixes, positions: positionsOf(state) },
|
|
451
|
-
signal,
|
|
452
|
-
);
|
|
453
|
-
const next = await applyEnumeration(options, state, enumeration, signal);
|
|
526
|
+
const next = await pullConfirmed(session, options, await loadState(options), signal);
|
|
454
527
|
return { positions: positionsOf(next) };
|
|
455
528
|
});
|
|
456
529
|
}
|
package/src/worker.js
CHANGED
|
@@ -958,7 +958,7 @@ async function reportedWorkingCopies(act, context, signal) {
|
|
|
958
958
|
return outcome;
|
|
959
959
|
}
|
|
960
960
|
|
|
961
|
-
function residentUpdate(context, wake) {
|
|
961
|
+
export function residentUpdate(context, wake) {
|
|
962
962
|
let requested = existsSync(updateDrain(context.directory));
|
|
963
963
|
let ready = null;
|
|
964
964
|
const waiting = [];
|
|
@@ -967,16 +967,27 @@ function residentUpdate(context, wake) {
|
|
|
967
967
|
return requested;
|
|
968
968
|
},
|
|
969
969
|
async request(serving) {
|
|
970
|
+
let heard = null;
|
|
971
|
+
let hear;
|
|
972
|
+
const answered = new Promise((resolve) => {
|
|
973
|
+
hear = (answer) => {
|
|
974
|
+
heard = answer;
|
|
975
|
+
resolve(answer);
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
waiting.push(hear);
|
|
970
979
|
if (!requested) {
|
|
971
980
|
requested = true;
|
|
972
981
|
await writeText(updateDrain(context.directory), "");
|
|
973
982
|
}
|
|
974
983
|
wake.notify();
|
|
975
984
|
const sessions = await heldSessions(context);
|
|
985
|
+
if (heard !== null) return heard;
|
|
986
|
+
if (ready === null && !serving && sessions.length === 0) return answered;
|
|
987
|
+
waiting.splice(waiting.indexOf(hear), 1);
|
|
976
988
|
if (ready !== null) return ready;
|
|
977
989
|
if (serving) return { ready: false, sessions, serving };
|
|
978
|
-
|
|
979
|
-
return new Promise((resolve) => waiting.push(resolve));
|
|
990
|
+
return { ready: false, sessions };
|
|
980
991
|
},
|
|
981
992
|
answer(answer) {
|
|
982
993
|
for (const resolve of waiting.splice(0)) resolve(answer);
|