awb-agent-manager 1.0.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -0
  3. package/dist/lib/agent-lockfile.d.ts +37 -0
  4. package/dist/lib/agent-lockfile.js +205 -0
  5. package/dist/lib/agent-lockfile.js.map +1 -0
  6. package/dist/lib/agent-manager-commands.d.ts +34 -0
  7. package/dist/lib/agent-manager-commands.js +531 -0
  8. package/dist/lib/agent-manager-commands.js.map +1 -0
  9. package/dist/lib/base-session-manager.d.ts +127 -0
  10. package/dist/lib/base-session-manager.js +667 -0
  11. package/dist/lib/base-session-manager.js.map +1 -0
  12. package/dist/lib/chat-attachment-prep.d.ts +35 -0
  13. package/dist/lib/chat-attachment-prep.js +180 -0
  14. package/dist/lib/chat-attachment-prep.js.map +1 -0
  15. package/dist/lib/chat-session-manager.d.ts +29 -0
  16. package/dist/lib/chat-session-manager.js +400 -0
  17. package/dist/lib/chat-session-manager.js.map +1 -0
  18. package/dist/lib/circuit-breaker.d.ts +34 -0
  19. package/dist/lib/circuit-breaker.js +146 -0
  20. package/dist/lib/circuit-breaker.js.map +1 -0
  21. package/dist/lib/cli-adapters/antigravity.d.ts +15 -0
  22. package/dist/lib/cli-adapters/antigravity.js +114 -0
  23. package/dist/lib/cli-adapters/antigravity.js.map +1 -0
  24. package/dist/lib/cli-adapters/base.d.ts +115 -0
  25. package/dist/lib/cli-adapters/base.js +111 -0
  26. package/dist/lib/cli-adapters/base.js.map +1 -0
  27. package/dist/lib/cli-adapters/claude.d.ts +19 -0
  28. package/dist/lib/cli-adapters/claude.js +233 -0
  29. package/dist/lib/cli-adapters/claude.js.map +1 -0
  30. package/dist/lib/cli-adapters/codex.d.ts +14 -0
  31. package/dist/lib/cli-adapters/codex.js +152 -0
  32. package/dist/lib/cli-adapters/codex.js.map +1 -0
  33. package/dist/lib/cli-adapters/deepseek.d.ts +15 -0
  34. package/dist/lib/cli-adapters/deepseek.js +70 -0
  35. package/dist/lib/cli-adapters/deepseek.js.map +1 -0
  36. package/dist/lib/cli-adapters/index.d.ts +8 -0
  37. package/dist/lib/cli-adapters/index.js +27 -0
  38. package/dist/lib/cli-adapters/index.js.map +1 -0
  39. package/dist/lib/cli-adapters/model-introspect.d.ts +3 -0
  40. package/dist/lib/cli-adapters/model-introspect.js +65 -0
  41. package/dist/lib/cli-adapters/model-introspect.js.map +1 -0
  42. package/dist/lib/cli-error-signatures.d.ts +9 -0
  43. package/dist/lib/cli-error-signatures.js +22 -0
  44. package/dist/lib/cli-error-signatures.js.map +1 -0
  45. package/dist/lib/cli-output-summary.d.ts +2 -0
  46. package/dist/lib/cli-output-summary.js +104 -0
  47. package/dist/lib/cli-output-summary.js.map +1 -0
  48. package/dist/lib/cli-resolver.d.ts +2 -0
  49. package/dist/lib/cli-resolver.js +177 -0
  50. package/dist/lib/cli-resolver.js.map +1 -0
  51. package/dist/lib/config.d.ts +13 -0
  52. package/dist/lib/config.js +124 -0
  53. package/dist/lib/config.js.map +1 -0
  54. package/dist/lib/constants.d.ts +36 -0
  55. package/dist/lib/constants.js +51 -0
  56. package/dist/lib/constants.js.map +1 -0
  57. package/dist/lib/environment-provisioner.d.ts +31 -0
  58. package/dist/lib/environment-provisioner.js +182 -0
  59. package/dist/lib/environment-provisioner.js.map +1 -0
  60. package/dist/lib/error-log-uploader.d.ts +19 -0
  61. package/dist/lib/error-log-uploader.js +138 -0
  62. package/dist/lib/error-log-uploader.js.map +1 -0
  63. package/dist/lib/event-dispatcher.d.ts +149 -0
  64. package/dist/lib/event-dispatcher.js +799 -0
  65. package/dist/lib/event-dispatcher.js.map +1 -0
  66. package/dist/lib/event-log-recorder.d.ts +11 -0
  67. package/dist/lib/event-log-recorder.js +77 -0
  68. package/dist/lib/event-log-recorder.js.map +1 -0
  69. package/dist/lib/event-stream.d.ts +26 -0
  70. package/dist/lib/event-stream.js +177 -0
  71. package/dist/lib/event-stream.js.map +1 -0
  72. package/dist/lib/fs-browser.d.ts +30 -0
  73. package/dist/lib/fs-browser.js +287 -0
  74. package/dist/lib/fs-browser.js.map +1 -0
  75. package/dist/lib/host-mcp/logs.d.ts +19 -0
  76. package/dist/lib/host-mcp/logs.js +102 -0
  77. package/dist/lib/host-mcp/logs.js.map +1 -0
  78. package/dist/lib/host-mcp/platform.d.ts +24 -0
  79. package/dist/lib/host-mcp/platform.js +133 -0
  80. package/dist/lib/host-mcp/platform.js.map +1 -0
  81. package/dist/lib/host-mcp/screens.d.ts +22 -0
  82. package/dist/lib/host-mcp/screens.js +172 -0
  83. package/dist/lib/host-mcp/screens.js.map +1 -0
  84. package/dist/lib/host-mcp/server.d.ts +3 -0
  85. package/dist/lib/host-mcp/server.js +233 -0
  86. package/dist/lib/host-mcp/server.js.map +1 -0
  87. package/dist/lib/host-mcp/tools.d.ts +172 -0
  88. package/dist/lib/host-mcp/tools.js +1241 -0
  89. package/dist/lib/host-mcp/tools.js.map +1 -0
  90. package/dist/lib/instance-heartbeat.d.ts +55 -0
  91. package/dist/lib/instance-heartbeat.js +118 -0
  92. package/dist/lib/instance-heartbeat.js.map +1 -0
  93. package/dist/lib/legacy-import.d.ts +17 -0
  94. package/dist/lib/legacy-import.js +70 -0
  95. package/dist/lib/legacy-import.js.map +1 -0
  96. package/dist/lib/logging.d.ts +3 -0
  97. package/dist/lib/logging.js +65 -0
  98. package/dist/lib/logging.js.map +1 -0
  99. package/dist/lib/managed-agent-context.d.ts +25 -0
  100. package/dist/lib/managed-agent-context.js +33 -0
  101. package/dist/lib/managed-agent-context.js.map +1 -0
  102. package/dist/lib/managed-agent-store.d.ts +34 -0
  103. package/dist/lib/managed-agent-store.js +160 -0
  104. package/dist/lib/managed-agent-store.js.map +1 -0
  105. package/dist/lib/managed-agents.d.ts +24 -0
  106. package/dist/lib/managed-agents.js +87 -0
  107. package/dist/lib/managed-agents.js.map +1 -0
  108. package/dist/lib/mcp-client.d.ts +8 -0
  109. package/dist/lib/mcp-client.js +122 -0
  110. package/dist/lib/mcp-client.js.map +1 -0
  111. package/dist/lib/orphan-cleanup.d.ts +6 -0
  112. package/dist/lib/orphan-cleanup.js +108 -0
  113. package/dist/lib/orphan-cleanup.js.map +1 -0
  114. package/dist/lib/presence-heartbeat.d.ts +8 -0
  115. package/dist/lib/presence-heartbeat.js +61 -0
  116. package/dist/lib/presence-heartbeat.js.map +1 -0
  117. package/dist/lib/prompts.d.ts +29 -0
  118. package/dist/lib/prompts.js +208 -0
  119. package/dist/lib/prompts.js.map +1 -0
  120. package/dist/lib/rest.d.ts +61 -0
  121. package/dist/lib/rest.js +307 -0
  122. package/dist/lib/rest.js.map +1 -0
  123. package/dist/lib/run-provisioner.d.ts +21 -0
  124. package/dist/lib/run-provisioner.js +115 -0
  125. package/dist/lib/run-provisioner.js.map +1 -0
  126. package/dist/lib/self-path.d.ts +7 -0
  127. package/dist/lib/self-path.js +29 -0
  128. package/dist/lib/self-path.js.map +1 -0
  129. package/dist/lib/self-update.d.ts +46 -0
  130. package/dist/lib/self-update.js +482 -0
  131. package/dist/lib/self-update.js.map +1 -0
  132. package/dist/lib/service-install.d.ts +23 -0
  133. package/dist/lib/service-install.js +673 -0
  134. package/dist/lib/service-install.js.map +1 -0
  135. package/dist/lib/setup.d.ts +16 -0
  136. package/dist/lib/setup.js +157 -0
  137. package/dist/lib/setup.js.map +1 -0
  138. package/dist/lib/subagent-manager.d.ts +84 -0
  139. package/dist/lib/subagent-manager.js +793 -0
  140. package/dist/lib/subagent-manager.js.map +1 -0
  141. package/dist/lib/subagent-monitor.d.ts +43 -0
  142. package/dist/lib/subagent-monitor.js +229 -0
  143. package/dist/lib/subagent-monitor.js.map +1 -0
  144. package/dist/lib/ticket-session-manager.d.ts +23 -0
  145. package/dist/lib/ticket-session-manager.js +471 -0
  146. package/dist/lib/ticket-session-manager.js.map +1 -0
  147. package/dist/lib/worktree-manager.d.ts +39 -0
  148. package/dist/lib/worktree-manager.js +226 -0
  149. package/dist/lib/worktree-manager.js.map +1 -0
  150. package/dist/main.d.ts +2 -0
  151. package/dist/main.js +670 -0
  152. package/dist/main.js.map +1 -0
  153. package/dist/package.json +57 -0
  154. package/package.json +57 -0
@@ -0,0 +1,667 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { createInterface } from 'node:readline';
4
+ import { spawn } from 'node:child_process';
5
+ import { SUBAGENTS_BASE_DIR, STOP_GRACE_MS } from './constants.js';
6
+ import { log } from './logging.js';
7
+ import { summarizeCliEvent } from './cli-output-summary.js';
8
+ import { createAdapter } from './cli-adapters/index.js';
9
+ import { ADAPTER_CAPABILITIES, PARSE_STAGE, describeHarness, partitionHarness, selectEffortSlice, } from './cli-adapters/base.js';
10
+ const { PERSISTENT_SESSION } = ADAPTER_CAPABILITIES;
11
+ const UNHEALTHY_TURN_THRESHOLD = 5;
12
+ const UNHEALTHY_DURATION_MS = 30 * 60 * 1000;
13
+ const HEALTH_SWEEP_INTERVAL_MS = 60 * 1000;
14
+ const OUTPUT_RING_MAX = 100;
15
+ export class BaseSessionManager {
16
+ _config;
17
+ #adapters = new Map();
18
+ _sessions = new Map();
19
+ _inflight = new Map();
20
+ _outputRings = new Map();
21
+ #dedupSet = new Set();
22
+ #dedupQueue = [];
23
+ #DEDUP_MAX = 200;
24
+ #healthTimer = null;
25
+ #keyField;
26
+ #logTag;
27
+ #cfgPrefix;
28
+ #kindLabel;
29
+ #monitor = null;
30
+ constructor(config, options) {
31
+ this._config = config;
32
+ this.#keyField = options.keyField;
33
+ this.#logTag = options.logTag;
34
+ this.#cfgPrefix = options.cfgPrefix;
35
+ this.#kindLabel = options.kindLabel;
36
+ }
37
+ get _adapter() {
38
+ return this._adapterFor('claude');
39
+ }
40
+ _adapterFor(cli) {
41
+ const t = String(cli || 'claude').toLowerCase();
42
+ let a = this.#adapters.get(t);
43
+ if (!a) {
44
+ a = createAdapter(t);
45
+ this.#adapters.set(t, a);
46
+ }
47
+ return a;
48
+ }
49
+ setMonitor(monitor) {
50
+ this.#monitor = monitor;
51
+ }
52
+ _getSession(sessionKey) {
53
+ return this._sessions.get(sessionKey);
54
+ }
55
+ _isPidAlive(pid) {
56
+ if (!pid || pid <= 0)
57
+ return false;
58
+ try {
59
+ process.kill(pid, 0);
60
+ return true;
61
+ }
62
+ catch (err) {
63
+ return err?.code === 'EPERM';
64
+ }
65
+ }
66
+ _getLiveSession(sessionKey) {
67
+ const sess = this._sessions.get(sessionKey);
68
+ if (!sess)
69
+ return undefined;
70
+ if (sess.unhealthyKilled) {
71
+ log(`${this.#logTag} unhealthy ${this.#keyField}=${sessionKey} pid=${sess.pid} — not reusing a session under teardown; purging in-memory record`);
72
+ if (sess.idleTimer) {
73
+ clearTimeout(sess.idleTimer);
74
+ sess.idleTimer = null;
75
+ }
76
+ this.#endTurn(sess);
77
+ this._sessions.delete(sessionKey);
78
+ return undefined;
79
+ }
80
+ if (this._isPidAlive(sess.pid))
81
+ return sess;
82
+ log(`${this.#logTag} stale ${this.#keyField}=${sessionKey} pid=${sess.pid} — child reaped without exit-handler cleanup; purging in-memory record`);
83
+ if (sess.idleTimer) {
84
+ clearTimeout(sess.idleTimer);
85
+ sess.idleTimer = null;
86
+ }
87
+ this.#endTurn(sess);
88
+ this._sessions.delete(sessionKey);
89
+ return undefined;
90
+ }
91
+ _ensureCapacity() {
92
+ const cap = this._config.delegation.maxConcurrent ?? 5;
93
+ if (this._sessions.size < cap)
94
+ return true;
95
+ return this.#evictLru();
96
+ }
97
+ async _spawnSession(sessionKey, rolePrompt, firstTurnText, { onProgress, monitorMeta, agentContext, firstTurnImages, harness: rawHarness, effortPreset, envVars } = {}) {
98
+ const adapter = this._adapterFor(agentContext?.cli);
99
+ if (!adapter.has(PERSISTENT_SESSION)) {
100
+ log(`${this.#logTag} adapter cli=${adapter.cliType} does not support persistent sessions; refusing to spawn`);
101
+ return null;
102
+ }
103
+ const effectiveApiKey = agentContext?.api_key || this._config.apiKey;
104
+ const effectiveCwd = agentContext?.cwd || undefined;
105
+ const { applied: harness, skipped: harnessSkipped } = partitionHarness(adapter, rawHarness);
106
+ if (harnessSkipped.length > 0) {
107
+ log(`${this.#logTag} harness keys skipped (cli=${adapter.cliType} can't express them): ${harnessSkipped.join(', ')}`);
108
+ }
109
+ if (harness) {
110
+ log(`${this.#logTag} harness applied: ${this.#keyField}=${sessionKey} cli=${adapter.cliType} ${describeHarness(harness)}`);
111
+ }
112
+ const slice = selectEffortSlice(adapter.cliType, effortPreset);
113
+ const effectiveModel = slice?.model ?? harness?.model ?? agentContext?.model ?? null;
114
+ const effortFlag = slice?.effort ?? null;
115
+ const ultracode = !!slice?.ultracode;
116
+ if (slice && (effortFlag || ultracode || slice.model)) {
117
+ log(`${this.#logTag} effort applied: ${this.#keyField}=${sessionKey} cli=${adapter.cliType} ` +
118
+ `effort=${effortFlag ?? '-'} ultracode=${ultracode} model=${slice.model ?? '-'}`);
119
+ }
120
+ let configPath = null;
121
+ let configPathIsTemp = false;
122
+ let pidPath = null;
123
+ try {
124
+ let descriptor = adapter.buildSessionSpawn({
125
+ rolePrompt: rolePrompt || '',
126
+ mcpConfigPath: null,
127
+ model: effectiveModel,
128
+ harness,
129
+ effort: effortFlag,
130
+ ultracode,
131
+ });
132
+ if (descriptor.needsMcpConfig) {
133
+ const needsSessionPin = !!(monitorMeta?.ticket_id && monitorMeta?.role);
134
+ if (agentContext?.mcp_config_path && !needsSessionPin) {
135
+ configPath = agentContext.mcp_config_path;
136
+ configPathIsTemp = false;
137
+ }
138
+ else {
139
+ configPath = join(SUBAGENTS_BASE_DIR, `${this.#cfgPrefix}${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
140
+ configPathIsTemp = true;
141
+ await fsp.mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
142
+ const headers = {
143
+ Authorization: `Bearer ${effectiveApiKey}`,
144
+ 'X-AWB-Client-Type': agentContext ? 'managed-subagent' : 'subagent',
145
+ };
146
+ if (monitorMeta?.ticket_id)
147
+ headers['X-AWB-Subagent-Ticket-Id'] = monitorMeta.ticket_id;
148
+ if (monitorMeta?.role)
149
+ headers['X-AWB-Subagent-Role'] = monitorMeta.role;
150
+ if (monitorMeta?.trigger_source)
151
+ headers['X-AWB-Subagent-Trigger-Source'] = monitorMeta.trigger_source;
152
+ const mcpConfig = {
153
+ mcpServers: {
154
+ awb: {
155
+ type: 'http',
156
+ url: `${this._config.url.replace(/\/$/, '')}/mcp`,
157
+ headers,
158
+ },
159
+ },
160
+ };
161
+ await fsp.writeFile(configPath, JSON.stringify(mcpConfig), { mode: 0o600 });
162
+ }
163
+ descriptor = adapter.buildSessionSpawn({
164
+ rolePrompt: rolePrompt || '',
165
+ mcpConfigPath: configPath,
166
+ model: effectiveModel,
167
+ harness,
168
+ effort: effortFlag,
169
+ ultracode,
170
+ });
171
+ }
172
+ const binOverride = adapter.cliType === 'claude' ? this._config.delegation.claudeBin : null;
173
+ const resolvedBin = adapter.resolveBin(binOverride);
174
+ const cliHomeEnvKey = adapter.configDirEnv();
175
+ const cliHomeEnv = cliHomeEnvKey && agentContext?.cli_home_dir
176
+ ? { [cliHomeEnvKey]: agentContext.cli_home_dir }
177
+ : {};
178
+ const credentialEnv = agentContext?.extra_env ?? {};
179
+ const baseEnv = { ...process.env };
180
+ if (agentContext?.credential_provider) {
181
+ const stripped = [];
182
+ for (const k of adapter.authEnvKeys()) {
183
+ if (k in baseEnv) {
184
+ delete baseEnv[k];
185
+ stripped.push(k);
186
+ }
187
+ }
188
+ if (stripped.length > 0) {
189
+ log(`${this.#logTag} env strip: agent=${agentContext.agent_id.slice(0, 8)} ` +
190
+ `provider=${agentContext.credential_provider} removed=${stripped.join(',')} ` +
191
+ `(operator-inherited auth would have overridden per-agent credential)`);
192
+ }
193
+ }
194
+ const child = spawn(resolvedBin, descriptor.args, {
195
+ stdio: descriptor.stdio || ['pipe', 'pipe', 'pipe'],
196
+ detached: process.platform !== 'win32',
197
+ windowsHide: true,
198
+ cwd: effectiveCwd,
199
+ env: {
200
+ ...baseEnv,
201
+ ...(envVars ?? {}),
202
+ AWB_API_KEY: effectiveApiKey,
203
+ ...cliHomeEnv,
204
+ ...credentialEnv,
205
+ ...adapter.harnessEnv(harness),
206
+ },
207
+ shell: descriptor.shell ?? /\.(cmd|bat|ps1)$/i.test(resolvedBin),
208
+ });
209
+ child.once('error', (err) => {
210
+ log(`${this.#logTag} spawn error: code=${err?.code || ''} cli=${adapter.cliType} bin=${resolvedBin} msg=${err?.message}`);
211
+ });
212
+ child.unref();
213
+ if (!child.pid) {
214
+ if (configPath && configPathIsTemp)
215
+ await fsp.unlink(configPath).catch(() => { });
216
+ return null;
217
+ }
218
+ if (configPath && configPathIsTemp) {
219
+ pidPath = configPath.replace(/\.json$/, '.pid');
220
+ await fsp.writeFile(pidPath, String(child.pid), { mode: 0o600 }).catch(() => { });
221
+ }
222
+ const sess = {
223
+ [this.#keyField]: sessionKey,
224
+ pid: child.pid,
225
+ cli_type: adapter.cliType,
226
+ adapter,
227
+ child,
228
+ configPath,
229
+ configPathIsTemp,
230
+ pidPath,
231
+ turnCount: 0,
232
+ startedAt: Date.now(),
233
+ lastTouchedAt: Date.now(),
234
+ idleTimer: null,
235
+ unrespondedTurnCount: 0,
236
+ unrespondedSince: null,
237
+ unhealthyKilled: false,
238
+ tap: null,
239
+ };
240
+ sess.tap =
241
+ this.#monitor?.register({
242
+ kind: this.#kindLabel === 'chat_session'
243
+ ? 'chat'
244
+ : this.#kindLabel === 'ticket_session'
245
+ ? 'ticket'
246
+ : 'oneshot',
247
+ sessionKey,
248
+ pid: child.pid,
249
+ ticketId: monitorMeta?.ticket_id,
250
+ ticketTitle: monitorMeta?.ticket_title,
251
+ role: monitorMeta?.role,
252
+ apiKey: agentContext?.api_key,
253
+ }) || null;
254
+ this.#wireStdio(sess);
255
+ this.#wireExit(sess);
256
+ log(`${this.#logTag} spawned new pid=${sess.pid} cli=${adapter.cliType} kind=${this.#kindLabel} ${this.#keyField}=${sessionKey}`);
257
+ this.#startTurn(sess, onProgress);
258
+ this._writeTurn(sess, firstTurnText, firstTurnImages);
259
+ sess.turnCount = 1;
260
+ this._resetIdleTimer(sess);
261
+ this._sessions.set(sessionKey, sess);
262
+ this.#ensureHealthSweep();
263
+ return sess;
264
+ }
265
+ catch (err) {
266
+ log(`${this.#logTag} spawn error ${this.#keyField}=${sessionKey}: ${err?.message ?? err}`);
267
+ if (configPath && configPathIsTemp)
268
+ await fsp.unlink(configPath).catch(() => { });
269
+ return null;
270
+ }
271
+ }
272
+ _sendFollowUp(sess, turnText, { checkMaxTurns = true, onProgress, images, } = {}) {
273
+ this.#startTurn(sess, onProgress);
274
+ this._writeTurn(sess, turnText, images);
275
+ sess.turnCount++;
276
+ sess.lastTouchedAt = Date.now();
277
+ this._resetIdleTimer(sess);
278
+ if (!checkMaxTurns)
279
+ return;
280
+ const maxTurns = this._config.delegation.maxTurnsPerSession ?? 30;
281
+ if (sess.turnCount >= maxTurns) {
282
+ log(`${this.#logTag} ${this.#keyField}=${sess[this.#keyField]} hit maxTurns=${maxTurns}, closing stdin for respawn`);
283
+ try {
284
+ sess.child.stdin.end();
285
+ }
286
+ catch {
287
+ }
288
+ }
289
+ }
290
+ #startTurn(sess, onProgress) {
291
+ this.#endTurn(sess);
292
+ if (typeof onProgress !== 'function')
293
+ return;
294
+ const turn = {
295
+ onProgress,
296
+ stage: null,
297
+ fired: { thinking: false, composing: false },
298
+ heartbeatTimer: null,
299
+ };
300
+ sess._currentTurn = turn;
301
+ turn.heartbeatTimer = setInterval(() => {
302
+ if (sess._currentTurn === turn && turn.stage) {
303
+ try {
304
+ turn.onProgress(turn.stage);
305
+ }
306
+ catch (err) {
307
+ log(`${this.#logTag} onProgress heartbeat error: ${err?.message ?? err}`);
308
+ }
309
+ }
310
+ }, 10_000);
311
+ turn.heartbeatTimer.unref?.();
312
+ }
313
+ #endTurn(sess) {
314
+ const turn = sess._currentTurn;
315
+ if (!turn)
316
+ return;
317
+ if (turn.heartbeatTimer)
318
+ clearInterval(turn.heartbeatTimer);
319
+ sess._currentTurn = null;
320
+ }
321
+ #advanceTurn(sess, parsed) {
322
+ const turn = sess._currentTurn;
323
+ if (!turn)
324
+ return;
325
+ if (!turn.fired.thinking && parsed.stage) {
326
+ turn.fired.thinking = true;
327
+ turn.stage = PARSE_STAGE.THINKING;
328
+ try {
329
+ turn.onProgress(PARSE_STAGE.THINKING);
330
+ }
331
+ catch (err) {
332
+ log(`${this.#logTag} onProgress(thinking) error: ${err?.message ?? err}`);
333
+ }
334
+ }
335
+ if (!turn.fired.composing && parsed.stage === PARSE_STAGE.COMPOSING) {
336
+ turn.fired.composing = true;
337
+ turn.stage = PARSE_STAGE.COMPOSING;
338
+ try {
339
+ turn.onProgress(PARSE_STAGE.COMPOSING);
340
+ }
341
+ catch (err) {
342
+ log(`${this.#logTag} onProgress(composing) error: ${err?.message ?? err}`);
343
+ }
344
+ }
345
+ if (parsed.isResult) {
346
+ sess.unrespondedTurnCount = 0;
347
+ sess.unrespondedSince = null;
348
+ try {
349
+ sess.onResult?.(parsed.raw);
350
+ }
351
+ catch (err) {
352
+ log(`${this.#logTag} onResult error: ${err?.message ?? err}`);
353
+ }
354
+ this.#endTurn(sess);
355
+ }
356
+ }
357
+ _writeTurn(sess, text, images) {
358
+ const wire = sess.adapter.formatTurn(String(text), images);
359
+ try {
360
+ sess.child.stdin.write(wire + '\n');
361
+ sess.tap?.inLine(wire);
362
+ sess.unrespondedTurnCount = (sess.unrespondedTurnCount || 0) + 1;
363
+ if (!sess.unrespondedSince)
364
+ sess.unrespondedSince = Date.now();
365
+ log(`${this.#logTag} dispatched turn ${this.#keyField}=${sess[this.#keyField]} pid=${sess.pid} turn=${sess.turnCount + 1} bytes=${Buffer.byteLength(text)}`);
366
+ }
367
+ catch (err) {
368
+ log(`${this.#logTag} stdin write failed pid=${sess.pid}: ${err?.message ?? err}`);
369
+ return;
370
+ }
371
+ if (sess.unrespondedTurnCount >= UNHEALTHY_TURN_THRESHOLD) {
372
+ this.#killUnhealthy(sess, `${sess.unrespondedTurnCount} consecutive turns without an LLM response`);
373
+ }
374
+ }
375
+ #wireStdio(sess) {
376
+ if (sess.child.stdout) {
377
+ const rlOut = createInterface({ input: sess.child.stdout });
378
+ const tag = this.#logTag.replace(/^\[|\]$/g, '');
379
+ rlOut.on('line', (line) => {
380
+ sess.tap?.outLine(line);
381
+ const parsed = sess.adapter.parseStdoutLine(line);
382
+ if (parsed.stage || parsed.isResult) {
383
+ sess.unrespondedTurnCount = 0;
384
+ sess.unrespondedSince = null;
385
+ }
386
+ this.#advanceTurn(sess, parsed);
387
+ if (!parsed.raw) {
388
+ const trimmed = line.trim();
389
+ if (trimmed)
390
+ this.#pushOutputLine(sess.pid, trimmed);
391
+ }
392
+ else {
393
+ const summary = summarizeCliEvent(parsed.raw);
394
+ if (summary)
395
+ this.#pushOutputLine(sess.pid, summary);
396
+ }
397
+ this._onStdoutParsed(sess, parsed, line);
398
+ if (parsed.isResult) {
399
+ const subtype = parsed.raw?.subtype || '-';
400
+ const isError = parsed.isError === true ? 'true' : (parsed.raw?.is_error ?? '-');
401
+ log(`[${tag}:${sess.pid}] result subtype=${subtype} is_error=${isError}`);
402
+ }
403
+ });
404
+ }
405
+ if (sess.child.stderr) {
406
+ const rlErr = createInterface({ input: sess.child.stderr });
407
+ const tag = this.#logTag.replace(/^\[|\]$/g, '');
408
+ rlErr.on('line', (line) => {
409
+ log(`[${tag}:${sess.pid}:err] ${line}`);
410
+ const trimmed = line.trim();
411
+ if (trimmed)
412
+ this.#pushOutputLine(sess.pid, trimmed);
413
+ this._onStderrLine(sess, line);
414
+ });
415
+ }
416
+ }
417
+ #pushOutputLine(pid, line) {
418
+ let ring = this._outputRings.get(pid);
419
+ if (!ring) {
420
+ ring = [];
421
+ this._outputRings.set(pid, ring);
422
+ }
423
+ ring.push(line);
424
+ while (ring.length > OUTPUT_RING_MAX)
425
+ ring.shift();
426
+ }
427
+ _collectOutputTail(pid, maxChars) {
428
+ const ring = this._outputRings.get(pid);
429
+ if (!ring || ring.length === 0)
430
+ return '';
431
+ let body = ring.join('\n').trim();
432
+ if (maxChars > 0 && body.length > maxChars) {
433
+ body = '…' + body.slice(-maxChars);
434
+ }
435
+ return body;
436
+ }
437
+ _clearOutputBuffer(pid) {
438
+ this._outputRings.delete(pid);
439
+ }
440
+ #wireExit(sess) {
441
+ sess.child.once('exit', async (code, signal) => {
442
+ if (sess.idleTimer) {
443
+ clearTimeout(sess.idleTimer);
444
+ sess.idleTimer = null;
445
+ }
446
+ this.#endTurn(sess);
447
+ const durationSec = Math.round((Date.now() - sess.startedAt) / 1000);
448
+ const key = sess[this.#keyField];
449
+ sess.tap?.end({ exit_code: code, signal });
450
+ log(`${this.#logTag} exit pid=${sess.pid} ${this.#keyField}=${key} code=${code} signal=${signal || '-'} turns=${sess.turnCount} duration=${durationSec}s`);
451
+ try {
452
+ await this._onChildExit(sess, code, signal);
453
+ }
454
+ catch (err) {
455
+ log(`${this.#logTag} _onChildExit error: ${err?.message ?? err}`);
456
+ }
457
+ this._clearOutputBuffer(sess.pid);
458
+ if (this._sessions.get(key) === sess)
459
+ this._sessions.delete(key);
460
+ if (sess.configPath && sess.configPathIsTemp) {
461
+ try {
462
+ await fsp.unlink(sess.configPath);
463
+ }
464
+ catch {
465
+ }
466
+ }
467
+ if (sess.pidPath) {
468
+ try {
469
+ await fsp.unlink(sess.pidPath);
470
+ }
471
+ catch {
472
+ }
473
+ }
474
+ });
475
+ sess.child.once('error', (err) => log(`${this.#logTag} child error pid=${sess.pid}: ${err?.message ?? err}`));
476
+ }
477
+ _resetIdleTimer(sess) {
478
+ if (sess.idleTimer)
479
+ clearTimeout(sess.idleTimer);
480
+ const mins = this._config.delegation.idleMinutes ?? 10;
481
+ sess.idleTimer = setTimeout(() => {
482
+ log(`${this.#logTag} idle, closing stdin ${this.#keyField}=${sess[this.#keyField]} pid=${sess.pid}`);
483
+ try {
484
+ sess.child.stdin.end();
485
+ }
486
+ catch {
487
+ }
488
+ }, mins * 60_000);
489
+ sess.idleTimer.unref?.();
490
+ }
491
+ _onStdoutParsed(_sess, _parsed, _rawLine) { }
492
+ _onStderrLine(_sess, _line) { }
493
+ async _onChildExit(_sess, _code, _signal) { }
494
+ #ensureHealthSweep() {
495
+ if (this.#healthTimer)
496
+ return;
497
+ this.#healthTimer = setInterval(() => this.#healthSweep(), HEALTH_SWEEP_INTERVAL_MS);
498
+ this.#healthTimer.unref?.();
499
+ }
500
+ #healthSweep() {
501
+ const now = Date.now();
502
+ for (const sess of this._sessions.values()) {
503
+ if (sess.unhealthyKilled)
504
+ continue;
505
+ if (!sess.unrespondedSince)
506
+ continue;
507
+ const elapsed = now - sess.unrespondedSince;
508
+ if (elapsed >= UNHEALTHY_DURATION_MS) {
509
+ this.#killUnhealthy(sess, `${Math.round(elapsed / 60_000)}m elapsed without an LLM response`);
510
+ }
511
+ }
512
+ }
513
+ #killUnhealthy(sess, reason) {
514
+ if (sess.unhealthyKilled)
515
+ return;
516
+ sess.unhealthyKilled = true;
517
+ const key = sess[this.#keyField];
518
+ log(`${this.#logTag} UNHEALTHY ${this.#keyField}=${key} pid=${sess.pid} — ${reason}; killing for respawn`);
519
+ if (this._sessions.get(key) === sess)
520
+ this._sessions.delete(key);
521
+ if (sess.idleTimer) {
522
+ clearTimeout(sess.idleTimer);
523
+ sess.idleTimer = null;
524
+ }
525
+ try {
526
+ sess.child.stdin.end();
527
+ }
528
+ catch {
529
+ }
530
+ try {
531
+ process.kill(sess.pid, 'SIGTERM');
532
+ }
533
+ catch {
534
+ }
535
+ setTimeout(() => {
536
+ try {
537
+ process.kill(sess.pid, 'SIGKILL');
538
+ }
539
+ catch {
540
+ }
541
+ }, STOP_GRACE_MS);
542
+ }
543
+ #evictLru() {
544
+ let oldestKey = null;
545
+ let oldest = Infinity;
546
+ for (const [k, s] of this._sessions.entries()) {
547
+ if (s.lastTouchedAt < oldest) {
548
+ oldest = s.lastTouchedAt;
549
+ oldestKey = k;
550
+ }
551
+ }
552
+ if (!oldestKey)
553
+ return false;
554
+ const s = this._sessions.get(oldestKey);
555
+ log(`${this.#logTag} evicting lru ${this.#keyField}=${oldestKey} pid=${s.pid}`);
556
+ if (s.idleTimer) {
557
+ clearTimeout(s.idleTimer);
558
+ s.idleTimer = null;
559
+ }
560
+ try {
561
+ s.child.stdin.end();
562
+ }
563
+ catch {
564
+ }
565
+ this._sessions.delete(oldestKey);
566
+ return true;
567
+ }
568
+ _rememberDedup(key) {
569
+ if (this.#dedupSet.has(key))
570
+ return false;
571
+ this.#dedupSet.add(key);
572
+ this.#dedupQueue.push(key);
573
+ while (this.#dedupQueue.length > this.#DEDUP_MAX) {
574
+ const old = this.#dedupQueue.shift();
575
+ if (old !== undefined)
576
+ this.#dedupSet.delete(old);
577
+ }
578
+ return true;
579
+ }
580
+ _forgetDedup(key) {
581
+ if (!this.#dedupSet.delete(key))
582
+ return;
583
+ const idx = this.#dedupQueue.indexOf(key);
584
+ if (idx >= 0)
585
+ this.#dedupQueue.splice(idx, 1);
586
+ }
587
+ async stopForAgent(agentId) {
588
+ if (!agentId)
589
+ return { count: 0, inflight: [] };
590
+ const victims = Array.from(this._sessions.values()).filter((s) => s.agentId === agentId);
591
+ if (victims.length === 0)
592
+ return { count: 0, inflight: [] };
593
+ for (const sess of victims) {
594
+ if (sess.idleTimer) {
595
+ clearTimeout(sess.idleTimer);
596
+ sess.idleTimer = null;
597
+ }
598
+ try {
599
+ sess.child.stdin.end();
600
+ }
601
+ catch {
602
+ }
603
+ try {
604
+ process.kill(sess.pid, 'SIGTERM');
605
+ }
606
+ catch {
607
+ }
608
+ }
609
+ log(`${this.#logTag} stopForAgent: agent=${agentId.slice(0, 8)} signalled ${victims.length} session(s) — SIGTERM`);
610
+ setTimeout(() => {
611
+ for (const sess of victims) {
612
+ try {
613
+ process.kill(sess.pid, 0);
614
+ try {
615
+ process.kill(sess.pid, 'SIGKILL');
616
+ }
617
+ catch {
618
+ }
619
+ }
620
+ catch {
621
+ }
622
+ }
623
+ }, STOP_GRACE_MS).unref?.();
624
+ const inflight = victims
625
+ .filter((s) => s.ticketId)
626
+ .map((s) => ({ ticketId: s.ticketId, role: s.role || '' }));
627
+ return { count: victims.length, inflight };
628
+ }
629
+ async stop() {
630
+ if (this.#healthTimer) {
631
+ clearInterval(this.#healthTimer);
632
+ this.#healthTimer = null;
633
+ }
634
+ const sessions = Array.from(this._sessions.values());
635
+ for (const sess of sessions) {
636
+ if (sess.idleTimer) {
637
+ clearTimeout(sess.idleTimer);
638
+ sess.idleTimer = null;
639
+ }
640
+ try {
641
+ sess.child.stdin.end();
642
+ }
643
+ catch {
644
+ }
645
+ try {
646
+ process.kill(sess.pid, 'SIGTERM');
647
+ }
648
+ catch {
649
+ }
650
+ }
651
+ if (sessions.length === 0) {
652
+ this._sessions.clear();
653
+ return;
654
+ }
655
+ await new Promise((r) => setTimeout(r, STOP_GRACE_MS));
656
+ for (const sess of sessions) {
657
+ try {
658
+ process.kill(sess.pid, 'SIGKILL');
659
+ }
660
+ catch {
661
+ }
662
+ }
663
+ this._sessions.clear();
664
+ log(`${this.constructor.name} stopped (terminated ${sessions.length} sessions)`);
665
+ }
666
+ }
667
+ //# sourceMappingURL=base-session-manager.js.map