parallel-codex-tui 0.1.0 → 0.1.3

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.
@@ -121,10 +121,19 @@ export class SessionIndex {
121
121
  source=excluded.source`)
122
122
  .run(taskId, record.worker_id, record.engine, record.role, record.session_id, record.cwd, record.created_at, record.last_used_at, record.source);
123
123
  }
124
+ async deleteNativeSession(taskId, workerId) {
125
+ this.db.prepare("DELETE FROM native_sessions WHERE task_id = ? AND worker_id = ?").run(taskId, workerId);
126
+ }
124
127
  async countRows(table) {
125
128
  const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get();
126
129
  return row.count;
127
130
  }
131
+ async workerNativeSessionId(taskId, workerId) {
132
+ const row = this.db
133
+ .prepare("SELECT native_session_id FROM workers WHERE task_id = ? AND worker_id = ?")
134
+ .get(taskId, workerId);
135
+ return row?.native_session_id ?? null;
136
+ }
128
137
  async rebuildFromFiles() {
129
138
  this.db.exec("DELETE FROM native_sessions; DELETE FROM workers; DELETE FROM turns; DELETE FROM tasks;");
130
139
  const sessions = join(this.projectRoot, this.dataDir, "sessions");
@@ -145,7 +154,10 @@ export class SessionIndex {
145
154
  async rebuildTask(taskDir, taskId) {
146
155
  const metaPath = join(taskDir, "meta.json");
147
156
  if (await pathExists(metaPath)) {
148
- await this.upsertTask(await readJson(metaPath, TaskMetaSchema));
157
+ const meta = await readJsonIfValid(metaPath, TaskMetaSchema);
158
+ if (meta) {
159
+ await this.upsertTask(meta);
160
+ }
149
161
  }
150
162
  const turnsDir = join(taskDir, "turns");
151
163
  if (await pathExists(turnsDir)) {
@@ -153,7 +165,10 @@ export class SessionIndex {
153
165
  for (const turnEntry of turnEntries) {
154
166
  const turnPath = join(turnsDir, turnEntry.name, "turn.json");
155
167
  if (turnEntry.isDirectory() && (await pathExists(turnPath))) {
156
- await this.upsertTurn(taskId, await readJson(turnPath, TurnMetaSchema));
168
+ const turn = await readJsonIfValid(turnPath, TurnMetaSchema);
169
+ if (turn) {
170
+ await this.upsertTurn(taskId, turn);
171
+ }
157
172
  }
158
173
  }
159
174
  }
@@ -164,17 +179,46 @@ export class SessionIndex {
164
179
  }
165
180
  const workerDir = join(taskDir, entry.name);
166
181
  const statusPath = join(workerDir, "status.json");
182
+ const nativePath = join(workerDir, "native-session.json");
167
183
  if (await pathExists(statusPath)) {
168
- await this.upsertWorker(taskId, await readJson(statusPath, WorkerStatusSchema), {
169
- dir: workerDir,
170
- statusPath,
171
- outputLogPath: join(workerDir, "output.log")
172
- });
184
+ const status = await this.readRebuildWorkerStatus(statusPath, nativePath);
185
+ if (status) {
186
+ await this.upsertWorker(taskId, status, {
187
+ dir: workerDir,
188
+ statusPath,
189
+ outputLogPath: join(workerDir, "output.log")
190
+ });
191
+ }
173
192
  }
174
- const nativePath = join(workerDir, "native-session.json");
175
193
  if (await pathExists(nativePath)) {
176
- await this.upsertNativeSession(taskId, await readJson(nativePath, NativeSessionSchema));
194
+ const nativeSession = await readJsonIfValid(nativePath, NativeSessionSchema);
195
+ if (nativeSession) {
196
+ await this.upsertNativeSession(taskId, nativeSession);
197
+ }
177
198
  }
178
199
  }
179
200
  }
201
+ async readRebuildWorkerStatus(statusPath, nativePath) {
202
+ const status = await readJsonIfValid(statusPath, WorkerStatusSchema);
203
+ if (!status) {
204
+ return null;
205
+ }
206
+ if (status.native_session_id && !(await readJsonIfValid(nativePath, NativeSessionSchema))) {
207
+ const nextStatus = { ...status };
208
+ delete nextStatus.native_session_id;
209
+ return WorkerStatusSchema.parse(nextStatus);
210
+ }
211
+ return status;
212
+ }
213
+ }
214
+ async function readJsonIfValid(path, schema) {
215
+ if (!(await pathExists(path))) {
216
+ return null;
217
+ }
218
+ try {
219
+ return await readJson(path, schema);
220
+ }
221
+ catch {
222
+ return null;
223
+ }
180
224
  }
@@ -3,7 +3,7 @@ import { basename, dirname, join } from "node:path";
3
3
  import { appendJsonLine, ensureDir, pathExists, readJson, readTextIfExists, removeIfExists, writeJson, writeText } from "./file-store.js";
4
4
  import { formatTaskTimestamp, taskDir } from "./paths.js";
5
5
  import { sessionsRoot } from "./paths.js";
6
- import { NativeSessionSchema, RouteDecisionSchema, TaskMetaSchema, TurnMetaSchema } from "../domain/schemas.js";
6
+ import { NativeSessionSchema, RouteDecisionSchema, TaskMetaSchema, TurnMetaSchema, WorkerStatusSchema } from "../domain/schemas.js";
7
7
  export class SessionManager {
8
8
  projectRoot;
9
9
  dataDir;
@@ -57,6 +57,10 @@ export class SessionManager {
57
57
  eventsPath: join(dir, "events.jsonl")
58
58
  };
59
59
  }
60
+ async hasTask(taskId) {
61
+ const task = this.taskFromId(taskId);
62
+ return Boolean(await readTaskMetaIfValid(task.metaPath));
63
+ }
60
64
  async latestTask() {
61
65
  const root = sessionsRoot(this.projectRoot, this.dataDir);
62
66
  if (!(await pathExists(root))) {
@@ -70,7 +74,10 @@ export class SessionManager {
70
74
  }
71
75
  const metaPath = join(root, entry.name, "meta.json");
72
76
  if (await pathExists(metaPath)) {
73
- const meta = await readJson(metaPath, TaskMetaSchema);
77
+ const meta = await readTaskMetaIfValid(metaPath);
78
+ if (!meta) {
79
+ continue;
80
+ }
74
81
  if (meta.mode === "complex") {
75
82
  tasks.push(meta);
76
83
  }
@@ -159,7 +166,15 @@ export class SessionManager {
159
166
  if (!(await pathExists(path))) {
160
167
  return null;
161
168
  }
162
- return readJson(path, NativeSessionSchema);
169
+ try {
170
+ return await readJson(path, NativeSessionSchema);
171
+ }
172
+ catch {
173
+ await removeIfExists(path);
174
+ await this.clearWorkerStatusNativeSession(worker);
175
+ await this.index?.deleteNativeSession(this.taskIdFromWorkerDir(worker.dir), this.workerIdFromWorkerDir(worker.dir));
176
+ return null;
177
+ }
163
178
  }
164
179
  async writeNativeSession(worker, record) {
165
180
  await writeJson(join(worker.dir, "native-session.json"), NativeSessionSchema.parse(record));
@@ -170,13 +185,21 @@ export class SessionManager {
170
185
  if (!(await pathExists(nativeSessionPath))) {
171
186
  return;
172
187
  }
173
- const record = await readJson(nativeSessionPath, NativeSessionSchema);
188
+ const record = await readNativeSessionIfValid(nativeSessionPath);
189
+ if (!record) {
190
+ await removeIfExists(nativeSessionPath);
191
+ await this.clearWorkerStatusNativeSession(worker);
192
+ await this.index?.deleteNativeSession(this.taskIdFromWorkerDir(worker.dir), this.workerIdFromWorkerDir(worker.dir));
193
+ return;
194
+ }
174
195
  await writeJson(join(worker.dir, "native-session.retired.json"), {
175
196
  ...record,
176
197
  retired_at: this.now().toISOString(),
177
198
  retired_reason: reason
178
199
  });
179
200
  await removeIfExists(nativeSessionPath);
201
+ await this.clearWorkerStatusNativeSession(worker);
202
+ await this.index?.deleteNativeSession(this.taskIdFromWorkerDir(worker.dir), record.worker_id);
180
203
  }
181
204
  async appendEvent(task, type, message) {
182
205
  const event = {
@@ -198,7 +221,10 @@ export class SessionManager {
198
221
  if (!this.index || !(await pathExists(task.metaPath))) {
199
222
  return;
200
223
  }
201
- await this.index.upsertTask(await readJson(task.metaPath, TaskMetaSchema));
224
+ const meta = await readTaskMetaIfValid(task.metaPath);
225
+ if (meta) {
226
+ await this.index.upsertTask(meta);
227
+ }
202
228
  }
203
229
  async backfillInitialTurn(task, fallbackRoute) {
204
230
  const firstTurn = this.turnFiles(task, "0001");
@@ -213,10 +239,8 @@ export class SessionManager {
213
239
  if (!request) {
214
240
  return;
215
241
  }
216
- const route = (await pathExists(task.routePath))
217
- ? await readJson(task.routePath, RouteDecisionSchema)
218
- : fallbackRoute;
219
- const meta = (await pathExists(task.metaPath)) ? await readJson(task.metaPath, TaskMetaSchema) : null;
242
+ const route = (await readRouteDecisionIfValid(task.routePath)) ?? fallbackRoute;
243
+ const meta = await readTaskMetaIfValid(task.metaPath);
220
244
  await this.writeTurn(task, "0001", request, route, meta ? new Date(meta.created_at) : this.now());
221
245
  }
222
246
  async writeTurn(task, turnId, request, route, createdAt) {
@@ -247,6 +271,30 @@ export class SessionManager {
247
271
  taskIdFromWorkerDir(workerDir) {
248
272
  return basename(dirname(workerDir));
249
273
  }
274
+ workerIdFromWorkerDir(workerDir) {
275
+ return basename(workerDir);
276
+ }
277
+ async clearWorkerStatusNativeSession(worker) {
278
+ const statusPath = join(worker.dir, "status.json");
279
+ if (!(await pathExists(statusPath))) {
280
+ return;
281
+ }
282
+ const status = await readWorkerStatusIfValid(statusPath);
283
+ if (!status) {
284
+ return;
285
+ }
286
+ if (!status.native_session_id) {
287
+ return;
288
+ }
289
+ const nextStatus = { ...status };
290
+ delete nextStatus.native_session_id;
291
+ await writeJson(statusPath, WorkerStatusSchema.parse(nextStatus));
292
+ await this.index?.upsertWorker(this.taskIdFromWorkerDir(worker.dir), nextStatus, {
293
+ dir: worker.dir,
294
+ statusPath,
295
+ outputLogPath: join(worker.dir, "output.log")
296
+ });
297
+ }
250
298
  async clearWorkerArtifacts(dir, role) {
251
299
  const files = role === "actor"
252
300
  ? ["worklog.md", "patch.diff"]
@@ -262,3 +310,47 @@ function titleFromRequest(request) {
262
310
  const firstLine = request.trim().split("\n")[0] ?? "Untitled task";
263
311
  return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
264
312
  }
313
+ async function readTaskMetaIfValid(metaPath) {
314
+ if (!(await pathExists(metaPath))) {
315
+ return null;
316
+ }
317
+ try {
318
+ return await readJson(metaPath, TaskMetaSchema);
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ async function readRouteDecisionIfValid(routePath) {
325
+ if (!(await pathExists(routePath))) {
326
+ return null;
327
+ }
328
+ try {
329
+ return await readJson(routePath, RouteDecisionSchema);
330
+ }
331
+ catch {
332
+ return null;
333
+ }
334
+ }
335
+ async function readNativeSessionIfValid(nativeSessionPath) {
336
+ if (!(await pathExists(nativeSessionPath))) {
337
+ return null;
338
+ }
339
+ try {
340
+ return await readJson(nativeSessionPath, NativeSessionSchema);
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ }
346
+ async function readWorkerStatusIfValid(statusPath) {
347
+ if (!(await pathExists(statusPath))) {
348
+ return null;
349
+ }
350
+ try {
351
+ return await readJson(statusPath, WorkerStatusSchema);
352
+ }
353
+ catch {
354
+ return null;
355
+ }
356
+ }
@@ -0,0 +1,118 @@
1
+ import { join, resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { z } from "zod";
4
+ import { ensureDir, pathExists, pathIsDirectory, readJson, readTextIfExists, writeJson, writeText } from "./file-store.js";
5
+ const lastWorkspaceFile = "last-workspace";
6
+ const workspacesFile = "workspaces.json";
7
+ const maxRememberedWorkspaces = 20;
8
+ const WorkspaceRegistrySchema = z.object({
9
+ version: z.literal(1).default(1),
10
+ workspaces: z
11
+ .array(z.object({
12
+ path: z.string().min(1),
13
+ last_used_at: z.string().min(1)
14
+ }))
15
+ .default([])
16
+ });
17
+ export async function resolveWorkspaceSelection(input) {
18
+ if (input.explicitWorkspace?.trim()) {
19
+ return resolveWorkspacePath(input.cwd, input.explicitWorkspace);
20
+ }
21
+ const [latest] = await listWorkspaceChoices(input.appRoot);
22
+ return latest ? latest.path : input.cwd;
23
+ }
24
+ export async function prepareWorkspace(appRoot, workspaceRoot) {
25
+ const resolved = resolveWorkspacePath(process.cwd(), workspaceRoot);
26
+ if ((await pathExists(resolved)) && !(await pathIsDirectory(resolved))) {
27
+ throw new Error(`Workspace path exists but is not a directory: ${resolved}`);
28
+ }
29
+ await ensureDir(resolved);
30
+ await ensureDir(join(resolved, ".parallel-codex"));
31
+ await rememberWorkspace(appRoot, resolved);
32
+ return resolved;
33
+ }
34
+ export async function hasSavedWorkspace(appRoot) {
35
+ return (await listWorkspaceChoices(appRoot)).length > 0;
36
+ }
37
+ export async function listWorkspaceChoices(appRoot) {
38
+ const entries = await readWorkspaceEntries(appRoot);
39
+ const legacy = (await readTextIfExists(lastWorkspacePath(appRoot))).trim();
40
+ if (legacy) {
41
+ entries.push({ path: legacy, last_used_at: "" });
42
+ }
43
+ const seen = new Set();
44
+ const unique = entries
45
+ .map((entry, index) => ({
46
+ path: resolveStoredWorkspacePath(appRoot, entry.path),
47
+ lastUsedAt: entry.last_used_at || null,
48
+ order: index
49
+ }))
50
+ .filter((entry) => {
51
+ if (seen.has(entry.path)) {
52
+ return false;
53
+ }
54
+ seen.add(entry.path);
55
+ return true;
56
+ })
57
+ .sort((left, right) => {
58
+ const byDate = (right.lastUsedAt ?? "").localeCompare(left.lastUsedAt ?? "");
59
+ return byDate === 0 ? left.order - right.order : byDate;
60
+ });
61
+ const choices = await Promise.all(unique.map(async (entry) => {
62
+ const exists = await pathExists(entry.path);
63
+ const isDirectory = exists && (await pathIsDirectory(entry.path));
64
+ return {
65
+ path: entry.path,
66
+ exists: isDirectory,
67
+ lastUsedAt: entry.lastUsedAt,
68
+ selectable: !exists || isDirectory
69
+ };
70
+ }));
71
+ return choices
72
+ .filter((choice) => choice.selectable)
73
+ .map(({ selectable: _selectable, ...choice }) => choice);
74
+ }
75
+ async function rememberWorkspace(appRoot, workspaceRoot) {
76
+ const now = new Date().toISOString();
77
+ const resolved = resolveWorkspacePath(process.cwd(), workspaceRoot);
78
+ const current = await readWorkspaceEntries(appRoot);
79
+ const next = {
80
+ version: 1,
81
+ workspaces: [
82
+ { path: resolved, last_used_at: now },
83
+ ...current.filter((entry) => resolveStoredWorkspacePath(appRoot, entry.path) !== resolved)
84
+ ].slice(0, maxRememberedWorkspaces)
85
+ };
86
+ await writeText(lastWorkspacePath(appRoot), `${resolved}\n`);
87
+ await writeJson(workspacesPath(appRoot), WorkspaceRegistrySchema.parse(next));
88
+ }
89
+ async function readWorkspaceEntries(appRoot) {
90
+ const file = workspacesPath(appRoot);
91
+ if (!(await pathExists(file))) {
92
+ return [];
93
+ }
94
+ try {
95
+ return (await readJson(file, WorkspaceRegistrySchema)).workspaces;
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ }
101
+ function lastWorkspacePath(appRoot) {
102
+ return join(appRoot, ".parallel-codex", lastWorkspaceFile);
103
+ }
104
+ function workspacesPath(appRoot) {
105
+ return join(appRoot, ".parallel-codex", workspacesFile);
106
+ }
107
+ export function resolveWorkspacePath(cwd, value) {
108
+ if (value === "~") {
109
+ return homedir();
110
+ }
111
+ if (value.startsWith("~/")) {
112
+ return resolve(homedir(), value.slice(2));
113
+ }
114
+ return resolve(cwd, value);
115
+ }
116
+ function resolveStoredWorkspacePath(appRoot, value) {
117
+ return resolveWorkspacePath(appRoot, value);
118
+ }
package/dist/doctor.js CHANGED
@@ -1,25 +1,23 @@
1
1
  import { constants } from "node:fs";
2
2
  import { access } from "node:fs/promises";
3
3
  import { delimiter, join } from "node:path";
4
+ import { prepareAppRoot } from "./core/app-root.js";
4
5
  import { configPath, loadConfig } from "./core/config.js";
5
6
  import { pathExists } from "./core/file-store.js";
7
+ import { prepareWorkspace } from "./core/workspace.js";
6
8
  export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
7
9
  const lines = ["parallel-codex-tui doctor"];
8
10
  let ok = true;
11
+ await prepareAppRoot(appRoot);
12
+ const preparedWorkspace = await prepareWorkspace(appRoot, workspaceRoot);
9
13
  if (isSupportedNodeVersion(process.versions.node)) {
10
14
  lines.push(`Node.js: ok (${process.versions.node})`);
11
15
  }
12
16
  else {
13
17
  ok = false;
14
- lines.push(`Node.js: unsupported (${process.versions.node}; need 22.5+)`);
15
- }
16
- if (await pathExists(workspaceRoot)) {
17
- lines.push(`workspace: ok (${workspaceRoot})`);
18
- }
19
- else {
20
- ok = false;
21
- lines.push(`workspace: missing (${workspaceRoot})`);
18
+ lines.push(`Node.js: unsupported (${process.versions.node}; need 26+)`);
22
19
  }
20
+ lines.push(`workspace: ok (${preparedWorkspace})`);
23
21
  const localConfigPath = configPath(appRoot);
24
22
  if (!(await pathExists(localConfigPath))) {
25
23
  ok = false;
@@ -51,21 +49,49 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
51
49
  lines.push(`${command}: missing`);
52
50
  }
53
51
  }
52
+ for (const check of configuredWorkerModelEnvChecks(config, env)) {
53
+ if (check.ok) {
54
+ lines.push(`${check.label}: ok`);
55
+ }
56
+ else {
57
+ ok = false;
58
+ lines.push(`${check.label}: missing env ${check.envName}`);
59
+ }
60
+ }
54
61
  return {
55
62
  ok,
56
63
  text: `${lines.join("\n")}\n`
57
64
  };
58
65
  }
59
- function isSupportedNodeVersion(version) {
66
+ export function isSupportedNodeVersion(version) {
60
67
  const [majorRaw = "0", minorRaw = "0"] = version.split(".");
61
68
  const major = Number.parseInt(majorRaw, 10);
62
69
  const minor = Number.parseInt(minorRaw, 10);
63
- return major > 22 || (major === 22 && minor >= 5);
70
+ return major >= 26;
64
71
  }
65
72
  function configuredCommands(config) {
73
+ return Array.from(new Set(configuredEngines(config, { includeRouter: true })
74
+ .map((engine) => commandForEngine(config, engine))
75
+ .filter((command) => Boolean(command) && command !== "mock")));
76
+ }
77
+ function commandForEngine(config, engine) {
78
+ if (engine === "router-codex") {
79
+ return config.router.codex.command;
80
+ }
81
+ if (engine === "codex") {
82
+ return config.workers.codex.command;
83
+ }
84
+ if (engine === "claude") {
85
+ return config.workers.claude.command;
86
+ }
87
+ return null;
88
+ }
89
+ function configuredEngines(config, options) {
66
90
  const engines = new Set();
67
- if (config.router.defaultMode === "auto") {
91
+ if (config.router.defaultMode === "auto" && options.includeRouter) {
68
92
  engines.add("router-codex");
93
+ }
94
+ if (config.router.defaultMode === "auto") {
69
95
  engines.add(config.pairing.main);
70
96
  engines.add(config.pairing.judge);
71
97
  engines.add(config.pairing.actor);
@@ -79,21 +105,29 @@ function configuredCommands(config) {
79
105
  engines.add(config.pairing.actor);
80
106
  engines.add(config.pairing.critic);
81
107
  }
82
- return Array.from(new Set(Array.from(engines)
83
- .map((engine) => commandForEngine(config, engine))
84
- .filter((command) => Boolean(command) && command !== "mock")));
108
+ return Array.from(engines);
85
109
  }
86
- function commandForEngine(config, engine) {
87
- if (engine === "router-codex") {
88
- return config.router.codex.command;
89
- }
90
- if (engine === "codex") {
91
- return config.workers.codex.command;
92
- }
93
- if (engine === "claude") {
94
- return config.workers.claude.command;
110
+ function configuredWorkerEngines(config) {
111
+ return configuredEngines(config, { includeRouter: false }).filter((engine) => engine === "codex" || engine === "claude");
112
+ }
113
+ function configuredWorkerModelEnvChecks(config, env) {
114
+ const checks = [];
115
+ for (const engine of configuredWorkerEngines(config)) {
116
+ const worker = engine === "codex" ? config.workers.codex : config.workers.claude;
117
+ for (const [key, value] of Object.entries(worker.model.env)) {
118
+ for (const envName of referencedEnvNames(value)) {
119
+ checks.push({
120
+ label: `workers.${engine}.model.env.${key}`,
121
+ envName,
122
+ ok: Boolean(env[envName])
123
+ });
124
+ }
125
+ }
95
126
  }
96
- return null;
127
+ return checks;
128
+ }
129
+ function referencedEnvNames(value) {
130
+ return Array.from(value.matchAll(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g), (match) => match[1] ?? "");
97
131
  }
98
132
  async function commandExists(command, env) {
99
133
  if (command.includes("/")) {
@@ -12,14 +12,16 @@ export class Orchestrator {
12
12
  sessions;
13
13
  workers;
14
14
  routeRunner;
15
- constructor(config, sessions, workers, routeRunner) {
15
+ routerCwd;
16
+ constructor(config, sessions, workers, routeRunner, routerCwd = config.projectRoot) {
16
17
  this.config = config;
17
18
  this.sessions = sessions;
18
19
  this.workers = workers;
19
20
  this.routeRunner = routeRunner;
21
+ this.routerCwd = routerCwd;
20
22
  }
21
23
  async handleRequest(input) {
22
- const route = await routeRequestWithCodex(input.request, this.config, this.routeRunner, input.cwd);
24
+ const route = await this.routeRequest(input.request);
23
25
  const workers = [];
24
26
  if (route.mode === "simple") {
25
27
  input.onStatus?.({ taskId: "main", main: "running" });
@@ -104,7 +106,7 @@ export class Orchestrator {
104
106
  }
105
107
  async handleTaskTurn(input) {
106
108
  const task = this.sessions.taskFromId(input.taskId);
107
- const route = await routeRequestWithCodex(input.request, this.config, this.routeRunner, input.cwd);
109
+ const route = await this.routeRequest(input.request);
108
110
  const turn = await this.sessions.appendTurn(task, {
109
111
  request: input.request,
110
112
  route
@@ -170,19 +172,7 @@ export class Orchestrator {
170
172
  }
171
173
  }
172
174
  async routeTaskFollowUp(input) {
173
- const task = this.sessions.taskFromId(input.taskId);
174
- const meta = (await pathExists(task.metaPath)) ? await readJson(task.metaPath, TaskMetaSchema) : null;
175
- const route = await routeRequestWithCodex([
176
- "Active task follow-up routing.",
177
- `Task id: ${input.taskId}`,
178
- `Task status: ${meta?.status ?? "unknown"}`,
179
- "",
180
- "Choose simple for status/log/reason/explanation questions that can be answered from session files.",
181
- "Choose complex for requests that change direction, ask for code changes, reruns, fixes, new experiments, or implementation work.",
182
- "",
183
- "Follow-up request:",
184
- input.request
185
- ].join("\n"), this.config, this.routeRunner, input.cwd);
175
+ const route = await this.routeRequest(input.request);
186
176
  return {
187
177
  mode: route.mode,
188
178
  taskId: route.mode === "complex" ? input.taskId : null,
@@ -191,7 +181,7 @@ export class Orchestrator {
191
181
  }
192
182
  async answerTaskQuestion(input) {
193
183
  const task = this.sessions.taskFromId(input.taskId);
194
- const meta = (await pathExists(task.metaPath)) ? await readJson(task.metaPath, TaskMetaSchema) : null;
184
+ const meta = await readTaskMetaIfValid(task.metaPath);
195
185
  const workerSummaries = await Promise.all(["judge", "actor", "critic"].map((role) => this.readLatestWorkerQuestionSummary(task, role)));
196
186
  const workers = workerSummaries.filter((worker) => worker !== null);
197
187
  const failed = workers.find((worker) => worker.status.state === "failed");
@@ -228,7 +218,10 @@ export class Orchestrator {
228
218
  if (!(await pathExists(statusPath))) {
229
219
  continue;
230
220
  }
231
- const status = await readJson(statusPath, WorkerStatusSchema);
221
+ const status = await readWorkerStatusIfValid(statusPath);
222
+ if (!status) {
223
+ continue;
224
+ }
232
225
  workers.push({
233
226
  id: status.worker_id,
234
227
  role: status.role,
@@ -240,6 +233,9 @@ export class Orchestrator {
240
233
  }
241
234
  return workers.sort((left, right) => workerRoleOrder(left.role) - workerRoleOrder(right.role));
242
235
  }
236
+ routeRequest(request) {
237
+ return routeRequestWithCodex(request, this.config, this.routeRunner, this.routerCwd);
238
+ }
243
239
  async runMain(input, workers) {
244
240
  const engine = this.config.pairing.main;
245
241
  const dir = this.sessions.mainSessionDir();
@@ -476,7 +472,10 @@ export class Orchestrator {
476
472
  if (!(await pathExists(files.statusPath))) {
477
473
  continue;
478
474
  }
479
- const status = await readJson(files.statusPath, WorkerStatusSchema);
475
+ const status = await readWorkerStatusIfValid(files.statusPath);
476
+ if (!status) {
477
+ continue;
478
+ }
480
479
  return {
481
480
  status,
482
481
  logTail: tailText(await readTextIfExists(files.outputLogPath), 8)
@@ -490,6 +489,25 @@ function ensureWorkerSuccess(workerId, exitCode) {
490
489
  throw new Error(`${workerId} failed with exit code ${exitCode}`);
491
490
  }
492
491
  }
492
+ async function readWorkerStatusIfValid(statusPath) {
493
+ try {
494
+ return await readJson(statusPath, WorkerStatusSchema);
495
+ }
496
+ catch {
497
+ return null;
498
+ }
499
+ }
500
+ async function readTaskMetaIfValid(metaPath) {
501
+ if (!(await pathExists(metaPath))) {
502
+ return null;
503
+ }
504
+ try {
505
+ return await readJson(metaPath, TaskMetaSchema);
506
+ }
507
+ catch {
508
+ return null;
509
+ }
510
+ }
493
511
  function summarizeRetirementReason(reason) {
494
512
  const lines = reason
495
513
  .split(/\r?\n/)
package/dist/tui/App.js CHANGED
@@ -10,7 +10,7 @@ import { AppShell } from "./AppShell.js";
10
10
  import { InputBar } from "./InputBar.js";
11
11
  import { applyNativeInputChunk } from "./native-input.js";
12
12
  import { nextScrollOffset } from "./scrolling.js";
13
- import { chooseSubmitTarget, nextSubmitMemoryState } from "./task-memory.js";
13
+ import { chooseSubmitTarget, nextSubmitMemoryState, shouldClearWorkersForSubmit } from "./task-memory.js";
14
14
  import { TerminalOutput } from "./TerminalOutput.js";
15
15
  import { NativeTerminalScreen } from "./terminal-screen.js";
16
16
  import { WorkerOutputView } from "./WorkerOutputView.js";
@@ -405,13 +405,6 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
405
405
  setInput("");
406
406
  busyRef.current = true;
407
407
  setBusy(true);
408
- setWorkers([]);
409
- selectedWorkerIndexRef.current = 0;
410
- autoSelectedFailedWorkerRef.current = false;
411
- userSelectedWorkerRef.current = false;
412
- setSelectedWorkerIndex(0);
413
- setWorkerScrollOffset(0);
414
- setWorkerMaxScrollOffset(0);
415
408
  setMessages((current) => [...current, { from: "user", text: request }]);
416
409
  try {
417
410
  const callbacks = {
@@ -433,6 +426,15 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNa
433
426
  })
434
427
  : undefined;
435
428
  const target = chooseSubmitTarget(memory, followUpRoute);
429
+ if (shouldClearWorkersForSubmit(target)) {
430
+ setWorkers([]);
431
+ selectedWorkerIndexRef.current = 0;
432
+ autoSelectedFailedWorkerRef.current = false;
433
+ userSelectedWorkerRef.current = false;
434
+ setSelectedWorkerIndex(0);
435
+ setWorkerScrollOffset(0);
436
+ setWorkerMaxScrollOffset(0);
437
+ }
436
438
  const result = target.kind === "task-turn"
437
439
  ? await orchestrator.handleTaskTurn({
438
440
  request,
@@ -24,3 +24,6 @@ export function nextSubmitMemoryState(current, target, result) {
24
24
  activeTaskId: result.mode === "complex" ? result.taskId : null
25
25
  };
26
26
  }
27
+ export function shouldClearWorkersForSubmit(target) {
28
+ return target.kind !== "task-question";
29
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.1.0";
1
+ export const version = "0.1.3";