@quolu/lattice 0.63.3 → 0.63.5
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 +1 -1
- package/src/runtime-decision-verifier.mjs +9 -4
- package/src/runtime-direct-os-observer.mjs +2 -7
- package/src/runtime-managed-supervisor.mjs +11 -21
- package/src/runtime-os-observation.mjs +58 -0
- package/src/runtime-pull-intake.mjs +79 -56
- package/src/runtime-work-order-controller.mjs +2 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.5",
|
|
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",
|
|
@@ -102,11 +102,16 @@ export function computeReadyFrontier(options = {}) {
|
|
|
102
102
|
return { dispatchable };
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// 宣言 write はファイルにもディレクトリにも成り得る(witness の writes は `templates` の
|
|
106
|
+
// ような素のディレクトリ名を普通に持つ)。末尾 `/` の有無で prefix 扱いを分けると、
|
|
107
|
+
// ディレクトリ境界を正しく宣言した task の配下ファイルが全部 undeclared_write になり、
|
|
108
|
+
// accept のたびに hold → worker SIGSTOP で卓が凍る(2026-08-22 実測: t1/t3 で連続被弾)。
|
|
105
109
|
function declaredWriteCovers(declaredWrites, observedPath) {
|
|
106
|
-
return declaredWrites.some((declared) =>
|
|
107
|
-
declared === observedPath
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
return declaredWrites.some((declared) => {
|
|
111
|
+
if (declared === observedPath) return true;
|
|
112
|
+
const prefix = declared.endsWith('/') ? declared : `${declared}/`;
|
|
113
|
+
return observedPath.startsWith(prefix);
|
|
114
|
+
});
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
function witnessSet(manifest, kinds) {
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
1
|
import { lstat, realpath } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
3
|
|
|
6
4
|
import { digestArtifact } from './artifact-contracts.mjs';
|
|
7
5
|
import { selfDigest } from './runtime-contracts.mjs';
|
|
8
6
|
import { validateProcessStartIdentity } from './runtime-controller-protocol.mjs';
|
|
9
7
|
import { captureWorktreeDiff } from './runtime-diff-observer.mjs';
|
|
8
|
+
import { observePsSnapshot } from './runtime-os-observation.mjs';
|
|
10
9
|
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
10
|
const GIT_SHA1 = /^[0-9a-f]{40}$/;
|
|
13
11
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
14
12
|
|
|
@@ -70,10 +68,7 @@ function validateResolvedBinding(value) {
|
|
|
70
68
|
|
|
71
69
|
async function runPsSnapshot() {
|
|
72
70
|
try {
|
|
73
|
-
|
|
74
|
-
'-axo', 'pid=,ppid=,pgid=,state=,lstart=',
|
|
75
|
-
], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
76
|
-
return stdout;
|
|
71
|
+
return await observePsSnapshot();
|
|
77
72
|
} catch (error) {
|
|
78
73
|
fail(`ps観測失敗: ${error?.code ?? error?.message ?? 'unknown'}`);
|
|
79
74
|
}
|
|
@@ -10,6 +10,7 @@ import { selfDigest } from './runtime-contracts.mjs';
|
|
|
10
10
|
import { captureWorktreeDiff } from './runtime-diff-observer.mjs';
|
|
11
11
|
import { pidsOwningSocketPath, socketPathsOwnedByPid } from './runtime-socket-owner.mjs';
|
|
12
12
|
import { createDirectOsProcessObserver as createDirectOsProcessObserverV2 } from './runtime-direct-os-observer.mjs';
|
|
13
|
+
import { observeExecutablePath, observeStartIdentityRaw, osObservationEnvironment } from './runtime-os-observation.mjs';
|
|
13
14
|
import {
|
|
14
15
|
armStagedWriteLease,
|
|
15
16
|
createControllerBootstrap,
|
|
@@ -59,14 +60,11 @@ export async function observeManagedProcessStartIdentity(pid) {
|
|
|
59
60
|
}
|
|
60
61
|
let startedIdentity;
|
|
61
62
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const { stdout } = await execFileAsync(executable, args, { encoding: 'utf8' });
|
|
68
|
-
startedIdentity = stdout.trim();
|
|
69
|
-
} catch { fail('ADAPTER_CONTROLLER_UNAVAILABLE', `process start identity観測失敗: ${pid}`); }
|
|
63
|
+
startedIdentity = await observeStartIdentityRaw(pid);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const cause = String(error?.stderr ?? '').trim() || error?.message || 'unknown';
|
|
66
|
+
fail('ADAPTER_CONTROLLER_UNAVAILABLE', `process start identity観測失敗: pid=${pid}, 観測コマンドの失敗=${cause}`);
|
|
67
|
+
}
|
|
70
68
|
if (!startedIdentity) fail('ADAPTER_CONTROLLER_UNAVAILABLE', `process不在: ${pid}`);
|
|
71
69
|
const identity = { schema: 'lattice.process_start_identity.v1', platform: process.platform, pid, started_identity: startedIdentity, identity_digest: '' };
|
|
72
70
|
identity.identity_digest = selfDigest(identity, 'identity_digest');
|
|
@@ -109,18 +107,10 @@ export async function observeProcessExecutablePath(pid) {
|
|
|
109
107
|
if (!Number.isSafeInteger(pid) || pid < 1) {
|
|
110
108
|
fail('ADAPTER_BINARY_IDENTITY_MISMATCH', 'exec後image PID不正');
|
|
111
109
|
}
|
|
112
|
-
if (process.platform
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
if (process.platform === 'darwin') {
|
|
116
|
-
const { stdout } = await execFileAsync(
|
|
117
|
-
'/bin/ps',
|
|
118
|
-
['-o', 'comm=', '-p', String(pid)],
|
|
119
|
-
{ encoding: 'utf8' },
|
|
120
|
-
);
|
|
121
|
-
return realpath(stdout.trim());
|
|
110
|
+
if (process.platform !== 'linux' && process.platform !== 'darwin') {
|
|
111
|
+
fail('ADAPTER_BINARY_IDENTITY_MISMATCH', `exec後image path観測未対応platform: ${process.platform}`);
|
|
122
112
|
}
|
|
123
|
-
|
|
113
|
+
return observeExecutablePath(pid);
|
|
124
114
|
}
|
|
125
115
|
|
|
126
116
|
/** storeが解決したimmutable bindingからprocess/worktree/checkpointをDirect OSで再観測する。 */
|
|
@@ -170,7 +160,7 @@ export function createDirectOsProcessObserver({ resolveObservationBinding }) {
|
|
|
170
160
|
let observedIdentity = resolved.process_start_identity;
|
|
171
161
|
let processGroupId = resolved.process_group_id;
|
|
172
162
|
try {
|
|
173
|
-
const { stdout } = await execFileAsync('/bin/ps', ['-o', 'lstart=,pgid=,state=', '-p', String(resolved.process_pid)], { encoding: 'utf8' });
|
|
163
|
+
const { stdout } = await execFileAsync('/bin/ps', ['-o', 'lstart=,pgid=,state=', '-p', String(resolved.process_pid)], { encoding: 'utf8', env: osObservationEnvironment() });
|
|
174
164
|
const line = stdout.trim();
|
|
175
165
|
if (line) {
|
|
176
166
|
const match = line.match(/^(.*\d{4})\s+(\d+)\s+(\S+)$/);
|
|
@@ -188,7 +178,7 @@ export function createDirectOsProcessObserver({ resolveObservationBinding }) {
|
|
|
188
178
|
}
|
|
189
179
|
if (!['stopped', 'exited'].includes(processState)) fail('HOLD_ACKS_INCOMPLETE', 'executor processがquiescedでない');
|
|
190
180
|
try {
|
|
191
|
-
const { stdout: groupStdout } = await execFileAsync('/bin/ps', ['-o', 'pid=,state=', '-g', String(resolved.process_group_id)], { encoding: 'utf8' });
|
|
181
|
+
const { stdout: groupStdout } = await execFileAsync('/bin/ps', ['-o', 'pid=,state=', '-g', String(resolved.process_group_id)], { encoding: 'utf8', env: osObservationEnvironment() });
|
|
192
182
|
for (const line of groupStdout.trim().split('\n').filter(Boolean)) {
|
|
193
183
|
const match = line.trim().match(/^(\d+)\s+(\S+)$/);
|
|
194
184
|
if (!match || (!match[2].startsWith('T') && !match[2].startsWith('Z'))) fail('HOLD_ACKS_INCOMPLETE', 'process group childがquiescedでない');
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
// OS観測はobserverのlocaleへ依存させない。psのlstart書式(LC_TIME)と非ASCII argvの
|
|
8
|
+
// エスケープ有無(LC_CTYPE)はlocaleで変わり、同じprocessでも観測者ごとにidentity digestが
|
|
9
|
+
// 割れる(2026-08-22 実測: 席fileと席自身の観測でWORKER_IDENTITY_MISMATCH)。
|
|
10
|
+
//
|
|
11
|
+
// 既知の限界: Linuxの`LC_ALL=C`は非ASCIIバイトを`?`へ非可逆に潰すため、非ASCII部分だけが
|
|
12
|
+
// 違うargvの識別力はLinuxでは落ちる(観測者間の一致は保たれる——潰れた`?`は誰が観測しても
|
|
13
|
+
// 同じ`?`になるので、locale不一致によるdigest分裂は防げる)。darwinの`ps`はvisエスケープで
|
|
14
|
+
// 非ASCIIを可逆に表現するため、この劣化は起きない。
|
|
15
|
+
export function osObservationEnvironment() { return { ...process.env, LC_ALL: 'C' }; }
|
|
16
|
+
|
|
17
|
+
/** processのstart identity(darwin/linuxはps lstart、win32はPowerShell StartTime Ticks)の生文字列を返す。 */
|
|
18
|
+
export async function observeStartIdentityRaw(pid) {
|
|
19
|
+
const executable = process.platform === 'win32' ? 'powershell.exe' : '/bin/ps';
|
|
20
|
+
const args = process.platform === 'win32'
|
|
21
|
+
? ['-NoProfile', '-NonInteractive', '-Command',
|
|
22
|
+
`[System.Diagnostics.Process]::GetProcessById(${pid}).StartTime.ToUniversalTime().Ticks`]
|
|
23
|
+
: ['-o', 'lstart=', '-p', String(pid)];
|
|
24
|
+
const { stdout } = await execFileAsync(executable, args, { encoding: 'utf8', env: osObservationEnvironment() });
|
|
25
|
+
return stdout.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** processのargv(`ps -o command=`)の生文字列を返す。 */
|
|
29
|
+
export async function observeArgv(pid) {
|
|
30
|
+
const { stdout } = await execFileAsync('/bin/ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf8', env: osObservationEnvironment() });
|
|
31
|
+
return stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** processのprocess group id(`ps -o pgid=`)の生文字列を返す。 */
|
|
35
|
+
export async function observePgid(pid) {
|
|
36
|
+
const { stdout } = await execFileAsync('/bin/ps', ['-o', 'pgid=', '-p', String(pid)], { encoding: 'utf8', env: osObservationEnvironment() });
|
|
37
|
+
return stdout.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 全processの`pid=,ppid=,pgid=,state=,lstart=`スナップショットの生stdoutを返す。 */
|
|
41
|
+
export async function observePsSnapshot() {
|
|
42
|
+
const { stdout } = await execFileAsync('/bin/ps', [
|
|
43
|
+
'-axo', 'pid=,ppid=,pgid=,state=,lstart=',
|
|
44
|
+
], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, env: osObservationEnvironment() });
|
|
45
|
+
return stdout;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 実行中processの実行imageのcanonical pathを観測する。linuxは`/proc/<pid>/exe`、
|
|
50
|
+
* darwinは`ps -o comm=`の結果をrealpathで正規化する。
|
|
51
|
+
*/
|
|
52
|
+
export async function observeExecutablePath(pid) {
|
|
53
|
+
if (process.platform === 'linux') {
|
|
54
|
+
return realpath(`/proc/${pid}/exe`);
|
|
55
|
+
}
|
|
56
|
+
const { stdout } = await execFileAsync('/bin/ps', ['-o', 'comm=', '-p', String(pid)], { encoding: 'utf8', env: osObservationEnvironment() });
|
|
57
|
+
return realpath(stdout.trim());
|
|
58
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { constants as fsConstants } from 'node:fs';
|
|
4
4
|
import {
|
|
5
5
|
lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile,
|
|
6
6
|
} from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
8
|
|
|
10
9
|
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
11
10
|
import { validateExpectedWorkerProcess } from './runtime-controller-protocol.mjs';
|
|
@@ -13,6 +12,7 @@ import { classifyObservedDiff } from './runtime-decision-verifier.mjs';
|
|
|
13
12
|
import { captureWorktreeDiff, detectCheckpointFindings } from './runtime-diff-observer.mjs';
|
|
14
13
|
import { acquireRuntimeLifecycleLock } from './runtime-lifecycle-lock.mjs';
|
|
15
14
|
import { observeManagedProcessStartIdentity } from './runtime-managed-supervisor.mjs';
|
|
15
|
+
import { observeArgv, observePgid } from './runtime-os-observation.mjs';
|
|
16
16
|
import { deliverWorkerSignal, observeWindowsWorkerProcess } from './runtime-windows-process.mjs';
|
|
17
17
|
import { ensureScriptedWorktree } from './runtime-scripted-worktree.mjs';
|
|
18
18
|
import {
|
|
@@ -33,7 +33,6 @@ const SHA1 = /^[0-9a-f]{40}$/u;
|
|
|
33
33
|
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
34
34
|
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
35
35
|
const MAX_FILE_BYTES = 8_388_608;
|
|
36
|
-
const execFileAsync = promisify(execFile);
|
|
37
36
|
|
|
38
37
|
export class PullRunError extends Error {
|
|
39
38
|
constructor(code, message, detail) {
|
|
@@ -372,10 +371,14 @@ function boundaryFor(artifact, taskId) {
|
|
|
372
371
|
return artifact?.task_boundaries?.find((entry) => entry.task_id === taskId) ?? null;
|
|
373
372
|
}
|
|
374
373
|
|
|
374
|
+
// boundary / changed のどちらも素のディレクトリ名で来る(末尾 `/` は保証されない)。
|
|
375
|
+
// 末尾 `/` の時だけ prefix 扱いにすると、ディレクトリ境界の配下変更が「触れていない」へ
|
|
376
|
+
// 誤判定される(declaredWriteCovers と同族。2026-08-22 実測)。
|
|
375
377
|
function pathTouches(boundary, changed) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
378
|
+
if (boundary === changed) return true;
|
|
379
|
+
const boundaryPrefix = boundary.endsWith('/') ? boundary : `${boundary}/`;
|
|
380
|
+
const changedPrefix = changed.endsWith('/') ? changed : `${changed}/`;
|
|
381
|
+
return changed.startsWith(boundaryPrefix) || boundary.startsWith(changedPrefix);
|
|
379
382
|
}
|
|
380
383
|
|
|
381
384
|
async function boundaryVerdict({ repoRoot, store, member, meta, taskId, intakeBase }) {
|
|
@@ -755,17 +758,19 @@ export async function intakePullTask({ repoRoot, runDir, taskId, environment = p
|
|
|
755
758
|
const updated = project(current.events, current.meta).intakes
|
|
756
759
|
.find((entry) => entry.task_id === taskId);
|
|
757
760
|
if (updated.worker && intervention.state === 'hold' && !updated.worker.stopped) {
|
|
758
|
-
await signalAttachedWorker(updated, 'SIGSTOP')
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
761
|
+
if (await signalAttachedWorker(updated, 'SIGSTOP')) {
|
|
762
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
763
|
+
events: current.events, meta: current.meta, kind: 'worker_stopped', taskId,
|
|
764
|
+
payload: { reason: intervention.reason },
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
763
767
|
} else if (updated.worker?.stopped && intervention.state === 'none') {
|
|
764
|
-
await signalAttachedWorker(updated, 'SIGCONT')
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
768
|
+
if (await signalAttachedWorker(updated, 'SIGCONT')) {
|
|
769
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
770
|
+
events: current.events, meta: current.meta, kind: 'worker_resumed', taskId,
|
|
771
|
+
payload: { released_by: 'intake_refresh' },
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
769
774
|
}
|
|
770
775
|
state = project(current.events, current.meta);
|
|
771
776
|
const result = { schema: 'lattice.pull_intake_result.v1', outcome: 'intaked',
|
|
@@ -922,10 +927,10 @@ async function observeProcessBinding(pid) {
|
|
|
922
927
|
};
|
|
923
928
|
}
|
|
924
929
|
const processStartIdentity = await observeManagedProcessStartIdentity(pid);
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
const processGroupId = Number(pgidOut
|
|
928
|
-
const argv = argvOut
|
|
930
|
+
const pgidOut = await observePgid(pid);
|
|
931
|
+
const argvOut = await observeArgv(pid);
|
|
932
|
+
const processGroupId = Number(pgidOut);
|
|
933
|
+
const argv = argvOut;
|
|
929
934
|
const expected = { pid, process_group_id: processGroupId, process_start_identity: processStartIdentity };
|
|
930
935
|
if (!validateExpectedWorkerProcess(expected) || argv.length === 0) {
|
|
931
936
|
fail('WORKER_IDENTITY_MISMATCH', 'worker process identityを完全観測できない');
|
|
@@ -937,16 +942,26 @@ async function observeProcessBinding(pid) {
|
|
|
937
942
|
};
|
|
938
943
|
}
|
|
939
944
|
|
|
945
|
+
// signal前の再認証は不変量(pid + lstart)だけで行う。argvはTUIがプロセスタイトルを
|
|
946
|
+
// 書き換えると変わり、pgidもprocess自身が変えられる——可変量を照合に使うと、生きている
|
|
947
|
+
// 正当なworkerへsignalできなくなり、detach/release/holdの全経路が恒久停止する。
|
|
948
|
+
// pid + lstart が同じなら同一process instanceであり、誤配は起きない。
|
|
949
|
+
// 不在(ESRCH)とpid再利用(lstart不一致)は「配達先がもう居ない」=falseで返し、
|
|
950
|
+
// 観測自体の失敗だけを失敗として扱う。
|
|
940
951
|
async function signalAttachedWorker(intake, signal) {
|
|
941
952
|
if (intake.worker === null) return false;
|
|
953
|
+
try { process.kill(intake.worker.pid, 0); }
|
|
954
|
+
catch (error) { if (error?.code === 'ESRCH') return false; }
|
|
942
955
|
let observed;
|
|
943
956
|
try { observed = await observeProcessBinding(intake.worker.pid); }
|
|
944
|
-
catch {
|
|
957
|
+
catch {
|
|
958
|
+
try { process.kill(intake.worker.pid, 0); }
|
|
959
|
+
catch (error) { if (error?.code === 'ESRCH') return false; }
|
|
960
|
+
fail('WORKER_IDENTITY_MISMATCH', 'signal前にworkerを再認証できない');
|
|
961
|
+
}
|
|
945
962
|
if (observed.process_start_identity.identity_digest
|
|
946
|
-
!== intake.worker.process_start_identity.identity_digest
|
|
947
|
-
|
|
948
|
-
|| observed.process_group_id !== intake.worker.process_group_id) {
|
|
949
|
-
fail('WORKER_IDENTITY_MISMATCH', 'signal前のlstart/argv/pgidがattach bindingと一致しない');
|
|
963
|
+
!== intake.worker.process_start_identity.identity_digest) {
|
|
964
|
+
return false;
|
|
950
965
|
}
|
|
951
966
|
try { deliverWorkerSignal(intake.worker.pid, signal); }
|
|
952
967
|
catch { fail('WORKER_SIGNAL_FAILED', `workerへ${signal}を送れない`); }
|
|
@@ -994,8 +1009,10 @@ export async function attachPullWorker({ runDir, taskId, input, environment = pr
|
|
|
994
1009
|
fail('WORKER_ALREADY_ATTACHED', `同じworkerは複数active intakeへattachできない: ${occupied.task_id}`);
|
|
995
1010
|
}
|
|
996
1011
|
if (occupied && occupied.intervention.state === 'hold') {
|
|
997
|
-
|
|
998
|
-
await signalAttachedWorker(occupied, 'SIGCONT')
|
|
1012
|
+
const resumed = occupied.worker.stopped
|
|
1013
|
+
? await signalAttachedWorker(occupied, 'SIGCONT')
|
|
1014
|
+
: false;
|
|
1015
|
+
if (resumed) {
|
|
999
1016
|
current = await appendEvent(runDir, current, buildEvent({
|
|
1000
1017
|
events: current.events, meta: current.meta, kind: 'worker_resumed',
|
|
1001
1018
|
taskId: occupied.task_id, payload: { released_by: 'stale_hold_reattach' },
|
|
@@ -1004,7 +1021,7 @@ export async function attachPullWorker({ runDir, taskId, input, environment = pr
|
|
|
1004
1021
|
current = await appendEvent(runDir, current, buildEvent({
|
|
1005
1022
|
events: current.events, meta: current.meta, kind: 'worker_detached',
|
|
1006
1023
|
taskId: occupied.task_id,
|
|
1007
|
-
payload: { detached_by: actor, pid: occupied.worker.pid, resumed
|
|
1024
|
+
payload: { detached_by: actor, pid: occupied.worker.pid, resumed },
|
|
1008
1025
|
}));
|
|
1009
1026
|
state = project(current.events, current.meta);
|
|
1010
1027
|
}
|
|
@@ -1023,11 +1040,12 @@ export async function attachPullWorker({ runDir, taskId, input, environment = pr
|
|
|
1023
1040
|
}
|
|
1024
1041
|
const refreshed = state.intakes.find((entry) => entry.task_id === taskId);
|
|
1025
1042
|
if (refreshed.intervention.state === 'hold' && !refreshed.worker.stopped) {
|
|
1026
|
-
await signalAttachedWorker(refreshed, 'SIGSTOP')
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1043
|
+
if (await signalAttachedWorker(refreshed, 'SIGSTOP')) {
|
|
1044
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
1045
|
+
events: current.events, meta: current.meta, kind: 'worker_stopped', taskId,
|
|
1046
|
+
payload: { reason: refreshed.intervention.reason },
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1031
1049
|
}
|
|
1032
1050
|
const result = { schema: 'lattice.pull_worker_attach_result.v1', outcome: 'attached',
|
|
1033
1051
|
task_id: taskId, pid: input.pid, stopped: project(current.events, current.meta)
|
|
@@ -1049,7 +1067,7 @@ export async function attachPullWorker({ runDir, taskId, input, environment = pr
|
|
|
1049
1067
|
* never verifiable and the hold was permanent.
|
|
1050
1068
|
*
|
|
1051
1069
|
* Authorization is process identity, not the actor: `signalAttachedWorker`
|
|
1052
|
-
* re-verifies lstart
|
|
1070
|
+
* re-verifies pid+lstart against the recorded binding before signalling,
|
|
1053
1071
|
* which is strictly stronger than an env-derived actor claim (env vars are
|
|
1054
1072
|
* trivially settable; a process's start identity is not). See
|
|
1055
1073
|
* `optionalActorFromEnvironment` for why the seat's own identity cannot be the
|
|
@@ -1074,9 +1092,10 @@ export async function detachPullWorker({ runDir, taskId, environment = process.e
|
|
|
1074
1092
|
// Resume before unbinding. Once the binding is gone nothing in this store can
|
|
1075
1093
|
// name that pid again, so a worker left stopped here would be unreachable by
|
|
1076
1094
|
// any future command — the same one-way door, one step further along.
|
|
1077
|
-
const resumed = intake.worker.stopped
|
|
1095
|
+
const resumed = intake.worker.stopped
|
|
1096
|
+
? await signalAttachedWorker(intake, 'SIGCONT')
|
|
1097
|
+
: false;
|
|
1078
1098
|
if (resumed) {
|
|
1079
|
-
await signalAttachedWorker(intake, 'SIGCONT');
|
|
1080
1099
|
current = await appendEvent(runDir, current, buildEvent({
|
|
1081
1100
|
events: current.events, meta: current.meta, kind: 'worker_resumed', taskId,
|
|
1082
1101
|
payload: { released_by: 'worker_detach' },
|
|
@@ -1220,11 +1239,12 @@ export async function acceptPullTask({ repoRoot, runDir, taskId, environment = p
|
|
|
1220
1239
|
const stoppedIntake = project(current.events, current.meta).intakes
|
|
1221
1240
|
.find((entry) => entry.task_id === affectedTaskId);
|
|
1222
1241
|
if (stoppedIntake.worker && !stoppedIntake.worker.stopped) {
|
|
1223
|
-
await signalAttachedWorker(stoppedIntake, 'SIGSTOP')
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1242
|
+
if (await signalAttachedWorker(stoppedIntake, 'SIGSTOP')) {
|
|
1243
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
1244
|
+
events: current.events, meta: current.meta, kind: 'worker_stopped', taskId: affectedTaskId,
|
|
1245
|
+
payload: { reason: 'runtime_conflict' },
|
|
1246
|
+
}));
|
|
1247
|
+
}
|
|
1228
1248
|
}
|
|
1229
1249
|
}
|
|
1230
1250
|
fail('RUNTIME_CONFLICT_HOLD', 'observed diffがruntime conflictを生成した', { findings });
|
|
@@ -1236,11 +1256,12 @@ export async function acceptPullTask({ repoRoot, runDir, taskId, environment = p
|
|
|
1236
1256
|
const resumed = project(current.events, current.meta).intakes
|
|
1237
1257
|
.find((entry) => entry.task_id === taskId);
|
|
1238
1258
|
if (resumed.worker?.stopped) {
|
|
1239
|
-
await signalAttachedWorker(resumed, 'SIGCONT')
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1259
|
+
if (await signalAttachedWorker(resumed, 'SIGCONT')) {
|
|
1260
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
1261
|
+
events: current.events, meta: current.meta, kind: 'worker_resumed', taskId,
|
|
1262
|
+
payload: { released_by_empty_findings: true },
|
|
1263
|
+
}));
|
|
1264
|
+
}
|
|
1244
1265
|
}
|
|
1245
1266
|
}
|
|
1246
1267
|
current = await appendEvent(runDir, current, buildEvent({
|
|
@@ -1265,11 +1286,12 @@ export async function acceptPullTask({ repoRoot, runDir, taskId, environment = p
|
|
|
1265
1286
|
const resumed = project(current.events, current.meta).intakes
|
|
1266
1287
|
.find((entry) => entry.task_id === waiting.task_id);
|
|
1267
1288
|
if (resumed.worker?.stopped) {
|
|
1268
|
-
await signalAttachedWorker(resumed, 'SIGCONT')
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1289
|
+
if (await signalAttachedWorker(resumed, 'SIGCONT')) {
|
|
1290
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
1291
|
+
events: current.events, meta: current.meta, kind: 'worker_resumed', taskId: waiting.task_id,
|
|
1292
|
+
payload: { released_by_accepted_task: taskId },
|
|
1293
|
+
}));
|
|
1294
|
+
}
|
|
1273
1295
|
}
|
|
1274
1296
|
}
|
|
1275
1297
|
state = project(current.events, current.meta);
|
|
@@ -1293,11 +1315,12 @@ export async function acceptPullTask({ repoRoot, runDir, taskId, environment = p
|
|
|
1293
1315
|
const resumed = project(current.events, current.meta).intakes
|
|
1294
1316
|
.find((entry) => entry.task_id === waiting.task_id);
|
|
1295
1317
|
if (resumed.worker?.stopped) {
|
|
1296
|
-
await signalAttachedWorker(resumed, 'SIGCONT')
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1318
|
+
if (await signalAttachedWorker(resumed, 'SIGCONT')) {
|
|
1319
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
1320
|
+
events: current.events, meta: current.meta, kind: 'worker_resumed', taskId: waiting.task_id,
|
|
1321
|
+
payload: { released_by_accepted_task: taskId },
|
|
1322
|
+
}));
|
|
1323
|
+
}
|
|
1301
1324
|
}
|
|
1302
1325
|
}
|
|
1303
1326
|
const result = { schema: 'lattice.pull_accept_result.v1', outcome: 'accepted',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
3
|
import { constants as fsConstants, readFileSync } from 'node:fs';
|
|
5
4
|
import {
|
|
6
5
|
chmod,
|
|
@@ -13,7 +12,6 @@ import {
|
|
|
13
12
|
rm,
|
|
14
13
|
} from 'node:fs/promises';
|
|
15
14
|
import path from 'node:path';
|
|
16
|
-
import { promisify } from 'node:util';
|
|
17
15
|
|
|
18
16
|
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
19
17
|
import {
|
|
@@ -32,6 +30,7 @@ import {
|
|
|
32
30
|
import { captureWorktreeDiff } from './runtime-diff-observer.mjs';
|
|
33
31
|
import { validateSupervisorWriteGate } from './runtime-gate-store.mjs';
|
|
34
32
|
import { observeManagedProcessStartIdentity } from './runtime-managed-supervisor.mjs';
|
|
33
|
+
import { observePsSnapshot } from './runtime-os-observation.mjs';
|
|
35
34
|
import {
|
|
36
35
|
scriptedWorktreeId as workOrderWorktreeId,
|
|
37
36
|
scriptedWorktreePath as workOrderWorktreePath,
|
|
@@ -45,7 +44,6 @@ import {
|
|
|
45
44
|
const MAX_DOCUMENT_BYTES = 8_388_608;
|
|
46
45
|
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
47
46
|
const ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u;
|
|
48
|
-
const execFileAsync = promisify(execFile);
|
|
49
47
|
const REQUEST_SCHEMA_TO_OPERATION = Object.freeze(Object.fromEntries([
|
|
50
48
|
['lattice.adapter_dispatch_request.v1', 'dispatch'],
|
|
51
49
|
['lattice.adapter_observe_request.v1', 'observe'],
|
|
@@ -328,12 +326,7 @@ function quiescedState(rawState) {
|
|
|
328
326
|
async function observeWorkerProcessTree(pid, { requireDescendantsInRootGroup = true } = {}) {
|
|
329
327
|
let stdout;
|
|
330
328
|
try {
|
|
331
|
-
|
|
332
|
-
'-axo', 'pid=,ppid=,pgid=,state=,lstart=',
|
|
333
|
-
], {
|
|
334
|
-
encoding: 'utf8',
|
|
335
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
336
|
-
}));
|
|
329
|
+
stdout = await observePsSnapshot();
|
|
337
330
|
} catch { fail('WORK_ORDER_REPORT_INVALID', 'worker_pidをOS観測できない'); }
|
|
338
331
|
const records = new Map();
|
|
339
332
|
for (const rawLine of stdout.split('\n')) {
|