@teamvibe/poller 0.1.11 → 0.1.12

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.
@@ -6,6 +6,8 @@ export interface SpawnResult {
6
6
  exitCode: number | null;
7
7
  error?: string;
8
8
  }
9
- export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void): Promise<SpawnResult>;
9
+ export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void): Promise<SpawnResult & {
10
+ newSessionId?: string;
11
+ }>;
10
12
  export declare function getActiveProcessCount(): number;
11
13
  export declare function isAtCapacity(): boolean;
@@ -275,6 +275,14 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
275
275
  }
276
276
  export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
277
277
  const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs, onMessageSent);
278
+ // If session ID is "already in use" (stale lock file), retry with no session ID (let Claude generate a new one)
279
+ if (!result.success && sessionId && result.error?.includes('already in use')) {
280
+ sessionLog.info('Session ID already in use (stale lock), retrying without session ID');
281
+ const { randomUUID } = await import('crypto');
282
+ const freshId = randomUUID();
283
+ const retryResult = await runClaudeCode(msg, sessionLog, cwd, freshId, true, lastMessageTs, onMessageSent);
284
+ return { ...retryResult, newSessionId: freshId };
285
+ }
278
286
  // If --resume failed, retry as a fresh session (session files may have been lost on container restart)
279
287
  if (!result.success && sessionId && !isFirstMessage) {
280
288
  sessionLog.info('Resume failed, retrying as fresh session (session files may have been lost)');
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { logger } from './logger.js';
4
4
  import { pollMessages, deleteMessage, extendVisibility, } from './sqs-poller.js';
5
5
  import { spawnClaudeCode, isAtCapacity, getActiveProcessCount } from './claude-spawner.js';
6
6
  import { sendSlackError, addReaction, getUserInfo, startTypingIndicator } from './slack-client.js';
7
- import { acquireSessionLock, releaseSessionLock } from './session-store.js';
7
+ import { acquireSessionLock, releaseSessionLock, updateSessionId } from './session-store.js';
8
8
  import { getBrainPath, ensureDirectories, ensureBaseBrain, pushBrainChanges } from './brain-manager.js';
9
9
  import { initAuth, stopRefresh } from './auth-provider.js';
10
10
  logger.info('TeamVibe Poller starting...');
@@ -135,6 +135,10 @@ async function processMessage(received) {
135
135
  try {
136
136
  const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () => stopTyping?.());
137
137
  stopTyping?.();
138
+ // If Claude generated a new session ID (stale lock recovery), persist it
139
+ if (result.newSessionId && lockToken) {
140
+ await updateSessionId(threadId, lockToken, result.newSessionId);
141
+ }
138
142
  if (result.success) {
139
143
  sessionLog.info('Claude Code completed successfully');
140
144
  // Push any changes in the channel brain repo
@@ -7,5 +7,6 @@ export interface AcquireLockResult {
7
7
  lockToken: string | null;
8
8
  }
9
9
  export declare function acquireSessionLock(threadId: string, workspacePath: string): Promise<AcquireLockResult>;
10
+ export declare function updateSessionId(threadId: string, lockToken: string, newSessionId: string): Promise<void>;
10
11
  export declare function releaseSessionLock(threadId: string, lockToken: string, newStatus?: SessionStatus, lastMessageTs?: string): Promise<void>;
11
12
  export declare function forceReleaseStalelock(threadId: string, maxAgeMs: number): Promise<boolean>;
@@ -147,6 +147,28 @@ export async function acquireSessionLock(threadId, workspacePath) {
147
147
  throw error;
148
148
  }
149
149
  }
150
+ export async function updateSessionId(threadId, lockToken, newSessionId) {
151
+ try {
152
+ await createDocClient().send(new UpdateCommand({
153
+ TableName: getTableName(),
154
+ Key: buildKey(threadId),
155
+ UpdateExpression: 'SET session_id = :newSessionId',
156
+ ConditionExpression: 'lock_token = :lockToken',
157
+ ExpressionAttributeValues: {
158
+ ':newSessionId': newSessionId,
159
+ ':lockToken': lockToken,
160
+ },
161
+ }));
162
+ logger.info(`Updated session_id for thread ${threadId} to ${newSessionId}`);
163
+ }
164
+ catch (error) {
165
+ if (error.name === 'ConditionalCheckFailedException') {
166
+ logger.warn(`Lock token mismatch when updating session_id for ${threadId}`);
167
+ return;
168
+ }
169
+ throw error;
170
+ }
171
+ }
150
172
  export async function releaseSessionLock(threadId, lockToken, newStatus = 'idle', lastMessageTs) {
151
173
  const now = Date.now();
152
174
  const updateExpression = lastMessageTs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {