@quolu/lattice 0.63.1 → 0.63.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.3",
|
|
4
4
|
"description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Quo / クオ at kitepon.dev",
|
|
@@ -319,6 +319,15 @@ async function replaceCanonical({ pathname, expectedBytes, value, validator, cra
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
/**
|
|
323
|
+
* control-events.jsonを書く他module(gate storeのatomic commit)が、このstoreのappendと
|
|
324
|
+
* 同じper-directory直列化を共有するための入口。同一process内で書き手が2つの規律に
|
|
325
|
+
* 分かれると、appendがcommitの読みと置換の間へ挟まり偽のcommitted prefix衝突になる。
|
|
326
|
+
*/
|
|
327
|
+
export function withControlJournalMutation(runDir, mutation) {
|
|
328
|
+
return enqueue(path.resolve(runDir), mutation);
|
|
329
|
+
}
|
|
330
|
+
|
|
322
331
|
function enqueue(runDir, mutation) {
|
|
323
332
|
const previous = mutationQueues.get(runDir) ?? Promise.resolve();
|
|
324
333
|
const result = previous.then(mutation);
|
|
@@ -4,6 +4,7 @@ import { lstat, mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
import { canonicalizeArtifact } from './artifact-contracts.mjs';
|
|
7
|
+
import { withControlJournalMutation } from './runtime-control-store.mjs';
|
|
7
8
|
import { selfDigest } from './runtime-contracts.mjs';
|
|
8
9
|
|
|
9
10
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
@@ -407,9 +408,17 @@ async function commitBuilt({ runDir, built, crashInjector, controlEventsPath = n
|
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
export async function commitRuntimeGateActivation(options) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
411
|
+
// control journalの読み(buildCommit)と置換(commitBuilt)の間へ、同一process内の
|
|
412
|
+
// control store appendが挟まると、committed prefix検査が偽のGATE_COMMIT_CONFLICTに
|
|
413
|
+
// なる(2026-08-22 CI負荷下で実被弾: managed recompileのepoch activationへdaemonの
|
|
414
|
+
// 並行appendが割り込んだ)。appendと同じper-directory直列化を共有して排除する。
|
|
415
|
+
// 別process間の競合検査は従来どおりprefix比較が持つ。
|
|
416
|
+
const eventsPath = options.controlEventsPath ?? path.join(options.runDir, 'control-events.json');
|
|
417
|
+
return withControlJournalMutation(path.dirname(eventsPath), async () => {
|
|
418
|
+
const built = await buildCommit(options);
|
|
419
|
+
return commitBuilt({ runDir: options.runDir, built, crashInjector: options.crashInjector,
|
|
420
|
+
controlEventsPath: options.controlEventsPath ?? null });
|
|
421
|
+
});
|
|
413
422
|
}
|
|
414
423
|
|
|
415
424
|
export async function recoverRuntimeGateCommit(options) {
|
|
@@ -1011,8 +1011,33 @@ async function activateManagedSupervisorController({ repoRoot, runDir, runId, ad
|
|
|
1011
1011
|
fail('ADAPTER_LAUNCH_INVALID', 'existing endpoint ownerが一意でない');
|
|
1012
1012
|
}
|
|
1013
1013
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1014
|
+
// socket fileの出現はbindの証拠でありlistenの証拠ではない。負荷下ではcontrollerが
|
|
1015
|
+
// bindとlistenの間でpreemptされ、fileを見て接続した一回だけECONNREFUSEDになる
|
|
1016
|
+
// (2026-08-22 CI負荷下で実被弾)。起動したchildに限り、listen受理までを起動待ちに
|
|
1017
|
+
// 含めてdeadline内はECONNREFUSEDだけを待ち直す。死んだcontrollerはexitCode検査が、
|
|
1018
|
+
// 起動しないcontrollerはdeadlineが従来どおり落とす。既存endpoint(child無し)は
|
|
1019
|
+
// owner検証済みのlistener前提なので単発のまま。
|
|
1020
|
+
const handshakeDeadline = Date.now() + timeoutMs;
|
|
1021
|
+
let handshake;
|
|
1022
|
+
for (;;) {
|
|
1023
|
+
try {
|
|
1024
|
+
handshake = await exchangeControllerHandshake({ socketPath: handshakeConnectPath, runId,
|
|
1025
|
+
supervisorSessionNonce, controllerSocketRef, timeoutMs });
|
|
1026
|
+
break;
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
const listenPending = child !== null && error instanceof ManagedRuntimeError
|
|
1029
|
+
&& ['ECONNREFUSED', 'ENOENT'].some((code) => String(error.message).includes(code))
|
|
1030
|
+
&& Date.now() < handshakeDeadline;
|
|
1031
|
+
if (!listenPending) throw error;
|
|
1032
|
+
if (child.exitCode !== null) {
|
|
1033
|
+
fail(
|
|
1034
|
+
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
1035
|
+
`controller exited: ${child.exitCode}${childStderr ? `: ${childStderr.trim()}` : ''}`,
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1016
1041
|
const controllerDescriptor = handshake.descriptor;
|
|
1017
1042
|
if (controllerDescriptor.adapter_kind !== adapterKind || controllerDescriptor.socket_ref !== controllerSocketRef
|
|
1018
1043
|
|| controllerDescriptor.capabilities.capabilities_digest !== launch.capabilities_digest
|
|
@@ -490,18 +490,11 @@ export async function activateEpochOneStore({
|
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
control_event_schema: 'lattice.runtime_control_event.v1',
|
|
499
|
-
epoch_bundle_schema: 'lattice.runtime_epoch_bundle.v1',
|
|
500
|
-
created_plan_digest: compileArtifact.plan.plan_digest,
|
|
501
|
-
};
|
|
502
|
-
meta.meta_digest = digestArtifact(meta);
|
|
503
|
-
await replaceDurableJson(runDir, 'run-meta.json', meta);
|
|
504
|
-
|
|
493
|
+
// pointerをmetaより先に書く。読み手はmeta v2を見てからpointerを読むので、逆順だと
|
|
494
|
+
// 2書き込みの間にmeta v2だけが見え、並行するreaderがINVALID_RUN_STORE
|
|
495
|
+
// (committed epoch pointerを読めない)で落ちる(2026-08-22 CI負荷下で実被弾)。
|
|
496
|
+
// この順ならmeta v2の可視がpointer実在を含意し、途中crashはmeta v1のまま
|
|
497
|
+
// (余剰のpointerはlegacy読取りに影響せず、再activationが置換する)。
|
|
505
498
|
const pointer = {
|
|
506
499
|
schema: 'lattice.committed_epoch_pointer.v1',
|
|
507
500
|
run_id: normalizedMeta.run_id,
|
|
@@ -513,6 +506,18 @@ export async function activateEpochOneStore({
|
|
|
513
506
|
};
|
|
514
507
|
pointer.pointer_digest = digestArtifact(pointer);
|
|
515
508
|
await replaceDurableJson(runDir, 'committed-epoch.json', pointer);
|
|
509
|
+
|
|
510
|
+
const meta = {
|
|
511
|
+
schema: 'lattice.run_meta.v2',
|
|
512
|
+
run_id: normalizedMeta.run_id,
|
|
513
|
+
executor_adapter: normalizedMeta.executor_adapter,
|
|
514
|
+
run_event_schema: 'lattice.run_event.v1',
|
|
515
|
+
control_event_schema: 'lattice.runtime_control_event.v1',
|
|
516
|
+
epoch_bundle_schema: 'lattice.runtime_epoch_bundle.v1',
|
|
517
|
+
created_plan_digest: compileArtifact.plan.plan_digest,
|
|
518
|
+
};
|
|
519
|
+
meta.meta_digest = digestArtifact(meta);
|
|
520
|
+
await replaceDurableJson(runDir, 'run-meta.json', meta);
|
|
516
521
|
return { bundle, meta, pointer };
|
|
517
522
|
}
|
|
518
523
|
|
package/src/todo-store.mjs
CHANGED
|
@@ -486,6 +486,11 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
486
486
|
const doneDigest = new Map();
|
|
487
487
|
const completion = new Map();
|
|
488
488
|
const authoredStartBindings = new Map();
|
|
489
|
+
// hard証拠検証は即時でなくreplay完了後、まだ現在状態を支えている記述子だけへ行う。
|
|
490
|
+
// reopen・再doneで差し替えられた古いdoneが指すblobは一度もcommitされないままGCで消え得るし、
|
|
491
|
+
// 履歴の完全性はevent_digest鎖が既に持つ。即時検証だと消えたblobを指す過去eventが
|
|
492
|
+
// storeを恒久的に書き込み不能へ落とす(2026-08-22実被弾: reopen済み旧doneのdangling blob)。
|
|
493
|
+
const pendingVerifications = new Map();
|
|
489
494
|
const importedGenesis = events[0]?.payload.historical_import === true;
|
|
490
495
|
let previousTime = null;
|
|
491
496
|
for (const event of events) {
|
|
@@ -528,13 +533,15 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
528
533
|
const state = states.get(migration.to_task_id);
|
|
529
534
|
Object.assign(state, structuredClone(migration.state), { evidence_unverified: false });
|
|
530
535
|
if (state.evidence !== null) {
|
|
536
|
+
const evidence = state.evidence;
|
|
537
|
+
const context = { plan_key: plan.plan_key, task_id: migration.to_task_id };
|
|
531
538
|
if (state.imported) {
|
|
532
|
-
if (verifyImportSource)
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
} else if (verifyEvidence)
|
|
536
|
-
|
|
537
|
-
}
|
|
539
|
+
if (verifyImportSource) {
|
|
540
|
+
pendingVerifications.set(`task:${migration.to_task_id}`, () => verifyImportSource(evidence, context));
|
|
541
|
+
}
|
|
542
|
+
} else if (verifyEvidence) {
|
|
543
|
+
pendingVerifications.set(`task:${migration.to_task_id}`, () => verifyEvidence(evidence, context));
|
|
544
|
+
}
|
|
538
545
|
}
|
|
539
546
|
if (state.status === 'done') {
|
|
540
547
|
doneDigest.set(migration.to_task_id, event.event_digest);
|
|
@@ -571,6 +578,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
571
578
|
if (currentStatus !== 'gate_ready') fail('STORE_INCONSISTENT', 'phase_gate_not_ready');
|
|
572
579
|
state.status = 'reviewing'; state.review_event_digest = event.event_digest;
|
|
573
580
|
state.decision_event_digest = null; state.decision_evidence = null;
|
|
581
|
+
pendingVerifications.delete(`phase:${event.phase_id}`);
|
|
574
582
|
} else if (event.kind === 'phase_accept') {
|
|
575
583
|
const phase = phasesOf(plan).find(({ phase_id }) => phase_id === event.phase_id);
|
|
576
584
|
const slots = event.payload.evidence_slots.map(({ slot_id }) => slot_id);
|
|
@@ -579,8 +587,12 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
579
587
|
fail('STORE_INCONSISTENT', 'phase_accept_binding_invalid');
|
|
580
588
|
}
|
|
581
589
|
if (verifyEvidence) {
|
|
582
|
-
|
|
583
|
-
|
|
590
|
+
const decisionEvidence = event.payload.decision_evidence;
|
|
591
|
+
const slotEvidence = event.payload.evidence_slots.map((slot) => slot.evidence);
|
|
592
|
+
pendingVerifications.set(`phase:${event.phase_id}`, () => {
|
|
593
|
+
verifyEvidence(decisionEvidence);
|
|
594
|
+
for (const evidence of slotEvidence) verifyEvidence(evidence);
|
|
595
|
+
});
|
|
584
596
|
}
|
|
585
597
|
state.status = 'accepted'; state.decision_event_digest = event.event_digest;
|
|
586
598
|
state.decision_evidence = event.payload.decision_evidence;
|
|
@@ -588,7 +600,10 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
588
600
|
if (currentStatus !== 'reviewing' || state.review_event_digest !== event.payload.review_event_digest) {
|
|
589
601
|
fail('STORE_INCONSISTENT', 'phase_reject_binding_invalid');
|
|
590
602
|
}
|
|
591
|
-
if (verifyEvidence)
|
|
603
|
+
if (verifyEvidence) {
|
|
604
|
+
const decisionEvidence = event.payload.decision_evidence;
|
|
605
|
+
pendingVerifications.set(`phase:${event.phase_id}`, () => verifyEvidence(decisionEvidence));
|
|
606
|
+
}
|
|
592
607
|
state.status = 'rejected'; state.decision_event_digest = event.event_digest;
|
|
593
608
|
state.decision_evidence = event.payload.decision_evidence;
|
|
594
609
|
} else if (event.kind === 'phase_close_unaudited') {
|
|
@@ -597,6 +612,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
597
612
|
if (currentStatus !== 'gate_ready') fail('STORE_INCONSISTENT', 'phase_gate_not_ready');
|
|
598
613
|
state.status = 'closed_unaudited'; state.decision_event_digest = event.event_digest;
|
|
599
614
|
state.decision_evidence = null;
|
|
615
|
+
pendingVerifications.delete(`phase:${event.phase_id}`);
|
|
600
616
|
} else {
|
|
601
617
|
// phase_reopen。ADR 0148裁定5: closed_unauditedもaccepted/rejectedと同じく
|
|
602
618
|
// reopenで初期状態へ戻せる——監査せずに閉じた工程を、後から本当に監査したくなった時に
|
|
@@ -624,6 +640,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
624
640
|
fail('STORE_INCONSISTENT', 'phase_reopen_has_started_successor');
|
|
625
641
|
}
|
|
626
642
|
Object.assign(state, emptyPhaseState(event.phase_id));
|
|
643
|
+
pendingVerifications.delete(`phase:${event.phase_id}`);
|
|
627
644
|
}
|
|
628
645
|
continue;
|
|
629
646
|
}
|
|
@@ -635,9 +652,12 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
635
652
|
if (!importedGenesis || state.status !== 'pending') {
|
|
636
653
|
fail('STORE_INCONSISTENT', 'invalid_historical_import_start_transition');
|
|
637
654
|
}
|
|
638
|
-
if (verifyImportSource)
|
|
639
|
-
|
|
640
|
-
|
|
655
|
+
if (verifyImportSource) {
|
|
656
|
+
const evidence = event.payload.evidence;
|
|
657
|
+
pendingVerifications.set(`task:${event.task_id}`, () => verifyImportSource(evidence, {
|
|
658
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
641
661
|
state.status = 'in-progress';
|
|
642
662
|
state.started_at = event.payload.started_at === 'unknown_requires_evidence'
|
|
643
663
|
? null : event.payload.started_at;
|
|
@@ -686,18 +706,24 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
686
706
|
} else if (event.kind === 'done') {
|
|
687
707
|
if (event.payload.done_mode === 'authored') {
|
|
688
708
|
if (state.status !== 'in-progress' || !dependenciesDone) fail('STORE_INCONSISTENT', 'invalid_done_transition');
|
|
689
|
-
if (verifyEvidence)
|
|
690
|
-
|
|
691
|
-
|
|
709
|
+
if (verifyEvidence) {
|
|
710
|
+
const evidence = event.payload.evidence;
|
|
711
|
+
pendingVerifications.set(`task:${event.task_id}`, () => verifyEvidence(evidence, {
|
|
712
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
692
715
|
state.status = 'done'; state.done_at = event.recorded_at; state.evidence = event.payload.evidence;
|
|
693
716
|
state.test_result = event.payload.test_result ?? null;
|
|
694
717
|
state.imported = false;
|
|
695
718
|
completion.set(event.task_id, { mode: 'authored', completed_at: event.recorded_at });
|
|
696
719
|
} else if (event.payload.done_mode === 'historical_import') {
|
|
697
720
|
if (!importedGenesis || state.status !== 'pending') fail('STORE_INCONSISTENT', 'invalid_historical_import_transition');
|
|
698
|
-
if (verifyImportSource)
|
|
699
|
-
|
|
700
|
-
|
|
721
|
+
if (verifyImportSource) {
|
|
722
|
+
const evidence = event.payload.evidence;
|
|
723
|
+
pendingVerifications.set(`task:${event.task_id}`, () => verifyImportSource(evidence, {
|
|
724
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
701
727
|
state.status = 'done'; state.done_at = event.payload.completed_at === 'unknown_requires_evidence'
|
|
702
728
|
? null : event.payload.completed_at;
|
|
703
729
|
state.evidence = event.payload.evidence; state.imported = true;
|
|
@@ -709,9 +735,12 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
709
735
|
|| doneDigest.get(event.task_id) !== event.payload.target_done_digest) {
|
|
710
736
|
fail('STORE_INCONSISTENT', 'invalid_evidence_promotion');
|
|
711
737
|
}
|
|
712
|
-
if (verifyEvidence)
|
|
713
|
-
|
|
714
|
-
|
|
738
|
+
if (verifyEvidence) {
|
|
739
|
+
const evidence = event.payload.evidence;
|
|
740
|
+
pendingVerifications.set(`task:${event.task_id}`, () => verifyEvidence(evidence, {
|
|
741
|
+
plan_key: plan.plan_key, task_id: event.task_id,
|
|
742
|
+
}));
|
|
743
|
+
}
|
|
715
744
|
state.evidence = event.payload.evidence;
|
|
716
745
|
completion.set(event.task_id, { mode: 'evidence_promotion', completed_at: current.completed_at });
|
|
717
746
|
}
|
|
@@ -739,8 +768,10 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
739
768
|
if (startedSuccessor && event.payload.override_reason === null) fail('STORE_INCONSISTENT', 'reopen_has_started_successor');
|
|
740
769
|
state.status = 'in-progress'; state.done_at = null; state.evidence = null; state.test_result = null;
|
|
741
770
|
completion.delete(event.task_id);
|
|
771
|
+
pendingVerifications.delete(`task:${event.task_id}`);
|
|
742
772
|
}
|
|
743
773
|
}
|
|
774
|
+
for (const verify of pendingVerifications.values()) verify();
|
|
744
775
|
return [...states.values()].sort((left, right) => left.task_id < right.task_id ? -1 : left.task_id > right.task_id ? 1 : 0);
|
|
745
776
|
}
|
|
746
777
|
|