@grackle-ai/server 0.15.0 → 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/grpc-service.d.ts +1 -1
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +46 -4
- package/dist/grpc-service.js.map +1 -1
- package/dist/task-store.d.ts +2 -2
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +3 -2
- package/dist/task-store.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +410 -65
- package/dist/ws-bridge.js.map +1 -1
- package/package.json +3 -3
package/dist/ws-bridge.js
CHANGED
|
@@ -4,7 +4,7 @@ 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";
|
|
@@ -110,7 +110,10 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
|
|
|
110
110
|
}
|
|
111
111
|
const adapter = adapterManager.getAdapter(env.adapterType);
|
|
112
112
|
if (!adapter) {
|
|
113
|
-
sendWs(ws, {
|
|
113
|
+
sendWs(ws, {
|
|
114
|
+
type: "error",
|
|
115
|
+
payload: { message: `No adapter for type: ${env.adapterType}` },
|
|
116
|
+
});
|
|
114
117
|
return undefined;
|
|
115
118
|
}
|
|
116
119
|
logger.info({ environmentId, ...logContext }, "Auto-provisioning environment");
|
|
@@ -123,7 +126,12 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
|
|
|
123
126
|
logger.info({ environmentId, stage: provEvent.stage, ...logContext }, "Auto-provision progress");
|
|
124
127
|
broadcast({
|
|
125
128
|
type: "provision_progress",
|
|
126
|
-
payload: {
|
|
129
|
+
payload: {
|
|
130
|
+
environmentId,
|
|
131
|
+
stage: provEvent.stage,
|
|
132
|
+
message: provEvent.message,
|
|
133
|
+
progress: provEvent.progress,
|
|
134
|
+
},
|
|
127
135
|
});
|
|
128
136
|
}
|
|
129
137
|
conn = await adapter.connect(environmentId, config, powerlineToken);
|
|
@@ -136,7 +144,12 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
|
|
|
136
144
|
logger.info({ environmentId, ...logContext }, "Auto-provision complete");
|
|
137
145
|
broadcast({
|
|
138
146
|
type: "provision_progress",
|
|
139
|
-
payload: {
|
|
147
|
+
payload: {
|
|
148
|
+
environmentId,
|
|
149
|
+
stage: "ready",
|
|
150
|
+
message: "Environment connected",
|
|
151
|
+
progress: 1,
|
|
152
|
+
},
|
|
140
153
|
});
|
|
141
154
|
return conn;
|
|
142
155
|
}
|
|
@@ -147,9 +160,19 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
|
|
|
147
160
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
148
161
|
broadcast({
|
|
149
162
|
type: "provision_progress",
|
|
150
|
-
payload: {
|
|
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
|
+
},
|
|
151
175
|
});
|
|
152
|
-
sendWs(ws, { type: "error", payload: { message: `Failed to auto-connect environment ${environmentId}: ${errorMessage}` } });
|
|
153
176
|
return undefined;
|
|
154
177
|
}
|
|
155
178
|
}
|
|
@@ -173,19 +196,29 @@ async function startTaskSession(ws, task, options) {
|
|
|
173
196
|
logger.warn({ taskId: task.id, environmentId }, "startTaskSession failed: environment not found");
|
|
174
197
|
return `Environment not found: ${environmentId}`;
|
|
175
198
|
}
|
|
176
|
-
const conn = await autoProvisionEnvironment(ws, environmentId, env, {
|
|
199
|
+
const conn = await autoProvisionEnvironment(ws, environmentId, env, {
|
|
200
|
+
taskId: task.id,
|
|
201
|
+
});
|
|
177
202
|
if (!conn) {
|
|
178
203
|
return undefined;
|
|
179
204
|
}
|
|
180
205
|
// Resolve persona
|
|
181
206
|
const resolvedPersonaId = options?.personaId || task.personaId;
|
|
182
|
-
const persona = resolvedPersonaId
|
|
207
|
+
const persona = resolvedPersonaId
|
|
208
|
+
? personaStore.getPersona(resolvedPersonaId)
|
|
209
|
+
: undefined;
|
|
183
210
|
if (resolvedPersonaId && !persona) {
|
|
184
211
|
return `Persona not found: ${resolvedPersonaId}`;
|
|
185
212
|
}
|
|
186
213
|
const sessionId = uuid();
|
|
187
|
-
const runtime = options?.runtime ||
|
|
188
|
-
|
|
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;
|
|
189
222
|
const maxTurns = persona?.maxTurns || 0;
|
|
190
223
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
191
224
|
const freshTask = taskStore.getTask(task.id) || task;
|
|
@@ -196,7 +229,14 @@ async function startTaskSession(ws, task, options) {
|
|
|
196
229
|
sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath);
|
|
197
230
|
taskStore.setTaskSession(freshTask.id, sessionId);
|
|
198
231
|
taskStore.markTaskStarted(freshTask.id);
|
|
199
|
-
broadcast({
|
|
232
|
+
broadcast({
|
|
233
|
+
type: "task_started",
|
|
234
|
+
payload: {
|
|
235
|
+
taskId: freshTask.id,
|
|
236
|
+
sessionId,
|
|
237
|
+
projectId: freshTask.projectId,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
200
240
|
let mcpServersJson = "";
|
|
201
241
|
if (persona) {
|
|
202
242
|
try {
|
|
@@ -219,7 +259,9 @@ async function startTaskSession(ws, task, options) {
|
|
|
219
259
|
model,
|
|
220
260
|
maxTurns,
|
|
221
261
|
branch: freshTask.branch,
|
|
222
|
-
worktreeBasePath: freshTask.branch
|
|
262
|
+
worktreeBasePath: freshTask.branch
|
|
263
|
+
? process.env.GRACKLE_WORKTREE_BASE || "/workspace"
|
|
264
|
+
: "",
|
|
223
265
|
systemContext,
|
|
224
266
|
projectId: freshTask.projectId,
|
|
225
267
|
taskId: freshTask.id,
|
|
@@ -240,7 +282,10 @@ async function startTaskSession(ws, task, options) {
|
|
|
240
282
|
else if (sess?.status === "failed") {
|
|
241
283
|
taskStore.markTaskCompleted(freshTask.id, "failed");
|
|
242
284
|
}
|
|
243
|
-
broadcast({
|
|
285
|
+
broadcast({
|
|
286
|
+
type: "task_updated",
|
|
287
|
+
payload: { taskId: freshTask.id, projectId: freshTask.projectId },
|
|
288
|
+
});
|
|
244
289
|
}
|
|
245
290
|
},
|
|
246
291
|
});
|
|
@@ -357,12 +402,18 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
357
402
|
const branch = msg.payload?.branch || "";
|
|
358
403
|
const systemContext = msg.payload?.systemContext || "";
|
|
359
404
|
if (!environmentId || !prompt) {
|
|
360
|
-
sendWs(ws, {
|
|
405
|
+
sendWs(ws, {
|
|
406
|
+
type: "error",
|
|
407
|
+
payload: { message: "environmentId and prompt required" },
|
|
408
|
+
});
|
|
361
409
|
return;
|
|
362
410
|
}
|
|
363
411
|
const env = envRegistry.getEnvironment(environmentId);
|
|
364
412
|
if (!env) {
|
|
365
|
-
sendWs(ws, {
|
|
413
|
+
sendWs(ws, {
|
|
414
|
+
type: "error",
|
|
415
|
+
payload: { message: `Environment not found: ${environmentId}` },
|
|
416
|
+
});
|
|
366
417
|
return;
|
|
367
418
|
}
|
|
368
419
|
// Auto-provision the environment if not already connected
|
|
@@ -407,29 +458,59 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
407
458
|
const sessionId = msg.payload?.sessionId;
|
|
408
459
|
const text = msg.payload?.text;
|
|
409
460
|
if (!sessionId || !text) {
|
|
410
|
-
sendWs(ws, {
|
|
461
|
+
sendWs(ws, {
|
|
462
|
+
type: "error",
|
|
463
|
+
payload: { message: "sessionId and text required" },
|
|
464
|
+
});
|
|
411
465
|
return;
|
|
412
466
|
}
|
|
413
467
|
const session = sessionStore.getSession(sessionId);
|
|
414
468
|
if (!session) {
|
|
415
|
-
sendWs(ws, {
|
|
469
|
+
sendWs(ws, {
|
|
470
|
+
type: "error",
|
|
471
|
+
payload: { message: `Session not found: ${sessionId}` },
|
|
472
|
+
});
|
|
416
473
|
return;
|
|
417
474
|
}
|
|
418
475
|
if (session.status !== "waiting_input") {
|
|
419
|
-
sendWs(ws, {
|
|
476
|
+
sendWs(ws, {
|
|
477
|
+
type: "error",
|
|
478
|
+
payload: {
|
|
479
|
+
message: `Session ${sessionId} is not currently waiting for input (status: ${session.status})`,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
420
482
|
return;
|
|
421
483
|
}
|
|
422
484
|
const conn = adapterManager.getConnection(session.environmentId);
|
|
423
485
|
if (!conn) {
|
|
424
|
-
sendWs(ws, {
|
|
486
|
+
sendWs(ws, {
|
|
487
|
+
type: "error",
|
|
488
|
+
payload: {
|
|
489
|
+
message: `Environment ${session.environmentId} is not connected`,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
425
492
|
return;
|
|
426
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);
|
|
427
505
|
try {
|
|
428
506
|
await conn.client.sendInput(create(powerline.InputMessageSchema, { sessionId, text }));
|
|
429
507
|
}
|
|
430
508
|
catch (err) {
|
|
431
509
|
const errMessage = err instanceof Error ? err.message : String(err);
|
|
432
|
-
sendWs(ws, {
|
|
510
|
+
sendWs(ws, {
|
|
511
|
+
type: "error",
|
|
512
|
+
payload: { message: `Failed to send input: ${errMessage}` },
|
|
513
|
+
});
|
|
433
514
|
}
|
|
434
515
|
break;
|
|
435
516
|
}
|
|
@@ -448,7 +529,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
448
529
|
await conn.client.kill(create(powerline.SessionIdSchema, { id: sessionId }));
|
|
449
530
|
}
|
|
450
531
|
catch (err) {
|
|
451
|
-
sendWs(ws, {
|
|
532
|
+
sendWs(ws, {
|
|
533
|
+
type: "error",
|
|
534
|
+
payload: { message: `Kill failed: ${err}` },
|
|
535
|
+
});
|
|
452
536
|
return;
|
|
453
537
|
}
|
|
454
538
|
}
|
|
@@ -507,6 +591,102 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
507
591
|
broadcast({ type: "project_archived", payload: { projectId } });
|
|
508
592
|
break;
|
|
509
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
|
+
}
|
|
510
690
|
// ─── Tasks ─────────────────────────────────────────────
|
|
511
691
|
case "list_tasks": {
|
|
512
692
|
const projectId = msg.payload?.projectId;
|
|
@@ -545,21 +725,45 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
545
725
|
const projectId = msg.payload?.projectId;
|
|
546
726
|
const title = msg.payload?.title;
|
|
547
727
|
if (!projectId || !title) {
|
|
548
|
-
sendWs(ws, {
|
|
728
|
+
sendWs(ws, {
|
|
729
|
+
type: "error",
|
|
730
|
+
payload: { message: "projectId and title required" },
|
|
731
|
+
});
|
|
549
732
|
return;
|
|
550
733
|
}
|
|
551
734
|
const project = projectStore.getProject(projectId);
|
|
552
735
|
if (!project) {
|
|
553
|
-
sendWs(ws, {
|
|
736
|
+
sendWs(ws, {
|
|
737
|
+
type: "error",
|
|
738
|
+
payload: { message: `Project not found: ${projectId}` },
|
|
739
|
+
});
|
|
554
740
|
return;
|
|
555
741
|
}
|
|
556
742
|
const parentTaskId = msg.payload?.parentTaskId || "";
|
|
557
743
|
const rawCanDecompose = msg.payload?.canDecompose;
|
|
558
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
|
+
}
|
|
559
756
|
const id = uuid().slice(0, 8);
|
|
560
|
-
taskStore.createTask(id, projectId, title, msg.payload?.description || "",
|
|
757
|
+
taskStore.createTask(id, projectId, title, msg.payload?.description || "", resolvedEnvId, msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose, msg.payload?.personaId || "");
|
|
561
758
|
const row = taskStore.getTask(id);
|
|
562
|
-
broadcast({
|
|
759
|
+
broadcast({
|
|
760
|
+
type: "task_created",
|
|
761
|
+
payload: {
|
|
762
|
+
task: row
|
|
763
|
+
? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) }
|
|
764
|
+
: null,
|
|
765
|
+
},
|
|
766
|
+
});
|
|
563
767
|
break;
|
|
564
768
|
}
|
|
565
769
|
case "start_task": {
|
|
@@ -568,15 +772,26 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
568
772
|
return;
|
|
569
773
|
const task = taskStore.getTask(taskId);
|
|
570
774
|
if (!task) {
|
|
571
|
-
sendWs(ws, {
|
|
775
|
+
sendWs(ws, {
|
|
776
|
+
type: "error",
|
|
777
|
+
payload: { message: `Task not found: ${taskId}` },
|
|
778
|
+
});
|
|
572
779
|
return;
|
|
573
780
|
}
|
|
574
781
|
if (!["pending", "assigned", "failed"].includes(task.status)) {
|
|
575
|
-
sendWs(ws, {
|
|
782
|
+
sendWs(ws, {
|
|
783
|
+
type: "error",
|
|
784
|
+
payload: {
|
|
785
|
+
message: `Task cannot be started (status: ${task.status})`,
|
|
786
|
+
},
|
|
787
|
+
});
|
|
576
788
|
return;
|
|
577
789
|
}
|
|
578
790
|
if (!taskStore.areDependenciesMet(taskId)) {
|
|
579
|
-
sendWs(ws, {
|
|
791
|
+
sendWs(ws, {
|
|
792
|
+
type: "error",
|
|
793
|
+
payload: { message: "Task has unmet dependencies" },
|
|
794
|
+
});
|
|
580
795
|
return;
|
|
581
796
|
}
|
|
582
797
|
const startError = await startTaskSession(ws, task, {
|
|
@@ -614,7 +829,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
614
829
|
if (!task)
|
|
615
830
|
return;
|
|
616
831
|
if (task.status !== "review") {
|
|
617
|
-
sendWs(ws, {
|
|
832
|
+
sendWs(ws, {
|
|
833
|
+
type: "error",
|
|
834
|
+
payload: {
|
|
835
|
+
message: `Task cannot be rejected (status: ${task.status})`,
|
|
836
|
+
},
|
|
837
|
+
});
|
|
618
838
|
return;
|
|
619
839
|
}
|
|
620
840
|
// Preserve runtime/model from the previous session so the retry
|
|
@@ -624,7 +844,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
624
844
|
: undefined;
|
|
625
845
|
// Store review notes and set status to assigned
|
|
626
846
|
taskStore.updateTask(task.id, task.title, task.description, "assigned", task.environmentId, safeParseJsonArray(task.dependsOn), reviewNotes);
|
|
627
|
-
broadcast({
|
|
847
|
+
broadcast({
|
|
848
|
+
type: "task_rejected",
|
|
849
|
+
payload: { taskId, projectId: task.projectId },
|
|
850
|
+
});
|
|
628
851
|
// Auto-retry: start a new session with the review feedback
|
|
629
852
|
const freshTask = taskStore.getTask(taskId);
|
|
630
853
|
if (freshTask) {
|
|
@@ -642,14 +865,51 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
642
865
|
const taskId = msg.payload?.taskId;
|
|
643
866
|
if (!taskId)
|
|
644
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
|
+
}
|
|
645
873
|
const children = taskStore.getChildren(taskId);
|
|
646
874
|
if (children.length > 0) {
|
|
647
|
-
sendWs(ws, {
|
|
875
|
+
sendWs(ws, {
|
|
876
|
+
type: "error",
|
|
877
|
+
payload: {
|
|
878
|
+
message: "Cannot delete task with children. Delete children first.",
|
|
879
|
+
},
|
|
880
|
+
});
|
|
648
881
|
return;
|
|
649
882
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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 } });
|
|
653
913
|
break;
|
|
654
914
|
}
|
|
655
915
|
// ─── Findings ──────────────────────────────────────────
|
|
@@ -681,7 +941,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
681
941
|
const projectId = msg.payload?.projectId;
|
|
682
942
|
const title = msg.payload?.title;
|
|
683
943
|
if (!projectId || !title) {
|
|
684
|
-
sendWs(ws, {
|
|
944
|
+
sendWs(ws, {
|
|
945
|
+
type: "error",
|
|
946
|
+
payload: { message: "projectId and title required" },
|
|
947
|
+
});
|
|
685
948
|
return;
|
|
686
949
|
}
|
|
687
950
|
const id = uuid().slice(0, 8);
|
|
@@ -696,17 +959,27 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
696
959
|
return;
|
|
697
960
|
const task = taskStore.getTask(taskId);
|
|
698
961
|
if (!task || !task.branch) {
|
|
699
|
-
sendWs(ws, {
|
|
962
|
+
sendWs(ws, {
|
|
963
|
+
type: "task_diff",
|
|
964
|
+
payload: { taskId, error: "No branch" },
|
|
965
|
+
});
|
|
700
966
|
return;
|
|
701
967
|
}
|
|
702
|
-
const environmentId = task.environmentId ||
|
|
968
|
+
const environmentId = task.environmentId ||
|
|
969
|
+
projectStore.getProject(task.projectId)?.defaultEnvironmentId;
|
|
703
970
|
if (!environmentId) {
|
|
704
|
-
sendWs(ws, {
|
|
971
|
+
sendWs(ws, {
|
|
972
|
+
type: "task_diff",
|
|
973
|
+
payload: { taskId, error: "No environment" },
|
|
974
|
+
});
|
|
705
975
|
return;
|
|
706
976
|
}
|
|
707
977
|
const conn = adapterManager.getConnection(environmentId);
|
|
708
978
|
if (!conn) {
|
|
709
|
-
sendWs(ws, {
|
|
979
|
+
sendWs(ws, {
|
|
980
|
+
type: "task_diff",
|
|
981
|
+
payload: { taskId, error: "Environment not connected" },
|
|
982
|
+
});
|
|
710
983
|
return;
|
|
711
984
|
}
|
|
712
985
|
try {
|
|
@@ -728,24 +1001,36 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
728
1001
|
});
|
|
729
1002
|
}
|
|
730
1003
|
catch (err) {
|
|
731
|
-
sendWs(ws, {
|
|
1004
|
+
sendWs(ws, {
|
|
1005
|
+
type: "task_diff",
|
|
1006
|
+
payload: { taskId, error: String(err) },
|
|
1007
|
+
});
|
|
732
1008
|
}
|
|
733
1009
|
break;
|
|
734
1010
|
}
|
|
735
1011
|
case "provision_environment": {
|
|
736
1012
|
const environmentId = msg.payload?.environmentId;
|
|
737
1013
|
if (!environmentId) {
|
|
738
|
-
sendWs(ws, {
|
|
1014
|
+
sendWs(ws, {
|
|
1015
|
+
type: "error",
|
|
1016
|
+
payload: { message: "environmentId required" },
|
|
1017
|
+
});
|
|
739
1018
|
return;
|
|
740
1019
|
}
|
|
741
1020
|
const env = envRegistry.getEnvironment(environmentId);
|
|
742
1021
|
if (!env) {
|
|
743
|
-
sendWs(ws, {
|
|
1022
|
+
sendWs(ws, {
|
|
1023
|
+
type: "error",
|
|
1024
|
+
payload: { message: `Environment not found: ${environmentId}` },
|
|
1025
|
+
});
|
|
744
1026
|
return;
|
|
745
1027
|
}
|
|
746
1028
|
const adapter = adapterManager.getAdapter(env.adapterType);
|
|
747
1029
|
if (!adapter) {
|
|
748
|
-
sendWs(ws, {
|
|
1030
|
+
sendWs(ws, {
|
|
1031
|
+
type: "error",
|
|
1032
|
+
payload: { message: `No adapter for type: ${env.adapterType}` },
|
|
1033
|
+
});
|
|
749
1034
|
return;
|
|
750
1035
|
}
|
|
751
1036
|
logger.info({ environmentId, adapterType: env.adapterType }, "Provisioning environment");
|
|
@@ -761,7 +1046,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
761
1046
|
logger.info({ environmentId, stage: event.stage, message: event.message }, "Provision progress");
|
|
762
1047
|
broadcast({
|
|
763
1048
|
type: "provision_progress",
|
|
764
|
-
payload: {
|
|
1049
|
+
payload: {
|
|
1050
|
+
environmentId,
|
|
1051
|
+
stage: event.stage,
|
|
1052
|
+
message: event.message,
|
|
1053
|
+
progress: event.progress,
|
|
1054
|
+
},
|
|
765
1055
|
});
|
|
766
1056
|
}
|
|
767
1057
|
logger.info({ environmentId }, "Provision complete, calling adapter.connect");
|
|
@@ -774,7 +1064,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
774
1064
|
logger.info({ environmentId }, "Environment connected");
|
|
775
1065
|
broadcast({
|
|
776
1066
|
type: "provision_progress",
|
|
777
|
-
payload: {
|
|
1067
|
+
payload: {
|
|
1068
|
+
environmentId,
|
|
1069
|
+
stage: "ready",
|
|
1070
|
+
message: "Environment connected",
|
|
1071
|
+
progress: 1,
|
|
1072
|
+
},
|
|
778
1073
|
});
|
|
779
1074
|
}
|
|
780
1075
|
catch (err) {
|
|
@@ -783,7 +1078,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
783
1078
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
784
1079
|
broadcast({
|
|
785
1080
|
type: "provision_progress",
|
|
786
|
-
payload: {
|
|
1081
|
+
payload: {
|
|
1082
|
+
environmentId,
|
|
1083
|
+
stage: "error",
|
|
1084
|
+
message: `Connection failed: ${errorMessage}`,
|
|
1085
|
+
progress: 0,
|
|
1086
|
+
},
|
|
787
1087
|
});
|
|
788
1088
|
}
|
|
789
1089
|
broadcastEnvironments();
|
|
@@ -793,12 +1093,18 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
793
1093
|
case "stop_environment": {
|
|
794
1094
|
const environmentId = msg.payload?.environmentId;
|
|
795
1095
|
if (!environmentId) {
|
|
796
|
-
sendWs(ws, {
|
|
1096
|
+
sendWs(ws, {
|
|
1097
|
+
type: "error",
|
|
1098
|
+
payload: { message: "environmentId required" },
|
|
1099
|
+
});
|
|
797
1100
|
return;
|
|
798
1101
|
}
|
|
799
1102
|
const env = envRegistry.getEnvironment(environmentId);
|
|
800
1103
|
if (!env) {
|
|
801
|
-
sendWs(ws, {
|
|
1104
|
+
sendWs(ws, {
|
|
1105
|
+
type: "error",
|
|
1106
|
+
payload: { message: `Environment not found: ${environmentId}` },
|
|
1107
|
+
});
|
|
802
1108
|
return;
|
|
803
1109
|
}
|
|
804
1110
|
const adapter = adapterManager.getAdapter(env.adapterType);
|
|
@@ -816,11 +1122,17 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
816
1122
|
const displayName = msg.payload?.displayName || "";
|
|
817
1123
|
const adapterType = msg.payload?.adapterType || "";
|
|
818
1124
|
if (!displayName || !adapterType) {
|
|
819
|
-
sendWs(ws, {
|
|
1125
|
+
sendWs(ws, {
|
|
1126
|
+
type: "error",
|
|
1127
|
+
payload: { message: "displayName and adapterType required" },
|
|
1128
|
+
});
|
|
820
1129
|
return;
|
|
821
1130
|
}
|
|
822
1131
|
if (!adapterManager.getAdapter(adapterType)) {
|
|
823
|
-
sendWs(ws, {
|
|
1132
|
+
sendWs(ws, {
|
|
1133
|
+
type: "error",
|
|
1134
|
+
payload: { message: `Unknown adapter type: ${adapterType}` },
|
|
1135
|
+
});
|
|
824
1136
|
return;
|
|
825
1137
|
}
|
|
826
1138
|
const baseEnvId = slugify(displayName) || uuid().slice(0, 8);
|
|
@@ -842,7 +1154,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
842
1154
|
JSON.parse(normalized);
|
|
843
1155
|
}
|
|
844
1156
|
catch {
|
|
845
|
-
sendWs(ws, {
|
|
1157
|
+
sendWs(ws, {
|
|
1158
|
+
type: "error",
|
|
1159
|
+
payload: { message: "adapterConfig string is not valid JSON" },
|
|
1160
|
+
});
|
|
846
1161
|
return;
|
|
847
1162
|
}
|
|
848
1163
|
adapterConfig = normalized;
|
|
@@ -851,7 +1166,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
851
1166
|
adapterConfig = JSON.stringify(rawAdapterConfig);
|
|
852
1167
|
}
|
|
853
1168
|
else {
|
|
854
|
-
sendWs(ws, {
|
|
1169
|
+
sendWs(ws, {
|
|
1170
|
+
type: "error",
|
|
1171
|
+
payload: {
|
|
1172
|
+
message: "adapterConfig must be an object or JSON string",
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
855
1175
|
return;
|
|
856
1176
|
}
|
|
857
1177
|
const defaultRuntime = msg.payload?.defaultRuntime || DEFAULT_RUNTIME;
|
|
@@ -864,7 +1184,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
864
1184
|
case "remove_environment": {
|
|
865
1185
|
const environmentId = msg.payload?.environmentId;
|
|
866
1186
|
if (!environmentId) {
|
|
867
|
-
sendWs(ws, {
|
|
1187
|
+
sendWs(ws, {
|
|
1188
|
+
type: "error",
|
|
1189
|
+
payload: { message: "environmentId required" },
|
|
1190
|
+
});
|
|
868
1191
|
return;
|
|
869
1192
|
}
|
|
870
1193
|
const env = envRegistry.getEnvironment(environmentId);
|
|
@@ -875,11 +1198,15 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
875
1198
|
try {
|
|
876
1199
|
await adapter.destroy(environmentId, config);
|
|
877
1200
|
}
|
|
878
|
-
catch {
|
|
1201
|
+
catch {
|
|
1202
|
+
/* best-effort */
|
|
1203
|
+
}
|
|
879
1204
|
try {
|
|
880
1205
|
await adapter.disconnect(environmentId);
|
|
881
1206
|
}
|
|
882
|
-
catch {
|
|
1207
|
+
catch {
|
|
1208
|
+
/* best-effort */
|
|
1209
|
+
}
|
|
883
1210
|
}
|
|
884
1211
|
}
|
|
885
1212
|
adapterManager.removeConnection(environmentId);
|
|
@@ -894,31 +1221,43 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
894
1221
|
case "list_codespaces": {
|
|
895
1222
|
try {
|
|
896
1223
|
const result = await exec("gh", [
|
|
897
|
-
"codespace",
|
|
898
|
-
"
|
|
899
|
-
"--
|
|
1224
|
+
"codespace",
|
|
1225
|
+
"list",
|
|
1226
|
+
"--json",
|
|
1227
|
+
"name,repository,state,gitStatus",
|
|
1228
|
+
"--limit",
|
|
1229
|
+
String(GH_CODESPACE_LIST_LIMIT),
|
|
900
1230
|
], { timeout: GH_CODESPACE_LIST_TIMEOUT_MS });
|
|
901
1231
|
const codespaces = JSON.parse(result.stdout || "[]");
|
|
902
1232
|
sendWs(ws, { type: "codespaces_list", payload: { codespaces } });
|
|
903
1233
|
}
|
|
904
1234
|
catch (err) {
|
|
905
1235
|
logger.warn({ err }, "Failed to list codespaces");
|
|
906
|
-
sendWs(ws, {
|
|
1236
|
+
sendWs(ws, {
|
|
1237
|
+
type: "codespaces_list",
|
|
1238
|
+
payload: { codespaces: [], error: String(err) },
|
|
1239
|
+
});
|
|
907
1240
|
}
|
|
908
1241
|
break;
|
|
909
1242
|
}
|
|
910
1243
|
case "create_codespace": {
|
|
911
1244
|
const repo = msg.payload?.repo;
|
|
912
1245
|
if (typeof repo !== "string" || repo.trim().length === 0) {
|
|
913
|
-
sendWs(ws, {
|
|
1246
|
+
sendWs(ws, {
|
|
1247
|
+
type: "codespace_create_error",
|
|
1248
|
+
payload: { message: "repo required" },
|
|
1249
|
+
});
|
|
914
1250
|
return;
|
|
915
1251
|
}
|
|
916
1252
|
const trimmedRepo = repo.trim();
|
|
917
1253
|
try {
|
|
918
1254
|
const result = await exec("gh", [
|
|
919
|
-
"codespace",
|
|
920
|
-
"
|
|
921
|
-
"--
|
|
1255
|
+
"codespace",
|
|
1256
|
+
"create",
|
|
1257
|
+
"--repo",
|
|
1258
|
+
trimmedRepo,
|
|
1259
|
+
"--machine",
|
|
1260
|
+
"basicLinux32gb",
|
|
922
1261
|
], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
|
|
923
1262
|
const codespaceName = result.stdout.trim();
|
|
924
1263
|
sendWs(ws, {
|
|
@@ -928,7 +1267,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
928
1267
|
}
|
|
929
1268
|
catch (err) {
|
|
930
1269
|
logger.error({ err, repo }, "Failed to create codespace");
|
|
931
|
-
sendWs(ws, {
|
|
1270
|
+
sendWs(ws, {
|
|
1271
|
+
type: "codespace_create_error",
|
|
1272
|
+
payload: { message: String(err) },
|
|
1273
|
+
});
|
|
932
1274
|
}
|
|
933
1275
|
break;
|
|
934
1276
|
}
|
|
@@ -953,7 +1295,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
953
1295
|
const name = msg.payload?.name;
|
|
954
1296
|
const value = msg.payload?.value;
|
|
955
1297
|
if (!name || !value) {
|
|
956
|
-
sendWs(ws, {
|
|
1298
|
+
sendWs(ws, {
|
|
1299
|
+
type: "error",
|
|
1300
|
+
payload: { message: "name and value required" },
|
|
1301
|
+
});
|
|
957
1302
|
return;
|
|
958
1303
|
}
|
|
959
1304
|
await tokenBroker.setToken({
|