@quolu/lattice 0.46.2 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.46.2",
3
+ "version": "0.47.0",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
@@ -111,9 +111,6 @@ function inspectArray(value, state, depth) {
111
111
  if (Object.getPrototypeOf(value) !== Array.prototype) {
112
112
  invalidArtifact('array prototypeが不正');
113
113
  }
114
- if (value.length > MAX_COLLECTION_ITEMS) {
115
- invalidArtifact('array item数が上限を超えている');
116
- }
117
114
 
118
115
  const ownKeys = Reflect.ownKeys(value);
119
116
  if (ownKeys.some((key) => typeof key !== 'string')) {
@@ -275,7 +275,17 @@ async function resolveProjectState({ cwd, cliVersion }) {
275
275
  : todo.next_ready.length > 0
276
276
  ? { command: `lattice todo start --plan ${todo.next_ready[0].plan_key} --task ${todo.next_ready[0].task_id}${todo.next_ready.length > 1 ? ' --parallel-frontier' : ''}`,
277
277
  reason: todo.next_ready.length > 1 ? 'parallel_frontier_present' : 'next_ready_present' }
278
- : { command: 'lattice todo status', reason: 'no_ready_task' };
278
+ // 監査待ちが在る限り「残作業なし」と答えない。優先順位はactive_run > next_ready >
279
+ // audit_pending > なしで、ready frontierが在る間はADR 0063の並列開始コマンドが勝つ
280
+ // ——ここを監査で上書きするとdispatchが再直列化する。監査待ちはtodo statusの
281
+ // audit_pending欄に常在するので、順位を下げても消えない。
282
+ // commandはverbatim実行可能で読み取り専用のものにする。`phase review`は
283
+ // `--reason <text>`のplaceholderを含みjournalを書き換えるので置かない
284
+ // (`todo phase status`の結果には既に両分岐のguidanceが載っている)。
285
+ : todo.audit_pending.length > 0
286
+ ? { command: `lattice todo phase status --plan ${todo.audit_pending[0].plan_key}`,
287
+ reason: 'audit_pending' }
288
+ : { command: 'lattice todo status', reason: 'no_ready_task' };
279
289
  const result = statusResult({
280
290
  cli: { available: true, version: cliVersion },
281
291
  project: { root: repoRoot, git_head: gitHead(repoRoot), project_id: store.project_id },
@@ -334,7 +344,7 @@ export async function runSessionContext({ cwd, stdout, cliVersion }) {
334
344
  schema: SESSION_CONTEXT_SCHEMA,
335
345
  // project discoveryの答えをそのまま埋める。hostは既存の検証器を再利用できる。
336
346
  status: state.result,
337
- // todoは`todo_status_result.v4`そのもの。新しい意味論を発明しない。
347
+ // todoは`lattice todo status`のresultそのもの。新しい意味論を発明しない。
338
348
  todo: state.todo,
339
349
  independence,
340
350
  result_digest: '',
@@ -467,31 +467,30 @@ async function runScopeViolation({ scaffold }) {
467
467
  runId, plan, events, packets, todoId: 'TA', detect: detectCheckpointFindings, recordedAt: RUN_TIMESTAMP,
468
468
  });
469
469
  events = classified.events;
470
- const held = decideHoldAndCarryOver({
471
- runId, request, plan, manifests, packets, events, recordedAt: RUN_TIMESTAMP,
472
- });
473
- events = held.events;
470
+ events = await driveToClose({ runId, plan, events, packets, manifests, adapter });
471
+ const state = projectRuntimeState({ events });
474
472
  return {
475
473
  request,
476
474
  plan,
477
475
  manifests,
478
- holdDecision: held.holdDecision,
479
476
  events,
480
477
  record: conditionRecord({
481
478
  condition: 'scope_violation',
482
- // offenderとそのaffected closure(ここではTAのみ)をhold。closure外のTBは
483
- // witnessを実証してcontinueする(plan条件表「offenderとaffected closure hold」)。
479
+ // directory名はRC3 artifact互換のため維持する。現契約では単独の予測超過は
480
+ // conflictではなく、観測を残したまま有効なreceiptを受理する。
484
481
  expected: {
485
- finding_kinds: ['undeclared_write'],
486
- hold_includes_offender: true,
487
- hold: ['TA'],
488
- continue: ['TB'],
482
+ observation_kinds: ['prediction_excess'],
483
+ conflict_finding_kinds: [],
484
+ frozen: false,
485
+ accepted: ['TA', 'TB'],
486
+ closed: true,
489
487
  },
490
488
  actual: {
491
- finding_kinds: [...new Set(classified.findings.map(({ kind }) => kind))],
492
- hold_includes_offender: held.holdDecision.hold_set.includes('TA'),
493
- hold: held.holdDecision.hold_set,
494
- continue: held.holdDecision.continue_set,
489
+ observation_kinds: [...new Set(classified.observations.map(({ kind }) => kind))],
490
+ conflict_finding_kinds: [...new Set(classified.findings.map(({ kind }) => kind))],
491
+ frozen: state.freeze !== null,
492
+ accepted: state.accepted,
493
+ closed: state.closed,
495
494
  },
496
495
  events,
497
496
  plan,
@@ -1334,7 +1333,7 @@ export async function runRc3ScriptedCampaign(options = {}) {
1334
1333
  const clean = await timed('clean_parallel', () => runCleanParallel({ scaffold }));
1335
1334
  const late = await timed('late_path_conflict', () => runLateConflict({ scaffold }));
1336
1335
  // 条件名`scope_violation`はRC3 manifestのdirectory名として凍結されているので動かさない。
1337
- // 中で期待するfinding種別だけが製品に追従して`undeclared_write`になる。
1336
+ // 中では単独の予測超過を観測し、conflictへ昇格しない現契約を検証する。
1338
1337
  const scope = await timed('scope_violation', () => runScopeViolation({ scaffold }));
1339
1338
  const semantic = await timed('semantic_unknown', () => runSemanticUnknown({ scaffold }));
1340
1339
  const stale = await timed('stale_receipt', () => runStaleReceipt({ scaffold }));
@@ -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,