@ai-devkit/agent-manager 0.29.0 → 0.31.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 (202) hide show
  1. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +1 -342
  2. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  3. package/dist/__tests__/database/DurableAgentsDatabase.test.js +31 -3
  4. package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -1
  5. package/dist/__tests__/durable/CodexCliProbe.test.js +129 -0
  6. package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -0
  7. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +84 -0
  8. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -0
  9. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +126 -0
  10. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -0
  11. package/dist/__tests__/durable/CodexPrintAgentService.test.js +271 -0
  12. package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -0
  13. package/dist/__tests__/durable/CodexPrintRunner.test.js +393 -0
  14. package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -0
  15. package/dist/__tests__/print/ClaudeCliProbe.test.js +3 -7
  16. package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
  17. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +25 -28
  18. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
  19. package/dist/__tests__/print/ClaudePrintAgentService.test.js +65 -0
  20. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
  21. package/dist/__tests__/print/ClaudePrintRunner.test.js +4 -10
  22. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
  23. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +79 -0
  24. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -0
  25. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +436 -0
  26. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -0
  27. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +76 -0
  28. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -0
  29. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +43 -0
  30. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -0
  31. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +202 -0
  32. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -0
  33. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +119 -0
  34. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -0
  35. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +386 -0
  36. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -0
  37. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +34 -0
  38. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -0
  39. package/dist/__tests__/utils/ClaudeSessionParser.test.js +2 -2
  40. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -1
  41. package/dist/adapters/index.d.ts +1 -1
  42. package/dist/adapters/index.d.ts.map +1 -1
  43. package/dist/adapters/index.js +1 -1
  44. package/dist/adapters/index.js.map +1 -1
  45. package/dist/database/connection.d.ts.map +1 -1
  46. package/dist/database/connection.js +5 -1
  47. package/dist/database/connection.js.map +1 -1
  48. package/dist/database/migrations/004_codex_durable_sessions.sql +1 -0
  49. package/dist/durable/DurableAgent.d.ts +23 -3
  50. package/dist/durable/DurableAgent.d.ts.map +1 -1
  51. package/dist/durable/DurableAgent.js +12 -0
  52. package/dist/durable/DurableAgent.js.map +1 -1
  53. package/dist/durable/DurableAgentRepository.d.ts +6 -7
  54. package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
  55. package/dist/durable/DurableAgentRepository.js +73 -42
  56. package/dist/durable/DurableAgentRepository.js.map +1 -1
  57. package/dist/durable/process.d.ts +8 -0
  58. package/dist/durable/process.d.ts.map +1 -0
  59. package/dist/durable/process.js +40 -0
  60. package/dist/durable/process.js.map +1 -0
  61. package/dist/durable/run.d.ts +32 -0
  62. package/dist/durable/run.d.ts.map +1 -0
  63. package/dist/durable/run.js +46 -0
  64. package/dist/durable/run.js.map +1 -0
  65. package/dist/durable/utils.d.ts +19 -0
  66. package/dist/durable/utils.d.ts.map +1 -0
  67. package/dist/durable/utils.js +52 -0
  68. package/dist/durable/utils.js.map +1 -0
  69. package/dist/index.d.ts +17 -13
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +9 -8
  72. package/dist/index.js.map +1 -1
  73. package/dist/providers/claude/ClaudeAgentMapper.d.ts +21 -0
  74. package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -0
  75. package/dist/providers/claude/ClaudeAgentMapper.js +42 -0
  76. package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -0
  77. package/dist/providers/claude/ClaudeCodeAdapter.d.ts +29 -0
  78. package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -0
  79. package/dist/providers/claude/ClaudeCodeAdapter.js +129 -0
  80. package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -0
  81. package/dist/{adapters/ClaudeCodeAdapter.d.ts → providers/claude/ClaudeSessionLocator.d.ts} +68 -52
  82. package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -0
  83. package/dist/{adapters/ClaudeCodeAdapter.js → providers/claude/ClaudeSessionLocator.js} +71 -189
  84. package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -0
  85. package/dist/{utils → providers/claude}/ClaudeSessionParser.d.ts +2 -2
  86. package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -0
  87. package/dist/{utils → providers/claude}/ClaudeSessionParser.js +1 -1
  88. package/dist/providers/claude/ClaudeSessionParser.js.map +1 -0
  89. package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -0
  90. package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.js +4 -9
  91. package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -0
  92. package/dist/{durable → providers/claude/durable}/ClaudePrintAgentService.d.ts +4 -9
  93. package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -0
  94. package/dist/providers/claude/durable/ClaudePrintAgentService.js +45 -0
  95. package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -0
  96. package/dist/{durable → providers/claude/durable}/ClaudePrintRunner.d.ts +3 -3
  97. package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -0
  98. package/dist/providers/claude/durable/ClaudePrintRunner.js +75 -0
  99. package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -0
  100. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +17 -0
  101. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -0
  102. package/dist/providers/claude/durable/ClaudeStreamParser.js +69 -0
  103. package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -0
  104. package/dist/providers/codex/durable/CodexCliProbe.d.ts +20 -0
  105. package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -0
  106. package/dist/providers/codex/durable/CodexCliProbe.js +61 -0
  107. package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -0
  108. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +40 -0
  109. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -0
  110. package/dist/providers/codex/durable/CodexPrintAgentService.js +50 -0
  111. package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -0
  112. package/dist/providers/codex/durable/CodexPrintRunner.d.ts +34 -0
  113. package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -0
  114. package/dist/providers/codex/durable/CodexPrintRunner.js +78 -0
  115. package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -0
  116. package/dist/providers/codex/durable/CodexStreamParser.d.ts +21 -0
  117. package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -0
  118. package/dist/providers/codex/durable/CodexStreamParser.js +86 -0
  119. package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -0
  120. package/dist/providers/pi/durable/PiCliProbe.d.ts +20 -0
  121. package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -0
  122. package/dist/providers/pi/durable/PiCliProbe.js +51 -0
  123. package/dist/providers/pi/durable/PiCliProbe.js.map +1 -0
  124. package/dist/providers/pi/durable/PiPrintAgentService.d.ts +39 -0
  125. package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -0
  126. package/dist/providers/pi/durable/PiPrintAgentService.js +47 -0
  127. package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -0
  128. package/dist/providers/pi/durable/PiPrintProtocol.d.ts +4 -0
  129. package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -0
  130. package/dist/providers/pi/durable/PiPrintProtocol.js +44 -0
  131. package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -0
  132. package/dist/providers/pi/durable/PiPrintRunner.d.ts +32 -0
  133. package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -0
  134. package/dist/providers/pi/durable/PiPrintRunner.js +61 -0
  135. package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -0
  136. package/dist/providers/pi/durable/PiStreamParser.d.ts +21 -0
  137. package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -0
  138. package/dist/providers/pi/durable/PiStreamParser.js +71 -0
  139. package/dist/providers/pi/durable/PiStreamParser.js.map +1 -0
  140. package/package.json +1 -1
  141. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1 -307
  142. package/src/__tests__/database/DurableAgentsDatabase.test.ts +28 -3
  143. package/src/__tests__/durable/CodexCliProbe.test.ts +63 -0
  144. package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +54 -0
  145. package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +79 -0
  146. package/src/__tests__/durable/CodexPrintAgentService.test.ts +139 -0
  147. package/src/__tests__/durable/CodexPrintRunner.test.ts +189 -0
  148. package/src/__tests__/fixtures/fake-codex.cjs +45 -0
  149. package/src/__tests__/print/ClaudeCliProbe.test.ts +3 -7
  150. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +11 -21
  151. package/src/__tests__/print/ClaudePrintAgentService.test.ts +33 -1
  152. package/src/__tests__/print/ClaudePrintRunner.test.ts +4 -10
  153. package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +84 -0
  154. package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +411 -0
  155. package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +38 -0
  156. package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +32 -0
  157. package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +69 -0
  158. package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +33 -0
  159. package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +107 -0
  160. package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +24 -0
  161. package/src/__tests__/utils/ClaudeSessionParser.test.ts +2 -2
  162. package/src/adapters/index.ts +1 -1
  163. package/src/database/connection.ts +3 -1
  164. package/src/database/migrations/004_codex_durable_sessions.sql +1 -0
  165. package/src/durable/DurableAgent.ts +51 -3
  166. package/src/durable/DurableAgentRepository.ts +60 -31
  167. package/src/durable/process.ts +28 -0
  168. package/src/durable/run.ts +74 -0
  169. package/src/durable/utils.ts +61 -0
  170. package/src/index.ts +31 -15
  171. package/src/providers/claude/ClaudeAgentMapper.ts +62 -0
  172. package/src/providers/claude/ClaudeCodeAdapter.ts +160 -0
  173. package/src/{adapters/ClaudeCodeAdapter.ts → providers/claude/ClaudeSessionLocator.ts} +97 -239
  174. package/src/{utils → providers/claude}/ClaudeSessionParser.ts +2 -2
  175. package/src/{durable → providers/claude/durable}/ClaudeCliProbe.ts +4 -10
  176. package/src/providers/claude/durable/ClaudePrintAgentService.ts +66 -0
  177. package/src/providers/claude/durable/ClaudePrintRunner.ts +92 -0
  178. package/src/providers/claude/durable/ClaudeStreamParser.ts +79 -0
  179. package/src/providers/codex/durable/CodexCliProbe.ts +60 -0
  180. package/src/providers/codex/durable/CodexPrintAgentService.ts +65 -0
  181. package/src/providers/codex/durable/CodexPrintRunner.ts +94 -0
  182. package/src/providers/codex/durable/CodexStreamParser.ts +104 -0
  183. package/src/providers/pi/durable/PiCliProbe.ts +48 -0
  184. package/src/providers/pi/durable/PiPrintAgentService.ts +69 -0
  185. package/src/providers/pi/durable/PiPrintProtocol.ts +39 -0
  186. package/src/providers/pi/durable/PiPrintRunner.ts +88 -0
  187. package/src/providers/pi/durable/PiStreamParser.ts +84 -0
  188. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +0 -1
  189. package/dist/adapters/ClaudeCodeAdapter.js.map +0 -1
  190. package/dist/durable/ClaudeCliProbe.d.ts.map +0 -1
  191. package/dist/durable/ClaudeCliProbe.js.map +0 -1
  192. package/dist/durable/ClaudePrintAgentService.d.ts.map +0 -1
  193. package/dist/durable/ClaudePrintAgentService.js +0 -66
  194. package/dist/durable/ClaudePrintAgentService.js.map +0 -1
  195. package/dist/durable/ClaudePrintRunner.d.ts.map +0 -1
  196. package/dist/durable/ClaudePrintRunner.js +0 -128
  197. package/dist/durable/ClaudePrintRunner.js.map +0 -1
  198. package/dist/utils/ClaudeSessionParser.d.ts.map +0 -1
  199. package/dist/utils/ClaudeSessionParser.js.map +0 -1
  200. package/src/durable/ClaudePrintAgentService.ts +0 -94
  201. package/src/durable/ClaudePrintRunner.ts +0 -139
  202. /package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.d.ts +0 -0
@@ -0,0 +1,107 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { PassThrough, Writable } from 'node:stream';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import type { PiDurableAgent } from '../../../../index.js';
5
+
6
+ const SESSION = '22222222-2222-4222-8222-222222222222';
7
+ function agent(sessionHealth: PiDurableAgent['sessionHealth'] = 'uninitialized'): PiDurableAgent {
8
+ return { id: '11111111-1111-4111-8111-111111111111', name: 'reviewer', provider: 'pi', mode: 'durable',
9
+ cwd: '/project', providerSessionId: SESSION, state: 'running', sessionHealth, createdAt: '',
10
+ updatedAt: '', lastActiveAt: null, lastResult: null, activeRun: null };
11
+ }
12
+ function fakeSpawn(lines: string[], exitCode = 0) {
13
+ const promptChunks: Buffer[] = [];
14
+ const child = new EventEmitter() as any;
15
+ child.pid = 4242; child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.kill = vi.fn();
16
+ child.stdin = new Writable({
17
+ write(chunk, _encoding, callback) { promptChunks.push(Buffer.from(chunk)); callback(); },
18
+ final(callback) { child.stdout.end(lines.join('\n')); queueMicrotask(() => child.emit('close', exitCode, null)); callback(); },
19
+ });
20
+ return { child, spawn: vi.fn(() => child), promptChunks };
21
+ }
22
+ function events(session = SESSION): string[] { return [
23
+ JSON.stringify({ type: 'session', version: 3, id: session, cwd: '/project' }),
24
+ JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'text', text: 'first' }] } }),
25
+ JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'thinking', thinking: 'hidden' }, { type: 'text', text: 'final' }] } }),
26
+ JSON.stringify({ type: 'agent_end', messages: [] }), '',
27
+ ]; }
28
+ async function runner(fixture: ReturnType<typeof fakeSpawn>, maxLineBytes?: number) {
29
+ const api = await import('../../../../index.js') as Record<string, unknown>;
30
+ expect(api).toHaveProperty('PiPrintRunner');
31
+ const Runner = api.PiPrintRunner as new (options: unknown) => any;
32
+ return new Runner({ spawn: fixture.spawn, maxLineBytes, processInspector: { getIdentity: () => ({ pid: 4242, startedAt: 'start' }) } });
33
+ }
34
+
35
+ describe('PiPrintRunner', () => {
36
+ it('binds a first session and returns the last completed assistant text', async () => {
37
+ const fixture = fakeSpawn(events()); const instance = await runner(fixture); const order: string[] = [];
38
+ const result = await instance.run({ agent: agent(), prompt: 'secret', executable: 'fake-pi',
39
+ onSpawn: async () => { expect(fixture.promptChunks).toHaveLength(0); order.push('spawn'); } });
40
+ expect(order).toEqual(['spawn']);
41
+ expect(fixture.spawn).toHaveBeenCalledWith('fake-pi', ['--mode', 'json', '--session-id', SESSION], expect.objectContaining({ cwd: '/project', shell: false }));
42
+ expect(Buffer.concat(fixture.promptChunks).toString()).toBe('secret');
43
+ expect(result).toEqual({ sessionId: SESSION, result: 'final', messages: ['first', 'final'], exitCode: 0 });
44
+ });
45
+
46
+ it('resumes the exact stored session and rejects mismatch', async () => {
47
+ const fixture = fakeSpawn(events('33333333-3333-4333-8333-333333333333'));
48
+ await expect((await runner(fixture)).run({ agent: agent('healthy'), prompt: 'later', onSpawn: vi.fn() }))
49
+ .rejects.toMatchObject({ code: 'PI_SESSION_MISMATCH' });
50
+ expect(fixture.spawn.mock.calls[0]![1]).toEqual(['--mode', 'json', '--session', SESSION]);
51
+ });
52
+
53
+ it.each([
54
+ ['malformed', ['{bad\n'], 'PI_PROTOCOL'], ['non-object', ['[]\n'], 'PI_PROTOCOL'],
55
+ ['truncated', ['{}'], 'PI_PROTOCOL'], ['missing session', [JSON.stringify({ type: 'agent_end' }), ''], 'PI_PROTOCOL'],
56
+ ['missing result', [JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'agent_end' }), ''], 'PI_RESULT_MISSING'],
57
+ ['missing end', [JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'text', text: 'x' }] } }), ''], 'PI_PROTOCOL'],
58
+ ])('rejects %s output', async (_name, lines, code) => {
59
+ await expect((await runner(fakeSpawn(lines as string[]))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
60
+ .rejects.toMatchObject({ code });
61
+ });
62
+
63
+ it('rejects oversized output, failed processes, and unverifiable identities', async () => {
64
+ await expect((await runner(fakeSpawn(['x'.repeat(20)]), 10)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
65
+ .rejects.toMatchObject({ code: 'PI_PROTOCOL' });
66
+ await expect((await runner(fakeSpawn(events(), 1))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
67
+ .rejects.toMatchObject({ code: 'PI_PROCESS' });
68
+ const fixture = fakeSpawn([]); const api = await import('../../../../index.js') as Record<string, unknown>;
69
+ const Runner = api.PiPrintRunner as new (options: unknown) => any;
70
+ await expect(new Runner({ spawn: fixture.spawn, processInspector: { getIdentity: () => null } }).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn() }))
71
+ .rejects.toMatchObject({ code: 'PI_PROCESS' });
72
+ expect(fixture.child.kill).toHaveBeenCalledOnce();
73
+ });
74
+
75
+ it('rejects invalid and duplicate session identities', async () => {
76
+ for (const lines of [
77
+ [JSON.stringify({ type: 'session', id: 'bad' }), ''],
78
+ [JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'session', id: SESSION }), ''],
79
+ ]) await expect((await runner(fakeSpawn(lines))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() }))
80
+ .rejects.toMatchObject({ code: 'PI_PROTOCOL' });
81
+ });
82
+
83
+ it('kills on spawn persistence failure', async () => {
84
+ const spawnFailure = fakeSpawn([]); const failure = new Error('cannot persist');
85
+ await expect((await runner(spawnFailure)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn().mockRejectedValue(failure), onSession: vi.fn() })).rejects.toBe(failure);
86
+ expect(spawnFailure.child.kill).toHaveBeenCalledOnce();
87
+ });
88
+
89
+ it('classifies spawn errors and a missing PID as process failures', async () => {
90
+ const errored = fakeSpawn([]);
91
+ errored.child.stdin = new Writable({ write(_chunk, _encoding, callback) { callback(); }, final(callback) { queueMicrotask(() => errored.child.emit('error', new Error('spawn'))); callback(); } });
92
+ await expect((await runner(errored)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() })).rejects.toMatchObject({ code: 'PI_PROCESS' });
93
+ const missing = fakeSpawn([]); missing.child.pid = undefined;
94
+ await expect((await runner(missing)).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() })).rejects.toMatchObject({ code: 'PI_PROCESS' });
95
+ expect(missing.child.kill).toHaveBeenCalledOnce();
96
+ });
97
+
98
+ it('accepts string assistant content and ignores empty or unrelated messages', async () => {
99
+ const lines = [JSON.stringify({ type: 'session', id: SESSION }), JSON.stringify({ type: 'future' }),
100
+ JSON.stringify({ type: 'message_end', message: { role: 'user', content: 'ignored' } }),
101
+ JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: ' ' } }),
102
+ JSON.stringify({ type: 'message_end', message: { role: 'assistant', content: 'answer' } }),
103
+ JSON.stringify({ type: 'agent_end' }), ''];
104
+ await expect((await runner(fakeSpawn(lines))).run({ agent: agent(), prompt: 'x', onSpawn: vi.fn(), onSession: vi.fn() }))
105
+ .resolves.toMatchObject({ result: 'answer' });
106
+ });
107
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { PiStreamParser } from '../../../../providers/pi/durable/PiStreamParser.js';
3
+
4
+ const SESSION = '22222222-2222-4222-8222-222222222222';
5
+
6
+ describe('PiStreamParser', () => {
7
+ it('ignores blank lines and normalizes unexpected parser failures', () => {
8
+ const parser = new PiStreamParser(SESSION, 1024);
9
+ parser.accept('\n');
10
+ parser.fail(new Error('unexpected'));
11
+
12
+ expect(parser.hasFailed()).toBe(true);
13
+ expect(() => parser.result({ code: 0, signal: null }))
14
+ .toThrowError(expect.objectContaining({ code: 'PI_PROTOCOL', message: 'Pi stream processing failed.' }));
15
+ });
16
+
17
+ it('includes the terminating signal in process failures', () => {
18
+ const parser = new PiStreamParser(SESSION, 1024);
19
+ parser.accept(`${JSON.stringify({ type: 'session', id: SESSION })}\n`);
20
+
21
+ expect(() => parser.result({ code: null, signal: 'SIGTERM' }))
22
+ .toThrowError(expect.objectContaining({ code: 'PI_PROCESS', message: 'Pi print run failed (SIGTERM)' }));
23
+ });
24
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for utils/ClaudeSessionParser.ts — focused on stripping
2
+ * Tests for providers/claude/ClaudeSessionParser.ts — focused on stripping
3
3
  * harness-injected XML tags from conversation content.
4
4
  */
5
5
 
@@ -7,7 +7,7 @@
7
7
  import * as fs from 'fs';
8
8
  import * as os from 'os';
9
9
  import * as path from 'path';
10
- import { ClaudeSessionParser } from '../../utils/ClaudeSessionParser.js';
10
+ import { ClaudeSessionParser } from '../../providers/claude/ClaudeSessionParser.js';
11
11
 
12
12
  interface JsonlEntry {
13
13
  type: 'user' | 'assistant' | 'system';
@@ -1,4 +1,4 @@
1
- export { ClaudeCodeAdapter } from './ClaudeCodeAdapter.js';
1
+ export { ClaudeCodeAdapter } from '../providers/claude/ClaudeCodeAdapter.js';
2
2
  export { CodexAdapter } from './CodexAdapter.js';
3
3
  export { CopilotAdapter } from './CopilotAdapter.js';
4
4
  export { GeminiCliAdapter } from './GeminiCliAdapter.js';
@@ -32,6 +32,7 @@ export class DatabaseConnection {
32
32
 
33
33
  this.db = new Database(this.dbPath, {
34
34
  readonly: this.readonly,
35
+ timeout: 5000,
35
36
  verbose: typeof options.verbose === 'function'
36
37
  ? options.verbose
37
38
  : options.verbose ? console.log : undefined,
@@ -50,7 +51,8 @@ export class DatabaseConnection {
50
51
  }
51
52
 
52
53
  private configure(): void {
53
- this.db.pragma('journal_mode = WAL');
54
+ const journalMode = this.db.pragma('journal_mode', { simple: true }) as string;
55
+ if (journalMode.toLowerCase() !== 'wal') this.db.pragma('journal_mode = WAL');
54
56
  this.db.pragma('foreign_keys = ON');
55
57
  this.db.pragma('synchronous = NORMAL');
56
58
  this.db.pragma('busy_timeout = 5000');
@@ -0,0 +1 @@
1
+ ALTER TABLE durable_agents ALTER COLUMN provider_session_id DROP NOT NULL;
@@ -1,6 +1,14 @@
1
1
  export type DurableAgentState = 'ready' | 'running' | 'degraded';
2
2
  export type DurableSessionHealth = 'uninitialized' | 'healthy' | 'unknown' | 'mismatch';
3
3
  export type DurableRunStatus = 'succeeded' | 'failed' | 'interrupted';
4
+ export type PiPrintErrorCode =
5
+ | 'PI_CLI_UNAVAILABLE'
6
+ | 'PI_CLI_UNSUPPORTED'
7
+ | 'PI_PROCESS'
8
+ | 'PI_PROTOCOL'
9
+ | 'PI_RESULT_MISSING'
10
+ | 'PI_SESSION_MISMATCH'
11
+ | 'PI_UNSUPPORTED';
4
12
 
5
13
  export const AGENT_MODES = {
6
14
  INTERACTIVE: 'interactive',
@@ -26,13 +34,13 @@ export interface DurableLastResult {
26
34
  summary: string;
27
35
  }
28
36
 
29
- export interface DurableAgent {
37
+ export type DurableProvider = 'claude' | 'codex' | 'pi';
38
+
39
+ export interface DurableAgentBase {
30
40
  id: string;
31
41
  name: string;
32
- provider: 'claude';
33
42
  mode: typeof AGENT_MODES.DURABLE;
34
43
  cwd: string;
35
- providerSessionId: string;
36
44
  state: DurableAgentState;
37
45
  sessionHealth: DurableSessionHealth;
38
46
  createdAt: string;
@@ -42,6 +50,23 @@ export interface DurableAgent {
42
50
  activeRun: DurableActiveRun | null;
43
51
  }
44
52
 
53
+ export interface ClaudeDurableAgent extends DurableAgentBase {
54
+ provider: 'claude';
55
+ providerSessionId: string;
56
+ }
57
+
58
+ export interface CodexDurableAgent extends DurableAgentBase {
59
+ provider: 'codex';
60
+ providerSessionId: string | null;
61
+ }
62
+
63
+ export interface PiDurableAgent extends DurableAgentBase {
64
+ provider: 'pi';
65
+ providerSessionId: string;
66
+ }
67
+
68
+ export type DurableAgent = ClaudeDurableAgent | CodexDurableAgent | PiDurableAgent;
69
+
45
70
  export class DurableAgentError extends Error {
46
71
  constructor(
47
72
  message: string,
@@ -89,3 +114,26 @@ export class ClaudePrintError extends DurableAgentError {
89
114
  this.name = 'ClaudePrintError';
90
115
  }
91
116
  }
117
+
118
+ export type CodexPrintErrorCode =
119
+ | 'CODEX_PROTOCOL'
120
+ | 'CODEX_PROCESS'
121
+ | 'CODEX_SESSION_MISMATCH'
122
+ | 'CODEX_UNSUPPORTED'
123
+ | 'CODEX_RESULT_MISSING'
124
+ | 'CODEX_CLI_UNSUPPORTED'
125
+ | 'CODEX_CLI_UNAVAILABLE';
126
+
127
+ export class CodexPrintError extends DurableAgentError {
128
+ constructor(message: string, code: CodexPrintErrorCode) {
129
+ super(message, code);
130
+ this.name = 'CodexPrintError';
131
+ }
132
+ }
133
+
134
+ export class PiPrintError extends DurableAgentError {
135
+ constructor(message: string, code: PiPrintErrorCode = 'PI_PROCESS') {
136
+ super(message, code);
137
+ this.name = 'PiPrintError';
138
+ }
139
+ }
@@ -1,8 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import { randomUUID } from 'crypto';
3
- import { execFileSync } from 'child_process';
4
3
  import { DatabaseConnection, DEFAULT_AGENT_REGISTRY_DB_PATH } from '../database/index.js';
5
- import { AGENT_MODES, type DurableActiveRun, type DurableAgent, type ProcessIdentity, type DurableRunStatus, type DurableSessionHealth } from './DurableAgent.js';
4
+ import { AGENT_MODES, CodexPrintError, type DurableActiveRun, type DurableAgent, type DurableProvider, type ProcessIdentity, type DurableRunStatus, type DurableSessionHealth } from './DurableAgent.js';
5
+ import { LocalProcessInspector, type ProcessInspector } from './process.js';
6
+ import { isUuid } from './utils.js';
6
7
  import {
7
8
  DurableAgentBusyError,
8
9
  DurableAgentNameConflictError,
@@ -11,7 +12,7 @@ import {
11
12
  } from './DurableAgent.js';
12
13
 
13
14
  interface DurableAgentRow {
14
- id: string; name: string; provider: 'claude'; mode: typeof AGENT_MODES.DURABLE; cwd: string; provider_session_id: string;
15
+ id: string; name: string; provider: DurableProvider; mode: typeof AGENT_MODES.DURABLE; cwd: string; provider_session_id: string | null;
15
16
  state: DurableAgent['state']; session_health: DurableSessionHealth; created_at: string; updated_at: string;
16
17
  last_active_at: string | null; last_result_status: DurableRunStatus | null;
17
18
  last_result_completed_at: string | null; last_result_exit_code: number | null; last_result_summary: string | null;
@@ -19,7 +20,7 @@ interface DurableAgentRow {
19
20
  active_provider_pid: number | null; active_provider_started_at: string | null; active_run_started_at: string | null;
20
21
  }
21
22
 
22
- export interface CreateDurableAgentInput { name: string; cwd: string }
23
+ export interface CreateDurableAgentInput { name: string; cwd: string; provider?: DurableProvider }
23
24
 
24
25
  export interface DurableAgentRepositoryOptions {
25
26
  dbPath?: string;
@@ -34,7 +35,8 @@ export interface DurableAgentRepositoryOptions {
34
35
  mutationLockStaleMs?: number;
35
36
  }
36
37
 
37
- export interface ProcessInspector { getIdentity(pid: number): ProcessIdentity | null }
38
+ export { LocalProcessInspector } from './process.js';
39
+ export type { ProcessInspector } from './process.js';
38
40
  export interface DurableRunCompletion {
39
41
  status: DurableRunStatus; exitCode: number | null; summary: string; sessionHealth: DurableSessionHealth;
40
42
  }
@@ -64,13 +66,14 @@ export class DurableAgentRepository {
64
66
  const cwd = this.canonicalDirectory(input.cwd);
65
67
  const timestamp = this.now().toISOString();
66
68
  const id = randomUUID();
67
- let providerSessionId = randomUUID();
69
+ const provider = input.provider ?? 'claude';
70
+ let providerSessionId = provider === 'codex' ? null : randomUUID();
68
71
  while (providerSessionId === id) providerSessionId = randomUUID();
69
72
  try {
70
73
  this.db.execute(`INSERT INTO durable_agents (
71
74
  id, name, provider, mode, cwd, provider_session_id, state, session_health, created_at, updated_at
72
- ) VALUES (?, ?, 'claude', ?, ?, ?, 'ready', 'uninitialized', ?, ?)`,
73
- [id, input.name, AGENT_MODES.DURABLE, cwd, providerSessionId, timestamp, timestamp]);
75
+ ) VALUES (?, ?, ?, ?, ?, ?, 'ready', 'uninitialized', ?, ?)`,
76
+ [id, input.name, provider, AGENT_MODES.DURABLE, cwd, providerSessionId, timestamp, timestamp]);
74
77
  } catch (error) {
75
78
  if (/UNIQUE constraint failed: durable_agents\.name/i.test((error as Error).message)) {
76
79
  throw new DurableAgentNameConflictError(input.name);
@@ -157,6 +160,42 @@ export class DurableAgentRepository {
157
160
  if (changed.changes !== 1) throw new DurableAgentRepositoryError('Print run ownership changed.');
158
161
  }
159
162
 
163
+ async bindProviderSession(id: string, token: string, providerSessionId: string): Promise<DurableAgent> {
164
+ this.assertWritable();
165
+ if (!isUuid(providerSessionId)) {
166
+ throw new DurableAgentRepositoryError('Invalid provider session id.');
167
+ }
168
+ try {
169
+ this.immediate(() => {
170
+ const agent = this.findById(id);
171
+ if (!agent) throw new DurableAgentNotFoundError(id);
172
+ if (agent.provider !== 'codex') {
173
+ throw new DurableAgentRepositoryError('Only Codex durable sessions can be bound after creation.');
174
+ }
175
+ if (agent.activeRun?.token !== token) {
176
+ throw new DurableAgentRepositoryError('Print run ownership changed.');
177
+ }
178
+ if (agent.providerSessionId !== null && agent.providerSessionId !== providerSessionId) {
179
+ throw new CodexPrintError('Durable agent provider session identity does not match.', 'CODEX_SESSION_MISMATCH');
180
+ }
181
+ if (agent.providerSessionId === providerSessionId) return;
182
+ const changed = this.db.execute(`UPDATE durable_agents SET provider_session_id = ?, updated_at = ?
183
+ WHERE id = ? AND provider = 'codex' AND state = 'running' AND active_run_token = ?
184
+ AND provider_session_id IS NULL`,
185
+ [providerSessionId, this.now().toISOString(), id, token]);
186
+ if (changed.changes !== 1) throw new DurableAgentRepositoryError('Print run ownership changed.');
187
+ });
188
+ } catch (error) {
189
+ if (error instanceof DurableAgentRepositoryError || error instanceof DurableAgentNotFoundError
190
+ || error instanceof CodexPrintError) throw error;
191
+ if (/UNIQUE constraint failed: durable_agents\.provider_session_id/i.test((error as Error).message)) {
192
+ throw new CodexPrintError('Provider session is already bound to another durable agent.', 'CODEX_SESSION_MISMATCH');
193
+ }
194
+ throw this.storageError('Failed to bind durable-agent provider session', error);
195
+ }
196
+ return this.requireById(id);
197
+ }
198
+
160
199
  async completeRun(id: string, token: string, result: DurableRunCompletion): Promise<DurableAgent> {
161
200
  this.assertWritable();
162
201
  const completedAt = this.now().toISOString();
@@ -227,6 +266,10 @@ export class DurableAgentRepository {
227
266
  }
228
267
 
229
268
  private fromRow(row: DurableAgentRow): DurableAgent {
269
+ if (!['claude', 'codex', 'pi'].includes(row.provider)
270
+ || (row.provider !== 'codex' && row.provider_session_id === null)) {
271
+ throw new DurableAgentRepositoryError(`Invalid durable-agent provider record: ${row.id}`);
272
+ }
230
273
  const activeRun: DurableActiveRun | null = row.active_run_token === null ? null : {
231
274
  token: row.active_run_token,
232
275
  owner: { pid: row.active_owner_pid!, startedAt: row.active_owner_started_at! },
@@ -235,9 +278,9 @@ export class DurableAgentRepository {
235
278
  },
236
279
  startedAt: row.active_run_started_at!,
237
280
  };
238
- return {
281
+ const base = {
239
282
  id: row.id, name: row.name, provider: row.provider, mode: row.mode, cwd: row.cwd,
240
- providerSessionId: row.provider_session_id, state: row.state, sessionHealth: row.session_health,
283
+ state: row.state, sessionHealth: row.session_health,
241
284
  createdAt: row.created_at, updatedAt: row.updated_at, lastActiveAt: row.last_active_at,
242
285
  lastResult: row.last_result_status === null ? null : {
243
286
  status: row.last_result_status, completedAt: row.last_result_completed_at!,
@@ -245,6 +288,13 @@ export class DurableAgentRepository {
245
288
  },
246
289
  activeRun,
247
290
  };
291
+ if (row.provider === 'claude') {
292
+ return { ...base, provider: 'claude', providerSessionId: row.provider_session_id! };
293
+ }
294
+ if (row.provider === 'pi') {
295
+ return { ...base, provider: 'pi', providerSessionId: row.provider_session_id! };
296
+ }
297
+ return { ...base, provider: 'codex', providerSessionId: row.provider_session_id };
248
298
  }
249
299
 
250
300
  private canonicalDirectory(input: string): string {
@@ -284,24 +334,3 @@ export class DurableAgentRepository {
284
334
  : new DurableAgentRepositoryError(`${prefix}: ${(error as Error).message}`);
285
335
  }
286
336
  }
287
-
288
- export class LocalProcessInspector implements ProcessInspector {
289
- getIdentity(pid: number): ProcessIdentity | null {
290
- if (!Number.isInteger(pid) || pid <= 0) return null;
291
- try {
292
- if (process.platform === 'linux') {
293
- const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
294
- const close = stat.lastIndexOf(')');
295
- const fields = stat.slice(close + 2).split(' ');
296
- const startTicks = fields[19];
297
- return startTicks ? { pid, startedAt: `linux:${startTicks}` } : null;
298
- }
299
- const startedAt = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
300
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
301
- }).trim();
302
- return startedAt ? { pid, startedAt } : null;
303
- } catch {
304
- return null;
305
- }
306
- }
307
- }
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import { execFileSync } from 'child_process';
3
+ import type { ProcessIdentity } from './DurableAgent.js';
4
+
5
+ export interface ProcessInspector {
6
+ getIdentity(pid: number): ProcessIdentity | null;
7
+ }
8
+
9
+ export class LocalProcessInspector implements ProcessInspector {
10
+ getIdentity(pid: number): ProcessIdentity | null {
11
+ if (!Number.isInteger(pid) || pid <= 0) return null;
12
+ try {
13
+ if (process.platform === 'linux') {
14
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
15
+ const close = stat.lastIndexOf(')');
16
+ const fields = stat.slice(close + 2).split(' ');
17
+ const startTicks = fields[19];
18
+ return startTicks ? { pid, startedAt: `linux:${startTicks}` } : null;
19
+ }
20
+ const startedAt = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
21
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
22
+ }).trim();
23
+ return startedAt ? { pid, startedAt } : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,74 @@
1
+ import type { DurableAgent, DurableProvider } from './DurableAgent.js';
2
+ import { DurableAgentNotFoundError } from './DurableAgent.js';
3
+ import type { DurableRunCompletion } from './DurableAgentRepository.js';
4
+ import { sanitizeText } from './utils.js';
5
+
6
+ const MAX_RESULT_SUMMARY_LENGTH = 4096;
7
+
8
+ export interface DurableRunRepository {
9
+ resolve(reference: string): Promise<DurableAgent | DurableAgent[] | null>;
10
+ acquireRun(id: string): Promise<{ agent: DurableAgent; token: string }>;
11
+ completeRun(id: string, token: string, result: DurableRunCompletion): Promise<DurableAgent>;
12
+ }
13
+
14
+ type ProviderAgent<Provider extends DurableProvider> = Extract<DurableAgent, { provider: Provider }>;
15
+
16
+ interface DurableRunResult {
17
+ result: string;
18
+ exitCode: number;
19
+ }
20
+
21
+ interface DurableRunOptions<Provider extends DurableProvider, Result extends DurableRunResult> {
22
+ reference: string;
23
+ provider: Provider;
24
+ repository: DurableRunRepository;
25
+ ambiguousError(): Error;
26
+ providerError(): Error;
27
+ execute(agent: ProviderAgent<Provider>, token: string): Promise<Result>;
28
+ isSessionMismatch(error: unknown): boolean;
29
+ }
30
+
31
+ export async function runDurableAgent<Provider extends DurableProvider, Result extends DurableRunResult>(
32
+ options: DurableRunOptions<Provider, Result>,
33
+ ): Promise<{ agent: ProviderAgent<Provider>; result: Result }> {
34
+ const resolved = await options.repository.resolve(options.reference);
35
+ if (!resolved) throw new DurableAgentNotFoundError(options.reference);
36
+ if (Array.isArray(resolved)) throw options.ambiguousError();
37
+ if (resolved.provider !== options.provider) throw options.providerError();
38
+
39
+ const target = resolved as ProviderAgent<Provider>;
40
+ const acquired = await options.repository.acquireRun(target.id);
41
+ let result: Result;
42
+ try {
43
+ result = await options.execute(acquired.agent as ProviderAgent<Provider>, acquired.token);
44
+ } catch (error) {
45
+ await options.repository.completeRun(
46
+ target.id,
47
+ acquired.token,
48
+ createFailedCompletion(error, options.isSessionMismatch(error)),
49
+ );
50
+ throw error;
51
+ }
52
+
53
+ await options.repository.completeRun(target.id, acquired.token, createSucceededCompletion(result));
54
+ return { agent: target, result };
55
+ }
56
+
57
+ function createSucceededCompletion(result: DurableRunResult): DurableRunCompletion {
58
+ return {
59
+ status: 'succeeded',
60
+ exitCode: result.exitCode,
61
+ summary: sanitizeText(result.result, MAX_RESULT_SUMMARY_LENGTH, { preserveFormatting: true }),
62
+ sessionHealth: 'healthy',
63
+ };
64
+ }
65
+
66
+ function createFailedCompletion(error: unknown, sessionMismatch: boolean): DurableRunCompletion {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ return {
69
+ status: 'failed',
70
+ exitCode: null,
71
+ summary: sanitizeText(message, MAX_RESULT_SUMMARY_LENGTH, { preserveFormatting: true }),
72
+ sessionHealth: sessionMismatch ? 'mismatch' : 'unknown',
73
+ };
74
+ }
@@ -0,0 +1,61 @@
1
+ import type { ChildProcessWithoutNullStreams } from 'child_process';
2
+
3
+ export interface ChildCloseResult {
4
+ code: number | null;
5
+ signal: NodeJS.Signals | null;
6
+ }
7
+
8
+ export function waitForChildClose(child: ChildProcessWithoutNullStreams): Promise<ChildCloseResult> {
9
+ return new Promise((resolve, reject) => {
10
+ child.once('error', reject);
11
+ child.once('close', (code, signal) => resolve({ code, signal }));
12
+ });
13
+ }
14
+
15
+ export function sanitizeText(
16
+ value: string,
17
+ max: number,
18
+ options: { preserveFormatting?: boolean } = {},
19
+ ): string {
20
+ return Array.from(value, (character) => {
21
+ const code = character.charCodeAt(0);
22
+ if (options.preserveFormatting && (code === 9 || code === 10 || code === 13)) return character;
23
+ return code <= 31 || code === 127 ? ' ' : character;
24
+ }).join('').trim().slice(0, max);
25
+ }
26
+
27
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
28
+
29
+ export function isUuid(value: string): boolean {
30
+ return UUID_PATTERN.test(value);
31
+ }
32
+
33
+ export class LineBuffer {
34
+ private buffer = Buffer.alloc(0);
35
+
36
+ constructor(
37
+ private readonly maxLineBytes: number,
38
+ private readonly createOversizedLineError: () => Error,
39
+ ) {}
40
+
41
+ accept(chunk: Buffer | string): Buffer[] {
42
+ this.buffer = Buffer.concat([this.buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
43
+ if (this.buffer.length > this.maxLineBytes && this.buffer.indexOf(0x0a) < 0) {
44
+ throw this.createOversizedLineError();
45
+ }
46
+
47
+ const lines: Buffer[] = [];
48
+ let newline: number;
49
+ while ((newline = this.buffer.indexOf(0x0a)) >= 0) {
50
+ const line = this.buffer.subarray(0, newline);
51
+ this.buffer = this.buffer.subarray(newline + 1);
52
+ if (line.length > this.maxLineBytes) throw this.createOversizedLineError();
53
+ lines.push(line);
54
+ }
55
+ return lines;
56
+ }
57
+
58
+ assertComplete(createIncompleteLineError: () => Error): void {
59
+ if (this.buffer.length > 0) throw createIncompleteLineError();
60
+ }
61
+ }