@grackle-ai/server 0.39.1 → 0.41.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 (62) hide show
  1. package/dist/db.d.ts.map +1 -1
  2. package/dist/db.js +62 -0
  3. package/dist/db.js.map +1 -1
  4. package/dist/env-registry.d.ts +1 -1
  5. package/dist/env-registry.d.ts.map +1 -1
  6. package/dist/env-registry.js +1 -2
  7. package/dist/env-registry.js.map +1 -1
  8. package/dist/event-bus.d.ts +37 -0
  9. package/dist/event-bus.d.ts.map +1 -0
  10. package/dist/event-bus.js +65 -0
  11. package/dist/event-bus.js.map +1 -0
  12. package/dist/event-processor.d.ts.map +1 -1
  13. package/dist/event-processor.js +14 -11
  14. package/dist/event-processor.js.map +1 -1
  15. package/dist/event-store.d.ts +9 -0
  16. package/dist/event-store.d.ts.map +1 -0
  17. package/dist/event-store.js +16 -0
  18. package/dist/event-store.js.map +1 -0
  19. package/dist/github-import.js +3 -5
  20. package/dist/github-import.js.map +1 -1
  21. package/dist/grpc-service.d.ts.map +1 -1
  22. package/dist/grpc-service.js +106 -129
  23. package/dist/grpc-service.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +10 -7
  26. package/dist/index.js.map +1 -1
  27. package/dist/project-store.d.ts +3 -1
  28. package/dist/project-store.d.ts.map +1 -1
  29. package/dist/project-store.js +5 -1
  30. package/dist/project-store.js.map +1 -1
  31. package/dist/reanimate-agent.d.ts +12 -0
  32. package/dist/reanimate-agent.d.ts.map +1 -0
  33. package/dist/reanimate-agent.js +78 -0
  34. package/dist/reanimate-agent.js.map +1 -0
  35. package/dist/resolve-persona.d.ts +29 -0
  36. package/dist/resolve-persona.d.ts.map +1 -0
  37. package/dist/resolve-persona.js +40 -0
  38. package/dist/resolve-persona.js.map +1 -0
  39. package/dist/schema.d.ts +123 -0
  40. package/dist/schema.d.ts.map +1 -1
  41. package/dist/schema.js +9 -0
  42. package/dist/schema.js.map +1 -1
  43. package/dist/session-store.d.ts +6 -1
  44. package/dist/session-store.d.ts.map +1 -1
  45. package/dist/session-store.js +23 -5
  46. package/dist/session-store.js.map +1 -1
  47. package/dist/settings-store.d.ts +14 -0
  48. package/dist/settings-store.d.ts.map +1 -0
  49. package/dist/settings-store.js +29 -0
  50. package/dist/settings-store.js.map +1 -0
  51. package/dist/task-store.d.ts +2 -2
  52. package/dist/task-store.d.ts.map +1 -1
  53. package/dist/task-store.js +10 -5
  54. package/dist/task-store.js.map +1 -1
  55. package/dist/ws-bridge.d.ts.map +1 -1
  56. package/dist/ws-bridge.js +163 -164
  57. package/dist/ws-bridge.js.map +1 -1
  58. package/dist/ws-broadcast.d.ts +5 -0
  59. package/dist/ws-broadcast.d.ts.map +1 -1
  60. package/dist/ws-broadcast.js +24 -1
  61. package/dist/ws-broadcast.js.map +1 -1
  62. package/package.json +7 -6
package/dist/ws-bridge.js CHANGED
@@ -14,7 +14,10 @@ import * as findingStore from "./finding-store.js";
14
14
  import * as personaStore from "./persona-store.js";
15
15
  import { v4 as uuid } from "uuid";
16
16
  import { join } from "node:path";
17
- import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, SESSION_STATUS, TASK_STATUS, eventTypeToString, } from "@grackle-ai/common";
17
+ import { LOGS_DIR, SESSION_STATUS, TASK_STATUS, eventTypeToString, } from "@grackle-ai/common";
18
+ import { resolvePersona } from "./resolve-persona.js";
19
+ import * as settingsStore from "./settings-store.js";
20
+ import { isAllowedSettingKey } from "./settings-store.js";
18
21
  import { grackleHome } from "./paths.js";
19
22
  import * as logWriter from "./log-writer.js";
20
23
  import { safeParseJsonArray } from "./json-helpers.js";
@@ -23,8 +26,11 @@ import { buildTaskSystemContext } from "./utils/system-context.js";
23
26
  import { slugify } from "./utils/slugify.js";
24
27
  import { processEventStream } from "./event-processor.js";
25
28
  import * as processorRegistry from "./processor-registry.js";
26
- import { broadcast, setWssInstance, broadcastEnvironments, envRowToWs } from "./ws-broadcast.js";
29
+ import { setWssInstance, envRowToWs } from "./ws-broadcast.js";
30
+ import { emit } from "./event-bus.js";
27
31
  import { buildMcpServersJson } from "./grpc-service.js";
32
+ import { reanimateAgent } from "./reanimate-agent.js";
33
+ import { ConnectError } from "@connectrpc/connect";
28
34
  import { computeTaskStatus } from "./compute-task-status.js";
29
35
  import { exec } from "./utils/exec.js";
30
36
  import { formatGhError } from "./utils/format-gh-error.js";
@@ -108,20 +114,17 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
108
114
  }
109
115
  logger.info({ environmentId, ...logContext }, "Auto-provisioning environment");
110
116
  envRegistry.updateEnvironmentStatus(environmentId, "connecting");
111
- broadcastEnvironments();
117
+ emit("environment.changed", {});
112
118
  try {
113
119
  const config = safeParseAdapterConfig(env.adapterConfig, environmentId);
114
120
  const powerlineToken = env.powerlineToken || "";
115
121
  for await (const provEvent of reconnectOrProvision(environmentId, adapter, config, powerlineToken, !!env.bootstrapped)) {
116
122
  logger.info({ environmentId, stage: provEvent.stage, ...logContext }, "Auto-provision progress");
117
- broadcast({
118
- type: "provision_progress",
119
- payload: {
120
- environmentId,
121
- stage: provEvent.stage,
122
- message: provEvent.message,
123
- progress: provEvent.progress,
124
- },
123
+ emit("environment.provision_progress", {
124
+ environmentId,
125
+ stage: provEvent.stage,
126
+ message: provEvent.message,
127
+ progress: provEvent.progress,
125
128
  });
126
129
  }
127
130
  conn = await adapter.connect(environmentId, config, powerlineToken);
@@ -130,32 +133,26 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
130
133
  await tokenBroker.pushToEnv(environmentId);
131
134
  envRegistry.updateEnvironmentStatus(environmentId, "connected");
132
135
  envRegistry.markBootstrapped(environmentId);
133
- broadcastEnvironments();
136
+ emit("environment.changed", {});
134
137
  logger.info({ environmentId, ...logContext }, "Auto-provision complete");
135
- broadcast({
136
- type: "provision_progress",
137
- payload: {
138
- environmentId,
139
- stage: "ready",
140
- message: "Environment connected",
141
- progress: 1,
142
- },
138
+ emit("environment.provision_progress", {
139
+ environmentId,
140
+ stage: "ready",
141
+ message: "Environment connected",
142
+ progress: 1,
143
143
  });
144
144
  return conn;
145
145
  }
146
146
  catch (err) {
147
147
  logger.error({ environmentId, ...logContext, err }, "Auto-provision failed");
148
148
  envRegistry.updateEnvironmentStatus(environmentId, "error");
149
- broadcastEnvironments();
149
+ emit("environment.changed", {});
150
150
  const errorMessage = err instanceof Error ? err.message : String(err);
151
- broadcast({
152
- type: "provision_progress",
153
- payload: {
154
- environmentId,
155
- stage: "error",
156
- message: `Auto-provision failed: ${errorMessage}`,
157
- progress: 0,
158
- },
151
+ emit("environment.provision_progress", {
152
+ environmentId,
153
+ stage: "error",
154
+ message: `Auto-provision failed: ${errorMessage}`,
155
+ progress: 0,
159
156
  });
160
157
  sendWs(ws, {
161
158
  type: "error",
@@ -192,51 +189,43 @@ async function startTaskSession(ws, task, options) {
192
189
  if (!conn) {
193
190
  return undefined;
194
191
  }
195
- // Resolve persona
196
- const resolvedPersonaId = options?.personaId || "";
197
- const persona = resolvedPersonaId
198
- ? personaStore.getPersona(resolvedPersonaId)
199
- : undefined;
200
- if (resolvedPersonaId && !persona) {
201
- return `Persona not found: ${resolvedPersonaId}`;
192
+ // Resolve persona via cascade (request → task → project → app default)
193
+ let resolved;
194
+ try {
195
+ resolved = resolvePersona(options?.personaId || "", task.defaultPersonaId, project.defaultPersonaId);
196
+ }
197
+ catch (err) {
198
+ return err.message;
202
199
  }
203
200
  const sessionId = uuid();
204
- const runtime = options?.runtime || persona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
205
- const model = options?.model || persona?.model ||
206
- process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
207
- const maxTurns = persona?.maxTurns || 0;
201
+ const { runtime, model, maxTurns, systemPrompt, persona: resolvedPersonaRow } = resolved;
208
202
  const logPath = join(grackleHome, LOGS_DIR, sessionId);
209
203
  const freshTask = taskStore.getTask(task.id) || task;
210
204
  let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description, options?.notes || "", freshTask.canDecompose);
211
- if (persona) {
212
- systemContext = persona.systemPrompt + "\n\n" + systemContext;
205
+ if (systemPrompt) {
206
+ systemContext = systemPrompt + "\n\n" + systemContext;
213
207
  }
214
- sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath, freshTask.id, resolvedPersonaId);
215
- broadcast({
216
- type: "task_started",
217
- payload: {
218
- taskId: freshTask.id,
219
- sessionId,
220
- projectId: freshTask.projectId,
221
- },
208
+ sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath, freshTask.id, resolved.personaId);
209
+ emit("task.started", {
210
+ taskId: freshTask.id,
211
+ sessionId,
212
+ projectId: freshTask.projectId,
222
213
  });
223
214
  // Re-push stored tokens + provider credentials (scoped to runtime) so they're fresh for this session.
224
215
  // For local envs, skip file tokens — the PowerLine is on the same machine.
225
216
  await tokenBroker.refreshTokensForTask(environmentId, runtime, env.adapterType === "local" ? { excludeFileTokens: true } : undefined);
226
217
  let mcpServersJson = "";
227
- if (persona) {
228
- try {
229
- const parsed = JSON.parse(persona.mcpServers || "[]");
230
- if (Array.isArray(parsed)) {
231
- const mcpServers = parsed;
232
- if (mcpServers.length > 0) {
233
- mcpServersJson = buildMcpServersJson(mcpServers);
234
- }
218
+ try {
219
+ const parsed = JSON.parse(resolvedPersonaRow.mcpServers || "[]");
220
+ if (Array.isArray(parsed)) {
221
+ const mcpServers = parsed;
222
+ if (mcpServers.length > 0) {
223
+ mcpServersJson = buildMcpServersJson(mcpServers);
235
224
  }
236
225
  }
237
- catch {
238
- logger.warn("Failed to parse persona.mcpServers JSON; ignoring");
239
- }
226
+ }
227
+ catch {
228
+ logger.warn("Failed to parse persona.mcpServers JSON; ignoring");
240
229
  }
241
230
  const powerlineReq = create(powerline.SpawnRequestSchema, {
242
231
  sessionId,
@@ -371,8 +360,6 @@ async function handleMessage(ws, msg, subscriptions) {
371
360
  case "spawn": {
372
361
  const environmentId = msg.payload?.environmentId;
373
362
  const prompt = msg.payload?.prompt;
374
- const model = msg.payload?.model || undefined;
375
- const runtime = msg.payload?.runtime || undefined;
376
363
  const branch = msg.payload?.branch || "";
377
364
  const systemContext = msg.payload?.systemContext || "";
378
365
  const spawnPersonaId = msg.payload?.personaId || "";
@@ -383,10 +370,13 @@ async function handleMessage(ws, msg, subscriptions) {
383
370
  });
384
371
  return;
385
372
  }
386
- // Resolve persona if specified
387
- const spawnPersona = spawnPersonaId ? personaStore.getPersona(spawnPersonaId) : undefined;
388
- if (spawnPersonaId && !spawnPersona) {
389
- sendWs(ws, { type: "error", payload: { message: `Persona not found: ${spawnPersonaId}` } });
373
+ // Resolve persona via cascade (request → app default)
374
+ let resolved;
375
+ try {
376
+ resolved = resolvePersona(spawnPersonaId);
377
+ }
378
+ catch (err) {
379
+ sendWs(ws, { type: "error", payload: { message: err.message } });
390
380
  return;
391
381
  }
392
382
  const env = envRegistry.getEnvironment(environmentId);
@@ -403,13 +393,11 @@ async function handleMessage(ws, msg, subscriptions) {
403
393
  return;
404
394
  }
405
395
  const sessionId = uuid();
406
- const sessionRuntime = runtime || spawnPersona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
407
- const sessionModel = model || spawnPersona?.model || process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
408
- const maxTurns = spawnPersona?.maxTurns || 0;
396
+ const { runtime: sessionRuntime, model: sessionModel, maxTurns, systemPrompt: spawnSystemPrompt } = resolved;
409
397
  const logPath = join(grackleHome, LOGS_DIR, sessionId);
410
398
  let finalSystemContext = systemContext;
411
- if (spawnPersona) {
412
- finalSystemContext = spawnPersona.systemPrompt + (systemContext ? "\n\n" + systemContext : "");
399
+ if (spawnSystemPrompt) {
400
+ finalSystemContext = spawnSystemPrompt + (systemContext ? "\n\n" + systemContext : "");
413
401
  }
414
402
  sessionStore.createSession(sessionId, environmentId, sessionRuntime, prompt, sessionModel, logPath);
415
403
  sendWs(ws, { type: "spawned", payload: { sessionId } });
@@ -532,11 +520,27 @@ async function handleMessage(ws, msg, subscriptions) {
532
520
  if (session.taskId) {
533
521
  const task = taskStore.getTask(session.taskId);
534
522
  if (task) {
535
- broadcast({ type: "task_updated", payload: { taskId: task.id, projectId: task.projectId } });
523
+ emit("task.updated", { taskId: task.id, projectId: task.projectId });
536
524
  }
537
525
  }
538
526
  break;
539
527
  }
528
+ case "resume_agent": {
529
+ const resumeSessionId = msg.payload?.sessionId;
530
+ if (!resumeSessionId) {
531
+ sendWs(ws, { type: "error", payload: { message: "sessionId required" } });
532
+ return;
533
+ }
534
+ try {
535
+ reanimateAgent(resumeSessionId);
536
+ sendWs(ws, { type: "agent_resumed", payload: { sessionId: resumeSessionId } });
537
+ }
538
+ catch (err) {
539
+ const message = err instanceof ConnectError ? err.message : String(err);
540
+ sendWs(ws, { type: "error", payload: { message } });
541
+ }
542
+ break;
543
+ }
540
544
  // ─── Projects ──────────────────────────────────────────
541
545
  case "list_projects": {
542
546
  const rows = projectStore.listProjects();
@@ -549,6 +553,7 @@ async function handleMessage(ws, msg, subscriptions) {
549
553
  description: r.description,
550
554
  repoUrl: r.repoUrl,
551
555
  defaultEnvironmentId: r.defaultEnvironmentId,
556
+ defaultPersonaId: r.defaultPersonaId,
552
557
  status: r.status,
553
558
  useWorktrees: r.useWorktrees,
554
559
  worktreeBasePath: r.worktreeBasePath,
@@ -575,16 +580,15 @@ async function handleMessage(ws, msg, subscriptions) {
575
580
  }
576
581
  // useWorktrees defaults to true when not specified
577
582
  const createUseWorktrees = msg.payload?.useWorktrees ?? true;
578
- projectStore.createProject(id, name, msg.payload?.description || "", msg.payload?.repoUrl || "", msg.payload?.defaultEnvironmentId || "", createUseWorktrees, typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "");
579
- const row = projectStore.getProject(id);
580
- broadcast({ type: "project_created", payload: { project: row } });
583
+ projectStore.createProject(id, name, msg.payload?.description || "", msg.payload?.repoUrl || "", msg.payload?.defaultEnvironmentId || "", createUseWorktrees, typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "", msg.payload?.defaultPersonaId || "");
584
+ emit("project.created", { projectId: id });
581
585
  break;
582
586
  }
583
587
  case "archive_project": {
584
588
  const projectId = msg.payload?.projectId;
585
589
  if (projectId)
586
590
  projectStore.archiveProject(projectId);
587
- broadcast({ type: "project_archived", payload: { projectId } });
591
+ emit("project.archived", { projectId });
588
592
  break;
589
593
  }
590
594
  case "update_project": {
@@ -612,6 +616,7 @@ async function handleMessage(ws, msg, subscriptions) {
612
616
  }
613
617
  const worktreesVal = typeof msg.payload?.useWorktrees === "boolean" ? msg.payload.useWorktrees : undefined;
614
618
  const worktreeBasePathVal = typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath : undefined;
619
+ const defaultPersonaIdVal = typeof msg.payload?.defaultPersonaId === "string" ? msg.payload.defaultPersonaId : undefined;
615
620
  projectStore.updateProject(projectId, {
616
621
  name: nameVal !== undefined ? nameVal.trim() : undefined,
617
622
  description: descVal,
@@ -619,8 +624,9 @@ async function handleMessage(ws, msg, subscriptions) {
619
624
  defaultEnvironmentId: envVal,
620
625
  useWorktrees: worktreesVal,
621
626
  worktreeBasePath: worktreeBasePathVal,
627
+ defaultPersonaId: defaultPersonaIdVal,
622
628
  });
623
- broadcast({ type: "project_updated", payload: { projectId } });
629
+ emit("project.updated", { projectId });
624
630
  break;
625
631
  }
626
632
  // ─── Personas ──────────────────────────────────────────
@@ -666,7 +672,7 @@ async function handleMessage(ws, msg, subscriptions) {
666
672
  personaId = `${slugify(personaName) || "persona"}-${uuid().slice(0, 4)}`;
667
673
  }
668
674
  personaStore.createPersona(personaId, personaName, msg.payload?.description || "", personaSystemPrompt, msg.payload?.toolConfig || "{}", msg.payload?.runtime || "", msg.payload?.model || "", msg.payload?.maxTurns || 0, msg.payload?.mcpServers || "[]");
669
- broadcast({ type: "persona_created", payload: { personaId } });
675
+ emit("persona.created", { personaId });
670
676
  break;
671
677
  }
672
678
  case "get_persona": {
@@ -702,10 +708,7 @@ async function handleMessage(ws, msg, subscriptions) {
702
708
  return;
703
709
  }
704
710
  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);
705
- broadcast({
706
- type: "persona_updated",
707
- payload: { personaId: updatePersonaId },
708
- });
711
+ emit("persona.updated", { personaId: updatePersonaId });
709
712
  break;
710
713
  }
711
714
  case "delete_persona": {
@@ -713,10 +716,45 @@ async function handleMessage(ws, msg, subscriptions) {
713
716
  if (!deletePersonaId)
714
717
  return;
715
718
  personaStore.deletePersona(deletePersonaId);
716
- broadcast({
717
- type: "persona_deleted",
718
- payload: { personaId: deletePersonaId },
719
- });
719
+ emit("persona.deleted", { personaId: deletePersonaId });
720
+ break;
721
+ }
722
+ // ─── Settings ────────────────────────────────────────────
723
+ case "get_setting": {
724
+ const key = msg.payload?.key;
725
+ if (typeof key !== "string" || !key)
726
+ return;
727
+ if (!isAllowedSettingKey(key)) {
728
+ sendWs(ws, { type: "error", payload: { message: `Setting key not allowed: ${key}` } });
729
+ return;
730
+ }
731
+ const value = settingsStore.getSetting(key) ?? "";
732
+ sendWs(ws, { type: "setting", payload: { key, value } });
733
+ break;
734
+ }
735
+ case "set_setting": {
736
+ const key = msg.payload?.key;
737
+ const value = msg.payload?.value || "";
738
+ if (typeof key !== "string" || !key)
739
+ return;
740
+ if (!isAllowedSettingKey(key)) {
741
+ sendWs(ws, { type: "error", payload: { message: `Setting key not allowed: ${key}` } });
742
+ return;
743
+ }
744
+ // Validate persona exists and has required fields when setting default_persona_id
745
+ if (key === "default_persona_id" && value) {
746
+ const persona = personaStore.getPersona(value);
747
+ if (!persona) {
748
+ sendWs(ws, { type: "error", payload: { message: `Persona not found: ${value}` } });
749
+ return;
750
+ }
751
+ if (!persona.runtime || !persona.model) {
752
+ sendWs(ws, { type: "error", payload: { message: `Persona "${persona.name}" must have runtime and model configured` } });
753
+ return;
754
+ }
755
+ }
756
+ settingsStore.setSetting(key, value);
757
+ emit("setting.changed", { key, value });
720
758
  break;
721
759
  }
722
760
  // ─── Tasks ─────────────────────────────────────────────
@@ -760,6 +798,7 @@ async function handleMessage(ws, msg, subscriptions) {
760
798
  depth: r.depth,
761
799
  childTaskIds: childIdsMap.get(r.id) ?? [],
762
800
  canDecompose: r.canDecompose,
801
+ defaultPersonaId: r.defaultPersonaId,
763
802
  };
764
803
  }),
765
804
  },
@@ -794,17 +833,8 @@ async function handleMessage(ws, msg, subscriptions) {
794
833
  const canDecompose = typeof rawCanDecompose === "boolean" ? rawCanDecompose : false;
795
834
  try {
796
835
  const id = uuid().slice(0, 8);
797
- taskStore.createTask(id, projectId, title, msg.payload?.description || "", msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose);
798
- const row = taskStore.getTask(id);
799
- broadcast({
800
- type: "task_created",
801
- payload: {
802
- task: row
803
- ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) }
804
- : null,
805
- requestId,
806
- },
807
- });
836
+ taskStore.createTask(id, projectId, title, msg.payload?.description || "", msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose, msg.payload?.defaultPersonaId || "");
837
+ emit("task.created", { taskId: id, projectId, requestId });
808
838
  }
809
839
  catch (error) {
810
840
  const message = error instanceof Error ? error.message : "Failed to create task";
@@ -858,10 +888,7 @@ async function handleMessage(ws, msg, subscriptions) {
858
888
  sendWs(ws, { type: "error", payload: { message: String(err) } });
859
889
  return;
860
890
  }
861
- broadcast({
862
- type: "task_started",
863
- payload: { taskId: updateTaskId, sessionId: lateBindSessionId, projectId: existingTask.projectId },
864
- });
891
+ emit("task.started", { taskId: updateTaskId, sessionId: lateBindSessionId, projectId: existingTask.projectId });
865
892
  break;
866
893
  }
867
894
  // Only allow editing not_started tasks (non-late-bind path)
@@ -886,18 +913,11 @@ async function handleMessage(ws, msg, subscriptions) {
886
913
  .filter((d) => d !== updateTaskId)),
887
914
  ]
888
915
  : safeParseJsonArray(existingTask.dependsOn);
889
- taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn);
890
- const updatedRow = taskStore.getTask(updateTaskId);
891
- broadcast({
892
- type: "task_updated",
893
- payload: {
894
- taskId: updateTaskId,
895
- projectId: existingTask.projectId,
896
- task: updatedRow
897
- ? { ...updatedRow, dependsOn: safeParseJsonArray(updatedRow.dependsOn) }
898
- : null,
899
- },
900
- });
916
+ const updatedDefaultPersonaId = typeof msg.payload?.defaultPersonaId === "string"
917
+ ? msg.payload.defaultPersonaId
918
+ : undefined;
919
+ taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn, updatedDefaultPersonaId);
920
+ emit("task.updated", { taskId: updateTaskId, projectId: existingTask.projectId });
901
921
  break;
902
922
  }
903
923
  case "start_task": {
@@ -933,8 +953,6 @@ async function handleMessage(ws, msg, subscriptions) {
933
953
  return;
934
954
  }
935
955
  const startError = await startTaskSession(ws, task, {
936
- runtime: msg.payload?.runtime,
937
- model: msg.payload?.model,
938
956
  personaId: msg.payload?.personaId || undefined,
939
957
  environmentId: msg.payload?.environmentId || undefined,
940
958
  notes: msg.payload?.notes || undefined,
@@ -959,10 +977,7 @@ async function handleMessage(ws, msg, subscriptions) {
959
977
  },
960
978
  });
961
979
  if (task) {
962
- broadcast({
963
- type: "task_completed",
964
- payload: { taskId, projectId: task.projectId },
965
- });
980
+ emit("task.completed", { taskId, projectId: task.projectId });
966
981
  }
967
982
  break;
968
983
  }
@@ -1015,10 +1030,7 @@ async function handleMessage(ws, msg, subscriptions) {
1015
1030
  projectId: task.projectId,
1016
1031
  taskId: task.id,
1017
1032
  });
1018
- broadcast({
1019
- type: "task_started",
1020
- payload: { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId },
1021
- });
1033
+ emit("task.started", { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId });
1022
1034
  break;
1023
1035
  }
1024
1036
  case "delete_task": {
@@ -1067,7 +1079,7 @@ async function handleMessage(ws, msg, subscriptions) {
1067
1079
  sendWs(ws, { type: "error", payload: { message: `Failed to delete task ${taskId}: no rows affected` } });
1068
1080
  return;
1069
1081
  }
1070
- broadcast({ type: "task_deleted", payload: { taskId, projectId: deletedTask.projectId } });
1082
+ emit("task.deleted", { taskId, projectId: deletedTask.projectId });
1071
1083
  break;
1072
1084
  }
1073
1085
  // ─── Task Sessions ─────────────────────────────────────
@@ -1218,7 +1230,7 @@ async function handleMessage(ws, msg, subscriptions) {
1218
1230
  }
1219
1231
  logger.info({ environmentId, adapterType: env.adapterType }, "Provisioning environment");
1220
1232
  envRegistry.updateEnvironmentStatus(environmentId, "connecting");
1221
- broadcastEnvironments();
1233
+ emit("environment.changed", {});
1222
1234
  // Run provision in background, broadcasting progress to all connected clients
1223
1235
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1224
1236
  (async () => {
@@ -1227,14 +1239,11 @@ async function handleMessage(ws, msg, subscriptions) {
1227
1239
  const powerlineToken = env.powerlineToken || "";
1228
1240
  for await (const event of reconnectOrProvision(environmentId, adapter, config, powerlineToken, !!env.bootstrapped)) {
1229
1241
  logger.info({ environmentId, stage: event.stage, message: event.message }, "Provision progress");
1230
- broadcast({
1231
- type: "provision_progress",
1232
- payload: {
1233
- environmentId,
1234
- stage: event.stage,
1235
- message: event.message,
1236
- progress: event.progress,
1237
- },
1242
+ emit("environment.provision_progress", {
1243
+ environmentId,
1244
+ stage: event.stage,
1245
+ message: event.message,
1246
+ progress: event.progress,
1238
1247
  });
1239
1248
  }
1240
1249
  logger.info({ environmentId }, "Provision complete, calling adapter.connect");
@@ -1245,31 +1254,25 @@ async function handleMessage(ws, msg, subscriptions) {
1245
1254
  envRegistry.updateEnvironmentStatus(environmentId, "connected");
1246
1255
  envRegistry.markBootstrapped(environmentId);
1247
1256
  logger.info({ environmentId }, "Environment connected");
1248
- broadcast({
1249
- type: "provision_progress",
1250
- payload: {
1251
- environmentId,
1252
- stage: "ready",
1253
- message: "Environment connected",
1254
- progress: 1,
1255
- },
1257
+ emit("environment.provision_progress", {
1258
+ environmentId,
1259
+ stage: "ready",
1260
+ message: "Environment connected",
1261
+ progress: 1,
1256
1262
  });
1257
1263
  }
1258
1264
  catch (err) {
1259
1265
  logger.error({ environmentId, err }, "Provision failed");
1260
1266
  envRegistry.updateEnvironmentStatus(environmentId, "error");
1261
1267
  const errorMessage = err instanceof Error ? err.message : String(err);
1262
- broadcast({
1263
- type: "provision_progress",
1264
- payload: {
1265
- environmentId,
1266
- stage: "error",
1267
- message: `Connection failed: ${errorMessage}`,
1268
- progress: 0,
1269
- },
1268
+ emit("environment.provision_progress", {
1269
+ environmentId,
1270
+ stage: "error",
1271
+ message: `Connection failed: ${errorMessage}`,
1272
+ progress: 0,
1270
1273
  });
1271
1274
  }
1272
- broadcastEnvironments();
1275
+ emit("environment.changed", {});
1273
1276
  })();
1274
1277
  break;
1275
1278
  }
@@ -1298,7 +1301,7 @@ async function handleMessage(ws, msg, subscriptions) {
1298
1301
  adapterManager.removeConnection(environmentId);
1299
1302
  envRegistry.updateEnvironmentStatus(environmentId, "disconnected");
1300
1303
  logger.info({ environmentId }, "Environment stopped");
1301
- broadcastEnvironments();
1304
+ emit("environment.changed", {});
1302
1305
  break;
1303
1306
  }
1304
1307
  case "add_environment": {
@@ -1357,11 +1360,10 @@ async function handleMessage(ws, msg, subscriptions) {
1357
1360
  });
1358
1361
  return;
1359
1362
  }
1360
- const defaultRuntime = msg.payload?.defaultRuntime || DEFAULT_RUNTIME;
1361
- envRegistry.addEnvironment(id, displayName, adapterType, adapterConfig, defaultRuntime);
1363
+ envRegistry.addEnvironment(id, displayName, adapterType, adapterConfig);
1362
1364
  logger.info({ id, displayName, adapterType }, "Environment added via WebSocket");
1363
- broadcast({ type: "environment_added", payload: { environmentId: id } });
1364
- broadcastEnvironments();
1365
+ emit("environment.added", { environmentId: id });
1366
+ emit("environment.changed", {});
1365
1367
  break;
1366
1368
  }
1367
1369
  case "remove_environment": {
@@ -1396,8 +1398,8 @@ async function handleMessage(ws, msg, subscriptions) {
1396
1398
  sessionStore.deleteByEnvironment(environmentId);
1397
1399
  envRegistry.removeEnvironment(environmentId);
1398
1400
  logger.info({ environmentId }, "Environment removed");
1399
- broadcast({ type: "environment_removed", payload: { environmentId } });
1400
- broadcastEnvironments();
1401
+ emit("environment.removed", { environmentId });
1402
+ emit("environment.changed", {});
1401
1403
  break;
1402
1404
  }
1403
1405
  // ─── Codespaces ─────────────────────────────────────
@@ -1497,7 +1499,7 @@ async function handleMessage(ws, msg, subscriptions) {
1497
1499
  value,
1498
1500
  expiresAt: msg.payload?.expiresAt || "",
1499
1501
  });
1500
- broadcast({ type: "token_changed" });
1502
+ emit("token.changed", {});
1501
1503
  break;
1502
1504
  }
1503
1505
  case "delete_token": {
@@ -1507,7 +1509,7 @@ async function handleMessage(ws, msg, subscriptions) {
1507
1509
  return;
1508
1510
  }
1509
1511
  await tokenBroker.deleteToken(tokenName);
1510
- broadcast({ type: "token_changed" });
1512
+ emit("token.changed", {});
1511
1513
  break;
1512
1514
  }
1513
1515
  case "get_credential_providers": {
@@ -1524,10 +1526,7 @@ async function handleMessage(ws, msg, subscriptions) {
1524
1526
  return;
1525
1527
  }
1526
1528
  credentialProviders.setCredentialProviders(msg.payload);
1527
- broadcast({
1528
- type: "credential_providers",
1529
- payload: credentialProviders.getCredentialProviders(),
1530
- });
1529
+ emit("credential.providers_changed", credentialProviders.getCredentialProviders());
1531
1530
  break;
1532
1531
  }
1533
1532
  }