@quolu/lattice 0.30.1 → 0.31.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.
|
|
3
|
+
"version": "0.31.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",
|
package/src/runtime-cli.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
19
19
|
|
|
20
20
|
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
21
21
|
import { collectSensorEvidence } from './sensor-adapter.mjs';
|
|
22
|
-
import { detectCheckpointFindings } from './runtime-diff-observer.mjs';
|
|
22
|
+
import { captureWorktreeDiff, detectCheckpointFindings } from './runtime-diff-observer.mjs';
|
|
23
23
|
import {
|
|
24
24
|
buildIoEscalation, createRunSentinel, probeIoWarning, syncSentinelWatches,
|
|
25
25
|
} from './runtime-io-sentinel.mjs';
|
|
@@ -139,6 +139,9 @@ 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
|
+
/** 走行中workerの完了を待つ上限と間隔。待てないと走行中の観測が成立しない。 */
|
|
143
|
+
const SCRIPTED_OBSERVE_TIMEOUT_MS = 120_000;
|
|
144
|
+
const SCRIPTED_OBSERVE_POLL_MS = 20;
|
|
142
145
|
|
|
143
146
|
class CliContractError extends Error {
|
|
144
147
|
constructor(code, message, detail) {
|
|
@@ -750,6 +753,7 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
750
753
|
initialEvents,
|
|
751
754
|
controlEvents,
|
|
752
755
|
sentinel = null,
|
|
756
|
+
preDispatchBindings = null,
|
|
753
757
|
}) {
|
|
754
758
|
let events = [...initialEvents];
|
|
755
759
|
const { plan, manifests, executor_packets: packets } = committed.bundle;
|
|
@@ -777,9 +781,13 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
777
781
|
// 木がTODOごとに分かれて初めて、書き込みの帰属をrootから決められる。
|
|
778
782
|
const worktreeByTodo = new Map();
|
|
779
783
|
for (const todoId of frontier) {
|
|
780
|
-
|
|
784
|
+
const worktreePath = await ensureScriptedWorktree({
|
|
781
785
|
repoRoot, runDir, packet: packets[todoId],
|
|
782
|
-
})
|
|
786
|
+
});
|
|
787
|
+
worktreeByTodo.set(todoId, worktreePath);
|
|
788
|
+
preDispatchBindings?.set(todoId, {
|
|
789
|
+
worktree_path: worktreePath, base_sha: packets[todoId].base_sha,
|
|
790
|
+
});
|
|
783
791
|
}
|
|
784
792
|
syncSentinelWatches({ sentinel, runningTodoIds: [...frontier],
|
|
785
793
|
rootOf: (todoId) => worktreeByTodo.get(todoId) });
|
|
@@ -869,23 +877,37 @@ async function driveInitialScriptedManagedEpoch({
|
|
|
869
877
|
event.kind === 'executor_dispatched'
|
|
870
878
|
&& event.payload?.executor_handle === executorHandle
|
|
871
879
|
));
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
)
|
|
880
|
+
// workerはdispatchで終わらない。走り続けている間はrunningが返るので、
|
|
881
|
+
// 完了まで待つ。ここが待てないと、走行中の観測が成立する構成を持てない。
|
|
882
|
+
const deadline = Date.now() + SCRIPTED_OBSERVE_TIMEOUT_MS;
|
|
883
|
+
for (;;) {
|
|
884
|
+
const response = await managedSupervisor.route('observe', controllerId, {
|
|
885
|
+
executor_handle: executorHandle,
|
|
886
|
+
expected_epoch: dispatch.plan_epoch,
|
|
887
|
+
expected_lease_digest: dispatch.payload.write_lease_digest,
|
|
888
|
+
});
|
|
889
|
+
if (response.observation.state === 'terminal') {
|
|
890
|
+
const receipt = await readScriptedControllerReceipt({
|
|
891
|
+
runDir,
|
|
892
|
+
controllerId,
|
|
893
|
+
payloadDigest: response.observation.payload_digest,
|
|
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); });
|
|
882
910
|
}
|
|
883
|
-
const receipt = await readScriptedControllerReceipt({
|
|
884
|
-
runDir,
|
|
885
|
-
controllerId,
|
|
886
|
-
payloadDigest: response.observation.payload_digest,
|
|
887
|
-
});
|
|
888
|
-
return { state: 'terminal', receipt };
|
|
889
911
|
},
|
|
890
912
|
};
|
|
891
913
|
const dispatched = await dispatchReadyFrontier({
|
|
@@ -1738,6 +1760,14 @@ export async function runManagedSupervisorDaemon({
|
|
|
1738
1760
|
* probeが撮るのはgitから読んだ本物のdiffなので、そのままfindingの証拠になる。
|
|
1739
1761
|
* fs eventをfindingへ昇格させる必要が無く、契約を1つも緩めずに済む。
|
|
1740
1762
|
*/
|
|
1763
|
+
/**
|
|
1764
|
+
* dispatch event耐久化より前のobservation binding。
|
|
1765
|
+
*
|
|
1766
|
+
* frontierのworktreeを用意した時点でsupervisorが知っている値を置く。警報はdispatchの
|
|
1767
|
+
* 最中に飛ぶので、eventの耐久化を待つと最も早い観測を取り逃す。
|
|
1768
|
+
*/
|
|
1769
|
+
const preDispatchBindings = new Map();
|
|
1770
|
+
|
|
1741
1771
|
const probeWarning = async (warning) => {
|
|
1742
1772
|
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events')
|
|
1743
1773
|
.catch(() => null);
|
|
@@ -1749,7 +1779,12 @@ export async function runManagedSupervisorDaemon({
|
|
|
1749
1779
|
for (const todoId of warning.todo_ids) {
|
|
1750
1780
|
const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
1751
1781
|
&& event.subject?.kind === 'todo' && event.subject.ref === todoId);
|
|
1752
|
-
|
|
1782
|
+
// dispatch eventは、frontier全体のdispatchが終わってから一括で耐久化される。
|
|
1783
|
+
// 警報はその最中に飛ぶので、eventだけを頼りにすると**いちばん早い観測を必ず取り逃す**。
|
|
1784
|
+
// supervisorはdispatchの前にworktreeを用意した時点で同じbindingを知っているので、
|
|
1785
|
+
// そちらを先に見る。値はどちらも同一で、早いか遅いかの違いしかない。
|
|
1786
|
+
const binding = dispatch?.payload?.direct_os_observation_binding
|
|
1787
|
+
?? preDispatchBindings.get(todoId);
|
|
1753
1788
|
if (typeof binding?.worktree_path !== 'string' || typeof binding?.base_sha !== 'string') continue;
|
|
1754
1789
|
roots[todoId] = binding.worktree_path;
|
|
1755
1790
|
// 観測できないTODOは「書いていない」へ丸めない。probeIoWarningが未観測として扱う。
|
|
@@ -1788,7 +1823,7 @@ export async function runManagedSupervisorDaemon({
|
|
|
1788
1823
|
* 事後に再読して再導出できる主張であることの担保である。fs eventは取りこぼすし再読もできない。
|
|
1789
1824
|
* ここで残すのは「機械が気づいた」という事実だけで、判定の正本はcheckpointのままである。
|
|
1790
1825
|
*
|
|
1791
|
-
*
|
|
1826
|
+
* 記録しない選択肢は無い。気づいたのに黙っている状態を残さない。
|
|
1792
1827
|
*/
|
|
1793
1828
|
const warningDigestOf = (warning) => digestArtifact({
|
|
1794
1829
|
warning_kind: warning.kind, todo_ids: [...warning.todo_ids].sort(), path: warning.path,
|
|
@@ -2090,6 +2125,7 @@ export async function runManagedSupervisorDaemon({
|
|
|
2090
2125
|
sentinel,
|
|
2091
2126
|
initialEvents: events,
|
|
2092
2127
|
controlEvents: () => controlEvents,
|
|
2128
|
+
preDispatchBindings,
|
|
2093
2129
|
});
|
|
2094
2130
|
}
|
|
2095
2131
|
if (restarting) {
|
|
@@ -158,7 +158,7 @@ export function validateRuntimeControlEventPayload(kind, value) {
|
|
|
158
158
|
}
|
|
159
159
|
// I/O sentinelの早期警報(ADR 0143)。**findingではない**——検知の正本はcheckpointのままで、
|
|
160
160
|
// これは「早くcheckpointを撮って確かめろ」という引き金の記録である。記録しない選択肢は無い:
|
|
161
|
-
//
|
|
161
|
+
// 機械が何かに気づいたのに黙っている状態を残さない。
|
|
162
162
|
if (kind === 'io_warning_observed') {
|
|
163
163
|
return exact(value, ['warning_kind', 'todo_ids', 'path', 'probe_outcome', 'warning_digest'])
|
|
164
164
|
&& ['io_overlap_warning', 'io_scope_warning'].includes(value.warning_kind)
|
|
@@ -251,15 +251,19 @@ function deterministicWriteBytes(packet, relativePath) {
|
|
|
251
251
|
})}\n`);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
async function executePacket({ packet, repoRoot }) {
|
|
254
|
+
async function executePacket({ packet, repoRoot, extraWrites = [] }) {
|
|
255
255
|
const writes = packet.scope?.writes;
|
|
256
256
|
if (!Array.isArray(writes) || writes.length === 0
|
|
257
257
|
|| writes.some((entry, index) => !safeWritePath(entry)
|
|
258
258
|
|| (index > 0 && writes[index - 1] >= entry))) {
|
|
259
259
|
fail('SCRIPTED_PACKET_REJECTED', 'scope.writesは非空の昇順一意pathでなければならない');
|
|
260
260
|
}
|
|
261
|
+
// 宣言scope外への書き込みは、検知そのものを検証するために要る。writeがすべて宣言内に
|
|
262
|
+
// 収まる限り、実行時competitionは原理的に一度も起きないので、検知経路を実runで通せない。
|
|
263
|
+
// 宣言と実writeが食い違う状態を意図して作れることが、この面の受入条件である。
|
|
264
|
+
const allWrites = [...new Set([...writes, ...extraWrites])].sort();
|
|
261
265
|
const prepared = [];
|
|
262
|
-
for (const relativePath of
|
|
266
|
+
for (const relativePath of allWrites) {
|
|
263
267
|
const target = await requireSafeTarget(repoRoot, relativePath);
|
|
264
268
|
const existedAtBase = await basePathExists(repoRoot, packet.base_sha, relativePath);
|
|
265
269
|
prepared.push({
|
|
@@ -318,6 +322,66 @@ async function readAndValidateGate(runDir, writeLease) {
|
|
|
318
322
|
* supervisor controller protocolを実装する決定論的scripted adapter。
|
|
319
323
|
* 時刻はheartbeat schedulingだけに使い、write bytes/handle/receiptへ混入させない。
|
|
320
324
|
*/
|
|
325
|
+
/**
|
|
326
|
+
* 登録済みadapter configから、このcontrollerの振る舞いを読む。
|
|
327
|
+
*
|
|
328
|
+
* **configはregistrationのdigestへ束縛されている。** repo内の任意のfileを信用するのではなく、
|
|
329
|
+
* `run adapter register`が記録した`config_digest`と一致するbytesだけを受ける。一致しなければ
|
|
330
|
+
* 既定の振る舞いへ落とすのではなく止める——registrationと実configがずれた状態で走ると、
|
|
331
|
+
* 記録されたrunの再現性が壊れる。
|
|
332
|
+
*
|
|
333
|
+
* 読むのは3つだけ:
|
|
334
|
+
* - `hold_ms`: 書き込み後にworkerが走り続ける時間。実行時観測が成立する窓を作る。
|
|
335
|
+
* - `extra_writes`: 宣言scope外へのwrite。競合検知そのものを検証するために要る。
|
|
336
|
+
* - `mode`: 既存の`deterministic`のみ。未知の値は黙って無視せず止める。
|
|
337
|
+
*/
|
|
338
|
+
async function readScriptedBehavior(repoRoot) {
|
|
339
|
+
const descriptorPath = path.join(repoRoot, '.lattice', 'runtime', 'adapter-registry',
|
|
340
|
+
'descriptors', 'scripted.json');
|
|
341
|
+
let descriptor;
|
|
342
|
+
try {
|
|
343
|
+
descriptor = JSON.parse(await readFile(descriptorPath, 'utf8'));
|
|
344
|
+
} catch {
|
|
345
|
+
// 未登録のまま走らせる経路(unit test等)は既定の振る舞いで動かす。
|
|
346
|
+
return { hold_ms: 0, extra_writes: [] };
|
|
347
|
+
}
|
|
348
|
+
if (typeof descriptor?.config_ref !== 'string' || typeof descriptor.config_digest !== 'string') {
|
|
349
|
+
return { hold_ms: 0, extra_writes: [] };
|
|
350
|
+
}
|
|
351
|
+
const configPath = path.join(repoRoot, ...descriptor.config_ref.split('/'));
|
|
352
|
+
let bytes;
|
|
353
|
+
try {
|
|
354
|
+
bytes = await readFile(configPath);
|
|
355
|
+
} catch {
|
|
356
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', '登録済みadapter configを読めない', {
|
|
357
|
+
config_ref: descriptor.config_ref,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (sha256Bytes(bytes) !== descriptor.config_digest) {
|
|
361
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'adapter configが登録時のdigestと一致しない', {
|
|
362
|
+
config_ref: descriptor.config_ref,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
let config;
|
|
366
|
+
try {
|
|
367
|
+
config = JSON.parse(bytes.toString('utf8'));
|
|
368
|
+
} catch {
|
|
369
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'adapter configのJSONが不正');
|
|
370
|
+
}
|
|
371
|
+
if (config?.mode !== undefined && config.mode !== 'deterministic') {
|
|
372
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', `未知のscripted mode: ${String(config.mode)}`);
|
|
373
|
+
}
|
|
374
|
+
const holdMs = config?.hold_ms ?? 0;
|
|
375
|
+
if (!Number.isSafeInteger(holdMs) || holdMs < 0 || holdMs > 600_000) {
|
|
376
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'hold_msが不正');
|
|
377
|
+
}
|
|
378
|
+
const extraWrites = config?.extra_writes ?? [];
|
|
379
|
+
if (!Array.isArray(extraWrites) || extraWrites.some((entry) => !safeWritePath(entry))) {
|
|
380
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'extra_writesが不正');
|
|
381
|
+
}
|
|
382
|
+
return { hold_ms: holdMs, extra_writes: [...extraWrites] };
|
|
383
|
+
}
|
|
384
|
+
|
|
321
385
|
export async function createScriptedAdapterController({
|
|
322
386
|
bootstrap,
|
|
323
387
|
runDir,
|
|
@@ -373,6 +437,7 @@ export async function createScriptedAdapterController({
|
|
|
373
437
|
descriptor_digest: '',
|
|
374
438
|
}, 'descriptor_digest');
|
|
375
439
|
const sessionNonceDigest = digestArtifact(bootstrap.supervisor_session_nonce);
|
|
440
|
+
const behavior = await readScriptedBehavior(canonicalRepoRoot);
|
|
376
441
|
const stagedLeases = new Map();
|
|
377
442
|
const armedLeases = new Map();
|
|
378
443
|
const preparedPackets = new Map();
|
|
@@ -551,40 +616,66 @@ export async function createScriptedAdapterController({
|
|
|
551
616
|
worktree_path: worktreePath,
|
|
552
617
|
});
|
|
553
618
|
}
|
|
554
|
-
const { observedDiff, checkpointDigest } = await executePacket({
|
|
555
|
-
packet,
|
|
556
|
-
repoRoot: await realpath(worktreePath),
|
|
557
|
-
});
|
|
558
619
|
const executorHandle = `scripted-${packet.packet_digest.slice(0, 24)}`;
|
|
559
620
|
const worktreeId = scriptedWorktreeId(packet);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
621
|
+
// **dispatchで作業を終わらせない。** 終わらせると、走行中のTODOが1つも存在しない
|
|
622
|
+
// runになり、実行時の観測——書き込みを見て、他のworkerとの重なりを掴む——が
|
|
623
|
+
// 原理的に成立しない。dispatchは作業を起こして返り、完了はobserveが拾う。
|
|
624
|
+
const progress = {
|
|
625
|
+
schema: 'lattice.scripted_adapter_progress.v1',
|
|
626
|
+
run_id: bootstrap.run_id,
|
|
627
|
+
todo_id: packet.todo_id,
|
|
563
628
|
executor_handle: executorHandle,
|
|
564
629
|
worktree_id: worktreeId,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
packet_digest: packet.packet_digest,
|
|
568
|
-
todo_id: packet.todo_id,
|
|
569
|
-
checkpoint_digest: checkpointDigest,
|
|
570
|
-
observed_diff: observedDiff,
|
|
571
|
-
receipt_digest: '',
|
|
572
|
-
}, 'receipt_digest');
|
|
573
|
-
if (!validateExecutorReceipt(receipt)) {
|
|
574
|
-
fail('SCRIPTED_EXECUTION_FAILED', '生成receiptがexecutor contractを満たさない');
|
|
575
|
-
}
|
|
576
|
-
const payloadDigest = await persistReceipt(receipt);
|
|
630
|
+
state: 'running',
|
|
631
|
+
};
|
|
577
632
|
const task = {
|
|
578
633
|
packet: structuredClone(packet),
|
|
579
634
|
lease: structuredClone(writeLease),
|
|
580
635
|
executorHandle,
|
|
581
636
|
worktreeId,
|
|
582
|
-
receipt,
|
|
583
|
-
payloadDigest,
|
|
584
|
-
state: '
|
|
637
|
+
receipt: null,
|
|
638
|
+
payloadDigest: digestArtifact(progress),
|
|
639
|
+
state: 'running',
|
|
640
|
+
failure: null,
|
|
641
|
+
settled: null,
|
|
585
642
|
};
|
|
586
643
|
tasks.set(executorHandle, task);
|
|
587
644
|
todoToHandle.set(packet.todo_id, executorHandle);
|
|
645
|
+
const worktreeReal = await realpath(worktreePath);
|
|
646
|
+
task.settled = (async () => {
|
|
647
|
+
try {
|
|
648
|
+
const { observedDiff, checkpointDigest } = await executePacket({
|
|
649
|
+
packet, repoRoot: worktreeReal, extraWrites: behavior.extra_writes,
|
|
650
|
+
});
|
|
651
|
+
// 書いた後も走り続ける。これが実行時観測の窓であり、0ならば窓は存在しない。
|
|
652
|
+
if (behavior.hold_ms > 0) {
|
|
653
|
+
await new Promise((resolve) => { setTimeout(resolve, behavior.hold_ms); });
|
|
654
|
+
}
|
|
655
|
+
const receipt = sign({
|
|
656
|
+
schema: 'lattice.executor_receipt.v1',
|
|
657
|
+
receipt_id: `receipt-${packet.packet_digest.slice(0, 24)}`,
|
|
658
|
+
executor_handle: executorHandle,
|
|
659
|
+
worktree_id: worktreeId,
|
|
660
|
+
base_sha: packet.base_sha,
|
|
661
|
+
plan_epoch: packet.plan_epoch,
|
|
662
|
+
packet_digest: packet.packet_digest,
|
|
663
|
+
todo_id: packet.todo_id,
|
|
664
|
+
checkpoint_digest: checkpointDigest,
|
|
665
|
+
observed_diff: observedDiff,
|
|
666
|
+
receipt_digest: '',
|
|
667
|
+
}, 'receipt_digest');
|
|
668
|
+
if (!validateExecutorReceipt(receipt)) {
|
|
669
|
+
throw new TypeError('生成receiptがexecutor contractを満たさない');
|
|
670
|
+
}
|
|
671
|
+
task.payloadDigest = await persistReceipt(receipt);
|
|
672
|
+
task.receipt = receipt;
|
|
673
|
+
task.state = 'terminal';
|
|
674
|
+
} catch (error) {
|
|
675
|
+
// 失敗を走行中のまま放置しない。observeがtypedに落ちる形へ残す。
|
|
676
|
+
task.failure = String(error?.detail?.reason ?? error?.message ?? error);
|
|
677
|
+
}
|
|
678
|
+
})();
|
|
588
679
|
return sign({
|
|
589
680
|
schema: 'lattice.adapter_dispatch_response.v1',
|
|
590
681
|
request_id: request.request_id,
|
|
@@ -602,6 +693,10 @@ export async function createScriptedAdapterController({
|
|
|
602
693
|
|| task.lease.lease_digest !== request.expected_lease_digest) {
|
|
603
694
|
fail('SCRIPTED_OBSERVATION_REJECTED', 'observe bindingがdispatch記録と一致しない');
|
|
604
695
|
}
|
|
696
|
+
// 走行中に落ちた作業を「まだ走っている」と言い続けない。
|
|
697
|
+
if (task.failure !== null) {
|
|
698
|
+
fail('SCRIPTED_EXECUTION_FAILED', 'worker実行が失敗した', { reason: task.failure });
|
|
699
|
+
}
|
|
605
700
|
const observation = sign({
|
|
606
701
|
schema: 'lattice.adapter_observation.v1',
|
|
607
702
|
state: task.state,
|
|
@@ -641,11 +736,20 @@ export async function createScriptedAdapterController({
|
|
|
641
736
|
}, 'response_digest');
|
|
642
737
|
}
|
|
643
738
|
if (operation === 'barrier') {
|
|
644
|
-
const acks =
|
|
739
|
+
const acks = [];
|
|
740
|
+
for (const binding of request.running_bindings) {
|
|
645
741
|
const task = tasks.get(binding.executor_handle);
|
|
646
742
|
if (task === undefined || task.packet.todo_id !== binding.todo_id) {
|
|
647
743
|
fail('SCRIPTED_BARRIER_REJECTED', 'barrierが未知のrunning bindingを含む');
|
|
648
744
|
}
|
|
745
|
+
// barrierは静止の宣言である。走行中の作業を残したままackを返すと、
|
|
746
|
+
// 「止まった」と言いながらworktreeが動き続ける。settleを待ってから答える。
|
|
747
|
+
if (task.settled !== null) await task.settled;
|
|
748
|
+
if (task.failure !== null) {
|
|
749
|
+
fail('SCRIPTED_BARRIER_REJECTED', 'barrier対象のworker実行が失敗している', {
|
|
750
|
+
reason: task.failure,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
649
753
|
task.state = 'held';
|
|
650
754
|
const processObservationDigest = digestArtifact({
|
|
651
755
|
schema: 'lattice.scripted_process_observation.v1',
|
|
@@ -657,7 +761,7 @@ export async function createScriptedAdapterController({
|
|
|
657
761
|
worktree_id: task.worktreeId,
|
|
658
762
|
checkpoint_digest: task.receipt.checkpoint_digest,
|
|
659
763
|
});
|
|
660
|
-
|
|
764
|
+
acks.push(sign({
|
|
661
765
|
schema: 'lattice.executor_quiescence_ack.v1',
|
|
662
766
|
ack_id: `barrier-${binding.packet_digest.slice(0, 24)}`,
|
|
663
767
|
run_id: bootstrap.run_id,
|
|
@@ -673,8 +777,8 @@ export async function createScriptedAdapterController({
|
|
|
673
777
|
worktree_fingerprint_digest: worktreeFingerprintDigest,
|
|
674
778
|
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
675
779
|
ack_digest: '',
|
|
676
|
-
}, 'ack_digest');
|
|
677
|
-
}
|
|
780
|
+
}, 'ack_digest'));
|
|
781
|
+
}
|
|
678
782
|
return sign({
|
|
679
783
|
schema: 'lattice.adapter_barrier_response.v1',
|
|
680
784
|
request_id: request.request_id,
|