@quolu/lattice 0.31.0 → 0.33.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/README.ja.md +11 -1
- package/README.md +11 -2
- package/bin/lattice-scripted-worker.mjs +59 -0
- package/bin/lattice.mjs +12 -1
- package/package.json +1 -1
- package/sensor/dist/bin/lattice-sensor.js +72 -4
- package/sensor/dist/db/migrations.d.ts +1 -1
- package/sensor/dist/db/migrations.d.ts.map +1 -1
- package/sensor/dist/db/migrations.js +32 -2
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +10 -0
- package/sensor/dist/db/queries.d.ts.map +1 -1
- package/sensor/dist/db/queries.js +53 -7
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +6 -1
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +89 -16
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/index.d.ts +8 -1
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +9 -0
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/resolution/import-resolver.d.ts +11 -0
- package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
- package/sensor/dist/resolution/import-resolver.js +13 -5
- package/sensor/dist/resolution/import-resolver.js.map +1 -1
- package/sensor/dist/resolution/index.d.ts.map +1 -1
- package/sensor/dist/resolution/index.js +11 -0
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/types.d.ts +4 -0
- package/sensor/dist/resolution/types.d.ts.map +1 -1
- package/sensor/dist/types.d.ts +16 -0
- package/sensor/dist/types.d.ts.map +1 -1
- package/src/artifact-contracts.mjs +3 -0
- package/src/cli-help.mjs +6 -0
- package/src/rc3-scripted-campaign.mjs +3 -1
- package/src/runtime-cli.mjs +454 -97
- package/src/runtime-contracts.mjs +1 -1
- package/src/runtime-control-store.mjs +24 -1
- package/src/runtime-controller-protocol.mjs +21 -3
- package/src/runtime-decision-verifier.mjs +1 -1
- package/src/runtime-diff-observer.mjs +2 -2
- package/src/runtime-front-end.mjs +47 -13
- package/src/runtime-hold-recompile.mjs +31 -2
- package/src/runtime-io-sentinel.mjs +16 -9
- package/src/runtime-managed-supervisor.mjs +9 -1
- package/src/runtime-multi-epoch-store.mjs +2 -2
- package/src/runtime-projection.mjs +27 -0
- package/src/runtime-scripted-adapter-controller.mjs +139 -25
- package/src/runtime-seam-resolve.mjs +156 -13
- package/src/runtime-seam-treatment.mjs +2 -1
- package/src/seam-apply.mjs +101 -4
- package/src/seam-cost.mjs +322 -0
- package/src/seam-gate.mjs +146 -0
- package/src/seam-rewrite.mjs +102 -16
- package/src/seam-verification.mjs +26 -3
- package/src/sensor-adapter.mjs +6 -1
- package/src/todo-cli.mjs +55 -0
package/src/runtime-cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { constants as fsConstants } from 'node:fs';
|
|
4
4
|
import {
|
|
5
5
|
lstat,
|
|
@@ -139,6 +139,20 @@ const KNOWN_ADAPTERS = Object.freeze(['scripted', 'isolated-worktree', 'actual-a
|
|
|
139
139
|
* 超えたら`rejected`として理由ごとjournalへ残す(黙って見送らない)。
|
|
140
140
|
*/
|
|
141
141
|
const IO_ESCALATION_LOCK_TIMEOUT_MS = 15_000;
|
|
142
|
+
/**
|
|
143
|
+
* 係争pathから資源idを導出する(front-endと同じ形)。
|
|
144
|
+
*
|
|
145
|
+
* front-endは`owns`から`own-<kind>-<sha256(target)先頭16>`を作る。path競合の直列化資源は
|
|
146
|
+
* その形でなければ、後継planの競合辺が実ownershipの資源と別物になってしまう。
|
|
147
|
+
* **形を1箇所で決める。** 別の形で作ると、同じpathが別の資源に見える。
|
|
148
|
+
*/
|
|
149
|
+
function ownedResourceId(kind, target) {
|
|
150
|
+
return `own-${kind}-${createHash('sha256').update(target, 'utf8').digest('hex').slice(0, 16)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ownedPathResourceId(pathValue) {
|
|
154
|
+
return ownedResourceId('path', pathValue);
|
|
155
|
+
}
|
|
142
156
|
/** 走行中workerの完了を待つ上限と間隔。待てないと走行中の観測が成立しない。 */
|
|
143
157
|
const SCRIPTED_OBSERVE_TIMEOUT_MS = 120_000;
|
|
144
158
|
const SCRIPTED_OBSERVE_POLL_MS = 20;
|
|
@@ -743,6 +757,80 @@ async function readScriptedControllerReceipt({
|
|
|
743
757
|
return receipt;
|
|
744
758
|
}
|
|
745
759
|
|
|
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
|
+
/**
|
|
801
|
+
* runが起こしたworker processを、耐久記録から回収する。
|
|
802
|
+
*
|
|
803
|
+
* **記録が正本である。** 誰を起こしたかは`executor_dispatched`の
|
|
804
|
+
* `direct_os_observation_binding`が持っており、controllerが落ちていても残る。
|
|
805
|
+
*
|
|
806
|
+
* pidだけで殺さない。pidは再利用されるので、start identityを取り直して記録と一致した
|
|
807
|
+
* ものだけを止める——一致しなければ、それは別のprocessである。
|
|
808
|
+
*
|
|
809
|
+
* SIGKILLで送る。holdで止めたworkerはSIGSTOP状態にあり、SIGTERMは継続されるまで
|
|
810
|
+
* 配送されない。停止したまま放置されたprocessは自分では終われない。
|
|
811
|
+
*/
|
|
812
|
+
async function reapRunWorkerProcesses({ events }) {
|
|
813
|
+
const reaped = [];
|
|
814
|
+
const skipped = [];
|
|
815
|
+
const seen = new Set();
|
|
816
|
+
for (const event of events) {
|
|
817
|
+
if (event.kind !== 'executor_dispatched') continue;
|
|
818
|
+
const binding = event.payload?.direct_os_observation_binding;
|
|
819
|
+
const pid = binding?.process_pid;
|
|
820
|
+
const recorded = binding?.process_start_identity?.identity_digest;
|
|
821
|
+
if (!Number.isSafeInteger(pid) || typeof recorded !== 'string' || seen.has(pid)) continue;
|
|
822
|
+
seen.add(pid);
|
|
823
|
+
const observed = await observeManagedProcessStartIdentity(pid).catch(() => null);
|
|
824
|
+
if (observed === null) continue; // 既に居ない
|
|
825
|
+
if (observed.identity_digest !== recorded) {
|
|
826
|
+
skipped.push({ pid, reason: 'start identity不一致(pid再利用)' });
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
try { process.kill(pid, 'SIGKILL'); reaped.push(pid); } catch { /* 競合で既に消えた */ }
|
|
830
|
+
}
|
|
831
|
+
return { reaped, skipped };
|
|
832
|
+
}
|
|
833
|
+
|
|
746
834
|
async function driveInitialScriptedManagedEpoch({
|
|
747
835
|
runDir,
|
|
748
836
|
repoRoot,
|
|
@@ -754,13 +842,13 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
754
842
|
controlEvents,
|
|
755
843
|
sentinel = null,
|
|
756
844
|
preDispatchBindings = null,
|
|
845
|
+
drainEscalations = null,
|
|
757
846
|
}) {
|
|
758
847
|
let events = [...initialEvents];
|
|
759
848
|
const { plan, manifests, executor_packets: packets } = committed.bundle;
|
|
760
849
|
const controllerId = activation.controllerDescriptor.controller_id;
|
|
761
850
|
const registrationDigest = activation.registration.registration_digest;
|
|
762
851
|
const sessionNonceDigest = digestArtifact(activation.sessionNonce);
|
|
763
|
-
const processGroupId = activation.childPid;
|
|
764
852
|
for (;;) {
|
|
765
853
|
const frontier = computeReadyFrontier({ plan, events }).dispatchable;
|
|
766
854
|
if (frontier.length === 0) break;
|
|
@@ -862,12 +950,19 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
862
950
|
controller_session_nonce_digest:
|
|
863
951
|
activation.controllerDescriptor.controller_session_nonce_digest,
|
|
864
952
|
direct_os_observation_binding: {
|
|
865
|
-
|
|
866
|
-
|
|
953
|
+
// **controllerではなくworker processを指す。** holdは静止の証明を要求し、
|
|
954
|
+
// 直接OS観測はここで名指しされたprocessが実際に停止していることを確かめる。
|
|
955
|
+
// controllerを指していた頃は、止めれば応答できず止めなければ証明できなかった。
|
|
956
|
+
process_pid: response.worker_process.pid,
|
|
957
|
+
process_group_id: response.worker_process.process_group_id,
|
|
867
958
|
process_start_identity:
|
|
868
|
-
structuredClone(
|
|
959
|
+
structuredClone(response.worker_process.process_start_identity),
|
|
960
|
+
// workerはさらに子を持たない。空配列は「子が居ない」という主張であり、
|
|
961
|
+
// 直接OS観測は実測と突き合わせて未記録のchildが居ないことまで確かめる。
|
|
962
|
+
process_children: [],
|
|
869
963
|
// TODOごとの木を指す。ここがrepo rootだった頃、帰属はrootから決まらなかった。
|
|
870
964
|
worktree_path: worktreeByTodo.get(packet.todo_id),
|
|
965
|
+
worktree_realpath: worktreeByTodo.get(packet.todo_id),
|
|
871
966
|
base_sha: packet.base_sha,
|
|
872
967
|
},
|
|
873
968
|
};
|
|
@@ -877,37 +972,27 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
877
972
|
event.kind === 'executor_dispatched'
|
|
878
973
|
&& event.payload?.executor_handle === executorHandle
|
|
879
974
|
));
|
|
880
|
-
// worker
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
});
|
|
895
|
-
return { state: 'terminal', receipt };
|
|
896
|
-
}
|
|
897
|
-
if (response.observation.state !== 'running') {
|
|
898
|
-
throw new ManagedRuntimeError(
|
|
899
|
-
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
900
|
-
`scripted controllerが未知のstateを返した: ${response.observation.state}`,
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
if (Date.now() >= deadline) {
|
|
904
|
-
throw new ManagedRuntimeError(
|
|
905
|
-
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
906
|
-
`scripted workerが時間内に終わらない: ${executorHandle}`,
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
await new Promise((resolve) => { setTimeout(resolve, SCRIPTED_OBSERVE_POLL_MS); });
|
|
975
|
+
// **ここでpollしない。** 待つのは駆動側の仕事である——workerが走っている間が
|
|
976
|
+
// 実行時競合を掴める唯一の窓であり、その窓は駆動側がeventsを手元に持っている時
|
|
977
|
+
// にしか安全に触れない。ここで待つと、窓の間ずっと駆動側が止まる。
|
|
978
|
+
const response = await managedSupervisor.route('observe', controllerId, {
|
|
979
|
+
executor_handle: executorHandle,
|
|
980
|
+
expected_epoch: dispatch.plan_epoch,
|
|
981
|
+
expected_lease_digest: dispatch.payload.write_lease_digest,
|
|
982
|
+
});
|
|
983
|
+
if (response.observation.state === 'running') return { state: 'running' };
|
|
984
|
+
if (response.observation.state !== 'terminal') {
|
|
985
|
+
throw new ManagedRuntimeError(
|
|
986
|
+
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
987
|
+
`scripted controllerが未知のstateを返した: ${response.observation.state}`,
|
|
988
|
+
);
|
|
910
989
|
}
|
|
990
|
+
const receipt = await readScriptedControllerReceipt({
|
|
991
|
+
runDir,
|
|
992
|
+
controllerId,
|
|
993
|
+
payloadDigest: response.observation.payload_digest,
|
|
994
|
+
});
|
|
995
|
+
return { state: 'terminal', receipt };
|
|
911
996
|
},
|
|
912
997
|
};
|
|
913
998
|
const dispatched = await dispatchReadyFrontier({
|
|
@@ -933,22 +1018,52 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
933
1018
|
rootOf: (todoId) => events.findLast((event) => event.kind === 'executor_dispatched'
|
|
934
1019
|
&& event.subject?.kind === 'todo' && event.subject.ref === todoId)
|
|
935
1020
|
?.payload?.direct_os_observation_binding?.worktree_path });
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1021
|
+
// **workerが走っている間だけが、実行時競合を掴める窓である。** 観測を1件ずつ待たずに
|
|
1022
|
+
// 回し、その合間に早期警報のescalationを捌く。捌くのは`replaceEventsAtomically`の
|
|
1023
|
+
// 直後だけ——そこでしかdiskとメモリのeventsが一致していない。
|
|
1024
|
+
const awaiting = new Set(dispatched.dispatched);
|
|
1025
|
+
const observeDeadline = Date.now() + SCRIPTED_OBSERVE_TIMEOUT_MS;
|
|
1026
|
+
let frozen = false;
|
|
1027
|
+
while (awaiting.size > 0) {
|
|
1028
|
+
const drained = await drainEscalations?.(events) ?? null;
|
|
1029
|
+
if (drained !== null) {
|
|
1030
|
+
events = drained;
|
|
1031
|
+
if (projectRuntimeState({ events }).freeze !== null) { frozen = true; break; }
|
|
1032
|
+
}
|
|
1033
|
+
let progressed = false;
|
|
1034
|
+
for (const todoId of [...awaiting]) {
|
|
1035
|
+
const observed = await observeExecutor({
|
|
1036
|
+
runId: request.request_id,
|
|
1037
|
+
todoId,
|
|
1038
|
+
plan,
|
|
1039
|
+
events,
|
|
1040
|
+
adapter: managedAdapter,
|
|
1041
|
+
recordedAt: canonicalNow(),
|
|
1042
|
+
});
|
|
1043
|
+
if (observed.observation.state === 'running') continue;
|
|
1044
|
+
events = observed.events;
|
|
1045
|
+
await replaceEventsAtomically(runDir, events);
|
|
1046
|
+
syncSentinelWatches({ sentinel, runningTodoIds: projectRuntimeState({ events }).running,
|
|
1047
|
+
rootOf: (id) => events.findLast((event) => event.kind === 'executor_dispatched'
|
|
1048
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === id)
|
|
1049
|
+
?.payload?.direct_os_observation_binding?.worktree_path });
|
|
1050
|
+
awaiting.delete(todoId);
|
|
1051
|
+
progressed = true;
|
|
1052
|
+
}
|
|
1053
|
+
if (awaiting.size === 0) break;
|
|
1054
|
+
if (Date.now() >= observeDeadline) {
|
|
1055
|
+
throw new ManagedRuntimeError(
|
|
1056
|
+
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
1057
|
+
`scripted workerが時間内に終わらない: ${[...awaiting].join(',')}`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
if (!progressed) {
|
|
1061
|
+
await new Promise((resolve) => { setTimeout(resolve, SCRIPTED_OBSERVE_POLL_MS); });
|
|
1062
|
+
}
|
|
951
1063
|
}
|
|
1064
|
+
// holdが掛かったらdispatchを進めない。freeze後のreceiptはstaleとして拒否されるので、
|
|
1065
|
+
// 裁定へ進むと「受理されなかった」で落ちるだけである。
|
|
1066
|
+
if (frozen) return events;
|
|
952
1067
|
const adjudicated = adjudicatePendingReceipts({
|
|
953
1068
|
runId: request.request_id,
|
|
954
1069
|
plan,
|
|
@@ -1103,6 +1218,64 @@ async function runSeamResolve({ runDir, repoRoot, findingDigest, requestPath, st
|
|
|
1103
1218
|
return resolution.lane === 'seam_transform' ? 0 : 1;
|
|
1104
1219
|
}
|
|
1105
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* 切断コストの内訳を、記録済みfindingについて投影する(read-only、docs/plan_seam-cost.md)。
|
|
1223
|
+
*
|
|
1224
|
+
* `seam resolve`が隔離worktreeで実変換まで行うのに対し、これは**変換を試す前の安い観測**である。
|
|
1225
|
+
* 「何を共有しているから単純に切れないのか」を数えられる事実として返し、試すかどうかの判断は
|
|
1226
|
+
* 操作するAIに残す。閾値も可否判定も返さない。投影であって記録ではないので、runへ何も書かない。
|
|
1227
|
+
*
|
|
1228
|
+
* symbolの帰属(concern_symbols)は入力で受ける。実行時のwitnessはpath単位の宣言しか持たず、
|
|
1229
|
+
* 係争fileの中で誰がどのsymbolを触るかはAIだけが知っている——ここで推定しない。
|
|
1230
|
+
*/
|
|
1231
|
+
async function runSeamProfile({ runDir, repoRoot, findingDigest, inputPath, stdout }) {
|
|
1232
|
+
const committed = await readCommittedEpochStore(runDir);
|
|
1233
|
+
if (committed === null) {
|
|
1234
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
|
|
1235
|
+
}
|
|
1236
|
+
const found = await readRuntimeFindingRecord({
|
|
1237
|
+
runDir, findingDigest, planEpoch: committed.pointer.plan_epoch,
|
|
1238
|
+
});
|
|
1239
|
+
if (found.record === null) throw new CliContractError('STALE_FINDING', found.reason);
|
|
1240
|
+
const finding = found.record.finding;
|
|
1241
|
+
if (typeof finding.path !== 'string') {
|
|
1242
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE',
|
|
1243
|
+
'path findingではない競合に切断コストの内訳は立たない');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const declaration = await readBoundedJson(inputPath, 'seam profile input');
|
|
1247
|
+
const identifier = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u;
|
|
1248
|
+
const symbols = declaration?.concern_symbols;
|
|
1249
|
+
const todoIds = [...finding.todo_ids].sort();
|
|
1250
|
+
const declaredIds = symbols === null || typeof symbols !== 'object' || Array.isArray(symbols)
|
|
1251
|
+
? null : Object.keys(symbols).sort();
|
|
1252
|
+
if (declaredIds === null
|
|
1253
|
+
|| declaredIds.join('\0') !== todoIds.join('\0')
|
|
1254
|
+
|| declaredIds.some((todoId) => !Array.isArray(symbols[todoId])
|
|
1255
|
+
|| symbols[todoId].length === 0
|
|
1256
|
+
|| !symbols[todoId].every((name) => identifier.test(name)))) {
|
|
1257
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE',
|
|
1258
|
+
'concern_symbolsがfindingのtodo集合と一致しない。'
|
|
1259
|
+
+ `{"concern_symbols": {${todoIds.map((id) => `"${id}": ["<symbol>"]`).join(', ')}}}の形で渡す`);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
let sourceText;
|
|
1263
|
+
try {
|
|
1264
|
+
sourceText = await readFile(path.join(repoRoot, finding.path), 'utf8');
|
|
1265
|
+
} catch {
|
|
1266
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE', `係争fileを読めない: ${finding.path}`);
|
|
1267
|
+
}
|
|
1268
|
+
const { computeSeamCostProfile } = await import('./seam-cost.mjs');
|
|
1269
|
+
const { profile, reasons } = await computeSeamCostProfile({
|
|
1270
|
+
repoRoot, sourcePath: finding.path, sourceText, ownedSymbolsByTask: symbols,
|
|
1271
|
+
});
|
|
1272
|
+
if (profile === null) {
|
|
1273
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE', reasons.join(',') || 'profile_unavailable');
|
|
1274
|
+
}
|
|
1275
|
+
stdout.write(`${JSON.stringify(profile)}\n`);
|
|
1276
|
+
return 0;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1106
1279
|
async function runObserve({ runDir, stdout }) {
|
|
1107
1280
|
const { events } = await readRunStore(runDir);
|
|
1108
1281
|
const chain = verifyRunEventChain({ events });
|
|
@@ -1152,7 +1325,9 @@ async function runStatus({ runDir, stdout }) {
|
|
|
1152
1325
|
if (managed !== null) {
|
|
1153
1326
|
output.schema = 'lattice.managed_run_status.v1';
|
|
1154
1327
|
const runtimeProjection = {
|
|
1155
|
-
|
|
1328
|
+
// v2は`treatment_advice`の追加。既定modeが直列化でも変換を試せる場合があることを、
|
|
1329
|
+
// 運転側が見える形にした(ct-003)。
|
|
1330
|
+
schema: 'lattice.runtime_status_projection.v2',
|
|
1156
1331
|
...projectRuntimeStatusOverlays({ events }),
|
|
1157
1332
|
runtime_frozen: managedFrozen,
|
|
1158
1333
|
};
|
|
@@ -1810,6 +1985,13 @@ export async function runManagedSupervisorDaemon({
|
|
|
1810
1985
|
* 「両方が書いた」と読めてしまう。これをholdへ繋ぐと、無実のTODOを止める。
|
|
1811
1986
|
* 帰属が立たないならescalationへ進めない。警報と実在の記録はそのまま残す。
|
|
1812
1987
|
*/
|
|
1988
|
+
/**
|
|
1989
|
+
* holdが要求する静止を、この構成で証明できるか。
|
|
1990
|
+
*
|
|
1991
|
+
* 直接OS観測はexecutorのprocessが停止していることを実測する。scripted controllerは
|
|
1992
|
+
* 自分のprocessで作業するので、そのprocessを止めると制御そのものが止まる——止めない限り
|
|
1993
|
+
* 証明できず、止めれば応答できない。**別processのexecutorを持つまでこの穴は埋まらない。**
|
|
1994
|
+
*/
|
|
1813
1995
|
const attributionIsDistinct = (warning, roots) => {
|
|
1814
1996
|
const paths = warning.todo_ids.map((todoId) => roots[todoId]);
|
|
1815
1997
|
if (paths.some((value) => typeof value !== 'string')) return false;
|
|
@@ -1853,17 +2035,7 @@ export async function runManagedSupervisorDaemon({
|
|
|
1853
2035
|
*
|
|
1854
2036
|
* @returns {{ok: true, expected_epoch: number}|{ok: false, outcome: string, detail: string}}
|
|
1855
2037
|
*/
|
|
1856
|
-
const
|
|
1857
|
-
let lock;
|
|
1858
|
-
try {
|
|
1859
|
-
lock = await acquireRuntimeLifecycleLock({ runDir,
|
|
1860
|
-
sessionNonceDigest: digestArtifact(sessionNonce), operation: 'finding_record',
|
|
1861
|
-
requestId: `${escalation.escalation_id}.checkpoint`,
|
|
1862
|
-
timeoutMs: IO_ESCALATION_LOCK_TIMEOUT_MS, retryIntervalMs: 25 });
|
|
1863
|
-
} catch (error) {
|
|
1864
|
-
return { ok: false, outcome: 'rejected',
|
|
1865
|
-
detail: `lifecycle lockを取れない: ${error?.code ?? 'RUN_BUSY'}` };
|
|
1866
|
-
}
|
|
2038
|
+
const recordProbeCheckpointsUnlocked = async (escalation, checkpointsByTodo) => {
|
|
1867
2039
|
try {
|
|
1868
2040
|
const active = await readCommittedEpochStore(runDir);
|
|
1869
2041
|
if (active === null) return { ok: false, outcome: 'skipped', detail: 'managed epochが未commit' };
|
|
@@ -1894,6 +2066,23 @@ export async function runManagedSupervisorDaemon({
|
|
|
1894
2066
|
} catch (error) {
|
|
1895
2067
|
return { ok: false, outcome: 'rejected',
|
|
1896
2068
|
detail: `probe checkpointを耐久化できない: ${error?.code ?? String(error?.message ?? error)}` };
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
/** lifecycle lockを自分で取ってから耐久化する版(epoch駆動の外から呼ぶ時)。 */
|
|
2073
|
+
const durablyRecordProbeCheckpoints = async (escalation, checkpointsByTodo) => {
|
|
2074
|
+
let lock;
|
|
2075
|
+
try {
|
|
2076
|
+
lock = await acquireRuntimeLifecycleLock({ runDir,
|
|
2077
|
+
sessionNonceDigest: digestArtifact(sessionNonce), operation: 'finding_record',
|
|
2078
|
+
requestId: `${escalation.escalation_id}.checkpoint`,
|
|
2079
|
+
timeoutMs: IO_ESCALATION_LOCK_TIMEOUT_MS, retryIntervalMs: 25 });
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
return { ok: false, outcome: 'rejected',
|
|
2082
|
+
detail: `lifecycle lockを取れない: ${error?.code ?? 'RUN_BUSY'}` };
|
|
2083
|
+
}
|
|
2084
|
+
try {
|
|
2085
|
+
return await recordProbeCheckpointsUnlocked(escalation, checkpointsByTodo);
|
|
1897
2086
|
} finally {
|
|
1898
2087
|
await lock.release();
|
|
1899
2088
|
}
|
|
@@ -1909,7 +2098,10 @@ export async function runManagedSupervisorDaemon({
|
|
|
1909
2098
|
*
|
|
1910
2099
|
* 途中で断られたらそこで止め、理由をjournalへ残す。静かに別経路へ逃げない。
|
|
1911
2100
|
*/
|
|
1912
|
-
const escalateIoWarning = async (warning, probed, packets) => {
|
|
2101
|
+
const escalateIoWarning = async (warning, probed, packets, options = {}) => {
|
|
2102
|
+
// 耐久化と送信の経路だけを差し替える。判断そのものはどちらの文脈でも同一にする——
|
|
2103
|
+
// 分けると「epoch駆動の中と外で違う止め方をする」ことになり、記録が読めなくなる。
|
|
2104
|
+
const { recordCheckpoints = durablyRecordProbeCheckpoints, dispatchControl = null } = options;
|
|
1913
2105
|
const built = buildIoEscalation({ warning, probe: probed,
|
|
1914
2106
|
checkpointsByTodo: probed.checkpoints, packets });
|
|
1915
2107
|
if (built === null) return null;
|
|
@@ -1928,7 +2120,7 @@ export async function runManagedSupervisorDaemon({
|
|
|
1928
2120
|
return decide('skipped',
|
|
1929
2121
|
'worktree rootを共有する構成では書き手を特定できない(帰属はrootだけで決まる)');
|
|
1930
2122
|
}
|
|
1931
|
-
const recorded = await
|
|
2123
|
+
const recorded = await recordCheckpoints(escalation, probed.checkpoints);
|
|
1932
2124
|
if (!recorded.ok) return decide(recorded.outcome, recorded.detail);
|
|
1933
2125
|
|
|
1934
2126
|
const submit = async (operation, { artifact = null, artifactDigest = null,
|
|
@@ -1940,13 +2132,15 @@ export async function runManagedSupervisorDaemon({
|
|
|
1940
2132
|
artifact, artifactDigest, checkpointDigest,
|
|
1941
2133
|
expectedEpoch: recorded.expected_epoch, expectedQueueDigest: null }),
|
|
1942
2134
|
});
|
|
1943
|
-
return handler(controlRequest);
|
|
2135
|
+
return dispatchControl === null ? handler(controlRequest) : dispatchControl(controlRequest);
|
|
1944
2136
|
};
|
|
1945
2137
|
const unmetOf = (response) => (response?.result?.unmet ?? []).join('/')
|
|
1946
2138
|
|| String(response?.outcome ?? 'unknown');
|
|
1947
2139
|
|
|
1948
2140
|
const findingResponse = await submit('finding_record', {
|
|
1949
|
-
|
|
2141
|
+
// control operationが要求するのはartifact全体のdigestである。`candidate_digest`は
|
|
2142
|
+
// 自分の欄を除いた自己digestなので、そのまま渡すとbindingが合わず必ず弾かれる。
|
|
2143
|
+
artifact: escalation.candidate, artifactDigest: digestArtifact(escalation.candidate),
|
|
1950
2144
|
checkpointDigest: escalation.checkpoint_digest,
|
|
1951
2145
|
}).catch((error) => ({ outcome: 'rejected',
|
|
1952
2146
|
result: { unmet: [String(error?.code ?? error?.message ?? error)] } }));
|
|
@@ -1970,6 +2164,17 @@ export async function runManagedSupervisorDaemon({
|
|
|
1970
2164
|
return decide('held', `早期警報からhold: ${warning.kind}/${warning.path}`, findingDigest);
|
|
1971
2165
|
};
|
|
1972
2166
|
|
|
2167
|
+
/**
|
|
2168
|
+
* epoch駆動中に届いた、実在確認済みの警報。
|
|
2169
|
+
*
|
|
2170
|
+
* **駆動中はここへ積むだけにする。** epoch駆動はrun eventsをメモリに抱えたままawaitを
|
|
2171
|
+
* またぎ、節目ごとに全体を置換する。その最中に横から追記すると、次の置換で消える——
|
|
2172
|
+
* 静かに記録を失う方向に壊れる。捌くのは駆動側の安全点(disk とメモリが一致している点)
|
|
2173
|
+
* であり、そこはworkerがまだ走っている最中なのでholdが成立する。
|
|
2174
|
+
*/
|
|
2175
|
+
const pendingEscalations = [];
|
|
2176
|
+
let epochDriveActive = false;
|
|
2177
|
+
|
|
1973
2178
|
/**
|
|
1974
2179
|
* 警報1件の全行程(probe → 記録 → escalation)。
|
|
1975
2180
|
*
|
|
@@ -1980,6 +2185,11 @@ export async function runManagedSupervisorDaemon({
|
|
|
1980
2185
|
const handleIoWarning = async (warning, packets) => {
|
|
1981
2186
|
const probed = await probeWarning(warning).catch(() => (
|
|
1982
2187
|
{ outcome: 'unprobed', writers: [], checkpoints: {} }));
|
|
2188
|
+
if (probed.outcome === 'observed' && epochDriveActive) {
|
|
2189
|
+
pendingEscalations.push({ warning, probed, packets });
|
|
2190
|
+
await recordIoWarning(warning, 'observed');
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
1983
2193
|
const decided = probed.outcome === 'observed'
|
|
1984
2194
|
? await escalateIoWarning(warning, probed, packets).catch(() => null)
|
|
1985
2195
|
: null;
|
|
@@ -1987,17 +2197,51 @@ export async function runManagedSupervisorDaemon({
|
|
|
1987
2197
|
decided === null || decided.outcome === 'skipped' ? probed.outcome : 'escalated');
|
|
1988
2198
|
};
|
|
1989
2199
|
|
|
2200
|
+
/**
|
|
2201
|
+
* epoch駆動の安全点でescalationを捌く(ADR 0143)。
|
|
2202
|
+
*
|
|
2203
|
+
* 呼ぶのは`replaceEventsAtomically`の直後だけとする。そこではdiskとメモリのeventsが
|
|
2204
|
+
* 一致しているので、control operationがdiskを読み書きしても駆動側の像とずれない。
|
|
2205
|
+
* 捌いた後はdiskを読み直して返す——`conflict`と`hold`はevents.jsonを書き換えるため。
|
|
2206
|
+
*
|
|
2207
|
+
* lifecycle lockは取らない。既に駆動側(activate)が握っている内側であり、ここで
|
|
2208
|
+
* 取り直すと自分自身を待つ。同じ理由で`handler`ではなく`executeControl`を直接使う。
|
|
2209
|
+
*
|
|
2210
|
+
* @returns {Promise<Array|null>} 捌いた結果のevents。何も捌かなければnull。
|
|
2211
|
+
*/
|
|
2212
|
+
const drainPendingEscalations = async (events) => {
|
|
2213
|
+
if (pendingEscalations.length === 0) return null;
|
|
2214
|
+
let current = events;
|
|
2215
|
+
while (pendingEscalations.length > 0) {
|
|
2216
|
+
const { warning, probed, packets } = pendingEscalations.shift();
|
|
2217
|
+
const decided = await escalateIoWarning(warning, probed, packets, {
|
|
2218
|
+
recordCheckpoints: recordProbeCheckpointsUnlocked,
|
|
2219
|
+
dispatchControl: (controlRequest) => executeControl(controlRequest),
|
|
2220
|
+
}).catch((error) => ({ outcome: 'rejected',
|
|
2221
|
+
detail: String(error?.message ?? error), finding_digest: null }));
|
|
2222
|
+
if (decided === null) continue;
|
|
2223
|
+
current = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2224
|
+
// holdが掛かったら、残りの警報は同じfreezeの下にある。二重にholdを試みない。
|
|
2225
|
+
if (projectRuntimeState({ events: current }).freeze !== null) break;
|
|
2226
|
+
}
|
|
2227
|
+
return current;
|
|
2228
|
+
};
|
|
2229
|
+
|
|
1990
2230
|
/** 警報の処理を直列化する鎖。`onWarning`はここへ繋ぐだけにする。 */
|
|
1991
2231
|
let escalationChain = Promise.resolve();
|
|
1992
2232
|
|
|
1993
|
-
const resolveObservationBinding = async ({ binding }) => {
|
|
2233
|
+
const resolveObservationBinding = async ({ binding, ack }) => {
|
|
2234
|
+
// rebind経路はrunning bindingを持たず、ackだけで来る(`kind: 'rebind'`)。どちらから来ても
|
|
2235
|
+
// 同じdispatch記録へ辿り着けなければ、繋ぎ直す相手を特定できない。
|
|
2236
|
+
const todoId = binding?.todo_id ?? ack?.todo_id;
|
|
2237
|
+
const executorHandle = binding?.executor_handle ?? ack?.executor_handle;
|
|
1994
2238
|
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1995
2239
|
const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
1996
|
-
&& event.subject?.kind === 'todo' && event.subject.ref ===
|
|
1997
|
-
&& event.payload?.executor_handle ===
|
|
2240
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId
|
|
2241
|
+
&& event.payload?.executor_handle === executorHandle);
|
|
1998
2242
|
const observation = dispatch?.payload?.direct_os_observation_binding;
|
|
1999
2243
|
if (observation === null || typeof observation !== 'object' || Array.isArray(observation)) {
|
|
2000
|
-
throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable Direct OS binding不足: ${
|
|
2244
|
+
throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable Direct OS binding不足: ${todoId ?? 'unknown'}`);
|
|
2001
2245
|
}
|
|
2002
2246
|
return structuredClone(observation);
|
|
2003
2247
|
};
|
|
@@ -2115,18 +2359,24 @@ export async function runManagedSupervisorDaemon({
|
|
|
2115
2359
|
return settled;
|
|
2116
2360
|
},
|
|
2117
2361
|
});
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2362
|
+
epochDriveActive = true;
|
|
2363
|
+
try {
|
|
2364
|
+
await driveInitialScriptedManagedEpoch({
|
|
2365
|
+
runDir,
|
|
2366
|
+
repoRoot,
|
|
2367
|
+
request,
|
|
2368
|
+
committed,
|
|
2369
|
+
activation,
|
|
2370
|
+
managedSupervisor,
|
|
2371
|
+
sentinel,
|
|
2372
|
+
initialEvents: events,
|
|
2373
|
+
controlEvents: () => controlEvents,
|
|
2374
|
+
preDispatchBindings,
|
|
2375
|
+
drainEscalations: drainPendingEscalations,
|
|
2376
|
+
});
|
|
2377
|
+
} finally {
|
|
2378
|
+
epochDriveActive = false;
|
|
2379
|
+
}
|
|
2130
2380
|
}
|
|
2131
2381
|
if (restarting) {
|
|
2132
2382
|
await managedSupervisor.recoveryBarrier({ barrierId: `recovery-${randomUUID()}`,
|
|
@@ -2316,6 +2566,19 @@ export async function runManagedSupervisorDaemon({
|
|
|
2316
2566
|
if (finding.plan_epoch !== active.pointer.plan_epoch) {
|
|
2317
2567
|
throw new ManagedRuntimeError('STALE_FINDING', 'findingはactive epochに属さない');
|
|
2318
2568
|
}
|
|
2569
|
+
// 競合は2者以上いて初めて競合である。1者しか名指していない観測——誰の領分とも
|
|
2570
|
+
// 重なっていない予測超過——でfreezeすると、抜け道が無い。処置は2つとも2者を要求する
|
|
2571
|
+
// (直列化は`todo_ids.length >= 2`、seam変換は2つの面へ切る)ので、legalなrecompileが
|
|
2572
|
+
// 作れないまま止まる。予測が狭かっただけなので、記録して次のcompileで宣言を実態へ
|
|
2573
|
+
// 合わせるのが正しい応答である。
|
|
2574
|
+
if ((finding.finding?.todo_ids ?? []).length < 2) {
|
|
2575
|
+
throw new ManagedRuntimeError('FINDING_NOT_A_CONFLICT',
|
|
2576
|
+
'1 TODOしか名指していない観測はfreezeへ運べない。'
|
|
2577
|
+
+ '予測超過は競合ではないので、記録のまま次のcompileで宣言を観測へ合わせる', {
|
|
2578
|
+
finding_kind: finding.finding?.kind ?? null,
|
|
2579
|
+
todo_ids: [...(finding.finding?.todo_ids ?? [])],
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2319
2582
|
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2320
2583
|
const todoId = finding.finding?.todo_ids?.[0] ?? active.bundle.plan.nodes[0].todo_id;
|
|
2321
2584
|
events.push(buildNextRunEvent({ events, runId: request.request_id, kind: 'conflict_found', planEpoch: active.pointer.plan_epoch,
|
|
@@ -2534,20 +2797,40 @@ export async function runManagedSupervisorDaemon({
|
|
|
2534
2797
|
!== canonicalizeArtifact(treatment.todo_ids ?? treatment.predecessor_task_ids)) {
|
|
2535
2798
|
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'treatmentがhold finding/todo集合と一致しない');
|
|
2536
2799
|
}
|
|
2537
|
-
if (recompileRequest.mode === 'intentional_serial'
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2800
|
+
if (recompileRequest.mode === 'intentional_serial') {
|
|
2801
|
+
// **path競合には資源idが無い。** finding契約はpath形の`resource_id`をnullと定めており、
|
|
2802
|
+
// path由来の資源idはmanifestの`resources`にも載らない(載るのは宣言したbare資源だけ)。
|
|
2803
|
+
// それでもrouteConflictTreatmentはpath競合を直列化レーンへ振る——請求項7の
|
|
2804
|
+
// 「一方を停止し、他方を確定し、停止した方を再開する」がその経路だからである。
|
|
2805
|
+
//
|
|
2806
|
+
// よってpath findingの時は、係争pathからfront-endと同じ形で資源idを導出して照合する。
|
|
2807
|
+
// 導出値との一致を要求するので、資源idを捏造できないという元の保証は保たれる
|
|
2808
|
+
// ——照合先がfindingそのものへ固定されるため、manifest所属を見る必要が無い。
|
|
2809
|
+
const expectedResource = treatmentFinding.finding.path === null
|
|
2810
|
+
? treatmentFinding.finding.resource_id
|
|
2811
|
+
: ownedPathResourceId(treatmentFinding.finding.path);
|
|
2812
|
+
if (expectedResource !== treatment.resource_id) {
|
|
2813
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceがfinding resourceと一致しない');
|
|
2814
|
+
}
|
|
2815
|
+
// 宣言済み資源の競合は、従来どおり実ownershipから再導出できることまで要求する。
|
|
2816
|
+
if (treatmentFinding.finding.path === null
|
|
2817
|
+
&& !treatment.todo_ids.every((todoId) => {
|
|
2818
|
+
const manifest = active.bundle.manifests[todoId];
|
|
2819
|
+
return manifest?.resources?.includes(treatment.resource_id)
|
|
2820
|
+
|| manifest?.state_effects?.some((effect) => effect.resource_id === treatment.resource_id);
|
|
2821
|
+
})) {
|
|
2822
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceをfresh ownershipから再導出できない');
|
|
2823
|
+
}
|
|
2548
2824
|
}
|
|
2549
2825
|
if (recompileRequest.mode === 'seam_split') {
|
|
2826
|
+
// **宣言された面の所有も数える。** `resources`/`state_effects`だけを見ていた間、
|
|
2827
|
+
// path所有はbefore/afterのどちらにも現れず、seam splitが述べる`added`は常に空の
|
|
2828
|
+
// 導出値と突き合わされていた——path競合を切るsplitは原理的に一致しなかった。
|
|
2829
|
+
// 資源idの合成形は`verifySeamSplitSuccessor`と同じ`own-<kind>-<sha16>`にする。
|
|
2550
2830
|
const ownership = (manifests) => Object.entries(manifests).flatMap(([todoId, manifest]) => [
|
|
2831
|
+
...(manifest.owns ?? []).map((own) => ({
|
|
2832
|
+
resource_id: ownedResourceId(own.kind, own.target),
|
|
2833
|
+
owner_todo_id: todoId, access_kind: 'own' })),
|
|
2551
2834
|
...manifest.resources.map((resourceId) => ({ resource_id: resourceId,
|
|
2552
2835
|
owner_todo_id: todoId, access_kind: 'own' })),
|
|
2553
2836
|
...manifest.state_effects.map((effect) => ({ resource_id: effect.resource_id,
|
|
@@ -2561,9 +2844,41 @@ export async function runManagedSupervisorDaemon({
|
|
|
2561
2844
|
].sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
|
|
2562
2845
|
const difference = (left, right) => left.filter((entry) => !right.some((other) =>
|
|
2563
2846
|
canonicalizeArtifact(entry) === canonicalizeArtifact(other)));
|
|
2564
|
-
|
|
2847
|
+
// **比較の起点は、観測へ合わせた後の宣言である。**
|
|
2848
|
+
//
|
|
2849
|
+
// 実行時に見つかった競合は、片方がその資源を所有していないから起きる。変換はその
|
|
2850
|
+
// 宣言を観測へ合わせてから導出されるので(翻訳段)、splitが述べる遷移も合わせた後の
|
|
2851
|
+
// 状態からのものになる。ここで記録のままのpredecessorと比べると、実行時に見つかった
|
|
2852
|
+
// 競合から作ったsplitは永久に一致しない——請求項8が実行時に届かなくなる。
|
|
2853
|
+
//
|
|
2854
|
+
// 合わせる材料はfindingそのものであり、宣言でも入力でもない。再導出なので、
|
|
2855
|
+
// 呼び出し側が何を主張しても結果は変わらない。
|
|
2856
|
+
const contestedResource = treatmentFinding.finding.path === null
|
|
2857
|
+
? treatmentFinding.finding.resource_id
|
|
2858
|
+
: ownedPathResourceId(treatmentFinding.finding.path);
|
|
2859
|
+
const observedTodoIds = [...treatmentFinding.finding.todo_ids].sort();
|
|
2860
|
+
const reconciledOwnership = [...ownership(active.bundle.manifests)];
|
|
2861
|
+
for (const todoId of observedTodoIds) {
|
|
2862
|
+
const entry = { resource_id: contestedResource, owner_todo_id: todoId, access_kind: 'own' };
|
|
2863
|
+
if (!reconciledOwnership.some((other) => canonicalizeArtifact(other) === canonicalizeArtifact(entry))) {
|
|
2864
|
+
reconciledOwnership.push(entry);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
const reconciledEdges = [...edges(active.bundle.plan)];
|
|
2868
|
+
for (let left = 0; left < observedTodoIds.length; left += 1) {
|
|
2869
|
+
for (let right = left + 1; right < observedTodoIds.length; right += 1) {
|
|
2870
|
+
const edge = { from_todo_id: observedTodoIds[left], to_todo_id: observedTodoIds[right],
|
|
2871
|
+
kind: 'conflict' };
|
|
2872
|
+
if (!reconciledEdges.some((other) => canonicalizeArtifact(other) === canonicalizeArtifact(edge))) {
|
|
2873
|
+
reconciledEdges.push(edge);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
const sortEntries = (entries) => [...entries]
|
|
2878
|
+
.sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
|
|
2879
|
+
const beforeOwnership = sortEntries(reconciledOwnership);
|
|
2565
2880
|
const afterOwnership = ownership(compiled.manifests);
|
|
2566
|
-
const beforeEdges =
|
|
2881
|
+
const beforeEdges = sortEntries(reconciledEdges);
|
|
2567
2882
|
const afterEdges = edges(newPlan);
|
|
2568
2883
|
const derivedOwnership = { added: difference(afterOwnership, beforeOwnership),
|
|
2569
2884
|
removed: difference(beforeOwnership, afterOwnership) };
|
|
@@ -2712,6 +3027,20 @@ export async function runManagedSupervisorDaemon({
|
|
|
2712
3027
|
rebindEvidence.set(todoId, { ...evidence,
|
|
2713
3028
|
controller_registration_digest: owner.registration.registration_digest });
|
|
2714
3029
|
}
|
|
3030
|
+
// **ここが再開の位置である。** rebindは静止を要求する(`write_enabled === false`)ので、
|
|
3031
|
+
// holdの直後に再開すると後継epochへ束ね直せない。carry-overは「作業を捨てない」で
|
|
3032
|
+
// あって「一度も止まらない」ではない——barrierで止まり、rebindで新epochへ繋がれ、
|
|
3033
|
+
// そこで初めて動き出す。
|
|
3034
|
+
if (rebound.size > 0) {
|
|
3035
|
+
const resumed = await resumeContinuedWorkers({ events, todoIds: [...rebound] });
|
|
3036
|
+
if (resumed.resumed.length > 0 || resumed.skipped.length > 0) {
|
|
3037
|
+
await appendControl({ run_id: request.request_id, kind: 'workers_resumed',
|
|
3038
|
+
session_nonce_digest: digestArtifact(sessionNonce), payload: {
|
|
3039
|
+
resumed_todo_ids: [...resumed.resumed].sort(),
|
|
3040
|
+
skipped_count: resumed.skipped.length,
|
|
3041
|
+
} });
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
2715
3044
|
for (const todoId of core.planDiff.redispatched) {
|
|
2716
3045
|
const successors = recompileRequest.task_migration.entries
|
|
2717
3046
|
.find((entry) => entry.predecessor_task_id === todoId)?.successor_task_ids ?? [];
|
|
@@ -2889,9 +3218,22 @@ export async function runManagedSupervisorDaemon({
|
|
|
2889
3218
|
recordedAt: canonicalNow() })];
|
|
2890
3219
|
}
|
|
2891
3220
|
await replaceEventsAtomically(runDir, events);
|
|
2892
|
-
// abandon
|
|
2893
|
-
//
|
|
3221
|
+
// **abandonは成果を捨てる決定である。** worker processの終了はcleanupではなく
|
|
3222
|
+
// その決定の一部なので、誰を止めたかを記録へ残す。closeでは何もしない——
|
|
3223
|
+
// 完走したrunのworkerは既にterminalであり、生きていればそれは欠陥である
|
|
3224
|
+
// (`closeRunIfComplete`が全TODO accepted を要求するので、そもそもcloseへ来ない)。
|
|
2894
3225
|
if (controlRequest.operation === 'abandon') {
|
|
3226
|
+
const reaped = await reapRunWorkerProcesses({ events }).catch(() => null);
|
|
3227
|
+
if (reaped !== null && (reaped.reaped.length > 0 || reaped.skipped.length > 0)) {
|
|
3228
|
+
await appendControl({ run_id: request.request_id, kind: 'worker_processes_terminated',
|
|
3229
|
+
session_nonce_digest: digestArtifact(sessionNonce), payload: {
|
|
3230
|
+
reason: shutdownReason,
|
|
3231
|
+
terminated_pids: [...reaped.reaped].sort((left, right) => left - right),
|
|
3232
|
+
skipped_count: reaped.skipped.length,
|
|
3233
|
+
} });
|
|
3234
|
+
}
|
|
3235
|
+
// 木も畳む。closeでは畳まない——木そのものがrunの成果であり、着地させる前に
|
|
3236
|
+
// 消したら受理した内容が残らない。
|
|
2895
3237
|
await removeScriptedWorktrees({ repoRoot, runDir }).catch(() => null);
|
|
2896
3238
|
}
|
|
2897
3239
|
await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
|
|
@@ -3415,6 +3757,11 @@ export async function runManagedSupervisorDaemon({
|
|
|
3415
3757
|
// 監視fdを残さない。取り残すとtest fixtureの後片付けが重くなる。
|
|
3416
3758
|
sentinel?.close();
|
|
3417
3759
|
sentinel = null;
|
|
3760
|
+
// **これは事故処理であって、正しい閉じ方ではない。** 正規の終了はabandon(破棄の決定)か
|
|
3761
|
+
// hold後の再開であり、そのどちらも通らずdaemonが落ちる時だけここへ来る。停止した
|
|
3762
|
+
// workerは自分では終われないので、記録から辿って道連れにする。
|
|
3763
|
+
await readBoundedJson(path.join(runDir, 'events.json'), 'run events')
|
|
3764
|
+
.then((events) => reapRunWorkerProcesses({ events })).catch(() => null);
|
|
3418
3765
|
if (activationCommitted && activation !== null) {
|
|
3419
3766
|
await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
|
|
3420
3767
|
session_nonce_digest: digestArtifact(sessionNonce), payload: { signal } });
|
|
@@ -3580,6 +3927,16 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
3580
3927
|
requestId: requestIdOverride,
|
|
3581
3928
|
});
|
|
3582
3929
|
};
|
|
3930
|
+
} else if (argv.length === 9
|
|
3931
|
+
&& argv[0] === 'run' && argv[1] === 'seam' && argv[2] === 'profile'
|
|
3932
|
+
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|
|
3933
|
+
&& argv[5] === '--finding' && /^[0-9a-f]{64}$/u.test(argv[6])
|
|
3934
|
+
&& argv[7] === '--input' && typeof argv[8] === 'string' && argv[8].length > 0) {
|
|
3935
|
+
action = async () => {
|
|
3936
|
+
const { repoRoot, runDir } = await resolveRunStore(cwd, argv[4]);
|
|
3937
|
+
return runSeamProfile({ runDir, repoRoot, findingDigest: argv[6],
|
|
3938
|
+
inputPath: path.resolve(cwd, argv[8]), stdout });
|
|
3939
|
+
};
|
|
3583
3940
|
} else if (argv.length === 9
|
|
3584
3941
|
&& argv[0] === 'run' && argv[1] === 'seam' && argv[2] === 'resolve'
|
|
3585
3942
|
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|