cuelo 0.10.69 → 0.10.71
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +23 -23
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/update-maintenance/route.js +3 -3
- package/.next/server/app/api/updates/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +4 -4
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover.html +1 -1
- package/.next/server/app/recover.rsc +2 -2
- package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
- package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
- package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
- package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +23 -23
- package/.next/server/chunks/1008.js +6 -5
- package/.next/server/chunks/402.js +1 -1
- package/.next/server/chunks/6429.js +4 -4
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/{layout-d9ccde470db63593.js → layout-69929b1152bb6134.js} +1 -1
- package/.next/static/chunks/app/{page-77d30b36da899bd2.js → page-07ae1110b1e4a1b6.js} +3 -3
- package/.next/trace +108 -108
- package/.next/trace-build +1 -1
- package/README.md +1 -1
- package/Tools/CUELO_Setup/files/native-runtime-patch.js +1 -1
- package/Tools/OMP_Global_Config/agent/extensions/command-guard/index.ts +174 -5
- package/Tools/OMP_Global_Config/agent/extensions/command-guard/task-guard.ts +216 -11
- package/Tools/OMP_Global_Config/agent/extensions/cross-review.ts +4 -2
- package/Tools/OMP_Global_Config/agent/extensions/edit-lessons.ts +114 -23
- package/Tools/OMP_Global_Config/agent/extensions/jev-runtime.ts +14 -15
- package/Tools/OMP_Global_Config/agent/extensions/lib/cross-review.ts +53 -10
- package/Tools/OMP_Global_Config/agent/extensions/lib/routing-ledger.ts +179 -0
- package/Tools/OMP_Global_Config/agent/rules/deploy-ops.md +1 -1
- package/Tools/OMP_Global_Config/agent/rules/harness-policy.json +1 -1
- package/Tools/OMP_Global_Config/agent/rules/subagent.md +4 -2
- package/bin/cuelo.js +3 -1
- package/install.mjs +277 -16
- package/package.json +1 -1
- /package/.next/static/{jzP8OH_sx-Lg230L5ANPF → UEE5papGQ6-VHe2VCtHNf}/_buildManifest.js +0 -0
- /package/.next/static/{jzP8OH_sx-Lg230L5ANPF → UEE5papGQ6-VHe2VCtHNf}/_ssgManifest.js +0 -0
package/.next/trace-build
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"run-webpack","duration":
|
|
1
|
+
[{"name":"run-webpack","duration":69369772,"timestamp":381380,"id":14,"parentId":1,"tags":{},"startTime":1792893364989,"traceId":"1e142890171be839"},{"name":"run-typescript","duration":14231324,"timestamp":70158528,"id":11007,"parentId":1,"tags":{},"startTime":1792893434766,"traceId":"1e142890171be839"},{"name":"static-check","duration":2436485,"timestamp":84458146,"id":11010,"parentId":1,"tags":{},"startTime":1792893449066,"traceId":"1e142890171be839"},{"name":"static-generation","duration":6868284,"timestamp":86970693,"id":11178,"parentId":1,"tags":{},"startTime":1792893451578,"traceId":"1e142890171be839"},{"name":"collect-build-traces","duration":15411171,"timestamp":86953851,"id":11175,"parentId":1,"tags":{},"startTime":1792893451561,"traceId":"1e142890171be839"},{"name":"telemetry-flush","duration":60,"timestamp":102368551,"id":11187,"parentId":1,"tags":{},"startTime":1792893466976,"traceId":"1e142890171be839"},{"name":"next-build","duration":102049528,"timestamp":319104,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1792893364926,"traceId":"1e142890171be839"}]
|
package/README.md
CHANGED
|
@@ -233,7 +233,7 @@ Windows의 WSL Ubuntu에서 실행 중인 CUELO를 갱신하는 동안에는 WSL
|
|
|
233
233
|
WSL 갱신에서는 시스템 시각이 보정되어도 실행 중인 서버를 같은 서버로 인식해, 진행 중인 대화의 중단·재개 절차를 건너뛰지 않습니다.
|
|
234
234
|
WSL 갱신은 실행 중인 서버와 분리해 새 버전을 미리 빌드한 뒤 교체합니다. 빌드 캐시는 재사용하고 다시 만들 산출물은 복사하지 않아 준비 과정의 디스크 쓰기를 줄입니다.
|
|
235
235
|
|
|
236
|
-
기존 프로필의 하네스·역할 설정은 앱 업데이트만으로 자동 덮어쓰지 않습니다. `
|
|
236
|
+
기존 프로필의 하네스·역할 설정은 앱 업데이트만으로 자동 덮어쓰지 않습니다. 새 버전의 하네스는 `node install.mjs harness-diff`로 차이를 보고 `node install.mjs harness-update`로 반영합니다. 사용자가 고치지 않은 파일만 백업 후 바꾸고, 고친 파일과 `config.yml`은 그대로 둡니다([기존 프로필의 하네스 갱신](./docs/installation.md#기존-프로필의-하네스-갱신)). 반영 후 CUELO와 `omp`를 다시 시작하세요.
|
|
237
237
|
|
|
238
238
|
```bash
|
|
239
239
|
git pull --ff-only
|
|
@@ -9,7 +9,7 @@ const MEMORY_RE = /(const phase1Model = await resolveMemoryModel\(\{[^}]*?fallba
|
|
|
9
9
|
const MEMORY_DONE = /const phase1Model = await resolveMemoryModel\(\{[^}]*?fallbackRole: "smol"/;
|
|
10
10
|
const EXPECTED = {
|
|
11
11
|
name: 'cuelo',
|
|
12
|
-
version: '0.10.
|
|
12
|
+
version: '0.10.71',
|
|
13
13
|
distribution: 'cuelo',
|
|
14
14
|
coreVersion: '18.12.6',
|
|
15
15
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
|
|
6
6
|
import { isToolCallEventType, type ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
7
7
|
import { resolveToCwd } from "@oh-my-pi/pi-coding-agent/tools/path-utils";
|
|
@@ -24,8 +24,12 @@ import { matchPowerShellSyntax } from "./powershell-syntax";
|
|
|
24
24
|
import {
|
|
25
25
|
bindSpawnAliases,
|
|
26
26
|
bindTaskGuardSession,
|
|
27
|
+
bindWriterOwners,
|
|
28
|
+
claimSharedWriters,
|
|
27
29
|
createOwnershipState,
|
|
28
30
|
createTaskGuardState,
|
|
31
|
+
createWriterClaimState,
|
|
32
|
+
decideOwnerWrite,
|
|
29
33
|
dropOwnedChildren,
|
|
30
34
|
hasUnreportedChildren,
|
|
31
35
|
matchBlockedEvalModelBridge,
|
|
@@ -40,16 +44,23 @@ import {
|
|
|
40
44
|
reservedMakers,
|
|
41
45
|
resetOwnershipState,
|
|
42
46
|
resetTaskGuardState,
|
|
47
|
+
resetWriterClaimState,
|
|
48
|
+
settleSharedWriters,
|
|
43
49
|
settleTaskCall,
|
|
44
50
|
stripScopePrefix,
|
|
45
51
|
unbindTaskGuardSession,
|
|
52
|
+
unbindWriterOwners,
|
|
53
|
+
writerOwnersOf,
|
|
46
54
|
type EvalCallInput,
|
|
47
55
|
type MakerSpawn,
|
|
56
|
+
type OwnershipOutsideRow,
|
|
48
57
|
type OwnershipState,
|
|
58
|
+
type RunningTaskJob,
|
|
49
59
|
type TaskCallInput,
|
|
50
60
|
type TaskCallOutcome,
|
|
51
61
|
type TaskJobRow,
|
|
52
62
|
type TreeSnapshot,
|
|
63
|
+
type WriterOwnerView,
|
|
53
64
|
} from "./task-guard";
|
|
54
65
|
|
|
55
66
|
/** WSL에서 Windows 실행 파일을 부를 수 있는지(binfmt interop 등록). Windows 출력 변환을 이때만 붙인다. */
|
|
@@ -105,6 +116,52 @@ function readTaskJobRegistry(ctx: unknown): TaskJobRow[] | undefined {
|
|
|
105
116
|
return rows;
|
|
106
117
|
}
|
|
107
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 세션이 소유한 실행 중 task job만 읽는다. core snapshot은 소유 세션 기준이라 child가 아니라 부모 ctx로 읽어야 한다.
|
|
121
|
+
* 조회 API가 없거나 실패하면 undefined(소유자 미상)이며, 빈 목록으로 대신하지 않는다.
|
|
122
|
+
*/
|
|
123
|
+
function readRunningTaskJobs(ctx: unknown): RunningTaskJob[] | undefined {
|
|
124
|
+
if (!ctx || typeof ctx !== "object" || !("getAsyncJobSnapshot" in ctx)) return undefined;
|
|
125
|
+
const read = ctx.getAsyncJobSnapshot;
|
|
126
|
+
if (typeof read !== "function") return undefined;
|
|
127
|
+
let snapshot: unknown;
|
|
128
|
+
try {
|
|
129
|
+
snapshot = read.call(ctx, { recentLimit: 0 });
|
|
130
|
+
} catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
if (!snapshot || typeof snapshot !== "object" || !("running" in snapshot) || !Array.isArray(snapshot.running)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const jobs: RunningTaskJob[] = [];
|
|
137
|
+
for (const job of snapshot.running) {
|
|
138
|
+
if (!job || typeof job !== "object" || !("type" in job) || job.type !== "task") continue;
|
|
139
|
+
if (!("id" in job) || typeof job.id !== "string" || !job.id) continue;
|
|
140
|
+
const agentId = "agentId" in job && typeof job.agentId === "string" && job.agentId ? job.agentId : undefined;
|
|
141
|
+
jobs.push({ id: job.id, ...(agentId ? { agentId } : {}) });
|
|
142
|
+
}
|
|
143
|
+
return jobs;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** edit·write 대상. core가 edit 이벤트 입력에 hashline 헤더를 풀어 `path`(한 파일)·`paths`(여러 파일)로 넣는다. */
|
|
147
|
+
function writeTargets(input: unknown): string[] {
|
|
148
|
+
if (!input || typeof input !== "object") return [];
|
|
149
|
+
const record = input as Record<string, unknown>;
|
|
150
|
+
const raw: unknown[] = [...(Array.isArray(record.paths) ? record.paths : []), record.path];
|
|
151
|
+
return [...new Set(raw.flatMap((value) => (typeof value === "string" && value.trim() ? [value.trim()] : [])))];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* OwnershipGuard의 `outside=none`이 아닌 완료 보고를 한 줄씩 덧붙이는 기록. 실행 프로필의 agent 루트
|
|
156
|
+
* (routing-ledger.jsonl·steering-gate-miss.jsonl과 같은 자리)에 둔다. 원장 레코드 종류는 늘리지 않는다.
|
|
157
|
+
*/
|
|
158
|
+
export const OWNERSHIP_OUTSIDE_LOG_PATH = join(import.meta.dir, "..", "..", "ownership-outside.jsonl");
|
|
159
|
+
|
|
160
|
+
export interface CommandGuardDeps {
|
|
161
|
+
/** 범위 밖 변경 기록 파일. 테스트만 바꾼다. 기본은 OWNERSHIP_OUTSIDE_LOG_PATH다. */
|
|
162
|
+
ownershipOutsideLog?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
108
165
|
/**
|
|
109
166
|
* bash 도구의 `cwd` 인자를 코어 bash와 같은 규칙으로 푼다. WSL에서 `E:/x`·`E:\x`는 코어가
|
|
110
167
|
* `/mnt/e/x`에서 실행하므로, `path.resolve`로 `<세션 cwd>/E:/x`를 만들면 없는 경로의 저장소를
|
|
@@ -159,6 +216,7 @@ function sameMakerReservation(
|
|
|
159
216
|
!right ||
|
|
160
217
|
left.name !== right.name ||
|
|
161
218
|
left.index !== right.index ||
|
|
219
|
+
left.isolated !== right.isolated ||
|
|
162
220
|
left.ownedPaths.length !== right.ownedPaths.length
|
|
163
221
|
) {
|
|
164
222
|
return false;
|
|
@@ -671,12 +729,105 @@ function hasOnlyExactCompletionCalls(input: EvalCallInput, target: string): bool
|
|
|
671
729
|
* 그 뒤 기존 task budget/ownership, eval bridge, bash guard를 같은 순서로 적용한다.
|
|
672
730
|
* 이 index.ts만 자동 로드되며 matcher/guard 오류는 잡지 않아 OMP 코어의 fail-closed 동작을 보존한다.
|
|
673
731
|
*/
|
|
674
|
-
|
|
732
|
+
function commandGuardWith(pi: ExtensionAPI, deps: CommandGuardDeps): void {
|
|
675
733
|
const taskGuard = createTaskGuardState();
|
|
676
734
|
const ownership = createOwnershipState();
|
|
677
735
|
const summonCalls = new Map<string, CharacterAlias[]>();
|
|
678
736
|
let pendingSummons: CharacterAlias[] = [];
|
|
679
737
|
let ownershipGeneration = 0;
|
|
738
|
+
const ownershipOutsideLog = deps.ownershipOutsideLog ?? OWNERSHIP_OUTSIDE_LOG_PATH;
|
|
739
|
+
|
|
740
|
+
// OwnerWriteGuard: 이 세션이 띄운 공유 작업공간 Maker의 쓰기 소유. child 세션은 자기 parentId로 이 view를 찾는다.
|
|
741
|
+
// 실행 중 job은 소유 세션 기준 snapshot에만 보이므로 이 세션이 최근 받은 ctx로 읽는다.
|
|
742
|
+
const writers = createWriterClaimState();
|
|
743
|
+
let ownerCtx: unknown;
|
|
744
|
+
let ownerCwd = "";
|
|
745
|
+
let boundOwnerId = "";
|
|
746
|
+
const writerView: WriterOwnerView = {
|
|
747
|
+
get cwd() {
|
|
748
|
+
return ownerCwd;
|
|
749
|
+
},
|
|
750
|
+
state: writers,
|
|
751
|
+
running: () => readRunningTaskJobs(ownerCtx),
|
|
752
|
+
};
|
|
753
|
+
const bindWriterView = (ctx: { cwd?: string; agent?: { id?: string } }): void => {
|
|
754
|
+
ownerCtx = ctx;
|
|
755
|
+
ownerCwd = eventSessionCwd(ctx);
|
|
756
|
+
const id = (ctx.agent?.id ?? "").trim();
|
|
757
|
+
if (!id || id === boundOwnerId) return;
|
|
758
|
+
if (boundOwnerId) unbindWriterOwners(boundOwnerId, writerView);
|
|
759
|
+
bindWriterOwners(id, writerView);
|
|
760
|
+
boundOwnerId = id;
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
/** 소유 밖 변경·미관측 보고 한 건을 덧붙인다. 진단 기록 실패가 보고 전달을 막지 않는다. */
|
|
764
|
+
const recordOwnershipOutside = (
|
|
765
|
+
row: OwnershipOutsideRow,
|
|
766
|
+
ctx: { sessionManager?: { getSessionId?: () => string } } | undefined,
|
|
767
|
+
): void => {
|
|
768
|
+
try {
|
|
769
|
+
const sessionId = (ctx?.sessionManager?.getSessionId?.() ?? "").trim();
|
|
770
|
+
appendFileSync(ownershipOutsideLog, `${JSON.stringify({ ts: new Date().toISOString(), sessionId, ...row })}\n`);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
pi.logger?.debug("command-guard: ownership-outside 기록 실패", {
|
|
773
|
+
error: error instanceof Error ? error.message : String(error),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* 공유 작업공간 Maker child의 edit·write가 같은 부모의 다른 활성 Maker 소유 경로를 건드리면 막을 이유를 돌린다.
|
|
780
|
+
* Main·Maker가 아닌 agent·격리 child(부모와 cwd가 다름)는 대상이 아니고, 부모 view나 job snapshot이 없어 소유자를
|
|
781
|
+
* 확정하지 못하면 debug 기록만 남기고 막지 않는다. bash 쓰기는 대상 경로를 믿을 만하게 풀 수 없어 보지 않는다.
|
|
782
|
+
*/
|
|
783
|
+
const ownerWriteBlockReason = (
|
|
784
|
+
toolName: string,
|
|
785
|
+
input: unknown,
|
|
786
|
+
ctx: { cwd?: string; agent?: { kind?: string; id?: string; name?: string; parentId?: string } },
|
|
787
|
+
): string | undefined => {
|
|
788
|
+
const agent = ctx.agent;
|
|
789
|
+
if (!agent) {
|
|
790
|
+
pi.logger?.debug("command-guard: OwnerWriteGuard 판정 생략(ctx.agent 없음)");
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
if (agent.kind !== "sub" || agent.name !== "maker") return undefined;
|
|
794
|
+
const skip = (why: string, detail?: Record<string, unknown>): undefined => {
|
|
795
|
+
pi.logger?.debug(`command-guard: OwnerWriteGuard 판정 생략(${why})`, { agentId: agent.id, ...detail });
|
|
796
|
+
return undefined;
|
|
797
|
+
};
|
|
798
|
+
const selfId = (agent.id ?? "").trim();
|
|
799
|
+
const parentId = (agent.parentId ?? "").trim();
|
|
800
|
+
if (!selfId || !parentId) return skip("agent id 미상");
|
|
801
|
+
const view = writerOwnersOf(parentId);
|
|
802
|
+
if (!view) return skip("부모 view 없음", { parentId });
|
|
803
|
+
const cwd = eventSessionCwd(ctx);
|
|
804
|
+
if (!view.cwd || normalizeRepositoryPath(cwd) !== normalizeRepositoryPath(view.cwd)) {
|
|
805
|
+
return skip("공유 checkout 아님", { cwd, parentCwd: view.cwd });
|
|
806
|
+
}
|
|
807
|
+
const targets: string[] = [];
|
|
808
|
+
for (const path of writeTargets(input)) {
|
|
809
|
+
let absolute: string;
|
|
810
|
+
try {
|
|
811
|
+
absolute = resolveToCwd(path, cwd);
|
|
812
|
+
} catch {
|
|
813
|
+
continue; // agent://·proc:// 같은 내부 URL은 작업 트리 파일이 아니다.
|
|
814
|
+
}
|
|
815
|
+
const rel = relative(view.cwd, absolute);
|
|
816
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
|
|
817
|
+
targets.push(rel);
|
|
818
|
+
}
|
|
819
|
+
if (targets.length === 0) return undefined;
|
|
820
|
+
const decision = decideOwnerWrite(view, selfId, targets);
|
|
821
|
+
if (!decision) return skip("부모 job snapshot 없음", { parentId });
|
|
822
|
+
if (decision.unknown.length > 0) skip("소유 주장 없는 실행 중 child", { unknown: decision.unknown });
|
|
823
|
+
if (!decision.conflict) return undefined;
|
|
824
|
+
const { owner, path } = decision.conflict;
|
|
825
|
+
return (
|
|
826
|
+
`[OwnerWriteGuard] ${path}은(는) 지금 실행 중인 다른 Maker ${owner}의 OWNED_PATHS 안이라 ${toolName}을 막았습니다. ` +
|
|
827
|
+
`직접 고치지 말고 필요한 변경·이유·근거 위치를 write agent://${parentId}로 Main에 보고하세요. ` +
|
|
828
|
+
"자기 OWNED_PATHS 안이나 다른 실행 중 Maker가 소유하지 않은 경로는 그대로 쓸 수 있습니다."
|
|
829
|
+
);
|
|
830
|
+
};
|
|
680
831
|
|
|
681
832
|
/**
|
|
682
833
|
* 한 terminal event의 exact child들은 스냅샷 하나를 공유한다. await 중 reset되거나 다른
|
|
@@ -684,7 +835,7 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
684
835
|
*/
|
|
685
836
|
const reportPendingOwnedChildren = async (
|
|
686
837
|
ids: readonly string[],
|
|
687
|
-
ctx: { cwd?: string } | undefined,
|
|
838
|
+
ctx: { cwd?: string; sessionManager?: { getSessionId?: () => string } } | undefined,
|
|
688
839
|
): Promise<string[]> => {
|
|
689
840
|
const pendingKeys = exactUnreportedOwnedChildKeys(ownership, ids);
|
|
690
841
|
if (pendingKeys.length === 0) return [];
|
|
@@ -696,7 +847,7 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
696
847
|
return entry !== undefined && !entry.reported;
|
|
697
848
|
});
|
|
698
849
|
if (stillPendingKeys.length === 0) return [];
|
|
699
|
-
return reportOwnedChildren(ownership, stillPendingKeys, tree);
|
|
850
|
+
return reportOwnedChildren(ownership, stillPendingKeys, tree, (row) => recordOwnershipOutside(row, ctx));
|
|
700
851
|
};
|
|
701
852
|
|
|
702
853
|
// tool-capable child가 실제로 뜨지 못했으면 첫 행동 계약을 다시 요구한다.
|
|
@@ -725,6 +876,8 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
725
876
|
outcome: TaskCallOutcome,
|
|
726
877
|
progress: readonly { index: number; id: string }[],
|
|
727
878
|
): void => {
|
|
879
|
+
// 쓰기 소유 주장은 요청 경계와 무관하게 이 호출의 결과로 in-flight를 내리고 canonical id를 묶는다.
|
|
880
|
+
settleSharedWriters(writers, toolCallId, progress);
|
|
728
881
|
const before = reservedMakers(taskGuard, toolCallId) ?? [];
|
|
729
882
|
const settlement = settleTaskCall(taskGuard, toolCallId, outcome);
|
|
730
883
|
if (!settlement) return;
|
|
@@ -760,6 +913,7 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
760
913
|
ownershipGeneration += 1;
|
|
761
914
|
resetTaskGuardState(taskGuard);
|
|
762
915
|
resetOwnershipState(ownership);
|
|
916
|
+
resetWriterClaimState(writers);
|
|
763
917
|
summonCalls.clear();
|
|
764
918
|
pendingSummons = [];
|
|
765
919
|
bindSession(ctx);
|
|
@@ -768,6 +922,8 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
768
922
|
pi.on("session_shutdown", () => {
|
|
769
923
|
if (boundSessionId) unbindTaskGuardSession(boundSessionId, taskGuard);
|
|
770
924
|
boundSessionId = "";
|
|
925
|
+
if (boundOwnerId) unbindWriterOwners(boundOwnerId, writerView);
|
|
926
|
+
boundOwnerId = "";
|
|
771
927
|
});
|
|
772
928
|
|
|
773
929
|
// 실제 사용자 입력 하나를 budget 단위로 본다. extension 내부 주입은 같은 요청의
|
|
@@ -915,6 +1071,8 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
915
1071
|
};
|
|
916
1072
|
}
|
|
917
1073
|
registerSpawnedMakers(ownership, event.toolCallId, currentMakers, snapshot);
|
|
1074
|
+
claimSharedWriters(writers, event.toolCallId, currentMakers);
|
|
1075
|
+
bindWriterView(ctx);
|
|
918
1076
|
}
|
|
919
1077
|
// task guard의 metadata 파생이 character summon brief보다 나중에 적용돼야 둘 다 보존된다.
|
|
920
1078
|
if (decision.input) return { input: decision.input };
|
|
@@ -922,6 +1080,11 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
922
1080
|
return;
|
|
923
1081
|
}
|
|
924
1082
|
|
|
1083
|
+
if (event.toolName === "edit" || event.toolName === "write") {
|
|
1084
|
+
const reason = ownerWriteBlockReason(event.toolName, event.input, ctx);
|
|
1085
|
+
if (reason) return { block: true, reason };
|
|
1086
|
+
}
|
|
1087
|
+
|
|
925
1088
|
// 호출을 받은 뒤 모드·roster가 바뀌어 alias의 target을 더는 풀 수 없으면 우회하지 않고 막는다(다음 사용자 입력이 새로 정한다).
|
|
926
1089
|
const unresolvedSummons = pendingSummons.filter((alias) => characterTarget(alias) === undefined);
|
|
927
1090
|
if (unresolvedSummons.length > 0) return { block: true, reason: unresolvedSummonReason(unresolvedSummons) };
|
|
@@ -1108,3 +1271,9 @@ export default function commandGuard(pi: ExtensionAPI): void {
|
|
|
1108
1271
|
);
|
|
1109
1272
|
});
|
|
1110
1273
|
}
|
|
1274
|
+
|
|
1275
|
+
export function createCommandGuard(deps: CommandGuardDeps = {}): (pi: ExtensionAPI) => void {
|
|
1276
|
+
return (pi) => commandGuardWith(pi, deps);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export default createCommandGuard();
|
|
@@ -64,6 +64,8 @@ export type MakerSpawn = {
|
|
|
64
64
|
/** 원본 tasks[] 안의 위치. progress[].index가 같은 기준을 쓰므로 이 값으로 대응시킨다. */
|
|
65
65
|
index: number;
|
|
66
66
|
ownedPaths: string[];
|
|
67
|
+
/** 격리 worktree에서 실행한다(`isolated: true|"capture"`). 공유 checkout의 쓰기 소유자가 아니다. */
|
|
68
|
+
isolated?: true;
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
export type TaskGuardState = {
|
|
@@ -105,6 +107,8 @@ type SpawnItem = {
|
|
|
105
107
|
name?: string;
|
|
106
108
|
/** 호출이 agent를 생략했다. 위임 정의가 maker 하나뿐이라 maker로 해석하고 반환 input에도 기록한다. */
|
|
107
109
|
agentOmitted?: true;
|
|
110
|
+
/** 항목 값이 있으면 그것, 없으면 상위 `isolated`가 격리를 요청했다(core spawnParamsFor와 같은 우선순위). */
|
|
111
|
+
isolated?: true;
|
|
108
112
|
};
|
|
109
113
|
|
|
110
114
|
export type TaskCallInput = Record<string, unknown>;
|
|
@@ -297,6 +301,11 @@ function canonicalizeTask(
|
|
|
297
301
|
return task.slice(0, raw.span.start) + rendered + task.slice(raw.span.end);
|
|
298
302
|
}
|
|
299
303
|
|
|
304
|
+
/** `isolated: true`와 `"capture"`(patch만 보존)는 둘 다 별도 worktree scope다(maker-routing workspaceOf와 같은 기준). */
|
|
305
|
+
function isolationRequested(value: unknown): boolean {
|
|
306
|
+
return value === true || value === "capture";
|
|
307
|
+
}
|
|
308
|
+
|
|
300
309
|
function normalizeSpawnItems(input: TaskCallInput): SpawnItem[] | string {
|
|
301
310
|
if (Array.isArray(input.tasks)) {
|
|
302
311
|
if (input.tasks.length === 0) return "[SpawnGuard] tasks[]가 비어 있습니다.";
|
|
@@ -316,6 +325,9 @@ function normalizeSpawnItems(input: TaskCallInput): SpawnItem[] | string {
|
|
|
316
325
|
...(typeof item.name === "string" && item.name.trim()
|
|
317
326
|
? { name: item.name.trim() }
|
|
318
327
|
: {}),
|
|
328
|
+
...(isolationRequested(item.isolated !== undefined ? item.isolated : input.isolated)
|
|
329
|
+
? { isolated: true as const }
|
|
330
|
+
: {}),
|
|
319
331
|
});
|
|
320
332
|
}
|
|
321
333
|
return items;
|
|
@@ -332,6 +344,7 @@ function normalizeSpawnItems(input: TaskCallInput): SpawnItem[] | string {
|
|
|
332
344
|
...(typeof input.name === "string" && input.name.trim()
|
|
333
345
|
? { name: input.name.trim() }
|
|
334
346
|
: {}),
|
|
347
|
+
...(isolationRequested(input.isolated) ? { isolated: true as const } : {}),
|
|
335
348
|
},
|
|
336
349
|
];
|
|
337
350
|
}
|
|
@@ -467,6 +480,7 @@ export function reserveTaskCall(
|
|
|
467
480
|
...(item.name ? { name: item.name } : {}),
|
|
468
481
|
index,
|
|
469
482
|
ownedPaths: metadata.ownedPaths,
|
|
483
|
+
...(item.isolated ? { isolated: true as const } : {}),
|
|
470
484
|
});
|
|
471
485
|
}
|
|
472
486
|
|
|
@@ -907,10 +921,15 @@ function outsidePaths(ownedPaths: readonly string[], changedPaths: readonly stri
|
|
|
907
921
|
.sort();
|
|
908
922
|
}
|
|
909
923
|
|
|
924
|
+
/** 정규화한 소유 경로와 소유 밖 변경. 스냅샷이 없으면 판정할 수 없어 `unobserved`다. */
|
|
925
|
+
function ownershipOutcome(input: OwnershipReportInput): { owned: string[]; outside: string[] | "unobserved" } {
|
|
926
|
+
const owned = input.ownedPaths.map(normalizeOwnedPath).filter((entry) => entry.length > 0);
|
|
927
|
+
return { owned, outside: input.changedPaths === undefined ? "unobserved" : outsidePaths(owned, input.changedPaths) };
|
|
928
|
+
}
|
|
929
|
+
|
|
910
930
|
/** OwnershipGuard 보고 한 줄. 공백 구분 key=value이며 인용부호를 쓰지 않는다. */
|
|
911
931
|
export function buildOwnershipReport(input: OwnershipReportInput): string {
|
|
912
|
-
const owned = input
|
|
913
|
-
const outside = input.changedPaths === undefined ? "unobserved" : outsidePaths(owned, input.changedPaths);
|
|
932
|
+
const { owned, outside } = ownershipOutcome(input);
|
|
914
933
|
return (
|
|
915
934
|
`[OwnershipGuard] child=${encodeReportToken(input.child)}` +
|
|
916
935
|
` owned=${owned.map(encodeReportToken).join(",")}` +
|
|
@@ -1103,14 +1122,29 @@ function findOwnedChildKey(state: OwnershipState, id: string): string | undefine
|
|
|
1103
1122
|
return found;
|
|
1104
1123
|
}
|
|
1105
1124
|
|
|
1125
|
+
/**
|
|
1126
|
+
* 소유 밖 변경이 있었거나(`outside` 목록) 변경을 관측하지 못한(`unobserved`) 완료 보고 한 건.
|
|
1127
|
+
* 보고 줄은 그 턴의 대화에만 남으므로 빈도를 셀 수 있게 index.ts가 이 행을 JSONL로 덧붙인다.
|
|
1128
|
+
*/
|
|
1129
|
+
export type OwnershipOutsideRow = {
|
|
1130
|
+
child: string;
|
|
1131
|
+
/** spawn 이름과 task 결과 progress id(canonical agent id). */
|
|
1132
|
+
childIds: string[];
|
|
1133
|
+
owned: string[];
|
|
1134
|
+
outside: string[] | "unobserved";
|
|
1135
|
+
concurrent: string[];
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1106
1138
|
/**
|
|
1107
1139
|
* 종료가 관측된 child들의 OwnershipGuard 줄을 만든다. child마다 정확히 한 번만 보고하고
|
|
1108
|
-
* (보고 후 스냅샷을 버리고 reported로 표시) 모르는 id는 무시한다.
|
|
1140
|
+
* (보고 후 스냅샷을 버리고 reported로 표시) 모르는 id는 무시한다. `outside=none`이 아닌 보고는
|
|
1141
|
+
* onOutside에도 넘긴다.
|
|
1109
1142
|
*/
|
|
1110
1143
|
export function reportOwnedChildren(
|
|
1111
1144
|
state: OwnershipState,
|
|
1112
1145
|
ids: readonly string[],
|
|
1113
1146
|
tree: TreeSnapshot | undefined,
|
|
1147
|
+
onOutside?: (row: OwnershipOutsideRow) => void,
|
|
1114
1148
|
): string[] {
|
|
1115
1149
|
const lines: string[] = [];
|
|
1116
1150
|
for (const id of ids) {
|
|
@@ -1124,14 +1158,185 @@ export function reportOwnedChildren(
|
|
|
1124
1158
|
const concurrent = concurrentChildNames(state, entry, entry.endOrder);
|
|
1125
1159
|
const changedPaths = entry.snapshot && tree ? diffTreeSnapshots(entry.snapshot, tree) : undefined;
|
|
1126
1160
|
entry.snapshot = undefined;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
);
|
|
1161
|
+
const input: OwnershipReportInput = {
|
|
1162
|
+
child: entry.child,
|
|
1163
|
+
ownedPaths: entry.ownedPaths,
|
|
1164
|
+
...(changedPaths ? { changedPaths } : {}),
|
|
1165
|
+
concurrent,
|
|
1166
|
+
};
|
|
1167
|
+
lines.push(buildOwnershipReport(input));
|
|
1168
|
+
const { owned, outside } = ownershipOutcome(input);
|
|
1169
|
+
if (onOutside && (outside === "unobserved" || outside.length > 0)) {
|
|
1170
|
+
onOutside({ child: entry.child, childIds: [...entry.aliases], owned, outside, concurrent });
|
|
1171
|
+
}
|
|
1135
1172
|
}
|
|
1136
1173
|
return lines;
|
|
1137
1174
|
}
|
|
1175
|
+
|
|
1176
|
+
// ============================================================================
|
|
1177
|
+
// OwnerWriteGuard: 공유 작업공간 Maker child가 같은 부모의 다른 실행 중 Maker가 소유한 경로에 edit·write하는 것을 막는다.
|
|
1178
|
+
// OWNED_PATHS 겹침은 발주 때만(maker-routing) 검사됐고, 실행 중 범위 밖 쓰기는 완료 뒤 OwnershipGuard 보고로만 드러났다.
|
|
1179
|
+
// 다른 활성 owner가 없는 범위 밖 경로(직접 원인인 인접 파일)는 그대로 허용한다. 소유자를 확정하지 못하면 막지 않는다.
|
|
1180
|
+
// ============================================================================
|
|
1181
|
+
|
|
1182
|
+
/** 공유 작업공간 maker spawn 하나의 쓰기 소유 주장. */
|
|
1183
|
+
type WriterClaim = {
|
|
1184
|
+
/** 보고 라벨: tasks[].name 또는 `unnamed`. */
|
|
1185
|
+
child: string;
|
|
1186
|
+
name?: string;
|
|
1187
|
+
ownedPaths: string[];
|
|
1188
|
+
/** spawn한 task 호출 id와 원본 tasks[] 위치. */
|
|
1189
|
+
callId: string;
|
|
1190
|
+
index: number;
|
|
1191
|
+
/** task 결과 progress가 준 canonical agent id. 실행 중 job과는 이 값으로만 대응시킨다. */
|
|
1192
|
+
ids: string[];
|
|
1193
|
+
/** 등록 순서. 같은 id를 가진 주장이 여럿이면 가장 최근 것이 그 job의 주장이다. */
|
|
1194
|
+
order: number;
|
|
1195
|
+
/** spawn한 task 호출이 아직 끝나지 않았다. foreground child는 job snapshot에 보이지 않으므로 이 표시로 실행 중이다. */
|
|
1196
|
+
inFlight: boolean;
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* 한 부모 세션의 쓰기 소유 주장. 완료 보고용 OwnershipState와 달리 사용자 입력마다 비우지 않는다:
|
|
1201
|
+
* 앞 요청이 띄운 Maker도 아직 실행 중이면 그 경로의 owner다. 세션이 바뀔 때만 비운다.
|
|
1202
|
+
*/
|
|
1203
|
+
export type WriterClaimState = { claims: Map<string, WriterClaim>; order: number };
|
|
1204
|
+
|
|
1205
|
+
export function createWriterClaimState(): WriterClaimState {
|
|
1206
|
+
return { claims: new Map(), order: 0 };
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export function resetWriterClaimState(state: WriterClaimState): void {
|
|
1210
|
+
state.claims.clear();
|
|
1211
|
+
state.order = 0;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** spawn 직전 공유 작업공간 maker 항목을 in-flight 주장으로 등록한다. 격리 항목은 공유 checkout을 쓰지 않아 주장하지 않는다. */
|
|
1215
|
+
export function claimSharedWriters(state: WriterClaimState, callId: string, makers: readonly MakerSpawn[]): void {
|
|
1216
|
+
for (const maker of makers) {
|
|
1217
|
+
if (maker.isolated) continue;
|
|
1218
|
+
const key = ownedChildKey(callId, maker.index);
|
|
1219
|
+
if (state.claims.has(key)) continue;
|
|
1220
|
+
state.order += 1;
|
|
1221
|
+
state.claims.set(key, {
|
|
1222
|
+
child: maker.name ?? "unnamed",
|
|
1223
|
+
...(maker.name ? { name: maker.name } : {}),
|
|
1224
|
+
ownedPaths: [...maker.ownedPaths],
|
|
1225
|
+
callId,
|
|
1226
|
+
index: maker.index,
|
|
1227
|
+
ids: [],
|
|
1228
|
+
order: state.order,
|
|
1229
|
+
inFlight: true,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* spawn한 task 호출이 끝났다(tool_result 또는 실행 전 종료). progress id를 canonical id로 묶고 in-flight를 내린다.
|
|
1236
|
+
* 이후 활성 여부는 부모의 실행 중 job으로만 정하므로, 뜨지 못한 child의 주장은 저절로 비활성이다.
|
|
1237
|
+
*/
|
|
1238
|
+
export function settleSharedWriters(
|
|
1239
|
+
state: WriterClaimState,
|
|
1240
|
+
callId: string,
|
|
1241
|
+
progress: readonly { index: number; id: string }[],
|
|
1242
|
+
): void {
|
|
1243
|
+
for (const claim of state.claims.values()) {
|
|
1244
|
+
if (claim.callId !== callId) continue;
|
|
1245
|
+
claim.inFlight = false;
|
|
1246
|
+
const id = progress.find((row) => row.index === claim.index)?.id;
|
|
1247
|
+
if (id && !claim.ids.includes(id)) claim.ids.push(id);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** 부모 세션이 소유한 실행 중 task job 한 건(core async job snapshot 행). */
|
|
1252
|
+
export type RunningTaskJob = { id: string; agentId?: string };
|
|
1253
|
+
|
|
1254
|
+
/** child가 부모 agent id로 찾는 부모 세션의 쓰기 소유 창구. */
|
|
1255
|
+
export type WriterOwnerView = {
|
|
1256
|
+
/** 부모 세션 cwd(절대경로). 같은 cwd의 child만 공유 checkout을 쓴다. */
|
|
1257
|
+
cwd: string;
|
|
1258
|
+
state: WriterClaimState;
|
|
1259
|
+
/** 부모 세션이 소유한 실행 중 task job. 읽지 못하면 undefined(소유자 미상). */
|
|
1260
|
+
running(): RunningTaskJob[] | undefined;
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
/** 부모와 child 세션은 같은 프로세스에서 확장 module을 공유한다. 여러 번 평가되어도 같은 map을 보도록 globalThis에 둔다. */
|
|
1264
|
+
const WRITER_OWNERS = Symbol.for("@oh-my-pi/omp/writer-owners/v1");
|
|
1265
|
+
const writerOwners: Map<string, WriterOwnerView> = (() => {
|
|
1266
|
+
const existing = Reflect.get(globalThis, WRITER_OWNERS) as Map<string, WriterOwnerView> | undefined;
|
|
1267
|
+
if (existing) return existing;
|
|
1268
|
+
const created = new Map<string, WriterOwnerView>();
|
|
1269
|
+
Reflect.set(globalThis, WRITER_OWNERS, created);
|
|
1270
|
+
return created;
|
|
1271
|
+
})();
|
|
1272
|
+
|
|
1273
|
+
export function bindWriterOwners(agentId: string, view: WriterOwnerView): void {
|
|
1274
|
+
const key = agentId.trim();
|
|
1275
|
+
if (key) writerOwners.set(key, view);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/** 자기 view일 때만 푼다. 같은 agent id를 이어받은 새 세션의 연결을 옛 인스턴스의 늦은 정리가 지우지 않는다. */
|
|
1279
|
+
export function unbindWriterOwners(agentId: string, view: WriterOwnerView): void {
|
|
1280
|
+
const key = agentId.trim();
|
|
1281
|
+
if (key && writerOwners.get(key) === view) writerOwners.delete(key);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export function writerOwnersOf(agentId: string): WriterOwnerView | undefined {
|
|
1285
|
+
return writerOwners.get(agentId.trim());
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
export type OwnerWriteDecision = {
|
|
1289
|
+
/** 다른 활성 owner가 가진 첫 대상. 없으면 허용이다. */
|
|
1290
|
+
conflict?: { owner: string; path: string };
|
|
1291
|
+
/** 소유 주장을 찾지 못한 실행 중 job·이름 없는 in-flight spawn. 막을 근거가 아니며 진단에만 쓴다. */
|
|
1292
|
+
unknown: string[];
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* child `selfId`가 부모 cwd 기준 상대경로 `targets`에 쓰려 할 때 다른 활성 owner와 충돌하는지 본다. 부모의 job snapshot을
|
|
1297
|
+
* 읽지 못하면 undefined(소유자 미상)다. 활성 owner는 spawn 호출이 아직 끝나지 않은 주장과, canonical id의 job이 지금
|
|
1298
|
+
* 실행 중인 주장(REWORK 재개 포함)이다. 자기 소유 경로는 겹쳐도 허용하고, 자기일 수 있는 주장은 다른 owner로 보지 않는다.
|
|
1299
|
+
*/
|
|
1300
|
+
export function decideOwnerWrite(
|
|
1301
|
+
view: WriterOwnerView,
|
|
1302
|
+
selfId: string,
|
|
1303
|
+
targets: readonly string[],
|
|
1304
|
+
): OwnerWriteDecision | undefined {
|
|
1305
|
+
const running = view.running();
|
|
1306
|
+
if (!running) return undefined;
|
|
1307
|
+
const own: WriterClaim[] = [];
|
|
1308
|
+
const others: WriterClaim[] = [];
|
|
1309
|
+
const unknown: string[] = [];
|
|
1310
|
+
for (const claim of view.state.claims.values()) {
|
|
1311
|
+
if (!claim.inFlight) continue;
|
|
1312
|
+
if (!claim.name) {
|
|
1313
|
+
unknown.push(`in-flight:${claim.callId}#${claim.index}`);
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
// 이름 있는 child의 agent id는 이름이거나 충돌 변형(`이름-2`, `이름-3` …)이다. 자기일 수 있으면 자기로 본다.
|
|
1317
|
+
const suffix = selfId.startsWith(`${claim.name}-`) ? selfId.slice(claim.name.length + 1) : "";
|
|
1318
|
+
(selfId === claim.name || /^\d+$/.test(suffix) ? own : others).push(claim);
|
|
1319
|
+
}
|
|
1320
|
+
for (const job of running) {
|
|
1321
|
+
// job id는 실행 handle이라 agent id와 다를 수 있다(충돌 시 `이름-2` 등). 주장과 자기 판정은 canonical agent id로만 하고,
|
|
1322
|
+
// snapshot에 agentId가 없는 옛 행만 job id를 대신 쓴다.
|
|
1323
|
+
const agentId = job.agentId ?? job.id;
|
|
1324
|
+
let match: WriterClaim | undefined;
|
|
1325
|
+
for (const claim of view.state.claims.values()) {
|
|
1326
|
+
if (!claim.ids.includes(agentId)) continue;
|
|
1327
|
+
if (!match || claim.order > match.order) match = claim;
|
|
1328
|
+
}
|
|
1329
|
+
if (!match) {
|
|
1330
|
+
if (agentId !== selfId) unknown.push(agentId);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
(agentId === selfId ? own : others).push(match);
|
|
1334
|
+
}
|
|
1335
|
+
const ownPaths = own.flatMap((claim) => claim.ownedPaths);
|
|
1336
|
+
for (const path of targets) {
|
|
1337
|
+
if (isPathOwned(ownPaths, path)) continue;
|
|
1338
|
+
const owner = others.find((claim) => isPathOwned(claim.ownedPaths, path));
|
|
1339
|
+
if (owner) return { conflict: { owner: owner.child, path: collapseOwnedPath(path) }, unknown };
|
|
1340
|
+
}
|
|
1341
|
+
return { unknown };
|
|
1342
|
+
}
|