@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.
- package/README.md +13 -0
- package/dist/brain-directory.d.ts +14 -0
- package/dist/brain-directory.js +16 -0
- package/dist/brain-manager.d.ts +2 -2
- package/dist/brain-manager.js +32 -19
- package/dist/brain-path.d.ts +9 -0
- package/dist/brain-path.js +65 -0
- package/dist/claude-spawner.d.ts +3 -2
- package/dist/claude-spawner.js +195 -74
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/event-queue.js +14 -1
- package/dist/inbox-manager.d.ts +11 -0
- package/dist/inbox-manager.js +101 -6
- package/dist/inbox-watcher.d.ts +9 -4
- package/dist/inbox-watcher.js +219 -49
- package/dist/maintenance-cron.d.ts +4 -2
- package/dist/maintenance-cron.js +56 -14
- package/dist/metrics/scheduler.d.ts +4 -1
- package/dist/metrics/scheduler.js +8 -6
- package/dist/poller.d.ts +2 -1
- package/dist/poller.js +50 -24
- package/dist/pool-logging.js +2 -2
- package/dist/pool-telemetry.d.ts +21 -5
- package/dist/pool-telemetry.js +28 -11
- package/dist/session-store.d.ts +1 -1
- package/dist/session-store.js +6 -2
- package/dist/startup-auth.js +4 -1
- package/dist/subscription-pool-client.js +7 -0
- package/dist/subscription-pool.d.ts +1 -0
- package/dist/subscription-pool.js +1 -1
- package/dist/subscription-runtime.d.ts +21 -3
- package/dist/subscription-runtime.js +17 -9
- package/dist/subscription-selection.d.ts +20 -3
- package/dist/subscription-selection.js +66 -4
- package/dist/subscription-usage-store.d.ts +2 -0
- package/dist/subscription-usage-store.js +41 -20
- package/dist/thread-routing.d.ts +23 -0
- package/dist/thread-routing.js +81 -0
- package/dist/types.d.ts +4 -10
- package/dist/usage-limit-messages.d.ts +1 -1
- package/dist/usage-limit-messages.js +1 -1
- package/dist/usage-probe-contract.d.ts +15 -0
- package/dist/usage-probe-contract.js +14 -0
- package/dist/usage-probe-job.js +20 -7
- package/dist/worktree-manager.d.ts +2 -1
- package/dist/worktree-manager.js +38 -10
- package/npm-shrinkwrap.json +2 -2
- 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
|
+
}
|
package/dist/brain-manager.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare function withBrainLock<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.
|
package/dist/brain-manager.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
44
|
-
const prior = brainLocks.get(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/dist/claude-spawner.d.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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;
|