@grackle-ai/server 0.26.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 +11 -7
- package/dist/compute-task-status.d.ts.map +1 -1
- package/dist/compute-task-status.js +35 -40
- package/dist/compute-task-status.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +50 -5
- package/dist/db.js.map +1 -1
- package/dist/event-processor.d.ts.map +1 -1
- package/dist/event-processor.js +15 -14
- package/dist/event-processor.js.map +1 -1
- package/dist/github-import.d.ts.map +1 -1
- package/dist/github-import.js +3 -2
- package/dist/github-import.js.map +1 -1
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +73 -41
- 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/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 +19 -38
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -3
- package/dist/schema.js.map +1 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +4 -3
- package/dist/session-store.js.map +1 -1
- package/dist/task-store.d.ts +6 -6
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +11 -11
- 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 +102 -105
- 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,10 +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";
|
|
25
|
+
import { broadcast, setWssInstance, broadcastEnvironments, envRowToWs } from "./ws-broadcast.js";
|
|
26
26
|
import { buildMcpServersJson } from "./grpc-service.js";
|
|
27
27
|
import { computeTaskStatus } from "./compute-task-status.js";
|
|
28
28
|
import { exec } from "./utils/exec.js";
|
|
29
|
+
import { formatGhError } from "./utils/format-gh-error.js";
|
|
29
30
|
const GH_CODESPACE_LIST_TIMEOUT_MS = 30_000;
|
|
30
31
|
const GH_CODESPACE_CREATE_TIMEOUT_MS = 300_000;
|
|
31
32
|
const GH_CODESPACE_LIST_LIMIT = 50;
|
|
@@ -43,6 +44,7 @@ export function createWsBridge(httpServer, verifyApiKey) {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
const subscriptions = new Map();
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
46
48
|
ws.on("message", async (data) => {
|
|
47
49
|
try {
|
|
48
50
|
const msg = JSON.parse(data.toString());
|
|
@@ -66,25 +68,6 @@ export function createWsBridge(httpServer, verifyApiKey) {
|
|
|
66
68
|
});
|
|
67
69
|
return wss;
|
|
68
70
|
}
|
|
69
|
-
/** Map a database environment row to the WebSocket payload shape. */
|
|
70
|
-
function envRowToWs(r) {
|
|
71
|
-
return {
|
|
72
|
-
id: r.id,
|
|
73
|
-
displayName: r.displayName,
|
|
74
|
-
adapterType: r.adapterType,
|
|
75
|
-
adapterConfig: r.adapterConfig,
|
|
76
|
-
defaultRuntime: r.defaultRuntime,
|
|
77
|
-
status: r.status,
|
|
78
|
-
bootstrapped: r.bootstrapped,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/** Broadcast the current environment list to all connected WebSocket clients. */
|
|
82
|
-
function broadcastEnvironments() {
|
|
83
|
-
broadcast({
|
|
84
|
-
type: "environments",
|
|
85
|
-
payload: { environments: envRegistry.listEnvironments().map(envRowToWs) },
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
71
|
/** Safely parse an adapter config string, returning an empty object on failure. */
|
|
89
72
|
function safeParseAdapterConfig(raw, environmentId) {
|
|
90
73
|
try {
|
|
@@ -213,18 +196,13 @@ async function startTaskSession(ws, task, options) {
|
|
|
213
196
|
return `Persona not found: ${resolvedPersonaId}`;
|
|
214
197
|
}
|
|
215
198
|
const sessionId = uuid();
|
|
216
|
-
const runtime = options?.runtime ||
|
|
217
|
-
|
|
218
|
-
env.
|
|
219
|
-
DEFAULT_RUNTIME;
|
|
220
|
-
const model = options?.model ||
|
|
221
|
-
persona?.model ||
|
|
222
|
-
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
223
|
-
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;
|
|
224
202
|
const maxTurns = persona?.maxTurns || 0;
|
|
225
203
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
226
204
|
const freshTask = taskStore.getTask(task.id) || task;
|
|
227
|
-
let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description,
|
|
205
|
+
let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description, options?.notes || "", freshTask.canDecompose);
|
|
228
206
|
if (persona) {
|
|
229
207
|
systemContext = persona.systemPrompt + "\n\n" + systemContext;
|
|
230
208
|
}
|
|
@@ -262,7 +240,7 @@ async function startTaskSession(ws, task, options) {
|
|
|
262
240
|
maxTurns,
|
|
263
241
|
branch: freshTask.branch,
|
|
264
242
|
worktreeBasePath: freshTask.branch
|
|
265
|
-
? process.env.GRACKLE_WORKTREE_BASE || "/workspace"
|
|
243
|
+
? (project.worktreeBasePath || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
266
244
|
: "",
|
|
267
245
|
systemContext,
|
|
268
246
|
projectId: freshTask.projectId,
|
|
@@ -312,7 +290,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
312
290
|
return;
|
|
313
291
|
}
|
|
314
292
|
const session = sessionStore.getSession(sessionId);
|
|
315
|
-
if (!session
|
|
293
|
+
if (!session?.logPath) {
|
|
316
294
|
return;
|
|
317
295
|
}
|
|
318
296
|
const entries = logWriter.readLog(session.logPath);
|
|
@@ -386,8 +364,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
386
364
|
case "spawn": {
|
|
387
365
|
const environmentId = msg.payload?.environmentId;
|
|
388
366
|
const prompt = msg.payload?.prompt;
|
|
389
|
-
const model = msg.payload?.model ||
|
|
390
|
-
const runtime = msg.payload?.runtime ||
|
|
367
|
+
const model = msg.payload?.model || undefined;
|
|
368
|
+
const runtime = msg.payload?.runtime || undefined;
|
|
391
369
|
const branch = msg.payload?.branch || "";
|
|
392
370
|
const systemContext = msg.payload?.systemContext || "";
|
|
393
371
|
const spawnPersonaId = msg.payload?.personaId || "";
|
|
@@ -418,7 +396,9 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
418
396
|
return;
|
|
419
397
|
}
|
|
420
398
|
const sessionId = uuid();
|
|
399
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime/model may be undefined from WS payload
|
|
421
400
|
const sessionRuntime = runtime || spawnPersona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
422
402
|
const sessionModel = model || spawnPersona?.model || process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
|
|
423
403
|
const maxTurns = spawnPersona?.maxTurns || 0;
|
|
424
404
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
@@ -435,7 +415,9 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
435
415
|
model: sessionModel,
|
|
436
416
|
maxTurns,
|
|
437
417
|
branch,
|
|
438
|
-
worktreeBasePath: branch
|
|
418
|
+
worktreeBasePath: branch
|
|
419
|
+
? ((typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "") || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
420
|
+
: "",
|
|
439
421
|
systemContext: finalSystemContext,
|
|
440
422
|
});
|
|
441
423
|
processEventStream(conn.client.spawn(powerlineReq), {
|
|
@@ -448,7 +430,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
448
430
|
sessionId,
|
|
449
431
|
eventType: "error",
|
|
450
432
|
timestamp: new Date().toISOString(),
|
|
451
|
-
content: `Spawn failed: ${err}`,
|
|
433
|
+
content: `Spawn failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
452
434
|
},
|
|
453
435
|
});
|
|
454
436
|
},
|
|
@@ -473,11 +455,11 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
473
455
|
});
|
|
474
456
|
return;
|
|
475
457
|
}
|
|
476
|
-
if (session.status !==
|
|
458
|
+
if (session.status !== SESSION_STATUS.IDLE) {
|
|
477
459
|
sendWs(ws, {
|
|
478
460
|
type: "error",
|
|
479
461
|
payload: {
|
|
480
|
-
message: `Session ${sessionId} is not currently
|
|
462
|
+
message: `Session ${sessionId} is not currently idle (status: ${session.status})`,
|
|
481
463
|
},
|
|
482
464
|
});
|
|
483
465
|
return;
|
|
@@ -530,15 +512,15 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
530
512
|
await conn.client.kill(create(powerline.SessionIdSchema, { id: sessionId }));
|
|
531
513
|
}
|
|
532
514
|
catch (err) {
|
|
533
|
-
logger.warn({ sessionId, err }, "PowerLine kill failed — marking session
|
|
515
|
+
logger.warn({ sessionId, err }, "PowerLine kill failed — marking session interrupted anyway");
|
|
534
516
|
}
|
|
535
517
|
}
|
|
536
|
-
sessionStore.updateSession(sessionId,
|
|
518
|
+
sessionStore.updateSession(sessionId, SESSION_STATUS.INTERRUPTED);
|
|
537
519
|
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
538
520
|
sessionId,
|
|
539
521
|
type: grackle.EventType.STATUS,
|
|
540
522
|
timestamp: new Date().toISOString(),
|
|
541
|
-
content:
|
|
523
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
542
524
|
raw: "",
|
|
543
525
|
}));
|
|
544
526
|
// Broadcast task_updated so frontend re-fetches computed status
|
|
@@ -564,6 +546,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
564
546
|
defaultEnvironmentId: r.defaultEnvironmentId,
|
|
565
547
|
status: r.status,
|
|
566
548
|
useWorktrees: r.useWorktrees,
|
|
549
|
+
worktreeBasePath: r.worktreeBasePath,
|
|
567
550
|
createdAt: r.createdAt,
|
|
568
551
|
updatedAt: r.updatedAt,
|
|
569
552
|
})),
|
|
@@ -587,7 +570,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
587
570
|
}
|
|
588
571
|
// useWorktrees defaults to true when not specified
|
|
589
572
|
const createUseWorktrees = msg.payload?.useWorktrees ?? true;
|
|
590
|
-
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() : "");
|
|
591
574
|
const row = projectStore.getProject(id);
|
|
592
575
|
broadcast({ type: "project_created", payload: { project: row } });
|
|
593
576
|
break;
|
|
@@ -611,7 +594,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
611
594
|
return;
|
|
612
595
|
}
|
|
613
596
|
const nameVal = typeof msg.payload?.name === "string" ? msg.payload.name : undefined;
|
|
614
|
-
if (nameVal
|
|
597
|
+
if (nameVal?.trim() === "") {
|
|
615
598
|
sendWs(ws, { type: "error", payload: { message: "Project name cannot be empty" } });
|
|
616
599
|
return;
|
|
617
600
|
}
|
|
@@ -623,12 +606,14 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
623
606
|
return;
|
|
624
607
|
}
|
|
625
608
|
const worktreesVal = typeof msg.payload?.useWorktrees === "boolean" ? msg.payload.useWorktrees : undefined;
|
|
609
|
+
const worktreeBasePathVal = typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath : undefined;
|
|
626
610
|
projectStore.updateProject(projectId, {
|
|
627
611
|
name: nameVal !== undefined ? nameVal.trim() : undefined,
|
|
628
612
|
description: descVal,
|
|
629
613
|
repoUrl: repoVal,
|
|
630
614
|
defaultEnvironmentId: envVal,
|
|
631
615
|
useWorktrees: worktreesVal,
|
|
616
|
+
worktreeBasePath: worktreeBasePathVal,
|
|
632
617
|
});
|
|
633
618
|
broadcast({ type: "project_updated", payload: { projectId } });
|
|
634
619
|
break;
|
|
@@ -761,7 +746,6 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
761
746
|
branch: r.branch,
|
|
762
747
|
latestSessionId: computed.latestSessionId,
|
|
763
748
|
dependsOn: safeParseJsonArray(r.dependsOn),
|
|
764
|
-
reviewNotes: r.reviewNotes,
|
|
765
749
|
sortOrder: r.sortOrder,
|
|
766
750
|
createdAt: r.createdAt,
|
|
767
751
|
parentTaskId: r.parentTaskId,
|
|
@@ -827,7 +811,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
827
811
|
sendWs(ws, { type: "error", payload: { message: `Session not found: ${lateBindSessionId}` } });
|
|
828
812
|
return;
|
|
829
813
|
}
|
|
830
|
-
const terminalStatuses = [
|
|
814
|
+
const terminalStatuses = [SESSION_STATUS.COMPLETED, SESSION_STATUS.FAILED, SESSION_STATUS.INTERRUPTED];
|
|
831
815
|
if (terminalStatuses.includes(session.status)) {
|
|
832
816
|
sendWs(ws, {
|
|
833
817
|
type: "error",
|
|
@@ -857,8 +841,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
857
841
|
});
|
|
858
842
|
break;
|
|
859
843
|
}
|
|
860
|
-
// Only allow editing
|
|
861
|
-
if (
|
|
844
|
+
// Only allow editing not_started tasks (non-late-bind path)
|
|
845
|
+
if (existingTask.status !== TASK_STATUS.NOT_STARTED) {
|
|
862
846
|
sendWs(ws, {
|
|
863
847
|
type: "error",
|
|
864
848
|
payload: { message: `Task ${updateTaskId} cannot be edited (status: ${existingTask.status})` },
|
|
@@ -879,7 +863,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
879
863
|
.filter((d) => d !== updateTaskId)),
|
|
880
864
|
]
|
|
881
865
|
: safeParseJsonArray(existingTask.dependsOn);
|
|
882
|
-
taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn
|
|
866
|
+
taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn);
|
|
883
867
|
const updatedRow = taskStore.getTask(updateTaskId);
|
|
884
868
|
broadcast({
|
|
885
869
|
type: "task_updated",
|
|
@@ -908,7 +892,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
908
892
|
{
|
|
909
893
|
const taskSessions = sessionStore.listSessionsForTask(taskId);
|
|
910
894
|
const { status: effectiveStatus } = computeTaskStatus(task.status, taskSessions);
|
|
911
|
-
if (![
|
|
895
|
+
if (![TASK_STATUS.NOT_STARTED, TASK_STATUS.FAILED].includes(effectiveStatus)) {
|
|
912
896
|
sendWs(ws, {
|
|
913
897
|
type: "error",
|
|
914
898
|
payload: {
|
|
@@ -930,80 +914,88 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
930
914
|
model: msg.payload?.model,
|
|
931
915
|
personaId: msg.payload?.personaId || undefined,
|
|
932
916
|
environmentId: msg.payload?.environmentId || undefined,
|
|
917
|
+
notes: msg.payload?.notes || undefined,
|
|
933
918
|
});
|
|
934
919
|
if (startError) {
|
|
935
920
|
sendWs(ws, { type: "error", payload: { message: startError } });
|
|
936
921
|
}
|
|
937
922
|
break;
|
|
938
923
|
}
|
|
939
|
-
case "
|
|
924
|
+
case "complete_task": {
|
|
940
925
|
const taskId = msg.payload?.taskId;
|
|
941
926
|
if (!taskId)
|
|
942
927
|
return;
|
|
943
|
-
taskStore.
|
|
928
|
+
taskStore.markTaskComplete(taskId, TASK_STATUS.COMPLETE);
|
|
944
929
|
const task = taskStore.getTask(taskId);
|
|
945
930
|
const unblocked = task ? taskStore.checkAndUnblock(task.projectId) : [];
|
|
946
931
|
sendWs(ws, {
|
|
947
|
-
type: "
|
|
932
|
+
type: "task_completed",
|
|
948
933
|
payload: {
|
|
949
934
|
taskId,
|
|
950
935
|
unblockedTaskIds: unblocked.map((t) => t.id),
|
|
951
936
|
},
|
|
952
937
|
});
|
|
938
|
+
if (task) {
|
|
939
|
+
broadcast({
|
|
940
|
+
type: "task_completed",
|
|
941
|
+
payload: { taskId, projectId: task.projectId },
|
|
942
|
+
});
|
|
943
|
+
}
|
|
953
944
|
break;
|
|
954
945
|
}
|
|
955
|
-
case "
|
|
946
|
+
case "resume_task": {
|
|
956
947
|
const taskId = msg.payload?.taskId;
|
|
957
|
-
const reviewNotes = msg.payload?.reviewNotes || "";
|
|
958
948
|
if (!taskId)
|
|
959
949
|
return;
|
|
960
950
|
const task = taskStore.getTask(taskId);
|
|
961
|
-
if (!task)
|
|
951
|
+
if (!task) {
|
|
952
|
+
sendWs(ws, { type: "error", payload: { message: `Task not found: ${taskId}` } });
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const latestSession = sessionStore.getLatestSessionForTask(taskId);
|
|
956
|
+
if (!latestSession) {
|
|
957
|
+
sendWs(ws, { type: "error", payload: { message: `Task ${taskId} has no sessions to resume` } });
|
|
962
958
|
return;
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const taskSessions = sessionStore.listSessionsByTaskIds([taskId]);
|
|
966
|
-
const { status: effectiveStatus } = computeTaskStatus(task.status, taskSessions);
|
|
967
|
-
if (effectiveStatus !== "review") {
|
|
959
|
+
}
|
|
960
|
+
if (![SESSION_STATUS.INTERRUPTED, SESSION_STATUS.COMPLETED].includes(latestSession.status)) {
|
|
968
961
|
sendWs(ws, {
|
|
969
962
|
type: "error",
|
|
970
|
-
payload: {
|
|
971
|
-
message: `Task cannot be rejected (status: ${effectiveStatus})`,
|
|
972
|
-
},
|
|
963
|
+
payload: { message: `Latest session ${latestSession.id} is not resumable (status: ${latestSession.status})` },
|
|
973
964
|
});
|
|
974
965
|
return;
|
|
975
966
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
// can derive the effective status from the new retry session. We use
|
|
981
|
-
// "pending" rather than "assigned" because rejection + retry is an
|
|
982
|
-
// automated flow — "assigned" implies deliberate human assignment.
|
|
983
|
-
taskStore.updateTask(task.id, task.title, task.description, "pending", safeParseJsonArray(task.dependsOn), reviewNotes);
|
|
984
|
-
broadcast({
|
|
985
|
-
type: "task_rejected",
|
|
986
|
-
payload: { taskId, projectId: task.projectId },
|
|
987
|
-
});
|
|
988
|
-
// Auto-retry: start a new session with the review feedback
|
|
989
|
-
const freshTask = taskStore.getTask(taskId);
|
|
990
|
-
if (freshTask) {
|
|
991
|
-
const retryError = await startTaskSession(ws, freshTask, {
|
|
992
|
-
runtime: previousSession?.runtime,
|
|
993
|
-
model: previousSession?.model,
|
|
994
|
-
environmentId: previousSession?.environmentId,
|
|
995
|
-
personaId: previousSession?.personaId,
|
|
967
|
+
if (!latestSession.runtimeSessionId) {
|
|
968
|
+
sendWs(ws, {
|
|
969
|
+
type: "error",
|
|
970
|
+
payload: { message: `Latest session ${latestSession.id} has no runtime session ID — cannot resume` },
|
|
996
971
|
});
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
payload: { taskId, projectId: freshTask.projectId },
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
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;
|
|
1006
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
|
+
});
|
|
1007
999
|
break;
|
|
1008
1000
|
}
|
|
1009
1001
|
case "delete_task": {
|
|
@@ -1037,12 +1029,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1037
1029
|
logger.warn({ taskId, sessionId: activeSession.id, err }, "Failed to kill session during task deletion");
|
|
1038
1030
|
}
|
|
1039
1031
|
}
|
|
1040
|
-
sessionStore.updateSession(activeSession.id,
|
|
1032
|
+
sessionStore.updateSession(activeSession.id, SESSION_STATUS.INTERRUPTED);
|
|
1041
1033
|
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
1042
1034
|
sessionId: activeSession.id,
|
|
1043
1035
|
type: grackle.EventType.STATUS,
|
|
1044
1036
|
timestamp: new Date().toISOString(),
|
|
1045
|
-
content:
|
|
1037
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
1046
1038
|
raw: "",
|
|
1047
1039
|
}));
|
|
1048
1040
|
}
|
|
@@ -1126,7 +1118,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1126
1118
|
if (!taskId)
|
|
1127
1119
|
return;
|
|
1128
1120
|
const task = taskStore.getTask(taskId);
|
|
1129
|
-
if (!task
|
|
1121
|
+
if (!task?.branch) {
|
|
1130
1122
|
sendWs(ws, {
|
|
1131
1123
|
type: "task_diff",
|
|
1132
1124
|
payload: { taskId, error: "No branch" },
|
|
@@ -1402,7 +1394,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1402
1394
|
logger.warn({ err }, "Failed to list codespaces");
|
|
1403
1395
|
sendWs(ws, {
|
|
1404
1396
|
type: "codespaces_list",
|
|
1405
|
-
payload: {
|
|
1397
|
+
payload: {
|
|
1398
|
+
codespaces: [],
|
|
1399
|
+
error: formatGhError(err, "list codespaces"),
|
|
1400
|
+
},
|
|
1406
1401
|
});
|
|
1407
1402
|
}
|
|
1408
1403
|
break;
|
|
@@ -1417,15 +1412,17 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1417
1412
|
return;
|
|
1418
1413
|
}
|
|
1419
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
|
+
}
|
|
1420
1422
|
try {
|
|
1421
|
-
const result = await exec("gh",
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
"--repo",
|
|
1425
|
-
trimmedRepo,
|
|
1426
|
-
"--machine",
|
|
1427
|
-
"basicLinux32gb",
|
|
1428
|
-
], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
|
|
1423
|
+
const result = await exec("gh", createArgs, {
|
|
1424
|
+
timeout: GH_CODESPACE_CREATE_TIMEOUT_MS,
|
|
1425
|
+
});
|
|
1429
1426
|
const codespaceName = result.stdout.trim();
|
|
1430
1427
|
sendWs(ws, {
|
|
1431
1428
|
type: "codespace_created",
|
|
@@ -1436,7 +1433,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1436
1433
|
logger.error({ err, repo }, "Failed to create codespace");
|
|
1437
1434
|
sendWs(ws, {
|
|
1438
1435
|
type: "codespace_create_error",
|
|
1439
|
-
payload: { message:
|
|
1436
|
+
payload: { message: formatGhError(err, "create codespace") },
|
|
1440
1437
|
});
|
|
1441
1438
|
}
|
|
1442
1439
|
break;
|