@peanut996/acp-router 0.8.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/jobs.js ADDED
@@ -0,0 +1,716 @@
1
+ import path from "node:path";
2
+ import { MAX_RECURSION_DEPTH, LOG_DIR, ACTIVE_JOB_STATUSES, TERMINAL_JOB_STATUSES, SERVER_NAME, SERVER_VERSION, COMMAND_TIMEOUT_MS } from "./constants.js";
3
+ import { safeEnv, clampInteger, preview, hashText, isPlainObject, createId, resolveBooleanOverride } from "./utils.js";
4
+ import { readConfig, readRegistry, writeRegistry, readJobEventLog, readLogTail, recordJobProcess, normalizeProcessInfo, appendJsonl } from "./storage.js";
5
+ import { discoverAgents, chooseAgent, validateWorktree, collectWorktreeState, isAcpRunReady, buildAcpUnavailableError, planLaunch, resolveAcpLaunchTarget } from "./agents.js";
6
+ import { AcpStdioClient, runAcpStdioJob } from "./acp-client.js";
7
+ const ACTIVE_RUNS = new Map();
8
+ async function createJob(args) {
9
+ const recursionDepth = Number.parseInt(process.env.ACP_ROUTER_DEPTH ?? "0", 10) || 0;
10
+ if (recursionDepth >= MAX_RECURSION_DEPTH) {
11
+ return {
12
+ status: "failed",
13
+ error: "recursion_limit",
14
+ message: `Agent Router recursion limit reached (depth=${recursionDepth}). This prevents infinite agent dispatch loops.`
15
+ };
16
+ }
17
+ const worktreeCheck = await validateWorktree(args.worktree);
18
+ if (!worktreeCheck.ok) {
19
+ return {
20
+ status: "failed",
21
+ error: worktreeCheck.reason,
22
+ message: "V1 requires an existing absolute worktree path before dispatching an external agent."
23
+ };
24
+ }
25
+ const config = await readConfig();
26
+ const registry = await readRegistry();
27
+ const mode = args.mode ?? "implementation";
28
+ const permissionProfile = args.permissionProfile ?? config.safety.defaultPermissionProfile;
29
+ if (permissionProfile === "bypassPermissions" && !config.safety.allowBypassPermissions) {
30
+ return {
31
+ status: "failed",
32
+ error: "bypassPermissions_disabled",
33
+ message: "The Agent Router config does not allow bypassPermissions by default."
34
+ };
35
+ }
36
+ const availableAgents = await discoverAgents({ includeNotInstalled: false }).then((value) => value.agents);
37
+ const selected = args.agent
38
+ ? { agentId: args.agent, reason: "agent explicitly requested" }
39
+ : chooseAgent(availableAgents, config, mode);
40
+ if (!selected.agentId) {
41
+ return {
42
+ status: "failed",
43
+ error: "no_available_agent",
44
+ message: "No available agent was configured or discovered. Use discover_coding_agents first."
45
+ };
46
+ }
47
+ const selectedAgent = availableAgents.find((agent) => agent.id === selected.agentId);
48
+ if (!selectedAgent || selectedAgent.status !== "available") {
49
+ return {
50
+ status: "failed",
51
+ error: "agent_unavailable",
52
+ agentId: selected.agentId,
53
+ message: "The requested agent is not currently available. Use discover_coding_agents to inspect status."
54
+ };
55
+ }
56
+ const activeConflict = findActiveWorktreeJob(registry, args.worktree, permissionProfile);
57
+ if (activeConflict) {
58
+ return {
59
+ status: "failed",
60
+ error: "worktree_locked",
61
+ jobId: activeConflict.jobId,
62
+ message: "Another writable Agent Router job is already active for this worktree."
63
+ };
64
+ }
65
+ const now = new Date().toISOString();
66
+ const jobId = createId("job");
67
+ const sessionId = args.sessionId || createId(`sess_${selected.agentId}`);
68
+ const logPath = path.join(LOG_DIR, `${jobId}.jsonl`);
69
+ const worktreeState = args.collectDiff === false
70
+ ? { skipped: true, reason: "collectDiff disabled" }
71
+ : await collectWorktreeState(args.worktree);
72
+ const launchingEnabled = resolveBooleanOverride(args.launchExternalAgents, config.safety.launchExternalAgents);
73
+ const inheritEnvironment = resolveBooleanOverride(args.inheritEnvironment, config.safety.inheritEnvironment);
74
+ const agentEnv = safeEnv({ inheritEnvironment });
75
+ const asyncRequested = args.async !== false;
76
+ if (launchingEnabled && !isAcpRunReady(selectedAgent)) {
77
+ return {
78
+ status: "failed",
79
+ error: "acp_required",
80
+ agentId: selected.agentId,
81
+ message: buildAcpUnavailableError(selectedAgent)
82
+ };
83
+ }
84
+ const launchPlan = planLaunch({ launchingEnabled, selectedAgent });
85
+ const adapterStatus = launchPlan.adapterStatus;
86
+ const initialStatus = launchPlan.runnable ? "running" : launchPlan.status;
87
+ const initialSummary = launchPlan.summary;
88
+ const initialRisks = launchPlan.risks;
89
+ const recentEvents = [
90
+ {
91
+ type: "job_created",
92
+ timestamp: now,
93
+ message: "Job recorded in local Agent Router registry."
94
+ },
95
+ {
96
+ type: adapterStatus,
97
+ timestamp: now,
98
+ message: initialSummary
99
+ }
100
+ ];
101
+ const session = registry.sessions[sessionId] ?? {
102
+ sessionId,
103
+ providerSessionId: null,
104
+ agentId: selected.agentId,
105
+ title: preview(args.prompt, 60),
106
+ status: "idle",
107
+ worktree: args.worktree,
108
+ createdAt: now,
109
+ updatedAt: now,
110
+ lastJobId: null,
111
+ source: "dispatcher_registry",
112
+ canContinue: true
113
+ };
114
+ const job = {
115
+ jobId,
116
+ sessionId,
117
+ agentId: selected.agentId,
118
+ status: initialStatus,
119
+ worktree: args.worktree,
120
+ mode,
121
+ permissionProfile,
122
+ collectDiff: args.collectDiff !== false,
123
+ promptPreview: preview(args.prompt, 160),
124
+ promptHash: await hashText(args.prompt),
125
+ startedAt: now,
126
+ endedAt: initialStatus === "running" ? null : now,
127
+ timeoutSec: args.timeoutSec ?? 3600,
128
+ metadata: isPlainObject(args.metadata) ? args.metadata : {},
129
+ recursionDepth,
130
+ resultSummary: initialSummary,
131
+ changedFiles: [],
132
+ validation: [],
133
+ risks: initialRisks,
134
+ logPath,
135
+ adapterStatus,
136
+ launchExternalAgents: launchingEnabled,
137
+ inheritEnvironment,
138
+ selectionReason: selected.reason,
139
+ worktreeState,
140
+ recentEvents
141
+ };
142
+ session.updatedAt = now;
143
+ session.lastJobId = jobId;
144
+ registry.sessions[sessionId] = session;
145
+ registry.jobs[jobId] = job;
146
+ await writeRegistry(registry);
147
+ await appendJsonl(logPath, recentEvents.map((event) => ({ ...event, jobId, sessionId, agentId: selected.agentId })));
148
+ if (launchPlan.runnable) {
149
+ const runRequest = {
150
+ args,
151
+ job,
152
+ session,
153
+ selectedAgent,
154
+ timeoutSec: args.timeoutSec ?? 3600,
155
+ agentEnv,
156
+ launchKind: launchPlan.kind
157
+ };
158
+ if (asyncRequested) {
159
+ startBackgroundJobRun(runRequest);
160
+ }
161
+ else {
162
+ await executeAndPersistJobRun(runRequest);
163
+ const updatedRegistry = await readRegistry();
164
+ Object.assign(job, updatedRegistry.jobs[jobId] ?? job);
165
+ Object.assign(session, updatedRegistry.sessions[sessionId] ?? session);
166
+ }
167
+ }
168
+ return {
169
+ jobId,
170
+ sessionId,
171
+ agentId: selected.agentId,
172
+ status: job.status,
173
+ worktree: args.worktree,
174
+ startedAt: now,
175
+ endedAt: job.endedAt,
176
+ summary: job.resultSummary,
177
+ changedFiles: job.changedFiles,
178
+ validation: job.validation,
179
+ risks: job.risks,
180
+ logPath,
181
+ worktreeState: job.worktreeState,
182
+ adapterStatus: job.adapterStatus,
183
+ providerSessionId: session.providerSessionId,
184
+ stopReason: job.stopReason,
185
+ failureReason: job.failureReason ?? null,
186
+ agentErrors: job.agentErrors ?? [],
187
+ availableModels: job.availableModels ?? session.availableModels ?? [],
188
+ agentConfigOptions: job.agentConfigOptions ?? session.agentConfigOptions ?? [],
189
+ launchExternalAgents: job.launchExternalAgents,
190
+ inheritEnvironment: job.inheritEnvironment,
191
+ selectionReason: selected.reason,
192
+ message: `${selected.agentId} job recorded by Agent Router alpha. Use get_coding_agent_job to inspect it.`
193
+ };
194
+ }
195
+ function startBackgroundJobRun(runRequest) {
196
+ executeAndPersistJobRun(runRequest).catch(async (error) => {
197
+ await markJobRunCrashed(runRequest, error);
198
+ });
199
+ }
200
+ async function executeAndPersistJobRun({ args, job, session, selectedAgent, timeoutSec, agentEnv, launchKind }) {
201
+ const controller = createRunController(job.jobId);
202
+ ACTIVE_RUNS.set(job.jobId, controller);
203
+ try {
204
+ if (launchKind !== "acp_stdio") {
205
+ throw new Error(`Unsupported launch kind: ${launchKind}. ACP is required and CLI fallback has been removed.`);
206
+ }
207
+ const runResult = await runAcpStdioJob({
208
+ args,
209
+ job,
210
+ session,
211
+ selectedAgent,
212
+ timeoutSec,
213
+ agentEnv,
214
+ controller
215
+ });
216
+ await persistJobRunResult({ job, session, selectedAgent, runResult });
217
+ }
218
+ finally {
219
+ ACTIVE_RUNS.delete(job.jobId);
220
+ }
221
+ }
222
+ function createRunController(jobId) {
223
+ return {
224
+ jobId,
225
+ cancelRequested: false,
226
+ cancelReason: null,
227
+ cancelProcess: null,
228
+ processInfo: null,
229
+ async recordProcess(processInfo) {
230
+ const normalized = normalizeProcessInfo(processInfo);
231
+ if (!normalized)
232
+ return;
233
+ this.processInfo = normalized;
234
+ await recordJobProcess(this.jobId, normalized);
235
+ },
236
+ cancel(reason) {
237
+ this.cancelRequested = true;
238
+ this.cancelReason = reason || "Cancelled by Agent Router caller.";
239
+ if (typeof this.cancelProcess === "function") {
240
+ return Boolean(this.cancelProcess());
241
+ }
242
+ return false;
243
+ }
244
+ };
245
+ }
246
+ async function persistJobRunResult({ job, session, selectedAgent, runResult }) {
247
+ const registry = await readRegistry();
248
+ const currentJob = registry.jobs[job.jobId] ?? job;
249
+ const currentSession = registry.sessions[session.sessionId] ?? session;
250
+ const jobPatch = currentJob.status === "cancelled" && runResult.jobPatch.status !== "cancelled"
251
+ ? {
252
+ ...runResult.jobPatch,
253
+ status: "cancelled",
254
+ endedAt: currentJob.endedAt ?? runResult.jobPatch.endedAt,
255
+ resultSummary: currentJob.resultSummary ?? "Cancelled by Agent Router caller.",
256
+ risks: currentJob.risks ?? []
257
+ }
258
+ : runResult.jobPatch;
259
+ const processRecord = isPlainObject(currentJob.process) ? { ...currentJob.process } : null;
260
+ Object.assign(currentJob, jobPatch);
261
+ if (processRecord) {
262
+ currentJob.process = {
263
+ ...processRecord,
264
+ status: currentJob.status,
265
+ endedAt: processRecord.endedAt ?? currentJob.endedAt ?? new Date().toISOString()
266
+ };
267
+ }
268
+ Object.assign(currentSession, runResult.sessionPatch);
269
+ currentJob.recentEvents = [...(currentJob.recentEvents ?? []), ...runResult.events];
270
+ currentSession.updatedAt = currentJob.endedAt;
271
+ currentSession.lastJobId = currentJob.jobId;
272
+ registry.jobs[currentJob.jobId] = currentJob;
273
+ registry.sessions[currentSession.sessionId] = currentSession;
274
+ await writeRegistry(registry);
275
+ }
276
+ async function markJobRunCrashed(runRequest, error) {
277
+ ACTIVE_RUNS.delete(runRequest.job.jobId);
278
+ const failedAt = new Date().toISOString();
279
+ const message = `Agent Router runner crashed: ${error.message}`;
280
+ const runResult = {
281
+ events: [
282
+ {
283
+ type: "dispatcher_runner_error",
284
+ timestamp: failedAt,
285
+ message,
286
+ errorMessage: error.message
287
+ }
288
+ ],
289
+ sessionPatch: {
290
+ status: "idle",
291
+ canContinue: Boolean(runRequest.session.providerSessionId)
292
+ },
293
+ jobPatch: {
294
+ status: "failed",
295
+ endedAt: failedAt,
296
+ failureReason: message,
297
+ resultSummary: message,
298
+ risks: ["Inspect the job log before re-running the agent."]
299
+ }
300
+ };
301
+ await persistJobRunResult({
302
+ job: runRequest.job,
303
+ session: runRequest.session,
304
+ selectedAgent: runRequest.selectedAgent,
305
+ runResult
306
+ });
307
+ }
308
+ async function listJobs(args) {
309
+ const registry = await readRegistry();
310
+ const limit = args.limit ?? 50;
311
+ const jobs = Object.values(registry.jobs)
312
+ .filter((job) => !args.status || job.status === args.status)
313
+ .filter((job) => !args.agent || job.agentId === args.agent)
314
+ .filter((job) => !args.worktree || job.worktree === args.worktree)
315
+ .sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)))
316
+ .slice(0, limit);
317
+ return { jobs };
318
+ }
319
+ async function getJob(args) {
320
+ const registry = await readRegistry();
321
+ const job = registry.jobs[args.jobId];
322
+ if (!job)
323
+ return { jobId: args.jobId, status: "not_found" };
324
+ return { job };
325
+ }
326
+ async function tailJobEvents(args) {
327
+ const registry = await readRegistry();
328
+ const job = registry.jobs[args.jobId];
329
+ if (!job) {
330
+ return {
331
+ jobId: args.jobId,
332
+ status: "not_found",
333
+ events: [],
334
+ nextEventIndex: null,
335
+ hasMore: false,
336
+ note: "No Agent Router job exists for this jobId."
337
+ };
338
+ }
339
+ const limit = clampInteger(args.limit, 50, 1, 200);
340
+ const afterEventIndex = Number.isInteger(args.afterEventIndex) ? args.afterEventIndex : null;
341
+ const startIndex = afterEventIndex == null ? 0 : afterEventIndex + 1;
342
+ const eventLog = await readJobEventLog(job.logPath);
343
+ const totalEvents = eventLog.events.length;
344
+ const events = eventLog.events.slice(startIndex, startIndex + limit);
345
+ const lastReturned = events.length > 0
346
+ ? events[events.length - 1].eventIndex
347
+ : afterEventIndex;
348
+ const result = {
349
+ jobId: job.jobId,
350
+ status: job.status,
351
+ agentId: job.agentId,
352
+ sessionId: job.sessionId,
353
+ adapterStatus: job.adapterStatus ?? null,
354
+ providerSessionId: registry.sessions[job.sessionId]?.providerSessionId ?? null,
355
+ failureReason: job.failureReason ?? null,
356
+ changedFiles: Array.isArray(job.changedFiles) ? job.changedFiles : [],
357
+ risks: Array.isArray(job.risks) ? job.risks : [],
358
+ startedAt: job.startedAt ?? null,
359
+ endedAt: job.endedAt ?? null,
360
+ logPath: job.logPath ?? null,
361
+ events,
362
+ nextEventIndex: lastReturned,
363
+ hasMore: startIndex + events.length < totalEvents,
364
+ totalEventCount: totalEvents
365
+ };
366
+ if (eventLog.note)
367
+ result.note = eventLog.note;
368
+ if (eventLog.parseErrors.length > 0)
369
+ result.parseErrors = eventLog.parseErrors;
370
+ if (args.includeLogTail === true) {
371
+ result.logTail = await readLogTail(job.logPath, clampInteger(args.logTailBytes, 8192, 1, 65536));
372
+ }
373
+ return result;
374
+ }
375
+ async function cancelJob(args) {
376
+ const registry = await readRegistry();
377
+ const job = registry.jobs[args.jobId];
378
+ if (!job)
379
+ return { jobId: args.jobId, status: "not_found" };
380
+ let activeProcessCancelled = false;
381
+ let activeProcessInfo = null;
382
+ if (!TERMINAL_JOB_STATUSES.has(job.status)) {
383
+ const activeRun = ACTIVE_RUNS.get(job.jobId);
384
+ if (activeRun) {
385
+ activeProcessInfo = activeRun.processInfo;
386
+ activeProcessCancelled = activeRun.cancel(args.reason || "Cancelled by Agent Router caller.");
387
+ }
388
+ job.status = "cancelled";
389
+ job.endedAt = new Date().toISOString();
390
+ job.resultSummary = args.reason || "Cancelled by Agent Router caller.";
391
+ if (isPlainObject(job.process) || isPlainObject(activeProcessInfo)) {
392
+ job.process = {
393
+ ...(isPlainObject(job.process) ? job.process : {}),
394
+ ...(isPlainObject(activeProcessInfo) ? activeProcessInfo : {}),
395
+ status: "cancelled",
396
+ killSignal: "SIGTERM",
397
+ killRequestedAt: job.endedAt,
398
+ killStatus: activeProcessCancelled ? "signal_sent" : "not_owned_by_current_server"
399
+ };
400
+ }
401
+ job.recentEvents = [
402
+ ...(job.recentEvents ?? []),
403
+ {
404
+ type: "cancelled",
405
+ timestamp: job.endedAt,
406
+ message: args.reason || "Cancelled by Agent Router caller."
407
+ }
408
+ ];
409
+ await writeRegistry(registry);
410
+ await appendJsonl(job.logPath ?? "", job.recentEvents.slice(-1).map((event) => ({ ...event, jobId: job.jobId, sessionId: job.sessionId, agentId: job.agentId })));
411
+ }
412
+ return { jobId: job.jobId, status: job.status, activeProcessCancelled };
413
+ }
414
+ async function listSessions(args) {
415
+ const registry = await readRegistry();
416
+ const config = await readConfig();
417
+ const limit = args.limit ?? 50;
418
+ const localSessions = Object.values(registry.sessions)
419
+ .filter((session) => args.includeArchived || session.status !== "archived")
420
+ .filter((session) => !args.agent || session.agentId === args.agent)
421
+ .filter((session) => !args.worktree || session.worktree === args.worktree)
422
+ .map(compactSessionForList);
423
+ const nativeResult = await maybeListNativeSessions({ args, config, registry });
424
+ const sessions = mergeSessionLists({
425
+ localSessions,
426
+ nativeSessions: nativeResult.sessions,
427
+ limit
428
+ });
429
+ return {
430
+ sessions,
431
+ nativeSessionList: nativeResult.meta
432
+ };
433
+ }
434
+ function compactSessionForList(session) {
435
+ return {
436
+ sessionId: session.sessionId,
437
+ providerSessionId: session.providerSessionId ?? null,
438
+ agentId: session.agentId,
439
+ title: session.title,
440
+ status: session.status,
441
+ worktree: session.worktree,
442
+ createdAt: session.createdAt,
443
+ updatedAt: session.updatedAt,
444
+ lastJobId: session.lastJobId,
445
+ source: session.source,
446
+ canContinue: session.canContinue,
447
+ availableModelCount: Array.isArray(session.availableModels) ? session.availableModels.length : 0,
448
+ configOptionCount: Array.isArray(session.agentConfigOptions) ? session.agentConfigOptions.length : 0,
449
+ additionalDirectories: Array.isArray(session.additionalDirectories) ? session.additionalDirectories : [],
450
+ nativeMeta: isPlainObject(session.nativeMeta) ? session.nativeMeta : null
451
+ };
452
+ }
453
+ async function maybeListNativeSessions({ args, config, registry }) {
454
+ if (config.safety.launchExternalAgents !== true) {
455
+ return { sessions: [], meta: { attempted: false, reason: "launch_external_agents_disabled" } };
456
+ }
457
+ if (args.worktree && !path.isAbsolute(args.worktree)) {
458
+ return { sessions: [], meta: { attempted: false, reason: "worktree_must_be_absolute" } };
459
+ }
460
+ const availableAgents = await discoverAgents({ includeNotInstalled: false }).then((value) => value.agents);
461
+ const acpAgents = availableAgents.filter((agent) => (agent.status === "available"
462
+ && agent.acp?.available
463
+ && (!args.agent || agent.id === args.agent)));
464
+ if (acpAgents.length === 0) {
465
+ return { sessions: [], meta: { attempted: false, reason: "no_native_acp_agent_available" } };
466
+ }
467
+ const sessions = [];
468
+ const agents = [];
469
+ const results = await Promise.allSettled(acpAgents.map((agent) => listAcpNativeSessions({
470
+ selectedAgent: agent,
471
+ worktree: args.worktree ?? null,
472
+ env: safeEnv({ inheritEnvironment: config.safety.inheritEnvironment === true })
473
+ }).then((result) => ({ agent, result }))));
474
+ for (const settled of results) {
475
+ if (settled.status === "fulfilled") {
476
+ const { agent, result } = settled.value;
477
+ sessions.push(...mapNativeSessions({
478
+ nativeSessions: result.sessions,
479
+ registry,
480
+ args,
481
+ agentId: agent.id
482
+ }));
483
+ agents.push({
484
+ attempted: true,
485
+ agentId: agent.id,
486
+ supported: result.supported,
487
+ sessionCount: result.sessions.length,
488
+ pages: result.pages,
489
+ nextCursor: result.nextCursor ?? null
490
+ });
491
+ }
492
+ else {
493
+ const agent = acpAgents[results.indexOf(settled)];
494
+ agents.push({
495
+ attempted: true,
496
+ agentId: agent.id,
497
+ supported: null,
498
+ error: settled.reason?.message ?? String(settled.reason)
499
+ });
500
+ }
501
+ }
502
+ return {
503
+ sessions,
504
+ meta: {
505
+ attempted: true,
506
+ agents
507
+ }
508
+ };
509
+ }
510
+ async function listAcpNativeSessions({ selectedAgent, worktree, env }) {
511
+ const cwd = worktree ?? process.cwd();
512
+ const launchTarget = resolveAcpLaunchTarget(selectedAgent.acp, selectedAgent, cwd);
513
+ if (!launchTarget)
514
+ throw new Error(`No ACP adapter is available for ${selectedAgent.id}.`);
515
+ const client = new AcpStdioClient({
516
+ command: launchTarget.command,
517
+ args: launchTarget.args,
518
+ cwd,
519
+ timeoutMs: COMMAND_TIMEOUT_MS,
520
+ env,
521
+ onEvent: () => { }
522
+ });
523
+ try {
524
+ await client.start();
525
+ const initialize = await client.request("initialize", {
526
+ protocolVersion: 1,
527
+ clientCapabilities: {},
528
+ clientInfo: {
529
+ name: SERVER_NAME,
530
+ title: "Agent Router",
531
+ version: SERVER_VERSION
532
+ }
533
+ });
534
+ const supported = Boolean(initialize?.agentCapabilities?.sessionCapabilities?.list);
535
+ if (!supported)
536
+ return { supported: false, sessions: [], pages: 0, nextCursor: null };
537
+ const sessions = [];
538
+ let cursor = null;
539
+ let pages = 0;
540
+ do {
541
+ const params = {};
542
+ if (worktree)
543
+ params.cwd = worktree;
544
+ if (cursor)
545
+ params.cursor = cursor;
546
+ const page = await client.request("session/list", params);
547
+ if (Array.isArray(page.sessions))
548
+ sessions.push(...page.sessions);
549
+ cursor = typeof page.nextCursor === "string" && page.nextCursor ? page.nextCursor : null;
550
+ pages += 1;
551
+ } while (cursor && pages < 10 && sessions.length < 500);
552
+ return { supported: true, sessions, pages, nextCursor: cursor };
553
+ }
554
+ finally {
555
+ client.dispose();
556
+ }
557
+ }
558
+ function mapNativeSessions({ nativeSessions, registry, args, agentId }) {
559
+ const localByProvider = new Map();
560
+ for (const session of Object.values(registry.sessions)) {
561
+ if (isPlainObject(session) && session.providerSessionId) {
562
+ localByProvider.set(session.providerSessionId, session);
563
+ }
564
+ }
565
+ const result = [];
566
+ for (const nativeSession of nativeSessions) {
567
+ if (!isPlainObject(nativeSession) || typeof nativeSession.sessionId !== "string")
568
+ continue;
569
+ const providerSessionId = nativeSession.sessionId;
570
+ const local = localByProvider.get(providerSessionId);
571
+ if (local) {
572
+ if (!args.includeArchived && local.status === "archived")
573
+ continue;
574
+ if (args.agent && local.agentId !== args.agent)
575
+ continue;
576
+ if (args.worktree && (nativeSession.cwd ?? local.worktree) !== args.worktree)
577
+ continue;
578
+ result.push(compactSessionForList({
579
+ ...local,
580
+ title: nativeSession.title ?? local.title,
581
+ worktree: nativeSession.cwd ?? local.worktree,
582
+ updatedAt: nativeSession.updatedAt ?? local.updatedAt,
583
+ source: local.source === "agent_native" ? "agent_native" : "dispatcher_registry+agent_native",
584
+ canContinue: true,
585
+ additionalDirectories: nativeSession.additionalDirectories,
586
+ nativeMeta: nativeSession._meta
587
+ }));
588
+ continue;
589
+ }
590
+ if (args.agent && args.agent !== agentId)
591
+ continue;
592
+ if (args.worktree && nativeSession.cwd !== args.worktree)
593
+ continue;
594
+ result.push(compactSessionForList({
595
+ sessionId: createNativeDispatcherSessionId(agentId, providerSessionId),
596
+ providerSessionId,
597
+ agentId,
598
+ title: nativeSession.title ?? `Native ${agentId} session`,
599
+ status: "idle",
600
+ worktree: nativeSession.cwd ?? null,
601
+ createdAt: nativeSession.updatedAt ?? null,
602
+ updatedAt: nativeSession.updatedAt ?? null,
603
+ lastJobId: null,
604
+ source: "agent_native",
605
+ canContinue: true,
606
+ additionalDirectories: nativeSession.additionalDirectories,
607
+ nativeMeta: nativeSession._meta
608
+ }));
609
+ }
610
+ return result;
611
+ }
612
+ function mergeSessionLists({ localSessions, nativeSessions, limit }) {
613
+ const byId = new Map();
614
+ for (const session of [...localSessions, ...nativeSessions]) {
615
+ if (!session?.sessionId)
616
+ continue;
617
+ byId.set(session.sessionId, { ...(byId.get(session.sessionId) ?? {}), ...session });
618
+ }
619
+ return Array.from(byId.values())
620
+ .sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")))
621
+ .slice(0, limit);
622
+ }
623
+ function createNativeDispatcherSessionId(agentId, providerSessionId) {
624
+ return `sess_native_${agentId}_${encodeBase64Url(providerSessionId)}`;
625
+ }
626
+ function parseNativeDispatcherSessionId(sessionId) {
627
+ const match = /^sess_native_([^_]+)_(.+)$/.exec(String(sessionId ?? ""));
628
+ if (!match)
629
+ return null;
630
+ const providerSessionId = decodeBase64Url(match[2]);
631
+ if (!providerSessionId)
632
+ return null;
633
+ return {
634
+ agentId: match[1],
635
+ providerSessionId
636
+ };
637
+ }
638
+ function encodeBase64Url(value) {
639
+ return Buffer.from(String(value), "utf8")
640
+ .toString("base64")
641
+ .replaceAll("+", "-")
642
+ .replaceAll("/", "_")
643
+ .replace(/=+$/u, "");
644
+ }
645
+ function decodeBase64Url(value) {
646
+ try {
647
+ const base64 = String(value).replaceAll("-", "+").replaceAll("_", "/");
648
+ const padded = `${base64}${"=".repeat((4 - (base64.length % 4)) % 4)}`;
649
+ return Buffer.from(padded, "base64").toString("utf8");
650
+ }
651
+ catch {
652
+ return null;
653
+ }
654
+ }
655
+ async function continueSession(args) {
656
+ const registry = await readRegistry();
657
+ let session = registry.sessions[args.sessionId];
658
+ if (!session) {
659
+ const nativeRef = parseNativeDispatcherSessionId(args.sessionId);
660
+ if (!nativeRef || nativeRef.agentId !== args.agent) {
661
+ return {
662
+ sessionId: args.sessionId,
663
+ status: "not_found",
664
+ message: "The dispatcher can only continue sessions already recorded in the registry or native ACP sessions returned by list_coding_agent_sessions."
665
+ };
666
+ }
667
+ const now = new Date().toISOString();
668
+ session = {
669
+ sessionId: args.sessionId,
670
+ providerSessionId: nativeRef.providerSessionId,
671
+ agentId: args.agent,
672
+ title: preview(args.prompt, 60),
673
+ status: "idle",
674
+ worktree: args.worktree,
675
+ createdAt: now,
676
+ updatedAt: now,
677
+ lastJobId: undefined,
678
+ source: "agent_native",
679
+ canContinue: true
680
+ };
681
+ registry.sessions[session.sessionId] = session;
682
+ await writeRegistry(registry);
683
+ }
684
+ return createJob({
685
+ agent: args.agent,
686
+ sessionId: args.sessionId,
687
+ prompt: args.prompt ?? "",
688
+ worktree: args.worktree ?? "",
689
+ async: args.async,
690
+ launchExternalAgents: args.launchExternalAgents,
691
+ inheritEnvironment: args.inheritEnvironment,
692
+ timeoutSec: args.timeoutSec,
693
+ mode: "implementation",
694
+ permissionProfile: "bypassPermissions",
695
+ collectDiff: true
696
+ });
697
+ }
698
+ async function archiveSession(args) {
699
+ const registry = await readRegistry();
700
+ const session = registry.sessions[args.sessionId];
701
+ if (!session)
702
+ return { sessionId: args.sessionId, status: "not_found" };
703
+ session.status = "archived";
704
+ session.updatedAt = new Date().toISOString();
705
+ await writeRegistry(registry);
706
+ return { sessionId: session.sessionId, status: session.status };
707
+ }
708
+ function findActiveWorktreeJob(registry, worktree, permissionProfile) {
709
+ if (permissionProfile === "plan")
710
+ return null;
711
+ return Object.values(registry.jobs).find((job) => (job.worktree === worktree
712
+ && job.permissionProfile !== "plan"
713
+ && ACTIVE_JOB_STATUSES.has(job.status))) ?? null;
714
+ }
715
+ export { ACTIVE_RUNS, createJob, startBackgroundJobRun, executeAndPersistJobRun, createRunController, persistJobRunResult, markJobRunCrashed, listJobs, getJob, tailJobEvents, cancelJob, listSessions, compactSessionForList, maybeListNativeSessions, listAcpNativeSessions, mapNativeSessions, mergeSessionLists, createNativeDispatcherSessionId, parseNativeDispatcherSessionId, encodeBase64Url, decodeBase64Url, continueSession, archiveSession, findActiveWorktreeJob };
716
+ //# sourceMappingURL=jobs.js.map