@orgloop/agentctl 1.2.1 → 1.4.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/adapters/claude-code.d.ts +3 -1
- package/dist/adapters/claude-code.js +51 -0
- package/dist/adapters/codex.d.ts +3 -1
- package/dist/adapters/codex.js +35 -0
- package/dist/adapters/openclaw.d.ts +3 -1
- package/dist/adapters/openclaw.js +61 -4
- package/dist/adapters/opencode.d.ts +3 -1
- package/dist/adapters/opencode.js +57 -0
- package/dist/adapters/pi-rust.d.ts +3 -1
- package/dist/adapters/pi-rust.js +74 -0
- package/dist/adapters/pi.d.ts +3 -1
- package/dist/adapters/pi.js +38 -0
- package/dist/cli.js +55 -96
- package/dist/core/types.d.ts +26 -2
- package/dist/daemon/fuse-engine.d.ts +13 -10
- package/dist/daemon/fuse-engine.js +69 -46
- package/dist/daemon/metrics.d.ts +8 -6
- package/dist/daemon/metrics.js +15 -11
- package/dist/daemon/server.js +159 -43
- package/dist/daemon/session-tracker.d.ts +42 -43
- package/dist/daemon/session-tracker.js +141 -255
- package/dist/daemon/state.d.ts +12 -2
- package/dist/hooks.d.ts +1 -1
- package/package.json +1 -1
- package/dist/merge.d.ts +0 -24
- package/dist/merge.js +0 -65
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,6 @@ import { DaemonClient } from "./client/daemon-client.js";
|
|
|
18
18
|
import { runHook } from "./hooks.js";
|
|
19
19
|
import { orchestrateLaunch, parseAdapterSlots, } from "./launch-orchestrator.js";
|
|
20
20
|
import { expandMatrix, parseMatrixFile } from "./matrix-parser.js";
|
|
21
|
-
import { mergeSession } from "./merge.js";
|
|
22
21
|
import { createWorktree } from "./worktree.js";
|
|
23
22
|
const adapters = {
|
|
24
23
|
"claude-code": new ClaudeCodeAdapter(),
|
|
@@ -95,6 +94,18 @@ function formatSession(s, showGroup) {
|
|
|
95
94
|
row.Prompt = (s.prompt || "-").slice(0, 60);
|
|
96
95
|
return row;
|
|
97
96
|
}
|
|
97
|
+
function formatDiscovered(s) {
|
|
98
|
+
return {
|
|
99
|
+
ID: s.id.slice(0, 8),
|
|
100
|
+
Adapter: s.adapter,
|
|
101
|
+
Status: s.status,
|
|
102
|
+
Model: s.model || "-",
|
|
103
|
+
CWD: s.cwd ? shortenPath(s.cwd) : "-",
|
|
104
|
+
PID: s.pid?.toString() || "-",
|
|
105
|
+
Started: s.startedAt ? timeAgo(s.startedAt) : "-",
|
|
106
|
+
Prompt: (s.prompt || "-").slice(0, 60),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
98
109
|
function formatRecord(s, showGroup) {
|
|
99
110
|
const row = {
|
|
100
111
|
ID: s.id.slice(0, 8),
|
|
@@ -178,6 +189,22 @@ function sessionToJson(s) {
|
|
|
178
189
|
meta: s.meta,
|
|
179
190
|
};
|
|
180
191
|
}
|
|
192
|
+
function discoveredToJson(s) {
|
|
193
|
+
return {
|
|
194
|
+
id: s.id,
|
|
195
|
+
adapter: s.adapter,
|
|
196
|
+
status: s.status,
|
|
197
|
+
startedAt: s.startedAt?.toISOString(),
|
|
198
|
+
stoppedAt: s.stoppedAt?.toISOString(),
|
|
199
|
+
cwd: s.cwd,
|
|
200
|
+
model: s.model,
|
|
201
|
+
prompt: s.prompt,
|
|
202
|
+
tokens: s.tokens,
|
|
203
|
+
cost: s.cost,
|
|
204
|
+
pid: s.pid,
|
|
205
|
+
nativeMetadata: s.nativeMetadata,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
181
208
|
// --- CLI ---
|
|
182
209
|
const program = new Command();
|
|
183
210
|
program
|
|
@@ -214,25 +241,40 @@ program
|
|
|
214
241
|
}
|
|
215
242
|
return;
|
|
216
243
|
}
|
|
217
|
-
// Direct fallback
|
|
218
|
-
|
|
219
|
-
let sessions = [];
|
|
244
|
+
// Direct fallback — discover-first
|
|
245
|
+
let discovered = [];
|
|
220
246
|
if (opts.adapter) {
|
|
221
247
|
const adapter = getAdapter(opts.adapter);
|
|
222
|
-
|
|
248
|
+
discovered = await adapter.discover();
|
|
223
249
|
}
|
|
224
250
|
else {
|
|
225
251
|
for (const adapter of getAllAdapters()) {
|
|
226
|
-
const
|
|
227
|
-
|
|
252
|
+
const d = await adapter.discover().catch(() => []);
|
|
253
|
+
discovered.push(...d);
|
|
228
254
|
}
|
|
229
255
|
}
|
|
256
|
+
// Apply status/all filters
|
|
257
|
+
if (opts.status) {
|
|
258
|
+
discovered = discovered.filter((s) => s.status === opts.status);
|
|
259
|
+
}
|
|
260
|
+
else if (!opts.all) {
|
|
261
|
+
discovered = discovered.filter((s) => s.status === "running");
|
|
262
|
+
}
|
|
263
|
+
// Sort: running first, then by most recent
|
|
264
|
+
discovered.sort((a, b) => {
|
|
265
|
+
if (a.status === "running" && b.status !== "running")
|
|
266
|
+
return -1;
|
|
267
|
+
if (b.status === "running" && a.status !== "running")
|
|
268
|
+
return 1;
|
|
269
|
+
const aTime = a.startedAt?.getTime() ?? 0;
|
|
270
|
+
const bTime = b.startedAt?.getTime() ?? 0;
|
|
271
|
+
return bTime - aTime;
|
|
272
|
+
});
|
|
230
273
|
if (opts.json) {
|
|
231
|
-
printJson(
|
|
274
|
+
printJson(discovered.map(discoveredToJson));
|
|
232
275
|
}
|
|
233
276
|
else {
|
|
234
|
-
|
|
235
|
-
printTable(sessions.map((s) => formatSession(s, hasGroups)));
|
|
277
|
+
printTable(discovered.map((s) => formatDiscovered(s)));
|
|
236
278
|
}
|
|
237
279
|
});
|
|
238
280
|
// status
|
|
@@ -420,18 +462,14 @@ program
|
|
|
420
462
|
.option("--matrix <file>", "YAML matrix file for advanced sweep launch")
|
|
421
463
|
.option("--on-create <script>", "Hook: run after session is created")
|
|
422
464
|
.option("--on-complete <script>", "Hook: run after session completes")
|
|
423
|
-
.option("--pre-merge <script>", "Hook: run before merge")
|
|
424
|
-
.option("--post-merge <script>", "Hook: run after merge")
|
|
425
465
|
.allowUnknownOption() // Allow interleaved --adapter/--model for parseAdapterSlots
|
|
426
466
|
.action(async (adapterName, opts) => {
|
|
427
467
|
let cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
|
|
428
468
|
// Collect hooks
|
|
429
|
-
const hooks = opts.onCreate || opts.onComplete
|
|
469
|
+
const hooks = opts.onCreate || opts.onComplete
|
|
430
470
|
? {
|
|
431
471
|
onCreate: opts.onCreate,
|
|
432
472
|
onComplete: opts.onComplete,
|
|
433
|
-
preMerge: opts.preMerge,
|
|
434
|
-
postMerge: opts.postMerge,
|
|
435
473
|
}
|
|
436
474
|
: undefined;
|
|
437
475
|
// --- Multi-adapter / matrix detection ---
|
|
@@ -623,85 +661,6 @@ program
|
|
|
623
661
|
console.log(JSON.stringify(out));
|
|
624
662
|
}
|
|
625
663
|
});
|
|
626
|
-
// --- Merge command (FEAT-4) ---
|
|
627
|
-
program
|
|
628
|
-
.command("merge <id>")
|
|
629
|
-
.description("Commit, push, and open PR for a session's work")
|
|
630
|
-
.option("-m, --message <text>", "Commit message")
|
|
631
|
-
.option("--remove-worktree", "Remove worktree after merge")
|
|
632
|
-
.option("--repo <path>", "Main repo path (for worktree removal)")
|
|
633
|
-
.option("--pre-merge <script>", "Hook: run before merge")
|
|
634
|
-
.option("--post-merge <script>", "Hook: run after merge")
|
|
635
|
-
.action(async (id, opts) => {
|
|
636
|
-
// Find session
|
|
637
|
-
const daemonRunning = await ensureDaemon();
|
|
638
|
-
let sessionCwd;
|
|
639
|
-
let sessionAdapter;
|
|
640
|
-
if (daemonRunning) {
|
|
641
|
-
try {
|
|
642
|
-
const session = await client.call("session.status", {
|
|
643
|
-
id,
|
|
644
|
-
});
|
|
645
|
-
sessionCwd = session.cwd;
|
|
646
|
-
sessionAdapter = session.adapter;
|
|
647
|
-
}
|
|
648
|
-
catch {
|
|
649
|
-
// Fall through to adapter
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
if (!sessionCwd) {
|
|
653
|
-
const adapter = getAdapter();
|
|
654
|
-
try {
|
|
655
|
-
const session = await adapter.status(id);
|
|
656
|
-
sessionCwd = session.cwd;
|
|
657
|
-
sessionAdapter = session.adapter;
|
|
658
|
-
}
|
|
659
|
-
catch (err) {
|
|
660
|
-
console.error(`Session not found: ${err.message}`);
|
|
661
|
-
process.exit(1);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if (!sessionCwd) {
|
|
665
|
-
console.error("Cannot determine session working directory");
|
|
666
|
-
process.exit(1);
|
|
667
|
-
}
|
|
668
|
-
const hooks = opts.preMerge || opts.postMerge
|
|
669
|
-
? { preMerge: opts.preMerge, postMerge: opts.postMerge }
|
|
670
|
-
: undefined;
|
|
671
|
-
// Pre-merge hook
|
|
672
|
-
if (hooks?.preMerge) {
|
|
673
|
-
await runHook(hooks, "preMerge", {
|
|
674
|
-
sessionId: id,
|
|
675
|
-
cwd: sessionCwd,
|
|
676
|
-
adapter: sessionAdapter || "claude-code",
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
const result = await mergeSession({
|
|
680
|
-
cwd: sessionCwd,
|
|
681
|
-
message: opts.message,
|
|
682
|
-
removeWorktree: opts.removeWorktree,
|
|
683
|
-
repoPath: opts.repo,
|
|
684
|
-
});
|
|
685
|
-
if (result.committed)
|
|
686
|
-
console.log("Changes committed");
|
|
687
|
-
if (result.pushed)
|
|
688
|
-
console.log("Pushed to remote");
|
|
689
|
-
if (result.prUrl)
|
|
690
|
-
console.log(`PR: ${result.prUrl}`);
|
|
691
|
-
if (result.worktreeRemoved)
|
|
692
|
-
console.log("Worktree removed");
|
|
693
|
-
if (!result.committed && !result.pushed) {
|
|
694
|
-
console.log("No changes to commit or push");
|
|
695
|
-
}
|
|
696
|
-
// Post-merge hook
|
|
697
|
-
if (hooks?.postMerge) {
|
|
698
|
-
await runHook(hooks, "postMerge", {
|
|
699
|
-
sessionId: id,
|
|
700
|
-
cwd: sessionCwd,
|
|
701
|
-
adapter: sessionAdapter || "claude-code",
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
});
|
|
705
664
|
// --- Worktree subcommand ---
|
|
706
665
|
const worktreeCmd = new Command("worktree").description("Manage agentctl-created worktrees");
|
|
707
666
|
worktreeCmd
|
|
@@ -846,7 +805,7 @@ program
|
|
|
846
805
|
// --- Fuses command ---
|
|
847
806
|
program
|
|
848
807
|
.command("fuses")
|
|
849
|
-
.description("List active
|
|
808
|
+
.description("List active fuse timers")
|
|
850
809
|
.option("--json", "Output as JSON")
|
|
851
810
|
.action(async (opts) => {
|
|
852
811
|
try {
|
|
@@ -861,7 +820,7 @@ program
|
|
|
861
820
|
}
|
|
862
821
|
printTable(fuses.map((f) => ({
|
|
863
822
|
Directory: shortenPath(f.directory),
|
|
864
|
-
|
|
823
|
+
Label: f.label || "-",
|
|
865
824
|
"Expires In": formatDuration(new Date(f.expiresAt).getTime() - Date.now()),
|
|
866
825
|
})));
|
|
867
826
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session discovered by an adapter's runtime — the ground truth for "what exists."
|
|
3
|
+
* This is the source of truth for session lifecycle in the discover-first model.
|
|
4
|
+
*/
|
|
5
|
+
export interface DiscoveredSession {
|
|
6
|
+
id: string;
|
|
7
|
+
status: "running" | "stopped";
|
|
8
|
+
adapter: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
startedAt?: Date;
|
|
12
|
+
stoppedAt?: Date;
|
|
13
|
+
pid?: number;
|
|
14
|
+
prompt?: string;
|
|
15
|
+
tokens?: {
|
|
16
|
+
in: number;
|
|
17
|
+
out: number;
|
|
18
|
+
};
|
|
19
|
+
cost?: number;
|
|
20
|
+
/** Adapter-native fields — whatever the runtime provides */
|
|
21
|
+
nativeMetadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
1
23
|
export interface AgentAdapter {
|
|
2
24
|
id: string;
|
|
25
|
+
/** Find all sessions currently managed by this adapter's runtime. */
|
|
26
|
+
discover(): Promise<DiscoveredSession[]>;
|
|
27
|
+
/** Check if a specific session is still alive. */
|
|
28
|
+
isAlive(sessionId: string): Promise<boolean>;
|
|
3
29
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
4
30
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
5
31
|
status(sessionId: string): Promise<AgentSession>;
|
|
@@ -64,6 +90,4 @@ export interface LaunchOpts {
|
|
|
64
90
|
export interface LifecycleHooks {
|
|
65
91
|
onCreate?: string;
|
|
66
92
|
onComplete?: string;
|
|
67
|
-
preMerge?: string;
|
|
68
|
-
postMerge?: string;
|
|
69
93
|
}
|
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
import type { EventEmitter } from "node:events";
|
|
2
|
-
import type { FuseTimer,
|
|
2
|
+
import type { FuseTimer, StateManager } from "./state.js";
|
|
3
3
|
export interface FuseEngineOpts {
|
|
4
4
|
defaultDurationMs: number;
|
|
5
5
|
emitter?: EventEmitter;
|
|
6
6
|
}
|
|
7
|
+
export interface SetFuseOpts {
|
|
8
|
+
directory: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
ttlMs?: number;
|
|
11
|
+
onExpire?: FuseTimer["onExpire"];
|
|
12
|
+
label?: string;
|
|
13
|
+
}
|
|
7
14
|
export declare class FuseEngine {
|
|
8
15
|
private timers;
|
|
9
16
|
private state;
|
|
10
17
|
private defaultDurationMs;
|
|
11
18
|
private emitter?;
|
|
12
19
|
constructor(state: StateManager, opts: FuseEngineOpts);
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} | null;
|
|
18
|
-
/** Called when a session exits. Starts fuse if applicable. */
|
|
19
|
-
onSessionExit(session: SessionRecord): void;
|
|
20
|
-
private startFuse;
|
|
20
|
+
/** Set a fuse for a directory. Called when a session exits or explicitly via API. */
|
|
21
|
+
setFuse(opts: SetFuseOpts): void;
|
|
22
|
+
/** Extend an existing fuse's TTL. Resets the timer to a new duration. */
|
|
23
|
+
extendFuse(directory: string, ttlMs?: number): boolean;
|
|
21
24
|
/** Cancel fuse for a directory. */
|
|
22
25
|
cancelFuse(directory: string, persist?: boolean): boolean;
|
|
23
26
|
/** Resume fuses from persisted state after daemon restart. */
|
|
24
27
|
resumeTimers(): void;
|
|
25
|
-
/** Fire a fuse —
|
|
28
|
+
/** Fire a fuse — execute the configured on-expire action. */
|
|
26
29
|
private fireFuse;
|
|
27
30
|
listActive(): FuseTimer[];
|
|
28
31
|
/** Clear all timers (for clean shutdown) */
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import { promisify } from "node:util";
|
|
5
3
|
const execAsync = promisify(exec);
|
|
6
4
|
export class FuseEngine {
|
|
@@ -13,43 +11,45 @@ export class FuseEngine {
|
|
|
13
11
|
this.defaultDurationMs = opts.defaultDurationMs;
|
|
14
12
|
this.emitter = opts.emitter;
|
|
15
13
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
/** Set a fuse for a directory. Called when a session exits or explicitly via API. */
|
|
15
|
+
setFuse(opts) {
|
|
16
|
+
const ttlMs = opts.ttlMs ?? this.defaultDurationMs;
|
|
17
|
+
// Cancel existing fuse for same directory
|
|
18
|
+
this.cancelFuse(opts.directory, false);
|
|
19
|
+
const expiresAt = new Date(Date.now() + ttlMs);
|
|
20
|
+
const fuse = {
|
|
21
|
+
directory: opts.directory,
|
|
22
|
+
ttlMs,
|
|
23
|
+
expiresAt: expiresAt.toISOString(),
|
|
24
|
+
sessionId: opts.sessionId,
|
|
25
|
+
onExpire: opts.onExpire,
|
|
26
|
+
label: opts.label,
|
|
28
27
|
};
|
|
28
|
+
this.state.addFuse(fuse);
|
|
29
|
+
const timeout = setTimeout(() => this.fireFuse(fuse), ttlMs);
|
|
30
|
+
this.timers.set(opts.directory, timeout);
|
|
31
|
+
this.emitter?.emit("fuse.set", fuse);
|
|
29
32
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!
|
|
36
|
-
return;
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
startFuse(directory, clusterName, branch, sessionId) {
|
|
40
|
-
// Cancel existing fuse for same directory
|
|
33
|
+
/** Extend an existing fuse's TTL. Resets the timer to a new duration. */
|
|
34
|
+
extendFuse(directory, ttlMs) {
|
|
35
|
+
const existing = this.state
|
|
36
|
+
.getFuses()
|
|
37
|
+
.find((f) => f.directory === directory);
|
|
38
|
+
if (!existing)
|
|
39
|
+
return false;
|
|
40
|
+
const duration = ttlMs ?? existing.ttlMs;
|
|
41
41
|
this.cancelFuse(directory, false);
|
|
42
|
-
const expiresAt = new Date(Date.now() +
|
|
42
|
+
const expiresAt = new Date(Date.now() + duration);
|
|
43
43
|
const fuse = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
branch,
|
|
44
|
+
...existing,
|
|
45
|
+
ttlMs: duration,
|
|
47
46
|
expiresAt: expiresAt.toISOString(),
|
|
48
|
-
sessionId,
|
|
49
47
|
};
|
|
50
48
|
this.state.addFuse(fuse);
|
|
51
|
-
const timeout = setTimeout(() => this.fireFuse(fuse),
|
|
49
|
+
const timeout = setTimeout(() => this.fireFuse(fuse), duration);
|
|
52
50
|
this.timers.set(directory, timeout);
|
|
51
|
+
this.emitter?.emit("fuse.extended", fuse);
|
|
52
|
+
return true;
|
|
53
53
|
}
|
|
54
54
|
/** Cancel fuse for a directory. */
|
|
55
55
|
cancelFuse(directory, persist = true) {
|
|
@@ -79,30 +79,53 @@ export class FuseEngine {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
/** Fire a fuse —
|
|
82
|
+
/** Fire a fuse — execute the configured on-expire action. */
|
|
83
83
|
async fireFuse(fuse) {
|
|
84
84
|
this.timers.delete(fuse.directory);
|
|
85
85
|
this.state.removeFuse(fuse.directory);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const label = fuse.label || fuse.directory;
|
|
87
|
+
console.log(`Fuse expired: ${label}`);
|
|
88
|
+
this.emitter?.emit("fuse.expired", fuse);
|
|
89
|
+
const action = fuse.onExpire;
|
|
90
|
+
if (!action)
|
|
91
|
+
return;
|
|
92
|
+
// Execute on-expire script
|
|
93
|
+
if (action.script) {
|
|
89
94
|
try {
|
|
90
|
-
await execAsync(
|
|
95
|
+
await execAsync(action.script, {
|
|
91
96
|
cwd: fuse.directory,
|
|
92
|
-
timeout:
|
|
97
|
+
timeout: 120_000,
|
|
98
|
+
});
|
|
99
|
+
console.log(`Fuse action completed: ${label}`);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.error(`Fuse action failed for ${label}:`, err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// POST to webhook
|
|
106
|
+
if (action.webhook) {
|
|
107
|
+
try {
|
|
108
|
+
const body = JSON.stringify({
|
|
109
|
+
type: "fuse.expired",
|
|
110
|
+
directory: fuse.directory,
|
|
111
|
+
sessionId: fuse.sessionId,
|
|
112
|
+
label: fuse.label,
|
|
113
|
+
expiredAt: new Date().toISOString(),
|
|
114
|
+
});
|
|
115
|
+
await fetch(action.webhook, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
body,
|
|
119
|
+
signal: AbortSignal.timeout(30_000),
|
|
93
120
|
});
|
|
94
121
|
}
|
|
95
|
-
catch {
|
|
96
|
-
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error(`Fuse webhook failed for ${label}:`, err);
|
|
97
124
|
}
|
|
98
|
-
await execAsync(`kind delete cluster --name ${fuse.clusterName}`, {
|
|
99
|
-
timeout: 120_000,
|
|
100
|
-
});
|
|
101
|
-
console.log(`Cluster ${fuse.clusterName} deleted`);
|
|
102
|
-
this.emitter?.emit("fuse.fired", fuse);
|
|
103
125
|
}
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
// Emit named event
|
|
127
|
+
if (action.event) {
|
|
128
|
+
this.emitter?.emit(action.event, fuse);
|
|
106
129
|
}
|
|
107
130
|
}
|
|
108
131
|
listActive() {
|
package/dist/daemon/metrics.d.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import type { FuseEngine } from "./fuse-engine.js";
|
|
2
2
|
import type { LockManager } from "./lock-manager.js";
|
|
3
|
-
import type { SessionTracker } from "./session-tracker.js";
|
|
4
3
|
export declare class MetricsRegistry {
|
|
5
|
-
private sessionTracker;
|
|
6
4
|
private lockManager;
|
|
7
5
|
private fuseEngine;
|
|
8
6
|
sessionsTotalCompleted: number;
|
|
9
7
|
sessionsTotalFailed: number;
|
|
10
8
|
sessionsTotalStopped: number;
|
|
11
|
-
|
|
12
|
-
clustersDeletedTotal: number;
|
|
9
|
+
fusesExpiredTotal: number;
|
|
13
10
|
sessionDurations: number[];
|
|
14
|
-
|
|
11
|
+
/** Last-known active session count, updated by session.list fan-out */
|
|
12
|
+
private _activeSessionCount;
|
|
13
|
+
constructor(lockManager: LockManager, fuseEngine: FuseEngine);
|
|
14
|
+
/** Update the active session gauge (called after session.list fan-out) */
|
|
15
|
+
setActiveSessionCount(count: number): void;
|
|
16
|
+
get activeSessionCount(): number;
|
|
15
17
|
recordSessionCompleted(durationSeconds?: number): void;
|
|
16
18
|
recordSessionFailed(durationSeconds?: number): void;
|
|
17
19
|
recordSessionStopped(durationSeconds?: number): void;
|
|
18
|
-
|
|
20
|
+
recordFuseExpired(): void;
|
|
19
21
|
generateMetrics(): string;
|
|
20
22
|
}
|
package/dist/daemon/metrics.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
export class MetricsRegistry {
|
|
2
|
-
sessionTracker;
|
|
3
2
|
lockManager;
|
|
4
3
|
fuseEngine;
|
|
5
4
|
sessionsTotalCompleted = 0;
|
|
6
5
|
sessionsTotalFailed = 0;
|
|
7
6
|
sessionsTotalStopped = 0;
|
|
8
|
-
|
|
9
|
-
clustersDeletedTotal = 0;
|
|
7
|
+
fusesExpiredTotal = 0;
|
|
10
8
|
sessionDurations = []; // seconds
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/** Last-known active session count, updated by session.list fan-out */
|
|
10
|
+
_activeSessionCount = 0;
|
|
11
|
+
constructor(lockManager, fuseEngine) {
|
|
13
12
|
this.lockManager = lockManager;
|
|
14
13
|
this.fuseEngine = fuseEngine;
|
|
15
14
|
}
|
|
15
|
+
/** Update the active session gauge (called after session.list fan-out) */
|
|
16
|
+
setActiveSessionCount(count) {
|
|
17
|
+
this._activeSessionCount = count;
|
|
18
|
+
}
|
|
19
|
+
get activeSessionCount() {
|
|
20
|
+
return this._activeSessionCount;
|
|
21
|
+
}
|
|
16
22
|
recordSessionCompleted(durationSeconds) {
|
|
17
23
|
this.sessionsTotalCompleted++;
|
|
18
24
|
if (durationSeconds != null)
|
|
@@ -28,9 +34,8 @@ export class MetricsRegistry {
|
|
|
28
34
|
if (durationSeconds != null)
|
|
29
35
|
this.sessionDurations.push(durationSeconds);
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
this.clustersDeletedTotal++;
|
|
37
|
+
recordFuseExpired() {
|
|
38
|
+
this.fusesExpiredTotal++;
|
|
34
39
|
}
|
|
35
40
|
generateMetrics() {
|
|
36
41
|
const lines = [];
|
|
@@ -45,7 +50,7 @@ export class MetricsRegistry {
|
|
|
45
50
|
lines.push(labels ? `${name}{${labels}} ${value}` : `${name} ${value}`);
|
|
46
51
|
};
|
|
47
52
|
// Gauges
|
|
48
|
-
g("agentctl_sessions_active", "Number of active sessions", this.
|
|
53
|
+
g("agentctl_sessions_active", "Number of active sessions", this._activeSessionCount);
|
|
49
54
|
const locks = this.lockManager.listAll();
|
|
50
55
|
g("agentctl_locks_active", "Number of active locks", locks.filter((l) => l.type === "auto").length, 'type="auto"');
|
|
51
56
|
g("agentctl_locks_active", "Number of active locks", locks.filter((l) => l.type === "manual").length, 'type="manual"');
|
|
@@ -54,8 +59,7 @@ export class MetricsRegistry {
|
|
|
54
59
|
c("agentctl_sessions_total", "Total sessions by status", this.sessionsTotalCompleted, 'status="completed"');
|
|
55
60
|
c("agentctl_sessions_total", "Total sessions by status", this.sessionsTotalFailed, 'status="failed"');
|
|
56
61
|
c("agentctl_sessions_total", "Total sessions by status", this.sessionsTotalStopped, 'status="stopped"');
|
|
57
|
-
c("
|
|
58
|
-
c("agentctl_kind_clusters_deleted_total", "Total Kind clusters deleted", this.clustersDeletedTotal);
|
|
62
|
+
c("agentctl_fuses_expired_total", "Total fuses expired", this.fusesExpiredTotal);
|
|
59
63
|
// Histogram (session duration)
|
|
60
64
|
lines.push("# HELP agentctl_session_duration_seconds Session duration histogram");
|
|
61
65
|
lines.push("# TYPE agentctl_session_duration_seconds histogram");
|