@grackle-ai/server 0.25.0 → 0.27.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/adapter-manager.d.ts.map +1 -1
- package/dist/adapter-manager.js +1 -0
- package/dist/adapter-manager.js.map +1 -1
- package/dist/adapters/codespace.js +1 -1
- package/dist/adapters/codespace.js.map +1 -1
- package/dist/adapters/docker.js +1 -1
- package/dist/adapters/docker.js.map +1 -1
- package/dist/adapters/local.js +1 -1
- package/dist/adapters/local.js.map +1 -1
- package/dist/adapters/remote-adapter-utils.d.ts.map +1 -1
- package/dist/adapters/remote-adapter-utils.js +5 -4
- package/dist/adapters/remote-adapter-utils.js.map +1 -1
- package/dist/adapters/ssh.js +1 -1
- package/dist/adapters/ssh.js.map +1 -1
- package/dist/compute-task-status.d.ts +31 -0
- package/dist/compute-task-status.d.ts.map +1 -0
- package/dist/compute-task-status.js +88 -0
- package/dist/compute-task-status.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +112 -19
- package/dist/db.js.map +1 -1
- package/dist/event-processor.d.ts +0 -1
- package/dist/event-processor.d.ts.map +1 -1
- package/dist/event-processor.js +28 -32
- package/dist/event-processor.js.map +1 -1
- package/dist/github-import.d.ts +1 -1
- package/dist/github-import.d.ts.map +1 -1
- package/dist/github-import.js +7 -7
- package/dist/github-import.js.map +1 -1
- package/dist/grpc-service.d.ts +0 -6
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +142 -117
- package/dist/grpc-service.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/processor-registry.d.ts +1 -2
- package/dist/processor-registry.d.ts.map +1 -1
- package/dist/processor-registry.js +1 -4
- package/dist/processor-registry.js.map +1 -1
- package/dist/project-store.d.ts +3 -1
- package/dist/project-store.d.ts.map +1 -1
- package/dist/project-store.js +5 -1
- package/dist/project-store.js.map +1 -1
- package/dist/schema.d.ts +38 -95
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -6
- package/dist/schema.js.map +1 -1
- package/dist/session-store.d.ts +7 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +42 -4
- package/dist/session-store.js.map +1 -1
- package/dist/task-store.d.ts +9 -10
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +14 -36
- package/dist/task-store.js.map +1 -1
- package/dist/transcript.js.map +1 -1
- package/dist/utils/format-gh-error.d.ts +6 -0
- package/dist/utils/format-gh-error.d.ts.map +1 -0
- package/dist/utils/format-gh-error.js +30 -0
- package/dist/utils/format-gh-error.js.map +1 -0
- package/dist/utils/system-context.d.ts +1 -1
- package/dist/utils/system-context.d.ts.map +1 -1
- package/dist/utils/system-context.js +2 -2
- package/dist/utils/system-context.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +175 -171
- package/dist/ws-bridge.js.map +1 -1
- package/dist/ws-broadcast.d.ts +5 -0
- package/dist/ws-broadcast.d.ts.map +1 -1
- package/dist/ws-broadcast.js +20 -0
- package/dist/ws-broadcast.js.map +1 -1
- package/package.json +4 -4
package/dist/grpc-service.js
CHANGED
|
@@ -12,40 +12,18 @@ import * as projectStore from "./project-store.js";
|
|
|
12
12
|
import * as taskStore from "./task-store.js";
|
|
13
13
|
import * as findingStore from "./finding-store.js";
|
|
14
14
|
import * as personaStore from "./persona-store.js";
|
|
15
|
-
import { broadcast } from "./ws-broadcast.js";
|
|
15
|
+
import { broadcast, broadcastEnvironments } from "./ws-broadcast.js";
|
|
16
16
|
import { processEventStream } from "./event-processor.js";
|
|
17
17
|
import * as processorRegistry from "./processor-registry.js";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
-
import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, MAX_TASK_DEPTH, taskStatusToEnum, taskStatusToString, projectStatusToEnum, } from "@grackle-ai/common";
|
|
19
|
+
import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, MAX_TASK_DEPTH, SESSION_STATUS, TASK_STATUS, taskStatusToEnum, taskStatusToString, projectStatusToEnum, } from "@grackle-ai/common";
|
|
20
20
|
import { grackleHome } from "./paths.js";
|
|
21
21
|
import { safeParseJsonArray } from "./json-helpers.js";
|
|
22
|
+
import { computeTaskStatus } from "./compute-task-status.js";
|
|
22
23
|
import { logger } from "./logger.js";
|
|
23
24
|
import { slugify } from "./utils/slugify.js";
|
|
24
25
|
import { buildTaskSystemContext } from "./utils/system-context.js";
|
|
25
26
|
import { importGitHubIssues as executeGitHubImport } from "./github-import.js";
|
|
26
|
-
/**
|
|
27
|
-
* Build a completion callback that transitions a task to review/failed
|
|
28
|
-
* when its associated session ends. Shared across startTask, resumeAgent,
|
|
29
|
-
* updateTask (late-bind), and WS bridge paths.
|
|
30
|
-
*/
|
|
31
|
-
export function makeTaskCompletionCallback(taskId, sessionId, projectId) {
|
|
32
|
-
return () => {
|
|
33
|
-
const t = taskStore.getTask(taskId);
|
|
34
|
-
if (t && (t.status === "in_progress" || t.status === "waiting_input")) {
|
|
35
|
-
const sess = sessionStore.getSession(sessionId);
|
|
36
|
-
if (sess?.status === "completed") {
|
|
37
|
-
taskStore.markTaskCompleted(taskId, "review");
|
|
38
|
-
}
|
|
39
|
-
else if (sess?.status === "failed") {
|
|
40
|
-
taskStore.markTaskCompleted(taskId, "failed");
|
|
41
|
-
}
|
|
42
|
-
broadcast({
|
|
43
|
-
type: "task_updated",
|
|
44
|
-
payload: { taskId, projectId },
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
27
|
function envRowToProto(row) {
|
|
50
28
|
return create(grackle.EnvironmentSchema, {
|
|
51
29
|
id: row.id,
|
|
@@ -75,7 +53,8 @@ function sessionRowToProto(row) {
|
|
|
75
53
|
suspendedAt: row.suspendedAt ?? "",
|
|
76
54
|
endedAt: row.endedAt ?? "",
|
|
77
55
|
error: row.error ?? "",
|
|
78
|
-
taskId: row.taskId
|
|
56
|
+
taskId: row.taskId,
|
|
57
|
+
personaId: row.personaId,
|
|
79
58
|
});
|
|
80
59
|
}
|
|
81
60
|
function projectRowToProto(row) {
|
|
@@ -89,23 +68,21 @@ function projectRowToProto(row) {
|
|
|
89
68
|
createdAt: row.createdAt,
|
|
90
69
|
updatedAt: row.updatedAt,
|
|
91
70
|
useWorktrees: row.useWorktrees,
|
|
71
|
+
worktreeBasePath: row.worktreeBasePath,
|
|
92
72
|
});
|
|
93
73
|
}
|
|
94
|
-
function taskRowToProto(row, childIds) {
|
|
74
|
+
function taskRowToProto(row, childIds, computedStatus, latestSessionId) {
|
|
95
75
|
return create(grackle.TaskSchema, {
|
|
96
76
|
id: row.id,
|
|
97
77
|
projectId: row.projectId,
|
|
98
78
|
title: row.title,
|
|
99
79
|
description: row.description,
|
|
100
|
-
status: taskStatusToEnum(row.status),
|
|
80
|
+
status: taskStatusToEnum(computedStatus ?? row.status),
|
|
101
81
|
branch: row.branch,
|
|
102
|
-
|
|
103
|
-
sessionId: row.sessionId,
|
|
82
|
+
latestSessionId: latestSessionId ?? "",
|
|
104
83
|
dependsOn: safeParseJsonArray(row.dependsOn),
|
|
105
|
-
assignedAt: row.assignedAt ?? "",
|
|
106
84
|
startedAt: row.startedAt ?? "",
|
|
107
85
|
completedAt: row.completedAt ?? "",
|
|
108
|
-
reviewNotes: row.reviewNotes,
|
|
109
86
|
createdAt: row.createdAt,
|
|
110
87
|
updatedAt: row.updatedAt,
|
|
111
88
|
sortOrder: row.sortOrder,
|
|
@@ -113,7 +90,6 @@ function taskRowToProto(row, childIds) {
|
|
|
113
90
|
depth: row.depth,
|
|
114
91
|
childTaskIds: childIds ?? taskStore.getChildren(row.id).map((c) => c.id),
|
|
115
92
|
canDecompose: row.canDecompose,
|
|
116
|
-
personaId: row.personaId,
|
|
117
93
|
});
|
|
118
94
|
}
|
|
119
95
|
function findingRowToProto(row) {
|
|
@@ -155,8 +131,10 @@ function personaRowToProto(row) {
|
|
|
155
131
|
model: row.model,
|
|
156
132
|
maxTurns: row.maxTurns,
|
|
157
133
|
mcpServers: mcpServers
|
|
158
|
-
.filter((s) =>
|
|
159
|
-
|
|
134
|
+
.filter((s) =>
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typeof null === "object", JSON.parse can return null
|
|
136
|
+
s !== null &&
|
|
137
|
+
typeof s === "object" &&
|
|
160
138
|
typeof s.name === "string" &&
|
|
161
139
|
typeof s.command === "string")
|
|
162
140
|
.map((s) => create(grackle.McpServerConfigSchema, {
|
|
@@ -253,8 +231,9 @@ export function registerGrackleRoutes(router) {
|
|
|
253
231
|
return;
|
|
254
232
|
}
|
|
255
233
|
envRegistry.updateEnvironmentStatus(req.id, "connecting");
|
|
234
|
+
broadcastEnvironments();
|
|
256
235
|
const config = JSON.parse(env.adapterConfig);
|
|
257
|
-
const powerlineToken = env.powerlineToken
|
|
236
|
+
const powerlineToken = env.powerlineToken;
|
|
258
237
|
for await (const event of reconnectOrProvision(req.id, adapter, config, powerlineToken, !!env.bootstrapped)) {
|
|
259
238
|
yield create(grackle.ProvisionEventSchema, {
|
|
260
239
|
stage: event.stage,
|
|
@@ -269,6 +248,7 @@ export function registerGrackleRoutes(router) {
|
|
|
269
248
|
await tokenBroker.pushToEnv(req.id);
|
|
270
249
|
envRegistry.updateEnvironmentStatus(req.id, "connected");
|
|
271
250
|
envRegistry.markBootstrapped(req.id);
|
|
251
|
+
broadcastEnvironments();
|
|
272
252
|
yield create(grackle.ProvisionEventSchema, {
|
|
273
253
|
stage: "ready",
|
|
274
254
|
message: "Environment connected",
|
|
@@ -277,9 +257,10 @@ export function registerGrackleRoutes(router) {
|
|
|
277
257
|
}
|
|
278
258
|
catch (err) {
|
|
279
259
|
envRegistry.updateEnvironmentStatus(req.id, "error");
|
|
260
|
+
broadcastEnvironments();
|
|
280
261
|
yield create(grackle.ProvisionEventSchema, {
|
|
281
262
|
stage: "error",
|
|
282
|
-
message: `Connection failed: ${err}`,
|
|
263
|
+
message: `Connection failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
283
264
|
progress: 0,
|
|
284
265
|
});
|
|
285
266
|
}
|
|
@@ -295,6 +276,7 @@ export function registerGrackleRoutes(router) {
|
|
|
295
276
|
}
|
|
296
277
|
adapterManager.removeConnection(req.id);
|
|
297
278
|
envRegistry.updateEnvironmentStatus(req.id, "disconnected");
|
|
279
|
+
broadcastEnvironments();
|
|
298
280
|
return create(grackle.EmptySchema, {});
|
|
299
281
|
},
|
|
300
282
|
async destroyEnvironment(req) {
|
|
@@ -308,6 +290,7 @@ export function registerGrackleRoutes(router) {
|
|
|
308
290
|
}
|
|
309
291
|
adapterManager.removeConnection(req.id);
|
|
310
292
|
envRegistry.updateEnvironmentStatus(req.id, "disconnected");
|
|
293
|
+
broadcastEnvironments();
|
|
311
294
|
return create(grackle.EmptySchema, {});
|
|
312
295
|
},
|
|
313
296
|
async spawnAgent(req) {
|
|
@@ -346,8 +329,10 @@ export function registerGrackleRoutes(router) {
|
|
|
346
329
|
prompt: req.prompt,
|
|
347
330
|
model,
|
|
348
331
|
maxTurns: persona?.maxTurns || 0,
|
|
349
|
-
branch: req.branch
|
|
350
|
-
worktreeBasePath: req.branch
|
|
332
|
+
branch: req.branch,
|
|
333
|
+
worktreeBasePath: req.branch
|
|
334
|
+
? (req.worktreeBasePath.trim() || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
335
|
+
: "",
|
|
351
336
|
systemContext,
|
|
352
337
|
mcpServersJson,
|
|
353
338
|
});
|
|
@@ -376,13 +361,11 @@ export function registerGrackleRoutes(router) {
|
|
|
376
361
|
// Auto-bind task context from DB if session was previously associated with a task
|
|
377
362
|
let resumeProjectId;
|
|
378
363
|
let resumeTaskId;
|
|
379
|
-
let resumeOnComplete;
|
|
380
364
|
if (session.taskId) {
|
|
381
365
|
const task = taskStore.getTask(session.taskId);
|
|
382
366
|
if (task) {
|
|
383
367
|
resumeProjectId = task.projectId;
|
|
384
368
|
resumeTaskId = task.id;
|
|
385
|
-
resumeOnComplete = makeTaskCompletionCallback(task.id, session.id, task.projectId);
|
|
386
369
|
}
|
|
387
370
|
}
|
|
388
371
|
processEventStream(conn.client.resume(powerlineReq), {
|
|
@@ -390,7 +373,6 @@ export function registerGrackleRoutes(router) {
|
|
|
390
373
|
logPath,
|
|
391
374
|
projectId: resumeProjectId,
|
|
392
375
|
taskId: resumeTaskId,
|
|
393
|
-
onComplete: resumeOnComplete,
|
|
394
376
|
});
|
|
395
377
|
const row = sessionStore.getSession(session.id);
|
|
396
378
|
return sessionRowToProto(row);
|
|
@@ -400,8 +382,8 @@ export function registerGrackleRoutes(router) {
|
|
|
400
382
|
if (!session) {
|
|
401
383
|
throw new Error(`Session not found: ${req.sessionId}`);
|
|
402
384
|
}
|
|
403
|
-
if (session.status !==
|
|
404
|
-
throw new Error(`Session ${req.sessionId} is not
|
|
385
|
+
if (session.status !== SESSION_STATUS.IDLE) {
|
|
386
|
+
throw new Error(`Session ${req.sessionId} is not idle (status: ${session.status})`);
|
|
405
387
|
}
|
|
406
388
|
const conn = adapterManager.getConnection(session.environmentId);
|
|
407
389
|
if (!conn) {
|
|
@@ -420,16 +402,28 @@ export function registerGrackleRoutes(router) {
|
|
|
420
402
|
}
|
|
421
403
|
const conn = adapterManager.getConnection(session.environmentId);
|
|
422
404
|
if (conn) {
|
|
423
|
-
|
|
405
|
+
try {
|
|
406
|
+
await conn.client.kill(create(powerline.SessionIdSchema, { id: req.id }));
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
logger.warn({ sessionId: req.id, err }, "PowerLine kill failed — marking session interrupted anyway");
|
|
410
|
+
}
|
|
424
411
|
}
|
|
425
|
-
sessionStore.updateSession(req.id,
|
|
412
|
+
sessionStore.updateSession(req.id, SESSION_STATUS.INTERRUPTED);
|
|
426
413
|
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
427
414
|
sessionId: req.id,
|
|
428
415
|
type: grackle.EventType.STATUS,
|
|
429
416
|
timestamp: new Date().toISOString(),
|
|
430
|
-
content:
|
|
417
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
431
418
|
raw: "",
|
|
432
419
|
}));
|
|
420
|
+
// Broadcast task_updated so frontend re-fetches computed status
|
|
421
|
+
if (session.taskId) {
|
|
422
|
+
const task = taskStore.getTask(session.taskId);
|
|
423
|
+
if (task) {
|
|
424
|
+
broadcast({ type: "task_updated", payload: { taskId: task.id, projectId: task.projectId } });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
433
427
|
return create(grackle.EmptySchema, {});
|
|
434
428
|
},
|
|
435
429
|
async listSessions(req) {
|
|
@@ -502,7 +496,7 @@ export function registerGrackleRoutes(router) {
|
|
|
502
496
|
}
|
|
503
497
|
// useWorktrees defaults to true when not specified
|
|
504
498
|
const useWorktrees = req.useWorktrees ?? true;
|
|
505
|
-
projectStore.createProject(id, req.name, req.description, req.repoUrl, req.defaultEnvironmentId, useWorktrees);
|
|
499
|
+
projectStore.createProject(id, req.name, req.description, req.repoUrl, req.defaultEnvironmentId, useWorktrees, req.worktreeBasePath ?? "");
|
|
506
500
|
broadcast({ type: "project_created", payload: { projectId: id } });
|
|
507
501
|
const row = projectStore.getProject(id);
|
|
508
502
|
return projectRowToProto(row);
|
|
@@ -523,7 +517,7 @@ export function registerGrackleRoutes(router) {
|
|
|
523
517
|
if (!existing) {
|
|
524
518
|
throw new Error(`Project not found: ${req.id}`);
|
|
525
519
|
}
|
|
526
|
-
if (req.name
|
|
520
|
+
if (req.name?.trim() === "") {
|
|
527
521
|
throw new Error("Project name cannot be empty");
|
|
528
522
|
}
|
|
529
523
|
if (req.repoUrl !== undefined && req.repoUrl !== "" && !/^https?:\/\//i.test(req.repoUrl)) {
|
|
@@ -535,6 +529,7 @@ export function registerGrackleRoutes(router) {
|
|
|
535
529
|
repoUrl: req.repoUrl,
|
|
536
530
|
defaultEnvironmentId: req.defaultEnvironmentId,
|
|
537
531
|
useWorktrees: req.useWorktrees ?? undefined,
|
|
532
|
+
worktreeBasePath: req.worktreeBasePath,
|
|
538
533
|
});
|
|
539
534
|
if (!row) {
|
|
540
535
|
throw new Error(`Project not found after update: ${req.id}`);
|
|
@@ -546,8 +541,21 @@ export function registerGrackleRoutes(router) {
|
|
|
546
541
|
async listTasks(req) {
|
|
547
542
|
const rows = taskStore.listTasks(req.id);
|
|
548
543
|
const childIdsMap = taskStore.buildChildIdsMap(rows);
|
|
544
|
+
// Batch-fetch sessions for all tasks and group by taskId
|
|
545
|
+
const taskIds = rows.map((r) => r.id);
|
|
546
|
+
const allSessions = sessionStore.listSessionsByTaskIds(taskIds);
|
|
547
|
+
const sessionsByTask = new Map();
|
|
548
|
+
for (const s of allSessions) {
|
|
549
|
+
const arr = sessionsByTask.get(s.taskId) ?? [];
|
|
550
|
+
arr.push(s);
|
|
551
|
+
sessionsByTask.set(s.taskId, arr);
|
|
552
|
+
}
|
|
549
553
|
return create(grackle.TaskListSchema, {
|
|
550
|
-
tasks: rows.map((r) =>
|
|
554
|
+
tasks: rows.map((r) => {
|
|
555
|
+
const taskSessions = sessionsByTask.get(r.id) ?? [];
|
|
556
|
+
const { status, latestSessionId } = computeTaskStatus(r.status, taskSessions);
|
|
557
|
+
return taskRowToProto(r, childIdsMap.get(r.id) ?? [], status, latestSessionId);
|
|
558
|
+
}),
|
|
551
559
|
});
|
|
552
560
|
},
|
|
553
561
|
async createTask(req) {
|
|
@@ -567,18 +575,7 @@ export function registerGrackleRoutes(router) {
|
|
|
567
575
|
}
|
|
568
576
|
}
|
|
569
577
|
const id = uuid().slice(0, 8);
|
|
570
|
-
|
|
571
|
-
let environmentId = req.environmentId;
|
|
572
|
-
if (!environmentId && req.parentTaskId) {
|
|
573
|
-
const parent = taskStore.getTask(req.parentTaskId);
|
|
574
|
-
if (parent?.environmentId) {
|
|
575
|
-
environmentId = parent.environmentId;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
if (!environmentId) {
|
|
579
|
-
environmentId = project.defaultEnvironmentId;
|
|
580
|
-
}
|
|
581
|
-
taskStore.createTask(id, req.projectId, req.title, req.description, environmentId, [...req.dependsOn], slugify(project.name), req.parentTaskId, req.canDecompose, req.personaId);
|
|
578
|
+
taskStore.createTask(id, req.projectId, req.title, req.description, [...req.dependsOn], slugify(project.name), req.parentTaskId, req.canDecompose);
|
|
582
579
|
const row = taskStore.getTask(id);
|
|
583
580
|
broadcast({
|
|
584
581
|
type: "task_created",
|
|
@@ -590,7 +587,9 @@ export function registerGrackleRoutes(router) {
|
|
|
590
587
|
const row = taskStore.getTask(req.id);
|
|
591
588
|
if (!row)
|
|
592
589
|
throw new Error(`Task not found: ${req.id}`);
|
|
593
|
-
|
|
590
|
+
const taskSessions = sessionStore.listSessionsForTask(req.id);
|
|
591
|
+
const { status, latestSessionId } = computeTaskStatus(row.status, taskSessions);
|
|
592
|
+
return taskRowToProto(row, undefined, status, latestSessionId);
|
|
594
593
|
},
|
|
595
594
|
async updateTask(req) {
|
|
596
595
|
const existing = taskStore.getTask(req.id);
|
|
@@ -604,16 +603,16 @@ export function registerGrackleRoutes(router) {
|
|
|
604
603
|
}
|
|
605
604
|
reqStatus = converted;
|
|
606
605
|
}
|
|
607
|
-
taskStore.updateTask(req.id, req.title !== "" ? req.title : existing.title, req.description !== "" ? req.description : existing.description, reqStatus, req.
|
|
606
|
+
taskStore.updateTask(req.id, req.title !== "" ? req.title : existing.title, req.description !== "" ? req.description : existing.description, reqStatus, req.dependsOn.length > 0
|
|
608
607
|
? [...req.dependsOn]
|
|
609
|
-
: safeParseJsonArray(existing.dependsOn)
|
|
608
|
+
: safeParseJsonArray(existing.dependsOn));
|
|
610
609
|
// Late-bind: associate an existing session with this task
|
|
611
610
|
if (req.sessionId !== "") {
|
|
612
611
|
const session = sessionStore.getSession(req.sessionId);
|
|
613
612
|
if (!session) {
|
|
614
613
|
throw new ConnectError(`Session not found: ${req.sessionId}`, Code.NotFound);
|
|
615
614
|
}
|
|
616
|
-
const terminalStatuses = [
|
|
615
|
+
const terminalStatuses = [SESSION_STATUS.COMPLETED, SESSION_STATUS.FAILED, SESSION_STATUS.INTERRUPTED];
|
|
617
616
|
if (terminalStatuses.includes(session.status)) {
|
|
618
617
|
throw new ConnectError(`Cannot bind terminal session ${req.sessionId} (status: ${session.status})`, Code.FailedPrecondition);
|
|
619
618
|
}
|
|
@@ -622,24 +621,27 @@ export function registerGrackleRoutes(router) {
|
|
|
622
621
|
throw new ConnectError(`No active event processor for session ${req.sessionId}`, Code.FailedPrecondition);
|
|
623
622
|
}
|
|
624
623
|
sessionStore.setSessionTask(req.sessionId, req.id);
|
|
625
|
-
|
|
626
|
-
taskStore.markTaskStarted(req.id);
|
|
627
|
-
const onComplete = makeTaskCompletionCallback(req.id, req.sessionId, existing.projectId);
|
|
628
|
-
processorRegistry.lateBind(req.sessionId, req.id, existing.projectId, onComplete);
|
|
624
|
+
processorRegistry.lateBind(req.sessionId, req.id, existing.projectId);
|
|
629
625
|
broadcast({
|
|
630
626
|
type: "task_started",
|
|
631
627
|
payload: { taskId: req.id, sessionId: req.sessionId, projectId: existing.projectId },
|
|
632
628
|
});
|
|
633
629
|
}
|
|
634
630
|
const row = taskStore.getTask(req.id);
|
|
635
|
-
|
|
631
|
+
const taskSessions = sessionStore.listSessionsForTask(req.id);
|
|
632
|
+
const { status, latestSessionId } = computeTaskStatus(row.status, taskSessions);
|
|
633
|
+
return taskRowToProto(row, undefined, status, latestSessionId);
|
|
636
634
|
},
|
|
637
635
|
async startTask(req) {
|
|
638
636
|
const task = taskStore.getTask(req.taskId);
|
|
639
637
|
if (!task)
|
|
640
638
|
throw new Error(`Task not found: ${req.taskId}`);
|
|
641
|
-
|
|
642
|
-
|
|
639
|
+
{
|
|
640
|
+
const taskSessions = sessionStore.listSessionsForTask(req.taskId);
|
|
641
|
+
const { status: effectiveStatus } = computeTaskStatus(task.status, taskSessions);
|
|
642
|
+
if (![TASK_STATUS.NOT_STARTED, TASK_STATUS.FAILED].includes(effectiveStatus)) {
|
|
643
|
+
throw new Error(`Task ${req.taskId} cannot be started (status: ${effectiveStatus})`);
|
|
644
|
+
}
|
|
643
645
|
}
|
|
644
646
|
if (!taskStore.areDependenciesMet(req.taskId)) {
|
|
645
647
|
throw new Error(`Task ${req.taskId} has unmet dependencies`);
|
|
@@ -647,14 +649,15 @@ export function registerGrackleRoutes(router) {
|
|
|
647
649
|
const project = projectStore.getProject(task.projectId);
|
|
648
650
|
if (!project)
|
|
649
651
|
throw new Error(`Project not found: ${task.projectId}`);
|
|
650
|
-
const environmentId =
|
|
651
|
-
if (!environmentId)
|
|
652
|
+
const environmentId = req.environmentId || project.defaultEnvironmentId;
|
|
653
|
+
if (!environmentId) {
|
|
652
654
|
throw new Error("No environment specified for task or project");
|
|
655
|
+
}
|
|
653
656
|
const conn = adapterManager.getConnection(environmentId);
|
|
654
657
|
if (!conn)
|
|
655
658
|
throw new Error(`Environment ${environmentId} not connected`);
|
|
656
|
-
// Resolve persona
|
|
657
|
-
const personaId = req.personaId ||
|
|
659
|
+
// Resolve persona from StartTaskRequest
|
|
660
|
+
const personaId = req.personaId || "";
|
|
658
661
|
const persona = personaId
|
|
659
662
|
? personaStore.getPersona(personaId)
|
|
660
663
|
: undefined;
|
|
@@ -673,13 +676,11 @@ export function registerGrackleRoutes(router) {
|
|
|
673
676
|
DEFAULT_MODEL;
|
|
674
677
|
const maxTurns = persona?.maxTurns || 0;
|
|
675
678
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
676
|
-
let systemContext = buildTaskSystemContext(task.title, task.description,
|
|
679
|
+
let systemContext = buildTaskSystemContext(task.title, task.description, req.notes || "", task.canDecompose);
|
|
677
680
|
if (persona) {
|
|
678
681
|
systemContext = persona.systemPrompt + "\n\n" + systemContext;
|
|
679
682
|
}
|
|
680
|
-
sessionStore.createSession(sessionId, environmentId, runtime, task.title, model, logPath, task.id);
|
|
681
|
-
taskStore.setTaskSession(task.id, sessionId);
|
|
682
|
-
taskStore.markTaskStarted(task.id);
|
|
683
|
+
sessionStore.createSession(sessionId, environmentId, runtime, task.title, model, logPath, task.id, personaId);
|
|
683
684
|
broadcast({
|
|
684
685
|
type: "task_started",
|
|
685
686
|
payload: { taskId: task.id, sessionId, projectId: task.projectId },
|
|
@@ -701,7 +702,9 @@ export function registerGrackleRoutes(router) {
|
|
|
701
702
|
model,
|
|
702
703
|
maxTurns,
|
|
703
704
|
branch: task.branch,
|
|
704
|
-
worktreeBasePath: task.branch && useWorktrees
|
|
705
|
+
worktreeBasePath: task.branch && useWorktrees
|
|
706
|
+
? (project.worktreeBasePath || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
707
|
+
: "",
|
|
705
708
|
systemContext,
|
|
706
709
|
projectId: task.projectId,
|
|
707
710
|
taskId: task.id,
|
|
@@ -712,16 +715,15 @@ export function registerGrackleRoutes(router) {
|
|
|
712
715
|
logPath,
|
|
713
716
|
projectId: task.projectId,
|
|
714
717
|
taskId: task.id,
|
|
715
|
-
onComplete: makeTaskCompletionCallback(task.id, sessionId, task.projectId),
|
|
716
718
|
});
|
|
717
719
|
const row = sessionStore.getSession(sessionId);
|
|
718
720
|
return sessionRowToProto(row);
|
|
719
721
|
},
|
|
720
|
-
async
|
|
722
|
+
async completeTask(req) {
|
|
721
723
|
const task = taskStore.getTask(req.id);
|
|
722
724
|
if (!task)
|
|
723
725
|
throw new Error(`Task not found: ${req.id}`);
|
|
724
|
-
taskStore.
|
|
726
|
+
taskStore.markTaskComplete(task.id, TASK_STATUS.COMPLETE);
|
|
725
727
|
// Check for newly unblocked tasks
|
|
726
728
|
const unblocked = taskStore.checkAndUnblock(task.projectId);
|
|
727
729
|
for (const t of unblocked) {
|
|
@@ -738,23 +740,50 @@ export function registerGrackleRoutes(router) {
|
|
|
738
740
|
}));
|
|
739
741
|
}
|
|
740
742
|
broadcast({
|
|
741
|
-
type: "
|
|
743
|
+
type: "task_completed",
|
|
742
744
|
payload: { taskId: task.id, projectId: task.projectId },
|
|
743
745
|
});
|
|
744
746
|
const row = taskStore.getTask(task.id);
|
|
745
|
-
|
|
747
|
+
const taskSessions = sessionStore.listSessionsForTask(task.id);
|
|
748
|
+
const { status, latestSessionId } = computeTaskStatus(row.status, taskSessions);
|
|
749
|
+
return taskRowToProto(row, undefined, status, latestSessionId);
|
|
746
750
|
},
|
|
747
|
-
async
|
|
751
|
+
async resumeTask(req) {
|
|
748
752
|
const task = taskStore.getTask(req.id);
|
|
749
753
|
if (!task)
|
|
750
754
|
throw new Error(`Task not found: ${req.id}`);
|
|
751
|
-
|
|
755
|
+
const latestSession = sessionStore.getLatestSessionForTask(req.id);
|
|
756
|
+
if (!latestSession) {
|
|
757
|
+
throw new Error(`Task ${req.id} has no sessions to resume`);
|
|
758
|
+
}
|
|
759
|
+
if (![SESSION_STATUS.INTERRUPTED, SESSION_STATUS.COMPLETED].includes(latestSession.status)) {
|
|
760
|
+
throw new Error(`Latest session ${latestSession.id} is not resumable (status: ${latestSession.status})`);
|
|
761
|
+
}
|
|
762
|
+
if (!latestSession.runtimeSessionId) {
|
|
763
|
+
throw new Error(`Latest session ${latestSession.id} has no runtime session ID — cannot resume`);
|
|
764
|
+
}
|
|
765
|
+
const conn = adapterManager.getConnection(latestSession.environmentId);
|
|
766
|
+
if (!conn) {
|
|
767
|
+
throw new Error(`Environment ${latestSession.environmentId} not connected`);
|
|
768
|
+
}
|
|
769
|
+
const powerlineReq = create(powerline.ResumeRequestSchema, {
|
|
770
|
+
sessionId: latestSession.id,
|
|
771
|
+
runtimeSessionId: latestSession.runtimeSessionId,
|
|
772
|
+
runtime: latestSession.runtime,
|
|
773
|
+
});
|
|
774
|
+
const logPath = latestSession.logPath || join(grackleHome, LOGS_DIR, latestSession.id);
|
|
775
|
+
processEventStream(conn.client.resume(powerlineReq), {
|
|
776
|
+
sessionId: latestSession.id,
|
|
777
|
+
logPath,
|
|
778
|
+
projectId: task.projectId,
|
|
779
|
+
taskId: task.id,
|
|
780
|
+
});
|
|
752
781
|
broadcast({
|
|
753
|
-
type: "
|
|
754
|
-
payload: { taskId: task.id, projectId: task.projectId },
|
|
782
|
+
type: "task_started",
|
|
783
|
+
payload: { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId },
|
|
755
784
|
});
|
|
756
|
-
const row =
|
|
757
|
-
return
|
|
785
|
+
const row = sessionStore.getSession(latestSession.id);
|
|
786
|
+
return sessionRowToProto(row);
|
|
758
787
|
},
|
|
759
788
|
async deleteTask(req) {
|
|
760
789
|
const task = taskStore.getTask(req.id);
|
|
@@ -765,28 +794,26 @@ export function registerGrackleRoutes(router) {
|
|
|
765
794
|
if (children.length > 0) {
|
|
766
795
|
throw new ConnectError("Cannot delete task with children. Delete children first.", Code.FailedPrecondition);
|
|
767
796
|
}
|
|
768
|
-
// Kill active
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
logger.warn({ taskId: req.id, sessionId: task.sessionId, err }, "Failed to kill session during task deletion");
|
|
779
|
-
}
|
|
797
|
+
// Kill all active sessions before deleting the task
|
|
798
|
+
const activeSessions = sessionStore.getActiveSessionsForTask(req.id);
|
|
799
|
+
for (const activeSession of activeSessions) {
|
|
800
|
+
const conn = adapterManager.getConnection(activeSession.environmentId);
|
|
801
|
+
if (conn) {
|
|
802
|
+
try {
|
|
803
|
+
await conn.client.kill(create(powerline.SessionIdSchema, { id: activeSession.id }));
|
|
804
|
+
}
|
|
805
|
+
catch (err) {
|
|
806
|
+
logger.warn({ taskId: req.id, sessionId: activeSession.id, err }, "Failed to kill session during task deletion");
|
|
780
807
|
}
|
|
781
|
-
sessionStore.updateSession(task.sessionId, "killed");
|
|
782
|
-
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
783
|
-
sessionId: task.sessionId,
|
|
784
|
-
type: grackle.EventType.STATUS,
|
|
785
|
-
timestamp: new Date().toISOString(),
|
|
786
|
-
content: "killed",
|
|
787
|
-
raw: "",
|
|
788
|
-
}));
|
|
789
808
|
}
|
|
809
|
+
sessionStore.updateSession(activeSession.id, SESSION_STATUS.INTERRUPTED);
|
|
810
|
+
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
811
|
+
sessionId: activeSession.id,
|
|
812
|
+
type: grackle.EventType.STATUS,
|
|
813
|
+
timestamp: new Date().toISOString(),
|
|
814
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
815
|
+
raw: "",
|
|
816
|
+
}));
|
|
790
817
|
}
|
|
791
818
|
const changes = taskStore.deleteTask(req.id);
|
|
792
819
|
if (changes === 0) {
|
|
@@ -847,10 +874,8 @@ export function registerGrackleRoutes(router) {
|
|
|
847
874
|
// Only update toolConfig/mcpServers if the request provides non-empty values;
|
|
848
875
|
// otherwise keep the existing stored value.
|
|
849
876
|
const hasNewToolConfig = !!req.toolConfig &&
|
|
850
|
-
(
|
|
851
|
-
req.toolConfig.
|
|
852
|
-
(req.toolConfig.disallowedTools &&
|
|
853
|
-
req.toolConfig.disallowedTools.length > 0));
|
|
877
|
+
(req.toolConfig.allowedTools.length > 0 ||
|
|
878
|
+
req.toolConfig.disallowedTools.length > 0);
|
|
854
879
|
const toolConfigJson = hasNewToolConfig
|
|
855
880
|
? JSON.stringify({
|
|
856
881
|
allowedTools: [...(req.toolConfig?.allowedTools || [])],
|