@teamvibe/poller 0.8.2-rc.2 → 0.8.3-rc.4
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/dist/classify-failed-iteration.d.ts +7 -0
- package/dist/classify-failed-iteration.js +12 -0
- package/dist/claude-quota.d.ts +15 -0
- package/dist/claude-quota.js +188 -0
- package/dist/claude-rate-limit.d.ts +19 -0
- package/dist/claude-rate-limit.js +44 -0
- package/dist/claude-spawner.d.ts +15 -0
- package/dist/claude-spawner.js +323 -25
- package/dist/config-client.d.ts +2 -0
- package/dist/config-client.js +3 -0
- package/dist/event-queue.js +2 -2
- package/dist/inbox-manager.d.ts +22 -1
- package/dist/inbox-manager.js +57 -3
- package/dist/inbox-watcher.d.ts +1 -1
- package/dist/inbox-watcher.js +42 -20
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +316 -54
- package/dist/pool-defer.d.ts +13 -0
- package/dist/pool-defer.js +81 -0
- package/dist/pool-logging.d.ts +18 -0
- package/dist/pool-logging.js +79 -0
- package/dist/pool-telemetry.d.ts +44 -0
- package/dist/pool-telemetry.js +107 -0
- package/dist/quota-pause.d.ts +16 -0
- package/dist/quota-pause.js +96 -0
- package/dist/session-store.d.ts +2 -2
- package/dist/session-store.js +6 -3
- package/dist/slack-client.d.ts +4 -1
- package/dist/slack-client.js +19 -1
- package/dist/subscription-pool-client.d.ts +35 -0
- package/dist/subscription-pool-client.js +143 -0
- package/dist/subscription-pool.d.ts +21 -0
- package/dist/subscription-pool.js +30 -0
- package/dist/subscription-runtime.d.ts +57 -0
- package/dist/subscription-runtime.js +126 -0
- package/dist/subscription-selection.d.ts +77 -0
- package/dist/subscription-selection.js +157 -0
- package/dist/subscription-usage-probe.d.ts +16 -0
- package/dist/subscription-usage-probe.js +106 -0
- package/dist/subscription-usage-store.d.ts +50 -0
- package/dist/subscription-usage-store.js +223 -0
- package/dist/types.d.ts +35 -10
- package/dist/usage-limit-messages.d.ts +22 -0
- package/dist/usage-limit-messages.js +44 -0
- package/dist/usage-probe-job.d.ts +17 -0
- package/dist/usage-probe-job.js +186 -0
- package/dist/worktree-manager.js +4 -0
- package/npm-shrinkwrap.json +78 -77
- package/package.json +1 -1
package/dist/inbox-manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync, readdirSync, existsSync } from 'fs';
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync, readdirSync, existsSync, rmSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
4
|
const INBOX_DIR = '.inbox';
|
|
@@ -96,7 +96,7 @@ export function markProcessed(brainPath, threadId, filename) {
|
|
|
96
96
|
/**
|
|
97
97
|
* Move a message from processing/ back to new/.
|
|
98
98
|
*
|
|
99
|
-
* Used
|
|
99
|
+
* Used for quota-paused spawn preparation and a poller-initiated *shutdown* kill (teamvibe.ai#73,
|
|
100
100
|
* PR#308 round-5): pickNextMessage() above moves a file to processing/ as
|
|
101
101
|
* soon as it's picked up, not once it completes, so a message killed
|
|
102
102
|
* mid-flight by shutdown() is left sitting in processing/. The inbox
|
|
@@ -126,7 +126,7 @@ export function requeueMessage(brainPath, threadId, filename) {
|
|
|
126
126
|
try {
|
|
127
127
|
if (existsSync(srcPath)) {
|
|
128
128
|
renameSync(srcPath, dstPath);
|
|
129
|
-
logger.info(`[Inbox] Requeued
|
|
129
|
+
logger.info(`[Inbox] Requeued pending message to new/: ${filename}`);
|
|
130
130
|
}
|
|
131
131
|
else {
|
|
132
132
|
logger.info(`[Inbox] Requeue skipped, ${filename} already gone from processing/`);
|
|
@@ -156,3 +156,57 @@ export function hasUnprocessed(brainPath, threadId) {
|
|
|
156
156
|
return false;
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
// Context and continuation state live beside inbox files, independently of the pause marker.
|
|
160
|
+
function writeInboxState(brainPath, threadId, name, value) {
|
|
161
|
+
ensureInboxDirs(brainPath, threadId);
|
|
162
|
+
const path = join(getInboxPath(brainPath, threadId), name);
|
|
163
|
+
writeFileSync(`${path}.tmp`, JSON.stringify(value), { mode: 0o600 });
|
|
164
|
+
renameSync(`${path}.tmp`, path);
|
|
165
|
+
}
|
|
166
|
+
export function saveInboxContext(brainPath, msg) {
|
|
167
|
+
const { botToken: _token, ...teamvibe } = msg.teamvibe;
|
|
168
|
+
writeInboxState(brainPath, msg.thread_id, 'context.json', { ...msg, teamvibe });
|
|
169
|
+
}
|
|
170
|
+
export function readInboxContext(brainPath, threadId) {
|
|
171
|
+
const root = getInboxPath(brainPath, threadId);
|
|
172
|
+
const candidates = [];
|
|
173
|
+
for (const subdir of [PROCESSING_DIR, NEW_DIR]) {
|
|
174
|
+
try {
|
|
175
|
+
const first = readdirSync(join(root, subdir)).filter(name => name.endsWith('.txt')).sort()[0];
|
|
176
|
+
if (first)
|
|
177
|
+
candidates.push(join(root, subdir, first));
|
|
178
|
+
}
|
|
179
|
+
catch { }
|
|
180
|
+
}
|
|
181
|
+
candidates.push(join(root, 'context.json'));
|
|
182
|
+
for (const path of candidates) {
|
|
183
|
+
try {
|
|
184
|
+
const msg = JSON.parse(readFileSync(path, 'utf8'));
|
|
185
|
+
if (msg.thread_id !== threadId || !['slack', 'email', 'cron', 'maintenance', 'webhook'].includes(msg.source) || typeof msg.teamvibe?.workspaceId !== 'string' || typeof msg.teamvibe?.channelId !== 'string' || !msg.response_context || typeof msg.text !== 'string' || !msg.sender || !Array.isArray(msg.attachments))
|
|
186
|
+
continue;
|
|
187
|
+
return { ...msg, teamvibe: { ...msg.teamvibe, botToken: '' } };
|
|
188
|
+
}
|
|
189
|
+
catch { }
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
export function saveInboxRuntime(brainPath, threadId, state) {
|
|
194
|
+
writeInboxState(brainPath, threadId, 'pool-session.json', state);
|
|
195
|
+
}
|
|
196
|
+
export function readInboxRuntime(brainPath, threadId) {
|
|
197
|
+
try {
|
|
198
|
+
const state = JSON.parse(readFileSync(join(getInboxPath(brainPath, threadId), 'pool-session.json'), 'utf8'));
|
|
199
|
+
if (typeof state.sessionId !== 'string' || typeof state.filename !== 'string' || !state.pin?.workspaceId || !state.pin?.accountId || !state.pin?.credentialVersion)
|
|
200
|
+
return undefined;
|
|
201
|
+
return state;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function clearInboxRuntime(brainPath, threadId) {
|
|
208
|
+
try {
|
|
209
|
+
rmSync(join(getInboxPath(brainPath, threadId), 'pool-session.json'), { force: true });
|
|
210
|
+
}
|
|
211
|
+
catch { }
|
|
212
|
+
}
|
package/dist/inbox-watcher.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export declare function parseSlackThreadId(threadId: string): {
|
|
|
36
36
|
export declare function listThreadDirs(inboxRoot: string): string[];
|
|
37
37
|
export declare function hasNewMessage(inboxRoot: string, threadId: string): boolean;
|
|
38
38
|
/**
|
|
39
|
-
* Scan every tracked brain's .inbox/ for a thread that has
|
|
39
|
+
* Scan every tracked brain's .inbox/ for a thread that has queued or retained work but
|
|
40
40
|
* no active runInboxLoop, and start one. This is the only config-only path
|
|
41
41
|
* that can wake a fully idle session (teamvibe.ai#242) — a file dropped into
|
|
42
42
|
* .inbox/ by e.g. a background task otherwise sits inert until the next SQS
|
package/dist/inbox-watcher.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readInboxContext, readInboxRuntime } from './inbox-manager.js';
|
|
2
|
+
import { isPoolDeferred, readPoolDefer } from './pool-defer.js';
|
|
1
3
|
import { existsSync, readdirSync, rmSync, watch as fsWatch } from 'fs';
|
|
2
4
|
import { join } from 'path';
|
|
3
5
|
import { config } from './config.js';
|
|
@@ -5,6 +7,7 @@ import { logger } from './logger.js';
|
|
|
5
7
|
import { isAtCapacity } from './claude-spawner.js';
|
|
6
8
|
import { getTrackedBrains } from './maintenance-cron.js';
|
|
7
9
|
import { runInboxLoop, isInboxLoopActive } from './poller.js';
|
|
10
|
+
import { isQuotaPaused } from './quota-pause.js';
|
|
8
11
|
const INBOX_SUBDIRS = ['new', 'processing', 'failed'];
|
|
9
12
|
let brainWatchers = new Map();
|
|
10
13
|
let fallbackInterval = null;
|
|
@@ -81,8 +84,8 @@ export function listThreadDirs(inboxRoot) {
|
|
|
81
84
|
return [];
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
const newDir = join(inboxRoot, threadId,
|
|
87
|
+
function hasMessage(inboxRoot, threadId, subdir) {
|
|
88
|
+
const newDir = join(inboxRoot, threadId, subdir);
|
|
86
89
|
try {
|
|
87
90
|
return existsSync(newDir) && readdirSync(newDir).some((f) => f.endsWith('.txt'));
|
|
88
91
|
}
|
|
@@ -90,38 +93,55 @@ export function hasNewMessage(inboxRoot, threadId) {
|
|
|
90
93
|
return false;
|
|
91
94
|
}
|
|
92
95
|
}
|
|
96
|
+
export function hasNewMessage(inboxRoot, threadId) {
|
|
97
|
+
return hasMessage(inboxRoot, threadId, 'new');
|
|
98
|
+
}
|
|
93
99
|
/**
|
|
94
|
-
* Scan every tracked brain's .inbox/ for a thread that has
|
|
100
|
+
* Scan every tracked brain's .inbox/ for a thread that has queued or retained work but
|
|
95
101
|
* no active runInboxLoop, and start one. This is the only config-only path
|
|
96
102
|
* that can wake a fully idle session (teamvibe.ai#242) — a file dropped into
|
|
97
103
|
* .inbox/ by e.g. a background task otherwise sits inert until the next SQS
|
|
98
104
|
* message for that thread, which may never come.
|
|
99
105
|
*/
|
|
100
106
|
export function checkInboxForNewSessions(deps = defaultDeps()) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
const brains = new Map();
|
|
108
|
+
for (const [, info] of deps.getTrackedBrains())
|
|
109
|
+
brains.set(join(deps.brainsPath, info.brainId), info);
|
|
110
|
+
// Durable inbox context discovers retained work even with empty bootstrap maps,
|
|
111
|
+
// absent/corrupt pause markers, or a non-Slack thread ID.
|
|
112
|
+
try {
|
|
113
|
+
for (const entry of readdirSync(deps.brainsPath, { withFileTypes: true })) {
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
const path = join(deps.brainsPath, entry.name);
|
|
116
|
+
if (existsSync(join(path, '.inbox')) && !brains.has(path))
|
|
117
|
+
brains.set(path, undefined);
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
|
-
|
|
120
|
+
}
|
|
121
|
+
catch { }
|
|
122
|
+
for (const [brainPath, info] of brains) {
|
|
123
|
+
if (deps.isAtCapacity())
|
|
124
|
+
return;
|
|
107
125
|
const inboxRoot = join(brainPath, '.inbox');
|
|
108
|
-
if (!existsSync(inboxRoot))
|
|
109
|
-
continue;
|
|
110
126
|
for (const threadId of listThreadDirs(inboxRoot)) {
|
|
111
|
-
if (deps.
|
|
127
|
+
if (deps.isAtCapacity())
|
|
128
|
+
return;
|
|
129
|
+
if (deps.isInboxLoopActive(threadId) || isPoolDeferred(threadId))
|
|
112
130
|
continue;
|
|
113
|
-
|
|
131
|
+
const completion = readInboxRuntime(brainPath, threadId)?.completed;
|
|
132
|
+
const retryCompletion = completion && (Date.now() < completion.observedAt || Date.now() >= completion.retryAt);
|
|
133
|
+
if (!hasNewMessage(inboxRoot, threadId) && !hasMessage(inboxRoot, threadId, 'processing') && !retryCompletion)
|
|
114
134
|
continue;
|
|
135
|
+
if (isQuotaPaused() && !readInboxRuntime(brainPath, threadId) && !readPoolDefer(threadId))
|
|
136
|
+
continue;
|
|
137
|
+
const durable = readInboxContext(brainPath, threadId);
|
|
115
138
|
const parsed = parseSlackThreadId(threadId);
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
const message = durable ?? (info && parsed ? buildSyntheticMessage(info, threadId, parsed) : undefined);
|
|
140
|
+
if (!message)
|
|
118
141
|
continue;
|
|
119
|
-
}
|
|
120
|
-
logger.info(`[InboxWatcher] New message for idle thread ${threadId}, starting session`);
|
|
121
142
|
const sessionLog = logger.createSession(`iw_${Math.random().toString(36).slice(2, 10)}`);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
logger.error(`[InboxWatcher] runInboxLoop failed for ${threadId}: ${error instanceof Error ? error.message : error}`);
|
|
143
|
+
deps.runInboxLoop(threadId, brainPath, message, sessionLog).catch(() => {
|
|
144
|
+
logger.error('[InboxWatcher] Retained inbox run failed; work remains saved');
|
|
125
145
|
});
|
|
126
146
|
}
|
|
127
147
|
}
|
|
@@ -157,7 +177,9 @@ export function pruneEmptyInboxDirs(deps = defaultDeps()) {
|
|
|
157
177
|
if (!existsSync(inboxRoot))
|
|
158
178
|
continue;
|
|
159
179
|
for (const threadId of listThreadDirs(inboxRoot)) {
|
|
160
|
-
if (deps.isInboxLoopActive(threadId))
|
|
180
|
+
if (deps.isInboxLoopActive(threadId) || isPoolDeferred(threadId))
|
|
181
|
+
continue;
|
|
182
|
+
if (readInboxRuntime(join(deps.brainsPath, info.brainId), threadId)?.completed)
|
|
161
183
|
continue;
|
|
162
184
|
if (!isThreadDirEmpty(inboxRoot, threadId))
|
|
163
185
|
continue;
|
package/dist/poller.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare function describeFailureReason(killedByPoller: KilledByPoller | u
|
|
|
33
33
|
export declare const END_OF_TURN_NO_FURTHER_RESPONSE_SENTINEL = "NO_FURTHER_RESPONSE_NEEDED";
|
|
34
34
|
export declare function endOfTurnFallbackText(hasSlackContext: boolean, result: Pick<SpawnResult, 'success' | 'sawSendMessageCall' | 'lastPlainTextContent'>): string | undefined;
|
|
35
35
|
export declare function runInboxLoop(threadId: string, kbPath: string, queueMessage: TeamVibeQueueMessage, sessionLog: ReturnType<typeof logger.createSession>): Promise<void>;
|
|
36
|
+
export declare function pollLoop(): Promise<void>;
|
|
36
37
|
export declare function __setSleepForTest(impl: (ms: number) => Promise<void>): void;
|
|
37
38
|
export declare function __setShuttingDownForTest(value: boolean): void;
|
|
38
39
|
export declare function startPoller(): Promise<void>;
|