maestro-agent 0.0.1 → 0.0.2
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/README.md +316 -2
- package/bin/maestro.ts +5 -0
- package/dist/maestro +0 -0
- package/dist/web/assets/Connections-DV2Kql1Z.js +1 -0
- package/dist/web/assets/GanttView-CCT_rFpY.js +39 -0
- package/dist/web/assets/Home-BFbUIh2z.js +1 -0
- package/dist/web/assets/HooksCrons-ASM5-jDm.js +1 -0
- package/dist/web/assets/ProjectDetail-KZZi6IAd.js +1 -0
- package/dist/web/assets/Roles-KQ94PG3H.js +4 -0
- package/dist/web/assets/ScheduledTasks-CdJHJpEV.js +1 -0
- package/dist/web/assets/Settings-CTflMta-.js +1 -0
- package/dist/web/assets/Skills-D09W1mwX.js +2 -0
- package/dist/web/assets/Wizard-CW6B0wc3.js +1 -0
- package/dist/web/assets/WorkspaceChat-CthETL_A.js +1 -0
- package/dist/web/assets/WorkspaceDashboard-DTAesQuT.js +1 -0
- package/dist/web/assets/WorkspaceNew-Em4msIKn.js +1 -0
- package/dist/web/assets/WorkspaceProjects-Dxg2BpQy.js +1 -0
- package/dist/web/assets/WorkspaceTasks-C20mnnkP.js +1 -0
- package/dist/web/assets/index-B1k33vcR.js +11 -0
- package/dist/web/assets/index-Bk2hHz7P.css +1 -0
- package/dist/web/assets/index-Ddy5AJwx.js +61 -0
- package/dist/web/assets/useEventStream-DTID465I.js +1 -0
- package/dist/web/index.html +13 -0
- package/package.json +49 -6
- package/src/api/agents.ts +76 -0
- package/src/api/audit.ts +19 -0
- package/src/api/autopilot.ts +73 -0
- package/src/api/chat.ts +801 -0
- package/src/api/chief.ts +84 -0
- package/src/api/config.ts +39 -0
- package/src/api/gantt.ts +72 -0
- package/src/api/hooks.ts +54 -0
- package/src/api/inbox.ts +125 -0
- package/src/api/lark.ts +32 -0
- package/src/api/memory.ts +37 -0
- package/src/api/ops.ts +89 -0
- package/src/api/projects.ts +105 -0
- package/src/api/roles.ts +123 -0
- package/src/api/runtimes.ts +62 -0
- package/src/api/scheduled-tasks.ts +203 -0
- package/src/api/sessions.ts +479 -0
- package/src/api/skills.ts +386 -0
- package/src/api/tasks.ts +457 -0
- package/src/api/telegram.ts +94 -0
- package/src/api/templates.ts +36 -0
- package/src/api/webhooks.ts +20 -0
- package/src/api/workspaces.ts +150 -0
- package/src/bridges/lark/index.ts +213 -0
- package/src/bridges/telegram/index.ts +273 -0
- package/src/bridges/telegram/polling.ts +185 -0
- package/src/chat/index.ts +86 -0
- package/src/chief/index.ts +461 -0
- package/src/core/cli.ts +333 -0
- package/src/core/db.ts +53 -0
- package/src/core/event-bus.ts +33 -0
- package/src/core/index.ts +6 -0
- package/src/core/migrations.ts +303 -0
- package/src/core/router.ts +69 -0
- package/src/core/schema.sql +232 -0
- package/src/core/server.ts +308 -0
- package/src/core/validate.ts +22 -0
- package/src/discovery/index.ts +194 -0
- package/src/gateway/adapters/telegram.ts +148 -0
- package/src/gateway/index.ts +31 -0
- package/src/gateway/manager.ts +176 -0
- package/src/gateway/types.ts +77 -0
- package/src/inbox/index.ts +500 -0
- package/src/ops/artifact-sync.ts +65 -0
- package/src/ops/autopilot.ts +338 -0
- package/src/ops/gc.ts +252 -0
- package/src/ops/index.ts +226 -0
- package/src/ops/project-serial.ts +52 -0
- package/src/ops/role-dispatch.ts +111 -0
- package/src/ops/runtime-scheduler.ts +447 -0
- package/src/ops/task-blocking.ts +65 -0
- package/src/ops/task-deps.ts +37 -0
- package/src/ops/task-workspace.ts +60 -0
- package/src/roles/index.ts +258 -0
- package/src/roles/prompt-assembler.ts +85 -0
- package/src/roles/workspace-role.ts +155 -0
- package/src/scheduler/index.ts +461 -0
- package/src/session/output-parser.ts +75 -0
- package/src/session/realtime-parser.ts +40 -0
- package/src/skills/builtin.ts +155 -0
- package/src/skills/skill-extractor.ts +452 -0
- package/src/skills/skill-md.ts +282 -0
- package/src/transport/http-api.ts +75 -0
- package/src/transport/index.ts +4 -0
- package/src/transport/local-pty.ts +119 -0
- package/src/transport/ssh.ts +176 -0
- package/src/transport/types.ts +20 -0
- package/src/workflows/index.ts +231 -0
- package/index.js +0 -1
- package/maestro-agent-0.0.1.tgz +0 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { appendFileSync, chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { generateId, now } from "../core/db";
|
|
4
|
+
import type { Router } from "../core/router";
|
|
5
|
+
import { body, json } from "../core/router";
|
|
6
|
+
import type { HubContext } from "../core/server";
|
|
7
|
+
import { required } from "../core/validate";
|
|
8
|
+
import { recordAgentExit } from "../ops";
|
|
9
|
+
import { isAtCapacity, notifyNextInQueue } from "../ops/runtime-scheduler";
|
|
10
|
+
import { assemblePrompt } from "../roles/prompt-assembler";
|
|
11
|
+
import { parseTranscript } from "../session/output-parser";
|
|
12
|
+
import { RealtimeParser, type SessionEvent } from "../session/realtime-parser";
|
|
13
|
+
import type { TransportProcess } from "../transport";
|
|
14
|
+
import { HttpApiTransport, SshTransport } from "../transport";
|
|
15
|
+
import { getWorkflowForTask, isActiveTaskStatus, transitionTask } from "../workflows";
|
|
16
|
+
|
|
17
|
+
export interface CreateSessionInput {
|
|
18
|
+
agent_id: string;
|
|
19
|
+
task_id?: string | null;
|
|
20
|
+
cmd?: string;
|
|
21
|
+
args?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RuntimeCommandPlan {
|
|
25
|
+
args: string[];
|
|
26
|
+
closeStdin: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerSessionRoutes(router: Router, ctx: HubContext) {
|
|
30
|
+
router.post("/api/sessions", async (req) => {
|
|
31
|
+
const input = await body<CreateSessionInput>(req);
|
|
32
|
+
const result = await createSession(ctx, input);
|
|
33
|
+
if ("error" in result) return json({ error: result.error }, result.status);
|
|
34
|
+
return json(result.session, 201);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
router.get("/api/sessions/:id", (_req, params) => {
|
|
38
|
+
const row = ctx.db.query("SELECT * FROM session WHERE id = ?").get(params.id);
|
|
39
|
+
if (!row) return json({ error: "Not found" }, 404);
|
|
40
|
+
return json(row);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// GET /api/sessions/:id/output?lines=N - get last N lines of terminal output
|
|
44
|
+
router.get("/api/sessions/:id/output", async (req, params) => {
|
|
45
|
+
const session = ctx.db.query("SELECT transcript_path FROM session WHERE id = ?").get(params.id) as any;
|
|
46
|
+
if (!session) return json({ error: "Not found" }, 404);
|
|
47
|
+
if (!session.transcript_path) return json({ lines: [] });
|
|
48
|
+
try {
|
|
49
|
+
const file = Bun.file(session.transcript_path);
|
|
50
|
+
const size = file.size;
|
|
51
|
+
if (size === 0) return json({ lines: [] });
|
|
52
|
+
const url = new URL(req.url, "http://localhost");
|
|
53
|
+
const maxLines = Math.min(parseInt(url.searchParams.get("lines") || "20", 10), 100);
|
|
54
|
+
// Only read last 32KB to avoid loading entire large transcript
|
|
55
|
+
const tailSize = Math.min(size, 32 * 1024);
|
|
56
|
+
const tailContent = await file.slice(size - tailSize, size).text();
|
|
57
|
+
const allLines = tailContent.split("\n").filter((l) => l.trim().length > 0);
|
|
58
|
+
const lines = allLines.slice(-maxLines);
|
|
59
|
+
return json({ lines });
|
|
60
|
+
} catch {
|
|
61
|
+
return json({ lines: [] });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// POST /api/sessions/:id/input - inject stdin via REST
|
|
66
|
+
router.post("/api/sessions/:id/input", async (req, params) => {
|
|
67
|
+
const { data } = await body<{ data: string }>(req);
|
|
68
|
+
if (!data) return json({ error: "data is required" }, 400);
|
|
69
|
+
const proc = ctx.sessions.get(params.id);
|
|
70
|
+
if (!proc) return json({ error: "Session not found or not running" }, 404);
|
|
71
|
+
if (!proc.stdin) return json({ error: "Session has no stdin" }, 400);
|
|
72
|
+
proc.stdin.write(data);
|
|
73
|
+
return json({ written: data.length });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
router.delete("/api/sessions/:id", (_req, params) => {
|
|
77
|
+
const proc = ctx.sessions.get(params.id);
|
|
78
|
+
if (proc) {
|
|
79
|
+
proc.kill();
|
|
80
|
+
ctx.sessions.delete(params.id);
|
|
81
|
+
}
|
|
82
|
+
const ts = now();
|
|
83
|
+
ctx.db.run("UPDATE session SET status = 'ended', ended_at = ? WHERE id = ?", [ts, params.id]);
|
|
84
|
+
// Release queued session
|
|
85
|
+
const session = ctx.db.query("SELECT agent_id FROM session WHERE id = ?").get(params.id) as any;
|
|
86
|
+
if (session?.agent_id) {
|
|
87
|
+
const agentRow = ctx.db.query("SELECT runtime_id FROM agent WHERE id = ?").get(session.agent_id) as any;
|
|
88
|
+
if (agentRow?.runtime_id) {
|
|
89
|
+
notifyNextInQueue(ctx.sessionQueue, agentRow.runtime_id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return json({ id: params.id, status: "ended" });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function createSession(ctx: HubContext, input: CreateSessionInput): Promise<
|
|
97
|
+
| { session: any }
|
|
98
|
+
| { error: string; status: number }
|
|
99
|
+
> {
|
|
100
|
+
const { agent_id, task_id, cmd, args } = input;
|
|
101
|
+
required({ agent_id }, ["agent_id"]);
|
|
102
|
+
const agent = ctx.db.query("SELECT * FROM agent WHERE id = ?").get(agent_id) as any;
|
|
103
|
+
if (!agent) return { error: "Agent not found", status: 404 };
|
|
104
|
+
|
|
105
|
+
const runtime = ctx.db.query("SELECT * FROM agent_runtime WHERE id = ?").get(agent.runtime_id) as any;
|
|
106
|
+
if (!runtime) return { error: "Runtime not found", status: 404 };
|
|
107
|
+
|
|
108
|
+
// Check capacity with oversubscription factor
|
|
109
|
+
const factor = runtime.oversubscription_factor ?? 1.0;
|
|
110
|
+
if (isAtCapacity(ctx.db, runtime.id, Number(runtime.capacity ?? -1), factor)) {
|
|
111
|
+
return { error: `Runtime capacity exceeded: ${runtime.id}`, status: 409 };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const id = generateId("sess");
|
|
115
|
+
const transcriptDir = join(ctx.hubDir, "transcripts");
|
|
116
|
+
mkdirSync(transcriptDir, { recursive: true });
|
|
117
|
+
const transcriptPath = join(transcriptDir, `${id}.log`);
|
|
118
|
+
|
|
119
|
+
const hasExplicitCommand = Boolean(cmd);
|
|
120
|
+
const command = cmd || runtime.cmd || "echo";
|
|
121
|
+
const commandPlan = args
|
|
122
|
+
? { args, closeStdin: false }
|
|
123
|
+
: (hasExplicitCommand ? { args: [], closeStdin: false } : getDefaultCommandPlan(runtime.type, input.task_id ? assemblePrompt(ctx, { agentId: agent_id, taskId: input.task_id }) : undefined));
|
|
124
|
+
const cwd = resolveWorkdir(ctx, agent, task_id || null);
|
|
125
|
+
|
|
126
|
+
return spawnSessionProcess(ctx, {
|
|
127
|
+
id,
|
|
128
|
+
agent_id,
|
|
129
|
+
task_id: task_id || null,
|
|
130
|
+
runtime,
|
|
131
|
+
command,
|
|
132
|
+
cmdArgs: commandPlan.args,
|
|
133
|
+
closeStdin: commandPlan.closeStdin,
|
|
134
|
+
cwd,
|
|
135
|
+
transcriptPath,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 默认启动参数:无权限限制模式
|
|
140
|
+
function getDefaultCommandPlan(runtimeType: string, prompt?: string): RuntimeCommandPlan {
|
|
141
|
+
switch (runtimeType) {
|
|
142
|
+
case "claude":
|
|
143
|
+
return { args: ["--print", "--dangerously-skip-permissions", ...(prompt ? [prompt] : [])], closeStdin: true };
|
|
144
|
+
case "codex":
|
|
145
|
+
return { args: ["exec", "--dangerously-bypass-approvals-and-sandbox", ...(prompt ? [prompt] : [])], closeStdin: true };
|
|
146
|
+
case "opencode":
|
|
147
|
+
return { args: ["run", "--dangerously-skip-permissions", ...(prompt ? [prompt] : [])], closeStdin: true };
|
|
148
|
+
case "blade":
|
|
149
|
+
return { args: ["--print", "--permission-mode=yolo", ...(prompt ? [prompt] : [])], closeStdin: true };
|
|
150
|
+
default:
|
|
151
|
+
return { args: prompt ? [prompt] : [], closeStdin: true };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveWorkdir(ctx: HubContext, agent: any, taskId?: string | null): string {
|
|
156
|
+
const projectWorkdir = taskId ? resolveProjectWorkdir(ctx, taskId) : null;
|
|
157
|
+
const defaultWorkdir = join(ctx.hubDir, "workspaces", agent.id);
|
|
158
|
+
const dir = projectWorkdir || agent.workdir || defaultWorkdir;
|
|
159
|
+
mkdirSync(dir, { recursive: true });
|
|
160
|
+
if (!agent.workdir && !projectWorkdir) {
|
|
161
|
+
ctx.db.run("UPDATE agent SET workdir = ? WHERE id = ?", [defaultWorkdir, agent.id]);
|
|
162
|
+
}
|
|
163
|
+
return dir;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveProjectWorkdir(ctx: HubContext, taskId: string): string | null {
|
|
167
|
+
const row = ctx.db.query(`
|
|
168
|
+
SELECT project.workdir
|
|
169
|
+
FROM task
|
|
170
|
+
JOIN project ON project.id = task.project_id
|
|
171
|
+
WHERE task.id = ?
|
|
172
|
+
AND project.workdir IS NOT NULL
|
|
173
|
+
AND project.workdir != ''
|
|
174
|
+
LIMIT 1
|
|
175
|
+
`).get(taskId) as any | null;
|
|
176
|
+
return row?.workdir || null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ensureMaestroBin(ctx: HubContext): string {
|
|
180
|
+
const binDir = join(ctx.hubDir, "bin");
|
|
181
|
+
mkdirSync(binDir, { recursive: true });
|
|
182
|
+
const binPath = join(binDir, "maestro");
|
|
183
|
+
const cliPath = join(ctx.workspaceRoot || process.cwd(), "bin", "maestro.ts");
|
|
184
|
+
writeFileSync(binPath, `#!/usr/bin/env sh\nexec bun run "${cliPath}" "$@" </dev/null\n`, "utf-8");
|
|
185
|
+
chmodSync(binPath, 0o755);
|
|
186
|
+
return binDir;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function streamProcessOutput(
|
|
190
|
+
ctx: HubContext,
|
|
191
|
+
sessionId: string,
|
|
192
|
+
stream: ReadableStream<Uint8Array>,
|
|
193
|
+
transcriptPath: string,
|
|
194
|
+
type: "stdout" | "stderr",
|
|
195
|
+
parser?: RealtimeParser,
|
|
196
|
+
) {
|
|
197
|
+
(async () => {
|
|
198
|
+
const reader = stream.getReader();
|
|
199
|
+
while (true) {
|
|
200
|
+
const { done, value } = await reader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
const text = new TextDecoder().decode(value);
|
|
203
|
+
try {
|
|
204
|
+
appendFileSync(transcriptPath, text);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (!isExpectedShutdownError(err)) console.warn("[session] transcript write skipped:", err);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
broadcast(ctx, sessionId, { type, data: text });
|
|
210
|
+
if (parser) parser.feed(text);
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function spawnSessionProcess(
|
|
216
|
+
ctx: HubContext,
|
|
217
|
+
input: {
|
|
218
|
+
id: string;
|
|
219
|
+
agent_id: string;
|
|
220
|
+
task_id: string | null;
|
|
221
|
+
runtime: any;
|
|
222
|
+
command: string;
|
|
223
|
+
cmdArgs: string[];
|
|
224
|
+
closeStdin: boolean;
|
|
225
|
+
cwd: string;
|
|
226
|
+
transcriptPath: string;
|
|
227
|
+
},
|
|
228
|
+
): Promise<{ session: any } | { error: string; status: number }> {
|
|
229
|
+
const ts = now();
|
|
230
|
+
// Assemble four-layer prompt and write to file
|
|
231
|
+
const promptDir = join(ctx.hubDir, "prompts");
|
|
232
|
+
mkdirSync(promptDir, { recursive: true });
|
|
233
|
+
const promptPath = join(promptDir, `${input.id}.md`);
|
|
234
|
+
const promptContent = assemblePrompt(ctx, { agentId: input.agent_id, taskId: input.task_id });
|
|
235
|
+
writeFileSync(promptPath, promptContent, "utf-8");
|
|
236
|
+
|
|
237
|
+
const hubEnv = {
|
|
238
|
+
MAESTRO_HUB_URL: `http://localhost:${ctx.port}`,
|
|
239
|
+
MAESTRO_AGENT_ID: input.agent_id,
|
|
240
|
+
MAESTRO_SESSION_ID: input.id,
|
|
241
|
+
MAESTRO_TASK_ID: input.task_id || "",
|
|
242
|
+
MAESTRO_HUB_DIR: ctx.hubDir,
|
|
243
|
+
MAESTRO_WORKDIR: input.cwd,
|
|
244
|
+
MAESTRO_PROMPT_FILE: promptPath,
|
|
245
|
+
};
|
|
246
|
+
let proc: TransportProcess;
|
|
247
|
+
try {
|
|
248
|
+
proc = await createTransportProcess(ctx, input.runtime, input.command, input.cmdArgs, { cwd: input.cwd, env: hubEnv });
|
|
249
|
+
} catch (err: any) {
|
|
250
|
+
return { error: err.message, status: 502 };
|
|
251
|
+
}
|
|
252
|
+
if (input.closeStdin) {
|
|
253
|
+
(proc.stdin as any)?.end?.();
|
|
254
|
+
(proc.stdin as any)?.close?.();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
ctx.db.run(
|
|
258
|
+
"INSERT INTO session (id, agent_id, task_id, pty_pid, transcript_path, status, started_at) VALUES (?, ?, ?, ?, ?, 'running', ?)",
|
|
259
|
+
[input.id, input.agent_id, input.task_id, proc.pid, input.transcriptPath, ts]
|
|
260
|
+
);
|
|
261
|
+
ctx.db.run("UPDATE agent SET status = 'working', last_active_at = ? WHERE id = ?", [ts, input.agent_id]);
|
|
262
|
+
startTrackedProcess(ctx, input, proc);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
session: { id: input.id, agent_id: input.agent_id, task_id: input.task_id, pid: proc.pid, status: "running", started_at: ts },
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function createTransportProcess(
|
|
270
|
+
ctx: HubContext,
|
|
271
|
+
runtime: any,
|
|
272
|
+
command: string,
|
|
273
|
+
args: string[],
|
|
274
|
+
opts: { cwd: string; env: Record<string, string> },
|
|
275
|
+
): TransportProcess | Promise<TransportProcess> {
|
|
276
|
+
if (runtime.transport === "http-api") {
|
|
277
|
+
if (!runtime.target) throw new Error("HTTP API runtime requires target");
|
|
278
|
+
return new HttpApiTransport({ target: runtime.target }).spawn(command, args, opts);
|
|
279
|
+
}
|
|
280
|
+
if (runtime.transport === "ssh") {
|
|
281
|
+
if (!runtime.target) throw new Error("SSH runtime requires target");
|
|
282
|
+
return new SshTransport({ target: runtime.target }).spawn(command, args, opts);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const maestroBinDir = ensureMaestroBin(ctx);
|
|
286
|
+
const path = process.env.PATH ? `${maestroBinDir}:${process.env.PATH}` : maestroBinDir;
|
|
287
|
+
const proc = Bun.spawn([command, ...args], {
|
|
288
|
+
cwd: opts.cwd,
|
|
289
|
+
env: { ...process.env, PATH: path, ...opts.env },
|
|
290
|
+
stdin: "pipe",
|
|
291
|
+
stdout: "pipe",
|
|
292
|
+
stderr: "pipe",
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
pid: proc.pid,
|
|
296
|
+
stdin: proc.stdin,
|
|
297
|
+
stdout: proc.stdout,
|
|
298
|
+
stderr: proc.stderr,
|
|
299
|
+
exited: proc.exited,
|
|
300
|
+
kill: () => proc.kill(),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function handleSessionEvent(
|
|
305
|
+
ctx: HubContext,
|
|
306
|
+
input: { id: string; agent_id: string; task_id: string | null },
|
|
307
|
+
evt: SessionEvent,
|
|
308
|
+
) {
|
|
309
|
+
if (!input.task_id) return;
|
|
310
|
+
const ts = now();
|
|
311
|
+
|
|
312
|
+
switch (evt.type) {
|
|
313
|
+
case "progress":
|
|
314
|
+
ctx.bus.publish("task.progress", { task_id: input.task_id, session_id: input.id, ...evt });
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case "artifact": {
|
|
318
|
+
const content = `Artifact: ${evt.kind} ${evt.path || ""}`.trim();
|
|
319
|
+
ctx.db.run(
|
|
320
|
+
"INSERT INTO task_thread_item (id, task_id, kind, author, content, ref_id, created_at) VALUES (?, ?, 'artifact', ?, ?, ?, ?)",
|
|
321
|
+
[generateId("ti"), input.task_id, input.agent_id, content, input.id, ts]
|
|
322
|
+
);
|
|
323
|
+
ctx.bus.publish("task.artifact", { task_id: input.task_id, session_id: input.id, ...evt });
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case "request_transition":
|
|
328
|
+
ctx.bus.publish("task.request_transition", {
|
|
329
|
+
task_id: input.task_id,
|
|
330
|
+
session_id: input.id,
|
|
331
|
+
to_status: evt.to_status,
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case "summary":
|
|
336
|
+
ctx.db.run(
|
|
337
|
+
"INSERT INTO task_thread_item (id, task_id, kind, author, content, ref_id, created_at) VALUES (?, ?, 'agent_output', ?, ?, ?, ?)",
|
|
338
|
+
[generateId("ti"), input.task_id, input.agent_id, String(evt.content || "").slice(0, 2000), input.id, ts]
|
|
339
|
+
);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function startTrackedProcess(
|
|
345
|
+
ctx: HubContext,
|
|
346
|
+
input: {
|
|
347
|
+
id: string;
|
|
348
|
+
agent_id: string;
|
|
349
|
+
task_id: string | null;
|
|
350
|
+
transcriptPath: string;
|
|
351
|
+
},
|
|
352
|
+
proc: TransportProcess,
|
|
353
|
+
) {
|
|
354
|
+
ctx.sessions.set(input.id, proc);
|
|
355
|
+
|
|
356
|
+
// Create realtime parser to capture structured JSON-line events from stdout
|
|
357
|
+
const parser = new RealtimeParser();
|
|
358
|
+
if (input.task_id) {
|
|
359
|
+
parser.on("event", (evt: SessionEvent) => {
|
|
360
|
+
handleSessionEvent(ctx, input, evt);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
streamProcessOutput(ctx, input.id, proc.stdout, input.transcriptPath, "stdout", parser);
|
|
365
|
+
streamProcessOutput(ctx, input.id, proc.stderr, input.transcriptPath, "stderr");
|
|
366
|
+
|
|
367
|
+
proc.exited.then((code) => {
|
|
368
|
+
parser.flush();
|
|
369
|
+
const endTs = now();
|
|
370
|
+
ctx.sessions.delete(input.id);
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
ctx.db.run("UPDATE session SET status = 'ended', ended_at = ? WHERE id = ?", [endTs, input.id]);
|
|
374
|
+
ctx.db.run("UPDATE agent SET status = 'idle', last_active_at = ? WHERE id = ?", [endTs, input.agent_id]);
|
|
375
|
+
|
|
376
|
+
// Post-session transcript parse: extract summary and write to task thread
|
|
377
|
+
if (input.task_id) {
|
|
378
|
+
try {
|
|
379
|
+
const transcript = readFileSync(input.transcriptPath, "utf-8");
|
|
380
|
+
const parsed = parseTranscript(transcript);
|
|
381
|
+
if (parsed.summary) {
|
|
382
|
+
ctx.db.run(
|
|
383
|
+
"INSERT INTO task_thread_item (id, task_id, kind, author, content, ref_id, created_at) VALUES (?, ?, 'agent_output', ?, ?, ?, ?)",
|
|
384
|
+
[generateId("ti"), input.task_id, input.agent_id, parsed.summary, input.id, endTs]
|
|
385
|
+
);
|
|
386
|
+
ctx.bus.publish("task.agent_output", { task_id: input.task_id, session_id: input.id, summary: parsed.summary });
|
|
387
|
+
}
|
|
388
|
+
} catch { /* transcript read failure is non-fatal */ }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const exit = recordAgentExit(ctx.db, { agentId: input.agent_id, sessionId: input.id, code });
|
|
392
|
+
if (exit.restart) {
|
|
393
|
+
for (let i = exit.attempts; i < 3; i++) {
|
|
394
|
+
recordAgentExit(ctx.db, { agentId: input.agent_id, sessionId: input.id, code });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
recordMissingTaskTransition(ctx, input, code, endTs);
|
|
398
|
+
ctx.bus.publish("session.ended", { id: input.id, agent_id: input.agent_id, code });
|
|
399
|
+
|
|
400
|
+
// Release next queued session waiting for this runtime
|
|
401
|
+
const agentRow = ctx.db.query("SELECT runtime_id FROM agent WHERE id = ?").get(input.agent_id) as any;
|
|
402
|
+
if (agentRow?.runtime_id) {
|
|
403
|
+
notifyNextInQueue(ctx.sessionQueue, agentRow.runtime_id);
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (!isExpectedShutdownError(err)) console.warn("[session] exit handling skipped:", err);
|
|
407
|
+
}
|
|
408
|
+
broadcast(ctx, input.id, { type: "exit", code });
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isExpectedShutdownError(err: unknown) {
|
|
413
|
+
const code = (err as any)?.code;
|
|
414
|
+
return code === "ENOENT" || code === "SQLITE_IOERR_VNODE" || code === "SQLITE_IOERR";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function recordMissingTaskTransition(
|
|
418
|
+
ctx: HubContext,
|
|
419
|
+
input: {
|
|
420
|
+
id: string;
|
|
421
|
+
agent_id: string;
|
|
422
|
+
task_id: string | null;
|
|
423
|
+
},
|
|
424
|
+
code: number | null,
|
|
425
|
+
ts: number,
|
|
426
|
+
) {
|
|
427
|
+
if (!input.task_id) return;
|
|
428
|
+
const task = ctx.db.query("SELECT * FROM task WHERE id = ?").get(input.task_id) as any | null;
|
|
429
|
+
if (!task) return;
|
|
430
|
+
if (task.assignee_agent_id !== input.agent_id) return;
|
|
431
|
+
if (!isActiveTaskStatus(ctx.db, task)) return;
|
|
432
|
+
const transitioned = ctx.db.query(
|
|
433
|
+
"SELECT id FROM task_thread_item WHERE task_id = ? AND kind = 'workflow_transition' AND created_at >= (SELECT started_at FROM session WHERE id = ?) LIMIT 1",
|
|
434
|
+
).get(input.task_id, input.id);
|
|
435
|
+
if (transitioned) return;
|
|
436
|
+
|
|
437
|
+
// If agent exited successfully (code 0), auto-complete the task
|
|
438
|
+
// Only auto-complete if the session ran for a meaningful duration (>5s)
|
|
439
|
+
// to avoid treating instant exits (startup failures) as "work completed"
|
|
440
|
+
if (code === 0) {
|
|
441
|
+
const session = ctx.db.query("SELECT started_at FROM session WHERE id = ?").get(input.id) as any;
|
|
442
|
+
const sessionDuration = session ? ts - session.started_at : 0;
|
|
443
|
+
if (sessionDuration > 5000) {
|
|
444
|
+
const workflow = getWorkflowForTask(ctx.db, input.task_id);
|
|
445
|
+
// Find an action that leads to the "done" status from current status
|
|
446
|
+
const completeAction = workflow.actions.find(
|
|
447
|
+
(a) => a.from.includes(task.status) && a.to === workflow.done_status
|
|
448
|
+
);
|
|
449
|
+
if (completeAction) {
|
|
450
|
+
// Provide default values for required fields so auto-complete doesn't fail validation
|
|
451
|
+
const defaults: Record<string, string> = {};
|
|
452
|
+
for (const field of completeAction.requires || []) {
|
|
453
|
+
defaults[field] = "Auto-completed: agent session exited successfully";
|
|
454
|
+
}
|
|
455
|
+
const result = transitionTask(ctx, input.task_id, completeAction.id, defaults, { actorId: input.agent_id });
|
|
456
|
+
if (result.ok) return; // Successfully auto-completed
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Fallback: record diagnostic (non-zero exit or no valid transition found)
|
|
462
|
+
const content = code === 0
|
|
463
|
+
? `Session ${input.id} ended without a workflow transition. The agent likely finished or stopped before running maestro inbox task transition.`
|
|
464
|
+
: `Session ${input.id} exited with code ${code ?? "unknown"} before a workflow transition.`;
|
|
465
|
+
ctx.db.run(
|
|
466
|
+
"INSERT INTO task_thread_item (id, task_id, kind, author, content, ref_id, created_at) VALUES (?, ?, 'session_diagnostic', 'system', ?, ?, ?)",
|
|
467
|
+
[generateId("ti"), input.task_id, content, input.id, ts],
|
|
468
|
+
);
|
|
469
|
+
ctx.bus.publish("task.session_diagnostic", { task_id: input.task_id, session_id: input.id, agent_id: input.agent_id, code });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function broadcast(ctx: HubContext, sessionId: string, payload: unknown) {
|
|
473
|
+
const clients = ctx.wsClients.get(sessionId);
|
|
474
|
+
if (!clients) return;
|
|
475
|
+
const msg = JSON.stringify(payload);
|
|
476
|
+
for (const ws of clients) {
|
|
477
|
+
ws.send(msg);
|
|
478
|
+
}
|
|
479
|
+
}
|