@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.
Files changed (65) hide show
  1. package/dist/adapter-manager.d.ts.map +1 -1
  2. package/dist/adapter-manager.js +1 -0
  3. package/dist/adapter-manager.js.map +1 -1
  4. package/dist/adapters/codespace.js +1 -1
  5. package/dist/adapters/codespace.js.map +1 -1
  6. package/dist/adapters/docker.js +1 -1
  7. package/dist/adapters/docker.js.map +1 -1
  8. package/dist/adapters/local.js +1 -1
  9. package/dist/adapters/local.js.map +1 -1
  10. package/dist/adapters/remote-adapter-utils.d.ts.map +1 -1
  11. package/dist/adapters/remote-adapter-utils.js +5 -4
  12. package/dist/adapters/remote-adapter-utils.js.map +1 -1
  13. package/dist/adapters/ssh.js +1 -1
  14. package/dist/adapters/ssh.js.map +1 -1
  15. package/dist/compute-task-status.d.ts +11 -7
  16. package/dist/compute-task-status.d.ts.map +1 -1
  17. package/dist/compute-task-status.js +35 -40
  18. package/dist/compute-task-status.js.map +1 -1
  19. package/dist/db.d.ts.map +1 -1
  20. package/dist/db.js +50 -5
  21. package/dist/db.js.map +1 -1
  22. package/dist/event-processor.d.ts.map +1 -1
  23. package/dist/event-processor.js +15 -14
  24. package/dist/event-processor.js.map +1 -1
  25. package/dist/github-import.d.ts.map +1 -1
  26. package/dist/github-import.js +3 -2
  27. package/dist/github-import.js.map +1 -1
  28. package/dist/grpc-service.d.ts.map +1 -1
  29. package/dist/grpc-service.js +73 -41
  30. package/dist/grpc-service.js.map +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +23 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/project-store.d.ts +3 -1
  35. package/dist/project-store.d.ts.map +1 -1
  36. package/dist/project-store.js +5 -1
  37. package/dist/project-store.js.map +1 -1
  38. package/dist/schema.d.ts +19 -38
  39. package/dist/schema.d.ts.map +1 -1
  40. package/dist/schema.js +2 -3
  41. package/dist/schema.js.map +1 -1
  42. package/dist/session-store.d.ts.map +1 -1
  43. package/dist/session-store.js +4 -3
  44. package/dist/session-store.js.map +1 -1
  45. package/dist/task-store.d.ts +6 -6
  46. package/dist/task-store.d.ts.map +1 -1
  47. package/dist/task-store.js +11 -11
  48. package/dist/task-store.js.map +1 -1
  49. package/dist/transcript.js.map +1 -1
  50. package/dist/utils/format-gh-error.d.ts +6 -0
  51. package/dist/utils/format-gh-error.d.ts.map +1 -0
  52. package/dist/utils/format-gh-error.js +30 -0
  53. package/dist/utils/format-gh-error.js.map +1 -0
  54. package/dist/utils/system-context.d.ts +1 -1
  55. package/dist/utils/system-context.d.ts.map +1 -1
  56. package/dist/utils/system-context.js +2 -2
  57. package/dist/utils/system-context.js.map +1 -1
  58. package/dist/ws-bridge.d.ts.map +1 -1
  59. package/dist/ws-bridge.js +102 -105
  60. package/dist/ws-bridge.js.map +1 -1
  61. package/dist/ws-broadcast.d.ts +5 -0
  62. package/dist/ws-broadcast.d.ts.map +1 -1
  63. package/dist/ws-broadcast.js +20 -0
  64. package/dist/ws-broadcast.js.map +1 -1
  65. 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
- persona?.runtime ||
218
- env.defaultRuntime ||
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, freshTask.reviewNotes, freshTask.canDecompose);
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 || !session.logPath) {
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 ? "/workspace" : "",
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 !== "waiting_input") {
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 waiting for input (status: ${session.status})`,
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 killed anyway");
515
+ logger.warn({ sessionId, err }, "PowerLine kill failed — marking session interrupted anyway");
534
516
  }
535
517
  }
536
- sessionStore.updateSession(sessionId, "killed");
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: "killed",
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 !== undefined && nameVal.trim() === "") {
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 = ["completed", "failed", "killed"];
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 pending/assigned tasks (non-late-bind path)
861
- if (!["pending", "assigned"].includes(existingTask.status)) {
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, existingTask.reviewNotes);
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 (!["pending", "assigned", "failed"].includes(effectiveStatus)) {
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 "approve_task": {
924
+ case "complete_task": {
940
925
  const taskId = msg.payload?.taskId;
941
926
  if (!taskId)
942
927
  return;
943
- taskStore.markTaskCompleted(taskId, "done");
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: "task_approved",
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 "reject_task": {
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
- // Use computed status (derived from session history) since the stored
964
- // status is never explicitly set to "review".
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
- // Preserve runtime/model from the previous session so the retry
977
- // doesn't unexpectedly switch runtimes/models.
978
- const previousSession = sessionStore.getLatestSessionForTask(task.id);
979
- // Store review notes and reset status to "pending" so computeTaskStatus
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
- if (retryError) {
998
- // Retry failed — set status to "assigned" so the user can retry manually.
999
- logger.warn({ taskId, error: retryError }, "Auto-retry after rejection failed — task set to assigned");
1000
- taskStore.updateTask(freshTask.id, freshTask.title, freshTask.description, "assigned", safeParseJsonArray(freshTask.dependsOn), freshTask.reviewNotes);
1001
- broadcast({
1002
- type: "task_updated",
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, "killed");
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: "killed",
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 || !task.branch) {
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: { codespaces: [], error: String(err) },
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
- "codespace",
1423
- "create",
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: String(err) },
1436
+ payload: { message: formatGhError(err, "create codespace") },
1440
1437
  });
1441
1438
  }
1442
1439
  break;