@teamvibe/poller 0.8.3 → 0.8.4-rc.2

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 (49) hide show
  1. package/README.md +13 -0
  2. package/dist/brain-directory.d.ts +14 -0
  3. package/dist/brain-directory.js +16 -0
  4. package/dist/brain-manager.d.ts +2 -2
  5. package/dist/brain-manager.js +32 -19
  6. package/dist/brain-path.d.ts +9 -0
  7. package/dist/brain-path.js +65 -0
  8. package/dist/claude-spawner.d.ts +3 -2
  9. package/dist/claude-spawner.js +195 -74
  10. package/dist/config.d.ts +4 -0
  11. package/dist/config.js +1 -0
  12. package/dist/event-queue.js +14 -1
  13. package/dist/inbox-manager.d.ts +11 -0
  14. package/dist/inbox-manager.js +101 -6
  15. package/dist/inbox-watcher.d.ts +9 -4
  16. package/dist/inbox-watcher.js +219 -49
  17. package/dist/maintenance-cron.d.ts +4 -2
  18. package/dist/maintenance-cron.js +56 -14
  19. package/dist/metrics/scheduler.d.ts +4 -1
  20. package/dist/metrics/scheduler.js +8 -6
  21. package/dist/poller.d.ts +2 -1
  22. package/dist/poller.js +50 -24
  23. package/dist/pool-logging.js +2 -2
  24. package/dist/pool-telemetry.d.ts +21 -5
  25. package/dist/pool-telemetry.js +28 -11
  26. package/dist/session-store.d.ts +1 -1
  27. package/dist/session-store.js +6 -2
  28. package/dist/startup-auth.js +4 -1
  29. package/dist/subscription-pool-client.js +7 -0
  30. package/dist/subscription-pool.d.ts +1 -0
  31. package/dist/subscription-pool.js +1 -1
  32. package/dist/subscription-runtime.d.ts +21 -3
  33. package/dist/subscription-runtime.js +17 -9
  34. package/dist/subscription-selection.d.ts +20 -3
  35. package/dist/subscription-selection.js +66 -4
  36. package/dist/subscription-usage-store.d.ts +2 -0
  37. package/dist/subscription-usage-store.js +41 -20
  38. package/dist/thread-routing.d.ts +23 -0
  39. package/dist/thread-routing.js +81 -0
  40. package/dist/types.d.ts +4 -10
  41. package/dist/usage-limit-messages.d.ts +1 -1
  42. package/dist/usage-limit-messages.js +1 -1
  43. package/dist/usage-probe-contract.d.ts +15 -0
  44. package/dist/usage-probe-contract.js +14 -0
  45. package/dist/usage-probe-job.js +20 -7
  46. package/dist/worktree-manager.d.ts +2 -1
  47. package/dist/worktree-manager.js +38 -10
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -74,11 +74,24 @@ All configuration is via environment variables (or `~/.teamvibe/.env`):
74
74
  |----------|----------|---------|-------------|
75
75
  | `TEAMVIBE_POLLER_TOKEN` | Yes | — | Your poller authentication token |
76
76
  | `CLAUDE_CODE_OAUTH_TOKEN` | No | — | Claude Code OAuth token (from `claude setup-token`) |
77
+ | `EXPOSE_CLAUDE_TOKEN_TO_AGENT` | No | `false` | Opt in per poller to exposing the session token to agent subprocesses; accepts `1`/`true`/`yes`/`on` (case-insensitive) |
78
+ | `TEAMVIBE_CLAUDE_OAUTH_TOKEN` | Injected | Unset | In the poller-injected env: with opt-in, the selected pool account's token, otherwise the poller's non-empty `CLAUDE_CODE_OAUTH_TOKEN`; inherited aliases are cleared. Claude settings can override this |
79
+ | `TEAMVIBE_POOL_ACCOUNT_ID` | Injected | Unset | In the poller-injected env: with opt-in and a selected pool account, its account ID (no secret); absent without a pool account. Claude settings can override this |
77
80
  | `TEAMVIBE_API_URL` | No | `https://poller.api.teamvibe.ai` | API endpoint |
78
81
  | `MAX_CONCURRENT_SESSIONS` | No | `5` | Max parallel Claude sessions |
79
82
  | `CLAUDE_CLI_PATH` | No | `claude` | Path to Claude Code CLI |
80
83
  | `TEAMVIBE_DATA_DIR` | No | `~/.teamvibe` | Data directory for brains and state |
81
84
 
85
+ Set `EXPOSE_CLAUDE_TOKEN_TO_AGENT=true` in the poller's environment to let agents run nested Claude sessions on the current session's account. Claude CLI strips `CLAUDE_CODE_OAUTH_TOKEN` from Bash subprocess environments by name, so agents can restore it from the alias:
86
+
87
+ ```bash
88
+ CLAUDE_CODE_OAUTH_TOKEN="$TEAMVIBE_CLAUDE_OAUTH_TOKEN" claude -p "…"
89
+ ```
90
+
91
+ In the environment the poller injects, the alias and account ID follow the selected account on resume and failover; without opt-in, the poller leaves both variables unset. **This guarantee applies only to the injected process environment.** Claude CLI subsequently applies settings-file `env` entries: a brain's `CLAUDE_CONFIG_DIR/settings.json` can restore either variable when the flag is off or override it with a stale value when the flag is on. Project `.claude/settings.json` and `.claude/settings.local.json` can also override them. `settings.local.json` is a project source, not a user file next to `CLAUDE_CONFIG_DIR/settings.json`; recent CLI versions also read project-local settings from the main checkout for worktree sessions ([Claude settings sources](https://code.claude.com/docs/en/settings)). Before each spawn, the poller emits one warning listing the keys and files defining either variable in these settings sources, never their values. It never edits brain settings. Remove conflicting entries yourself to preserve the injected values in the agent's environment.
92
+
93
+ **Security trade-off:** with opt-in, the agent and its subprocesses can read and use the token, bypassing Claude CLI's credential filtering. Enable this only for agents you trust with that credential. With no settings override, nested sessions use the selected session account; this option adds no nested-session usage accounting. Poller-side Claude output logs, returned output/errors, typing status and fallback Slack replies redact the actual injected Claude token and alias values, plus assignments to either token variable name, whether or not opt-in is enabled. This does not prevent agents from using credentials or sending them directly through their own tools.
94
+
82
95
  ## How It Works
83
96
 
84
97
  The poller authenticates with the TeamVibe API using your token, then continuously polls an SQS queue for incoming messages from Slack. When a message arrives, it spawns a Claude Code session with the appropriate brain (knowledge base) and streams the response back to Slack.
@@ -0,0 +1,14 @@
1
+ import type { TeamVibeQueueMessage } from './types.js';
2
+ /** Selected from platform routing; never inferred from files or replay payloads. */
3
+ export type BrainDirectory = {
4
+ path: string;
5
+ workspaceId: string;
6
+ brainId: string;
7
+ } | {
8
+ path: string;
9
+ workspaceId?: never;
10
+ brainId?: never;
11
+ };
12
+ /** Bare paths are fallback directories and carry no brain identity. */
13
+ export declare function executionDirectory(directory: BrainDirectory | string, brainsPath?: string): BrainDirectory;
14
+ export declare function selectBrainDirectory(message: TeamVibeQueueMessage): Promise<BrainDirectory>;
@@ -0,0 +1,16 @@
1
+ import { resolveBrainDir } from './brain-path.js';
2
+ import { getBrainPath } from './brain-manager.js';
3
+ /** Bare paths are fallback directories and carry no brain identity. */
4
+ export function executionDirectory(directory, brainsPath) {
5
+ if (typeof directory === 'string')
6
+ return { path: directory };
7
+ if (directory.workspaceId !== undefined || directory.brainId !== undefined) {
8
+ return { ...directory, path: resolveBrainDir(directory.workspaceId, directory.brainId, brainsPath) };
9
+ }
10
+ return directory;
11
+ }
12
+ export async function selectBrainDirectory(message) {
13
+ const { brain, workspaceId } = message.teamvibe;
14
+ const path = await getBrainPath(brain, message.response_context.slack?.channel, workspaceId);
15
+ return brain?.gitRepoUrl ? { path, workspaceId, brainId: brain.brainId } : { path };
16
+ }
@@ -1,4 +1,4 @@
1
- export declare function withBrainLock<T>(brainId: string, fn: () => Promise<T>): Promise<T>;
1
+ export declare function withBrainLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
2
2
  interface BrainConfig {
3
3
  brainId: string;
4
4
  gitRepoUrl: string;
@@ -31,7 +31,7 @@ export declare function getBrainPath(brain?: BrainConfig, channelId?: string, wo
31
31
  * Update a brain repo (respects cooldown). Exported for testing — production
32
32
  * call sites should go through `getBrainPath` / `ensureBaseBrain`.
33
33
  */
34
- export declare function updateBrain(brainDir: string, brainId: string, branch: string): Promise<void>;
34
+ export declare function updateBrain(brainDir: string, brainId: string, branch: string, workspaceId?: string): Promise<void>;
35
35
  /**
36
36
  * Ensure base brain repo is cloned and up to date.
37
37
  * Uses same cooldown pattern as channel brains.
@@ -2,10 +2,11 @@ import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import { existsSync, mkdirSync } from 'fs';
4
4
  import { readdir, stat, unlink, mkdir, writeFile } from 'fs/promises';
5
- import { join, basename } from 'path';
5
+ import { join, basename, dirname } from 'path';
6
6
  import { config } from './config.js';
7
7
  import { logger } from './logger.js';
8
8
  import { emitError } from './event-queue.js';
9
+ import { brainLockKey, resolveBrainDir, fallbackBrainPath, legacyBrainPath } from './brain-path.js';
9
10
  import { startupFailureDetail } from './startup-auth.js';
10
11
  const execAsync = promisify(exec);
11
12
  // Clean env for git commands:
@@ -30,7 +31,8 @@ function gitExec(command, options) {
30
31
  const GIT_SCAN_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
31
32
  // Cooldown tracker per brain
32
33
  const lastUpdateTimes = new Map();
33
- // Per-brainId mutex serializing updateBrain/pushBrainChanges for a given
34
+ const loggedLegacyDirectories = new Set();
35
+ // Per-workspace:brainId mutex serializing updateBrain/pushBrainChanges for a given
34
36
  // brain. Two threads of the SAME brain running concurrently in this process
35
37
  // share one brainDir (tv#76/tv#277 review) — without this, cleanupStaleLocks()
36
38
  // can unlink a git lock file that a still-in-flight sibling call's own git
@@ -40,10 +42,10 @@ const lastUpdateTimes = new Map();
40
42
  // in-process mutex fully closes the race; no DynamoDB-level lock is needed.
41
43
  const brainLocks = new Map();
42
44
  // Exported for testing — production call sites go through updateBrain / pushBrainChanges above.
43
- export function withBrainLock(brainId, fn) {
44
- const prior = brainLocks.get(brainId) ?? Promise.resolve();
45
+ export function withBrainLock(key, fn) {
46
+ const prior = brainLocks.get(key) ?? Promise.resolve();
45
47
  const run = prior.then(fn, fn);
46
- brainLocks.set(brainId, run.then(() => undefined, () => undefined));
48
+ brainLocks.set(key, run.then(() => undefined, () => undefined));
47
49
  return run;
48
50
  }
49
51
  /**
@@ -173,7 +175,7 @@ export async function ensureGitAttributes(brainDir) {
173
175
  export async function getBrainPath(brain, channelId, workspaceId) {
174
176
  if (!brain?.gitRepoUrl) {
175
177
  if (channelId) {
176
- const channelDir = join(config.BRAINS_PATH, `channel_${channelId}`);
178
+ const channelDir = fallbackBrainPath(channelId);
177
179
  if (!existsSync(channelDir)) {
178
180
  mkdirSync(channelDir, { recursive: true });
179
181
  }
@@ -181,7 +183,17 @@ export async function getBrainPath(brain, channelId, workspaceId) {
181
183
  }
182
184
  return config.DEFAULT_BRAIN_PATH;
183
185
  }
184
- const brainDir = join(config.BRAINS_PATH, brain.brainId);
186
+ const brainDir = resolveBrainDir(workspaceId, brain.brainId);
187
+ return withBrainLock(brainLockKey(brainDir, brain.brainId, workspaceId), () => getWorkspaceBrainPath(brain, workspaceId));
188
+ }
189
+ async function getWorkspaceBrainPath(brain, workspaceId) {
190
+ const brainDir = resolveBrainDir(workspaceId, brain.brainId);
191
+ const legacyDir = legacyBrainPath(brain.brainId);
192
+ if (existsSync(legacyDir) && !loggedLegacyDirectories.has(legacyDir)) {
193
+ loggedLegacyDirectories.add(legacyDir);
194
+ logger.info(`Legacy brain directory ${legacyDir} left untouched; it can be deleted once its unpushed changes are checked. A fresh workspace-scoped clone will be used.`);
195
+ }
196
+ mkdirSync(dirname(brainDir), { recursive: true });
185
197
  if (!existsSync(brainDir)) {
186
198
  logger.info(`Cloning brain ${brain.brainId} from ${brain.gitRepoUrl} (branch: ${brain.branch})...`);
187
199
  await gitExec(`git clone --recurse-submodules ${brain.gitRepoUrl} ${brainDir}`);
@@ -202,7 +214,7 @@ export async function getBrainPath(brain, channelId, workspaceId) {
202
214
  reportBrainSync(brain.brainId, workspaceId, 'pulled');
203
215
  }
204
216
  else if (config.BRAIN_AUTO_UPDATE) {
205
- await updateBrain(brainDir, brain.brainId, brain.branch);
217
+ await updateBrainLocked(brainDir, brain.brainId, brain.branch, `${workspaceId}:${brain.brainId}`);
206
218
  if (workspaceId)
207
219
  reportBrainSync(brain.brainId, workspaceId, 'pulled');
208
220
  }
@@ -215,12 +227,13 @@ export async function getBrainPath(brain, channelId, workspaceId) {
215
227
  * Update a brain repo (respects cooldown). Exported for testing — production
216
228
  * call sites should go through `getBrainPath` / `ensureBaseBrain`.
217
229
  */
218
- export async function updateBrain(brainDir, brainId, branch) {
219
- return withBrainLock(brainId, () => updateBrainLocked(brainDir, brainId, branch));
230
+ export async function updateBrain(brainDir, brainId, branch, workspaceId) {
231
+ const key = brainLockKey(brainDir, brainId, workspaceId);
232
+ return withBrainLock(key, () => updateBrainLocked(brainDir, brainId, branch, key));
220
233
  }
221
- async function updateBrainLocked(brainDir, brainId, branch) {
234
+ async function updateBrainLocked(brainDir, brainId, branch, key) {
222
235
  const now = Date.now();
223
- const lastUpdate = lastUpdateTimes.get(brainId) || 0;
236
+ const lastUpdate = lastUpdateTimes.get(key) || 0;
224
237
  if (now - lastUpdate < config.BRAIN_UPDATE_COOLDOWN_MS) {
225
238
  logger.debug(`Brain ${brainId} update skipped - cooldown active (${Math.round((config.BRAIN_UPDATE_COOLDOWN_MS - (now - lastUpdate)) / 1000)}s remaining)`);
226
239
  return;
@@ -268,7 +281,7 @@ async function updateBrainLocked(brainDir, brainId, branch) {
268
281
  await gitExec(`git fetch origin ${branch} && git reset --hard origin/${branch} && git submodule update --init --recursive`, {
269
282
  cwd: brainDir,
270
283
  });
271
- lastUpdateTimes.set(brainId, Date.now());
284
+ lastUpdateTimes.set(key, Date.now());
272
285
  logger.info(`Brain ${brainId} updated successfully`);
273
286
  }
274
287
  catch (error) {
@@ -286,7 +299,7 @@ export async function ensureBaseBrain() {
286
299
  if (!existsSync(brainDir)) {
287
300
  logger.info(`Cloning base brain (branch: ${config.BASE_BRAIN_BRANCH})...`);
288
301
  await gitExec(`git clone --recurse-submodules --branch ${config.BASE_BRAIN_BRANCH} ${config.BASE_BRAIN_REPO} ${brainDir}`);
289
- lastUpdateTimes.set('__base_brain__', Date.now());
302
+ lastUpdateTimes.set(brainDir, Date.now());
290
303
  logger.info('Base brain cloned successfully');
291
304
  }
292
305
  else if (config.BRAIN_AUTO_UPDATE) {
@@ -308,7 +321,7 @@ async function gitStashList(brainDir) {
308
321
  const { stdout } = await gitExec('git stash list', { cwd: brainDir });
309
322
  return stdout.trim();
310
323
  }
311
- // Per-brainId rescue-ref name, seeded with a timestamp the first time a given
324
+ // Per-clone rescue-ref name, seeded with a timestamp the first time a given
312
325
  // brainId hits an unresolved push-race conflict, then reused (force-pushed
313
326
  // to) on every later pushBrainChangesLocked invocation that still can't
314
327
  // converge — e.g. the next message's push attempt while the conflict remains
@@ -332,13 +345,13 @@ const rescueRefNames = new Map();
332
345
  * pre-rebase local branch) — this function pushes HEAD as-is.
333
346
  */
334
347
  async function pushRescueRef(brainDir, brainId) {
335
- let refName = rescueRefNames.get(brainId);
348
+ let refName = rescueRefNames.get(brainDir);
336
349
  if (!refName) {
337
350
  // Unix seconds (not ms) — matches the <timestamp> format documented on
338
351
  // the consuming side (poller-brain#443's MAINTENANCE.md `oldestAgeDays`
339
352
  // derivation), so both sides agree on how to parse the ref name.
340
353
  refName = `conflict/${brainId}-${Math.floor(Date.now() / 1000)}`;
341
- rescueRefNames.set(brainId, refName);
354
+ rescueRefNames.set(brainDir, refName);
342
355
  }
343
356
  try {
344
357
  await gitExec(`git push --force origin HEAD:refs/heads/${refName}`, { cwd: brainDir });
@@ -408,7 +421,7 @@ export async function findOversizedBlobs(brainDir, maxBytes) {
408
421
  * process-wide env/module-load-order tricks.
409
422
  */
410
423
  export async function pushBrainChanges(brainDir, brainId, workspaceId, retryOptions) {
411
- return withBrainLock(brainId, () => pushBrainChangesLocked(brainDir, brainId, workspaceId, retryOptions));
424
+ return withBrainLock(brainLockKey(brainDir, brainId, workspaceId), () => pushBrainChangesLocked(brainDir, brainId, workspaceId, retryOptions));
412
425
  }
413
426
  async function pushBrainChangesLocked(brainDir, brainId, workspaceId, retryOptions) {
414
427
  await cleanupStaleLocks(brainDir);
@@ -476,7 +489,7 @@ async function pushBrainChangesLocked(brainDir, brainId, workspaceId, retryOptio
476
489
  // A successful push means any earlier unresolved conflict for this
477
490
  // brainId (if any) is now resolved — a future conflict is a new,
478
491
  // unrelated incident, so stop reusing that rescue ref name.
479
- rescueRefNames.delete(brainId);
492
+ rescueRefNames.delete(brainDir);
480
493
  if (attempt > 1) {
481
494
  logger.info(`Brain ${brainId} push-race auto-resolved after ${attempt} attempts`);
482
495
  emitError('brain', `Brain ${brainId} push-race auto-resolved after ${attempt} attempts`, {
@@ -0,0 +1,9 @@
1
+ /** One policy for workspace and brain IDs, before any filesystem access. */
2
+ export declare function validateBrainId(id: string): void;
3
+ export declare function resolveBrainDir(workspaceId: string, brainId: string, brainsPath?: string): string;
4
+ /** Validate identity and the supplied clone path before update/push touches Git. */
5
+ export declare function brainLockKey(brainPath: string, brainId: string, workspaceId?: string): string;
6
+ /** Existing fallback locations have no workspace/brain identity. */
7
+ export declare function fallbackBrainPath(channelId?: string): string;
8
+ /** Legacy clones are never adopted; their inbox drops may be recovered. */
9
+ export declare function legacyBrainPath(brainId: string, brainsPath?: string): string;
@@ -0,0 +1,65 @@
1
+ import { dirname, join, resolve, sep } from 'path';
2
+ import { realpathSync } from 'fs';
3
+ import { config } from './config.js';
4
+ import { emitError } from './event-queue.js';
5
+ import { logger } from './logger.js';
6
+ /** One policy for workspace and brain IDs, before any filesystem access. */
7
+ export function validateBrainId(id) {
8
+ if (typeof id !== 'string' || !/^[A-Za-z0-9._-]+$/.test(id) || id === '.' || id === '..') {
9
+ rejectBrainDirectory('invalid workspace or brain ID');
10
+ }
11
+ }
12
+ function rejectBrainDirectory(reason) {
13
+ const message = `Invalid brain directory: ${reason}`;
14
+ logger.error(message);
15
+ emitError('brain', message);
16
+ throw new Error(message);
17
+ }
18
+ export function resolveBrainDir(workspaceId, brainId, brainsPath = config.BRAINS_PATH) {
19
+ validateBrainId(workspaceId);
20
+ validateBrainId(brainId);
21
+ const workspacePath = resolve(brainsPath, workspaceId);
22
+ const brainPath = resolve(workspacePath, brainId);
23
+ // Defense in depth: even if the ID policy changes, a clone must be strictly
24
+ // inside its workspace, never the workspace itself or a sibling directory.
25
+ if (!brainPath.startsWith(workspacePath + sep))
26
+ rejectBrainDirectory('clone escapes its workspace');
27
+ // Check each existing component, including BRAINS_PATH. Checking only the
28
+ // leaf misses aliases when a workspace or brain has not been created yet.
29
+ // This prevents routing mistakes, not races with an adversarial OS peer.
30
+ const root = resolve(brainsPath);
31
+ for (let current = brainPath;; current = dirname(current)) {
32
+ let physical;
33
+ try {
34
+ physical = realpathSync(current);
35
+ }
36
+ catch (error) {
37
+ if (error.code !== 'ENOENT')
38
+ rejectBrainDirectory('cannot resolve physical clone path');
39
+ }
40
+ if (physical !== undefined && physical !== current)
41
+ rejectBrainDirectory('physical clone path disagrees with workspace and brain IDs');
42
+ if (current === root)
43
+ break;
44
+ }
45
+ return brainPath;
46
+ }
47
+ /** Validate identity and the supplied clone path before update/push touches Git. */
48
+ export function brainLockKey(brainPath, brainId, workspaceId) {
49
+ validateBrainId(brainId);
50
+ if (workspaceId === undefined)
51
+ return brainPath; // Base brain has no workspace.
52
+ const expected = resolveBrainDir(workspaceId, brainId);
53
+ if (resolve(brainPath) !== expected)
54
+ rejectBrainDirectory('clone path disagrees with workspace and brain IDs');
55
+ return `${workspaceId}:${brainId}`;
56
+ }
57
+ /** Existing fallback locations have no workspace/brain identity. */
58
+ export function fallbackBrainPath(channelId) {
59
+ return channelId ? join(config.BRAINS_PATH, `channel_${channelId}`) : config.DEFAULT_BRAIN_PATH;
60
+ }
61
+ /** Legacy clones are never adopted; their inbox drops may be recovered. */
62
+ export function legacyBrainPath(brainId, brainsPath = config.BRAINS_PATH) {
63
+ validateBrainId(brainId);
64
+ return join(brainsPath, brainId);
65
+ }
@@ -1,6 +1,7 @@
1
1
  import { type ChildProcess } from 'child_process';
2
2
  import type { TeamVibeQueueMessage, SessionMetrics } from './types.js';
3
3
  import { type SessionLogger } from './logger.js';
4
+ import { type BrainDirectory } from './brain-directory.js';
4
5
  import { type TypingIndicator } from './slack-client.js';
5
6
  import { type ResolvedModelConfig } from './config-client.js';
6
7
  import type { KilledByPoller } from './classify-failed-iteration.js';
@@ -241,10 +242,10 @@ export declare function resolveTypingStatus(event: any, assistantText?: string):
241
242
  export declare function finalizeSpawnResult(retryResult: SpawnResult, isFirstMessage: boolean, effectiveSessionId: string | undefined): SpawnResult & {
242
243
  newSessionId?: string;
243
244
  };
244
- export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void, typingIndicator?: TypingIndicator): Promise<SpawnResult & {
245
+ export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, selectedDirectory: BrainDirectory | string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void, typingIndicator?: TypingIndicator): Promise<SpawnResult & {
245
246
  newSessionId?: string;
246
247
  }>;
247
- export declare function spawnPoolClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void, typingIndicator?: TypingIndicator, poolRuntime?: PoolRuntimeOptions): Promise<SpawnResult & {
248
+ export declare function spawnPoolClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, selectedDirectory: BrainDirectory | string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void, typingIndicator?: TypingIndicator, poolRuntime?: PoolRuntimeOptions): Promise<SpawnResult & {
248
249
  newSessionId?: string;
249
250
  }>;
250
251
  export declare function getActiveProcessCount(): number;