@grackle-ai/server 0.25.0 → 0.27.0
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/dist/adapter-manager.d.ts.map +1 -1
- package/dist/adapter-manager.js +1 -0
- package/dist/adapter-manager.js.map +1 -1
- package/dist/adapters/codespace.js +1 -1
- package/dist/adapters/codespace.js.map +1 -1
- package/dist/adapters/docker.js +1 -1
- package/dist/adapters/docker.js.map +1 -1
- package/dist/adapters/local.js +1 -1
- package/dist/adapters/local.js.map +1 -1
- package/dist/adapters/remote-adapter-utils.d.ts.map +1 -1
- package/dist/adapters/remote-adapter-utils.js +5 -4
- package/dist/adapters/remote-adapter-utils.js.map +1 -1
- package/dist/adapters/ssh.js +1 -1
- package/dist/adapters/ssh.js.map +1 -1
- package/dist/compute-task-status.d.ts +31 -0
- package/dist/compute-task-status.d.ts.map +1 -0
- package/dist/compute-task-status.js +88 -0
- package/dist/compute-task-status.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +112 -19
- package/dist/db.js.map +1 -1
- package/dist/event-processor.d.ts +0 -1
- package/dist/event-processor.d.ts.map +1 -1
- package/dist/event-processor.js +28 -32
- package/dist/event-processor.js.map +1 -1
- package/dist/github-import.d.ts +1 -1
- package/dist/github-import.d.ts.map +1 -1
- package/dist/github-import.js +7 -7
- package/dist/github-import.js.map +1 -1
- package/dist/grpc-service.d.ts +0 -6
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +142 -117
- package/dist/grpc-service.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/processor-registry.d.ts +1 -2
- package/dist/processor-registry.d.ts.map +1 -1
- package/dist/processor-registry.js +1 -4
- package/dist/processor-registry.js.map +1 -1
- package/dist/project-store.d.ts +3 -1
- package/dist/project-store.d.ts.map +1 -1
- package/dist/project-store.js +5 -1
- package/dist/project-store.js.map +1 -1
- package/dist/schema.d.ts +38 -95
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -6
- package/dist/schema.js.map +1 -1
- package/dist/session-store.d.ts +7 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +42 -4
- package/dist/session-store.js.map +1 -1
- package/dist/task-store.d.ts +9 -10
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +14 -36
- package/dist/task-store.js.map +1 -1
- package/dist/transcript.js.map +1 -1
- package/dist/utils/format-gh-error.d.ts +6 -0
- package/dist/utils/format-gh-error.d.ts.map +1 -0
- package/dist/utils/format-gh-error.js +30 -0
- package/dist/utils/format-gh-error.js.map +1 -0
- package/dist/utils/system-context.d.ts +1 -1
- package/dist/utils/system-context.d.ts.map +1 -1
- package/dist/utils/system-context.js +2 -2
- package/dist/utils/system-context.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +175 -171
- package/dist/ws-bridge.js.map +1 -1
- package/dist/ws-broadcast.d.ts +5 -0
- package/dist/ws-broadcast.d.ts.map +1 -1
- package/dist/ws-broadcast.js +20 -0
- package/dist/ws-broadcast.js.map +1 -1
- package/package.json +4 -4
package/dist/ws-bridge.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as findingStore from "./finding-store.js";
|
|
|
13
13
|
import * as personaStore from "./persona-store.js";
|
|
14
14
|
import { v4 as uuid } from "uuid";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, eventTypeToString, } from "@grackle-ai/common";
|
|
16
|
+
import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, SESSION_STATUS, TASK_STATUS, eventTypeToString, } from "@grackle-ai/common";
|
|
17
17
|
import { grackleHome } from "./paths.js";
|
|
18
18
|
import * as logWriter from "./log-writer.js";
|
|
19
19
|
import { safeParseJsonArray } from "./json-helpers.js";
|
|
@@ -22,9 +22,11 @@ import { buildTaskSystemContext } from "./utils/system-context.js";
|
|
|
22
22
|
import { slugify } from "./utils/slugify.js";
|
|
23
23
|
import { processEventStream } from "./event-processor.js";
|
|
24
24
|
import * as processorRegistry from "./processor-registry.js";
|
|
25
|
-
import { broadcast, setWssInstance } from "./ws-broadcast.js";
|
|
26
|
-
import { buildMcpServersJson
|
|
25
|
+
import { broadcast, setWssInstance, broadcastEnvironments, envRowToWs } from "./ws-broadcast.js";
|
|
26
|
+
import { buildMcpServersJson } from "./grpc-service.js";
|
|
27
|
+
import { computeTaskStatus } from "./compute-task-status.js";
|
|
27
28
|
import { exec } from "./utils/exec.js";
|
|
29
|
+
import { formatGhError } from "./utils/format-gh-error.js";
|
|
28
30
|
const GH_CODESPACE_LIST_TIMEOUT_MS = 30_000;
|
|
29
31
|
const GH_CODESPACE_CREATE_TIMEOUT_MS = 300_000;
|
|
30
32
|
const GH_CODESPACE_LIST_LIMIT = 50;
|
|
@@ -42,6 +44,7 @@ export function createWsBridge(httpServer, verifyApiKey) {
|
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
const subscriptions = new Map();
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
45
48
|
ws.on("message", async (data) => {
|
|
46
49
|
try {
|
|
47
50
|
const msg = JSON.parse(data.toString());
|
|
@@ -65,25 +68,6 @@ export function createWsBridge(httpServer, verifyApiKey) {
|
|
|
65
68
|
});
|
|
66
69
|
return wss;
|
|
67
70
|
}
|
|
68
|
-
/** Map a database environment row to the WebSocket payload shape. */
|
|
69
|
-
function envRowToWs(r) {
|
|
70
|
-
return {
|
|
71
|
-
id: r.id,
|
|
72
|
-
displayName: r.displayName,
|
|
73
|
-
adapterType: r.adapterType,
|
|
74
|
-
adapterConfig: r.adapterConfig,
|
|
75
|
-
defaultRuntime: r.defaultRuntime,
|
|
76
|
-
status: r.status,
|
|
77
|
-
bootstrapped: r.bootstrapped,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
/** Broadcast the current environment list to all connected WebSocket clients. */
|
|
81
|
-
function broadcastEnvironments() {
|
|
82
|
-
broadcast({
|
|
83
|
-
type: "environments",
|
|
84
|
-
payload: { environments: envRegistry.listEnvironments().map(envRowToWs) },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
71
|
/** Safely parse an adapter config string, returning an empty object on failure. */
|
|
88
72
|
function safeParseAdapterConfig(raw, environmentId) {
|
|
89
73
|
try {
|
|
@@ -191,7 +175,7 @@ async function startTaskSession(ws, task, options) {
|
|
|
191
175
|
logger.warn({ taskId: task.id }, "startTaskSession failed: project not found");
|
|
192
176
|
return `Project not found: ${task.projectId}`;
|
|
193
177
|
}
|
|
194
|
-
const environmentId =
|
|
178
|
+
const environmentId = options?.environmentId || project.defaultEnvironmentId;
|
|
195
179
|
const env = envRegistry.getEnvironment(environmentId);
|
|
196
180
|
if (!env) {
|
|
197
181
|
logger.warn({ taskId: task.id, environmentId }, "startTaskSession failed: environment not found");
|
|
@@ -204,7 +188,7 @@ async function startTaskSession(ws, task, options) {
|
|
|
204
188
|
return undefined;
|
|
205
189
|
}
|
|
206
190
|
// Resolve persona
|
|
207
|
-
const resolvedPersonaId = options?.personaId ||
|
|
191
|
+
const resolvedPersonaId = options?.personaId || "";
|
|
208
192
|
const persona = resolvedPersonaId
|
|
209
193
|
? personaStore.getPersona(resolvedPersonaId)
|
|
210
194
|
: undefined;
|
|
@@ -212,24 +196,17 @@ async function startTaskSession(ws, task, options) {
|
|
|
212
196
|
return `Persona not found: ${resolvedPersonaId}`;
|
|
213
197
|
}
|
|
214
198
|
const sessionId = uuid();
|
|
215
|
-
const runtime = options?.runtime ||
|
|
216
|
-
|
|
217
|
-
env.
|
|
218
|
-
DEFAULT_RUNTIME;
|
|
219
|
-
const model = options?.model ||
|
|
220
|
-
persona?.model ||
|
|
221
|
-
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
222
|
-
DEFAULT_MODEL;
|
|
199
|
+
const runtime = options?.runtime || persona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
|
|
200
|
+
const model = options?.model || persona?.model ||
|
|
201
|
+
process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
|
|
223
202
|
const maxTurns = persona?.maxTurns || 0;
|
|
224
203
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
225
204
|
const freshTask = taskStore.getTask(task.id) || task;
|
|
226
|
-
let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description,
|
|
205
|
+
let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description, options?.notes || "", freshTask.canDecompose);
|
|
227
206
|
if (persona) {
|
|
228
207
|
systemContext = persona.systemPrompt + "\n\n" + systemContext;
|
|
229
208
|
}
|
|
230
|
-
sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath, freshTask.id);
|
|
231
|
-
taskStore.setTaskSession(freshTask.id, sessionId);
|
|
232
|
-
taskStore.markTaskStarted(freshTask.id);
|
|
209
|
+
sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath, freshTask.id, resolvedPersonaId);
|
|
233
210
|
broadcast({
|
|
234
211
|
type: "task_started",
|
|
235
212
|
payload: {
|
|
@@ -263,7 +240,7 @@ async function startTaskSession(ws, task, options) {
|
|
|
263
240
|
maxTurns,
|
|
264
241
|
branch: freshTask.branch,
|
|
265
242
|
worktreeBasePath: freshTask.branch
|
|
266
|
-
? process.env.GRACKLE_WORKTREE_BASE || "/workspace"
|
|
243
|
+
? (project.worktreeBasePath || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
267
244
|
: "",
|
|
268
245
|
systemContext,
|
|
269
246
|
projectId: freshTask.projectId,
|
|
@@ -275,7 +252,6 @@ async function startTaskSession(ws, task, options) {
|
|
|
275
252
|
logPath,
|
|
276
253
|
projectId: freshTask.projectId,
|
|
277
254
|
taskId: freshTask.id,
|
|
278
|
-
onComplete: makeTaskCompletionCallback(freshTask.id, sessionId, freshTask.projectId),
|
|
279
255
|
});
|
|
280
256
|
return undefined;
|
|
281
257
|
}
|
|
@@ -314,7 +290,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
314
290
|
return;
|
|
315
291
|
}
|
|
316
292
|
const session = sessionStore.getSession(sessionId);
|
|
317
|
-
if (!session
|
|
293
|
+
if (!session?.logPath) {
|
|
318
294
|
return;
|
|
319
295
|
}
|
|
320
296
|
const entries = logWriter.readLog(session.logPath);
|
|
@@ -388,8 +364,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
388
364
|
case "spawn": {
|
|
389
365
|
const environmentId = msg.payload?.environmentId;
|
|
390
366
|
const prompt = msg.payload?.prompt;
|
|
391
|
-
const model = msg.payload?.model ||
|
|
392
|
-
const runtime = msg.payload?.runtime ||
|
|
367
|
+
const model = msg.payload?.model || undefined;
|
|
368
|
+
const runtime = msg.payload?.runtime || undefined;
|
|
393
369
|
const branch = msg.payload?.branch || "";
|
|
394
370
|
const systemContext = msg.payload?.systemContext || "";
|
|
395
371
|
const spawnPersonaId = msg.payload?.personaId || "";
|
|
@@ -420,7 +396,9 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
420
396
|
return;
|
|
421
397
|
}
|
|
422
398
|
const sessionId = uuid();
|
|
399
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime/model may be undefined from WS payload
|
|
423
400
|
const sessionRuntime = runtime || spawnPersona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
424
402
|
const sessionModel = model || spawnPersona?.model || process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
|
|
425
403
|
const maxTurns = spawnPersona?.maxTurns || 0;
|
|
426
404
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
@@ -437,7 +415,9 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
437
415
|
model: sessionModel,
|
|
438
416
|
maxTurns,
|
|
439
417
|
branch,
|
|
440
|
-
worktreeBasePath: branch
|
|
418
|
+
worktreeBasePath: branch
|
|
419
|
+
? ((typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "") || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
420
|
+
: "",
|
|
441
421
|
systemContext: finalSystemContext,
|
|
442
422
|
});
|
|
443
423
|
processEventStream(conn.client.spawn(powerlineReq), {
|
|
@@ -450,7 +430,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
450
430
|
sessionId,
|
|
451
431
|
eventType: "error",
|
|
452
432
|
timestamp: new Date().toISOString(),
|
|
453
|
-
content: `Spawn failed: ${err}`,
|
|
433
|
+
content: `Spawn failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
454
434
|
},
|
|
455
435
|
});
|
|
456
436
|
},
|
|
@@ -475,11 +455,11 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
475
455
|
});
|
|
476
456
|
return;
|
|
477
457
|
}
|
|
478
|
-
if (session.status !==
|
|
458
|
+
if (session.status !== SESSION_STATUS.IDLE) {
|
|
479
459
|
sendWs(ws, {
|
|
480
460
|
type: "error",
|
|
481
461
|
payload: {
|
|
482
|
-
message: `Session ${sessionId} is not currently
|
|
462
|
+
message: `Session ${sessionId} is not currently idle (status: ${session.status})`,
|
|
483
463
|
},
|
|
484
464
|
});
|
|
485
465
|
return;
|
|
@@ -532,21 +512,24 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
532
512
|
await conn.client.kill(create(powerline.SessionIdSchema, { id: sessionId }));
|
|
533
513
|
}
|
|
534
514
|
catch (err) {
|
|
535
|
-
|
|
536
|
-
type: "error",
|
|
537
|
-
payload: { message: `Kill failed: ${err}` },
|
|
538
|
-
});
|
|
539
|
-
return;
|
|
515
|
+
logger.warn({ sessionId, err }, "PowerLine kill failed — marking session interrupted anyway");
|
|
540
516
|
}
|
|
541
517
|
}
|
|
542
|
-
sessionStore.updateSession(sessionId,
|
|
518
|
+
sessionStore.updateSession(sessionId, SESSION_STATUS.INTERRUPTED);
|
|
543
519
|
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
544
520
|
sessionId,
|
|
545
521
|
type: grackle.EventType.STATUS,
|
|
546
522
|
timestamp: new Date().toISOString(),
|
|
547
|
-
content:
|
|
523
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
548
524
|
raw: "",
|
|
549
525
|
}));
|
|
526
|
+
// Broadcast task_updated so frontend re-fetches computed status
|
|
527
|
+
if (session.taskId) {
|
|
528
|
+
const task = taskStore.getTask(session.taskId);
|
|
529
|
+
if (task) {
|
|
530
|
+
broadcast({ type: "task_updated", payload: { taskId: task.id, projectId: task.projectId } });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
550
533
|
break;
|
|
551
534
|
}
|
|
552
535
|
// ─── Projects ──────────────────────────────────────────
|
|
@@ -563,6 +546,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
563
546
|
defaultEnvironmentId: r.defaultEnvironmentId,
|
|
564
547
|
status: r.status,
|
|
565
548
|
useWorktrees: r.useWorktrees,
|
|
549
|
+
worktreeBasePath: r.worktreeBasePath,
|
|
566
550
|
createdAt: r.createdAt,
|
|
567
551
|
updatedAt: r.updatedAt,
|
|
568
552
|
})),
|
|
@@ -586,7 +570,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
586
570
|
}
|
|
587
571
|
// useWorktrees defaults to true when not specified
|
|
588
572
|
const createUseWorktrees = msg.payload?.useWorktrees ?? true;
|
|
589
|
-
projectStore.createProject(id, name, msg.payload?.description || "", msg.payload?.repoUrl || "", msg.payload?.defaultEnvironmentId || "", createUseWorktrees);
|
|
573
|
+
projectStore.createProject(id, name, msg.payload?.description || "", msg.payload?.repoUrl || "", msg.payload?.defaultEnvironmentId || "", createUseWorktrees, typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "");
|
|
590
574
|
const row = projectStore.getProject(id);
|
|
591
575
|
broadcast({ type: "project_created", payload: { project: row } });
|
|
592
576
|
break;
|
|
@@ -610,7 +594,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
610
594
|
return;
|
|
611
595
|
}
|
|
612
596
|
const nameVal = typeof msg.payload?.name === "string" ? msg.payload.name : undefined;
|
|
613
|
-
if (nameVal
|
|
597
|
+
if (nameVal?.trim() === "") {
|
|
614
598
|
sendWs(ws, { type: "error", payload: { message: "Project name cannot be empty" } });
|
|
615
599
|
return;
|
|
616
600
|
}
|
|
@@ -622,12 +606,14 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
622
606
|
return;
|
|
623
607
|
}
|
|
624
608
|
const worktreesVal = typeof msg.payload?.useWorktrees === "boolean" ? msg.payload.useWorktrees : undefined;
|
|
609
|
+
const worktreeBasePathVal = typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath : undefined;
|
|
625
610
|
projectStore.updateProject(projectId, {
|
|
626
611
|
name: nameVal !== undefined ? nameVal.trim() : undefined,
|
|
627
612
|
description: descVal,
|
|
628
613
|
repoUrl: repoVal,
|
|
629
614
|
defaultEnvironmentId: envVal,
|
|
630
615
|
useWorktrees: worktreesVal,
|
|
616
|
+
worktreeBasePath: worktreeBasePathVal,
|
|
631
617
|
});
|
|
632
618
|
broadcast({ type: "project_updated", payload: { projectId } });
|
|
633
619
|
break;
|
|
@@ -735,29 +721,39 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
735
721
|
return;
|
|
736
722
|
const rows = taskStore.listTasks(projectId);
|
|
737
723
|
const childIdsMap = taskStore.buildChildIdsMap(rows);
|
|
724
|
+
// Batch-fetch sessions for all tasks and group by taskId
|
|
725
|
+
const taskIds = rows.map((r) => r.id);
|
|
726
|
+
const allSessions = sessionStore.listSessionsByTaskIds(taskIds);
|
|
727
|
+
const sessionsByTask = new Map();
|
|
728
|
+
for (const s of allSessions) {
|
|
729
|
+
const arr = sessionsByTask.get(s.taskId) ?? [];
|
|
730
|
+
arr.push(s);
|
|
731
|
+
sessionsByTask.set(s.taskId, arr);
|
|
732
|
+
}
|
|
738
733
|
sendWs(ws, {
|
|
739
734
|
type: "tasks",
|
|
740
735
|
payload: {
|
|
741
736
|
projectId,
|
|
742
|
-
tasks: rows.map((r) =>
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
737
|
+
tasks: rows.map((r) => {
|
|
738
|
+
const taskSessions = sessionsByTask.get(r.id) ?? [];
|
|
739
|
+
const computed = computeTaskStatus(r.status, taskSessions);
|
|
740
|
+
return {
|
|
741
|
+
id: r.id,
|
|
742
|
+
projectId: r.projectId,
|
|
743
|
+
title: r.title,
|
|
744
|
+
description: r.description,
|
|
745
|
+
status: computed.status,
|
|
746
|
+
branch: r.branch,
|
|
747
|
+
latestSessionId: computed.latestSessionId,
|
|
748
|
+
dependsOn: safeParseJsonArray(r.dependsOn),
|
|
749
|
+
sortOrder: r.sortOrder,
|
|
750
|
+
createdAt: r.createdAt,
|
|
751
|
+
parentTaskId: r.parentTaskId,
|
|
752
|
+
depth: r.depth,
|
|
753
|
+
childTaskIds: childIdsMap.get(r.id) ?? [],
|
|
754
|
+
canDecompose: r.canDecompose,
|
|
755
|
+
};
|
|
756
|
+
}),
|
|
761
757
|
},
|
|
762
758
|
});
|
|
763
759
|
break;
|
|
@@ -783,19 +779,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
783
779
|
const parentTaskId = msg.payload?.parentTaskId || "";
|
|
784
780
|
const rawCanDecompose = msg.payload?.canDecompose;
|
|
785
781
|
const canDecompose = typeof rawCanDecompose === "boolean" ? rawCanDecompose : undefined;
|
|
786
|
-
// Resolve environment: explicit > parent task's env > project default
|
|
787
|
-
let resolvedEnvId = msg.payload?.environmentId || "";
|
|
788
|
-
if (!resolvedEnvId && parentTaskId) {
|
|
789
|
-
const parentTask = taskStore.getTask(parentTaskId);
|
|
790
|
-
if (parentTask?.environmentId) {
|
|
791
|
-
resolvedEnvId = parentTask.environmentId;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
if (!resolvedEnvId) {
|
|
795
|
-
resolvedEnvId = project.defaultEnvironmentId;
|
|
796
|
-
}
|
|
797
782
|
const id = uuid().slice(0, 8);
|
|
798
|
-
taskStore.createTask(id, projectId, title, msg.payload?.description || "",
|
|
783
|
+
taskStore.createTask(id, projectId, title, msg.payload?.description || "", msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose);
|
|
799
784
|
const row = taskStore.getTask(id);
|
|
800
785
|
broadcast({
|
|
801
786
|
type: "task_created",
|
|
@@ -826,7 +811,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
826
811
|
sendWs(ws, { type: "error", payload: { message: `Session not found: ${lateBindSessionId}` } });
|
|
827
812
|
return;
|
|
828
813
|
}
|
|
829
|
-
const terminalStatuses = [
|
|
814
|
+
const terminalStatuses = [SESSION_STATUS.COMPLETED, SESSION_STATUS.FAILED, SESSION_STATUS.INTERRUPTED];
|
|
830
815
|
if (terminalStatuses.includes(session.status)) {
|
|
831
816
|
sendWs(ws, {
|
|
832
817
|
type: "error",
|
|
@@ -843,11 +828,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
843
828
|
return;
|
|
844
829
|
}
|
|
845
830
|
sessionStore.setSessionTask(lateBindSessionId, updateTaskId);
|
|
846
|
-
taskStore.setTaskSession(updateTaskId, lateBindSessionId);
|
|
847
|
-
taskStore.markTaskStarted(updateTaskId);
|
|
848
|
-
const onComplete = makeTaskCompletionCallback(updateTaskId, lateBindSessionId, existingTask.projectId);
|
|
849
831
|
try {
|
|
850
|
-
processorRegistry.lateBind(lateBindSessionId, updateTaskId, existingTask.projectId
|
|
832
|
+
processorRegistry.lateBind(lateBindSessionId, updateTaskId, existingTask.projectId);
|
|
851
833
|
}
|
|
852
834
|
catch (err) {
|
|
853
835
|
sendWs(ws, { type: "error", payload: { message: String(err) } });
|
|
@@ -859,8 +841,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
859
841
|
});
|
|
860
842
|
break;
|
|
861
843
|
}
|
|
862
|
-
// Only allow editing
|
|
863
|
-
if (
|
|
844
|
+
// Only allow editing not_started tasks (non-late-bind path)
|
|
845
|
+
if (existingTask.status !== TASK_STATUS.NOT_STARTED) {
|
|
864
846
|
sendWs(ws, {
|
|
865
847
|
type: "error",
|
|
866
848
|
payload: { message: `Task ${updateTaskId} cannot be edited (status: ${existingTask.status})` },
|
|
@@ -881,13 +863,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
881
863
|
.filter((d) => d !== updateTaskId)),
|
|
882
864
|
]
|
|
883
865
|
: safeParseJsonArray(existingTask.dependsOn);
|
|
884
|
-
|
|
885
|
-
? msg.payload.environmentId
|
|
886
|
-
: existingTask.environmentId;
|
|
887
|
-
const updatedPersonaId = typeof msg.payload?.personaId === "string"
|
|
888
|
-
? msg.payload.personaId
|
|
889
|
-
: existingTask.personaId;
|
|
890
|
-
taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedEnvironmentId, updatedDependsOn, existingTask.reviewNotes, updatedPersonaId);
|
|
866
|
+
taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn);
|
|
891
867
|
const updatedRow = taskStore.getTask(updateTaskId);
|
|
892
868
|
broadcast({
|
|
893
869
|
type: "task_updated",
|
|
@@ -913,14 +889,18 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
913
889
|
});
|
|
914
890
|
return;
|
|
915
891
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
892
|
+
{
|
|
893
|
+
const taskSessions = sessionStore.listSessionsForTask(taskId);
|
|
894
|
+
const { status: effectiveStatus } = computeTaskStatus(task.status, taskSessions);
|
|
895
|
+
if (![TASK_STATUS.NOT_STARTED, TASK_STATUS.FAILED].includes(effectiveStatus)) {
|
|
896
|
+
sendWs(ws, {
|
|
897
|
+
type: "error",
|
|
898
|
+
payload: {
|
|
899
|
+
message: `Task cannot be started (status: ${effectiveStatus})`,
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
924
904
|
}
|
|
925
905
|
if (!taskStore.areDependenciesMet(taskId)) {
|
|
926
906
|
sendWs(ws, {
|
|
@@ -932,68 +912,90 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
932
912
|
const startError = await startTaskSession(ws, task, {
|
|
933
913
|
runtime: msg.payload?.runtime,
|
|
934
914
|
model: msg.payload?.model,
|
|
935
|
-
personaId: msg.payload?.personaId ||
|
|
915
|
+
personaId: msg.payload?.personaId || undefined,
|
|
916
|
+
environmentId: msg.payload?.environmentId || undefined,
|
|
917
|
+
notes: msg.payload?.notes || undefined,
|
|
936
918
|
});
|
|
937
919
|
if (startError) {
|
|
938
920
|
sendWs(ws, { type: "error", payload: { message: startError } });
|
|
939
921
|
}
|
|
940
922
|
break;
|
|
941
923
|
}
|
|
942
|
-
case "
|
|
924
|
+
case "complete_task": {
|
|
943
925
|
const taskId = msg.payload?.taskId;
|
|
944
926
|
if (!taskId)
|
|
945
927
|
return;
|
|
946
|
-
taskStore.
|
|
928
|
+
taskStore.markTaskComplete(taskId, TASK_STATUS.COMPLETE);
|
|
947
929
|
const task = taskStore.getTask(taskId);
|
|
948
930
|
const unblocked = task ? taskStore.checkAndUnblock(task.projectId) : [];
|
|
949
931
|
sendWs(ws, {
|
|
950
|
-
type: "
|
|
932
|
+
type: "task_completed",
|
|
951
933
|
payload: {
|
|
952
934
|
taskId,
|
|
953
935
|
unblockedTaskIds: unblocked.map((t) => t.id),
|
|
954
936
|
},
|
|
955
937
|
});
|
|
938
|
+
if (task) {
|
|
939
|
+
broadcast({
|
|
940
|
+
type: "task_completed",
|
|
941
|
+
payload: { taskId, projectId: task.projectId },
|
|
942
|
+
});
|
|
943
|
+
}
|
|
956
944
|
break;
|
|
957
945
|
}
|
|
958
|
-
case "
|
|
946
|
+
case "resume_task": {
|
|
959
947
|
const taskId = msg.payload?.taskId;
|
|
960
|
-
const reviewNotes = msg.payload?.reviewNotes || "";
|
|
961
948
|
if (!taskId)
|
|
962
949
|
return;
|
|
963
950
|
const task = taskStore.getTask(taskId);
|
|
964
|
-
if (!task)
|
|
951
|
+
if (!task) {
|
|
952
|
+
sendWs(ws, { type: "error", payload: { message: `Task not found: ${taskId}` } });
|
|
965
953
|
return;
|
|
966
|
-
|
|
954
|
+
}
|
|
955
|
+
const latestSession = sessionStore.getLatestSessionForTask(taskId);
|
|
956
|
+
if (!latestSession) {
|
|
957
|
+
sendWs(ws, { type: "error", payload: { message: `Task ${taskId} has no sessions to resume` } });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (![SESSION_STATUS.INTERRUPTED, SESSION_STATUS.COMPLETED].includes(latestSession.status)) {
|
|
967
961
|
sendWs(ws, {
|
|
968
962
|
type: "error",
|
|
969
|
-
payload: {
|
|
970
|
-
message: `Task cannot be rejected (status: ${task.status})`,
|
|
971
|
-
},
|
|
963
|
+
payload: { message: `Latest session ${latestSession.id} is not resumable (status: ${latestSession.status})` },
|
|
972
964
|
});
|
|
973
965
|
return;
|
|
974
966
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
: undefined;
|
|
980
|
-
// Store review notes and set status to assigned
|
|
981
|
-
taskStore.updateTask(task.id, task.title, task.description, "assigned", task.environmentId, safeParseJsonArray(task.dependsOn), reviewNotes, task.personaId);
|
|
982
|
-
broadcast({
|
|
983
|
-
type: "task_rejected",
|
|
984
|
-
payload: { taskId, projectId: task.projectId },
|
|
985
|
-
});
|
|
986
|
-
// Auto-retry: start a new session with the review feedback
|
|
987
|
-
const freshTask = taskStore.getTask(taskId);
|
|
988
|
-
if (freshTask) {
|
|
989
|
-
const retryError = await startTaskSession(ws, freshTask, {
|
|
990
|
-
runtime: previousSession?.runtime,
|
|
991
|
-
model: previousSession?.model,
|
|
967
|
+
if (!latestSession.runtimeSessionId) {
|
|
968
|
+
sendWs(ws, {
|
|
969
|
+
type: "error",
|
|
970
|
+
payload: { message: `Latest session ${latestSession.id} has no runtime session ID — cannot resume` },
|
|
992
971
|
});
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const env = envRegistry.getEnvironment(latestSession.environmentId);
|
|
975
|
+
if (!env) {
|
|
976
|
+
sendWs(ws, { type: "error", payload: { message: `Environment not found: ${latestSession.environmentId}` } });
|
|
977
|
+
return;
|
|
996
978
|
}
|
|
979
|
+
const conn = await autoProvisionEnvironment(ws, latestSession.environmentId, env, { taskId });
|
|
980
|
+
if (!conn) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const powerlineReq = create(powerline.ResumeRequestSchema, {
|
|
984
|
+
sessionId: latestSession.id,
|
|
985
|
+
runtimeSessionId: latestSession.runtimeSessionId,
|
|
986
|
+
runtime: latestSession.runtime,
|
|
987
|
+
});
|
|
988
|
+
const logPath = latestSession.logPath || join(grackleHome, LOGS_DIR, latestSession.id);
|
|
989
|
+
processEventStream(conn.client.resume(powerlineReq), {
|
|
990
|
+
sessionId: latestSession.id,
|
|
991
|
+
logPath,
|
|
992
|
+
projectId: task.projectId,
|
|
993
|
+
taskId: task.id,
|
|
994
|
+
});
|
|
995
|
+
broadcast({
|
|
996
|
+
type: "task_started",
|
|
997
|
+
payload: { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId },
|
|
998
|
+
});
|
|
997
999
|
break;
|
|
998
1000
|
}
|
|
999
1001
|
case "delete_task": {
|
|
@@ -1015,28 +1017,26 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1015
1017
|
});
|
|
1016
1018
|
return;
|
|
1017
1019
|
}
|
|
1018
|
-
// Kill active
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
logger.warn({ taskId, sessionId: deletedTask.sessionId, err }, "Failed to kill session during task deletion");
|
|
1029
|
-
}
|
|
1020
|
+
// Kill all active sessions before deleting the task
|
|
1021
|
+
const activeSessions = sessionStore.getActiveSessionsForTask(taskId);
|
|
1022
|
+
for (const activeSession of activeSessions) {
|
|
1023
|
+
const conn = adapterManager.getConnection(activeSession.environmentId);
|
|
1024
|
+
if (conn) {
|
|
1025
|
+
try {
|
|
1026
|
+
await conn.client.kill(create(powerline.SessionIdSchema, { id: activeSession.id }));
|
|
1027
|
+
}
|
|
1028
|
+
catch (err) {
|
|
1029
|
+
logger.warn({ taskId, sessionId: activeSession.id, err }, "Failed to kill session during task deletion");
|
|
1030
1030
|
}
|
|
1031
|
-
sessionStore.updateSession(deletedTask.sessionId, "killed");
|
|
1032
|
-
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
1033
|
-
sessionId: deletedTask.sessionId,
|
|
1034
|
-
type: grackle.EventType.STATUS,
|
|
1035
|
-
timestamp: new Date().toISOString(),
|
|
1036
|
-
content: "killed",
|
|
1037
|
-
raw: "",
|
|
1038
|
-
}));
|
|
1039
1031
|
}
|
|
1032
|
+
sessionStore.updateSession(activeSession.id, SESSION_STATUS.INTERRUPTED);
|
|
1033
|
+
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
1034
|
+
sessionId: activeSession.id,
|
|
1035
|
+
type: grackle.EventType.STATUS,
|
|
1036
|
+
timestamp: new Date().toISOString(),
|
|
1037
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
1038
|
+
raw: "",
|
|
1039
|
+
}));
|
|
1040
1040
|
}
|
|
1041
1041
|
const changes = taskStore.deleteTask(taskId);
|
|
1042
1042
|
if (changes === 0) {
|
|
@@ -1118,15 +1118,14 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1118
1118
|
if (!taskId)
|
|
1119
1119
|
return;
|
|
1120
1120
|
const task = taskStore.getTask(taskId);
|
|
1121
|
-
if (!task
|
|
1121
|
+
if (!task?.branch) {
|
|
1122
1122
|
sendWs(ws, {
|
|
1123
1123
|
type: "task_diff",
|
|
1124
1124
|
payload: { taskId, error: "No branch" },
|
|
1125
1125
|
});
|
|
1126
1126
|
return;
|
|
1127
1127
|
}
|
|
1128
|
-
const environmentId = task.
|
|
1129
|
-
projectStore.getProject(task.projectId)?.defaultEnvironmentId;
|
|
1128
|
+
const environmentId = projectStore.getProject(task.projectId)?.defaultEnvironmentId;
|
|
1130
1129
|
if (!environmentId) {
|
|
1131
1130
|
sendWs(ws, {
|
|
1132
1131
|
type: "task_diff",
|
|
@@ -1395,7 +1394,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1395
1394
|
logger.warn({ err }, "Failed to list codespaces");
|
|
1396
1395
|
sendWs(ws, {
|
|
1397
1396
|
type: "codespaces_list",
|
|
1398
|
-
payload: {
|
|
1397
|
+
payload: {
|
|
1398
|
+
codespaces: [],
|
|
1399
|
+
error: formatGhError(err, "list codespaces"),
|
|
1400
|
+
},
|
|
1399
1401
|
});
|
|
1400
1402
|
}
|
|
1401
1403
|
break;
|
|
@@ -1410,15 +1412,17 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1410
1412
|
return;
|
|
1411
1413
|
}
|
|
1412
1414
|
const trimmedRepo = repo.trim();
|
|
1415
|
+
const machine = typeof msg.payload?.machine === "string"
|
|
1416
|
+
? msg.payload.machine.trim()
|
|
1417
|
+
: "";
|
|
1418
|
+
const createArgs = ["codespace", "create", "--repo", trimmedRepo];
|
|
1419
|
+
if (machine) {
|
|
1420
|
+
createArgs.push("--machine", machine);
|
|
1421
|
+
}
|
|
1413
1422
|
try {
|
|
1414
|
-
const result = await exec("gh",
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
"--repo",
|
|
1418
|
-
trimmedRepo,
|
|
1419
|
-
"--machine",
|
|
1420
|
-
"basicLinux32gb",
|
|
1421
|
-
], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
|
|
1423
|
+
const result = await exec("gh", createArgs, {
|
|
1424
|
+
timeout: GH_CODESPACE_CREATE_TIMEOUT_MS,
|
|
1425
|
+
});
|
|
1422
1426
|
const codespaceName = result.stdout.trim();
|
|
1423
1427
|
sendWs(ws, {
|
|
1424
1428
|
type: "codespace_created",
|
|
@@ -1429,7 +1433,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1429
1433
|
logger.error({ err, repo }, "Failed to create codespace");
|
|
1430
1434
|
sendWs(ws, {
|
|
1431
1435
|
type: "codespace_create_error",
|
|
1432
|
-
payload: { message:
|
|
1436
|
+
payload: { message: formatGhError(err, "create codespace") },
|
|
1433
1437
|
});
|
|
1434
1438
|
}
|
|
1435
1439
|
break;
|