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