@grackle-ai/server 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +18 -1
- package/dist/db.js.map +1 -1
- package/dist/grpc-service.d.ts +1 -1
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +48 -5
- package/dist/grpc-service.js.map +1 -1
- package/dist/schema.d.ts +19 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-store.d.ts +3 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +10 -3
- package/dist/session-store.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/utils/system-context.d.ts.map +1 -1
- package/dist/utils/system-context.js +1 -1
- package/dist/utils/system-context.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +436 -66
- 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;
|
|
@@ -193,10 +226,17 @@ async function startTaskSession(ws, task, options) {
|
|
|
193
226
|
if (persona) {
|
|
194
227
|
systemContext = persona.systemPrompt + "\n\n" + systemContext;
|
|
195
228
|
}
|
|
196
|
-
sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath);
|
|
229
|
+
sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath, freshTask.id);
|
|
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,76 @@ 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 } });
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
// ─── Task Sessions ─────────────────────────────────────
|
|
916
|
+
case "get_task_sessions": {
|
|
917
|
+
const taskId = msg.payload?.taskId;
|
|
918
|
+
if (typeof taskId !== "string" || taskId.length === 0) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const taskSessions = sessionStore.listSessionsForTask(taskId);
|
|
922
|
+
sendWs(ws, {
|
|
923
|
+
type: "task_sessions",
|
|
924
|
+
payload: {
|
|
925
|
+
taskId,
|
|
926
|
+
sessions: taskSessions.map((r) => ({
|
|
927
|
+
id: r.id,
|
|
928
|
+
environmentId: r.environmentId,
|
|
929
|
+
runtime: r.runtime,
|
|
930
|
+
status: r.status,
|
|
931
|
+
prompt: r.prompt,
|
|
932
|
+
startedAt: r.startedAt,
|
|
933
|
+
endedAt: r.endedAt ?? "",
|
|
934
|
+
error: r.error ?? "",
|
|
935
|
+
})),
|
|
936
|
+
},
|
|
937
|
+
});
|
|
653
938
|
break;
|
|
654
939
|
}
|
|
655
940
|
// ─── Findings ──────────────────────────────────────────
|
|
@@ -681,7 +966,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
681
966
|
const projectId = msg.payload?.projectId;
|
|
682
967
|
const title = msg.payload?.title;
|
|
683
968
|
if (!projectId || !title) {
|
|
684
|
-
sendWs(ws, {
|
|
969
|
+
sendWs(ws, {
|
|
970
|
+
type: "error",
|
|
971
|
+
payload: { message: "projectId and title required" },
|
|
972
|
+
});
|
|
685
973
|
return;
|
|
686
974
|
}
|
|
687
975
|
const id = uuid().slice(0, 8);
|
|
@@ -696,17 +984,27 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
696
984
|
return;
|
|
697
985
|
const task = taskStore.getTask(taskId);
|
|
698
986
|
if (!task || !task.branch) {
|
|
699
|
-
sendWs(ws, {
|
|
987
|
+
sendWs(ws, {
|
|
988
|
+
type: "task_diff",
|
|
989
|
+
payload: { taskId, error: "No branch" },
|
|
990
|
+
});
|
|
700
991
|
return;
|
|
701
992
|
}
|
|
702
|
-
const environmentId = task.environmentId ||
|
|
993
|
+
const environmentId = task.environmentId ||
|
|
994
|
+
projectStore.getProject(task.projectId)?.defaultEnvironmentId;
|
|
703
995
|
if (!environmentId) {
|
|
704
|
-
sendWs(ws, {
|
|
996
|
+
sendWs(ws, {
|
|
997
|
+
type: "task_diff",
|
|
998
|
+
payload: { taskId, error: "No environment" },
|
|
999
|
+
});
|
|
705
1000
|
return;
|
|
706
1001
|
}
|
|
707
1002
|
const conn = adapterManager.getConnection(environmentId);
|
|
708
1003
|
if (!conn) {
|
|
709
|
-
sendWs(ws, {
|
|
1004
|
+
sendWs(ws, {
|
|
1005
|
+
type: "task_diff",
|
|
1006
|
+
payload: { taskId, error: "Environment not connected" },
|
|
1007
|
+
});
|
|
710
1008
|
return;
|
|
711
1009
|
}
|
|
712
1010
|
try {
|
|
@@ -728,24 +1026,36 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
728
1026
|
});
|
|
729
1027
|
}
|
|
730
1028
|
catch (err) {
|
|
731
|
-
sendWs(ws, {
|
|
1029
|
+
sendWs(ws, {
|
|
1030
|
+
type: "task_diff",
|
|
1031
|
+
payload: { taskId, error: String(err) },
|
|
1032
|
+
});
|
|
732
1033
|
}
|
|
733
1034
|
break;
|
|
734
1035
|
}
|
|
735
1036
|
case "provision_environment": {
|
|
736
1037
|
const environmentId = msg.payload?.environmentId;
|
|
737
1038
|
if (!environmentId) {
|
|
738
|
-
sendWs(ws, {
|
|
1039
|
+
sendWs(ws, {
|
|
1040
|
+
type: "error",
|
|
1041
|
+
payload: { message: "environmentId required" },
|
|
1042
|
+
});
|
|
739
1043
|
return;
|
|
740
1044
|
}
|
|
741
1045
|
const env = envRegistry.getEnvironment(environmentId);
|
|
742
1046
|
if (!env) {
|
|
743
|
-
sendWs(ws, {
|
|
1047
|
+
sendWs(ws, {
|
|
1048
|
+
type: "error",
|
|
1049
|
+
payload: { message: `Environment not found: ${environmentId}` },
|
|
1050
|
+
});
|
|
744
1051
|
return;
|
|
745
1052
|
}
|
|
746
1053
|
const adapter = adapterManager.getAdapter(env.adapterType);
|
|
747
1054
|
if (!adapter) {
|
|
748
|
-
sendWs(ws, {
|
|
1055
|
+
sendWs(ws, {
|
|
1056
|
+
type: "error",
|
|
1057
|
+
payload: { message: `No adapter for type: ${env.adapterType}` },
|
|
1058
|
+
});
|
|
749
1059
|
return;
|
|
750
1060
|
}
|
|
751
1061
|
logger.info({ environmentId, adapterType: env.adapterType }, "Provisioning environment");
|
|
@@ -761,7 +1071,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
761
1071
|
logger.info({ environmentId, stage: event.stage, message: event.message }, "Provision progress");
|
|
762
1072
|
broadcast({
|
|
763
1073
|
type: "provision_progress",
|
|
764
|
-
payload: {
|
|
1074
|
+
payload: {
|
|
1075
|
+
environmentId,
|
|
1076
|
+
stage: event.stage,
|
|
1077
|
+
message: event.message,
|
|
1078
|
+
progress: event.progress,
|
|
1079
|
+
},
|
|
765
1080
|
});
|
|
766
1081
|
}
|
|
767
1082
|
logger.info({ environmentId }, "Provision complete, calling adapter.connect");
|
|
@@ -774,7 +1089,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
774
1089
|
logger.info({ environmentId }, "Environment connected");
|
|
775
1090
|
broadcast({
|
|
776
1091
|
type: "provision_progress",
|
|
777
|
-
payload: {
|
|
1092
|
+
payload: {
|
|
1093
|
+
environmentId,
|
|
1094
|
+
stage: "ready",
|
|
1095
|
+
message: "Environment connected",
|
|
1096
|
+
progress: 1,
|
|
1097
|
+
},
|
|
778
1098
|
});
|
|
779
1099
|
}
|
|
780
1100
|
catch (err) {
|
|
@@ -783,7 +1103,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
783
1103
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
784
1104
|
broadcast({
|
|
785
1105
|
type: "provision_progress",
|
|
786
|
-
payload: {
|
|
1106
|
+
payload: {
|
|
1107
|
+
environmentId,
|
|
1108
|
+
stage: "error",
|
|
1109
|
+
message: `Connection failed: ${errorMessage}`,
|
|
1110
|
+
progress: 0,
|
|
1111
|
+
},
|
|
787
1112
|
});
|
|
788
1113
|
}
|
|
789
1114
|
broadcastEnvironments();
|
|
@@ -793,12 +1118,18 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
793
1118
|
case "stop_environment": {
|
|
794
1119
|
const environmentId = msg.payload?.environmentId;
|
|
795
1120
|
if (!environmentId) {
|
|
796
|
-
sendWs(ws, {
|
|
1121
|
+
sendWs(ws, {
|
|
1122
|
+
type: "error",
|
|
1123
|
+
payload: { message: "environmentId required" },
|
|
1124
|
+
});
|
|
797
1125
|
return;
|
|
798
1126
|
}
|
|
799
1127
|
const env = envRegistry.getEnvironment(environmentId);
|
|
800
1128
|
if (!env) {
|
|
801
|
-
sendWs(ws, {
|
|
1129
|
+
sendWs(ws, {
|
|
1130
|
+
type: "error",
|
|
1131
|
+
payload: { message: `Environment not found: ${environmentId}` },
|
|
1132
|
+
});
|
|
802
1133
|
return;
|
|
803
1134
|
}
|
|
804
1135
|
const adapter = adapterManager.getAdapter(env.adapterType);
|
|
@@ -816,11 +1147,17 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
816
1147
|
const displayName = msg.payload?.displayName || "";
|
|
817
1148
|
const adapterType = msg.payload?.adapterType || "";
|
|
818
1149
|
if (!displayName || !adapterType) {
|
|
819
|
-
sendWs(ws, {
|
|
1150
|
+
sendWs(ws, {
|
|
1151
|
+
type: "error",
|
|
1152
|
+
payload: { message: "displayName and adapterType required" },
|
|
1153
|
+
});
|
|
820
1154
|
return;
|
|
821
1155
|
}
|
|
822
1156
|
if (!adapterManager.getAdapter(adapterType)) {
|
|
823
|
-
sendWs(ws, {
|
|
1157
|
+
sendWs(ws, {
|
|
1158
|
+
type: "error",
|
|
1159
|
+
payload: { message: `Unknown adapter type: ${adapterType}` },
|
|
1160
|
+
});
|
|
824
1161
|
return;
|
|
825
1162
|
}
|
|
826
1163
|
const baseEnvId = slugify(displayName) || uuid().slice(0, 8);
|
|
@@ -842,7 +1179,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
842
1179
|
JSON.parse(normalized);
|
|
843
1180
|
}
|
|
844
1181
|
catch {
|
|
845
|
-
sendWs(ws, {
|
|
1182
|
+
sendWs(ws, {
|
|
1183
|
+
type: "error",
|
|
1184
|
+
payload: { message: "adapterConfig string is not valid JSON" },
|
|
1185
|
+
});
|
|
846
1186
|
return;
|
|
847
1187
|
}
|
|
848
1188
|
adapterConfig = normalized;
|
|
@@ -851,7 +1191,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
851
1191
|
adapterConfig = JSON.stringify(rawAdapterConfig);
|
|
852
1192
|
}
|
|
853
1193
|
else {
|
|
854
|
-
sendWs(ws, {
|
|
1194
|
+
sendWs(ws, {
|
|
1195
|
+
type: "error",
|
|
1196
|
+
payload: {
|
|
1197
|
+
message: "adapterConfig must be an object or JSON string",
|
|
1198
|
+
},
|
|
1199
|
+
});
|
|
855
1200
|
return;
|
|
856
1201
|
}
|
|
857
1202
|
const defaultRuntime = msg.payload?.defaultRuntime || DEFAULT_RUNTIME;
|
|
@@ -864,7 +1209,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
864
1209
|
case "remove_environment": {
|
|
865
1210
|
const environmentId = msg.payload?.environmentId;
|
|
866
1211
|
if (!environmentId) {
|
|
867
|
-
sendWs(ws, {
|
|
1212
|
+
sendWs(ws, {
|
|
1213
|
+
type: "error",
|
|
1214
|
+
payload: { message: "environmentId required" },
|
|
1215
|
+
});
|
|
868
1216
|
return;
|
|
869
1217
|
}
|
|
870
1218
|
const env = envRegistry.getEnvironment(environmentId);
|
|
@@ -875,11 +1223,15 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
875
1223
|
try {
|
|
876
1224
|
await adapter.destroy(environmentId, config);
|
|
877
1225
|
}
|
|
878
|
-
catch {
|
|
1226
|
+
catch {
|
|
1227
|
+
/* best-effort */
|
|
1228
|
+
}
|
|
879
1229
|
try {
|
|
880
1230
|
await adapter.disconnect(environmentId);
|
|
881
1231
|
}
|
|
882
|
-
catch {
|
|
1232
|
+
catch {
|
|
1233
|
+
/* best-effort */
|
|
1234
|
+
}
|
|
883
1235
|
}
|
|
884
1236
|
}
|
|
885
1237
|
adapterManager.removeConnection(environmentId);
|
|
@@ -894,31 +1246,43 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
894
1246
|
case "list_codespaces": {
|
|
895
1247
|
try {
|
|
896
1248
|
const result = await exec("gh", [
|
|
897
|
-
"codespace",
|
|
898
|
-
"
|
|
899
|
-
"--
|
|
1249
|
+
"codespace",
|
|
1250
|
+
"list",
|
|
1251
|
+
"--json",
|
|
1252
|
+
"name,repository,state,gitStatus",
|
|
1253
|
+
"--limit",
|
|
1254
|
+
String(GH_CODESPACE_LIST_LIMIT),
|
|
900
1255
|
], { timeout: GH_CODESPACE_LIST_TIMEOUT_MS });
|
|
901
1256
|
const codespaces = JSON.parse(result.stdout || "[]");
|
|
902
1257
|
sendWs(ws, { type: "codespaces_list", payload: { codespaces } });
|
|
903
1258
|
}
|
|
904
1259
|
catch (err) {
|
|
905
1260
|
logger.warn({ err }, "Failed to list codespaces");
|
|
906
|
-
sendWs(ws, {
|
|
1261
|
+
sendWs(ws, {
|
|
1262
|
+
type: "codespaces_list",
|
|
1263
|
+
payload: { codespaces: [], error: String(err) },
|
|
1264
|
+
});
|
|
907
1265
|
}
|
|
908
1266
|
break;
|
|
909
1267
|
}
|
|
910
1268
|
case "create_codespace": {
|
|
911
1269
|
const repo = msg.payload?.repo;
|
|
912
1270
|
if (typeof repo !== "string" || repo.trim().length === 0) {
|
|
913
|
-
sendWs(ws, {
|
|
1271
|
+
sendWs(ws, {
|
|
1272
|
+
type: "codespace_create_error",
|
|
1273
|
+
payload: { message: "repo required" },
|
|
1274
|
+
});
|
|
914
1275
|
return;
|
|
915
1276
|
}
|
|
916
1277
|
const trimmedRepo = repo.trim();
|
|
917
1278
|
try {
|
|
918
1279
|
const result = await exec("gh", [
|
|
919
|
-
"codespace",
|
|
920
|
-
"
|
|
921
|
-
"--
|
|
1280
|
+
"codespace",
|
|
1281
|
+
"create",
|
|
1282
|
+
"--repo",
|
|
1283
|
+
trimmedRepo,
|
|
1284
|
+
"--machine",
|
|
1285
|
+
"basicLinux32gb",
|
|
922
1286
|
], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
|
|
923
1287
|
const codespaceName = result.stdout.trim();
|
|
924
1288
|
sendWs(ws, {
|
|
@@ -928,7 +1292,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
928
1292
|
}
|
|
929
1293
|
catch (err) {
|
|
930
1294
|
logger.error({ err, repo }, "Failed to create codespace");
|
|
931
|
-
sendWs(ws, {
|
|
1295
|
+
sendWs(ws, {
|
|
1296
|
+
type: "codespace_create_error",
|
|
1297
|
+
payload: { message: String(err) },
|
|
1298
|
+
});
|
|
932
1299
|
}
|
|
933
1300
|
break;
|
|
934
1301
|
}
|
|
@@ -953,7 +1320,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
953
1320
|
const name = msg.payload?.name;
|
|
954
1321
|
const value = msg.payload?.value;
|
|
955
1322
|
if (!name || !value) {
|
|
956
|
-
sendWs(ws, {
|
|
1323
|
+
sendWs(ws, {
|
|
1324
|
+
type: "error",
|
|
1325
|
+
payload: { message: "name and value required" },
|
|
1326
|
+
});
|
|
957
1327
|
return;
|
|
958
1328
|
}
|
|
959
1329
|
await tokenBroker.setToken({
|