@grackle-ai/server 0.24.1 → 0.26.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/compute-task-status.d.ts +27 -0
- package/dist/compute-task-status.d.ts.map +1 -0
- package/dist/compute-task-status.js +93 -0
- package/dist/compute-task-status.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +62 -14
- package/dist/db.js.map +1 -1
- package/dist/event-processor.d.ts +15 -1
- package/dist/event-processor.d.ts.map +1 -1
- package/dist/event-processor.js +164 -99
- 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 +4 -5
- package/dist/github-import.js.map +1 -1
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +112 -72
- package/dist/grpc-service.js.map +1 -1
- package/dist/processor-registry.d.ts +25 -0
- package/dist/processor-registry.d.ts.map +1 -0
- package/dist/processor-registry.js +58 -0
- package/dist/processor-registry.js.map +1 -0
- package/dist/schema.d.ts +19 -57
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -3
- package/dist/schema.js.map +1 -1
- package/dist/session-store.d.ts +9 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +46 -2
- package/dist/session-store.js.map +1 -1
- package/dist/task-store.d.ts +6 -7
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +6 -28
- package/dist/task-store.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +138 -104
- package/dist/ws-bridge.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SessionRow } from "./schema.js";
|
|
2
|
+
/** Result of computing a task's effective status from its session history. */
|
|
3
|
+
export interface TaskStatusResult {
|
|
4
|
+
/** The computed effective status string (e.g. "in_progress", "review", "pending"). */
|
|
5
|
+
status: string;
|
|
6
|
+
/** The ID of the most recent session (by startedAt), or empty string if none. */
|
|
7
|
+
latestSessionId: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compute the effective task status from the stored DB status and the task's
|
|
11
|
+
* session history. Pure function — no DB access, no side effects.
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* 1. Human-authoritative statuses ("done") are sticky — always returned as-is.
|
|
15
|
+
* 2. No sessions → return storedStatus unchanged.
|
|
16
|
+
* 3. Any active session (pending/running/waiting_input) → prefer waiting_input > in_progress.
|
|
17
|
+
* 4. Latest terminal session determines status:
|
|
18
|
+
* - completed → "review"
|
|
19
|
+
* - failed → "failed"
|
|
20
|
+
* - killed → "pending" (retryable)
|
|
21
|
+
*
|
|
22
|
+
* @param storedStatus - The task's status as stored in the DB.
|
|
23
|
+
* @param sessions - All sessions for this task, in any order.
|
|
24
|
+
* @returns Computed status and the ID of the latest session.
|
|
25
|
+
*/
|
|
26
|
+
export declare function computeTaskStatus(storedStatus: string, sessions: Pick<SessionRow, "id" | "status" | "startedAt">[]): TaskStatusResult;
|
|
27
|
+
//# sourceMappingURL=compute-task-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compute-task-status.d.ts","sourceRoot":"","sources":["../src/compute-task-status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,sFAAsF;IACtF,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,eAAe,EAAE,MAAM,CAAC;CACzB;AAsBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,QAAQ,GAAG,WAAW,CAAC,EAAE,GAC1D,gBAAgB,CAsDlB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Session statuses that indicate the session is actively running. */
|
|
2
|
+
const ACTIVE_SESSION_STATUSES = new Set([
|
|
3
|
+
"pending",
|
|
4
|
+
"running",
|
|
5
|
+
"waiting_input",
|
|
6
|
+
]);
|
|
7
|
+
/**
|
|
8
|
+
* Human-authoritative task statuses. These are sticky — once a human sets
|
|
9
|
+
* these statuses, session state does not override them.
|
|
10
|
+
*
|
|
11
|
+
* Note: "assigned" is intentionally NOT included. While it is set by humans,
|
|
12
|
+
* it must yield to active session status (e.g. a running session should show
|
|
13
|
+
* "in_progress", not "assigned"). The "assigned" status is only meaningful
|
|
14
|
+
* when there are no sessions.
|
|
15
|
+
*/
|
|
16
|
+
const HUMAN_AUTHORITATIVE_STATUSES = new Set([
|
|
17
|
+
"done",
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Compute the effective task status from the stored DB status and the task's
|
|
21
|
+
* session history. Pure function — no DB access, no side effects.
|
|
22
|
+
*
|
|
23
|
+
* Rules:
|
|
24
|
+
* 1. Human-authoritative statuses ("done") are sticky — always returned as-is.
|
|
25
|
+
* 2. No sessions → return storedStatus unchanged.
|
|
26
|
+
* 3. Any active session (pending/running/waiting_input) → prefer waiting_input > in_progress.
|
|
27
|
+
* 4. Latest terminal session determines status:
|
|
28
|
+
* - completed → "review"
|
|
29
|
+
* - failed → "failed"
|
|
30
|
+
* - killed → "pending" (retryable)
|
|
31
|
+
*
|
|
32
|
+
* @param storedStatus - The task's status as stored in the DB.
|
|
33
|
+
* @param sessions - All sessions for this task, in any order.
|
|
34
|
+
* @returns Computed status and the ID of the latest session.
|
|
35
|
+
*/
|
|
36
|
+
export function computeTaskStatus(storedStatus, sessions) {
|
|
37
|
+
// Human-authoritative statuses are always sticky
|
|
38
|
+
if (HUMAN_AUTHORITATIVE_STATUSES.has(storedStatus)) {
|
|
39
|
+
const latestSessionId = sessions.length > 0
|
|
40
|
+
? getLatestSession(sessions).id
|
|
41
|
+
: "";
|
|
42
|
+
return { status: storedStatus, latestSessionId };
|
|
43
|
+
}
|
|
44
|
+
// No sessions → return stored status, but clamp transient statuses back to
|
|
45
|
+
// "pending" since they should not persist without an active session (e.g.
|
|
46
|
+
// stale rows after migration).
|
|
47
|
+
if (sessions.length === 0) {
|
|
48
|
+
const TRANSIENT_STATUSES = new Set(["in_progress", "waiting_input"]);
|
|
49
|
+
const status = TRANSIENT_STATUSES.has(storedStatus) ? "pending" : storedStatus;
|
|
50
|
+
return { status, latestSessionId: "" };
|
|
51
|
+
}
|
|
52
|
+
// Check for any active sessions
|
|
53
|
+
const activeSessions = sessions.filter((s) => ACTIVE_SESSION_STATUSES.has(s.status));
|
|
54
|
+
if (activeSessions.length > 0) {
|
|
55
|
+
// Prefer waiting_input over in_progress if any session is waiting
|
|
56
|
+
const hasWaitingInput = activeSessions.some((s) => s.status === "waiting_input");
|
|
57
|
+
return {
|
|
58
|
+
status: hasWaitingInput ? "waiting_input" : "in_progress",
|
|
59
|
+
latestSessionId: getLatestSession(sessions).id,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// All sessions are terminal — use the latest one to determine status
|
|
63
|
+
const latest = getLatestSession(sessions);
|
|
64
|
+
let status;
|
|
65
|
+
switch (latest.status) {
|
|
66
|
+
case "completed":
|
|
67
|
+
status = "review";
|
|
68
|
+
break;
|
|
69
|
+
case "failed":
|
|
70
|
+
status = "failed";
|
|
71
|
+
break;
|
|
72
|
+
case "killed":
|
|
73
|
+
status = "pending";
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
status = storedStatus;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
return { status, latestSessionId: latest.id };
|
|
80
|
+
}
|
|
81
|
+
/** Get the most recent session by startedAt (descending), breaking ties by ID. */
|
|
82
|
+
function getLatestSession(sessions) {
|
|
83
|
+
return sessions.reduce((latest, current) => {
|
|
84
|
+
if (current.startedAt > latest.startedAt) {
|
|
85
|
+
return current;
|
|
86
|
+
}
|
|
87
|
+
if (current.startedAt === latest.startedAt && current.id > latest.id) {
|
|
88
|
+
return current;
|
|
89
|
+
}
|
|
90
|
+
return latest;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=compute-task-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compute-task-status.js","sourceRoot":"","sources":["../src/compute-task-status.ts"],"names":[],"mappings":"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"}
|
package/dist/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAMtC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AA4CtC,+EAA+E;AAC/E,wBAAgB,YAAY,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAMtC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AA4CtC,+EAA+E;AAC/E,wBAAgB,YAAY,IAAI,IAAI,CAiRnC;AAKD,yDAAyD;AACzD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,QAAA,MAAM,EAAE,EAAE,qBAAqB,CAAC,OAAO,MAAM,CAAC,GAAG;IAC/C,OAAO,EAAE,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;CACV,CAAC;AAEhC,eAAe,EAAE,CAAC"}
|
package/dist/db.js
CHANGED
|
@@ -69,11 +69,12 @@ export function initDatabase() {
|
|
|
69
69
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
70
70
|
log_path TEXT,
|
|
71
71
|
turns INTEGER NOT NULL DEFAULT 0,
|
|
72
|
-
started_at TEXT NOT NULL DEFAULT (
|
|
72
|
+
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
73
73
|
suspended_at TEXT,
|
|
74
74
|
ended_at TEXT,
|
|
75
75
|
error TEXT,
|
|
76
|
-
task_id TEXT NOT NULL DEFAULT ''
|
|
76
|
+
task_id TEXT NOT NULL DEFAULT '',
|
|
77
|
+
persona_id TEXT NOT NULL DEFAULT ''
|
|
77
78
|
);
|
|
78
79
|
|
|
79
80
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
@@ -101,8 +102,6 @@ export function initDatabase() {
|
|
|
101
102
|
description TEXT NOT NULL DEFAULT '',
|
|
102
103
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
103
104
|
branch TEXT NOT NULL DEFAULT '',
|
|
104
|
-
env_id TEXT NOT NULL DEFAULT '',
|
|
105
|
-
session_id TEXT NOT NULL DEFAULT '',
|
|
106
105
|
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
107
106
|
assigned_at TEXT,
|
|
108
107
|
started_at TEXT,
|
|
@@ -113,8 +112,7 @@ export function initDatabase() {
|
|
|
113
112
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
114
113
|
parent_task_id TEXT NOT NULL DEFAULT '',
|
|
115
114
|
depth INTEGER NOT NULL DEFAULT 0,
|
|
116
|
-
can_decompose INTEGER NOT NULL DEFAULT 0
|
|
117
|
-
persona_id TEXT NOT NULL DEFAULT ''
|
|
115
|
+
can_decompose INTEGER NOT NULL DEFAULT 0
|
|
118
116
|
);
|
|
119
117
|
|
|
120
118
|
CREATE TABLE IF NOT EXISTS findings (
|
|
@@ -171,8 +169,6 @@ export function initDatabase() {
|
|
|
171
169
|
UPDATE tasks SET description = '' WHERE description IS NULL;
|
|
172
170
|
UPDATE tasks SET status = 'pending' WHERE status IS NULL;
|
|
173
171
|
UPDATE tasks SET branch = '' WHERE branch IS NULL;
|
|
174
|
-
UPDATE tasks SET env_id = '' WHERE env_id IS NULL;
|
|
175
|
-
UPDATE tasks SET session_id = '' WHERE session_id IS NULL;
|
|
176
172
|
UPDATE tasks SET depends_on = '[]' WHERE depends_on IS NULL;
|
|
177
173
|
UPDATE tasks SET review_notes = '' WHERE review_notes IS NULL;
|
|
178
174
|
UPDATE tasks SET created_at = datetime('now') WHERE created_at IS NULL;
|
|
@@ -238,14 +234,66 @@ export function initDatabase() {
|
|
|
238
234
|
/* column already exists */
|
|
239
235
|
}
|
|
240
236
|
// Migration: backfill task_id on existing sessions from tasks.session_id.
|
|
241
|
-
//
|
|
237
|
+
// Guard with try/catch since session_id column may have been dropped already.
|
|
238
|
+
try {
|
|
239
|
+
sqlite.exec(`
|
|
240
|
+
UPDATE sessions SET task_id = (
|
|
241
|
+
SELECT id FROM tasks WHERE tasks.session_id = sessions.id LIMIT 1
|
|
242
|
+
) WHERE task_id = '' AND EXISTS (
|
|
243
|
+
SELECT 1 FROM tasks WHERE tasks.session_id = sessions.id
|
|
244
|
+
)
|
|
245
|
+
`);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* tasks.session_id column already dropped */
|
|
249
|
+
}
|
|
250
|
+
// Migration: add persona_id column to sessions if missing
|
|
251
|
+
try {
|
|
252
|
+
sqlite.exec("ALTER TABLE sessions ADD COLUMN persona_id TEXT NOT NULL DEFAULT ''");
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* column already exists */
|
|
256
|
+
}
|
|
257
|
+
// Migration: copy persona_id from tasks to sessions before dropping
|
|
258
|
+
try {
|
|
259
|
+
sqlite.exec(`
|
|
260
|
+
UPDATE sessions SET persona_id = (
|
|
261
|
+
SELECT persona_id FROM tasks WHERE tasks.session_id = sessions.id LIMIT 1
|
|
262
|
+
) WHERE persona_id = '' AND task_id != ''
|
|
263
|
+
`);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
/* tasks.session_id or tasks.persona_id column may not exist */
|
|
267
|
+
}
|
|
268
|
+
// Migration: drop columns that moved off the tasks table
|
|
269
|
+
try {
|
|
270
|
+
sqlite.exec("ALTER TABLE tasks DROP COLUMN session_id");
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
/* column already dropped or never existed */
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
sqlite.exec("ALTER TABLE tasks DROP COLUMN env_id");
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
/* column already dropped or never existed */
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
sqlite.exec("ALTER TABLE tasks DROP COLUMN persona_id");
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
/* column already dropped or never existed */
|
|
286
|
+
}
|
|
287
|
+
// Migration: normalize existing started_at values from SQLite datetime('now')
|
|
288
|
+
// format (YYYY-MM-DD HH:MM:SS) to ISO 8601 (YYYY-MM-DDTHH:MM:SS.000Z) so
|
|
289
|
+
// ordering is consistent with newly inserted rows.
|
|
242
290
|
sqlite.exec(`
|
|
243
|
-
UPDATE sessions
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
SELECT 1 FROM tasks WHERE tasks.session_id = sessions.id
|
|
247
|
-
)
|
|
291
|
+
UPDATE sessions
|
|
292
|
+
SET started_at = replace(started_at, ' ', 'T') || '.000Z'
|
|
293
|
+
WHERE started_at NOT LIKE '%T%'
|
|
248
294
|
`);
|
|
295
|
+
// Index for efficient session-by-task lookups
|
|
296
|
+
sqlite.exec("CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id)");
|
|
249
297
|
}
|
|
250
298
|
// Run init immediately for backwards compatibility — stores import db at module load
|
|
251
299
|
initDatabase();
|
package/dist/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAE5C,MAAM,MAAM,GAAW,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAEtD,IAAI,MAAqC,CAAC;AAC1C,IAAI,CAAC;IACH,MAAM,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,oCAAoC,CAAC,EAAE,CAAC;QACvF,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;YACE,EAAE;YACF,iDAAiD;YACjD,EAAE;YACF,2EAA2E;YAC3E,2EAA2E;YAC3E,qCAAqC;YACrC,EAAE;YACF,wCAAwC;YACxC,EAAE;YACF,4CAA4C;YAC5C,yBAAyB;YACzB,EAAE;YACF,kEAAkE;YAClE,iEAAiE;YACjE,kBAAkB;YAClB,EAAE;YACF,gCAAgC;YAChC,EAAE;SACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,GAAG,CAAC;AACZ,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAEpC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAEnC,+EAA+E;AAC/E,MAAM,UAAU,YAAY;IAC1B,wDAAwD;IACxD,MAAM,CAAC,IAAI,CAAC
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import { powerline } from "@grackle-ai/common";
|
|
2
|
+
import type { ProcessorContext } from "./processor-registry.js";
|
|
2
3
|
/** Options for processing an agent event stream. */
|
|
3
4
|
export interface EventStreamOptions {
|
|
4
5
|
sessionId: string;
|
|
5
6
|
logPath: string;
|
|
6
7
|
projectId?: string;
|
|
7
8
|
taskId?: string;
|
|
8
|
-
onComplete?: () => void;
|
|
9
9
|
onError?: (error: unknown) => void;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Process a finding event, storing it in the finding store and broadcasting.
|
|
13
|
+
* Shared between live event processing and replay on late-bind.
|
|
14
|
+
*/
|
|
15
|
+
export declare function processFindingEvent(ctx: ProcessorContext, content: string, sessionId: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Process a subtask creation event, creating a child task and broadcasting.
|
|
18
|
+
* Shared between live event processing and replay on late-bind.
|
|
19
|
+
*/
|
|
20
|
+
export declare function processSubtaskEvent(ctx: ProcessorContext, content: string, subtaskLocalIdMap: Map<string, string>): void;
|
|
11
21
|
/**
|
|
12
22
|
* Process an async iterable of agent events from a PowerLine spawn or resume stream.
|
|
13
23
|
* Handles event transformation, logging, finding interception, status updates, and cleanup.
|
|
14
24
|
*
|
|
15
25
|
* This function is fire-and-forget: it runs in the background and does not throw.
|
|
16
26
|
* Callers should use `onComplete` and `onError` callbacks for post-processing.
|
|
27
|
+
*
|
|
28
|
+
* Supports late-binding: if a task is associated with the session after the stream starts,
|
|
29
|
+
* the processor registry notifies this function via a bind listener, and pre-association
|
|
30
|
+
* events are replayed from the session log.
|
|
17
31
|
*/
|
|
18
32
|
export declare function processEventStream(events: AsyncIterable<powerline.AgentEvent>, options: EventStreamOptions): void;
|
|
19
33
|
//# sourceMappingURL=event-processor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-processor.d.ts","sourceRoot":"","sources":["../src/event-processor.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,SAAS,EAAmB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"event-processor.d.ts","sourceRoot":"","sources":["../src/event-processor.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,SAAS,EAAmB,MAAM,oBAAoB,CAAC;AAczE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAKhE,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,IAAI,CAiBN;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,gBAAgB,EACrB,OAAO,EAAE,MAAM,EACf,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,IAAI,CAiGN;AAsCD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,UAAU,CAAC,EAC3C,OAAO,EAAE,kBAAkB,GAC1B,IAAI,CAiHN"}
|
package/dist/event-processor.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as logWriter from "./log-writer.js";
|
|
|
7
7
|
import * as findingStore from "./finding-store.js";
|
|
8
8
|
import * as taskStore from "./task-store.js";
|
|
9
9
|
import * as projectStore from "./project-store.js";
|
|
10
|
+
import * as processorRegistry from "./processor-registry.js";
|
|
10
11
|
import { slugify } from "./utils/slugify.js";
|
|
11
12
|
import { writeTranscript } from "./transcript.js";
|
|
12
13
|
import { broadcast } from "./ws-broadcast.js";
|
|
@@ -14,19 +15,162 @@ import { safeParseJsonArray } from "./json-helpers.js";
|
|
|
14
15
|
import { logger } from "./logger.js";
|
|
15
16
|
/** Terminal session statuses that indicate the session has already ended. */
|
|
16
17
|
const TERMINAL_STATUSES = ["completed", "failed", "killed"];
|
|
18
|
+
/**
|
|
19
|
+
* Process a finding event, storing it in the finding store and broadcasting.
|
|
20
|
+
* Shared between live event processing and replay on late-bind.
|
|
21
|
+
*/
|
|
22
|
+
export function processFindingEvent(ctx, content, sessionId) {
|
|
23
|
+
if (!ctx.projectId) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(content);
|
|
28
|
+
const findingId = uuid();
|
|
29
|
+
findingStore.postFinding(findingId, ctx.projectId, ctx.taskId || "", sessionId, data.category || "general", data.title || "Untitled", data.content || "", data.tags || []);
|
|
30
|
+
broadcast({ type: "finding_posted", payload: { projectId: ctx.projectId, findingId } });
|
|
31
|
+
logger.info({ findingId, projectId: ctx.projectId, title: data.title }, "Finding stored");
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.error({ err, projectId: ctx.projectId, taskId: ctx.taskId }, "Failed to store finding");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Process a subtask creation event, creating a child task and broadcasting.
|
|
39
|
+
* Shared between live event processing and replay on late-bind.
|
|
40
|
+
*/
|
|
41
|
+
export function processSubtaskEvent(ctx, content, subtaskLocalIdMap) {
|
|
42
|
+
if (!ctx.taskId) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(content);
|
|
47
|
+
const parentTask = taskStore.getTask(ctx.taskId);
|
|
48
|
+
if (!parentTask) {
|
|
49
|
+
logger.warn({ taskId: ctx.taskId }, "Subtask creation failed: parent task not found");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!parentTask.canDecompose) {
|
|
53
|
+
logger.warn({ taskId: ctx.taskId }, "Subtask creation failed: parent task cannot decompose");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const project = projectStore.getProject(parentTask.projectId);
|
|
57
|
+
if (!project) {
|
|
58
|
+
logger.warn({ projectId: parentTask.projectId }, "Subtask creation failed: project not found");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Validate required fields
|
|
62
|
+
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
63
|
+
const description = typeof data.description === "string" ? data.description.trim() : "";
|
|
64
|
+
if (!title || !description) {
|
|
65
|
+
logger.warn({ taskId: ctx.taskId, rawTitle: data.title, rawDescription: data.description }, "Subtask creation failed: invalid title or description");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Normalize and validate depends_on, local_id, and can_decompose
|
|
69
|
+
const dependsOn = Array.isArray(data.depends_on)
|
|
70
|
+
? data.depends_on.filter((d) => typeof d === "string").map(d => d.trim()).filter(Boolean)
|
|
71
|
+
: [];
|
|
72
|
+
const localId = typeof data.local_id === "string" ? data.local_id.trim() : "";
|
|
73
|
+
const canDecompose = typeof data.can_decompose === "boolean" ? data.can_decompose : false;
|
|
74
|
+
// Resolve depends_on local IDs to real task IDs
|
|
75
|
+
const resolvedDeps = [];
|
|
76
|
+
for (const localDep of dependsOn) {
|
|
77
|
+
const realId = subtaskLocalIdMap.get(localDep);
|
|
78
|
+
if (realId) {
|
|
79
|
+
resolvedDeps.push(realId);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
logger.warn({ localDep, taskId: ctx.taskId }, "Subtask dependency local_id not found, skipping");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const subtaskId = uuid().slice(0, 8);
|
|
86
|
+
taskStore.createTask(subtaskId, parentTask.projectId, title, description, resolvedDeps, slugify(project.name), ctx.taskId, canDecompose);
|
|
87
|
+
// Record the local_id → real ID mapping, detecting duplicates
|
|
88
|
+
if (localId) {
|
|
89
|
+
if (subtaskLocalIdMap.has(localId)) {
|
|
90
|
+
logger.warn({
|
|
91
|
+
localId,
|
|
92
|
+
existingSubtaskId: subtaskLocalIdMap.get(localId),
|
|
93
|
+
newSubtaskId: subtaskId,
|
|
94
|
+
parentTaskId: ctx.taskId,
|
|
95
|
+
}, "Duplicate subtask local_id encountered; keeping existing mapping");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
subtaskLocalIdMap.set(localId, subtaskId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const row = taskStore.getTask(subtaskId);
|
|
102
|
+
broadcast({
|
|
103
|
+
type: "task_created",
|
|
104
|
+
payload: { task: row ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) } : null },
|
|
105
|
+
});
|
|
106
|
+
logger.info({ subtaskId, parentTaskId: ctx.taskId, title }, "Subtask created");
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
logger.error({ err, taskId: ctx.taskId }, "Failed to create subtask");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Replay pre-association events from the session log through finding/subtask interceptors.
|
|
114
|
+
* Called when a session is late-bound to a task. Does not re-publish to streamHub.
|
|
115
|
+
*
|
|
116
|
+
* Note: Uses synchronous readFileSync while the log is written via a buffered WriteStream.
|
|
117
|
+
* Events written very recently may still be in the write buffer and not yet flushed to disk.
|
|
118
|
+
* In practice this is negligible since replay targets events written before the current
|
|
119
|
+
* iteration of the for-await loop, which are already flushed by the time lateBind is called.
|
|
120
|
+
*/
|
|
121
|
+
function replayLoggedEvents(ctx, subtaskLocalIdMap) {
|
|
122
|
+
try {
|
|
123
|
+
const entries = logWriter.readLog(ctx.logPath);
|
|
124
|
+
let findingsReplayed = 0;
|
|
125
|
+
let subtasksReplayed = 0;
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (entry.type === "finding") {
|
|
128
|
+
processFindingEvent(ctx, entry.content, entry.session_id);
|
|
129
|
+
findingsReplayed++;
|
|
130
|
+
}
|
|
131
|
+
else if (entry.type === "subtask_create") {
|
|
132
|
+
processSubtaskEvent(ctx, entry.content, subtaskLocalIdMap);
|
|
133
|
+
subtasksReplayed++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (findingsReplayed > 0 || subtasksReplayed > 0) {
|
|
137
|
+
logger.info({ sessionId: ctx.sessionId, taskId: ctx.taskId, findingsReplayed, subtasksReplayed }, "Replayed pre-association events from session log");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
logger.error({ err, sessionId: ctx.sessionId }, "Failed to replay logged events");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
17
144
|
/**
|
|
18
145
|
* Process an async iterable of agent events from a PowerLine spawn or resume stream.
|
|
19
146
|
* Handles event transformation, logging, finding interception, status updates, and cleanup.
|
|
20
147
|
*
|
|
21
148
|
* This function is fire-and-forget: it runs in the background and does not throw.
|
|
22
149
|
* Callers should use `onComplete` and `onError` callbacks for post-processing.
|
|
150
|
+
*
|
|
151
|
+
* Supports late-binding: if a task is associated with the session after the stream starts,
|
|
152
|
+
* the processor registry notifies this function via a bind listener, and pre-association
|
|
153
|
+
* events are replayed from the session log.
|
|
23
154
|
*/
|
|
24
155
|
export function processEventStream(events, options) {
|
|
25
|
-
const { sessionId, logPath,
|
|
156
|
+
const { sessionId, logPath, onError } = options;
|
|
157
|
+
// Create a mutable context that can be updated via the processor registry
|
|
158
|
+
const ctx = {
|
|
159
|
+
sessionId,
|
|
160
|
+
logPath,
|
|
161
|
+
projectId: options.projectId || "",
|
|
162
|
+
taskId: options.taskId || "",
|
|
163
|
+
};
|
|
164
|
+
/** Maps local_id strings (assigned by the agent) to real task IDs, scoped to this stream. */
|
|
165
|
+
const subtaskLocalIdMap = new Map();
|
|
166
|
+
processorRegistry.register(ctx);
|
|
167
|
+
// Register the bind listener synchronously alongside register() to close the race
|
|
168
|
+
// window where lateBind() could fire between register and the async IIFE starting.
|
|
169
|
+
processorRegistry.onBind(sessionId, () => {
|
|
170
|
+
replayLoggedEvents(ctx, subtaskLocalIdMap);
|
|
171
|
+
});
|
|
26
172
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
27
173
|
(async () => {
|
|
28
|
-
/** Maps local_id strings (assigned by the agent) to real task IDs, scoped to this stream. */
|
|
29
|
-
const subtaskLocalIdMap = new Map();
|
|
30
174
|
try {
|
|
31
175
|
logWriter.initLog(logPath);
|
|
32
176
|
sessionStore.updateSessionStatus(sessionId, "running");
|
|
@@ -41,110 +185,19 @@ export function processEventStream(events, options) {
|
|
|
41
185
|
logWriter.writeEvent(logPath, sessionEvent);
|
|
42
186
|
streamHub.publish(sessionEvent);
|
|
43
187
|
// Intercept finding events and store + broadcast them
|
|
44
|
-
if (event.type === "finding" && projectId) {
|
|
45
|
-
|
|
46
|
-
const data = JSON.parse(event.content);
|
|
47
|
-
const findingId = uuid();
|
|
48
|
-
findingStore.postFinding(findingId, projectId, taskId || "", sessionId, data.category || "general", data.title || "Untitled", data.content || "", data.tags || []);
|
|
49
|
-
broadcast({ type: "finding_posted", payload: { projectId, findingId } });
|
|
50
|
-
logger.info({ findingId, projectId, title: data.title }, "Finding stored");
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
logger.error({ err, projectId, taskId }, "Failed to store finding");
|
|
54
|
-
}
|
|
188
|
+
if (event.type === "finding" && ctx.projectId) {
|
|
189
|
+
processFindingEvent(ctx, event.content, sessionId);
|
|
55
190
|
}
|
|
56
191
|
// Intercept subtask creation events and create child tasks
|
|
57
|
-
if (event.type === "subtask_create" && taskId) {
|
|
58
|
-
|
|
59
|
-
const data = JSON.parse(event.content);
|
|
60
|
-
const parentTask = taskStore.getTask(taskId);
|
|
61
|
-
if (!parentTask) {
|
|
62
|
-
logger.warn({ taskId }, "Subtask creation failed: parent task not found");
|
|
63
|
-
}
|
|
64
|
-
else if (!parentTask.canDecompose) {
|
|
65
|
-
logger.warn({ taskId }, "Subtask creation failed: parent task cannot decompose");
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
const project = projectStore.getProject(parentTask.projectId);
|
|
69
|
-
if (!project) {
|
|
70
|
-
logger.warn({ projectId: parentTask.projectId }, "Subtask creation failed: project not found");
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
// Validate required fields
|
|
74
|
-
const title = typeof data.title === "string" ? data.title.trim() : "";
|
|
75
|
-
const description = typeof data.description === "string" ? data.description.trim() : "";
|
|
76
|
-
if (!title || !description) {
|
|
77
|
-
logger.warn({ taskId, rawTitle: data.title, rawDescription: data.description }, "Subtask creation failed: invalid title or description");
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
// Normalize and validate depends_on, local_id, and can_decompose
|
|
81
|
-
const dependsOn = Array.isArray(data.depends_on)
|
|
82
|
-
? data.depends_on.filter((d) => typeof d === "string").map(d => d.trim()).filter(Boolean)
|
|
83
|
-
: [];
|
|
84
|
-
const localId = typeof data.local_id === "string" ? data.local_id.trim() : "";
|
|
85
|
-
const canDecompose = typeof data.can_decompose === "boolean" ? data.can_decompose : false;
|
|
86
|
-
// Resolve depends_on local IDs to real task IDs
|
|
87
|
-
const resolvedDeps = [];
|
|
88
|
-
for (const localDep of dependsOn) {
|
|
89
|
-
const realId = subtaskLocalIdMap.get(localDep);
|
|
90
|
-
if (realId) {
|
|
91
|
-
resolvedDeps.push(realId);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
logger.warn({ localDep, taskId }, "Subtask dependency local_id not found, skipping");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const subtaskId = uuid().slice(0, 8);
|
|
98
|
-
const environmentId = parentTask.environmentId || project.defaultEnvironmentId;
|
|
99
|
-
taskStore.createTask(subtaskId, parentTask.projectId, title, description, environmentId, resolvedDeps, slugify(project.name), taskId, canDecompose);
|
|
100
|
-
// Record the local_id → real ID mapping, detecting duplicates
|
|
101
|
-
if (localId) {
|
|
102
|
-
if (subtaskLocalIdMap.has(localId)) {
|
|
103
|
-
logger.warn({
|
|
104
|
-
localId,
|
|
105
|
-
existingSubtaskId: subtaskLocalIdMap.get(localId),
|
|
106
|
-
newSubtaskId: subtaskId,
|
|
107
|
-
parentTaskId: taskId,
|
|
108
|
-
}, "Duplicate subtask local_id encountered; keeping existing mapping");
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
subtaskLocalIdMap.set(localId, subtaskId);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const row = taskStore.getTask(subtaskId);
|
|
115
|
-
broadcast({
|
|
116
|
-
type: "task_created",
|
|
117
|
-
payload: { task: row ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) } : null },
|
|
118
|
-
});
|
|
119
|
-
logger.info({ subtaskId, parentTaskId: taskId, title }, "Subtask created");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
logger.error({ err, taskId }, "Failed to create subtask");
|
|
126
|
-
}
|
|
192
|
+
if (event.type === "subtask_create" && ctx.taskId) {
|
|
193
|
+
processSubtaskEvent(ctx, event.content, subtaskLocalIdMap);
|
|
127
194
|
}
|
|
128
195
|
if (event.type === "status") {
|
|
129
196
|
if (event.content === "waiting_input") {
|
|
130
197
|
sessionStore.updateSessionStatus(sessionId, "waiting_input");
|
|
131
|
-
if (taskId) {
|
|
132
|
-
const t = taskStore.getTask(taskId);
|
|
133
|
-
if (t && t.status === "in_progress") {
|
|
134
|
-
taskStore.updateTaskStatus(taskId, "waiting_input");
|
|
135
|
-
broadcast({ type: "task_updated", payload: { taskId, projectId } });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
198
|
}
|
|
139
199
|
else if (event.content === "running") {
|
|
140
200
|
sessionStore.updateSessionStatus(sessionId, "running");
|
|
141
|
-
if (taskId) {
|
|
142
|
-
const t = taskStore.getTask(taskId);
|
|
143
|
-
if (t && t.status === "waiting_input") {
|
|
144
|
-
taskStore.updateTaskStatus(taskId, "in_progress");
|
|
145
|
-
broadcast({ type: "task_updated", payload: { taskId, projectId } });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
201
|
}
|
|
149
202
|
else if (event.content === "completed") {
|
|
150
203
|
sessionStore.updateSession(sessionId, "completed");
|
|
@@ -155,12 +208,21 @@ export function processEventStream(events, options) {
|
|
|
155
208
|
else if (event.content === "killed") {
|
|
156
209
|
sessionStore.updateSession(sessionId, "killed");
|
|
157
210
|
}
|
|
211
|
+
// Broadcast task_updated on status changes so frontend re-fetches computed status.
|
|
212
|
+
// This covers both terminal events (completed/failed/killed) and non-terminal
|
|
213
|
+
// transitions (running, waiting_input) that affect the computed task status.
|
|
214
|
+
if (ctx.taskId && ["completed", "failed", "killed", "running", "waiting_input"].includes(event.content)) {
|
|
215
|
+
broadcast({ type: "task_updated", payload: { taskId: ctx.taskId, projectId: ctx.projectId } });
|
|
216
|
+
}
|
|
158
217
|
}
|
|
159
218
|
}
|
|
160
219
|
// Fallback: if stream ended without a terminal status event, mark completed
|
|
161
220
|
const current = sessionStore.getSession(sessionId);
|
|
162
221
|
if (current && !TERMINAL_STATUSES.includes(current.status)) {
|
|
163
222
|
sessionStore.updateSession(sessionId, "completed");
|
|
223
|
+
if (ctx.taskId) {
|
|
224
|
+
broadcast({ type: "task_updated", payload: { taskId: ctx.taskId, projectId: ctx.projectId } });
|
|
225
|
+
}
|
|
164
226
|
}
|
|
165
227
|
}
|
|
166
228
|
catch (err) {
|
|
@@ -188,14 +250,17 @@ export function processEventStream(events, options) {
|
|
|
188
250
|
}));
|
|
189
251
|
onError?.(err);
|
|
190
252
|
}
|
|
253
|
+
if (ctx.taskId) {
|
|
254
|
+
broadcast({ type: "task_updated", payload: { taskId: ctx.taskId, projectId: ctx.projectId } });
|
|
255
|
+
}
|
|
191
256
|
}
|
|
192
257
|
finally {
|
|
258
|
+
processorRegistry.unregister(sessionId);
|
|
193
259
|
logWriter.endSession(logPath);
|
|
194
260
|
try {
|
|
195
261
|
writeTranscript(logPath);
|
|
196
262
|
}
|
|
197
263
|
catch { /* non-critical */ }
|
|
198
|
-
onComplete?.();
|
|
199
264
|
}
|
|
200
265
|
})();
|
|
201
266
|
}
|