@quolu/lattice 0.46.2 → 0.48.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/bin/lattice-dashboard.mjs +2 -2
- package/package.json +1 -1
- package/src/artifact-contracts.mjs +0 -3
- package/src/cli-help.mjs +2 -2
- package/src/project-cli.mjs +19 -3
- 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 +111 -18
- package/src/todo-contracts.mjs +84 -14
- 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 +26 -29
- package/src/todo-note-store.mjs +148 -21
- package/src/todo-parallel-candidates.mjs +114 -0
- package/src/todo-status.mjs +254 -11
- package/src/todo-store.mjs +148 -2
- package/src/witness-scaffold.mjs +13 -32
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,
|
|
@@ -235,14 +235,18 @@ const WITNESS_PROVENANCE = Object.freeze([
|
|
|
235
235
|
]);
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
|
-
* 現行のrun request契約。v3
|
|
239
|
-
* 既存request
|
|
238
|
+
* 現行のrun request契約。v4は計画境界を予測として扱う。v3以前の厳密なcompile契約は、
|
|
239
|
+
* 既存requestの意味を変えないため読み口として残す。
|
|
240
240
|
*
|
|
241
241
|
* v2はこの系列ではない。ADR 0064のepoch後継request(`predecessor_request_digest`と
|
|
242
242
|
* `task_migration_digest`を持つ別shape)が既に使っている番号なので、飛ばして採番する。
|
|
243
243
|
*/
|
|
244
|
-
export const RUN_REQUEST_SCHEMA = 'lattice.run_request.
|
|
245
|
-
export const
|
|
244
|
+
export const RUN_REQUEST_SCHEMA = 'lattice.run_request.v4';
|
|
245
|
+
export const RUN_REQUEST_DECLARATIVE_SCHEMA = 'lattice.run_request.v3';
|
|
246
|
+
export const RUN_REQUEST_LEGACY_SCHEMAS = Object.freeze([
|
|
247
|
+
RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
248
|
+
'lattice.run_request.v1',
|
|
249
|
+
]);
|
|
246
250
|
export const RUN_REQUEST_SCHEMAS = Object.freeze([
|
|
247
251
|
RUN_REQUEST_SCHEMA,
|
|
248
252
|
...RUN_REQUEST_LEGACY_SCHEMAS,
|
|
@@ -305,7 +309,7 @@ export function explainRunRequest(value) {
|
|
|
305
309
|
if (!exactRecord(value, RUN_REQUEST_FIELDS)) return reject('unexpected_or_missing_top_level_keys', '');
|
|
306
310
|
if (!RUN_REQUEST_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
307
311
|
// 創作宣言はv2から。v1のclosed shapeは余分fieldを拒否するので加算互換が成立しない。
|
|
308
|
-
const allowCreates = value.schema
|
|
312
|
+
const allowCreates = [RUN_REQUEST_SCHEMA, RUN_REQUEST_DECLARATIVE_SCHEMA].includes(value.schema);
|
|
309
313
|
if (!identifier(value.request_id)) return reject('invalid_identifier', '/request_id');
|
|
310
314
|
if (!exactRecord(value.repo, ['base_sha', 'root_kind'])) return reject('unexpected_or_missing_keys', '/repo');
|
|
311
315
|
if (!gitSha(value.repo.base_sha)) return reject('invalid_git_sha', '/repo/base_sha');
|
|
@@ -124,12 +124,14 @@ function witnessSet(manifest, kinds) {
|
|
|
124
124
|
* unknown findingにする。
|
|
125
125
|
*/
|
|
126
126
|
export function classifyObservedDiff(options = {}) {
|
|
127
|
-
const { plan, manifests, observations } = options;
|
|
127
|
+
const { plan, manifests, observations, relevantTodoIds = null } = options;
|
|
128
128
|
requirePlan(plan);
|
|
129
129
|
if (manifests === null || typeof manifests !== 'object' || Array.isArray(manifests)
|
|
130
|
-
|| !Array.isArray(observations)
|
|
130
|
+
|| !Array.isArray(observations)
|
|
131
|
+
|| !(relevantTodoIds === null || Array.isArray(relevantTodoIds))) {
|
|
131
132
|
invalidVerification('manifests/observationsが不正');
|
|
132
133
|
}
|
|
134
|
+
const relevant = new Set(relevantTodoIds ?? Object.keys(manifests));
|
|
133
135
|
|
|
134
136
|
const findings = [];
|
|
135
137
|
const observedByTodo = new Map();
|
|
@@ -187,7 +189,8 @@ export function classifyObservedDiff(options = {}) {
|
|
|
187
189
|
for (const [todoId, paths] of observedByTodo) {
|
|
188
190
|
for (const [otherId, manifest] of Object.entries(manifests)) {
|
|
189
191
|
if (otherId === todoId) continue;
|
|
190
|
-
|
|
192
|
+
if (!relevant.has(otherId)) continue;
|
|
193
|
+
const otherDeclared = [...(manifest.reads ?? []), ...(manifest.writes ?? [])];
|
|
191
194
|
for (const path of paths) {
|
|
192
195
|
if (!declaredWriteCovers(otherDeclared, path)) continue;
|
|
193
196
|
const pair = sorted([todoId, otherId]);
|
|
@@ -453,6 +456,24 @@ export function recomputeReceiptDecisions(options = {}) {
|
|
|
453
456
|
const dispatch = dispatchEventForReceipt === undefined
|
|
454
457
|
? undefined
|
|
455
458
|
: { sequence: dispatchEventForReceipt.sequence, payload: dispatchEventForReceipt.payload };
|
|
459
|
+
const originBindingRetained = (() => {
|
|
460
|
+
if (!Number.isSafeInteger(receipt.plan_epoch) || receipt.plan_epoch >= plan.plan_epoch) return false;
|
|
461
|
+
for (let epoch = receipt.plan_epoch; epoch < plan.plan_epoch; epoch += 1) {
|
|
462
|
+
const witnessed = events.some((event) => event.sequence < receipt.sequence
|
|
463
|
+
&& event.kind === 'carry_over_witnessed' && event.plan_epoch === epoch
|
|
464
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
|
|
465
|
+
const continued = events.some((event) => event.sequence < receipt.sequence
|
|
466
|
+
&& event.kind === 'hold_decided' && event.plan_epoch === epoch
|
|
467
|
+
&& event.payload?.continue_set?.includes(receipt.todo_id));
|
|
468
|
+
const recompiled = events.some((event) => event.sequence < receipt.sequence
|
|
469
|
+
&& event.kind === 'plan_recompiled' && event.plan_epoch === epoch + 1);
|
|
470
|
+
const invalidated = events.some((event) => event.sequence < receipt.sequence
|
|
471
|
+
&& event.kind === 'context_invalidated' && event.plan_epoch === epoch + 1
|
|
472
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
|
|
473
|
+
if (!witnessed || !continued || !recompiled || invalidated) return false;
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
})();
|
|
456
477
|
if (dispatch === undefined) {
|
|
457
478
|
return reject('not_dispatched');
|
|
458
479
|
}
|
|
@@ -465,12 +486,12 @@ export function recomputeReceiptDecisions(options = {}) {
|
|
|
465
486
|
if (typeof plan.base_sha === 'string' && payload.base_sha !== plan.base_sha) {
|
|
466
487
|
return reject('base_mismatch');
|
|
467
488
|
}
|
|
468
|
-
if (receipt.plan_epoch !== plan.plan_epoch) {
|
|
489
|
+
if (receipt.plan_epoch !== plan.plan_epoch && !originBindingRetained) {
|
|
469
490
|
return reject('epoch_mismatch');
|
|
470
491
|
}
|
|
471
492
|
// dispatchが旧epochのTODOが現epoch receiptを名乗る場合はepoch_rebound必須
|
|
472
493
|
// (rebindなしのepoch自称を受理しない。Decision 7.3/7.4)。
|
|
473
|
-
if (dispatchEventForReceipt.plan_epoch !== plan.plan_epoch) {
|
|
494
|
+
if (dispatchEventForReceipt.plan_epoch !== plan.plan_epoch && !originBindingRetained) {
|
|
474
495
|
const rebound = state.rebinds[receipt.todo_id];
|
|
475
496
|
if (rebound === undefined
|
|
476
497
|
|| rebound.payload?.new_plan_epoch !== plan.plan_epoch
|