@ai-devkit/agent-manager 0.26.4 → 0.28.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 (110) hide show
  1. package/README.md +12 -4
  2. package/dist/AgentManager.d.ts +5 -0
  3. package/dist/AgentManager.d.ts.map +1 -1
  4. package/dist/AgentManager.js +21 -1
  5. package/dist/AgentManager.js.map +1 -1
  6. package/dist/__tests__/AgentManager.test.js +112 -0
  7. package/dist/__tests__/AgentManager.test.js.map +1 -1
  8. package/dist/__tests__/adapters/CodexAdapter.test.js +199 -0
  9. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  10. package/dist/__tests__/database/DurableAgentsDatabase.test.js +100 -0
  11. package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -0
  12. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +7 -7
  13. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
  14. package/dist/__tests__/print/ClaudePrintAgentService.test.js +7 -7
  15. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
  16. package/dist/__tests__/print/ClaudePrintRunner.test.js +1 -1
  17. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
  18. package/dist/__tests__/print/DurableAgent.test.js +17 -0
  19. package/dist/__tests__/print/DurableAgent.test.js.map +1 -0
  20. package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js +186 -0
  21. package/dist/__tests__/print/DurableAgentRepository.sqlite.test.js.map +1 -0
  22. package/dist/__tests__/print/{PrintAgentStore.test.js → DurableAgentRepository.test.js} +63 -110
  23. package/dist/__tests__/print/DurableAgentRepository.test.js.map +1 -0
  24. package/dist/__tests__/utils/AgentRegistry.test.js +67 -0
  25. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  26. package/dist/adapters/AgentAdapter.d.ts +2 -0
  27. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  28. package/dist/adapters/AgentAdapter.js.map +1 -1
  29. package/dist/adapters/CodexAdapter.d.ts +1 -0
  30. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  31. package/dist/adapters/CodexAdapter.js +16 -6
  32. package/dist/adapters/CodexAdapter.js.map +1 -1
  33. package/dist/database/connection.d.ts +1 -0
  34. package/dist/database/connection.d.ts.map +1 -1
  35. package/dist/database/connection.js +20 -5
  36. package/dist/database/connection.js.map +1 -1
  37. package/dist/database/migrations/002_pins.sql +1 -0
  38. package/dist/database/migrations/003_durable_agents.sql +43 -0
  39. package/dist/durable/ClaudeCliProbe.d.ts.map +1 -0
  40. package/dist/{print → durable}/ClaudeCliProbe.js +1 -1
  41. package/dist/durable/ClaudeCliProbe.js.map +1 -0
  42. package/dist/{print → durable}/ClaudePrintAgentService.d.ts +11 -11
  43. package/dist/durable/ClaudePrintAgentService.d.ts.map +1 -0
  44. package/dist/{print → durable}/ClaudePrintAgentService.js +12 -12
  45. package/dist/durable/ClaudePrintAgentService.js.map +1 -0
  46. package/dist/{print → durable}/ClaudePrintRunner.d.ts +3 -3
  47. package/dist/durable/ClaudePrintRunner.d.ts.map +1 -0
  48. package/dist/{print → durable}/ClaudePrintRunner.js +2 -2
  49. package/dist/durable/ClaudePrintRunner.js.map +1 -0
  50. package/dist/durable/DurableAgent.d.ts +61 -0
  51. package/dist/durable/DurableAgent.d.ts.map +1 -0
  52. package/dist/durable/DurableAgent.js +46 -0
  53. package/dist/durable/DurableAgent.js.map +1 -0
  54. package/dist/durable/DurableAgentRepository.d.ts +60 -0
  55. package/dist/durable/DurableAgentRepository.d.ts.map +1 -0
  56. package/dist/durable/DurableAgentRepository.js +325 -0
  57. package/dist/durable/DurableAgentRepository.js.map +1 -0
  58. package/dist/index.d.ts +12 -12
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +7 -7
  61. package/dist/index.js.map +1 -1
  62. package/dist/utils/AgentRegistry.d.ts +5 -0
  63. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  64. package/dist/utils/AgentRegistry.js +19 -2
  65. package/dist/utils/AgentRegistry.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/AgentManager.ts +21 -0
  68. package/src/__tests__/AgentManager.test.ts +117 -0
  69. package/src/__tests__/adapters/CodexAdapter.test.ts +132 -0
  70. package/src/__tests__/database/DurableAgentsDatabase.test.ts +77 -0
  71. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +6 -6
  72. package/src/__tests__/print/ClaudePrintAgentService.test.ts +7 -7
  73. package/src/__tests__/print/ClaudePrintRunner.test.ts +3 -3
  74. package/src/__tests__/print/{PrintAgent.test.ts → DurableAgent.test.ts} +6 -6
  75. package/src/__tests__/print/DurableAgentRepository.sqlite.test.ts +105 -0
  76. package/src/__tests__/print/DurableAgentRepository.test.ts +163 -0
  77. package/src/__tests__/utils/AgentRegistry.test.ts +68 -0
  78. package/src/adapters/AgentAdapter.ts +3 -0
  79. package/src/adapters/CodexAdapter.ts +14 -11
  80. package/src/database/connection.ts +18 -5
  81. package/src/database/migrations/002_pins.sql +1 -0
  82. package/src/database/migrations/003_durable_agents.sql +43 -0
  83. package/src/{print → durable}/ClaudeCliProbe.ts +1 -1
  84. package/src/{print → durable}/ClaudePrintAgentService.ts +21 -21
  85. package/src/{print → durable}/ClaudePrintRunner.ts +4 -4
  86. package/src/durable/DurableAgent.ts +91 -0
  87. package/src/durable/DurableAgentRepository.ts +307 -0
  88. package/src/index.ts +27 -26
  89. package/src/utils/AgentRegistry.ts +21 -0
  90. package/dist/__tests__/print/PrintAgent.test.js +0 -17
  91. package/dist/__tests__/print/PrintAgent.test.js.map +0 -1
  92. package/dist/__tests__/print/PrintAgentStore.test.js.map +0 -1
  93. package/dist/print/ClaudeCliProbe.d.ts.map +0 -1
  94. package/dist/print/ClaudeCliProbe.js.map +0 -1
  95. package/dist/print/ClaudePrintAgentService.d.ts.map +0 -1
  96. package/dist/print/ClaudePrintAgentService.js.map +0 -1
  97. package/dist/print/ClaudePrintRunner.d.ts.map +0 -1
  98. package/dist/print/ClaudePrintRunner.js.map +0 -1
  99. package/dist/print/PrintAgent.d.ts +0 -57
  100. package/dist/print/PrintAgent.d.ts.map +0 -1
  101. package/dist/print/PrintAgent.js +0 -42
  102. package/dist/print/PrintAgent.js.map +0 -1
  103. package/dist/print/PrintAgentStore.d.ts +0 -69
  104. package/dist/print/PrintAgentStore.d.ts.map +0 -1
  105. package/dist/print/PrintAgentStore.js +0 -484
  106. package/dist/print/PrintAgentStore.js.map +0 -1
  107. package/src/__tests__/print/PrintAgentStore.test.ts +0 -192
  108. package/src/print/PrintAgent.ts +0 -86
  109. package/src/print/PrintAgentStore.ts +0 -503
  110. /package/dist/{print → durable}/ClaudeCliProbe.d.ts +0 -0
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
+ import Database from 'better-sqlite3';
4
5
  import { AgentRegistry, RenameNotFoundError, RenameConflictError, type RegistryEntry } from '../../utils/AgentRegistry.js';
5
6
 
6
7
  function makeEntry(over: Partial<RegistryEntry> = {}): RegistryEntry {
@@ -13,6 +14,7 @@ function makeEntry(over: Partial<RegistryEntry> = {}): RegistryEntry {
13
14
  startedAt: '2026-05-30T00:00:00.000Z',
14
15
  sessionId: 'sid-1',
15
16
  sessionFilePath: '/tmp/session.jsonl',
17
+ pinned: false,
16
18
  ...over,
17
19
  };
18
20
  }
@@ -172,6 +174,72 @@ describe('AgentRegistry', () => {
172
174
  });
173
175
  });
174
176
 
177
+ describe('pinning', () => {
178
+ it('defaults new rows to unpinned and toggles the persisted state', () => {
179
+ registry.register(makeEntry());
180
+
181
+ expect(registry.lookup('agent1')?.pinned).toBe(false);
182
+ expect(registry.togglePin('claude', process.pid)).toBe(true);
183
+ expect(registry.lookup('agent1')?.pinned).toBe(true);
184
+ expect(registry.togglePin('claude', process.pid)).toBe(false);
185
+ expect(registry.lookup('agent1')?.pinned).toBe(false);
186
+ });
187
+
188
+ it('updates existing recency when toggled', () => {
189
+ let now = new Date('2026-08-16T10:00:00.000Z');
190
+ const clocked = new AgentRegistry(regPath, { now: () => now });
191
+ clocked.register(makeEntry());
192
+ now = new Date('2026-08-16T10:01:00.000Z');
193
+
194
+ clocked.togglePin('claude', process.pid);
195
+
196
+ expect(clocked.lookup('agent1')?.updatedAt).toBe(now.toISOString());
197
+ const db = new Database(regPath.replace(/\.json$/, '.db'), { readonly: true });
198
+ const row = db.prepare('SELECT updated_at FROM agents WHERE type = ? AND pid = ?')
199
+ .get('claude', process.pid) as { updated_at: string };
200
+ db.close();
201
+ expect(row.updated_at).toBe(now.toISOString());
202
+ });
203
+
204
+ it('returns null when the process row has disappeared', () => {
205
+ expect(registry.togglePin('claude', 999999)).toBeNull();
206
+ });
207
+
208
+ it('preserves a pin when poll registration updates the row', () => {
209
+ registry.register(makeEntry({ sessionId: 'before' }));
210
+ registry.togglePin('claude', process.pid);
211
+
212
+ registry.register(makeEntry({ sessionId: 'after' }));
213
+
214
+ expect(registry.lookup('agent1')).toMatchObject({ sessionId: 'after', pinned: true });
215
+ });
216
+
217
+ it('preserves a pin through rename', () => {
218
+ registry.register(makeEntry({ name: 'before' }));
219
+ registry.togglePin('claude', process.pid);
220
+
221
+ registry.rename('before', 'after');
222
+
223
+ expect(registry.lookup('after')?.pinned).toBe(true);
224
+ });
225
+
226
+ it('removes the pin with a pruned process row', () => {
227
+ registry.register(makeEntry({ pid: 999999 }));
228
+ registry.togglePin('claude', 999999);
229
+
230
+ registry.prune();
231
+
232
+ expect(registry.lookup('agent1')).toBeNull();
233
+ });
234
+
235
+ it('reports a clear error when a readonly registry toggles a pin', () => {
236
+ registry.register(makeEntry());
237
+ const readonlyRegistry = new AgentRegistry(regPath, { readonly: true });
238
+
239
+ expect(() => readonlyRegistry.togglePin('claude', process.pid)).toThrow(/readonly/i);
240
+ });
241
+ });
242
+
175
243
  describe('list', () => {
176
244
  it('returns empty array when database does not contain entries', () => {
177
245
  expect(registry.list()).toEqual([]);
@@ -48,6 +48,9 @@ export interface AgentInfo {
48
48
  /** Timestamp of last activity */
49
49
  lastActive: Date;
50
50
 
51
+ /** Whether the live process is pinned in the agent console */
52
+ pinned?: boolean;
53
+
51
54
  /** Path to the session JSONL file on disk */
52
55
  sessionFilePath?: string;
53
56
  }
@@ -119,11 +119,11 @@ export class CodexAdapter implements AgentAdapter {
119
119
  const processes = relevant.filter((process) => this.canHandle(process));
120
120
  if (processes.length === 0) return [];
121
121
 
122
- const { cachedAgents, remaining } = this.tryRegistryCache(processes);
123
- if (remaining.length === 0) return cachedAgents;
122
+ const mappingResult = this.mapSessionMappingMatches(processes);
123
+ const { cachedAgents, remaining } = this.tryRegistryCache(mappingResult.fallback);
124
+ if (remaining.length === 0) return [...mappingResult.agents, ...cachedAgents];
124
125
 
125
- const mappingResult = this.mapSessionMappingMatches(remaining);
126
- const { direct, fallback } = this.tryResumeMatching(mappingResult.fallback);
126
+ const { direct, fallback } = this.tryResumeMatching(remaining);
127
127
  const directResult = this.mapDirectMatches(direct);
128
128
  const { sessions, contentCache } = this.discoverSessions(fallback);
129
129
  if (sessions.length === 0) {
@@ -887,13 +887,12 @@ export class CodexAdapter implements AgentAdapter {
887
887
  const ts = this.parseTimestamp(entry.timestamp);
888
888
  if (ts) lastTimestamp = ts;
889
889
 
890
- if (
891
- !firstUserMessage &&
892
- entry.payload?.type === 'user_message' &&
893
- typeof entry.payload.message === 'string' &&
894
- entry.payload.message.trim().length > 0
895
- ) {
896
- firstUserMessage = entry.payload.message.trim();
890
+ if (!firstUserMessage) {
891
+ const message = this.toConversationMessage(entry, false);
892
+ const content = message?.content.trim() ?? '';
893
+ if (message?.role === 'user' && content.length > 0 && !this.isSyntheticUserMessage(content)) {
894
+ firstUserMessage = content;
895
+ }
897
896
  }
898
897
  }
899
898
 
@@ -917,4 +916,8 @@ export class CodexAdapter implements AgentAdapter {
917
916
  sessionFilePath: filePath,
918
917
  };
919
918
  }
919
+
920
+ private isSyntheticUserMessage(content: string): boolean {
921
+ return content.startsWith('<environment_context>') && content.includes('</environment_context>');
922
+ }
920
923
  }
@@ -1,5 +1,5 @@
1
1
  import Database from 'better-sqlite3';
2
- import { mkdirSync } from 'fs';
2
+ import { existsSync, mkdirSync } from 'fs';
3
3
  import { dirname, join } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { initializeSchema } from './schema.js';
@@ -20,20 +20,33 @@ export function resolveAgentRegistryDbPath(filePath?: string): string {
20
20
  export class DatabaseConnection {
21
21
  private db: Database.Database;
22
22
  private readonly dbPath: string;
23
+ private readonly readonly: boolean;
23
24
 
24
25
  constructor(options: DatabaseOptions = {}) {
25
26
  this.dbPath = options.dbPath ?? DEFAULT_AGENT_REGISTRY_DB_PATH;
26
- mkdirSync(dirname(this.dbPath), { recursive: true });
27
+ this.readonly = options.readonly ?? false;
28
+ if (this.readonly && !existsSync(this.dbPath)) {
29
+ throw new Error(`Cannot open readonly agent database because it does not exist: ${this.dbPath}`);
30
+ }
31
+ if (!this.readonly) mkdirSync(dirname(this.dbPath), { recursive: true });
27
32
 
28
33
  this.db = new Database(this.dbPath, {
29
- readonly: options.readonly ?? false,
34
+ readonly: this.readonly,
30
35
  verbose: typeof options.verbose === 'function'
31
36
  ? options.verbose
32
37
  : options.verbose ? console.log : undefined,
33
38
  });
34
39
 
35
- this.configure();
36
- initializeSchema(this);
40
+ if (this.readonly) {
41
+ const version = this.db.pragma('user_version', { simple: true }) as number;
42
+ if (version < 3) {
43
+ this.db.close();
44
+ throw new Error(`Readonly agent database requires schema version 3 (found ${version}).`);
45
+ }
46
+ } else {
47
+ this.configure();
48
+ initializeSchema(this);
49
+ }
37
50
  }
38
51
 
39
52
  private configure(): void {
@@ -0,0 +1 @@
1
+ ALTER TABLE agents ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,43 @@
1
+ CREATE TABLE durable_agents (
2
+ id TEXT PRIMARY KEY,
3
+ name TEXT NOT NULL COLLATE NOCASE UNIQUE,
4
+ provider TEXT NOT NULL,
5
+ mode TEXT NOT NULL DEFAULT 'durable',
6
+ cwd TEXT NOT NULL,
7
+ provider_session_id TEXT NOT NULL UNIQUE,
8
+ state TEXT NOT NULL CHECK (state IN ('ready','running','degraded')),
9
+ session_health TEXT NOT NULL CHECK (session_health IN ('uninitialized','healthy','unknown','mismatch')),
10
+ created_at TEXT NOT NULL,
11
+ updated_at TEXT NOT NULL,
12
+ last_active_at TEXT NULL,
13
+ last_result_status TEXT NULL CHECK (last_result_status IS NULL OR last_result_status IN ('succeeded','failed','interrupted')),
14
+ last_result_completed_at TEXT NULL,
15
+ last_result_exit_code INTEGER NULL,
16
+ last_result_summary TEXT NULL,
17
+ active_run_token TEXT UNIQUE,
18
+ active_owner_pid INTEGER NULL,
19
+ active_owner_started_at TEXT NULL,
20
+ active_provider_pid INTEGER NULL,
21
+ active_provider_started_at TEXT NULL,
22
+ active_run_started_at TEXT NULL,
23
+ CHECK (
24
+ (state = 'running'
25
+ AND active_run_token IS NOT NULL
26
+ AND active_owner_pid IS NOT NULL
27
+ AND active_owner_started_at IS NOT NULL
28
+ AND active_run_started_at IS NOT NULL)
29
+ OR
30
+ (state <> 'running'
31
+ AND active_run_token IS NULL
32
+ AND active_owner_pid IS NULL
33
+ AND active_owner_started_at IS NULL
34
+ AND active_provider_pid IS NULL
35
+ AND active_provider_started_at IS NULL
36
+ AND active_run_started_at IS NULL)
37
+ ),
38
+ CHECK ((active_provider_pid IS NULL) = (active_provider_started_at IS NULL)),
39
+ CHECK ((last_result_status IS NULL) = (last_result_completed_at IS NULL))
40
+ );
41
+
42
+ CREATE INDEX idx_durable_agents_state ON durable_agents(state);
43
+ CREATE INDEX idx_durable_agents_list ON durable_agents(updated_at DESC, name COLLATE NOCASE);
@@ -1,6 +1,6 @@
1
1
  import { execFile } from 'child_process';
2
2
  import { promisify } from 'util';
3
- import { ClaudePrintError } from './PrintAgent.js';
3
+ import { ClaudePrintError } from './DurableAgent.js';
4
4
 
5
5
  type ExecResult = { stdout: string; stderr: string };
6
6
  type Exec = (file: string, args: string[]) => Promise<ExecResult>;
@@ -1,23 +1,23 @@
1
- import type { PrintAgent, ProcessIdentity } from './PrintAgent.js';
2
- import { ClaudePrintError, PrintAgentNotFoundError } from './PrintAgent.js';
1
+ import type { DurableAgent, ProcessIdentity } from './DurableAgent.js';
2
+ import { ClaudePrintError, DurableAgentNotFoundError } from './DurableAgent.js';
3
3
  import { ClaudeCliProbe } from './ClaudeCliProbe.js';
4
4
  import { ClaudePrintRunner, type ClaudePrintRunResult } from './ClaudePrintRunner.js';
5
- import { PrintAgentStore, type CreatePrintAgentInput, type PrintRunCompletion } from './PrintAgentStore.js';
5
+ import { DurableAgentRepository, type CreateDurableAgentInput, type DurableRunCompletion } from './DurableAgentRepository.js';
6
6
 
7
- interface StoreLike {
8
- create(input: CreatePrintAgentInput): Promise<PrintAgent>;
9
- list(): Promise<PrintAgent[]>;
10
- resolve(reference: string): Promise<PrintAgent | PrintAgent[] | null>;
11
- acquireRun(id: string): Promise<{ agent: PrintAgent; token: string }>;
7
+ interface RepositoryLike {
8
+ create(input: CreateDurableAgentInput): Promise<DurableAgent>;
9
+ list(): Promise<DurableAgent[]>;
10
+ resolve(reference: string): Promise<DurableAgent | DurableAgent[] | null>;
11
+ acquireRun(id: string): Promise<{ agent: DurableAgent; token: string }>;
12
12
  recordProviderProcess(id: string, token: string, identity: ProcessIdentity): Promise<void>;
13
- completeRun(id: string, token: string, result: PrintRunCompletion): Promise<PrintAgent>;
13
+ completeRun(id: string, token: string, result: DurableRunCompletion): Promise<DurableAgent>;
14
14
  }
15
15
 
16
16
  interface ProbeLike { validate(): Promise<{ executable: string; version: string }> }
17
17
  interface RunnerLike { run(request: Parameters<ClaudePrintRunner['run']>[0]): Promise<ClaudePrintRunResult> }
18
18
 
19
19
  export interface ClaudePrintAgentServiceOptions {
20
- store?: StoreLike;
20
+ repository?: RepositoryLike;
21
21
  probe?: ProbeLike;
22
22
  runner?: RunnerLike;
23
23
  executable?: string;
@@ -29,39 +29,39 @@ export interface ClaudePrintSendResult extends ClaudePrintRunResult {
29
29
  }
30
30
 
31
31
  export class ClaudePrintAgentService {
32
- readonly store: StoreLike;
32
+ readonly repository: RepositoryLike;
33
33
  private readonly probe: ProbeLike;
34
34
  private readonly runner: RunnerLike;
35
35
  private readonly executable?: string;
36
36
 
37
37
  constructor(options: ClaudePrintAgentServiceOptions = {}) {
38
- this.store = options.store ?? new PrintAgentStore();
38
+ this.repository = options.repository ?? new DurableAgentRepository();
39
39
  this.probe = options.probe ?? new ClaudeCliProbe();
40
40
  this.runner = options.runner ?? new ClaudePrintRunner();
41
41
  this.executable = options.executable;
42
42
  }
43
43
 
44
- async create(input: CreatePrintAgentInput): Promise<PrintAgent> {
44
+ async create(input: CreateDurableAgentInput): Promise<DurableAgent> {
45
45
  await this.probe.validate();
46
- return this.store.create(input);
46
+ return this.repository.create(input);
47
47
  }
48
48
 
49
49
  async send(reference: string, prompt: string): Promise<ClaudePrintSendResult> {
50
- const resolved = await this.store.resolve(reference);
51
- if (!resolved) throw new PrintAgentNotFoundError(reference);
50
+ const resolved = await this.repository.resolve(reference);
51
+ if (!resolved) throw new DurableAgentNotFoundError(reference);
52
52
  if (Array.isArray(resolved)) {
53
- throw new ClaudePrintError(`Multiple print agents match "${reference}".`, 'PRINT_AGENT_AMBIGUOUS');
53
+ throw new ClaudePrintError(`Multiple durable agents match "${reference}".`, 'DURABLE_AGENT_AMBIGUOUS');
54
54
  }
55
- const acquired = await this.store.acquireRun(resolved.id);
55
+ const acquired = await this.repository.acquireRun(resolved.id);
56
56
  try {
57
57
  const result = await this.runner.run({
58
58
  agent: acquired.agent,
59
59
  prompt,
60
60
  executable: this.executable,
61
61
  firstRun: acquired.agent.sessionHealth === 'uninitialized',
62
- onSpawn: (identity) => this.store.recordProviderProcess(resolved.id, acquired.token, identity),
62
+ onSpawn: (identity) => this.repository.recordProviderProcess(resolved.id, acquired.token, identity),
63
63
  });
64
- await this.store.completeRun(resolved.id, acquired.token, {
64
+ await this.repository.completeRun(resolved.id, acquired.token, {
65
65
  status: 'succeeded',
66
66
  exitCode: result.exitCode,
67
67
  summary: sanitize(result.result, 4096),
@@ -73,7 +73,7 @@ export class ClaudePrintAgentService {
73
73
  const sessionHealth = error instanceof ClaudePrintError && error.code === 'CLAUDE_SESSION_MISMATCH'
74
74
  ? 'mismatch' as const
75
75
  : 'unknown' as const;
76
- await this.store.completeRun(resolved.id, acquired.token, {
76
+ await this.repository.completeRun(resolved.id, acquired.token, {
77
77
  status: 'failed',
78
78
  exitCode: null,
79
79
  summary: sanitize(failure.message, 4096),
@@ -1,7 +1,7 @@
1
1
  import { spawn as nodeSpawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'child_process';
2
- import type { PrintAgent, ProcessIdentity } from './PrintAgent.js';
3
- import { ClaudePrintError } from './PrintAgent.js';
4
- import { LocalProcessInspector, type ProcessInspector } from './PrintAgentStore.js';
2
+ import type { DurableAgent, ProcessIdentity } from './DurableAgent.js';
3
+ import { ClaudePrintError } from './DurableAgent.js';
4
+ import { LocalProcessInspector, type ProcessInspector } from './DurableAgentRepository.js';
5
5
 
6
6
  type Spawn = (
7
7
  command: string,
@@ -10,7 +10,7 @@ type Spawn = (
10
10
  ) => ChildProcessWithoutNullStreams;
11
11
 
12
12
  export interface ClaudePrintRunRequest {
13
- agent: PrintAgent;
13
+ agent: DurableAgent;
14
14
  prompt: string;
15
15
  executable?: string;
16
16
  firstRun: boolean;
@@ -0,0 +1,91 @@
1
+ export type DurableAgentState = 'ready' | 'running' | 'degraded';
2
+ export type DurableSessionHealth = 'uninitialized' | 'healthy' | 'unknown' | 'mismatch';
3
+ export type DurableRunStatus = 'succeeded' | 'failed' | 'interrupted';
4
+
5
+ export const AGENT_MODES = {
6
+ INTERACTIVE: 'interactive',
7
+ DURABLE: 'durable',
8
+ } as const;
9
+
10
+ export interface ProcessIdentity {
11
+ pid: number;
12
+ startedAt: string;
13
+ }
14
+
15
+ export interface DurableActiveRun {
16
+ token: string;
17
+ owner: ProcessIdentity;
18
+ provider: ProcessIdentity | null;
19
+ startedAt: string;
20
+ }
21
+
22
+ export interface DurableLastResult {
23
+ status: DurableRunStatus;
24
+ completedAt: string;
25
+ exitCode: number | null;
26
+ summary: string;
27
+ }
28
+
29
+ export interface DurableAgent {
30
+ id: string;
31
+ name: string;
32
+ provider: 'claude';
33
+ mode: typeof AGENT_MODES.DURABLE;
34
+ cwd: string;
35
+ providerSessionId: string;
36
+ state: DurableAgentState;
37
+ sessionHealth: DurableSessionHealth;
38
+ createdAt: string;
39
+ updatedAt: string;
40
+ lastActiveAt: string | null;
41
+ lastResult: DurableLastResult | null;
42
+ activeRun: DurableActiveRun | null;
43
+ }
44
+
45
+ export class DurableAgentError extends Error {
46
+ constructor(
47
+ message: string,
48
+ public readonly code: string,
49
+ ) {
50
+ super(message);
51
+ this.name = 'DurableAgentError';
52
+ }
53
+ }
54
+
55
+ export class DurableAgentBusyError extends DurableAgentError {
56
+ constructor(
57
+ public readonly agentId: string,
58
+ agentName: string,
59
+ ) {
60
+ super(`Durable agent "${agentName}" is busy.`, 'DURABLE_AGENT_BUSY');
61
+ this.name = 'DurableAgentBusyError';
62
+ }
63
+ }
64
+
65
+ export class DurableAgentNotFoundError extends DurableAgentError {
66
+ constructor(public readonly reference: string) {
67
+ super(`Durable agent "${reference}" was not found.`, 'DURABLE_AGENT_NOT_FOUND');
68
+ this.name = 'DurableAgentNotFoundError';
69
+ }
70
+ }
71
+
72
+ export class DurableAgentRepositoryError extends DurableAgentError {
73
+ constructor(message: string) {
74
+ super(message, 'DURABLE_AGENT_REPOSITORY');
75
+ this.name = 'DurableAgentRepositoryError';
76
+ }
77
+ }
78
+
79
+ export class DurableAgentNameConflictError extends DurableAgentError {
80
+ constructor(public readonly agentName: string) {
81
+ super(`Durable agent name "${agentName}" is already in use.`, 'DURABLE_AGENT_NAME_CONFLICT');
82
+ this.name = 'DurableAgentNameConflictError';
83
+ }
84
+ }
85
+
86
+ export class ClaudePrintError extends DurableAgentError {
87
+ constructor(message: string, code = 'CLAUDE_PRINT_FAILED') {
88
+ super(message, code);
89
+ this.name = 'ClaudePrintError';
90
+ }
91
+ }