parallel-codex-tui 0.1.3 → 0.1.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/.parallel-codex/config.example.toml +136 -3
- package/README.md +299 -21
- package/dist/bootstrap.js +37 -17
- package/dist/cli-args.js +34 -3
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +82 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +7 -71
- package/dist/cli.js +234 -24
- package/dist/core/collaboration-timeline.js +268 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +297 -109
- package/dist/core/file-store.js +119 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +462 -35
- package/dist/core/session-index.js +412 -88
- package/dist/core/session-manager.js +1110 -40
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +18 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +373 -34
- package/dist/domain/schemas.js +142 -7
- package/dist/orchestrator/collaboration-channel.js +289 -6
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +2086 -203
- package/dist/orchestrator/prompts.js +168 -5
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +927 -0
- package/dist/tui/App.js +3187 -161
- package/dist/tui/AppShell.js +196 -25
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +232 -0
- package/dist/tui/InputBar.js +581 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +210 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1404 -161
- package/dist/tui/WorkerOverviewView.js +269 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +49 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +360 -11
- package/dist/tui/task-memory.js +13 -1
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +213 -0
- package/dist/workers/live-probe.js +177 -0
- package/dist/workers/mock-adapter.js +73 -8
- package/dist/workers/native-attach.js +106 -16
- package/dist/workers/process-adapter.js +572 -77
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -20
- package/package.json +11 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { NativeSessionSchema, RetiredNativeSessionSchema, TurnMetaSchema, WorkerStatusSchema } from "../domain/schemas.js";
|
|
4
|
+
import { pathExists, readJson, readTextIfExists } from "./file-store.js";
|
|
5
|
+
export async function loadTaskSessionDetails(input) {
|
|
6
|
+
const [turns, workers] = await Promise.all([
|
|
7
|
+
readTaskTurns(input.taskDir),
|
|
8
|
+
readTaskWorkers(input.taskDir, input.modelNames ?? {})
|
|
9
|
+
]);
|
|
10
|
+
const turnById = new Map(turns.map((turn) => [turn.turnId, turn]));
|
|
11
|
+
for (const worker of workers) {
|
|
12
|
+
let turn = turnById.get(worker.turnId);
|
|
13
|
+
if (!turn) {
|
|
14
|
+
turn = {
|
|
15
|
+
turnId: worker.turnId,
|
|
16
|
+
createdAt: worker.lastActivityAt,
|
|
17
|
+
request: "",
|
|
18
|
+
workers: []
|
|
19
|
+
};
|
|
20
|
+
turnById.set(worker.turnId, turn);
|
|
21
|
+
turns.push(turn);
|
|
22
|
+
}
|
|
23
|
+
turn.workers.push(worker);
|
|
24
|
+
}
|
|
25
|
+
turns.sort((left, right) => left.turnId.localeCompare(right.turnId));
|
|
26
|
+
for (const turn of turns) {
|
|
27
|
+
turn.workers.sort(compareTaskSessionWorkers);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
task: input.task,
|
|
31
|
+
projectName: basename(input.task.cwd) || input.task.cwd,
|
|
32
|
+
projectPath: input.task.cwd,
|
|
33
|
+
turns,
|
|
34
|
+
workers
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function readTaskTurns(taskDir) {
|
|
38
|
+
const turnsDir = join(taskDir, "turns");
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = await readdir(turnsDir, { withFileTypes: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const turns = await Promise.all(entries
|
|
47
|
+
.filter((entry) => entry.isDirectory() && /^\d{4}$/.test(entry.name))
|
|
48
|
+
.map(async (entry) => {
|
|
49
|
+
const dir = join(turnsDir, entry.name);
|
|
50
|
+
try {
|
|
51
|
+
const meta = await readJson(join(dir, "turn.json"), TurnMetaSchema);
|
|
52
|
+
if (meta.turn_id !== entry.name) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
turnId: meta.turn_id,
|
|
57
|
+
createdAt: meta.created_at,
|
|
58
|
+
request: compactTaskRequest(await readTextIfExists(join(dir, "user.md"))),
|
|
59
|
+
workers: []
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
return turns
|
|
67
|
+
.filter((turn) => turn !== null)
|
|
68
|
+
.sort((left, right) => left.turnId.localeCompare(right.turnId));
|
|
69
|
+
}
|
|
70
|
+
async function readTaskWorkers(taskDir, modelNames) {
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = await readdir(taskDir, { withFileTypes: true });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const workers = await Promise.all(entries
|
|
79
|
+
.filter((entry) => entry.isDirectory())
|
|
80
|
+
.map(async (entry) => {
|
|
81
|
+
const dir = join(taskDir, entry.name);
|
|
82
|
+
const statusPath = join(dir, "status.json");
|
|
83
|
+
if (!(await pathExists(statusPath))) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const status = await readJson(statusPath, WorkerStatusSchema);
|
|
88
|
+
const nativeSession = await readActiveNativeSession(dir, status.worker_id);
|
|
89
|
+
return {
|
|
90
|
+
id: status.worker_id,
|
|
91
|
+
turnId: taskSessionWorkerTurnId(status.worker_id, status.feature_id),
|
|
92
|
+
...(status.feature_id ? { featureId: status.feature_id } : {}),
|
|
93
|
+
...(status.feature_title ? { featureTitle: status.feature_title } : {}),
|
|
94
|
+
role: status.role,
|
|
95
|
+
engine: status.engine,
|
|
96
|
+
model: status.model_name?.trim() || modelNames[status.engine]?.trim() || "",
|
|
97
|
+
...(status.model_provider?.trim() ? { modelProvider: status.model_provider.trim() } : {}),
|
|
98
|
+
state: status.state,
|
|
99
|
+
phase: status.phase,
|
|
100
|
+
summary: status.summary,
|
|
101
|
+
lastActivityAt: laterTimestamp(status.last_event_at, nativeSession?.lastUsedAt),
|
|
102
|
+
dir,
|
|
103
|
+
statusPath,
|
|
104
|
+
outputLogPath: join(dir, "output.log"),
|
|
105
|
+
nativeSession
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
return workers
|
|
113
|
+
.filter((worker) => worker !== null)
|
|
114
|
+
.sort((left, right) => (left.turnId.localeCompare(right.turnId)
|
|
115
|
+
|| compareTaskSessionWorkers(left, right)));
|
|
116
|
+
}
|
|
117
|
+
async function readActiveNativeSession(workerDir, workerId) {
|
|
118
|
+
const activePath = join(workerDir, "native-session.json");
|
|
119
|
+
if (!(await pathExists(activePath))) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const active = await readJson(activePath, NativeSessionSchema);
|
|
124
|
+
if (active.worker_id !== workerId) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const retiredPath = join(workerDir, "native-session.retired.json");
|
|
128
|
+
if (await pathExists(retiredPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const retired = await readJson(retiredPath, RetiredNativeSessionSchema);
|
|
131
|
+
if (retired.session_id === active.session_id) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// A malformed retirement record cannot hide a valid active session.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
sessionId: active.session_id,
|
|
141
|
+
cwd: active.cwd,
|
|
142
|
+
writableDirs: active.writable_dirs ?? [],
|
|
143
|
+
createdAt: active.created_at,
|
|
144
|
+
lastUsedAt: active.last_used_at,
|
|
145
|
+
source: active.source
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export function taskSessionWorkerTurnId(workerId, featureId) {
|
|
153
|
+
const featureTurn = featureId?.match(/^(\d{4})(?:-|$)/)?.[1];
|
|
154
|
+
const waveTurn = workerId.match(/-wave-(\d{4})-/)?.[1];
|
|
155
|
+
const finalTurn = workerId.match(/-final-(\d{4})$/)?.[1];
|
|
156
|
+
const taskTurn = workerId.match(/-(\d{4})$/)?.[1];
|
|
157
|
+
return featureTurn ?? waveTurn ?? finalTurn ?? taskTurn ?? "0001";
|
|
158
|
+
}
|
|
159
|
+
function compareTaskSessionWorkers(left, right) {
|
|
160
|
+
return taskSessionWorkerStage(left) - taskSessionWorkerStage(right)
|
|
161
|
+
|| left.id.localeCompare(right.id);
|
|
162
|
+
}
|
|
163
|
+
function taskSessionWorkerStage(worker) {
|
|
164
|
+
if (worker.role === "judge" && /-final-\d{4}$/.test(worker.id)) {
|
|
165
|
+
return 4;
|
|
166
|
+
}
|
|
167
|
+
return ["main", "judge", "actor", "critic"].indexOf(worker.role);
|
|
168
|
+
}
|
|
169
|
+
function compactTaskRequest(request) {
|
|
170
|
+
const value = request.replace(/\s+/g, " ").trim();
|
|
171
|
+
return value.length > 160 ? `${value.slice(0, 157)}...` : value;
|
|
172
|
+
}
|
|
173
|
+
function laterTimestamp(left, right) {
|
|
174
|
+
return right && right.localeCompare(left) > 0 ? right : left;
|
|
175
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const TASK_STATE_TRANSITIONS = {
|
|
2
|
+
created: new Set(["routed", "paused", "failed", "cancelled"]),
|
|
3
|
+
routed: new Set(["judging", "paused", "failed", "cancelled"]),
|
|
4
|
+
judging: new Set(["ready_for_pair", "paused", "failed", "cancelled"]),
|
|
5
|
+
ready_for_pair: new Set(["actor_running", "paused", "done", "failed", "cancelled"]),
|
|
6
|
+
actor_running: new Set(["critic_running", "verifying", "paused", "failed", "cancelled"]),
|
|
7
|
+
critic_running: new Set(["revision_needed", "integrating", "paused", "failed", "cancelled"]),
|
|
8
|
+
revision_needed: new Set(["actor_running", "paused", "failed", "cancelled"]),
|
|
9
|
+
integrating: new Set(["actor_running", "verifying", "paused", "done", "failed", "cancelled"]),
|
|
10
|
+
verifying: new Set(["revision_needed", "integrating", "paused", "done", "failed", "cancelled"]),
|
|
11
|
+
paused: new Set(["routed", "judging", "ready_for_pair", "failed", "cancelled"]),
|
|
12
|
+
done: new Set(["routed", "cancelled"]),
|
|
13
|
+
failed: new Set(["routed", "judging", "ready_for_pair"]),
|
|
14
|
+
cancelled: new Set(["routed", "judging", "ready_for_pair"])
|
|
15
|
+
};
|
|
16
|
+
export function taskStateTransitionAllowed(from, to) {
|
|
17
|
+
return from === to || TASK_STATE_TRANSITIONS[from].has(to);
|
|
18
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sessionsRoot } from "./paths.js";
|
|
4
|
+
const TASK_DIRECTORY_PATTERN = /^task-/;
|
|
5
|
+
const TURN_DIRECTORY_PATTERN = /^turn-(\d{4,})$/;
|
|
6
|
+
const WAVE_DIRECTORY_PATTERN = /^wave-(\d{4,})$/;
|
|
7
|
+
const COMMIT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
|
|
8
|
+
const COMMIT_PROTOCOL = "atomic-claim-v1";
|
|
9
|
+
export async function reconcileWorkspaceCommitIntents(projectRoot, dataDir) {
|
|
10
|
+
const recovery = { cleaned: 0, preserved: 0 };
|
|
11
|
+
const taskEntries = await directoryEntries(sessionsRoot(projectRoot, dataDir));
|
|
12
|
+
for (const taskEntry of taskEntries) {
|
|
13
|
+
if (!taskEntry.isDirectory() || !TASK_DIRECTORY_PATTERN.test(taskEntry.name)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const workspacesDir = join(sessionsRoot(projectRoot, dataDir), taskEntry.name, "workspaces");
|
|
17
|
+
for (const turnEntry of await directoryEntries(workspacesDir)) {
|
|
18
|
+
const turnMatch = turnEntry.isDirectory() ? TURN_DIRECTORY_PATTERN.exec(turnEntry.name) : null;
|
|
19
|
+
if (!turnMatch) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
for (const waveEntry of await directoryEntries(join(workspacesDir, turnEntry.name))) {
|
|
23
|
+
const waveMatch = waveEntry.isDirectory() ? WAVE_DIRECTORY_PATTERN.exec(waveEntry.name) : null;
|
|
24
|
+
if (!waveMatch) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const waveDir = join(workspacesDir, turnEntry.name, waveEntry.name);
|
|
28
|
+
const pendingPath = join(waveDir, "integration.pending.json");
|
|
29
|
+
const pendingRecord = await readJsonRecord(pendingPath);
|
|
30
|
+
if (pendingRecord === undefined) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const pending = parseCommitEvidence(pendingRecord, "committing");
|
|
34
|
+
const integrated = parseCommitEvidence(await readJsonRecord(join(waveDir, "integration.json")), "integrated");
|
|
35
|
+
const directoryTurnId = turnMatch[1];
|
|
36
|
+
const directoryWave = Number(waveMatch[1]);
|
|
37
|
+
if (!pending
|
|
38
|
+
|| !integrated
|
|
39
|
+
|| pending.turnId !== directoryTurnId
|
|
40
|
+
|| pending.wave !== directoryWave
|
|
41
|
+
|| !sameCommit(pending, integrated)) {
|
|
42
|
+
recovery.preserved += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await rm(pendingPath, { force: true });
|
|
47
|
+
recovery.cleaned += 1;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
recovery.preserved += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return recovery;
|
|
56
|
+
}
|
|
57
|
+
async function directoryEntries(path) {
|
|
58
|
+
try {
|
|
59
|
+
return await readdir(path, { withFileTypes: true });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function readJsonRecord(path) {
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
68
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
69
|
+
? value
|
|
70
|
+
: null;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return error.code === "ENOENT" ? undefined : null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function parseCommitEvidence(record, state) {
|
|
77
|
+
const featureIds = stringArray(record?.feature_ids);
|
|
78
|
+
const changedPaths = stringArray(record?.changed_paths);
|
|
79
|
+
const commitId = record?.commit_id;
|
|
80
|
+
const commitProtocol = record?.commit_protocol;
|
|
81
|
+
if (record?.version !== 1
|
|
82
|
+
|| record.state !== state
|
|
83
|
+
|| typeof record.turn_id !== "string"
|
|
84
|
+
|| !Number.isInteger(record.wave)
|
|
85
|
+
|| record.wave < 1
|
|
86
|
+
|| !featureIds
|
|
87
|
+
|| !changedPaths
|
|
88
|
+
|| (commitId !== undefined && (typeof commitId !== "string" || !COMMIT_ID_PATTERN.test(commitId)))
|
|
89
|
+
|| (commitProtocol !== undefined && commitProtocol !== COMMIT_PROTOCOL)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
version: 1,
|
|
94
|
+
state,
|
|
95
|
+
turnId: record.turn_id,
|
|
96
|
+
wave: record.wave,
|
|
97
|
+
featureIds,
|
|
98
|
+
...(typeof commitId === "string" ? { commitId } : {}),
|
|
99
|
+
...(commitProtocol === COMMIT_PROTOCOL ? { commitProtocol } : {}),
|
|
100
|
+
changedPaths
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function stringArray(value) {
|
|
104
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
105
|
+
? value
|
|
106
|
+
: null;
|
|
107
|
+
}
|
|
108
|
+
function sameCommit(pending, integrated) {
|
|
109
|
+
return pending.turnId === integrated.turnId
|
|
110
|
+
&& pending.wave === integrated.wave
|
|
111
|
+
&& sameStrings(pending.featureIds, integrated.featureIds)
|
|
112
|
+
&& sameStrings(pending.changedPaths, integrated.changedPaths)
|
|
113
|
+
&& (pending.commitId === undefined || pending.commitId === integrated.commitId)
|
|
114
|
+
&& (pending.commitProtocol === undefined || pending.commitProtocol === integrated.commitProtocol);
|
|
115
|
+
}
|
|
116
|
+
function sameStrings(left, right) {
|
|
117
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
118
|
+
}
|
package/dist/core/workspace.js
CHANGED
|
@@ -2,9 +2,12 @@ import { join, resolve } from "node:path";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { ensureDir, pathExists, pathIsDirectory, readJson, readTextIfExists, writeJson, writeText } from "./file-store.js";
|
|
5
|
+
import { runWithLeaseFinalization } from "./lease-finalization.js";
|
|
6
|
+
import { acquireProcessMutationTurn } from "./process-mutation-turn.js";
|
|
5
7
|
const lastWorkspaceFile = "last-workspace";
|
|
6
8
|
const workspacesFile = "workspaces.json";
|
|
7
9
|
const maxRememberedWorkspaces = 20;
|
|
10
|
+
const workspaceRegistryIntentPrefix = ".workspace-registry-claim-";
|
|
8
11
|
const WorkspaceRegistrySchema = z.object({
|
|
9
12
|
version: z.literal(1).default(1),
|
|
10
13
|
workspaces: z
|
|
@@ -73,18 +76,23 @@ export async function listWorkspaceChoices(appRoot) {
|
|
|
73
76
|
.map(({ selectable: _selectable, ...choice }) => choice);
|
|
74
77
|
}
|
|
75
78
|
async function rememberWorkspace(appRoot, workspaceRoot) {
|
|
76
|
-
const now = new Date().toISOString();
|
|
77
79
|
const resolved = resolveWorkspacePath(process.cwd(), workspaceRoot);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
const mutationTurn = await acquireProcessMutationTurn(join(appRoot, ".parallel-codex"), {
|
|
81
|
+
intentPrefix: workspaceRegistryIntentPrefix,
|
|
82
|
+
timeoutMessage: "Timed out waiting to update the workspace registry."
|
|
83
|
+
});
|
|
84
|
+
await runWithLeaseFinalization("Workspace registry update", mutationTurn, async () => {
|
|
85
|
+
const current = await readWorkspaceEntries(appRoot);
|
|
86
|
+
const next = {
|
|
87
|
+
version: 1,
|
|
88
|
+
workspaces: [
|
|
89
|
+
{ path: resolved, last_used_at: new Date().toISOString() },
|
|
90
|
+
...current.filter((entry) => resolveStoredWorkspacePath(appRoot, entry.path) !== resolved)
|
|
91
|
+
].slice(0, maxRememberedWorkspaces)
|
|
92
|
+
};
|
|
93
|
+
await writeJson(workspacesPath(appRoot), WorkspaceRegistrySchema.parse(next));
|
|
94
|
+
await writeText(lastWorkspacePath(appRoot), `${resolved}\n`);
|
|
95
|
+
});
|
|
88
96
|
}
|
|
89
97
|
async function readWorkspaceEntries(appRoot) {
|
|
90
98
|
const file = workspacesPath(appRoot);
|