@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.
@@ -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 driveInitialScriptedManagedEpoch({
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
- await managedSupervisor.barrierAll({
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
- const stagedLeases = [];
883
- for (const todoId of frontier) {
884
- const packet = packets[todoId];
885
- const staged = {
886
- schema: 'lattice.runtime_write_lease.v1',
887
- lease_id: `lease-${packet.packet_digest.slice(0, 24)}`,
888
- run_id: request.request_id,
889
- todo_id: todoId,
890
- plan_epoch: plan.plan_epoch,
891
- packet_digest: packet.packet_digest,
892
- controller_registration_digest: registrationDigest,
893
- supervisor_session_nonce_digest: sessionNonceDigest,
894
- state: 'staged',
895
- ttl_ms: 60_000,
896
- issued_control_digest: issuedControlDigest,
897
- lease_digest: '',
898
- };
899
- staged.lease_digest = selfDigest(staged, 'lease_digest');
900
- await managedSupervisor.prepareController({
901
- controllerId,
902
- executorPacket: packet,
903
- stagedLease: staged,
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を進めない。freeze後のreceiptはstaleとして拒否されるので、
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
- if (adjudicated.decisions.some((decision) => decision.decision !== 'accepted')) {
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
- 'scripted controller receiptが受理されなかった',
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 driveInitialScriptedManagedEpoch({
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
- if (match === undefined || verified === undefined) throw new ManagedRuntimeError(
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 = [...new Set([...rebound, ...preparedSuccessors])].sort();
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は`owns[].creates`だけがv1との差であり、境界宣言としては同値である。
239
- * 既存requestの書き換えを要求しないため、v1は読み口として残す。
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.v3';
245
- export const RUN_REQUEST_LEGACY_SCHEMAS = Object.freeze(['lattice.run_request.v1']);
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 === RUN_REQUEST_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
- const otherDeclared = manifest.writes ?? [];
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