cuelo 0.6.8 → 0.6.9
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 +9 -9
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +2 -2
- package/.next/required-server-files.json +2 -2
- 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/sessions/search/route.js +1 -1
- 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 +6 -6
- 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 +9 -9
- 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-50d78c1f0e021377.js → layout-0082ed8a34057701.js} +1 -1
- package/.next/static/chunks/app/page-ca1d4378b3ad4549.js +22 -0
- package/.next/trace +105 -105
- package/.next/trace-build +1 -1
- package/README.md +10 -6
- package/Tools/CUELO_Setup/files/native-runtime-patch.js +5 -5
- package/Tools/OMP_Global_Config/agent/extensions/jev-runtime.ts +55 -13
- package/Tools/OMP_Global_Config/agent/extensions/lib/maker-routing.ts +57 -12
- package/Tools/OMP_Global_Config/agent/extensions/lib/routing-ledger.ts +36 -7
- package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +445 -53
- package/bin/prepare-runtime.js +37 -6
- package/install.mjs +3 -1
- package/package.json +8 -7
- package/.next/static/chunks/app/page-4c2544dfbb3d1f9a.js +0 -22
- /package/.next/static/{hPKzLPSEaHxrT4rE0H1XO → 4swMbGViH4DcOvsX9-ZTC}/_buildManifest.js +0 -0
- /package/.next/static/{hPKzLPSEaHxrT4rE0H1XO → 4swMbGViH4DcOvsX9-ZTC}/_ssgManifest.js +0 -0
package/.next/trace-build
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"run-webpack","duration":
|
|
1
|
+
[{"name":"run-webpack","duration":101868985,"timestamp":684209,"id":14,"parentId":1,"tags":{},"startTime":1790818791514,"traceId":"b778babbdc8d61c8"},{"name":"run-typescript","duration":16676433,"timestamp":102902705,"id":10735,"parentId":1,"tags":{},"startTime":1790818893732,"traceId":"b778babbdc8d61c8"},{"name":"static-check","duration":4397314,"timestamp":119679483,"id":10738,"parentId":1,"tags":{},"startTime":1790818910508,"traceId":"b778babbdc8d61c8"},{"name":"static-generation","duration":11479379,"timestamp":124174322,"id":10892,"parentId":1,"tags":{},"startTime":1790818915003,"traceId":"b778babbdc8d61c8"},{"name":"collect-build-traces","duration":20134699,"timestamp":124144006,"id":10889,"parentId":1,"tags":{},"startTime":1790818914972,"traceId":"b778babbdc8d61c8"},{"name":"telemetry-flush","duration":89,"timestamp":144283259,"id":10901,"parentId":1,"tags":{},"startTime":1790818935111,"traceId":"b778babbdc8d61c8"},{"name":"next-build","duration":143711222,"timestamp":572151,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1790818791402,"traceId":"b778babbdc8d61c8"}]
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Use the same `~/.omp/agent` sessions, credentials and model configuration from t
|
|
|
16
16
|
|
|
17
17
|
**한국어:** CUELO는 omp(oh-my-pi)를 위한 오픈소스 AI 코딩 에이전트 작업 공간이자 하네스입니다. 웹 앱은 omp SDK를 서버 안에서 실행하고, `omp` CLI와 같은 `~/.omp/agent` 디렉터리를 씁니다. 터미널에서 하던 세션을 브라우저에서 이어 가고, 다시 터미널로 돌아가도 기록·계정·모델 설정을 공유합니다.
|
|
18
18
|
|
|
19
|
-
내장 OMP 코어는 **18.4.
|
|
19
|
+
내장 OMP 코어는 **18.4.5**입니다. 추론 강도를 바꾸거나 서브에이전트를 다시 이어도 예비 모델 설정을 유지하며, 별도로 설치한 `omp` CLI의 버전은 웹 업데이트와 독립적입니다.
|
|
20
20
|
|
|
21
21
|
CUELO의 목적은 작업 중 확인한 실패와 막힘을 기억·작업 규칙·실행 절차에 반영해, 사람이 같은 문제를 다시 지적하지 않아도 다음 작업에서 실수를 줄이는 것입니다. 원인을 확인하고 필요한 부분만 바꾸며, 다음 작업에서 적용과 효과를 확인하는 것까지를 지향합니다. 기록을 저장했다는 이유만으로 자동 개선이 끝났다고 보지는 않습니다.
|
|
22
22
|
|
|
@@ -43,10 +43,11 @@ CUELO의 목적은 작업 중 확인한 실패와 막힘을 기억·작업 규
|
|
|
43
43
|
- 작업 로그 패널에서 이번 턴의 과정 항목과 턴마다 쓴 파일을 따로 봅니다.
|
|
44
44
|
- 입력창에 이미지·문서 외에 음성(받아쓰기), 코드·설정 파일(본문 첨부), 그 밖의 파일(저장 후 경로 첨부)을 붙여넣기·드롭·선택으로 넣습니다. 음성 받아쓰기에는 Google Antigravity 계정이 필요합니다.
|
|
45
45
|
- 에이전트가 생성하거나 확인한 이미지가 그 턴의 대화 흐름에 썸네일로 보이고, 누르면 크게 열립니다.
|
|
46
|
-
- Ctrl+K 팔레트로
|
|
46
|
+
- Ctrl+K 팔레트로 대화 내용을 검색해 해당 메시지로 이동합니다. 압축으로 사라진 과거 메시지는 제외하고, 현재 볼 수 있는 메시지를 세션당 최대 3건 보여 줍니다. 긴 대화 안에서는 Ctrl+F로 찾고, 오른쪽 질문 레일로 내가 한 질문 사이를 오갑니다.
|
|
47
47
|
- 실행이 3분 넘게 조용하면 멈춤 경고를, 서버나 PC가 꺼져 실행이 끊긴 대화를 열면 중단 알림을 보여 줍니다. 컨텍스트 압축 중에는 경과 시간을 표시합니다.
|
|
48
48
|
- 모델 선택기 맨 위에 자주 고른 모델이 나오고, 팔레트에서 넓은 대화 폭을 켤 수 있습니다.
|
|
49
|
-
- 입력창 아래 줄은 Main 프리셋 → 그 제공자의 모델 → 추론 강도
|
|
49
|
+
- 입력창 아래 줄은 Main 프리셋 → 그 제공자의 모델 → 추론 강도 순서입니다. "자동, 최대 high"처럼 상한을 고를 수 있고, Auto가 실제로 적용한 값은 「현재 추론 강도」로 따로 표시합니다. 새 대화는 선택한 상한 적용을 마친 뒤 첫 메시지를 보내며, 적용에 실패하면 재전송 때 다시 시도합니다.
|
|
50
|
+
- 도구 옆 시간에는 모델 응답 생성 시간을 포함하지 않습니다. 실행 전에 취소된 도구나 완료 시각을 확인할 수 없는 옛 기록에는 시간을 추정해 붙이지 않습니다.
|
|
50
51
|
|
|
51
52
|
**계정과 자원**
|
|
52
53
|
|
|
@@ -85,11 +86,11 @@ CUELO의 목적은 작업 중 확인한 실패와 막힘을 기억·작업 규
|
|
|
85
86
|
|
|
86
87
|
- 모델 역할(`default`, `smol`, `slow`, `plan`, `commit`, `task` 등)별 모델 지정·전환 — TUI의 `/model`과 같은 `config.yml`을 씁니다.
|
|
87
88
|
- 입력창의 `/` 명령은 대기 중뿐 아니라 실행 중에도 Enter·Steer·Follow-up으로 실행할 수 있습니다. 내장 명령은 즉시 처리하고, 발견된 확장·스킬·프롬프트 템플릿 명령은 해당 명령 경로로 전달합니다. 사용할 수 없거나 실행 중 허용되지 않는 명령은 안내하며 일반 채팅으로 전송하지 않습니다.
|
|
88
|
-
-
|
|
89
|
+
- 응답을 기다리며 보낸 Steer를 실시간으로 반영할 수 없을 때, 아직 답변이나 도구 호출이 나오지 않았다면 현재 추론 요청만 끊고 새 지시로 이어갑니다. 이미 보인 답변이나 도구 호출은 보존하며, 대화 전체를 중단하지 않습니다.
|
|
89
90
|
- 에이전트가 일반 구현 방식을 고르는 질문에 추천안을 붙여 물으면, 2분 동안 답이 없을 때 추천안으로 진행합니다. 질문 창에 그 안내가 표시되고, 옵션을 고르거나 답을 입력하면 대기 시간이 다시 시작됩니다. 자동 선택은 사용자 승인으로 치지 않으며, 배포·삭제·비용·계정·권한 확인 질문은 답할 때까지 기다립니다.
|
|
90
91
|
- Main이 OpenAI 모델에서 Fast를 켜면 새로 만드는 OpenAI 계열 SubAgent도 Fast로 요청합니다. Anthropic·Google SubAgent에는 Fast를 자동으로 넘기지 않고, Fast가 꺼져 있으면 SubAgent도 표준 처리로 요청합니다.
|
|
91
92
|
- 네이티브 창을 다루는 `computer.run`에서 버튼을 한 번 누른 뒤, 원하는 행·문구·값이 나타날 때까지 로컬 읽기로만 다시 확인하고 결과만 돌려줍니다. 기다리는 동안 다시 누르지 않으며, 제한 시간 초과·취소·읽기 오류를 서로 구분해 알립니다. 저장·전송처럼 결과가 남는 클릭의 승인 절차는 바뀌지 않습니다([사용법](./docs/harness.md#typed-judgment-routing-jev)).
|
|
92
|
-
- provider 로그인·API 키, `models.yml`, 플러그인, 스킬을 웹에서 관리합니다.
|
|
93
|
+
- provider 로그인·API 키, `models.yml`, 플러그인, 스킬을 웹에서 관리합니다. 플러그인 비활성화 설정을 저장해도 이미 불러온 플러그인은 CUELO 런타임을 재시작해야 멈춥니다. 「세션 다시 불러오기」는 플러그인을 해제하지 않습니다.
|
|
93
94
|
- 프로젝트 파일 탐색과 소스·문서·이미지·오디오·PDF 미리보기, Git worktree 전환.
|
|
94
95
|
- 프로젝트 신뢰: 설정에서 프로젝트별 승인을 관리·해제하고, 실행 중에는 작업을 끊지 않고 신뢰 승인을 예약합니다. 신뢰하지 않은 저장소의 확장·훅·도구·MCP는 실행하지 않습니다([docs/project-trust.md](./docs/project-trust.md)).
|
|
95
96
|
- 비밀번호 잠금(HTTP Basic Auth, scrypt 해시 저장)과 `/recover` 복구([docs/authentication.md](./docs/authentication.md)).
|
|
@@ -177,7 +178,8 @@ node install.mjs start
|
|
|
177
178
|
## 개발
|
|
178
179
|
|
|
179
180
|
```bash
|
|
180
|
-
bun install
|
|
181
|
+
bun install --backend=copyfile
|
|
182
|
+
bun run prepare:source # SDK 호환 패치를 이 작업 사본에 적용
|
|
181
183
|
bun run dev # http://127.0.0.1:30141
|
|
182
184
|
bun run typecheck
|
|
183
185
|
bun run lint
|
|
@@ -187,6 +189,8 @@ bun run test:coverage # 앱·설치기 테스트 + 커버리지 표
|
|
|
187
189
|
|
|
188
190
|
개발 서버를 쓰는 동안 같은 출력 경로에 `bun run build`를 돌리지 마세요. `.next/`를 덮어써서 개발 서버가 깨집니다. 검증용 production 빌드는 별도 소스 복사본이나 격리된 `CUELO_DIST_DIR`에서 실행하세요.
|
|
189
191
|
|
|
192
|
+
의존성을 다시 설치하거나 갱신한 뒤에는 `bun run prepare:source`를 다시 실행하세요. 이 준비 단계는 작업 사본의 SDK만 다루며 사용자 프로필이나 실행 중인 설치본을 변경하지 않습니다.
|
|
193
|
+
|
|
190
194
|
참고 문서: [OMP 하네스](./docs/harness.md) · [인증](./docs/authentication.md) · [프로젝트 신뢰](./docs/project-trust.md) · [worktree](./docs/worktrees.md) · [다국어](./docs/i18n.md) · [Docker](./docs/docker.md)
|
|
191
195
|
|
|
192
196
|
## 계보와 라이선스
|
|
@@ -9,12 +9,12 @@ 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.6.
|
|
12
|
+
version: '0.6.9',
|
|
13
13
|
distribution: 'cuelo',
|
|
14
|
-
coreVersion: '18.4.
|
|
14
|
+
coreVersion: '18.4.5',
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// OMP core 18.4.
|
|
17
|
+
// OMP core 18.4.5 `src/sdk.ts`: session-scoped AsyncJobManager for a process
|
|
18
18
|
// that hosts several top-level sessions (CUELO opens one per browser session).
|
|
19
19
|
// Upstream lets only the first top-level session construct a manager; every
|
|
20
20
|
// later one got `asyncJobManager: undefined`, so its `task` calls logged
|
|
@@ -23,7 +23,7 @@ const EXPECTED = {
|
|
|
23
23
|
// fix: it is disposed with its owning session (CUELO idle-shuts sessions
|
|
24
24
|
// after 10 minutes) and would cancel every other session's jobs, and a
|
|
25
25
|
// disposed manager throws on the next register(). Each edit is an exact text
|
|
26
|
-
// anchor of the pristine 18.4.
|
|
26
|
+
// anchor of the pristine 18.4.5 source (also present after core patch) and fails closed when it drifts.
|
|
27
27
|
// `insertBefore` edits keep their anchor (the helper is inserted in front of
|
|
28
28
|
// it), so their anchor count stays 1 after patching.
|
|
29
29
|
const SDK_FILE = path.join('node_modules', '@oh-my-pi', 'pi-coding-agent', 'src', 'sdk.ts');
|
|
@@ -177,7 +177,7 @@ function readNativePackage(target) {
|
|
|
177
177
|
throw new Error(`unexpected native package ${packageJson.name}@${packageJson.version}`);
|
|
178
178
|
}
|
|
179
179
|
if (!build || build.distribution !== EXPECTED.distribution || build.coreVersion !== EXPECTED.coreVersion) {
|
|
180
|
-
throw new Error('native cueloBuild metadata does not match cuelo / core 18.4.
|
|
180
|
+
throw new Error('native cueloBuild metadata does not match cuelo / core 18.4.5');
|
|
181
181
|
}
|
|
182
182
|
return packageJson;
|
|
183
183
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
-
import { registerMakerRouting } from "./lib/maker-routing";
|
|
3
|
+
import { registerMakerRouting, workspaceOf, type Owner } from "./lib/maker-routing";
|
|
4
4
|
import { clearPreparedTaskSession } from "./lib/prepared-task";
|
|
5
|
-
import { createRoutingLedger, DEFAULT_LEDGER_PATH, scopedAttempts, type AttemptIdentity, type OutcomeRecord, type VerdictRecord } from "./lib/routing-ledger";
|
|
5
|
+
import { createRoutingLedger, DEFAULT_LEDGER_PATH, scopedAttempts, type AttemptIdentity, type DispatchOwnership, type DispatchRecord, type OutcomeRecord, type VerdictRecord } from "./lib/routing-ledger";
|
|
6
6
|
import {
|
|
7
7
|
advanceTodoProgressState,
|
|
8
8
|
assessTodoProgress,
|
|
@@ -162,6 +162,8 @@ interface SpawnedTaskMeta {
|
|
|
162
162
|
agentId?: string;
|
|
163
163
|
/** 그 spawn이 등록된 async jobId. details가 단일 spawn의 jobId를 줄 때만 채운다. */
|
|
164
164
|
jobId?: string;
|
|
165
|
+
/** core spawn의 격리 요청(항목 값 우선, 없으면 상위 값). */
|
|
166
|
+
workspace: DispatchOwnership["workspace"];
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
function extractSpawnedTasks(
|
|
@@ -188,6 +190,7 @@ function extractSpawnedTasks(
|
|
|
188
190
|
// 현재/이전 effort는 블라인드 판정을 깨므로 state에 싣지 않는다.
|
|
189
191
|
guard,
|
|
190
192
|
todoBindings: captureTodoBindings(guard.progress, currentTodos),
|
|
193
|
+
workspace: workspaceOf(input, item),
|
|
191
194
|
});
|
|
192
195
|
}
|
|
193
196
|
return tasks;
|
|
@@ -858,7 +861,13 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
858
861
|
});
|
|
859
862
|
// spawn으로 실제 관측된 attempt. routing_verdict는 여기 있는 identity만 받는다.
|
|
860
863
|
// 상태를 함께 두어 실행 중 attempt를 완료로 오인하지 않게 하고, session_start에서 원장 scoped 기록으로 복원한다.
|
|
861
|
-
|
|
864
|
+
// ownership은 그 attempt dispatch의 소유 계약(과거 row는 null=미상), restored는 session_start 원장 복원 계열이다.
|
|
865
|
+
type ObservedAttempt = AttemptIdentity & {
|
|
866
|
+
name: string;
|
|
867
|
+
status: "running" | OutcomeRecord["status"];
|
|
868
|
+
ownership: DispatchOwnership | null;
|
|
869
|
+
restored: boolean;
|
|
870
|
+
};
|
|
862
871
|
const observedAttempts = new Map<string, ObservedAttempt>();
|
|
863
872
|
/** outcome을 이미 기록한 attempt. 같은 attempt를 두 번 소비하지 않는다. */
|
|
864
873
|
const settledAttempts = new Set<string>();
|
|
@@ -939,16 +948,17 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
939
948
|
}
|
|
940
949
|
const routing = registerMakerRouting(pi, {
|
|
941
950
|
ledger: baseLedger,
|
|
942
|
-
owners: () => {
|
|
951
|
+
owners: (ctx) => {
|
|
943
952
|
const activeTasks = [...liveMakers.values()].flat();
|
|
944
953
|
const activeNames = new Set(
|
|
945
954
|
activeTasks.flatMap((task) => task.name ? [task.name] : []),
|
|
946
955
|
);
|
|
947
|
-
const owners = [...knownMakers.values()].map((task) => ({
|
|
956
|
+
const owners: Owner[] = [...knownMakers.values()].map((task) => ({
|
|
948
957
|
name: task.name ?? null,
|
|
949
958
|
primaryDeliverable: task.guard.primaryDeliverable ?? null,
|
|
950
959
|
ownedPaths: task.guard.ownedPaths,
|
|
951
960
|
active: Boolean(task.name && activeNames.has(task.name)),
|
|
961
|
+
workspace: task.workspace,
|
|
952
962
|
}));
|
|
953
963
|
for (const task of activeTasks) {
|
|
954
964
|
if (task.name) continue;
|
|
@@ -957,8 +967,36 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
957
967
|
primaryDeliverable: task.guard.primaryDeliverable ?? null,
|
|
958
968
|
ownedPaths: task.guard.ownedPaths,
|
|
959
969
|
active: true,
|
|
970
|
+
workspace: task.workspace,
|
|
960
971
|
});
|
|
961
972
|
}
|
|
973
|
+
// reload 전 spawn은 원장 복원 attempt로만 남는다. 이 runtime이 다시 관측한 이름은 위 목록이 우선한다.
|
|
974
|
+
// 이름별(이름 없으면 assignment별) 최신 attempt 하나만 owner로 두어 재작업이 소유를 중복 주장하지 않게 한다.
|
|
975
|
+
const restored = new Map<string, ObservedAttempt>();
|
|
976
|
+
for (const entry of observedAttempts.values()) {
|
|
977
|
+
if (!entry.restored || (entry.name && knownMakers.has(entry.name))) continue;
|
|
978
|
+
restored.set(entry.name || entry.assignmentId, entry);
|
|
979
|
+
}
|
|
980
|
+
// 원장 running은 프로세스 재시작으로 끝나지 못한 row일 수 있다. core async snapshot에 지금 실행 중인
|
|
981
|
+
// job이 있을 때만 active로 보아, 끝난·죽은 attempt가 영구 잠금이 되지 않게 한다.
|
|
982
|
+
const running = ctx.getAsyncJobSnapshot?.()?.running ?? [];
|
|
983
|
+
for (const entry of restored.values()) {
|
|
984
|
+
const active = entry.status === "running" && running.some((job) =>
|
|
985
|
+
job.agentId === entry.agentId || (entry.jobId !== "" && job.id === entry.jobId));
|
|
986
|
+
if (entry.ownership) {
|
|
987
|
+
if (!entry.name && !active) continue;
|
|
988
|
+
owners.push({
|
|
989
|
+
name: entry.name || null,
|
|
990
|
+
primaryDeliverable: entry.ownership.primaryDeliverable,
|
|
991
|
+
ownedPaths: entry.ownership.ownedPaths,
|
|
992
|
+
active,
|
|
993
|
+
workspace: entry.ownership.workspace,
|
|
994
|
+
});
|
|
995
|
+
} else if (active) {
|
|
996
|
+
// 소유 경로를 기록하지 않은 과거 row는 빈 소유로 위장하지 않는다. 실행 중일 때만 미상 owner로 둔다.
|
|
997
|
+
owners.push({ name: entry.name || null, primaryDeliverable: null, ownedPaths: [], active, ownershipUnknown: true });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
962
1000
|
return owners;
|
|
963
1001
|
},
|
|
964
1002
|
// legacy-pi loader가 host SDK specifier를 재작성하므로 미러의 static runtime import는 불가하다.
|
|
@@ -1094,7 +1132,7 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
1094
1132
|
jobId: job.jobId,
|
|
1095
1133
|
};
|
|
1096
1134
|
const sourceDispatch = baseLedger.read()
|
|
1097
|
-
.filter((record) => record.type === "dispatch" && record.attemptId === source.attemptId)
|
|
1135
|
+
.filter((record): record is DispatchRecord => record.type === "dispatch" && record.attemptId === source.attemptId)
|
|
1098
1136
|
.at(-1);
|
|
1099
1137
|
baseLedger.append({
|
|
1100
1138
|
type: "dispatch",
|
|
@@ -1110,8 +1148,10 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
1110
1148
|
chosenEffort: sourceDispatch?.chosenEffort ?? "",
|
|
1111
1149
|
routingReason: sourceDispatch?.routingReason ?? false,
|
|
1112
1150
|
purpose: sourceDispatch?.purpose ?? null,
|
|
1151
|
+
// 재개 attempt는 원 attempt의 소유 계약을 그대로 잇는다. 미상이면 필드를 만들지 않는다.
|
|
1152
|
+
...(source.ownership ? { ownership: source.ownership } : {}),
|
|
1113
1153
|
});
|
|
1114
|
-
observedAttempts.set(identity.attemptId, { ...identity, name: source.name, status: "running" });
|
|
1154
|
+
observedAttempts.set(identity.attemptId, { ...identity, name: source.name, status: "running", ownership: source.ownership, restored: source.restored });
|
|
1115
1155
|
pendingResume.delete(agentId);
|
|
1116
1156
|
resumeAttemptId = identity.attemptId;
|
|
1117
1157
|
}
|
|
@@ -1253,7 +1293,7 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
1253
1293
|
preWriteJobs.clear();
|
|
1254
1294
|
pendingResume.clear();
|
|
1255
1295
|
for (const entry of scopedAttempts(baseLedger.read(), typeof sessionId === "string" ? sessionId : "")) {
|
|
1256
|
-
observedAttempts.set(entry.identity.attemptId, { ...entry.identity, name: entry.name, status: entry.status });
|
|
1296
|
+
observedAttempts.set(entry.identity.attemptId, { ...entry.identity, name: entry.name, status: entry.status, ownership: entry.ownership, restored: true });
|
|
1257
1297
|
if (entry.status !== "running") settledAttempts.add(entry.identity.attemptId);
|
|
1258
1298
|
}
|
|
1259
1299
|
// durable outcome이 남긴 jobId는 reload 뒤에도 중복 소비를 막는다.
|
|
@@ -1584,7 +1624,7 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
1584
1624
|
};
|
|
1585
1625
|
// 원 attempt의 dispatch metadata를 정확한 identity로 복사해 재작업 attempt가 같은 모델·effort 관측을 유지한다.
|
|
1586
1626
|
const source = baseLedger.read()
|
|
1587
|
-
.filter((record) => record.type === "dispatch" && record.attemptId === previous.attemptId)
|
|
1627
|
+
.filter((record): record is DispatchRecord => record.type === "dispatch" && record.attemptId === previous.attemptId)
|
|
1588
1628
|
.at(-1);
|
|
1589
1629
|
baseLedger.append({
|
|
1590
1630
|
type: "dispatch",
|
|
@@ -1600,8 +1640,9 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
1600
1640
|
chosenEffort: source?.chosenEffort ?? "",
|
|
1601
1641
|
routingReason: source?.routingReason ?? false,
|
|
1602
1642
|
purpose: source?.purpose ?? null,
|
|
1643
|
+
...(previous.ownership ? { ownership: previous.ownership } : {}),
|
|
1603
1644
|
});
|
|
1604
|
-
observedAttempts.set(identity.attemptId, { ...identity, name: previous.name, status: "running" });
|
|
1645
|
+
observedAttempts.set(identity.attemptId, { ...identity, name: previous.name, status: "running", ownership: previous.ownership, restored: previous.restored });
|
|
1605
1646
|
// 바인딩에 전송 직후 관측한 정확한 새 jobId를 둔다. 다른 job이 먼저 정산돼도 이 attempt를 소비하지 않는다.
|
|
1606
1647
|
resumeByAgent.set(targetAgentId, { attemptId: identity.attemptId, jobId: fresh[0]!, sentAt });
|
|
1607
1648
|
sendAdvisory(
|
|
@@ -1661,12 +1702,13 @@ export function createJevRuntime(deps: JevRuntimeDeps = {}) {
|
|
|
1661
1702
|
const dispatched = routing.noteSpawned(event.input, sessionId, event.toolCallId, ids);
|
|
1662
1703
|
const identities: string[] = [];
|
|
1663
1704
|
for (const task of spawned) {
|
|
1664
|
-
const
|
|
1665
|
-
if (!
|
|
1705
|
+
const entry = dispatched.get(task.index);
|
|
1706
|
+
if (!entry || !entry.identity.sessionId) continue;
|
|
1707
|
+
const { identity, ownership } = entry;
|
|
1666
1708
|
task.identity = identity;
|
|
1667
1709
|
if (identity.agentId) task.agentId = identity.agentId;
|
|
1668
1710
|
if (identity.jobId) task.jobId = identity.jobId;
|
|
1669
|
-
observedAttempts.set(identity.attemptId, { ...identity, name: task.name ?? "", status: "running" });
|
|
1711
|
+
observedAttempts.set(identity.attemptId, { ...identity, name: task.name ?? "", status: "running", ownership, restored: false });
|
|
1670
1712
|
identities.push(`${task.name ?? "<unnamed>"} agentId=${identity.agentId || "<unobserved>"} jobId=${identity.jobId || "<unobserved>"} ${identity.sessionId}|${identity.assignmentId}|${identity.attemptId}`);
|
|
1671
1713
|
}
|
|
1672
1714
|
if (identities.length > 0) {
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@oh-my-pi/pi-coding-agent";
|
|
3
3
|
import { isPathOwned, parseOwnedPaths } from "../command-guard/task-guard";
|
|
4
4
|
import { resolvePreparedTaskInput, storePreparedTaskBatch } from "./prepared-task";
|
|
5
|
-
import { assignmentsByName, summarizeHistory, type AttemptIdentity, type DispatchRecord, type RoutingHistory, type RoutingLedger } from "./routing-ledger";
|
|
5
|
+
import { assignmentsByName, summarizeHistory, type AttemptIdentity, type DispatchOwnership, type DispatchRecord, type RoutingHistory, type RoutingLedger } from "./routing-ledger";
|
|
6
6
|
|
|
7
7
|
export interface RoutingFacts {
|
|
8
8
|
goal: string;
|
|
@@ -25,6 +25,10 @@ export interface Owner {
|
|
|
25
25
|
primaryDeliverable: string | null;
|
|
26
26
|
ownedPaths: string[];
|
|
27
27
|
active?: boolean;
|
|
28
|
+
/** 관측된 spawn의 작업공간. 없으면 공유로 본다. */
|
|
29
|
+
workspace?: DispatchOwnership["workspace"];
|
|
30
|
+
/** 과거 원장 row처럼 소유 경로를 알 수 없는 owner. ownedPaths가 비어도 빈 소유가 아니다. */
|
|
31
|
+
ownershipUnknown?: boolean;
|
|
28
32
|
}
|
|
29
33
|
interface Candidate { profile: string; model: string; efforts: string[] }
|
|
30
34
|
/** registry·추론 강도 확인에 실패한 후보. 다른 후보의 발주를 막지 않고, 다른 모델로 대체하지도 않는다. */
|
|
@@ -75,7 +79,8 @@ export type QuotaSnapshot =
|
|
|
75
79
|
export interface RoutingDeps {
|
|
76
80
|
judge: (ctx: ExtensionContext, request: { state: unknown; questions: Record<string, RoutingQuestion> }, signal?: AbortSignal) => Promise<{ answers: Record<string, RoutingAnswer> }>;
|
|
77
81
|
settings: (ctx: ExtensionContext) => Promise<{ getModelRoles(): Readonly<Record<string, string>> } | undefined>;
|
|
78
|
-
|
|
82
|
+
/** 현재 owner 목록. ctx는 실행 중 여부를 core async snapshot으로 확인할 때 쓴다. */
|
|
83
|
+
owners: (ctx: ExtensionContext) => Owner[];
|
|
79
84
|
policy?: () => RoutingPolicy;
|
|
80
85
|
candidates?: (ctx: ExtensionContext, policy: RoutingPolicy) => Promise<Candidate[]>;
|
|
81
86
|
/** 후보 provider별 계정 잔량. 생략하면 CUELO usage 사이드카(`OMP_USAGE_PORT`, 기본 30142)를 읽는다. */
|
|
@@ -399,6 +404,20 @@ function relevantOwners(contract: DispatchContract, owners: Owner[]): Owner[] {
|
|
|
399
404
|
);
|
|
400
405
|
}
|
|
401
406
|
|
|
407
|
+
type Workspace = DispatchOwnership["workspace"];
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* core spawnParamsFor와 같은 우선순위로 task 항목의 격리 요청을 읽는다. 항목 값이 있으면 그것, 없으면 상위 값이다.
|
|
411
|
+
* isolated 요청은 격리 준비 실패 시 공유 cwd로 fallback하지 않고 실패하므로(structured-subagent) 별도 worktree scope다.
|
|
412
|
+
*/
|
|
413
|
+
export function workspaceOf(input: Record<string, unknown>, item: Record<string, unknown>): Workspace {
|
|
414
|
+
return (item.isolated !== undefined ? item.isolated : input.isolated) === true ? "isolated" : "shared";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 같은 checkout을 동시에 쓰는 경우만 충돌이다. 격리 worktree는 서로·공유 작업공간과 별도 scope다. */
|
|
418
|
+
const sharesCheckout = (left: Workspace | undefined, right: Workspace | undefined) =>
|
|
419
|
+
left !== "isolated" && right !== "isolated";
|
|
420
|
+
|
|
402
421
|
function ownerRevision(owners: Owner[]): string {
|
|
403
422
|
return JSON.stringify(owners.map((owner) => [
|
|
404
423
|
owner.name,
|
|
@@ -498,7 +517,9 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
498
517
|
};
|
|
499
518
|
const bindings = new Map<string, Promise<Prepared>>();
|
|
500
519
|
// 검사를 통과한 발주의 이력 초안. spawn 성공(noteSpawned) 때만 identity와 함께 ledger에 남긴다.
|
|
501
|
-
|
|
520
|
+
// 새 초안은 항상 소유 계약을 싣는다. 원장의 과거 row만 ownership이 없을 수 있다.
|
|
521
|
+
type DispatchDraft = Omit<DispatchRecord, "ts" | "ownership" | keyof AttemptIdentity> & { ownership: DispatchOwnership };
|
|
522
|
+
const pendingDispatches = new Map<string, DispatchDraft>();
|
|
502
523
|
// 거절 진단 전용 색인. 준비된 계약을 이름별로 시간순으로 모아 어긋난 필드를 지목하며,
|
|
503
524
|
// 통과·차단 판정에는 관여하지 않는다.
|
|
504
525
|
const preparedContracts = new Map<string, DispatchContract[]>();
|
|
@@ -646,7 +667,7 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
646
667
|
/** 배치 공통 정보(candidates·quota·instruction)는 한 번만, task별 route는 배열로 돌려준다. */
|
|
647
668
|
async function prepareBatch(context: string, tasks: RouteTask[], ctx: ExtensionContext, signal?: AbortSignal, callId = "") {
|
|
648
669
|
const current = policy();
|
|
649
|
-
const allOwners = deps.owners();
|
|
670
|
+
const allOwners = deps.owners(ctx);
|
|
650
671
|
const atGeneration = generation;
|
|
651
672
|
// TaskGuard는 같은 요청의 첫 child가 확립한 WORK_CLASS/PRIMARY_DELIVERABLE을 이후 child에
|
|
652
673
|
// 파생한다. 준비 도구도 같은 batch와 이미 연결된 task에서 그 두 필드만 상속한다.
|
|
@@ -877,7 +898,7 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
877
898
|
candidateLock ??= { workClass: contract.workClass, primaryDeliverable: contract.primaryDeliverable };
|
|
878
899
|
return contract;
|
|
879
900
|
});
|
|
880
|
-
const drafts: [string,
|
|
901
|
+
const drafts: [string, DispatchDraft][] = [];
|
|
881
902
|
for (const [itemIndex, item] of tasks.entries()) {
|
|
882
903
|
const requested = typeof item.model === "string" ? item.model : "";
|
|
883
904
|
const task = typeof item.task === "string" ? item.task : "";
|
|
@@ -911,7 +932,8 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
911
932
|
return { block: true, reason: "candidate 확인 중 같은 task 계약에 더 최신 maker_route 준비가 생겼습니다. 최신 판단으로 발주하세요." };
|
|
912
933
|
}
|
|
913
934
|
if (prepared.revision !== revisionOf(current, candidates)) return { block: true, reason: "후보 또는 판단 기준이 바뀌었습니다. maker_route로 변경된 조건만 다시 판단하세요." };
|
|
914
|
-
const
|
|
935
|
+
const allOwners = deps.owners(ctx);
|
|
936
|
+
const owners = relevantOwners(contract, allOwners);
|
|
915
937
|
if (ownerRevision(prepared.owners) !== ownerRevision(owners)) return { block: true, reason: "현재 소유권 충돌 조건이 바뀌었습니다. maker_route로 owner placement만 다시 판단하세요." };
|
|
916
938
|
// 같은 모델이 여러 profile에 있을 수 있다(HARD_*). 추천 profile, 이어서 추천 등급의
|
|
917
939
|
// profile 구간으로 검사해 등급의 강도 구간을 모델 이름으로 우회하지 못하게 한다. 추천 등급에 그 모델이
|
|
@@ -937,11 +959,17 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
937
959
|
const index = candidates.findIndex((candidate) => candidate.profile === selected.profile);
|
|
938
960
|
const recommendedEffort = selected.efforts.length === 1 ? selected.efforts[0] : answers?.[`effort${index}`]?.choice;
|
|
939
961
|
const placement = placementOf(prepared, owners);
|
|
940
|
-
const
|
|
962
|
+
const workspace = workspaceOf(canonicalInput, item);
|
|
963
|
+
// 같은 checkout을 쓰는 active owner만 충돌이다. 소유 경로 미상 active owner는 빈 소유로 보지 않고 공유 발주 전체와 충돌로 둔다.
|
|
964
|
+
const activeConflicts = [
|
|
965
|
+
...owners.filter((owner) => owner.active === true && sharesCheckout(owner.workspace, workspace)),
|
|
966
|
+
...allOwners.filter((owner) => owner.active === true && owner.ownershipUnknown === true && sharesCheckout(owner.workspace, workspace)),
|
|
967
|
+
];
|
|
941
968
|
if (activeConflicts.length > 0) {
|
|
969
|
+
const unknown = activeConflicts.some((owner) => owner.ownershipUnknown === true);
|
|
942
970
|
return {
|
|
943
971
|
block: true,
|
|
944
|
-
reason: `현재 active owner의 소유 경로와 충돌합니다: ${activeConflicts.map((owner) => owner.name ?? "<unnamed>").join(", ")}. 오래된 owner index나 다른 경로 owner로 자동 배정하지 말고 현재 owner에게 지시하거나 maker_route로 placement를 다시
|
|
972
|
+
reason: `현재 active owner의 소유 경로와 충돌합니다: ${activeConflicts.map((owner) => owner.name ?? "<unnamed>").join(", ")}. 오래된 owner index나 다른 경로 owner로 자동 배정하지 말고 현재 owner에게 지시하거나 maker_route로 placement를 다시 판단하세요.${unknown ? " 소유 경로가 기록되지 않은 과거 발주가 아직 실행 중이라 공유 작업공간의 어느 경로와도 겹칠 수 있다고 봅니다. 그 owner가 끝난 뒤 발주하거나 isolated로 발주하세요." : ""}`,
|
|
945
973
|
};
|
|
946
974
|
}
|
|
947
975
|
const existingOwner = placement.action === "instruct-existing" || placement.action === "retarget-existing";
|
|
@@ -964,8 +992,25 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
964
992
|
chosenEffort: effort,
|
|
965
993
|
routingReason,
|
|
966
994
|
purpose: guardFields(task)?.("PURPOSE").toLowerCase() || null,
|
|
995
|
+
ownership: { primaryDeliverable: contract.primaryDeliverable, ownedPaths: contract.ownedPaths, workspace },
|
|
967
996
|
}]);
|
|
968
997
|
}
|
|
998
|
+
// 같은 배치의 공유 작업공간 항목끼리는 게시 전에 한꺼번에 비교한다. 하나라도 겹치면 어떤 초안도 게시하지 않는다.
|
|
999
|
+
const overlaps: string[] = [];
|
|
1000
|
+
for (const [leftIndex, [, left]] of drafts.entries()) {
|
|
1001
|
+
for (const [, right] of drafts.slice(leftIndex + 1)) {
|
|
1002
|
+
if (!sharesCheckout(left.ownership.workspace, right.ownership.workspace)) continue;
|
|
1003
|
+
const shared = left.ownership.ownedPaths.filter((path) =>
|
|
1004
|
+
right.ownership.ownedPaths.some((other) => ownedPathsOverlap(path, other)));
|
|
1005
|
+
if (shared.length > 0) overlaps.push(`'${clipped(left.name, 40)}' ↔ '${clipped(right.name, 40)}'(${clippedList(shared)})`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (overlaps.length > 0) {
|
|
1009
|
+
return {
|
|
1010
|
+
block: true,
|
|
1011
|
+
reason: `같은 task 배치에서 공유 작업공간 Maker의 OWNED_PATHS가 겹칩니다: ${overlaps.slice(0, DIAGNOSTIC_LIST_LIMIT).join("; ")}. 배치 전체를 발주하지 않았습니다. 경로를 나누거나 한 owner에게 맡기거나, 겹치는 쪽을 순서대로 내거나 isolated로 발주하세요.`,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
969
1014
|
for (const [key, draft] of drafts) pendingDispatches.set(key, draft);
|
|
970
1015
|
return canonicalInput === input ? undefined : { input: canonicalInput };
|
|
971
1016
|
}
|
|
@@ -973,16 +1018,16 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
973
1018
|
/**
|
|
974
1019
|
* task spawn 성공 경계. TaskGuard lock을 확립하고, 검사를 통과해 실제로 spawn된 발주만 이력에 남긴다.
|
|
975
1020
|
* 실제 attempt identity는 이 spawn의 task 호출 id·index·session으로만 캡처한다(준비 handle과 섞지 않는다).
|
|
976
|
-
* 돌려주는 map은 task index → 관측된 identity
|
|
1021
|
+
* 돌려주는 map은 task index → 관측된 identity와 원장에 남긴 소유 계약이며, settle 시 같은 identity로 outcome을 잇는다.
|
|
977
1022
|
*/
|
|
978
1023
|
function noteSpawned(
|
|
979
1024
|
input: Record<string, unknown>,
|
|
980
1025
|
sessionId: string,
|
|
981
1026
|
callId: string,
|
|
982
1027
|
ids: ReadonlyMap<number, { agentId: string; jobId: string }>,
|
|
983
|
-
): Map<number, AttemptIdentity> {
|
|
1028
|
+
): Map<number, { identity: AttemptIdentity; ownership: DispatchOwnership }> {
|
|
984
1029
|
const tasks = (Array.isArray(input.tasks) ? input.tasks : [input]) as Record<string, unknown>[];
|
|
985
|
-
const dispatched = new Map<number, AttemptIdentity>();
|
|
1030
|
+
const dispatched = new Map<number, { identity: AttemptIdentity; ownership: DispatchOwnership }>();
|
|
986
1031
|
// 같은 session 원장에서 이름별 assignment·최대 attempt를 읽어 FINDING_ID 재작업만 같은 assignment로 잇는다.
|
|
987
1032
|
// 원장에서 읽으므로 reload 뒤에도 연결이 유지되고, 관측되지 않은 준비만으로는 연결하지 않는다.
|
|
988
1033
|
const known = assignmentsByName(deps.ledger?.read() ?? [], sessionId);
|
|
@@ -1012,7 +1057,7 @@ export function registerMakerRouting(pi: ExtensionAPI, deps: RoutingDeps) {
|
|
|
1012
1057
|
jobId: ids.get(index)?.jobId ?? "",
|
|
1013
1058
|
};
|
|
1014
1059
|
deps.ledger?.append({ ...draft, ...identity, ts: new Date().toISOString() });
|
|
1015
|
-
dispatched.set(index, identity);
|
|
1060
|
+
dispatched.set(index, { identity, ownership: draft.ownership });
|
|
1016
1061
|
known.set(name, { assignmentId, lastAttempt: attempt });
|
|
1017
1062
|
}
|
|
1018
1063
|
contractLock ??= candidateLock;
|
|
@@ -24,6 +24,16 @@ export interface AttemptIdentity {
|
|
|
24
24
|
jobId: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 실제 spawn으로 관측된 발주의 소유 계약. restart 뒤 owner 충돌 판정을 복원하는 근거다.
|
|
29
|
+
* workspace는 core spawn의 격리 요청이다: isolated 요청은 격리 준비에 실패하면 공유 cwd로 fallback하지 않고 실패한다.
|
|
30
|
+
*/
|
|
31
|
+
export interface DispatchOwnership {
|
|
32
|
+
primaryDeliverable: string;
|
|
33
|
+
ownedPaths: string[];
|
|
34
|
+
workspace: "shared" | "isolated";
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
export interface DispatchRecord extends AttemptIdentity {
|
|
28
38
|
type: "dispatch";
|
|
29
39
|
ts: string;
|
|
@@ -40,6 +50,8 @@ export interface DispatchRecord extends AttemptIdentity {
|
|
|
40
50
|
chosenEffort: string;
|
|
41
51
|
routingReason: boolean;
|
|
42
52
|
purpose: string | null;
|
|
53
|
+
/** 이 필드가 생기기 전 기록에는 없다. 없으면 소유 미상이며 빈 소유가 아니다. */
|
|
54
|
+
ownership?: DispatchOwnership;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export interface OutcomeRecord extends AttemptIdentity {
|
|
@@ -182,13 +194,27 @@ export function assignmentsByName(
|
|
|
182
194
|
return known;
|
|
183
195
|
}
|
|
184
196
|
|
|
197
|
+
/** 기록된 ownership이 기대 형태일 때만 쓴다. 과거·손상 row는 null(미상)이며 빈 소유로 바꾸지 않는다. */
|
|
198
|
+
function ownershipOf(record: DispatchRecord): DispatchOwnership | null {
|
|
199
|
+
const value = record.ownership;
|
|
200
|
+
if (!value || typeof value.primaryDeliverable !== "string" || !Array.isArray(value.ownedPaths)
|
|
201
|
+
|| !value.ownedPaths.every((path) => typeof path === "string")
|
|
202
|
+
|| (value.workspace !== "shared" && value.workspace !== "isolated")) return null;
|
|
203
|
+
return { primaryDeliverable: value.primaryDeliverable, ownedPaths: [...value.ownedPaths], workspace: value.workspace };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ScopedAttempt {
|
|
207
|
+
identity: AttemptIdentity;
|
|
208
|
+
name: string;
|
|
209
|
+
status: OutcomeRecord["status"] | "running";
|
|
210
|
+
/** 그 attempt dispatch의 소유 계약. dispatch가 없거나 과거 형식이면 null(미상). */
|
|
211
|
+
ownership: DispatchOwnership | null;
|
|
212
|
+
}
|
|
213
|
+
|
|
185
214
|
/** 같은 session에서 identity가 있는 attempt를 원장에서 복원한다. dispatch·outcome·verdict 어느 것으로도 등록된다. */
|
|
186
|
-
export function scopedAttempts(
|
|
187
|
-
records: readonly LedgerRecord[],
|
|
188
|
-
sessionId: string,
|
|
189
|
-
): { identity: AttemptIdentity; name: string; status: OutcomeRecord["status"] | "running" }[] {
|
|
215
|
+
export function scopedAttempts(records: readonly LedgerRecord[], sessionId: string): ScopedAttempt[] {
|
|
190
216
|
if (!sessionId) return [];
|
|
191
|
-
const byAttempt = new Map<string,
|
|
217
|
+
const byAttempt = new Map<string, ScopedAttempt>();
|
|
192
218
|
const order: string[] = [];
|
|
193
219
|
for (const record of records) {
|
|
194
220
|
if (!hasIdentity(record) || record.sessionId !== sessionId) continue;
|
|
@@ -204,9 +230,12 @@ export function scopedAttempts(
|
|
|
204
230
|
},
|
|
205
231
|
name: "",
|
|
206
232
|
status: "running",
|
|
233
|
+
ownership: null,
|
|
207
234
|
};
|
|
208
|
-
if (record.type === "dispatch")
|
|
209
|
-
|
|
235
|
+
if (record.type === "dispatch") {
|
|
236
|
+
entry.name = record.name;
|
|
237
|
+
entry.ownership = ownershipOf(record);
|
|
238
|
+
} else if (record.type === "outcome") entry.status = record.status;
|
|
210
239
|
if (!byAttempt.has(key)) order.push(key);
|
|
211
240
|
byAttempt.set(key, entry);
|
|
212
241
|
}
|