@quolu/lattice 0.46.2 → 0.47.0
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 +1 -1
- package/src/artifact-contracts.mjs +0 -3
- package/src/project-cli.mjs +12 -2
- package/src/rc3-scripted-campaign.mjs +15 -16
- package/src/runtime-cli.mjs +230 -146
- package/src/runtime-contracts.mjs +9 -5
- package/src/runtime-decision-verifier.mjs +26 -5
- package/src/runtime-engine.mjs +48 -5
- package/src/runtime-front-end.mjs +34 -15
- package/src/runtime-hold-recompile.mjs +11 -5
- package/src/runtime-managed-supervisor.mjs +54 -15
- package/src/runtime-scripted-adapter-controller.mjs +43 -19
- package/src/runtime-seam-resolve.mjs +4 -2
- package/src/seam-apply.mjs +3 -3
- package/src/seam-verification.mjs +38 -2
- package/src/todo-audit-pending.mjs +91 -0
- package/src/todo-cli.mjs +2 -0
- package/src/todo-dashboard-registry.mjs +2 -2
- package/src/todo-gantt-html-style.mjs +6 -2
- package/src/todo-gantt-html.mjs +30 -2
- package/src/todo-gantt-scope.mjs +9 -5
- package/src/todo-independence-contracts.mjs +10 -6
- package/src/todo-independence-guidance.mjs +2 -28
- package/src/todo-status.mjs +85 -9
- package/src/todo-store.mjs +9 -0
- package/src/witness-scaffold.mjs +13 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
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",
|
|
@@ -111,9 +111,6 @@ function inspectArray(value, state, depth) {
|
|
|
111
111
|
if (Object.getPrototypeOf(value) !== Array.prototype) {
|
|
112
112
|
invalidArtifact('array prototypeが不正');
|
|
113
113
|
}
|
|
114
|
-
if (value.length > MAX_COLLECTION_ITEMS) {
|
|
115
|
-
invalidArtifact('array item数が上限を超えている');
|
|
116
|
-
}
|
|
117
114
|
|
|
118
115
|
const ownKeys = Reflect.ownKeys(value);
|
|
119
116
|
if (ownKeys.some((key) => typeof key !== 'string')) {
|
package/src/project-cli.mjs
CHANGED
|
@@ -275,7 +275,17 @@ async function resolveProjectState({ cwd, cliVersion }) {
|
|
|
275
275
|
: todo.next_ready.length > 0
|
|
276
276
|
? { command: `lattice todo start --plan ${todo.next_ready[0].plan_key} --task ${todo.next_ready[0].task_id}${todo.next_ready.length > 1 ? ' --parallel-frontier' : ''}`,
|
|
277
277
|
reason: todo.next_ready.length > 1 ? 'parallel_frontier_present' : 'next_ready_present' }
|
|
278
|
-
|
|
278
|
+
// 監査待ちが在る限り「残作業なし」と答えない。優先順位はactive_run > next_ready >
|
|
279
|
+
// audit_pending > なしで、ready frontierが在る間はADR 0063の並列開始コマンドが勝つ
|
|
280
|
+
// ——ここを監査で上書きするとdispatchが再直列化する。監査待ちはtodo statusの
|
|
281
|
+
// audit_pending欄に常在するので、順位を下げても消えない。
|
|
282
|
+
// commandはverbatim実行可能で読み取り専用のものにする。`phase review`は
|
|
283
|
+
// `--reason <text>`のplaceholderを含みjournalを書き換えるので置かない
|
|
284
|
+
// (`todo phase status`の結果には既に両分岐のguidanceが載っている)。
|
|
285
|
+
: todo.audit_pending.length > 0
|
|
286
|
+
? { command: `lattice todo phase status --plan ${todo.audit_pending[0].plan_key}`,
|
|
287
|
+
reason: 'audit_pending' }
|
|
288
|
+
: { command: 'lattice todo status', reason: 'no_ready_task' };
|
|
279
289
|
const result = statusResult({
|
|
280
290
|
cli: { available: true, version: cliVersion },
|
|
281
291
|
project: { root: repoRoot, git_head: gitHead(repoRoot), project_id: store.project_id },
|
|
@@ -334,7 +344,7 @@ export async function runSessionContext({ cwd, stdout, cliVersion }) {
|
|
|
334
344
|
schema: SESSION_CONTEXT_SCHEMA,
|
|
335
345
|
// project discoveryの答えをそのまま埋める。hostは既存の検証器を再利用できる。
|
|
336
346
|
status: state.result,
|
|
337
|
-
// todoは`
|
|
347
|
+
// todoは`lattice todo status`のresultそのもの。新しい意味論を発明しない。
|
|
338
348
|
todo: state.todo,
|
|
339
349
|
independence,
|
|
340
350
|
result_digest: '',
|
|
@@ -467,31 +467,30 @@ async function runScopeViolation({ scaffold }) {
|
|
|
467
467
|
runId, plan, events, packets, todoId: 'TA', detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
|
|
468
468
|
});
|
|
469
469
|
events = classified.events;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
});
|
|
473
|
-
events = held.events;
|
|
470
|
+
events = await driveToClose({ runId, plan, events, packets, manifests, adapter });
|
|
471
|
+
const state = projectRuntimeState({ events });
|
|
474
472
|
return {
|
|
475
473
|
request,
|
|
476
474
|
plan,
|
|
477
475
|
manifests,
|
|
478
|
-
holdDecision: held.holdDecision,
|
|
479
476
|
events,
|
|
480
477
|
record: conditionRecord({
|
|
481
478
|
condition: 'scope_violation',
|
|
482
|
-
//
|
|
483
|
-
//
|
|
479
|
+
// directory名はRC3 artifact互換のため維持する。現契約では単独の予測超過は
|
|
480
|
+
// conflictではなく、観測を残したまま有効なreceiptを受理する。
|
|
484
481
|
expected: {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
482
|
+
observation_kinds: ['prediction_excess'],
|
|
483
|
+
conflict_finding_kinds: [],
|
|
484
|
+
frozen: false,
|
|
485
|
+
accepted: ['TA', 'TB'],
|
|
486
|
+
closed: true,
|
|
489
487
|
},
|
|
490
488
|
actual: {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
489
|
+
observation_kinds: [...new Set(classified.observations.map(({ kind }) => kind))],
|
|
490
|
+
conflict_finding_kinds: [...new Set(classified.findings.map(({ kind }) => kind))],
|
|
491
|
+
frozen: state.freeze !== null,
|
|
492
|
+
accepted: state.accepted,
|
|
493
|
+
closed: state.closed,
|
|
495
494
|
},
|
|
496
495
|
events,
|
|
497
496
|
plan,
|
|
@@ -1334,7 +1333,7 @@ export async function runRc3ScriptedCampaign(options = {}) {
|
|
|
1334
1333
|
const clean = await timed('clean_parallel', () => runCleanParallel({ scaffold }));
|
|
1335
1334
|
const late = await timed('late_path_conflict', () => runLateConflict({ scaffold }));
|
|
1336
1335
|
// 条件名`scope_violation`はRC3 manifestのdirectory名として凍結されているので動かさない。
|
|
1337
|
-
//
|
|
1336
|
+
// 中では単独の予測超過を観測し、conflictへ昇格しない現契約を検証する。
|
|
1338
1337
|
const scope = await timed('scope_violation', () => runScopeViolation({ scaffold }));
|
|
1339
1338
|
const semantic = await timed('semantic_unknown', () => runSemanticUnknown({ scaffold }));
|
|
1340
1339
|
const stale = await timed('stale_receipt', () => runStaleReceipt({ scaffold }));
|
package/src/runtime-cli.mjs
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
import { verifyRunEventChain } from './runtime-event-store.mjs';
|
|
60
60
|
import { projectRuntimeState, projectRuntimeStatusOverlays } from './runtime-projection.mjs';
|
|
61
61
|
import {
|
|
62
|
+
affectedTodoIds,
|
|
62
63
|
decideHoldAndCarryOver,
|
|
63
64
|
recompileNextEpochPlan,
|
|
64
65
|
validateRuntimeRecompileRequest,
|
|
@@ -757,46 +758,6 @@ async function readScriptedControllerReceipt({
|
|
|
757
758
|
return receipt;
|
|
758
759
|
}
|
|
759
760
|
|
|
760
|
-
/**
|
|
761
|
-
* hold裁定をworker processへ反映する(請求項7・8の再開側)。
|
|
762
|
-
*
|
|
763
|
-
* **barrierは全workerを止める。** 静止の証明はrun全体に対して要るからである。hold裁定は
|
|
764
|
-
* 止めた相手を`hold_set`と`continue_set`へ分け、後者は「影響閉包の外なので作業を捨てない」
|
|
765
|
-
* と判定されたものである。
|
|
766
|
-
*
|
|
767
|
-
* **carry-overは「一度も止まらない」ではない。** rebindは静止を要求する
|
|
768
|
-
* (`write_enabled === false`)ので、holdの直後に再開すると後継epochへ束ね直せず
|
|
769
|
-
* `EPOCH_REBIND_INCOMPLETE`で落ちる。止まり、繋がれ、そこで動き出すのが正しい順序である。
|
|
770
|
-
* よってこの関数を呼ぶのはrebindが済んだ後だけとする。
|
|
771
|
-
*
|
|
772
|
-
* pidだけで再開しない。pidは再利用されるので、start identityを照合して記録と同じprocessで
|
|
773
|
-
* あることを確かめる——別のprocessをSIGCONTするのは、止めるのと同じくらい危険である。
|
|
774
|
-
*/
|
|
775
|
-
async function resumeContinuedWorkers({ events, todoIds }) {
|
|
776
|
-
if (!Array.isArray(todoIds) || todoIds.length === 0) return { resumed: [], skipped: [] };
|
|
777
|
-
const resumed = [];
|
|
778
|
-
const skipped = [];
|
|
779
|
-
for (const todoId of todoIds) {
|
|
780
|
-
const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
781
|
-
&& event.subject?.kind === 'todo' && event.subject.ref === todoId);
|
|
782
|
-
const binding = dispatch?.payload?.direct_os_observation_binding;
|
|
783
|
-
const pid = binding?.process_pid;
|
|
784
|
-
const recorded = binding?.process_start_identity?.identity_digest;
|
|
785
|
-
if (!Number.isSafeInteger(pid) || typeof recorded !== 'string') continue;
|
|
786
|
-
// pidは再利用される。記録と同じprocessであることを確かめてから再開する——
|
|
787
|
-
// 別のprocessをSIGCONTするのは、止めるのと同じくらい危険である。
|
|
788
|
-
const observed = await observeManagedProcessStartIdentity(pid).catch(() => null);
|
|
789
|
-
if (observed === null || observed.identity_digest !== recorded) {
|
|
790
|
-
skipped.push({ todo_id: todoId, reason: observed === null ? 'process不在' : 'start identity不一致' });
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
try { process.kill(pid, 'SIGCONT'); resumed.push(todoId); } catch {
|
|
794
|
-
skipped.push({ todo_id: todoId, reason: 'SIGCONT失敗' });
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
return { resumed, skipped };
|
|
798
|
-
}
|
|
799
|
-
|
|
800
761
|
/**
|
|
801
762
|
* runが起こしたworker processを、耐久記録から回収する。
|
|
802
763
|
*
|
|
@@ -831,7 +792,7 @@ async function reapRunWorkerProcesses({ events }) {
|
|
|
831
792
|
return { reaped, skipped };
|
|
832
793
|
}
|
|
833
794
|
|
|
834
|
-
async function
|
|
795
|
+
async function driveScriptedManagedEpoch({
|
|
835
796
|
runDir,
|
|
836
797
|
repoRoot,
|
|
837
798
|
request,
|
|
@@ -843,20 +804,22 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
843
804
|
sentinel = null,
|
|
844
805
|
preDispatchBindings = null,
|
|
845
806
|
drainEscalations = null,
|
|
807
|
+
escalateTerminalConflict = null,
|
|
808
|
+
preactivated = null,
|
|
846
809
|
}) {
|
|
847
810
|
let events = [...initialEvents];
|
|
848
811
|
const { plan, manifests, executor_packets: packets } = committed.bundle;
|
|
849
812
|
const controllerId = activation.controllerDescriptor.controller_id;
|
|
850
813
|
const registrationDigest = activation.registration.registration_digest;
|
|
851
814
|
const sessionNonceDigest = digestArtifact(activation.sessionNonce);
|
|
815
|
+
const preactivatedByPacket = new Map((preactivated?.armedLeases ?? []).map((lease) => [
|
|
816
|
+
lease.packet_digest,
|
|
817
|
+
lease,
|
|
818
|
+
]));
|
|
852
819
|
for (;;) {
|
|
853
820
|
const frontier = computeReadyFrontier({ plan, events }).dispatchable;
|
|
854
821
|
if (frontier.length === 0) break;
|
|
855
|
-
|
|
856
|
-
barrierId: `dispatch-${plan.plan_epoch}-${events.length}`,
|
|
857
|
-
reason: 'initial_scripted_dispatch',
|
|
858
|
-
frozenEventDigest: events.at(-1).event_digest,
|
|
859
|
-
});
|
|
822
|
+
const alreadyRunning = projectRuntimeState({ events }).running;
|
|
860
823
|
const issuedControlDigest = controlEvents().at(-1)?.event_digest;
|
|
861
824
|
if (typeof issuedControlDigest !== 'string') {
|
|
862
825
|
throw new ManagedRuntimeError(
|
|
@@ -879,47 +842,63 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
879
842
|
}
|
|
880
843
|
syncSentinelWatches({ sentinel, runningTodoIds: [...frontier],
|
|
881
844
|
rootOf: (todoId) => worktreeByTodo.get(todoId) });
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
845
|
+
let activated = frontier.every((todoId) => preactivatedByPacket.has(packets[todoId].packet_digest))
|
|
846
|
+
? { armedLeases: frontier.map((todoId) => {
|
|
847
|
+
const packetDigest = packets[todoId].packet_digest;
|
|
848
|
+
const lease = preactivatedByPacket.get(packetDigest);
|
|
849
|
+
preactivatedByPacket.delete(packetDigest);
|
|
850
|
+
return lease;
|
|
851
|
+
}) }
|
|
852
|
+
: null;
|
|
853
|
+
if (activated === null) {
|
|
854
|
+
await managedSupervisor.barrierAll({
|
|
855
|
+
barrierId: `dispatch-${plan.plan_epoch}-${events.length}`,
|
|
856
|
+
reason: 'scripted_dispatch',
|
|
857
|
+
frozenEventDigest: events.at(-1).event_digest,
|
|
858
|
+
});
|
|
859
|
+
const stagedLeases = [];
|
|
860
|
+
for (const todoId of frontier) {
|
|
861
|
+
const packet = packets[todoId];
|
|
862
|
+
const staged = {
|
|
863
|
+
schema: 'lattice.runtime_write_lease.v1',
|
|
864
|
+
lease_id: `lease-${packet.packet_digest.slice(0, 24)}`,
|
|
865
|
+
run_id: request.request_id,
|
|
866
|
+
todo_id: todoId,
|
|
867
|
+
plan_epoch: plan.plan_epoch,
|
|
868
|
+
packet_digest: packet.packet_digest,
|
|
869
|
+
controller_registration_digest: registrationDigest,
|
|
870
|
+
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
871
|
+
state: 'staged',
|
|
872
|
+
ttl_ms: 60_000,
|
|
873
|
+
issued_control_digest: issuedControlDigest,
|
|
874
|
+
lease_digest: '',
|
|
875
|
+
};
|
|
876
|
+
staged.lease_digest = selfDigest(staged, 'lease_digest');
|
|
877
|
+
await managedSupervisor.prepareController({
|
|
878
|
+
controllerId,
|
|
879
|
+
executorPacket: packet,
|
|
880
|
+
stagedLease: staged,
|
|
881
|
+
});
|
|
882
|
+
stagedLeases.push(staged);
|
|
883
|
+
}
|
|
884
|
+
const activationDigest = digestArtifact({
|
|
885
|
+
schema: 'lattice.scripted_activation.v1',
|
|
886
|
+
committed_epoch_pointer_digest: committed.pointer.pointer_digest,
|
|
887
|
+
staged_lease_digests: stagedLeases.map((lease) => lease.lease_digest).sort(),
|
|
888
|
+
});
|
|
889
|
+
activated = await managedSupervisor.commitWriteGate({
|
|
890
|
+
planEpoch: plan.plan_epoch,
|
|
891
|
+
committedEpochDigest: committed.pointer.pointer_digest,
|
|
892
|
+
activationDigest,
|
|
893
|
+
commitReleaseBarrier: (barrier) => commitReleaseEpochBarrier({ runDir, barrier }),
|
|
894
|
+
committedAt: canonicalNow(),
|
|
904
895
|
});
|
|
905
|
-
stagedLeases.push(staged);
|
|
906
896
|
}
|
|
907
|
-
const activationDigest = digestArtifact({
|
|
908
|
-
schema: 'lattice.initial_scripted_activation.v1',
|
|
909
|
-
committed_epoch_pointer_digest: committed.pointer.pointer_digest,
|
|
910
|
-
staged_lease_digests: stagedLeases.map((lease) => lease.lease_digest).sort(),
|
|
911
|
-
});
|
|
912
|
-
const activated = await managedSupervisor.commitWriteGate({
|
|
913
|
-
planEpoch: plan.plan_epoch,
|
|
914
|
-
committedEpochDigest: committed.pointer.pointer_digest,
|
|
915
|
-
activationDigest,
|
|
916
|
-
commitReleaseBarrier: (barrier) => commitReleaseEpochBarrier({ runDir, barrier }),
|
|
917
|
-
committedAt: canonicalNow(),
|
|
918
|
-
});
|
|
919
897
|
const armedByPacket = new Map(activated.armedLeases.map((lease) => [
|
|
920
898
|
lease.packet_digest,
|
|
921
899
|
lease,
|
|
922
900
|
]));
|
|
901
|
+
const terminalCheckpointByHandle = new Map();
|
|
923
902
|
const managedAdapter = {
|
|
924
903
|
async dispatch({ packet }) {
|
|
925
904
|
const lease = armedByPacket.get(packet.packet_digest);
|
|
@@ -992,6 +971,17 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
992
971
|
controllerId,
|
|
993
972
|
payloadDigest: response.observation.payload_digest,
|
|
994
973
|
});
|
|
974
|
+
const binding = dispatch.payload?.direct_os_observation_binding;
|
|
975
|
+
if (typeof binding?.worktree_path !== 'string' || typeof binding?.base_sha !== 'string') {
|
|
976
|
+
throw new ManagedRuntimeError('ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
977
|
+
`terminal diffのorigin bindingが無い: ${dispatch.subject.ref}`);
|
|
978
|
+
}
|
|
979
|
+
const checkpoint = await captureWorktreeDiff({
|
|
980
|
+
worktreePath: binding.worktree_path, baseSha: binding.base_sha,
|
|
981
|
+
});
|
|
982
|
+
// receiptのcheckpointはexecutor申告、これはsupervisorの独立観測でdigest schemaも異なる。
|
|
983
|
+
// 同じeventへ混ぜず、receipt記録後に別checkpointとして追記する。
|
|
984
|
+
terminalCheckpointByHandle.set(executorHandle, checkpoint);
|
|
995
985
|
return { state: 'terminal', receipt };
|
|
996
986
|
},
|
|
997
987
|
};
|
|
@@ -1021,7 +1011,8 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
1021
1011
|
// **workerが走っている間だけが、実行時競合を掴める窓である。** 観測を1件ずつ待たずに
|
|
1022
1012
|
// 回し、その合間に早期警報のescalationを捌く。捌くのは`replaceEventsAtomically`の
|
|
1023
1013
|
// 直後だけ——そこでしかdiskとメモリのeventsが一致していない。
|
|
1024
|
-
const awaiting = new Set(dispatched.dispatched);
|
|
1014
|
+
const awaiting = new Set([...dispatched.dispatched, ...alreadyRunning]);
|
|
1015
|
+
const completedReceiptIds = new Set();
|
|
1025
1016
|
const observeDeadline = Date.now() + SCRIPTED_OBSERVE_TIMEOUT_MS;
|
|
1026
1017
|
let frozen = false;
|
|
1027
1018
|
while (awaiting.size > 0) {
|
|
@@ -1042,6 +1033,74 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
1042
1033
|
});
|
|
1043
1034
|
if (observed.observation.state === 'running') continue;
|
|
1044
1035
|
events = observed.events;
|
|
1036
|
+
if (observed.observation.state === 'terminal') {
|
|
1037
|
+
const dispatchForTerminal = events.findLast((event) => (
|
|
1038
|
+
event.kind === 'executor_dispatched'
|
|
1039
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId
|
|
1040
|
+
));
|
|
1041
|
+
const terminalCheckpoint = terminalCheckpointByHandle
|
|
1042
|
+
.get(dispatchForTerminal.payload.executor_handle);
|
|
1043
|
+
if (terminalCheckpoint === undefined) {
|
|
1044
|
+
throw new ManagedRuntimeError('ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
1045
|
+
`terminal diffが保存されていない: ${todoId}`);
|
|
1046
|
+
}
|
|
1047
|
+
terminalCheckpointByHandle.delete(dispatchForTerminal.payload.executor_handle);
|
|
1048
|
+
events.push(buildNextRunEvent({
|
|
1049
|
+
events,
|
|
1050
|
+
runId: request.request_id,
|
|
1051
|
+
kind: 'checkpoint_observed',
|
|
1052
|
+
planEpoch: plan.plan_epoch,
|
|
1053
|
+
subject: { kind: 'todo', ref: todoId },
|
|
1054
|
+
payload: { ...terminalCheckpoint, observed_by: 'supervisor_terminal' },
|
|
1055
|
+
recordedAt: canonicalNow(),
|
|
1056
|
+
}));
|
|
1057
|
+
const recordedReceipt = events.findLast((event) => event.kind === 'receipt_recorded'
|
|
1058
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId);
|
|
1059
|
+
completedReceiptIds.add(recordedReceipt.payload.receipt_id);
|
|
1060
|
+
const terminalEvent = events.findLast((event) => event.kind === 'executor_terminal'
|
|
1061
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId);
|
|
1062
|
+
const currentDispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
1063
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId
|
|
1064
|
+
&& event.sequence < terminalEvent.sequence);
|
|
1065
|
+
const overlappingTodoIds = plan.nodes.map((node) => node.todo_id)
|
|
1066
|
+
.filter((otherId) => {
|
|
1067
|
+
if (otherId === todoId) return false;
|
|
1068
|
+
const otherDispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
1069
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === otherId
|
|
1070
|
+
&& event.sequence < terminalEvent.sequence);
|
|
1071
|
+
if (otherDispatch === undefined) return false;
|
|
1072
|
+
const otherTerminal = events.find((event) => event.kind === 'executor_terminal'
|
|
1073
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === otherId
|
|
1074
|
+
&& event.sequence > otherDispatch.sequence);
|
|
1075
|
+
return otherTerminal === undefined || otherTerminal.sequence > currentDispatch.sequence;
|
|
1076
|
+
});
|
|
1077
|
+
const relevantTodoIds = [todoId, ...overlappingTodoIds];
|
|
1078
|
+
const terminalCheckpointEvent = events.findLast((event) => event.kind === 'checkpoint_observed'
|
|
1079
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId
|
|
1080
|
+
&& event.payload?.observed_by === 'supervisor_terminal');
|
|
1081
|
+
const observations = [{ todo_id: todoId,
|
|
1082
|
+
paths: terminalCheckpointEvent.payload.diff.entries.map((entry) => entry.path) }];
|
|
1083
|
+
for (const peerId of overlappingTodoIds) {
|
|
1084
|
+
const peerCheckpoint = events.findLast((event) => event.kind === 'checkpoint_observed'
|
|
1085
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === peerId
|
|
1086
|
+
&& event.payload?.observed_by === 'supervisor_terminal');
|
|
1087
|
+
if (peerCheckpoint !== undefined) observations.push({ todo_id: peerId,
|
|
1088
|
+
paths: peerCheckpoint.payload.diff.entries.map((entry) => entry.path) });
|
|
1089
|
+
}
|
|
1090
|
+
const actionable = classifyObservedDiff({ plan, manifests, observations,
|
|
1091
|
+
relevantTodoIds }).findings.filter((finding) => finding.kind === 'observed_write_conflict'
|
|
1092
|
+
&& finding.todo_ids.includes(todoId));
|
|
1093
|
+
if (actionable.length > 0) {
|
|
1094
|
+
if (typeof escalateTerminalConflict !== 'function') {
|
|
1095
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED',
|
|
1096
|
+
'terminal conflictのmanaged escalation経路が無い');
|
|
1097
|
+
}
|
|
1098
|
+
await replaceEventsAtomically(runDir, events);
|
|
1099
|
+
events = await escalateTerminalConflict({ finding: actionable[0],
|
|
1100
|
+
checkpointDigest: terminalCheckpointEvent.payload.checkpoint_digest });
|
|
1101
|
+
frozen = true;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1045
1104
|
await replaceEventsAtomically(runDir, events);
|
|
1046
1105
|
syncSentinelWatches({ sentinel, runningTodoIds: projectRuntimeState({ events }).running,
|
|
1047
1106
|
rootOf: (id) => events.findLast((event) => event.kind === 'executor_dispatched'
|
|
@@ -1049,7 +1108,9 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
1049
1108
|
?.payload?.direct_os_observation_binding?.worktree_path });
|
|
1050
1109
|
awaiting.delete(todoId);
|
|
1051
1110
|
progressed = true;
|
|
1111
|
+
if (frozen) break;
|
|
1052
1112
|
}
|
|
1113
|
+
if (frozen) break;
|
|
1053
1114
|
if (awaiting.size === 0) break;
|
|
1054
1115
|
if (Date.now() >= observeDeadline) {
|
|
1055
1116
|
throw new ManagedRuntimeError(
|
|
@@ -1061,8 +1122,8 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
1061
1122
|
await new Promise((resolve) => { setTimeout(resolve, SCRIPTED_OBSERVE_POLL_MS); });
|
|
1062
1123
|
}
|
|
1063
1124
|
}
|
|
1064
|
-
// holdが掛かったらdispatch
|
|
1065
|
-
//
|
|
1125
|
+
// holdが掛かったらdispatchを進めない。記録済みreceiptは失わず、対象群の再compile後に
|
|
1126
|
+
// origin bindingとcarry witnessを満たすものだけを後続epochで裁定する。
|
|
1066
1127
|
if (frozen) return events;
|
|
1067
1128
|
const adjudicated = adjudicatePendingReceipts({
|
|
1068
1129
|
runId: request.request_id,
|
|
@@ -1070,10 +1131,14 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
1070
1131
|
events,
|
|
1071
1132
|
recordedAt: canonicalNow(),
|
|
1072
1133
|
});
|
|
1073
|
-
|
|
1134
|
+
const rejectedCompleted = adjudicated.decisions.filter((decision) => (
|
|
1135
|
+
completedReceiptIds.has(decision.receipt_id) && decision.decision !== 'accepted'
|
|
1136
|
+
));
|
|
1137
|
+
if (rejectedCompleted.length > 0) {
|
|
1074
1138
|
throw new ManagedRuntimeError(
|
|
1075
1139
|
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
1076
|
-
|
|
1140
|
+
`scripted controller receiptが受理されなかった: ${rejectedCompleted
|
|
1141
|
+
.map((decision) => `${decision.receipt_id}:${decision.detail}`).join(',')}`,
|
|
1077
1142
|
);
|
|
1078
1143
|
}
|
|
1079
1144
|
events = adjudicated.events;
|
|
@@ -2227,6 +2292,49 @@ export async function runManagedSupervisorDaemon({
|
|
|
2227
2292
|
return current;
|
|
2228
2293
|
};
|
|
2229
2294
|
|
|
2295
|
+
const escalateTerminalConflict = async ({ finding, checkpointDigest }) => {
|
|
2296
|
+
const activeEpoch = await readCommittedEpochStore(runDir);
|
|
2297
|
+
const candidate = {
|
|
2298
|
+
schema: 'lattice.runtime_finding_candidate.v1',
|
|
2299
|
+
proposed_kind: finding.kind,
|
|
2300
|
+
todo_ids: [...finding.todo_ids].sort(),
|
|
2301
|
+
path: finding.path,
|
|
2302
|
+
resource_id: null,
|
|
2303
|
+
evidence_digests: [checkpointDigest],
|
|
2304
|
+
candidate_digest: '',
|
|
2305
|
+
};
|
|
2306
|
+
candidate.candidate_digest = selfDigest(candidate, 'candidate_digest');
|
|
2307
|
+
const submit = (operation, { artifact = null, artifactDigest = null,
|
|
2308
|
+
checkpoint = null } = {}) => executeControl(createRuntimeControlRequest({
|
|
2309
|
+
requestId: `terminal-${randomUUID()}-${operation.replace(/_/gu, '-')}`,
|
|
2310
|
+
runId: request.request_id, operation, sessionNonce,
|
|
2311
|
+
payload: controlOperationPayload({ operation, runRef: request.request_id,
|
|
2312
|
+
artifact, artifactDigest, checkpointDigest: checkpoint,
|
|
2313
|
+
expectedEpoch: activeEpoch.pointer.plan_epoch,
|
|
2314
|
+
expectedQueueDigest: null }),
|
|
2315
|
+
}));
|
|
2316
|
+
const recorded = await submit('finding_record', {
|
|
2317
|
+
artifact: candidate, artifactDigest: digestArtifact(candidate),
|
|
2318
|
+
checkpoint: checkpointDigest,
|
|
2319
|
+
});
|
|
2320
|
+
if (recorded.outcome !== 'completed') {
|
|
2321
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED',
|
|
2322
|
+
'terminal findingを耐久化できない');
|
|
2323
|
+
}
|
|
2324
|
+
const findingDigest = recorded.result.finding_digest;
|
|
2325
|
+
const conflicted = await submit('conflict', { artifactDigest: findingDigest });
|
|
2326
|
+
if (conflicted.outcome !== 'completed') {
|
|
2327
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED',
|
|
2328
|
+
'terminal findingをfreezeへ運べない');
|
|
2329
|
+
}
|
|
2330
|
+
const held = await submit('hold');
|
|
2331
|
+
if (held.outcome !== 'completed') {
|
|
2332
|
+
throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE',
|
|
2333
|
+
'terminal conflictの対象barrierが完了しない');
|
|
2334
|
+
}
|
|
2335
|
+
return readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2230
2338
|
/** 警報の処理を直列化する鎖。`onWarning`はここへ繋ぐだけにする。 */
|
|
2231
2339
|
let escalationChain = Promise.resolve();
|
|
2232
2340
|
|
|
@@ -2366,7 +2474,7 @@ export async function runManagedSupervisorDaemon({
|
|
|
2366
2474
|
});
|
|
2367
2475
|
epochDriveActive = true;
|
|
2368
2476
|
try {
|
|
2369
|
-
await
|
|
2477
|
+
await driveScriptedManagedEpoch({
|
|
2370
2478
|
runDir,
|
|
2371
2479
|
repoRoot,
|
|
2372
2480
|
request,
|
|
@@ -2378,6 +2486,7 @@ export async function runManagedSupervisorDaemon({
|
|
|
2378
2486
|
controlEvents: () => controlEvents,
|
|
2379
2487
|
preDispatchBindings,
|
|
2380
2488
|
drainEscalations: drainPendingEscalations,
|
|
2489
|
+
escalateTerminalConflict,
|
|
2381
2490
|
});
|
|
2382
2491
|
} finally {
|
|
2383
2492
|
epochDriveActive = false;
|
|
@@ -2488,10 +2597,11 @@ export async function runManagedSupervisorDaemon({
|
|
|
2488
2597
|
const verified = independentlyClassified.find((findingValue) => findingValue.kind === candidate.proposed_kind
|
|
2489
2598
|
&& findingValue.path === candidate.path
|
|
2490
2599
|
&& canonicalizeArtifact(findingValue.todo_ids) === canonicalizeArtifact(candidate.todo_ids));
|
|
2491
|
-
|
|
2600
|
+
const terminalSupervisorObservation = observed.payload?.observed_by === 'supervisor_terminal';
|
|
2601
|
+
if ((!terminalSupervisorObservation && match === undefined) || verified === undefined) throw new ManagedRuntimeError(
|
|
2492
2602
|
'FINDING_UNRESOLVED', 'path findingのproducer/verifier再導出が一致しない');
|
|
2493
|
-
derivedTodoIds = [...match.todo_ids];
|
|
2494
|
-
derivedKind = match.kind;
|
|
2603
|
+
derivedTodoIds = [...(match ?? verified).todo_ids];
|
|
2604
|
+
derivedKind = (match ?? verified).kind;
|
|
2495
2605
|
} else {
|
|
2496
2606
|
const match = independentlyClassified.find((findingValue) => findingValue.kind === candidate.proposed_kind
|
|
2497
2607
|
&& findingValue.resource_id === candidate.resource_id
|
|
@@ -2603,6 +2713,9 @@ export async function runManagedSupervisorDaemon({
|
|
|
2603
2713
|
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2604
2714
|
const conflict = events.findLast((event) => event.kind === 'conflict_found');
|
|
2605
2715
|
if (!conflict) throw new ManagedRuntimeError('FINDING_UNRESOLVED', '保存済みconflictなし');
|
|
2716
|
+
const active = await readCommittedEpochStore(runDir);
|
|
2717
|
+
const holdTodoIds = affectedTodoIds(active.bundle.plan, active.bundle.manifests,
|
|
2718
|
+
conflict.payload.todo_ids);
|
|
2606
2719
|
const holdBarrierId = `barrier-${randomUUID()}`;
|
|
2607
2720
|
const holdRecordedAt = canonicalNow();
|
|
2608
2721
|
await appendControl({ run_id: request.request_id, kind: 'hold_prepared',
|
|
@@ -2615,7 +2728,7 @@ export async function runManagedSupervisorDaemon({
|
|
|
2615
2728
|
const held = await managedSupervisor.holdConflict({
|
|
2616
2729
|
findingDigest: conflict.payload.finding_digest, frozenEventDigest: events.at(-1).event_digest,
|
|
2617
2730
|
barrierId: holdBarrierId, reason: conflict.payload.kind,
|
|
2618
|
-
recordedAt: holdRecordedAt,
|
|
2731
|
+
recordedAt: holdRecordedAt, todoIds: holdTodoIds,
|
|
2619
2732
|
});
|
|
2620
2733
|
controlEvents = await eventStore.readEvents();
|
|
2621
2734
|
const quiesced = controlEvents.filter((event) => event.kind === 'executor_quiesced'
|
|
@@ -2629,7 +2742,6 @@ export async function runManagedSupervisorDaemon({
|
|
|
2629
2742
|
barrier_evidence_digest: evidence.payload.evidence_digest },
|
|
2630
2743
|
recordedAt: holdRecordedAt }));
|
|
2631
2744
|
}
|
|
2632
|
-
const active = await readCommittedEpochStore(runDir);
|
|
2633
2745
|
const decided = decideHoldAndCarryOver({ runId: request.request_id,
|
|
2634
2746
|
request: active.bundle.request, plan: active.bundle.plan,
|
|
2635
2747
|
manifests: active.bundle.manifests, packets: active.bundle.executor_packets,
|
|
@@ -3018,39 +3130,11 @@ export async function runManagedSupervisorDaemon({
|
|
|
3018
3130
|
lease.lease_digest = selfDigest(lease, 'lease_digest');
|
|
3019
3131
|
return lease;
|
|
3020
3132
|
};
|
|
3021
|
-
const rebound = new Set(Object.keys(rebindPackets));
|
|
3022
|
-
const rebindEvidence = new Map();
|
|
3023
3133
|
const preparedSuccessors = new Set();
|
|
3024
|
-
for (const [todoId, packet] of Object.entries(rebindPackets)) {
|
|
3025
|
-
const owner = controllerForTodo(todoId);
|
|
3026
|
-
const dispatch = projectRuntimeState({ events }).dispatches[todoId];
|
|
3027
|
-
const evidence = await managedSupervisor.rebindController({ controllerId: owner.controllerDescriptor.controller_id, rebindPacket: packet,
|
|
3028
|
-
stagedLease: stagedLease(todoId, packet.packet_digest, owner), expected: { todo_id: todoId,
|
|
3029
|
-
executor_handle: dispatch.payload.executor_handle, worktree_id: dispatch.payload.worktree_id,
|
|
3030
|
-
predecessor_packet_digest: active.bundle.executor_packets[todoId].packet_digest,
|
|
3031
|
-
rebind_packet_digest: packet.packet_digest } });
|
|
3032
|
-
rebindEvidence.set(todoId, { ...evidence,
|
|
3033
|
-
controller_registration_digest: owner.registration.registration_digest });
|
|
3034
|
-
}
|
|
3035
|
-
// **ここが再開の位置である。** rebindは静止を要求する(`write_enabled === false`)ので、
|
|
3036
|
-
// holdの直後に再開すると後継epochへ束ね直せない。carry-overは「作業を捨てない」で
|
|
3037
|
-
// あって「一度も止まらない」ではない——barrierで止まり、rebindで新epochへ繋がれ、
|
|
3038
|
-
// そこで初めて動き出す。
|
|
3039
|
-
if (rebound.size > 0) {
|
|
3040
|
-
const resumed = await resumeContinuedWorkers({ events, todoIds: [...rebound] });
|
|
3041
|
-
if (resumed.resumed.length > 0 || resumed.skipped.length > 0) {
|
|
3042
|
-
await appendControl({ run_id: request.request_id, kind: 'workers_resumed',
|
|
3043
|
-
session_nonce_digest: digestArtifact(sessionNonce), payload: {
|
|
3044
|
-
resumed_todo_ids: [...resumed.resumed].sort(),
|
|
3045
|
-
skipped_count: resumed.skipped.length,
|
|
3046
|
-
} });
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
3134
|
for (const todoId of core.planDiff.redispatched) {
|
|
3050
3135
|
const successors = recompileRequest.task_migration.entries
|
|
3051
3136
|
.find((entry) => entry.predecessor_task_id === todoId)?.successor_task_ids ?? [];
|
|
3052
3137
|
for (const successorId of successors) {
|
|
3053
|
-
if (rebound.has(successorId)) continue;
|
|
3054
3138
|
const packet = executorPackets[successorId];
|
|
3055
3139
|
const owner = controllerForTodo(successorId);
|
|
3056
3140
|
if (packet !== undefined) await managedSupervisor.prepareController({
|
|
@@ -3064,36 +3148,13 @@ export async function runManagedSupervisorDaemon({
|
|
|
3064
3148
|
.filter((receipt) => receipt.accepted_sequence !== null
|
|
3065
3149
|
&& acceptedCheckpointDigests.has(receipt.payload?.checkpoint_digest))
|
|
3066
3150
|
.map((receipt) => receipt.todo_id));
|
|
3151
|
+
const carriedTodoIds = new Set(planDiff.carried_over);
|
|
3067
3152
|
const expectedLiveTodoIds = newPlan.nodes.map((node) => node.todo_id)
|
|
3068
|
-
.filter((todoId) => !acceptedTodoIds.has(todoId)).sort();
|
|
3069
|
-
const activatedTodoIds = [...
|
|
3153
|
+
.filter((todoId) => !acceptedTodoIds.has(todoId) && !carriedTodoIds.has(todoId)).sort();
|
|
3154
|
+
const activatedTodoIds = [...preparedSuccessors].sort();
|
|
3070
3155
|
if (canonicalizeArtifact(expectedLiveTodoIds) !== canonicalizeArtifact(activatedTodoIds)) {
|
|
3071
3156
|
throw new ManagedRuntimeError('EPOCH_REBIND_INCOMPLETE', 'successor live taskのrebind/prepare集合が完全でない');
|
|
3072
3157
|
}
|
|
3073
|
-
// 全direct ack後だけepoch_reboundをexact-once batchで保存する。
|
|
3074
|
-
const reboundRecordedAt = canonicalNow();
|
|
3075
|
-
const proposedReboundBatch = [];
|
|
3076
|
-
let proposedReboundEvents = [...events];
|
|
3077
|
-
for (const [todoId, packet] of Object.entries(rebindPackets)
|
|
3078
|
-
.sort(([left], [right]) => left.localeCompare(right))) {
|
|
3079
|
-
const event = buildNextRunEvent({ events: proposedReboundEvents,
|
|
3080
|
-
runId: active.meta.run_id, kind: 'epoch_rebound', planEpoch: nextEpoch,
|
|
3081
|
-
subject: { kind: 'todo', ref: todoId }, payload: { ...packet,
|
|
3082
|
-
rebind_ack_digest: rebindEvidence.get(todoId).ack.ack_digest,
|
|
3083
|
-
control_event_digest: rebindEvidence.get(todoId).control_event_digest,
|
|
3084
|
-
controller_registration_digest: rebindEvidence.get(todoId).controller_registration_digest },
|
|
3085
|
-
recordedAt: reboundRecordedAt });
|
|
3086
|
-
proposedReboundEvents.push(event);
|
|
3087
|
-
proposedReboundBatch.push(event);
|
|
3088
|
-
}
|
|
3089
|
-
events = await publishRuntimeEventBatch({ runDir,
|
|
3090
|
-
transactionId: recompileRequest.request_id, phase: 'epoch_rebound', planEpoch: nextEpoch,
|
|
3091
|
-
bindingDigest: digestArtifact({ successor_epoch: nextEpoch,
|
|
3092
|
-
successor_bundle_digest: staged.bundle_digest,
|
|
3093
|
-
ordered_rebind_packet_digests: Object.entries(rebindPackets)
|
|
3094
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
3095
|
-
.map(([, packet]) => packet.packet_digest) }),
|
|
3096
|
-
currentEvents: events, proposedBatch: proposedReboundBatch, crashInjector });
|
|
3097
3158
|
controlEvents = await eventStore.readEvents();
|
|
3098
3159
|
const committed = await commitStagedSuccessorEpoch({ runDir,
|
|
3099
3160
|
transactionId: staged.transaction_id,
|
|
@@ -3137,6 +3198,29 @@ export async function runManagedSupervisorDaemon({
|
|
|
3137
3198
|
request_id: controlRequest.request_id, gate_digest: activated.gate.gate_digest,
|
|
3138
3199
|
event_digest: events.at(-1).event_digest,
|
|
3139
3200
|
});
|
|
3201
|
+
if (recompileRequest.mode === 'intentional_serial'
|
|
3202
|
+
&& isDistributedScriptedControllerActivation(activation)) {
|
|
3203
|
+
epochDriveActive = true;
|
|
3204
|
+
try {
|
|
3205
|
+
events = await driveScriptedManagedEpoch({
|
|
3206
|
+
runDir,
|
|
3207
|
+
repoRoot,
|
|
3208
|
+
request: committed.bundle.request,
|
|
3209
|
+
committed,
|
|
3210
|
+
activation,
|
|
3211
|
+
managedSupervisor,
|
|
3212
|
+
initialEvents: events,
|
|
3213
|
+
controlEvents: () => controlEvents,
|
|
3214
|
+
sentinel,
|
|
3215
|
+
preDispatchBindings,
|
|
3216
|
+
drainEscalations: drainPendingEscalations,
|
|
3217
|
+
escalateTerminalConflict,
|
|
3218
|
+
preactivated: activated,
|
|
3219
|
+
});
|
|
3220
|
+
} finally {
|
|
3221
|
+
epochDriveActive = false;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3140
3224
|
const result = buildControlResult({ operation: 'recompile', outcome: 'recompiled',
|
|
3141
3225
|
eventHeadDigest: events.at(-1).event_digest,
|
|
3142
3226
|
controlHeadDigest: (await eventStore.readEvents()).at(-1).event_digest,
|