@orgloop/agentctl 1.4.0 → 1.5.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/README.md +10 -4
- package/dist/adapters/claude-code.js +1 -1
- package/dist/cli.js +18 -9
- package/dist/daemon/lock-manager.d.ts +2 -0
- package/dist/daemon/lock-manager.js +11 -1
- package/dist/daemon/server.js +55 -13
- package/dist/daemon/session-tracker.d.ts +18 -0
- package/dist/daemon/session-tracker.js +105 -0
- package/dist/hooks.js +2 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,12 +42,16 @@ agentctl list
|
|
|
42
42
|
# List all sessions (including stopped, last 7 days)
|
|
43
43
|
agentctl list -a
|
|
44
44
|
|
|
45
|
-
# Peek at recent output from a session
|
|
45
|
+
# Peek at recent output from a session (alias: logs)
|
|
46
46
|
agentctl peek <session-id>
|
|
47
|
+
agentctl logs <session-id>
|
|
47
48
|
|
|
48
49
|
# Launch a new Claude Code session
|
|
49
50
|
agentctl launch -p "Read the spec and implement phase 2"
|
|
50
51
|
|
|
52
|
+
# Launch in a specific directory
|
|
53
|
+
agentctl launch -p "Fix the auth bug" --cwd ~/code/mono
|
|
54
|
+
|
|
51
55
|
# Launch a new Pi session
|
|
52
56
|
agentctl launch pi -p "Refactor the auth module"
|
|
53
57
|
|
|
@@ -58,7 +62,7 @@ agentctl stop <session-id>
|
|
|
58
62
|
agentctl resume <session-id> "fix the failing tests"
|
|
59
63
|
```
|
|
60
64
|
|
|
61
|
-
Session IDs support prefix matching — `agentctl peek abc123` matches any session starting with `abc123`.
|
|
65
|
+
Session IDs support prefix matching — `agentctl peek abc123` (or `agentctl logs abc123`) matches any session starting with `abc123`.
|
|
62
66
|
|
|
63
67
|
### Parallel Multi-Adapter Launch
|
|
64
68
|
|
|
@@ -141,20 +145,22 @@ agentctl status <id> [options]
|
|
|
141
145
|
--adapter <name> Adapter to use
|
|
142
146
|
--json Output as JSON
|
|
143
147
|
|
|
144
|
-
agentctl peek <id> [options]
|
|
148
|
+
agentctl peek|logs <id> [options]
|
|
145
149
|
-n, --lines <n> Number of recent messages (default: 20)
|
|
146
150
|
--adapter <name> Adapter to use
|
|
147
151
|
|
|
148
152
|
agentctl launch [adapter] [options]
|
|
149
153
|
-p, --prompt <text> Prompt to send (required)
|
|
150
154
|
--spec <path> Spec file path
|
|
151
|
-
--cwd <dir> Working directory
|
|
155
|
+
--cwd <dir> Working directory (default: current directory)
|
|
152
156
|
--model <model> Model to use (e.g. sonnet, opus)
|
|
153
157
|
--adapter <name> Adapter to launch (repeatable for parallel launch)
|
|
154
158
|
--matrix <file> YAML matrix file for advanced sweep launch
|
|
155
159
|
--group <id> Filter by launch group (for list command)
|
|
156
160
|
--force Override directory locks
|
|
157
161
|
|
|
162
|
+
When `--cwd` is omitted, the agent launches in the current working directory (`$PWD`). This means you should either `cd` into the target project first or pass `--cwd` explicitly. Launching from an unrelated directory (e.g. `~`) will start the agent in the wrong place.
|
|
163
|
+
|
|
158
164
|
agentctl stop <id> [options]
|
|
159
165
|
--force Force kill (SIGINT then SIGKILL)
|
|
160
166
|
--adapter <name> Adapter to use
|
|
@@ -206,7 +206,7 @@ export class ClaudeCodeAdapter {
|
|
|
206
206
|
// Claude Code's stream-json format emits a line with sessionId early on.
|
|
207
207
|
let resolvedSessionId;
|
|
208
208
|
if (pid) {
|
|
209
|
-
resolvedSessionId = await this.pollForSessionId(logPath, pid,
|
|
209
|
+
resolvedSessionId = await this.pollForSessionId(logPath, pid, 15000);
|
|
210
210
|
}
|
|
211
211
|
const sessionId = resolvedSessionId || (pid ? `pending-${pid}` : crypto.randomUUID());
|
|
212
212
|
// Persist session metadata so status checks work after wrapper exits
|
package/dist/cli.js
CHANGED
|
@@ -336,13 +336,8 @@ program
|
|
|
336
336
|
console.error(`Session not found: ${id}`);
|
|
337
337
|
process.exit(1);
|
|
338
338
|
});
|
|
339
|
-
// peek
|
|
340
|
-
|
|
341
|
-
.command("peek <id>")
|
|
342
|
-
.description("Peek at recent output from a session")
|
|
343
|
-
.option("-n, --lines <n>", "Number of recent messages", "20")
|
|
344
|
-
.option("--adapter <name>", "Adapter to use")
|
|
345
|
-
.action(async (id, opts) => {
|
|
339
|
+
// Shared handler for peek/logs
|
|
340
|
+
async function peekAction(id, opts) {
|
|
346
341
|
const daemonRunning = await ensureDaemon();
|
|
347
342
|
if (daemonRunning) {
|
|
348
343
|
try {
|
|
@@ -386,7 +381,21 @@ program
|
|
|
386
381
|
}
|
|
387
382
|
console.error(`Session not found: ${id}`);
|
|
388
383
|
process.exit(1);
|
|
389
|
-
}
|
|
384
|
+
}
|
|
385
|
+
// peek
|
|
386
|
+
program
|
|
387
|
+
.command("peek <id>")
|
|
388
|
+
.description("Peek at recent output from a session (alias: logs)")
|
|
389
|
+
.option("-n, --lines <n>", "Number of recent messages", "20")
|
|
390
|
+
.option("--adapter <name>", "Adapter to use")
|
|
391
|
+
.action(peekAction);
|
|
392
|
+
// logs — alias for peek with higher default line count
|
|
393
|
+
program
|
|
394
|
+
.command("logs <id>")
|
|
395
|
+
.description("Show recent session output (alias for peek, default 50 lines)")
|
|
396
|
+
.option("-n, --lines <n>", "Number of recent messages", "50")
|
|
397
|
+
.option("--adapter <name>", "Adapter to use")
|
|
398
|
+
.action(peekAction);
|
|
390
399
|
// stop
|
|
391
400
|
program
|
|
392
401
|
.command("stop <id>")
|
|
@@ -453,7 +462,7 @@ program
|
|
|
453
462
|
.description("Launch a new agent session (or multiple with --adapter flags)")
|
|
454
463
|
.requiredOption("-p, --prompt <text>", "Prompt to send")
|
|
455
464
|
.option("--spec <path>", "Spec file path")
|
|
456
|
-
.option("--cwd <dir>", "Working directory")
|
|
465
|
+
.option("--cwd <dir>", "Working directory (default: current directory)")
|
|
457
466
|
.option("--model <model>", "Model to use (e.g. sonnet, opus)")
|
|
458
467
|
.option("--force", "Override directory locks")
|
|
459
468
|
.option("--worktree <repo>", "Auto-create git worktree from this repo before launch")
|
|
@@ -12,5 +12,7 @@ export declare class LockManager {
|
|
|
12
12
|
manualLock(directory: string, by?: string, reason?: string): Lock;
|
|
13
13
|
/** Manual unlock. Only removes manual locks. */
|
|
14
14
|
manualUnlock(directory: string): void;
|
|
15
|
+
/** Update the sessionId on auto-locks when a pending ID is resolved to a real UUID. */
|
|
16
|
+
updateAutoLockSessionId(oldId: string, newId: string): void;
|
|
15
17
|
listAll(): Lock[];
|
|
16
18
|
}
|
|
@@ -43,7 +43,7 @@ export class LockManager {
|
|
|
43
43
|
const resolved = path.resolve(directory);
|
|
44
44
|
const existing = this.check(resolved);
|
|
45
45
|
if (existing?.type === "manual") {
|
|
46
|
-
throw new Error(`Already manually locked by ${existing.lockedBy}
|
|
46
|
+
throw new Error(`Already manually locked by ${existing.lockedBy ?? "unknown"}${existing.reason ? `: ${existing.reason}` : ""}`);
|
|
47
47
|
}
|
|
48
48
|
const lock = {
|
|
49
49
|
directory: resolved,
|
|
@@ -65,6 +65,16 @@ export class LockManager {
|
|
|
65
65
|
throw new Error(`No manual lock on ${resolved}`);
|
|
66
66
|
this.state.removeLocks((l) => l.directory === resolved && l.type === "manual");
|
|
67
67
|
}
|
|
68
|
+
/** Update the sessionId on auto-locks when a pending ID is resolved to a real UUID. */
|
|
69
|
+
updateAutoLockSessionId(oldId, newId) {
|
|
70
|
+
const locks = this.state.getLocks();
|
|
71
|
+
for (const lock of locks) {
|
|
72
|
+
if (lock.type === "auto" && lock.sessionId === oldId) {
|
|
73
|
+
lock.sessionId = newId;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.state.markDirty();
|
|
77
|
+
}
|
|
68
78
|
listAll() {
|
|
69
79
|
return this.state.getLocks();
|
|
70
80
|
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -79,6 +79,10 @@ export async function startDaemon(opts = {}) {
|
|
|
79
79
|
sessionTracker.startLaunchCleanup((deadId) => {
|
|
80
80
|
lockManager.autoUnlock(deadId);
|
|
81
81
|
});
|
|
82
|
+
// 11b. Start periodic background resolution of pending-* session IDs (10s interval)
|
|
83
|
+
sessionTracker.startPendingResolution((oldId, newId) => {
|
|
84
|
+
lockManager.updateAutoLockSessionId(oldId, newId);
|
|
85
|
+
});
|
|
82
86
|
// 12. Create request handler
|
|
83
87
|
const handleRequest = createRequestHandler({
|
|
84
88
|
sessionTracker,
|
|
@@ -149,6 +153,7 @@ export async function startDaemon(opts = {}) {
|
|
|
149
153
|
// Shutdown function
|
|
150
154
|
const shutdown = async () => {
|
|
151
155
|
sessionTracker.stopLaunchCleanup();
|
|
156
|
+
sessionTracker.stopPendingResolution();
|
|
152
157
|
fuseEngine.shutdown();
|
|
153
158
|
state.flush();
|
|
154
159
|
await state.persist();
|
|
@@ -306,7 +311,17 @@ function createRequestHandler(ctx) {
|
|
|
306
311
|
return sessions;
|
|
307
312
|
}
|
|
308
313
|
case "session.status": {
|
|
309
|
-
|
|
314
|
+
let id = params.id;
|
|
315
|
+
// On-demand resolution: if pending-*, try to resolve first
|
|
316
|
+
const trackedForResolve = ctx.sessionTracker.getSession(id);
|
|
317
|
+
const resolveTarget = trackedForResolve?.id || id;
|
|
318
|
+
if (resolveTarget.startsWith("pending-")) {
|
|
319
|
+
const resolvedId = await ctx.sessionTracker.resolvePendingId(resolveTarget);
|
|
320
|
+
if (resolvedId !== resolveTarget) {
|
|
321
|
+
ctx.lockManager.updateAutoLockSessionId(resolveTarget, resolvedId);
|
|
322
|
+
id = resolvedId;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
310
325
|
// Check launch metadata to determine adapter
|
|
311
326
|
const launchRecord = ctx.sessionTracker.getSession(id);
|
|
312
327
|
const adapterName = params.adapter || launchRecord?.adapter;
|
|
@@ -356,13 +371,21 @@ function createRequestHandler(ctx) {
|
|
|
356
371
|
}
|
|
357
372
|
case "session.peek": {
|
|
358
373
|
// Auto-detect adapter from tracked session, fall back to param or claude-code
|
|
359
|
-
|
|
374
|
+
let tracked = ctx.sessionTracker.getSession(params.id);
|
|
375
|
+
let peekId = tracked?.id || params.id;
|
|
376
|
+
// On-demand resolution: if pending-*, try to resolve before peeking
|
|
377
|
+
if (peekId.startsWith("pending-")) {
|
|
378
|
+
const resolvedId = await ctx.sessionTracker.resolvePendingId(peekId);
|
|
379
|
+
if (resolvedId !== peekId) {
|
|
380
|
+
ctx.lockManager.updateAutoLockSessionId(peekId, resolvedId);
|
|
381
|
+
peekId = resolvedId;
|
|
382
|
+
tracked = ctx.sessionTracker.getSession(resolvedId);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
360
385
|
const adapterName = params.adapter || tracked?.adapter || "claude-code";
|
|
361
386
|
const adapter = ctx.adapters[adapterName];
|
|
362
387
|
if (!adapter)
|
|
363
388
|
throw new Error(`Unknown adapter: ${adapterName}`);
|
|
364
|
-
// Use the full session ID if we resolved it from the tracker
|
|
365
|
-
const peekId = tracked?.id || params.id;
|
|
366
389
|
return adapter.peek(peekId, {
|
|
367
390
|
lines: params.lines,
|
|
368
391
|
});
|
|
@@ -373,7 +396,7 @@ function createRequestHandler(ctx) {
|
|
|
373
396
|
const lock = ctx.lockManager.check(cwd);
|
|
374
397
|
if (lock && !params.force) {
|
|
375
398
|
if (lock.type === "manual") {
|
|
376
|
-
throw new Error(`Directory locked by ${lock.lockedBy}
|
|
399
|
+
throw new Error(`Directory locked by ${lock.lockedBy ?? "unknown"}${lock.reason ? `: ${lock.reason}` : ""}. Use --force to override.`);
|
|
377
400
|
}
|
|
378
401
|
throw new Error(`Directory in use by session ${lock.sessionId?.slice(0, 8)}. Use --force to override.`);
|
|
379
402
|
}
|
|
@@ -408,14 +431,24 @@ function createRequestHandler(ctx) {
|
|
|
408
431
|
}
|
|
409
432
|
case "session.stop": {
|
|
410
433
|
const id = params.id;
|
|
411
|
-
|
|
434
|
+
let launchRecord = ctx.sessionTracker.getSession(id);
|
|
435
|
+
let sessionId = launchRecord?.id || id;
|
|
436
|
+
// On-demand resolution: if pending-*, try to resolve before stopping
|
|
437
|
+
if (sessionId.startsWith("pending-")) {
|
|
438
|
+
const resolvedId = await ctx.sessionTracker.resolvePendingId(sessionId);
|
|
439
|
+
if (resolvedId !== sessionId) {
|
|
440
|
+
ctx.lockManager.updateAutoLockSessionId(sessionId, resolvedId);
|
|
441
|
+
sessionId = resolvedId;
|
|
442
|
+
launchRecord = ctx.sessionTracker.getSession(resolvedId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
412
445
|
// Ghost pending entry with dead PID: remove from state with --force
|
|
413
|
-
if (
|
|
446
|
+
if (sessionId.startsWith("pending-") &&
|
|
414
447
|
params.force &&
|
|
415
|
-
launchRecord
|
|
448
|
+
launchRecord?.pid &&
|
|
416
449
|
!isProcessAlive(launchRecord.pid)) {
|
|
417
|
-
ctx.lockManager.autoUnlock(
|
|
418
|
-
ctx.sessionTracker.removeSession(
|
|
450
|
+
ctx.lockManager.autoUnlock(sessionId);
|
|
451
|
+
ctx.sessionTracker.removeSession(sessionId);
|
|
419
452
|
return null;
|
|
420
453
|
}
|
|
421
454
|
const adapterName = params.adapter || launchRecord?.adapter;
|
|
@@ -424,7 +457,6 @@ function createRequestHandler(ctx) {
|
|
|
424
457
|
const adapter = ctx.adapters[adapterName];
|
|
425
458
|
if (!adapter)
|
|
426
459
|
throw new Error(`Unknown adapter: ${adapterName}`);
|
|
427
|
-
const sessionId = launchRecord?.id || id;
|
|
428
460
|
await adapter.stop(sessionId, {
|
|
429
461
|
force: params.force,
|
|
430
462
|
});
|
|
@@ -439,14 +471,24 @@ function createRequestHandler(ctx) {
|
|
|
439
471
|
}
|
|
440
472
|
case "session.resume": {
|
|
441
473
|
const id = params.id;
|
|
442
|
-
|
|
474
|
+
let launchRecord = ctx.sessionTracker.getSession(id);
|
|
475
|
+
let resumeId = launchRecord?.id || id;
|
|
476
|
+
// On-demand resolution: if pending-*, try to resolve before resuming
|
|
477
|
+
if (resumeId.startsWith("pending-")) {
|
|
478
|
+
const resolvedId = await ctx.sessionTracker.resolvePendingId(resumeId);
|
|
479
|
+
if (resolvedId !== resumeId) {
|
|
480
|
+
ctx.lockManager.updateAutoLockSessionId(resumeId, resolvedId);
|
|
481
|
+
resumeId = resolvedId;
|
|
482
|
+
launchRecord = ctx.sessionTracker.getSession(resolvedId);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
443
485
|
const adapterName = params.adapter || launchRecord?.adapter;
|
|
444
486
|
if (!adapterName)
|
|
445
487
|
throw new Error(`Session not found: ${id}. Specify --adapter to resume a non-daemon session.`);
|
|
446
488
|
const adapter = ctx.adapters[adapterName];
|
|
447
489
|
if (!adapter)
|
|
448
490
|
throw new Error(`Unknown adapter: ${adapterName}`);
|
|
449
|
-
await adapter.resume(
|
|
491
|
+
await adapter.resume(resumeId, params.message);
|
|
450
492
|
return null;
|
|
451
493
|
}
|
|
452
494
|
// --- Prune command (#40) --- kept for CLI backward compat
|
|
@@ -20,6 +20,7 @@ export declare class SessionTracker {
|
|
|
20
20
|
private adapters;
|
|
21
21
|
private readonly isProcessAlive;
|
|
22
22
|
private cleanupHandle;
|
|
23
|
+
private pendingResolutionHandle;
|
|
23
24
|
constructor(state: StateManager, opts: SessionTrackerOpts);
|
|
24
25
|
/**
|
|
25
26
|
* Start periodic PID liveness check for daemon-launched sessions.
|
|
@@ -28,6 +29,23 @@ export declare class SessionTracker {
|
|
|
28
29
|
*/
|
|
29
30
|
startLaunchCleanup(onDead?: (sessionId: string) => void): void;
|
|
30
31
|
stopLaunchCleanup(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Start periodic background resolution of pending-* session IDs.
|
|
34
|
+
* Runs every 10s, discovers real UUIDs via adapter PID matching.
|
|
35
|
+
*/
|
|
36
|
+
startPendingResolution(onResolved?: (oldId: string, newId: string) => void): void;
|
|
37
|
+
stopPendingResolution(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a single pending-* session ID on demand.
|
|
40
|
+
* Returns the resolved real UUID, or the original ID if resolution fails.
|
|
41
|
+
*/
|
|
42
|
+
resolvePendingId(id: string): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Batch-resolve all pending-* session IDs via adapter discovery.
|
|
45
|
+
* Groups pending sessions by adapter to minimize discover() calls.
|
|
46
|
+
* Returns a map of oldId → newId for each resolved session.
|
|
47
|
+
*/
|
|
48
|
+
resolvePendingSessions(): Promise<Map<string, string>>;
|
|
31
49
|
/** Track a newly launched session (stores launch metadata in state) */
|
|
32
50
|
track(session: AgentSession, adapterName: string): SessionRecord;
|
|
33
51
|
/** Get session launch metadata by id (exact or prefix match) */
|
|
@@ -20,6 +20,7 @@ export class SessionTracker {
|
|
|
20
20
|
adapters;
|
|
21
21
|
isProcessAlive;
|
|
22
22
|
cleanupHandle = null;
|
|
23
|
+
pendingResolutionHandle = null;
|
|
23
24
|
constructor(state, opts) {
|
|
24
25
|
this.state = state;
|
|
25
26
|
this.adapters = opts.adapters;
|
|
@@ -47,6 +48,110 @@ export class SessionTracker {
|
|
|
47
48
|
this.cleanupHandle = null;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Start periodic background resolution of pending-* session IDs.
|
|
53
|
+
* Runs every 10s, discovers real UUIDs via adapter PID matching.
|
|
54
|
+
*/
|
|
55
|
+
startPendingResolution(onResolved) {
|
|
56
|
+
if (this.pendingResolutionHandle)
|
|
57
|
+
return;
|
|
58
|
+
this.pendingResolutionHandle = setInterval(async () => {
|
|
59
|
+
const resolved = await this.resolvePendingSessions();
|
|
60
|
+
if (onResolved) {
|
|
61
|
+
for (const [oldId, newId] of resolved) {
|
|
62
|
+
onResolved(oldId, newId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, 10_000);
|
|
66
|
+
}
|
|
67
|
+
stopPendingResolution() {
|
|
68
|
+
if (this.pendingResolutionHandle) {
|
|
69
|
+
clearInterval(this.pendingResolutionHandle);
|
|
70
|
+
this.pendingResolutionHandle = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a single pending-* session ID on demand.
|
|
75
|
+
* Returns the resolved real UUID, or the original ID if resolution fails.
|
|
76
|
+
*/
|
|
77
|
+
async resolvePendingId(id) {
|
|
78
|
+
if (!id.startsWith("pending-"))
|
|
79
|
+
return id;
|
|
80
|
+
const record = this.getSession(id);
|
|
81
|
+
if (!record || !record.pid)
|
|
82
|
+
return id;
|
|
83
|
+
const adapter = this.adapters[record.adapter];
|
|
84
|
+
if (!adapter)
|
|
85
|
+
return id;
|
|
86
|
+
try {
|
|
87
|
+
const discovered = await adapter.discover();
|
|
88
|
+
const match = discovered.find((d) => d.pid === record.pid);
|
|
89
|
+
if (match && match.id !== id) {
|
|
90
|
+
// Resolve: move state from pending ID to real UUID
|
|
91
|
+
const updatedRecord = { ...record, id: match.id };
|
|
92
|
+
this.state.removeSession(id);
|
|
93
|
+
this.state.setSession(match.id, updatedRecord);
|
|
94
|
+
return match.id;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Adapter failed — return original ID
|
|
99
|
+
}
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Batch-resolve all pending-* session IDs via adapter discovery.
|
|
104
|
+
* Groups pending sessions by adapter to minimize discover() calls.
|
|
105
|
+
* Returns a map of oldId → newId for each resolved session.
|
|
106
|
+
*/
|
|
107
|
+
async resolvePendingSessions() {
|
|
108
|
+
const resolved = new Map();
|
|
109
|
+
const sessions = this.state.getSessions();
|
|
110
|
+
// Group pending sessions by adapter
|
|
111
|
+
const pendingByAdapter = new Map();
|
|
112
|
+
for (const [id, record] of Object.entries(sessions)) {
|
|
113
|
+
if (!id.startsWith("pending-"))
|
|
114
|
+
continue;
|
|
115
|
+
if (record.status === "stopped" || record.status === "completed")
|
|
116
|
+
continue;
|
|
117
|
+
if (!record.pid)
|
|
118
|
+
continue;
|
|
119
|
+
const list = pendingByAdapter.get(record.adapter) || [];
|
|
120
|
+
list.push({ id, record });
|
|
121
|
+
pendingByAdapter.set(record.adapter, list);
|
|
122
|
+
}
|
|
123
|
+
if (pendingByAdapter.size === 0)
|
|
124
|
+
return resolved;
|
|
125
|
+
// For each adapter with pending sessions, run discover() once
|
|
126
|
+
for (const [adapterName, pendings] of pendingByAdapter) {
|
|
127
|
+
const adapter = this.adapters[adapterName];
|
|
128
|
+
if (!adapter)
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
const discovered = await adapter.discover();
|
|
132
|
+
const pidToId = new Map();
|
|
133
|
+
for (const d of discovered) {
|
|
134
|
+
if (d.pid)
|
|
135
|
+
pidToId.set(d.pid, d.id);
|
|
136
|
+
}
|
|
137
|
+
for (const { id, record } of pendings) {
|
|
138
|
+
if (!record.pid)
|
|
139
|
+
continue;
|
|
140
|
+
const resolvedId = pidToId.get(record.pid);
|
|
141
|
+
if (resolvedId && resolvedId !== id) {
|
|
142
|
+
const updatedRecord = { ...record, id: resolvedId };
|
|
143
|
+
this.state.removeSession(id);
|
|
144
|
+
this.state.setSession(resolvedId, updatedRecord);
|
|
145
|
+
resolved.set(id, resolvedId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Adapter failed — skip
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return resolved;
|
|
154
|
+
}
|
|
50
155
|
/** Track a newly launched session (stores launch metadata in state) */
|
|
51
156
|
track(session, adapterName) {
|
|
52
157
|
const record = sessionToRecord(session, adapterName);
|
package/dist/hooks.js
CHANGED
|
@@ -31,13 +31,12 @@ export async function runHook(hooks, phase, ctx) {
|
|
|
31
31
|
const result = await execAsync(script, {
|
|
32
32
|
cwd: ctx.cwd,
|
|
33
33
|
env,
|
|
34
|
-
timeout:
|
|
34
|
+
timeout: 300_000,
|
|
35
35
|
});
|
|
36
36
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
37
37
|
}
|
|
38
38
|
catch (err) {
|
|
39
39
|
const e = err;
|
|
40
|
-
|
|
41
|
-
return { stdout: e.stdout || "", stderr: e.stderr || "" };
|
|
40
|
+
throw new Error(`Hook ${phase} failed: ${e.message}`);
|
|
42
41
|
}
|
|
43
42
|
}
|