@herdctl/core 5.10.1 → 5.12.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.
Files changed (60) hide show
  1. package/dist/fleet-manager/__tests__/agent-management.test.d.ts +9 -0
  2. package/dist/fleet-manager/__tests__/agent-management.test.d.ts.map +1 -0
  3. package/dist/fleet-manager/__tests__/agent-management.test.js +358 -0
  4. package/dist/fleet-manager/__tests__/agent-management.test.js.map +1 -0
  5. package/dist/fleet-manager/__tests__/errors.test.js +40 -2
  6. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
  7. package/dist/fleet-manager/__tests__/session-control.test.d.ts +14 -0
  8. package/dist/fleet-manager/__tests__/session-control.test.d.ts.map +1 -0
  9. package/dist/fleet-manager/__tests__/session-control.test.js +292 -0
  10. package/dist/fleet-manager/__tests__/session-control.test.js.map +1 -0
  11. package/dist/fleet-manager/__tests__/trigger.test.js +137 -2
  12. package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -1
  13. package/dist/fleet-manager/agent-management.d.ts +107 -0
  14. package/dist/fleet-manager/agent-management.d.ts.map +1 -0
  15. package/dist/fleet-manager/agent-management.js +237 -0
  16. package/dist/fleet-manager/agent-management.js.map +1 -0
  17. package/dist/fleet-manager/errors.d.ts +30 -0
  18. package/dist/fleet-manager/errors.d.ts.map +1 -1
  19. package/dist/fleet-manager/errors.js +40 -0
  20. package/dist/fleet-manager/errors.js.map +1 -1
  21. package/dist/fleet-manager/fleet-manager.d.ts +166 -2
  22. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  23. package/dist/fleet-manager/fleet-manager.js +289 -2
  24. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  25. package/dist/fleet-manager/index.d.ts +1 -0
  26. package/dist/fleet-manager/index.d.ts.map +1 -1
  27. package/dist/fleet-manager/index.js +2 -0
  28. package/dist/fleet-manager/index.js.map +1 -1
  29. package/dist/fleet-manager/job-control.d.ts.map +1 -1
  30. package/dist/fleet-manager/job-control.js +54 -9
  31. package/dist/fleet-manager/job-control.js.map +1 -1
  32. package/dist/fleet-manager/types.d.ts +28 -0
  33. package/dist/fleet-manager/types.d.ts.map +1 -1
  34. package/dist/runner/index.d.ts +1 -1
  35. package/dist/runner/index.d.ts.map +1 -1
  36. package/dist/runner/index.js +1 -1
  37. package/dist/runner/index.js.map +1 -1
  38. package/dist/runner/runtime/__tests__/cli-runtime.test.js +35 -0
  39. package/dist/runner/runtime/__tests__/cli-runtime.test.js.map +1 -1
  40. package/dist/runner/runtime/__tests__/cli-session-path.test.js +63 -6
  41. package/dist/runner/runtime/__tests__/cli-session-path.test.js.map +1 -1
  42. package/dist/runner/runtime/__tests__/docker-security.test.js +39 -0
  43. package/dist/runner/runtime/__tests__/docker-security.test.js.map +1 -1
  44. package/dist/runner/runtime/cli-session-path.d.ts +18 -6
  45. package/dist/runner/runtime/cli-session-path.d.ts.map +1 -1
  46. package/dist/runner/runtime/cli-session-path.js +48 -8
  47. package/dist/runner/runtime/cli-session-path.js.map +1 -1
  48. package/dist/runner/runtime/index.d.ts +1 -0
  49. package/dist/runner/runtime/index.d.ts.map +1 -1
  50. package/dist/runner/runtime/index.js +5 -0
  51. package/dist/runner/runtime/index.js.map +1 -1
  52. package/dist/scheduler/errors.d.ts +1 -0
  53. package/dist/scheduler/errors.d.ts.map +1 -1
  54. package/dist/state/__tests__/session-discovery.test.js +172 -6
  55. package/dist/state/__tests__/session-discovery.test.js.map +1 -1
  56. package/dist/state/session-discovery.d.ts +45 -0
  57. package/dist/state/session-discovery.d.ts.map +1 -1
  58. package/dist/state/session-discovery.js +78 -4
  59. package/dist/state/session-discovery.js.map +1 -1
  60. package/package.json +1 -1
@@ -11,9 +11,10 @@
11
11
  * @module fleet-manager
12
12
  */
13
13
  import { EventEmitter } from "node:events";
14
- import { type ResolvedAgent, type ResolvedConfig } from "../config/index.js";
14
+ import { type AgentConfig, type ResolvedAgent, type ResolvedConfig } from "../config/index.js";
15
15
  import { Scheduler } from "../scheduler/index.js";
16
- import { type StateDirectory } from "../state/index.js";
16
+ import { type ChatMessage, type DiscoveredSession, type StateDirectory } from "../state/index.js";
17
+ import { type AddAgentOptions } from "./agent-management.js";
17
18
  import type { IChatManager } from "./chat-manager-interface.js";
18
19
  import type { FleetManagerContext } from "./context.js";
19
20
  import type { AgentInfo, CancelJobResult, ConfigChange, ConfigReloadedPayload, FleetManagerLogger, FleetManagerOptions, FleetManagerState, FleetManagerStatus, FleetManagerStopOptions, FleetStatus, ForkJobResult, JobModifications, LogEntry, LogStreamOptions, ScheduleInfo, TriggerOptions, TriggerResult } from "./types.js";
@@ -40,9 +41,12 @@ export declare class FleetManager extends EventEmitter implements FleetManagerCo
40
41
  private statusQueries;
41
42
  private scheduleManagement;
42
43
  private configReloadModule;
44
+ private agentManagement;
43
45
  private jobControl;
44
46
  private logStreaming;
45
47
  private scheduleExecutor;
48
+ private sessionDiscovery;
49
+ private sessionMetadataStore;
46
50
  private chatManagers;
47
51
  constructor(options: FleetManagerOptions);
48
52
  getConfig(): ResolvedConfig | null;
@@ -92,6 +96,142 @@ export declare class FleetManager extends EventEmitter implements FleetManagerCo
92
96
  disableSchedule(agentName: string, scheduleName: string): Promise<ScheduleInfo>;
93
97
  reload(): Promise<ConfigReloadedPayload>;
94
98
  computeConfigChanges(oldConfig: ResolvedConfig | null, newConfig: ResolvedConfig): ConfigChange[];
99
+ /**
100
+ * Register an agent at runtime without writing YAML or calling `reload()`.
101
+ *
102
+ * The config is validated, merged with fleet defaults, normalized (working
103
+ * directory resolved to an absolute path), and wired into the running fleet
104
+ * so it is immediately triggerable and appears in fleet status. A
105
+ * `config:reloaded` event is emitted describing the change.
106
+ *
107
+ * This is the programmatic counterpart to editing `herdctl.yaml` and calling
108
+ * `reload()` — useful for apps that manage agents in memory rather than on
109
+ * disk.
110
+ *
111
+ * @param agent - The agent configuration to register (must include `name`)
112
+ * @param options - Resolution options (base dir, defaults merge, replace)
113
+ * @returns Info for the newly registered agent
114
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
115
+ * @throws {ConfigurationError} If validation fails or the name collides
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * await fleet.addAgent({
120
+ * name: "keeper-myproject",
121
+ * working_directory: "/abs/projects/myproject",
122
+ * runtime: "cli",
123
+ * permission_mode: "acceptEdits",
124
+ * });
125
+ * await fleet.trigger("keeper-myproject", undefined, { prompt: "Hello" });
126
+ * ```
127
+ */
128
+ addAgent(agent: AgentConfig | (Record<string, unknown> & {
129
+ name: string;
130
+ }), options?: AddAgentOptions): Promise<AgentInfo>;
131
+ /**
132
+ * Unregister an agent at runtime.
133
+ *
134
+ * Removes the agent from the in-memory config and the scheduler. Accepts a
135
+ * qualified name or a local name. Running jobs are unaffected.
136
+ *
137
+ * @param name - The agent qualified name or local name to remove
138
+ * @returns `true` if an agent was removed, `false` if no match was found
139
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
140
+ */
141
+ removeAgent(name: string): Promise<boolean>;
142
+ /**
143
+ * List discovered Claude Code sessions for an agent.
144
+ *
145
+ * Derives the agent's working directory and Docker mode from the loaded
146
+ * config, so consumers don't have to map agent → working directory by hand.
147
+ * Sessions are returned sorted by modification time (newest first).
148
+ *
149
+ * Note: sessions are keyed by working directory. This method uses the agent's
150
+ * *configured* `working_directory`. If you triggered the agent with a
151
+ * per-trigger `workingDirectory` override (see {@link TriggerOptions}), the
152
+ * resulting sessions live under that override directory and will NOT appear
153
+ * here — list them by scanning that directory instead (e.g. the directory
154
+ * grouped `getAllSessions` view on the underlying `SessionDiscoveryService`).
155
+ *
156
+ * @param name - The agent qualified name or local name
157
+ * @param options - Optional `limit` for top-N enrichment
158
+ * @returns Array of discovered sessions (empty if the agent has no working
159
+ * directory or no sessions yet)
160
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
161
+ * @throws {AgentNotFoundError} If no agent with that name exists
162
+ */
163
+ getAgentSessions(name: string, options?: {
164
+ limit?: number;
165
+ }): Promise<DiscoveredSession[]>;
166
+ /**
167
+ * Get the parsed chat messages for one of an agent's sessions.
168
+ *
169
+ * Derives the working directory and Docker mode from the loaded config.
170
+ *
171
+ * @param name - The agent qualified name or local name
172
+ * @param sessionId - The session ID to read
173
+ * @returns Array of chat messages (empty if the agent has no working directory)
174
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
175
+ * @throws {AgentNotFoundError} If no agent with that name exists
176
+ */
177
+ getAgentSessionMessages(name: string, sessionId: string): Promise<ChatMessage[]>;
178
+ /**
179
+ * Delete one of an agent's Claude Code session transcripts from disk.
180
+ *
181
+ * Resolves the agent's working directory and Docker mode from the loaded
182
+ * config, computes the CLI (or Docker) transcript file path with the same
183
+ * encoder Claude Code uses, deletes it, and invalidates the session-discovery
184
+ * cache so a subsequent {@link getAgentSessions} no longer lists it.
185
+ *
186
+ * The `sessionId` is validated (only `[A-Za-z0-9-]` is allowed) to prevent
187
+ * path traversal before any filesystem access.
188
+ *
189
+ * @param name - The agent qualified name or local name
190
+ * @param sessionId - The session ID whose transcript should be removed
191
+ * @returns `true` if a file was removed, `false` if no transcript existed (or
192
+ * the agent has no working directory)
193
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
194
+ * @throws {AgentNotFoundError} If no agent with that name exists
195
+ * @throws {Error} If `sessionId` contains invalid characters
196
+ */
197
+ deleteSession(name: string, sessionId: string): Promise<boolean>;
198
+ /**
199
+ * Set (or clear) the custom display name for one of an agent's sessions.
200
+ *
201
+ * Writes through this fleet's shared {@link SessionMetadataStore} — the same
202
+ * store the session-discovery service reads — so a subsequent
203
+ * {@link getAgentSessions} reflects the new `customName` immediately. Passing
204
+ * `null` or an empty/whitespace string clears any existing custom name.
205
+ *
206
+ * @param name - The agent qualified name or local name
207
+ * @param sessionId - The session ID to (re)name
208
+ * @param customName - The custom name to set, or `null`/empty to clear it
209
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
210
+ * @throws {AgentNotFoundError} If no agent with that name exists
211
+ */
212
+ setSessionName(name: string, sessionId: string, customName: string | null): Promise<void>;
213
+ /**
214
+ * Drop the cached session listing for an agent so the next
215
+ * {@link getAgentSessions} call rebuilds it from disk.
216
+ *
217
+ * The underlying {@link SessionDiscoveryService} caches each working
218
+ * directory's listing for up to its TTL (default 30s). That cache is now
219
+ * mtime-aware, so a *newly created* transcript file is normally picked up
220
+ * immediately. Call this when you want to force a fresh listing regardless —
221
+ * e.g. after each chat turn, or on filesystems whose directory mtime has
222
+ * coarse (1-second) granularity where a same-second create might otherwise be
223
+ * masked until the TTL expires. It also drops the attribution index so a
224
+ * session created this turn (whose job record was just written) is attributed.
225
+ *
226
+ * Resolves the agent's working directory and Docker mode from the loaded
227
+ * config (no override directories — see {@link getAgentSessions}). A no-op
228
+ * when the agent has no working directory.
229
+ *
230
+ * @param name - The agent qualified name or local name
231
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
232
+ * @throws {AgentNotFoundError} If no agent with that name exists
233
+ */
234
+ invalidateSessions(name: string): void;
95
235
  trigger(agentName: string, scheduleName?: string, options?: TriggerOptions): Promise<TriggerResult>;
96
236
  cancelJob(jobId: string, options?: {
97
237
  timeout?: number;
@@ -133,6 +273,30 @@ export declare class FleetManager extends EventEmitter implements FleetManagerCo
133
273
  * @throws ConfigurationError if duplicate qualified names are found
134
274
  */
135
275
  private validateUniqueAgentNames;
276
+ /**
277
+ * Get (lazily creating) the shared SessionMetadataStore bound to this fleet's
278
+ * state directory. Shared with the SessionDiscoveryService so metadata writes
279
+ * (e.g. {@link setSessionName}) and reads (during discovery) use one cache.
280
+ */
281
+ private getSessionMetadataStore;
282
+ /**
283
+ * Get (lazily creating) the SessionDiscoveryService bound to this fleet's
284
+ * state directory. Reused across calls so its caches are shared. The
285
+ * service shares this fleet's {@link SessionMetadataStore} so custom-name
286
+ * writes are immediately visible to discovery.
287
+ */
288
+ private getSessionDiscovery;
289
+ /**
290
+ * Look up an agent by qualified or local name and derive the inputs the
291
+ * SessionDiscoveryService needs (working directory + Docker mode).
292
+ *
293
+ * @param name - The agent qualified name or local name
294
+ * @param operation - The public operation name, used for the
295
+ * {@link InvalidStateError} message when the fleet is uninitialized
296
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
297
+ * @throws {AgentNotFoundError} If no agent with that name exists
298
+ */
299
+ private resolveAgentForSessions;
136
300
  private initializeStateDir;
137
301
  private startSchedulerAsync;
138
302
  private handleScheduleTrigger;
@@ -1 +1 @@
1
- {"version":3,"file":"fleet-manager.d.ts","sourceRoot":"","sources":["../../src/fleet-manager/fleet-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fleet-manager.d.ts","sourceRoot":"","sources":["../../src/fleet-manager/fleet-manager.ts"],"names":[],"mappings":"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"}
@@ -11,19 +11,23 @@
11
11
  * @module fleet-manager
12
12
  */
13
13
  import { EventEmitter } from "node:events";
14
+ import { rm } from "node:fs/promises";
14
15
  import { resolve } from "node:path";
15
16
  import { ConfigError, ConfigNotFoundError, loadConfig, } from "../config/index.js";
17
+ import { getCliSessionFile, getDockerSessionFile } from "../runner/runtime/cli-session-path.js";
16
18
  import { Scheduler } from "../scheduler/index.js";
17
- import { initStateDirectory } from "../state/index.js";
19
+ import { initStateDirectory, SessionDiscoveryService, SessionMetadataStore, } from "../state/index.js";
18
20
  import { createLogger } from "../utils/logger.js";
21
+ import { AgentManagement } from "./agent-management.js";
19
22
  import { ConfigReload, computeConfigChanges } from "./config-reload.js";
20
- import { ConfigurationError, FleetManagerShutdownError, FleetManagerStateDirError, InvalidStateError, } from "./errors.js";
23
+ import { AgentNotFoundError, ConfigurationError, FleetManagerShutdownError, FleetManagerStateDirError, InvalidStateError, } from "./errors.js";
21
24
  import { JobControl } from "./job-control.js";
22
25
  import { LogStreaming } from "./log-streaming.js";
23
26
  import { ScheduleExecutor } from "./schedule-executor.js";
24
27
  import { ScheduleManagement } from "./schedule-management.js";
25
28
  // Module classes
26
29
  import { StatusQueries } from "./status-queries.js";
30
+ import { resolveWorkingDirectory } from "./working-directory-helper.js";
27
31
  const DEFAULT_CHECK_INTERVAL = 1000;
28
32
  function createDefaultLogger() {
29
33
  return createLogger("fleet-manager");
@@ -55,9 +59,16 @@ export class FleetManager extends EventEmitter {
55
59
  statusQueries;
56
60
  scheduleManagement;
57
61
  configReloadModule;
62
+ agentManagement;
58
63
  jobControl;
59
64
  logStreaming;
60
65
  scheduleExecutor;
66
+ // Lazily-created session discovery service (for getAgentSessions/* helpers)
67
+ sessionDiscovery = null;
68
+ // Lazily-created session metadata store, shared with sessionDiscovery so a
69
+ // setSessionName() write is reflected by a subsequent getAgentSessions()
70
+ // without a stale in-memory cache.
71
+ sessionMetadataStore = null;
61
72
  // Chat managers (Discord, Slack, etc.)
62
73
  // Key is platform name (e.g., "discord", "slack")
63
74
  chatManagers = new Map();
@@ -385,6 +396,218 @@ export class FleetManager extends EventEmitter {
385
396
  computeConfigChanges(oldConfig, newConfig) {
386
397
  return computeConfigChanges(oldConfig, newConfig);
387
398
  }
399
+ // Programmatic Agent Management
400
+ /**
401
+ * Register an agent at runtime without writing YAML or calling `reload()`.
402
+ *
403
+ * The config is validated, merged with fleet defaults, normalized (working
404
+ * directory resolved to an absolute path), and wired into the running fleet
405
+ * so it is immediately triggerable and appears in fleet status. A
406
+ * `config:reloaded` event is emitted describing the change.
407
+ *
408
+ * This is the programmatic counterpart to editing `herdctl.yaml` and calling
409
+ * `reload()` — useful for apps that manage agents in memory rather than on
410
+ * disk.
411
+ *
412
+ * @param agent - The agent configuration to register (must include `name`)
413
+ * @param options - Resolution options (base dir, defaults merge, replace)
414
+ * @returns Info for the newly registered agent
415
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
416
+ * @throws {ConfigurationError} If validation fails or the name collides
417
+ *
418
+ * @example
419
+ * ```typescript
420
+ * await fleet.addAgent({
421
+ * name: "keeper-myproject",
422
+ * working_directory: "/abs/projects/myproject",
423
+ * runtime: "cli",
424
+ * permission_mode: "acceptEdits",
425
+ * });
426
+ * await fleet.trigger("keeper-myproject", undefined, { prompt: "Hello" });
427
+ * ```
428
+ */
429
+ async addAgent(agent, options) {
430
+ return this.agentManagement.addAgent(agent, options);
431
+ }
432
+ /**
433
+ * Unregister an agent at runtime.
434
+ *
435
+ * Removes the agent from the in-memory config and the scheduler. Accepts a
436
+ * qualified name or a local name. Running jobs are unaffected.
437
+ *
438
+ * @param name - The agent qualified name or local name to remove
439
+ * @returns `true` if an agent was removed, `false` if no match was found
440
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
441
+ */
442
+ async removeAgent(name) {
443
+ return this.agentManagement.removeAgent(name);
444
+ }
445
+ // Session Access (convenience wrappers over SessionDiscoveryService)
446
+ /**
447
+ * List discovered Claude Code sessions for an agent.
448
+ *
449
+ * Derives the agent's working directory and Docker mode from the loaded
450
+ * config, so consumers don't have to map agent → working directory by hand.
451
+ * Sessions are returned sorted by modification time (newest first).
452
+ *
453
+ * Note: sessions are keyed by working directory. This method uses the agent's
454
+ * *configured* `working_directory`. If you triggered the agent with a
455
+ * per-trigger `workingDirectory` override (see {@link TriggerOptions}), the
456
+ * resulting sessions live under that override directory and will NOT appear
457
+ * here — list them by scanning that directory instead (e.g. the directory
458
+ * grouped `getAllSessions` view on the underlying `SessionDiscoveryService`).
459
+ *
460
+ * @param name - The agent qualified name or local name
461
+ * @param options - Optional `limit` for top-N enrichment
462
+ * @returns Array of discovered sessions (empty if the agent has no working
463
+ * directory or no sessions yet)
464
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
465
+ * @throws {AgentNotFoundError} If no agent with that name exists
466
+ */
467
+ async getAgentSessions(name, options) {
468
+ const { agent, workingDirectory, dockerEnabled } = this.resolveAgentForSessions(name, "getAgentSessions");
469
+ if (!workingDirectory) {
470
+ return [];
471
+ }
472
+ return this.getSessionDiscovery().getAgentSessions(agent.qualifiedName, workingDirectory, dockerEnabled, options);
473
+ }
474
+ /**
475
+ * Get the parsed chat messages for one of an agent's sessions.
476
+ *
477
+ * Derives the working directory and Docker mode from the loaded config.
478
+ *
479
+ * @param name - The agent qualified name or local name
480
+ * @param sessionId - The session ID to read
481
+ * @returns Array of chat messages (empty if the agent has no working directory)
482
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
483
+ * @throws {AgentNotFoundError} If no agent with that name exists
484
+ */
485
+ async getAgentSessionMessages(name, sessionId) {
486
+ const { workingDirectory, dockerEnabled } = this.resolveAgentForSessions(name, "getAgentSessionMessages");
487
+ if (!workingDirectory) {
488
+ return [];
489
+ }
490
+ return this.getSessionDiscovery().getSessionMessages(workingDirectory, sessionId, {
491
+ dockerEnabled,
492
+ });
493
+ }
494
+ /**
495
+ * Delete one of an agent's Claude Code session transcripts from disk.
496
+ *
497
+ * Resolves the agent's working directory and Docker mode from the loaded
498
+ * config, computes the CLI (or Docker) transcript file path with the same
499
+ * encoder Claude Code uses, deletes it, and invalidates the session-discovery
500
+ * cache so a subsequent {@link getAgentSessions} no longer lists it.
501
+ *
502
+ * The `sessionId` is validated (only `[A-Za-z0-9-]` is allowed) to prevent
503
+ * path traversal before any filesystem access.
504
+ *
505
+ * @param name - The agent qualified name or local name
506
+ * @param sessionId - The session ID whose transcript should be removed
507
+ * @returns `true` if a file was removed, `false` if no transcript existed (or
508
+ * the agent has no working directory)
509
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
510
+ * @throws {AgentNotFoundError} If no agent with that name exists
511
+ * @throws {Error} If `sessionId` contains invalid characters
512
+ */
513
+ async deleteSession(name, sessionId) {
514
+ const { workingDirectory, dockerEnabled } = this.resolveAgentForSessions(name, "deleteSession");
515
+ if (!workingDirectory) {
516
+ return false;
517
+ }
518
+ // Compute the transcript path. getCliSessionFile/getDockerSessionFile both
519
+ // reject a sessionId containing anything other than [A-Za-z0-9-], so this
520
+ // throws before touching the filesystem when traversal is attempted.
521
+ const sessionFile = dockerEnabled
522
+ ? getDockerSessionFile(this.stateDir, sessionId)
523
+ : getCliSessionFile(workingDirectory, sessionId);
524
+ let removed;
525
+ try {
526
+ await rm(sessionFile);
527
+ removed = true;
528
+ }
529
+ catch (error) {
530
+ if (error.code === "ENOENT") {
531
+ // No transcript on disk — nothing to remove.
532
+ removed = false;
533
+ }
534
+ else {
535
+ throw error;
536
+ }
537
+ }
538
+ // Invalidate the discovery cache so the deleted session disappears from
539
+ // subsequent listings even within the cache TTL window.
540
+ this.getSessionDiscovery().invalidateCache(workingDirectory, { dockerEnabled });
541
+ this.logger.debug(`deleteSession: ${removed ? "removed" : "no file for"} session "${sessionId}" of agent "${name}"`);
542
+ return removed;
543
+ }
544
+ /**
545
+ * Set (or clear) the custom display name for one of an agent's sessions.
546
+ *
547
+ * Writes through this fleet's shared {@link SessionMetadataStore} — the same
548
+ * store the session-discovery service reads — so a subsequent
549
+ * {@link getAgentSessions} reflects the new `customName` immediately. Passing
550
+ * `null` or an empty/whitespace string clears any existing custom name.
551
+ *
552
+ * @param name - The agent qualified name or local name
553
+ * @param sessionId - The session ID to (re)name
554
+ * @param customName - The custom name to set, or `null`/empty to clear it
555
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
556
+ * @throws {AgentNotFoundError} If no agent with that name exists
557
+ */
558
+ async setSessionName(name, sessionId, customName) {
559
+ // Resolve to validate state + agent existence and to key metadata by the
560
+ // agent's qualified name (consistent with how discovery stores custom names).
561
+ const { agent } = this.resolveAgentForSessions(name, "setSessionName");
562
+ const store = this.getSessionMetadataStore();
563
+ const trimmed = customName?.trim();
564
+ if (trimmed) {
565
+ await store.setCustomName(agent.qualifiedName, sessionId, trimmed);
566
+ this.logger.debug(`setSessionName: set custom name for session "${sessionId}" of agent "${agent.qualifiedName}"`);
567
+ }
568
+ else {
569
+ await store.removeCustomName(agent.qualifiedName, sessionId);
570
+ this.logger.debug(`setSessionName: cleared custom name for session "${sessionId}" of agent "${agent.qualifiedName}"`);
571
+ }
572
+ // Custom names are read live (not part of the directory listing cache), and
573
+ // the metadata store is shared with discovery, so the change is already
574
+ // visible. Invalidate the directory listing too for safety/consistency.
575
+ const workingDirectory = resolveWorkingDirectory(agent);
576
+ if (workingDirectory) {
577
+ this.getSessionDiscovery().invalidateCache(workingDirectory, {
578
+ dockerEnabled: agent.docker?.enabled === true,
579
+ });
580
+ }
581
+ }
582
+ /**
583
+ * Drop the cached session listing for an agent so the next
584
+ * {@link getAgentSessions} call rebuilds it from disk.
585
+ *
586
+ * The underlying {@link SessionDiscoveryService} caches each working
587
+ * directory's listing for up to its TTL (default 30s). That cache is now
588
+ * mtime-aware, so a *newly created* transcript file is normally picked up
589
+ * immediately. Call this when you want to force a fresh listing regardless —
590
+ * e.g. after each chat turn, or on filesystems whose directory mtime has
591
+ * coarse (1-second) granularity where a same-second create might otherwise be
592
+ * masked until the TTL expires. It also drops the attribution index so a
593
+ * session created this turn (whose job record was just written) is attributed.
594
+ *
595
+ * Resolves the agent's working directory and Docker mode from the loaded
596
+ * config (no override directories — see {@link getAgentSessions}). A no-op
597
+ * when the agent has no working directory.
598
+ *
599
+ * @param name - The agent qualified name or local name
600
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
601
+ * @throws {AgentNotFoundError} If no agent with that name exists
602
+ */
603
+ invalidateSessions(name) {
604
+ const { workingDirectory, dockerEnabled } = this.resolveAgentForSessions(name, "invalidateSessions");
605
+ if (!workingDirectory) {
606
+ return;
607
+ }
608
+ this.getSessionDiscovery().invalidateWorkingDirectory(workingDirectory, { dockerEnabled });
609
+ this.logger.debug(`invalidateSessions: cleared session cache for agent "${name}"`);
610
+ }
388
611
  // Job Control
389
612
  async trigger(agentName, scheduleName, options) {
390
613
  return this.jobControl.trigger(agentName, scheduleName, options);
@@ -417,6 +640,9 @@ export class FleetManager extends EventEmitter {
417
640
  this.configReloadModule = new ConfigReload(this, () => this.loadConfiguration(), (config) => {
418
641
  this.config = config;
419
642
  });
643
+ this.agentManagement = new AgentManagement(this, (config) => {
644
+ this.config = config;
645
+ }, (name) => this.statusQueries.getAgentInfoByName(name));
420
646
  this.jobControl = new JobControl(this, () => this.statusQueries.getAgentInfo());
421
647
  this.logStreaming = new LogStreaming(this);
422
648
  this.scheduleExecutor = new ScheduleExecutor(this);
@@ -576,6 +802,67 @@ export class FleetManager extends EventEmitter {
576
802
  throw new ConfigurationError(`Duplicate agent qualified names found: ${duplicateList}. Agent names must be unique within a fleet.`);
577
803
  }
578
804
  }
805
+ /**
806
+ * Get (lazily creating) the shared SessionMetadataStore bound to this fleet's
807
+ * state directory. Shared with the SessionDiscoveryService so metadata writes
808
+ * (e.g. {@link setSessionName}) and reads (during discovery) use one cache.
809
+ */
810
+ getSessionMetadataStore() {
811
+ if (!this.sessionMetadataStore) {
812
+ this.sessionMetadataStore = new SessionMetadataStore(this.stateDir);
813
+ }
814
+ return this.sessionMetadataStore;
815
+ }
816
+ /**
817
+ * Get (lazily creating) the SessionDiscoveryService bound to this fleet's
818
+ * state directory. Reused across calls so its caches are shared. The
819
+ * service shares this fleet's {@link SessionMetadataStore} so custom-name
820
+ * writes are immediately visible to discovery.
821
+ */
822
+ getSessionDiscovery() {
823
+ if (!this.sessionDiscovery) {
824
+ this.sessionDiscovery = new SessionDiscoveryService({
825
+ stateDir: this.stateDir,
826
+ sessionMetadataStore: this.getSessionMetadataStore(),
827
+ });
828
+ }
829
+ return this.sessionDiscovery;
830
+ }
831
+ /**
832
+ * Look up an agent by qualified or local name and derive the inputs the
833
+ * SessionDiscoveryService needs (working directory + Docker mode).
834
+ *
835
+ * @param name - The agent qualified name or local name
836
+ * @param operation - The public operation name, used for the
837
+ * {@link InvalidStateError} message when the fleet is uninitialized
838
+ * @throws {InvalidStateError} If the fleet manager is not yet initialized
839
+ * @throws {AgentNotFoundError} If no agent with that name exists
840
+ */
841
+ resolveAgentForSessions(name, operation) {
842
+ // Guard against pre-init calls. Without this, an uninitialized fleet has a
843
+ // null config, so the agent lookup below would throw AgentNotFoundError —
844
+ // masking the real cause and contradicting the documented behavior (these
845
+ // helpers throw InvalidStateError before initialize()).
846
+ if (this.status === "uninitialized" || !this.config) {
847
+ throw new InvalidStateError(operation, this.status, [
848
+ "initialized",
849
+ "starting",
850
+ "running",
851
+ "stopping",
852
+ "stopped",
853
+ ]);
854
+ }
855
+ const agents = this.config.agents;
856
+ const agent = agents.find((a) => a.qualifiedName === name) ?? agents.find((a) => a.name === name);
857
+ if (!agent) {
858
+ throw new AgentNotFoundError(name);
859
+ }
860
+ return {
861
+ agent,
862
+ workingDirectory: resolveWorkingDirectory(agent),
863
+ dockerEnabled: agent.docker?.enabled === true,
864
+ };
865
+ }
579
866
  async initializeStateDir() {
580
867
  try {
581
868
  return await initStateDirectory({ path: this.stateDir });