@grackle-ai/server 0.14.10 → 0.15.1

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/ws-bridge.js CHANGED
@@ -4,12 +4,13 @@ import { grackle, powerline } from "@grackle-ai/common";
4
4
  import * as envRegistry from "./env-registry.js";
5
5
  import * as sessionStore from "./session-store.js";
6
6
  import * as adapterManager from "./adapter-manager.js";
7
- import { reconnectOrProvision } from "./adapters/adapter.js";
7
+ import { reconnectOrProvision, } from "./adapters/adapter.js";
8
8
  import * as streamHub from "./stream-hub.js";
9
9
  import * as tokenBroker from "./token-broker.js";
10
10
  import * as projectStore from "./project-store.js";
11
11
  import * as taskStore from "./task-store.js";
12
12
  import * as findingStore from "./finding-store.js";
13
+ import * as personaStore from "./persona-store.js";
13
14
  import { v4 as uuid } from "uuid";
14
15
  import { join } from "node:path";
15
16
  import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, eventTypeToString, } from "@grackle-ai/common";
@@ -21,6 +22,7 @@ import { buildTaskSystemContext } from "./utils/system-context.js";
21
22
  import { slugify } from "./utils/slugify.js";
22
23
  import { processEventStream } from "./event-processor.js";
23
24
  import { broadcast, setWssInstance } from "./ws-broadcast.js";
25
+ import { buildMcpServersJson } from "./grpc-service.js";
24
26
  import { exec } from "./utils/exec.js";
25
27
  const GH_CODESPACE_LIST_TIMEOUT_MS = 30_000;
26
28
  const GH_CODESPACE_CREATE_TIMEOUT_MS = 300_000;
@@ -108,7 +110,10 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
108
110
  }
109
111
  const adapter = adapterManager.getAdapter(env.adapterType);
110
112
  if (!adapter) {
111
- sendWs(ws, { type: "error", payload: { message: `No adapter for type: ${env.adapterType}` } });
113
+ sendWs(ws, {
114
+ type: "error",
115
+ payload: { message: `No adapter for type: ${env.adapterType}` },
116
+ });
112
117
  return undefined;
113
118
  }
114
119
  logger.info({ environmentId, ...logContext }, "Auto-provisioning environment");
@@ -121,7 +126,12 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
121
126
  logger.info({ environmentId, stage: provEvent.stage, ...logContext }, "Auto-provision progress");
122
127
  broadcast({
123
128
  type: "provision_progress",
124
- payload: { environmentId, stage: provEvent.stage, message: provEvent.message, progress: provEvent.progress },
129
+ payload: {
130
+ environmentId,
131
+ stage: provEvent.stage,
132
+ message: provEvent.message,
133
+ progress: provEvent.progress,
134
+ },
125
135
  });
126
136
  }
127
137
  conn = await adapter.connect(environmentId, config, powerlineToken);
@@ -134,7 +144,12 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
134
144
  logger.info({ environmentId, ...logContext }, "Auto-provision complete");
135
145
  broadcast({
136
146
  type: "provision_progress",
137
- payload: { environmentId, stage: "ready", message: "Environment connected", progress: 1 },
147
+ payload: {
148
+ environmentId,
149
+ stage: "ready",
150
+ message: "Environment connected",
151
+ progress: 1,
152
+ },
138
153
  });
139
154
  return conn;
140
155
  }
@@ -145,9 +160,19 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
145
160
  const errorMessage = err instanceof Error ? err.message : String(err);
146
161
  broadcast({
147
162
  type: "provision_progress",
148
- payload: { environmentId, stage: "error", message: `Auto-provision failed: ${errorMessage}`, progress: 0 },
163
+ payload: {
164
+ environmentId,
165
+ stage: "error",
166
+ message: `Auto-provision failed: ${errorMessage}`,
167
+ progress: 0,
168
+ },
169
+ });
170
+ sendWs(ws, {
171
+ type: "error",
172
+ payload: {
173
+ message: `Failed to auto-connect environment ${environmentId}: ${errorMessage}`,
174
+ },
149
175
  });
150
- sendWs(ws, { type: "error", payload: { message: `Failed to auto-connect environment ${environmentId}: ${errorMessage}` } });
151
176
  return undefined;
152
177
  }
153
178
  }
@@ -171,34 +196,76 @@ async function startTaskSession(ws, task, options) {
171
196
  logger.warn({ taskId: task.id, environmentId }, "startTaskSession failed: environment not found");
172
197
  return `Environment not found: ${environmentId}`;
173
198
  }
174
- // autoProvisionEnvironment already sends a detailed WS error on failure,
175
- // so we return undefined to avoid duplicate error messages to the client.
176
- const conn = await autoProvisionEnvironment(ws, environmentId, env, { taskId: task.id });
199
+ const conn = await autoProvisionEnvironment(ws, environmentId, env, {
200
+ taskId: task.id,
201
+ });
177
202
  if (!conn) {
178
203
  return undefined;
179
204
  }
205
+ // Resolve persona
206
+ const resolvedPersonaId = options?.personaId || task.personaId;
207
+ const persona = resolvedPersonaId
208
+ ? personaStore.getPersona(resolvedPersonaId)
209
+ : undefined;
210
+ if (resolvedPersonaId && !persona) {
211
+ return `Persona not found: ${resolvedPersonaId}`;
212
+ }
180
213
  const sessionId = uuid();
181
- const runtime = options?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
182
- const model = options?.model || process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
214
+ const runtime = options?.runtime ||
215
+ persona?.runtime ||
216
+ env.defaultRuntime ||
217
+ DEFAULT_RUNTIME;
218
+ const model = options?.model ||
219
+ persona?.model ||
220
+ process.env.GRACKLE_DEFAULT_MODEL ||
221
+ DEFAULT_MODEL;
222
+ const maxTurns = persona?.maxTurns || 0;
183
223
  const logPath = join(grackleHome, LOGS_DIR, sessionId);
184
- // Re-read task to get latest reviewNotes (important for reject→retry flow)
185
224
  const freshTask = taskStore.getTask(task.id) || task;
186
- const systemContext = buildTaskSystemContext(freshTask.title, freshTask.description, freshTask.reviewNotes, freshTask.canDecompose);
225
+ let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description, freshTask.reviewNotes, freshTask.canDecompose);
226
+ if (persona) {
227
+ systemContext = persona.systemPrompt + "\n\n" + systemContext;
228
+ }
187
229
  sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath);
188
230
  taskStore.setTaskSession(freshTask.id, sessionId);
189
231
  taskStore.markTaskStarted(freshTask.id);
190
- broadcast({ type: "task_started", payload: { taskId: freshTask.id, sessionId, projectId: freshTask.projectId } });
232
+ broadcast({
233
+ type: "task_started",
234
+ payload: {
235
+ taskId: freshTask.id,
236
+ sessionId,
237
+ projectId: freshTask.projectId,
238
+ },
239
+ });
240
+ let mcpServersJson = "";
241
+ if (persona) {
242
+ try {
243
+ const parsed = JSON.parse(persona.mcpServers || "[]");
244
+ if (Array.isArray(parsed)) {
245
+ const mcpServers = parsed;
246
+ if (mcpServers.length > 0) {
247
+ mcpServersJson = buildMcpServersJson(mcpServers);
248
+ }
249
+ }
250
+ }
251
+ catch {
252
+ logger.warn("Failed to parse persona.mcpServers JSON; ignoring");
253
+ }
254
+ }
191
255
  const powerlineReq = create(powerline.SpawnRequestSchema, {
192
256
  sessionId,
193
257
  runtime,
194
258
  prompt: freshTask.title,
195
259
  model,
196
- maxTurns: 0,
260
+ maxTurns,
197
261
  branch: freshTask.branch,
198
- worktreeBasePath: freshTask.branch ? (process.env.GRACKLE_WORKTREE_BASE || "/workspace") : "",
262
+ worktreeBasePath: freshTask.branch
263
+ ? process.env.GRACKLE_WORKTREE_BASE || "/workspace"
264
+ : "",
199
265
  systemContext,
200
266
  projectId: freshTask.projectId,
201
267
  taskId: freshTask.id,
268
+ mcpServersJson,
202
269
  });
203
270
  processEventStream(conn.client.spawn(powerlineReq), {
204
271
  sessionId,
@@ -215,7 +282,10 @@ async function startTaskSession(ws, task, options) {
215
282
  else if (sess?.status === "failed") {
216
283
  taskStore.markTaskCompleted(freshTask.id, "failed");
217
284
  }
218
- broadcast({ type: "task_updated", payload: { taskId: freshTask.id, projectId: freshTask.projectId } });
285
+ broadcast({
286
+ type: "task_updated",
287
+ payload: { taskId: freshTask.id, projectId: freshTask.projectId },
288
+ });
219
289
  }
220
290
  },
221
291
  });
@@ -332,12 +402,18 @@ async function handleMessage(ws, msg, subscriptions) {
332
402
  const branch = msg.payload?.branch || "";
333
403
  const systemContext = msg.payload?.systemContext || "";
334
404
  if (!environmentId || !prompt) {
335
- sendWs(ws, { type: "error", payload: { message: "environmentId and prompt required" } });
405
+ sendWs(ws, {
406
+ type: "error",
407
+ payload: { message: "environmentId and prompt required" },
408
+ });
336
409
  return;
337
410
  }
338
411
  const env = envRegistry.getEnvironment(environmentId);
339
412
  if (!env) {
340
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
413
+ sendWs(ws, {
414
+ type: "error",
415
+ payload: { message: `Environment not found: ${environmentId}` },
416
+ });
341
417
  return;
342
418
  }
343
419
  // Auto-provision the environment if not already connected
@@ -382,29 +458,59 @@ async function handleMessage(ws, msg, subscriptions) {
382
458
  const sessionId = msg.payload?.sessionId;
383
459
  const text = msg.payload?.text;
384
460
  if (!sessionId || !text) {
385
- sendWs(ws, { type: "error", payload: { message: "sessionId and text required" } });
461
+ sendWs(ws, {
462
+ type: "error",
463
+ payload: { message: "sessionId and text required" },
464
+ });
386
465
  return;
387
466
  }
388
467
  const session = sessionStore.getSession(sessionId);
389
468
  if (!session) {
390
- sendWs(ws, { type: "error", payload: { message: `Session not found: ${sessionId}` } });
469
+ sendWs(ws, {
470
+ type: "error",
471
+ payload: { message: `Session not found: ${sessionId}` },
472
+ });
391
473
  return;
392
474
  }
393
475
  if (session.status !== "waiting_input") {
394
- sendWs(ws, { type: "error", payload: { message: `Session ${sessionId} is not currently waiting for input (status: ${session.status})` } });
476
+ sendWs(ws, {
477
+ type: "error",
478
+ payload: {
479
+ message: `Session ${sessionId} is not currently waiting for input (status: ${session.status})`,
480
+ },
481
+ });
395
482
  return;
396
483
  }
397
484
  const conn = adapterManager.getConnection(session.environmentId);
398
485
  if (!conn) {
399
- sendWs(ws, { type: "error", payload: { message: `Environment ${session.environmentId} is not connected` } });
486
+ sendWs(ws, {
487
+ type: "error",
488
+ payload: {
489
+ message: `Environment ${session.environmentId} is not connected`,
490
+ },
491
+ });
400
492
  return;
401
493
  }
494
+ // Record the user's input as a session event before forwarding to the agent
495
+ const userInputEvent = create(grackle.SessionEventSchema, {
496
+ sessionId,
497
+ type: grackle.EventType.USER_INPUT,
498
+ timestamp: new Date().toISOString(),
499
+ content: text,
500
+ });
501
+ if (session.logPath) {
502
+ logWriter.writeEvent(session.logPath, userInputEvent);
503
+ }
504
+ streamHub.publish(userInputEvent);
402
505
  try {
403
506
  await conn.client.sendInput(create(powerline.InputMessageSchema, { sessionId, text }));
404
507
  }
405
508
  catch (err) {
406
509
  const errMessage = err instanceof Error ? err.message : String(err);
407
- sendWs(ws, { type: "error", payload: { message: `Failed to send input: ${errMessage}` } });
510
+ sendWs(ws, {
511
+ type: "error",
512
+ payload: { message: `Failed to send input: ${errMessage}` },
513
+ });
408
514
  }
409
515
  break;
410
516
  }
@@ -423,7 +529,10 @@ async function handleMessage(ws, msg, subscriptions) {
423
529
  await conn.client.kill(create(powerline.SessionIdSchema, { id: sessionId }));
424
530
  }
425
531
  catch (err) {
426
- sendWs(ws, { type: "error", payload: { message: `Kill failed: ${err}` } });
532
+ sendWs(ws, {
533
+ type: "error",
534
+ payload: { message: `Kill failed: ${err}` },
535
+ });
427
536
  return;
428
537
  }
429
538
  }
@@ -482,6 +591,102 @@ async function handleMessage(ws, msg, subscriptions) {
482
591
  broadcast({ type: "project_archived", payload: { projectId } });
483
592
  break;
484
593
  }
594
+ // ─── Personas ──────────────────────────────────────────
595
+ case "list_personas": {
596
+ const rows = personaStore.listPersonas();
597
+ sendWs(ws, {
598
+ type: "personas",
599
+ payload: {
600
+ personas: rows.map((r) => ({
601
+ id: r.id,
602
+ name: r.name,
603
+ description: r.description,
604
+ systemPrompt: r.systemPrompt,
605
+ toolConfig: r.toolConfig,
606
+ runtime: r.runtime,
607
+ model: r.model,
608
+ maxTurns: r.maxTurns,
609
+ mcpServers: r.mcpServers,
610
+ createdAt: r.createdAt,
611
+ updatedAt: r.updatedAt,
612
+ })),
613
+ },
614
+ });
615
+ break;
616
+ }
617
+ case "create_persona": {
618
+ const personaName = msg.payload?.name;
619
+ if (!personaName) {
620
+ sendWs(ws, { type: "error", payload: { message: "name required" } });
621
+ return;
622
+ }
623
+ const personaSystemPrompt = msg.payload?.systemPrompt;
624
+ if (!personaSystemPrompt) {
625
+ sendWs(ws, {
626
+ type: "error",
627
+ payload: { message: "systemPrompt required" },
628
+ });
629
+ return;
630
+ }
631
+ let personaId = slugify(personaName) || uuid().slice(0, 8);
632
+ const MAX_ID_RETRIES = 10;
633
+ for (let i = 0; i < MAX_ID_RETRIES && personaStore.getPersona(personaId); i++) {
634
+ personaId = `${slugify(personaName) || "persona"}-${uuid().slice(0, 4)}`;
635
+ }
636
+ personaStore.createPersona(personaId, personaName, msg.payload?.description || "", personaSystemPrompt, msg.payload?.toolConfig || "{}", msg.payload?.runtime || "", msg.payload?.model || "", msg.payload?.maxTurns || 0, msg.payload?.mcpServers || "[]");
637
+ broadcast({ type: "persona_created", payload: { personaId } });
638
+ break;
639
+ }
640
+ case "get_persona": {
641
+ const getPersonaId = msg.payload?.personaId;
642
+ if (!getPersonaId)
643
+ return;
644
+ const personaRow = personaStore.getPersona(getPersonaId);
645
+ if (!personaRow) {
646
+ sendWs(ws, {
647
+ type: "error",
648
+ payload: { message: `Persona not found: ${getPersonaId}` },
649
+ });
650
+ return;
651
+ }
652
+ sendWs(ws, { type: "persona", payload: { persona: personaRow } });
653
+ break;
654
+ }
655
+ case "update_persona": {
656
+ const updatePersonaId = msg.payload?.personaId;
657
+ if (!updatePersonaId) {
658
+ sendWs(ws, {
659
+ type: "error",
660
+ payload: { message: "personaId required" },
661
+ });
662
+ return;
663
+ }
664
+ const existingPersona = personaStore.getPersona(updatePersonaId);
665
+ if (!existingPersona) {
666
+ sendWs(ws, {
667
+ type: "error",
668
+ payload: { message: `Persona not found: ${updatePersonaId}` },
669
+ });
670
+ return;
671
+ }
672
+ personaStore.updatePersona(updatePersonaId, msg.payload?.name || existingPersona.name, msg.payload?.description || existingPersona.description, msg.payload?.systemPrompt || existingPersona.systemPrompt, msg.payload?.toolConfig || existingPersona.toolConfig, msg.payload?.runtime || existingPersona.runtime, msg.payload?.model || existingPersona.model, msg.payload?.maxTurns || existingPersona.maxTurns, msg.payload?.mcpServers || existingPersona.mcpServers);
673
+ broadcast({
674
+ type: "persona_updated",
675
+ payload: { personaId: updatePersonaId },
676
+ });
677
+ break;
678
+ }
679
+ case "delete_persona": {
680
+ const deletePersonaId = msg.payload?.personaId;
681
+ if (!deletePersonaId)
682
+ return;
683
+ personaStore.deletePersona(deletePersonaId);
684
+ broadcast({
685
+ type: "persona_deleted",
686
+ payload: { personaId: deletePersonaId },
687
+ });
688
+ break;
689
+ }
485
690
  // ─── Tasks ─────────────────────────────────────────────
486
691
  case "list_tasks": {
487
692
  const projectId = msg.payload?.projectId;
@@ -510,6 +715,7 @@ async function handleMessage(ws, msg, subscriptions) {
510
715
  depth: r.depth,
511
716
  childTaskIds: childIdsMap.get(r.id) ?? [],
512
717
  canDecompose: r.canDecompose,
718
+ personaId: r.personaId,
513
719
  })),
514
720
  },
515
721
  });
@@ -519,21 +725,45 @@ async function handleMessage(ws, msg, subscriptions) {
519
725
  const projectId = msg.payload?.projectId;
520
726
  const title = msg.payload?.title;
521
727
  if (!projectId || !title) {
522
- sendWs(ws, { type: "error", payload: { message: "projectId and title required" } });
728
+ sendWs(ws, {
729
+ type: "error",
730
+ payload: { message: "projectId and title required" },
731
+ });
523
732
  return;
524
733
  }
525
734
  const project = projectStore.getProject(projectId);
526
735
  if (!project) {
527
- sendWs(ws, { type: "error", payload: { message: `Project not found: ${projectId}` } });
736
+ sendWs(ws, {
737
+ type: "error",
738
+ payload: { message: `Project not found: ${projectId}` },
739
+ });
528
740
  return;
529
741
  }
530
742
  const parentTaskId = msg.payload?.parentTaskId || "";
531
743
  const rawCanDecompose = msg.payload?.canDecompose;
532
744
  const canDecompose = typeof rawCanDecompose === "boolean" ? rawCanDecompose : undefined;
745
+ // Resolve environment: explicit > parent task's env > project default
746
+ let resolvedEnvId = msg.payload?.environmentId || "";
747
+ if (!resolvedEnvId && parentTaskId) {
748
+ const parentTask = taskStore.getTask(parentTaskId);
749
+ if (parentTask?.environmentId) {
750
+ resolvedEnvId = parentTask.environmentId;
751
+ }
752
+ }
753
+ if (!resolvedEnvId) {
754
+ resolvedEnvId = project.defaultEnvironmentId;
755
+ }
533
756
  const id = uuid().slice(0, 8);
534
- taskStore.createTask(id, projectId, title, msg.payload?.description || "", msg.payload?.environmentId || project.defaultEnvironmentId, msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose);
757
+ taskStore.createTask(id, projectId, title, msg.payload?.description || "", resolvedEnvId, msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose, msg.payload?.personaId || "");
535
758
  const row = taskStore.getTask(id);
536
- broadcast({ type: "task_created", payload: { task: row ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) } : null } });
759
+ broadcast({
760
+ type: "task_created",
761
+ payload: {
762
+ task: row
763
+ ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) }
764
+ : null,
765
+ },
766
+ });
537
767
  break;
538
768
  }
539
769
  case "start_task": {
@@ -542,20 +772,32 @@ async function handleMessage(ws, msg, subscriptions) {
542
772
  return;
543
773
  const task = taskStore.getTask(taskId);
544
774
  if (!task) {
545
- sendWs(ws, { type: "error", payload: { message: `Task not found: ${taskId}` } });
775
+ sendWs(ws, {
776
+ type: "error",
777
+ payload: { message: `Task not found: ${taskId}` },
778
+ });
546
779
  return;
547
780
  }
548
781
  if (!["pending", "assigned", "failed"].includes(task.status)) {
549
- sendWs(ws, { type: "error", payload: { message: `Task cannot be started (status: ${task.status})` } });
782
+ sendWs(ws, {
783
+ type: "error",
784
+ payload: {
785
+ message: `Task cannot be started (status: ${task.status})`,
786
+ },
787
+ });
550
788
  return;
551
789
  }
552
790
  if (!taskStore.areDependenciesMet(taskId)) {
553
- sendWs(ws, { type: "error", payload: { message: "Task has unmet dependencies" } });
791
+ sendWs(ws, {
792
+ type: "error",
793
+ payload: { message: "Task has unmet dependencies" },
794
+ });
554
795
  return;
555
796
  }
556
797
  const startError = await startTaskSession(ws, task, {
557
798
  runtime: msg.payload?.runtime,
558
799
  model: msg.payload?.model,
800
+ personaId: msg.payload?.personaId || task.personaId || undefined,
559
801
  });
560
802
  if (startError) {
561
803
  sendWs(ws, { type: "error", payload: { message: startError } });
@@ -587,7 +829,12 @@ async function handleMessage(ws, msg, subscriptions) {
587
829
  if (!task)
588
830
  return;
589
831
  if (task.status !== "review") {
590
- sendWs(ws, { type: "error", payload: { message: `Task cannot be rejected (status: ${task.status})` } });
832
+ sendWs(ws, {
833
+ type: "error",
834
+ payload: {
835
+ message: `Task cannot be rejected (status: ${task.status})`,
836
+ },
837
+ });
591
838
  return;
592
839
  }
593
840
  // Preserve runtime/model from the previous session so the retry
@@ -597,7 +844,10 @@ async function handleMessage(ws, msg, subscriptions) {
597
844
  : undefined;
598
845
  // Store review notes and set status to assigned
599
846
  taskStore.updateTask(task.id, task.title, task.description, "assigned", task.environmentId, safeParseJsonArray(task.dependsOn), reviewNotes);
600
- broadcast({ type: "task_rejected", payload: { taskId, projectId: task.projectId } });
847
+ broadcast({
848
+ type: "task_rejected",
849
+ payload: { taskId, projectId: task.projectId },
850
+ });
601
851
  // Auto-retry: start a new session with the review feedback
602
852
  const freshTask = taskStore.getTask(taskId);
603
853
  if (freshTask) {
@@ -615,14 +865,51 @@ async function handleMessage(ws, msg, subscriptions) {
615
865
  const taskId = msg.payload?.taskId;
616
866
  if (!taskId)
617
867
  return;
868
+ const deletedTask = taskStore.getTask(taskId);
869
+ if (!deletedTask) {
870
+ sendWs(ws, { type: "error", payload: { message: `Task not found: ${taskId}` } });
871
+ return;
872
+ }
618
873
  const children = taskStore.getChildren(taskId);
619
874
  if (children.length > 0) {
620
- sendWs(ws, { type: "error", payload: { message: "Cannot delete task with children. Delete children first." } });
875
+ sendWs(ws, {
876
+ type: "error",
877
+ payload: {
878
+ message: "Cannot delete task with children. Delete children first.",
879
+ },
880
+ });
621
881
  return;
622
882
  }
623
- const deletedTask = taskStore.getTask(taskId);
624
- taskStore.deleteTask(taskId);
625
- broadcast({ type: "task_deleted", payload: { taskId, projectId: deletedTask?.projectId } });
883
+ // Kill active session before deleting the task
884
+ if (deletedTask.sessionId) {
885
+ const activeSession = sessionStore.getSession(deletedTask.sessionId);
886
+ if (activeSession && (activeSession.status === "running" || activeSession.status === "waiting_input")) {
887
+ const conn = adapterManager.getConnection(activeSession.environmentId);
888
+ if (conn) {
889
+ try {
890
+ await conn.client.kill(create(powerline.SessionIdSchema, { id: deletedTask.sessionId }));
891
+ }
892
+ catch (err) {
893
+ logger.warn({ taskId, sessionId: deletedTask.sessionId, err }, "Failed to kill session during task deletion");
894
+ }
895
+ }
896
+ sessionStore.updateSession(deletedTask.sessionId, "killed");
897
+ streamHub.publish(create(grackle.SessionEventSchema, {
898
+ sessionId: deletedTask.sessionId,
899
+ type: grackle.EventType.STATUS,
900
+ timestamp: new Date().toISOString(),
901
+ content: "killed",
902
+ raw: "",
903
+ }));
904
+ }
905
+ }
906
+ const changes = taskStore.deleteTask(taskId);
907
+ if (changes === 0) {
908
+ logger.error({ taskId }, "deleteTask returned 0 changes despite task existing");
909
+ sendWs(ws, { type: "error", payload: { message: `Failed to delete task ${taskId}: no rows affected` } });
910
+ return;
911
+ }
912
+ broadcast({ type: "task_deleted", payload: { taskId, projectId: deletedTask.projectId } });
626
913
  break;
627
914
  }
628
915
  // ─── Findings ──────────────────────────────────────────
@@ -654,7 +941,10 @@ async function handleMessage(ws, msg, subscriptions) {
654
941
  const projectId = msg.payload?.projectId;
655
942
  const title = msg.payload?.title;
656
943
  if (!projectId || !title) {
657
- sendWs(ws, { type: "error", payload: { message: "projectId and title required" } });
944
+ sendWs(ws, {
945
+ type: "error",
946
+ payload: { message: "projectId and title required" },
947
+ });
658
948
  return;
659
949
  }
660
950
  const id = uuid().slice(0, 8);
@@ -669,17 +959,27 @@ async function handleMessage(ws, msg, subscriptions) {
669
959
  return;
670
960
  const task = taskStore.getTask(taskId);
671
961
  if (!task || !task.branch) {
672
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "No branch" } });
962
+ sendWs(ws, {
963
+ type: "task_diff",
964
+ payload: { taskId, error: "No branch" },
965
+ });
673
966
  return;
674
967
  }
675
- const environmentId = task.environmentId || projectStore.getProject(task.projectId)?.defaultEnvironmentId;
968
+ const environmentId = task.environmentId ||
969
+ projectStore.getProject(task.projectId)?.defaultEnvironmentId;
676
970
  if (!environmentId) {
677
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "No environment" } });
971
+ sendWs(ws, {
972
+ type: "task_diff",
973
+ payload: { taskId, error: "No environment" },
974
+ });
678
975
  return;
679
976
  }
680
977
  const conn = adapterManager.getConnection(environmentId);
681
978
  if (!conn) {
682
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "Environment not connected" } });
979
+ sendWs(ws, {
980
+ type: "task_diff",
981
+ payload: { taskId, error: "Environment not connected" },
982
+ });
683
983
  return;
684
984
  }
685
985
  try {
@@ -701,24 +1001,36 @@ async function handleMessage(ws, msg, subscriptions) {
701
1001
  });
702
1002
  }
703
1003
  catch (err) {
704
- sendWs(ws, { type: "task_diff", payload: { taskId, error: String(err) } });
1004
+ sendWs(ws, {
1005
+ type: "task_diff",
1006
+ payload: { taskId, error: String(err) },
1007
+ });
705
1008
  }
706
1009
  break;
707
1010
  }
708
1011
  case "provision_environment": {
709
1012
  const environmentId = msg.payload?.environmentId;
710
1013
  if (!environmentId) {
711
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1014
+ sendWs(ws, {
1015
+ type: "error",
1016
+ payload: { message: "environmentId required" },
1017
+ });
712
1018
  return;
713
1019
  }
714
1020
  const env = envRegistry.getEnvironment(environmentId);
715
1021
  if (!env) {
716
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
1022
+ sendWs(ws, {
1023
+ type: "error",
1024
+ payload: { message: `Environment not found: ${environmentId}` },
1025
+ });
717
1026
  return;
718
1027
  }
719
1028
  const adapter = adapterManager.getAdapter(env.adapterType);
720
1029
  if (!adapter) {
721
- sendWs(ws, { type: "error", payload: { message: `No adapter for type: ${env.adapterType}` } });
1030
+ sendWs(ws, {
1031
+ type: "error",
1032
+ payload: { message: `No adapter for type: ${env.adapterType}` },
1033
+ });
722
1034
  return;
723
1035
  }
724
1036
  logger.info({ environmentId, adapterType: env.adapterType }, "Provisioning environment");
@@ -734,7 +1046,12 @@ async function handleMessage(ws, msg, subscriptions) {
734
1046
  logger.info({ environmentId, stage: event.stage, message: event.message }, "Provision progress");
735
1047
  broadcast({
736
1048
  type: "provision_progress",
737
- payload: { environmentId, stage: event.stage, message: event.message, progress: event.progress },
1049
+ payload: {
1050
+ environmentId,
1051
+ stage: event.stage,
1052
+ message: event.message,
1053
+ progress: event.progress,
1054
+ },
738
1055
  });
739
1056
  }
740
1057
  logger.info({ environmentId }, "Provision complete, calling adapter.connect");
@@ -747,7 +1064,12 @@ async function handleMessage(ws, msg, subscriptions) {
747
1064
  logger.info({ environmentId }, "Environment connected");
748
1065
  broadcast({
749
1066
  type: "provision_progress",
750
- payload: { environmentId, stage: "ready", message: "Environment connected", progress: 1 },
1067
+ payload: {
1068
+ environmentId,
1069
+ stage: "ready",
1070
+ message: "Environment connected",
1071
+ progress: 1,
1072
+ },
751
1073
  });
752
1074
  }
753
1075
  catch (err) {
@@ -756,7 +1078,12 @@ async function handleMessage(ws, msg, subscriptions) {
756
1078
  const errorMessage = err instanceof Error ? err.message : String(err);
757
1079
  broadcast({
758
1080
  type: "provision_progress",
759
- payload: { environmentId, stage: "error", message: `Connection failed: ${errorMessage}`, progress: 0 },
1081
+ payload: {
1082
+ environmentId,
1083
+ stage: "error",
1084
+ message: `Connection failed: ${errorMessage}`,
1085
+ progress: 0,
1086
+ },
760
1087
  });
761
1088
  }
762
1089
  broadcastEnvironments();
@@ -766,12 +1093,18 @@ async function handleMessage(ws, msg, subscriptions) {
766
1093
  case "stop_environment": {
767
1094
  const environmentId = msg.payload?.environmentId;
768
1095
  if (!environmentId) {
769
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1096
+ sendWs(ws, {
1097
+ type: "error",
1098
+ payload: { message: "environmentId required" },
1099
+ });
770
1100
  return;
771
1101
  }
772
1102
  const env = envRegistry.getEnvironment(environmentId);
773
1103
  if (!env) {
774
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
1104
+ sendWs(ws, {
1105
+ type: "error",
1106
+ payload: { message: `Environment not found: ${environmentId}` },
1107
+ });
775
1108
  return;
776
1109
  }
777
1110
  const adapter = adapterManager.getAdapter(env.adapterType);
@@ -789,11 +1122,17 @@ async function handleMessage(ws, msg, subscriptions) {
789
1122
  const displayName = msg.payload?.displayName || "";
790
1123
  const adapterType = msg.payload?.adapterType || "";
791
1124
  if (!displayName || !adapterType) {
792
- sendWs(ws, { type: "error", payload: { message: "displayName and adapterType required" } });
1125
+ sendWs(ws, {
1126
+ type: "error",
1127
+ payload: { message: "displayName and adapterType required" },
1128
+ });
793
1129
  return;
794
1130
  }
795
1131
  if (!adapterManager.getAdapter(adapterType)) {
796
- sendWs(ws, { type: "error", payload: { message: `Unknown adapter type: ${adapterType}` } });
1132
+ sendWs(ws, {
1133
+ type: "error",
1134
+ payload: { message: `Unknown adapter type: ${adapterType}` },
1135
+ });
797
1136
  return;
798
1137
  }
799
1138
  const baseEnvId = slugify(displayName) || uuid().slice(0, 8);
@@ -815,7 +1154,10 @@ async function handleMessage(ws, msg, subscriptions) {
815
1154
  JSON.parse(normalized);
816
1155
  }
817
1156
  catch {
818
- sendWs(ws, { type: "error", payload: { message: "adapterConfig string is not valid JSON" } });
1157
+ sendWs(ws, {
1158
+ type: "error",
1159
+ payload: { message: "adapterConfig string is not valid JSON" },
1160
+ });
819
1161
  return;
820
1162
  }
821
1163
  adapterConfig = normalized;
@@ -824,7 +1166,12 @@ async function handleMessage(ws, msg, subscriptions) {
824
1166
  adapterConfig = JSON.stringify(rawAdapterConfig);
825
1167
  }
826
1168
  else {
827
- sendWs(ws, { type: "error", payload: { message: "adapterConfig must be an object or JSON string" } });
1169
+ sendWs(ws, {
1170
+ type: "error",
1171
+ payload: {
1172
+ message: "adapterConfig must be an object or JSON string",
1173
+ },
1174
+ });
828
1175
  return;
829
1176
  }
830
1177
  const defaultRuntime = msg.payload?.defaultRuntime || DEFAULT_RUNTIME;
@@ -837,7 +1184,10 @@ async function handleMessage(ws, msg, subscriptions) {
837
1184
  case "remove_environment": {
838
1185
  const environmentId = msg.payload?.environmentId;
839
1186
  if (!environmentId) {
840
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1187
+ sendWs(ws, {
1188
+ type: "error",
1189
+ payload: { message: "environmentId required" },
1190
+ });
841
1191
  return;
842
1192
  }
843
1193
  const env = envRegistry.getEnvironment(environmentId);
@@ -848,11 +1198,15 @@ async function handleMessage(ws, msg, subscriptions) {
848
1198
  try {
849
1199
  await adapter.destroy(environmentId, config);
850
1200
  }
851
- catch { /* best-effort */ }
1201
+ catch {
1202
+ /* best-effort */
1203
+ }
852
1204
  try {
853
1205
  await adapter.disconnect(environmentId);
854
1206
  }
855
- catch { /* best-effort */ }
1207
+ catch {
1208
+ /* best-effort */
1209
+ }
856
1210
  }
857
1211
  }
858
1212
  adapterManager.removeConnection(environmentId);
@@ -867,31 +1221,43 @@ async function handleMessage(ws, msg, subscriptions) {
867
1221
  case "list_codespaces": {
868
1222
  try {
869
1223
  const result = await exec("gh", [
870
- "codespace", "list",
871
- "--json", "name,repository,state,gitStatus",
872
- "--limit", String(GH_CODESPACE_LIST_LIMIT),
1224
+ "codespace",
1225
+ "list",
1226
+ "--json",
1227
+ "name,repository,state,gitStatus",
1228
+ "--limit",
1229
+ String(GH_CODESPACE_LIST_LIMIT),
873
1230
  ], { timeout: GH_CODESPACE_LIST_TIMEOUT_MS });
874
1231
  const codespaces = JSON.parse(result.stdout || "[]");
875
1232
  sendWs(ws, { type: "codespaces_list", payload: { codespaces } });
876
1233
  }
877
1234
  catch (err) {
878
1235
  logger.warn({ err }, "Failed to list codespaces");
879
- sendWs(ws, { type: "codespaces_list", payload: { codespaces: [], error: String(err) } });
1236
+ sendWs(ws, {
1237
+ type: "codespaces_list",
1238
+ payload: { codespaces: [], error: String(err) },
1239
+ });
880
1240
  }
881
1241
  break;
882
1242
  }
883
1243
  case "create_codespace": {
884
1244
  const repo = msg.payload?.repo;
885
1245
  if (typeof repo !== "string" || repo.trim().length === 0) {
886
- sendWs(ws, { type: "codespace_create_error", payload: { message: "repo required" } });
1246
+ sendWs(ws, {
1247
+ type: "codespace_create_error",
1248
+ payload: { message: "repo required" },
1249
+ });
887
1250
  return;
888
1251
  }
889
1252
  const trimmedRepo = repo.trim();
890
1253
  try {
891
1254
  const result = await exec("gh", [
892
- "codespace", "create",
893
- "--repo", trimmedRepo,
894
- "--machine", "basicLinux32gb",
1255
+ "codespace",
1256
+ "create",
1257
+ "--repo",
1258
+ trimmedRepo,
1259
+ "--machine",
1260
+ "basicLinux32gb",
895
1261
  ], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
896
1262
  const codespaceName = result.stdout.trim();
897
1263
  sendWs(ws, {
@@ -901,7 +1267,10 @@ async function handleMessage(ws, msg, subscriptions) {
901
1267
  }
902
1268
  catch (err) {
903
1269
  logger.error({ err, repo }, "Failed to create codespace");
904
- sendWs(ws, { type: "codespace_create_error", payload: { message: String(err) } });
1270
+ sendWs(ws, {
1271
+ type: "codespace_create_error",
1272
+ payload: { message: String(err) },
1273
+ });
905
1274
  }
906
1275
  break;
907
1276
  }
@@ -926,7 +1295,10 @@ async function handleMessage(ws, msg, subscriptions) {
926
1295
  const name = msg.payload?.name;
927
1296
  const value = msg.payload?.value;
928
1297
  if (!name || !value) {
929
- sendWs(ws, { type: "error", payload: { message: "name and value required" } });
1298
+ sendWs(ws, {
1299
+ type: "error",
1300
+ payload: { message: "name and value required" },
1301
+ });
930
1302
  return;
931
1303
  }
932
1304
  await tokenBroker.setToken({