@teamvibe/poller 0.1.59 → 0.1.61

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.
@@ -271,8 +271,17 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
271
271
  args.push('--model', model);
272
272
  sessionLog.info(`Using model: ${model}`);
273
273
  }
274
- // Mailbox protocol: instruct Claude to check for mid-task messages
275
- args.push('--append-system-prompt', 'Between major steps, check for new user messages: run `ls .mailbox/new/ 2>/dev/null`. If files exist, read each (oldest first), delete it, and act on the message immediately. If empty, continue your current work. Check before starting, every 2-3 tool calls, and after finishing.');
274
+ // Inbox protocol: instruct Claude to check for mid-task messages (thread-scoped)
275
+ const inboxNewDir = `.inbox/${msg.thread_id}/new`;
276
+ args.push('--append-system-prompt', [
277
+ `CRITICAL — INBOX PROTOCOL: You MUST check for new user messages after EVERY tool call.`,
278
+ `Run: ls ${inboxNewDir}/ 2>/dev/null`,
279
+ `If files exist: read each file (oldest first), delete it, and act on the message IMMEDIATELY — it takes priority over your current task.`,
280
+ `If the message says "stop", finish your current step and stop.`,
281
+ `If empty: continue your work.`,
282
+ `You MUST check the inbox: (1) before starting work, (2) after EVERY tool call, (3) after finishing all work.`,
283
+ `Never skip an inbox check. This is how the user communicates with you during long tasks.`,
284
+ ].join('\n'));
276
285
  // Handle session continuity
277
286
  if (sessionId) {
278
287
  if (isFirstMessage) {
@@ -314,6 +323,7 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
314
323
  TEAMVIBE_CHANNEL_ID: msg.teamvibe.channelId,
315
324
  TEAMVIBE_BOT_ID: msg.teamvibe.botId,
316
325
  TEAMVIBE_POLLER_ID: getPollerId() || '',
326
+ INBOX_THREAD_ID: msg.thread_id,
317
327
  },
318
328
  stdio: ['pipe', 'pipe', 'pipe'],
319
329
  });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Write a new message to the inbox.
3
+ * Returns the filename of the created message file.
4
+ */
5
+ export declare function writeMessage(brainPath: string, threadId: string, content: string): string;
6
+ /**
7
+ * Pick the oldest message from processing/ (crash recovery) or new/.
8
+ * If found in new/, moves it to processing/.
9
+ * Returns { filename, content } or null if no messages.
10
+ */
11
+ export declare function pickNextMessage(brainPath: string, threadId: string): {
12
+ filename: string;
13
+ content: string;
14
+ } | null;
15
+ /**
16
+ * Delete a message file from processing/ (marks it as done).
17
+ */
18
+ export declare function markProcessed(brainPath: string, threadId: string, filename: string): void;
19
+ /**
20
+ * Check if there are unprocessed messages in new/ or processing/.
21
+ */
22
+ export declare function hasUnprocessed(brainPath: string, threadId: string): boolean;
@@ -0,0 +1,89 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync, readdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { logger } from './logger.js';
4
+ const INBOX_DIR = '.inbox';
5
+ const NEW_DIR = 'new';
6
+ const PROCESSING_DIR = 'processing';
7
+ function getInboxPath(brainPath, threadId) {
8
+ return join(brainPath, INBOX_DIR, threadId);
9
+ }
10
+ function ensureInboxDirs(brainPath, threadId) {
11
+ const inbox = getInboxPath(brainPath, threadId);
12
+ mkdirSync(join(inbox, NEW_DIR), { recursive: true });
13
+ mkdirSync(join(inbox, PROCESSING_DIR), { recursive: true });
14
+ }
15
+ /**
16
+ * Write a new message to the inbox.
17
+ * Returns the filename of the created message file.
18
+ */
19
+ export function writeMessage(brainPath, threadId, content) {
20
+ ensureInboxDirs(brainPath, threadId);
21
+ const timestamp = process.hrtime.bigint().toString();
22
+ const filename = `${timestamp}.txt`;
23
+ const filepath = join(getInboxPath(brainPath, threadId), NEW_DIR, filename);
24
+ writeFileSync(filepath, content, 'utf-8');
25
+ logger.info(`[Inbox] Wrote message: ${filename} (${content.length} chars)`);
26
+ return filename;
27
+ }
28
+ /**
29
+ * Pick the oldest message from processing/ (crash recovery) or new/.
30
+ * If found in new/, moves it to processing/.
31
+ * Returns { filename, content } or null if no messages.
32
+ */
33
+ export function pickNextMessage(brainPath, threadId) {
34
+ ensureInboxDirs(brainPath, threadId);
35
+ const inbox = getInboxPath(brainPath, threadId);
36
+ const processingDir = join(inbox, PROCESSING_DIR);
37
+ const newDir = join(inbox, NEW_DIR);
38
+ // First check processing/ (crash recovery)
39
+ const processingFiles = readdirSync(processingDir).filter(f => f.endsWith('.txt')).sort();
40
+ if (processingFiles.length > 0) {
41
+ const filename = processingFiles[0];
42
+ const content = readFileSync(join(processingDir, filename), 'utf-8');
43
+ logger.info(`[Inbox] Resuming processing message: ${filename}`);
44
+ return { filename, content };
45
+ }
46
+ // Then check new/
47
+ const newFiles = readdirSync(newDir).filter(f => f.endsWith('.txt')).sort();
48
+ if (newFiles.length > 0) {
49
+ const filename = newFiles[0];
50
+ const srcPath = join(newDir, filename);
51
+ const dstPath = join(processingDir, filename);
52
+ const content = readFileSync(srcPath, 'utf-8');
53
+ renameSync(srcPath, dstPath);
54
+ logger.info(`[Inbox] Picked message: ${filename} (moved to processing)`);
55
+ return { filename, content };
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Delete a message file from processing/ (marks it as done).
61
+ */
62
+ export function markProcessed(brainPath, threadId, filename) {
63
+ const filepath = join(getInboxPath(brainPath, threadId), PROCESSING_DIR, filename);
64
+ try {
65
+ if (existsSync(filepath)) {
66
+ unlinkSync(filepath);
67
+ logger.info(`[Inbox] Deleted processed message: ${filename}`);
68
+ }
69
+ }
70
+ catch (error) {
71
+ logger.warn(`[Inbox] Failed to delete ${filename}: ${error}`);
72
+ }
73
+ }
74
+ /**
75
+ * Check if there are unprocessed messages in new/ or processing/.
76
+ */
77
+ export function hasUnprocessed(brainPath, threadId) {
78
+ const inbox = getInboxPath(brainPath, threadId);
79
+ const newDir = join(inbox, NEW_DIR);
80
+ const processingDir = join(inbox, PROCESSING_DIR);
81
+ try {
82
+ const newCount = existsSync(newDir) ? readdirSync(newDir).filter(f => f.endsWith('.txt')).length : 0;
83
+ const procCount = existsSync(processingDir) ? readdirSync(processingDir).filter(f => f.endsWith('.txt')).length : 0;
84
+ return (newCount + procCount) > 0;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
@@ -2,21 +2,25 @@
2
2
  * Write a new message to the mailbox.
3
3
  * Returns the filename of the created message file.
4
4
  */
5
- export declare function writeMessage(brainPath: string, content: string): string;
5
+ export declare function writeMessage(brainPath: string, threadId: string, content: string): string;
6
6
  /**
7
7
  * Pick the oldest message from processing/ (crash recovery) or new/.
8
8
  * If found in new/, moves it to processing/.
9
9
  * Returns { filename, content } or null if no messages.
10
10
  */
11
- export declare function pickNextMessage(brainPath: string): {
11
+ export declare function pickNextMessage(brainPath: string, threadId: string): {
12
12
  filename: string;
13
13
  content: string;
14
14
  } | null;
15
15
  /**
16
16
  * Delete a message file from processing/ (marks it as done).
17
17
  */
18
- export declare function markProcessed(brainPath: string, filename: string): void;
18
+ export declare function markProcessed(brainPath: string, threadId: string, filename: string): void;
19
19
  /**
20
20
  * Check if there are unprocessed messages in new/ or processing/.
21
21
  */
22
- export declare function hasUnprocessed(brainPath: string): boolean;
22
+ export declare function hasUnprocessed(brainPath: string, threadId: string): boolean;
23
+ /**
24
+ * Get the mailbox new/ directory path for use in Claude's system prompt.
25
+ */
26
+ export declare function getMailboxNewPath(brainPath: string, threadId: string): string;
@@ -4,24 +4,25 @@ import { logger } from './logger.js';
4
4
  const MAILBOX_DIR = '.mailbox';
5
5
  const NEW_DIR = 'new';
6
6
  const PROCESSING_DIR = 'processing';
7
- function ensureMailboxDirs(brainPath) {
8
- const mailbox = join(brainPath, MAILBOX_DIR);
9
- const newDir = join(mailbox, NEW_DIR);
10
- const processingDir = join(mailbox, PROCESSING_DIR);
11
- mkdirSync(newDir, { recursive: true });
12
- mkdirSync(processingDir, { recursive: true });
7
+ function getMailboxPath(brainPath, threadId) {
8
+ return join(brainPath, MAILBOX_DIR, threadId);
9
+ }
10
+ function ensureMailboxDirs(brainPath, threadId) {
11
+ const mailbox = getMailboxPath(brainPath, threadId);
12
+ mkdirSync(join(mailbox, NEW_DIR), { recursive: true });
13
+ mkdirSync(join(mailbox, PROCESSING_DIR), { recursive: true });
13
14
  }
14
15
  /**
15
16
  * Write a new message to the mailbox.
16
17
  * Returns the filename of the created message file.
17
18
  */
18
- export function writeMessage(brainPath, content) {
19
- ensureMailboxDirs(brainPath);
19
+ export function writeMessage(brainPath, threadId, content) {
20
+ ensureMailboxDirs(brainPath, threadId);
20
21
  const timestamp = process.hrtime.bigint().toString();
21
22
  const filename = `${timestamp}.txt`;
22
- const filepath = join(brainPath, MAILBOX_DIR, NEW_DIR, filename);
23
+ const filepath = join(getMailboxPath(brainPath, threadId), NEW_DIR, filename);
23
24
  writeFileSync(filepath, content, 'utf-8');
24
- logger.info(`[Mailbox] Wrote message: ${filename} (${content.length} chars)`);
25
+ logger.info(`[Mailbox] Wrote message: ${filename} to ${getMailboxPath(brainPath, threadId)}/new/ (${content.length} chars, threadId=${threadId})`);
25
26
  return filename;
26
27
  }
27
28
  /**
@@ -29,10 +30,11 @@ export function writeMessage(brainPath, content) {
29
30
  * If found in new/, moves it to processing/.
30
31
  * Returns { filename, content } or null if no messages.
31
32
  */
32
- export function pickNextMessage(brainPath) {
33
- ensureMailboxDirs(brainPath);
34
- const processingDir = join(brainPath, MAILBOX_DIR, PROCESSING_DIR);
35
- const newDir = join(brainPath, MAILBOX_DIR, NEW_DIR);
33
+ export function pickNextMessage(brainPath, threadId) {
34
+ ensureMailboxDirs(brainPath, threadId);
35
+ const mailbox = getMailboxPath(brainPath, threadId);
36
+ const processingDir = join(mailbox, PROCESSING_DIR);
37
+ const newDir = join(mailbox, NEW_DIR);
36
38
  // First check processing/ (crash recovery)
37
39
  const processingFiles = readdirSync(processingDir).filter(f => f.endsWith('.txt')).sort();
38
40
  if (processingFiles.length > 0) {
@@ -57,8 +59,8 @@ export function pickNextMessage(brainPath) {
57
59
  /**
58
60
  * Delete a message file from processing/ (marks it as done).
59
61
  */
60
- export function markProcessed(brainPath, filename) {
61
- const filepath = join(brainPath, MAILBOX_DIR, PROCESSING_DIR, filename);
62
+ export function markProcessed(brainPath, threadId, filename) {
63
+ const filepath = join(getMailboxPath(brainPath, threadId), PROCESSING_DIR, filename);
62
64
  try {
63
65
  if (existsSync(filepath)) {
64
66
  unlinkSync(filepath);
@@ -72,9 +74,10 @@ export function markProcessed(brainPath, filename) {
72
74
  /**
73
75
  * Check if there are unprocessed messages in new/ or processing/.
74
76
  */
75
- export function hasUnprocessed(brainPath) {
76
- const newDir = join(brainPath, MAILBOX_DIR, NEW_DIR);
77
- const processingDir = join(brainPath, MAILBOX_DIR, PROCESSING_DIR);
77
+ export function hasUnprocessed(brainPath, threadId) {
78
+ const mailbox = getMailboxPath(brainPath, threadId);
79
+ const newDir = join(mailbox, NEW_DIR);
80
+ const processingDir = join(mailbox, PROCESSING_DIR);
78
81
  try {
79
82
  const newCount = existsSync(newDir) ? readdirSync(newDir).filter(f => f.endsWith('.txt')).length : 0;
80
83
  const procCount = existsSync(processingDir) ? readdirSync(processingDir).filter(f => f.endsWith('.txt')).length : 0;
@@ -84,3 +87,9 @@ export function hasUnprocessed(brainPath) {
84
87
  return false;
85
88
  }
86
89
  }
90
+ /**
91
+ * Get the mailbox new/ directory path for use in Claude's system prompt.
92
+ */
93
+ export function getMailboxNewPath(brainPath, threadId) {
94
+ return join(getMailboxPath(brainPath, threadId), NEW_DIR);
95
+ }
package/dist/poller.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { config } from './config.js';
2
2
  import { logger } from './logger.js';
3
3
  import { pollMessages, deleteMessage, } from './sqs-poller.js';
4
- import { spawnClaudeCode, isAtCapacity, getActiveProcessCount, killAllActiveProcesses, buildPrompt } from './claude-spawner.js';
4
+ import { spawnClaudeCode, isAtCapacity, getActiveProcessCount, killAllActiveProcesses } from './claude-spawner.js';
5
5
  import { sendSlackError, addReaction, getUserInfo, startTypingIndicator } from './slack-client.js';
6
6
  import { acquireSessionLock, releaseSessionLock, updateSessionId } from './session-store.js';
7
7
  import { getBrainPath, ensureDirectories, ensureBaseBrain, pushBrainChanges } from './brain-manager.js';
@@ -9,12 +9,12 @@ import { postMaintenanceReports } from './maintenance-reporter.js';
9
9
  import { initAuth, stopRefresh } from './auth-provider.js';
10
10
  import { registerBrain, startHeartbeatLoop, stopHeartbeatLoop } from './heartbeat-manager.js';
11
11
  import { startEventQueue, stopEventQueue, flush as flushEventQueue } from './event-queue.js';
12
- import { writeMessage, pickNextMessage, markProcessed, hasUnprocessed } from './mailbox-manager.js';
12
+ import { writeMessage, pickNextMessage, markProcessed, hasUnprocessed } from './inbox-manager.js';
13
13
  // Track active message processing
14
14
  const processingMessages = new Set();
15
15
  // Track which threads have an active processing loop
16
16
  const activeLoops = new Set();
17
- async function runMailboxLoop(threadId, kbPath, queueMessage, sessionLog) {
17
+ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
18
18
  if (activeLoops.has(threadId)) {
19
19
  sessionLog.info(`Loop already running for thread ${threadId}, skipping`);
20
20
  return;
@@ -24,12 +24,12 @@ async function runMailboxLoop(threadId, kbPath, queueMessage, sessionLog) {
24
24
  try {
25
25
  let lockResult = await acquireSessionLock(threadId, kbPath);
26
26
  if (!lockResult.success) {
27
- sessionLog.info(`Session ${threadId} is locked, mailbox message will be picked up by active session`);
27
+ sessionLog.info(`Session ${threadId} is locked, inbox message will be picked up by active session`);
28
28
  return;
29
29
  }
30
30
  const { session, lockToken } = lockResult;
31
31
  while (true) {
32
- const message = pickNextMessage(kbPath);
32
+ const message = pickNextMessage(kbPath, threadId);
33
33
  if (!message) {
34
34
  sessionLog.info(`[Loop] No more messages for thread ${threadId}`);
35
35
  break;
@@ -44,12 +44,13 @@ async function runMailboxLoop(threadId, kbPath, queueMessage, sessionLog) {
44
44
  ? startTypingIndicator(queueMessage)
45
45
  : undefined;
46
46
  try {
47
- // Build a temporary message with the mailbox content as text
48
- const mailboxMsg = {
47
+ // Reconstruct queue message from stored inbox data
48
+ const storedData = JSON.parse(message.content);
49
+ const inboxMsg = {
49
50
  ...queueMessage,
50
- text: message.content,
51
+ ...storedData,
51
52
  };
52
- const result = await spawnClaudeCode(mailboxMsg, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () => typing?.stop(), typing);
53
+ const result = await spawnClaudeCode(inboxMsg, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () => typing?.stop(), typing);
53
54
  typing?.stop();
54
55
  if (result.newSessionId && lockToken) {
55
56
  await updateSessionId(threadId, lockToken, result.newSessionId);
@@ -81,8 +82,8 @@ async function runMailboxLoop(threadId, kbPath, queueMessage, sessionLog) {
81
82
  sessionLog.error(`[Loop] Error in iteration #${iteration}: ${error instanceof Error ? error.message : error}`);
82
83
  }
83
84
  // Safety net: ensure the processing file is cleaned up
84
- markProcessed(kbPath, message.filename);
85
- if (!hasUnprocessed(kbPath)) {
85
+ markProcessed(kbPath, threadId, message.filename);
86
+ if (!hasUnprocessed(kbPath, threadId)) {
86
87
  break;
87
88
  }
88
89
  sessionLog.info(`[Loop] More messages found, continuing...`);
@@ -141,21 +142,29 @@ async function processMessage(received) {
141
142
  }
142
143
  // Register brain for heartbeat tracking
143
144
  registerBrain(queueMessage);
144
- // Build prompt and write to mailbox
145
- const prompt = buildPrompt(queueMessage);
146
- writeMessage(kbPath, prompt);
147
- sessionLog.info(`Wrote message to mailbox (${prompt.length} chars)`);
148
- // Delete SQS message immediately — it's now persisted in the mailbox
145
+ // Write raw message to inbox (spawnClaudeCode handles prompt formatting)
146
+ writeMessage(kbPath, threadId, JSON.stringify({
147
+ text: queueMessage.text,
148
+ sender: queueMessage.sender,
149
+ type: queueMessage.type,
150
+ source: queueMessage.source,
151
+ attachments: queueMessage.attachments,
152
+ approval: queueMessage.approval,
153
+ button: queueMessage.button,
154
+ modal: queueMessage.modal,
155
+ }));
156
+ sessionLog.info(`Wrote message to inbox`);
157
+ // Delete SQS message immediately — it's now persisted in the inbox
149
158
  if (receiptHandle) {
150
159
  await deleteMessage(receiptHandle);
151
- sessionLog.info('SQS message deleted (persisted in mailbox)');
160
+ sessionLog.info('SQS message deleted (persisted in inbox)');
152
161
  }
153
162
  processingMessages.add(messageId);
154
163
  try {
155
- await runMailboxLoop(threadId, kbPath, queueMessage, sessionLog);
164
+ await runInboxLoop(threadId, kbPath, queueMessage, sessionLog);
156
165
  }
157
166
  catch (error) {
158
- sessionLog.error(`Error in mailbox loop: ${error instanceof Error ? error.message : error}`);
167
+ sessionLog.error(`Error in inbox loop: ${error instanceof Error ? error.message : error}`);
159
168
  if (hasSlackContext) {
160
169
  try {
161
170
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {