@teamvibe/poller 0.6.0 → 0.7.0
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/brain-manager.d.ts +43 -1
- package/dist/brain-manager.js +259 -6
- package/dist/classify-failed-iteration.d.ts +1 -1
- package/dist/classify-failed-iteration.js +33 -1
- package/dist/claude-spawner.d.ts +178 -1
- package/dist/claude-spawner.js +690 -92
- package/dist/config.d.ts +32 -0
- package/dist/config.js +23 -2
- package/dist/cron-telemetry.d.ts +3 -0
- package/dist/cron-telemetry.js +16 -0
- package/dist/event-queue.d.ts +30 -0
- package/dist/event-queue.js +4 -0
- package/dist/inbox-manager.d.ts +25 -0
- package/dist/inbox-manager.js +47 -0
- package/dist/inbox-watcher.d.ts +67 -0
- package/dist/inbox-watcher.js +262 -0
- package/dist/maintenance-activity.d.ts +18 -0
- package/dist/maintenance-activity.js +28 -0
- package/dist/maintenance-cron.d.ts +22 -0
- package/dist/maintenance-cron.js +24 -1
- package/dist/maintenance-reporter.js +193 -88
- package/dist/metrics/collector.d.ts +2 -0
- package/dist/metrics/collector.js +7 -0
- package/dist/poller.d.ts +19 -0
- package/dist/poller.js +171 -5
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
15
15
|
LOCK_RETRY_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
|
16
16
|
IDLE_WATCHDOG_WARN_MS: z.ZodDefault<z.ZodNumber>;
|
|
17
17
|
IDLE_WATCHDOG_KILL_MS: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
IDLE_WATCHDOG_COMPACTION_GRACE_MS: z.ZodDefault<z.ZodNumber>;
|
|
18
19
|
NETWORK_ERROR_KILL_MS: z.ZodDefault<z.ZodNumber>;
|
|
19
20
|
MAX_CONSECUTIVE_NET_FAILURES: z.ZodDefault<z.ZodNumber>;
|
|
20
21
|
TEAMVIBE_DATA_DIR: z.ZodDefault<z.ZodString>;
|
|
@@ -28,11 +29,18 @@ declare const configSchema: z.ZodObject<{
|
|
|
28
29
|
BASE_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
29
30
|
BRAIN_AUTO_UPDATE: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
30
31
|
BRAIN_UPDATE_COOLDOWN_MS: z.ZodDefault<z.ZodNumber>;
|
|
32
|
+
BRAIN_MAX_BLOB_BYTES: z.ZodDefault<z.ZodNumber>;
|
|
33
|
+
BRAIN_PUSH_RETRY_MAX: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
BRAIN_PUSH_RETRY_BACKOFF_MS: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
GIT_LOCK_MAX_AGE_MS: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
GIT_SUBMODULE_LOCK_MAX_AGE_MS: z.ZodDefault<z.ZodNumber>;
|
|
31
37
|
POLLER_TZ: z.ZodOptional<z.ZodString>;
|
|
32
38
|
MAINTENANCE_CRON: z.ZodDefault<z.ZodString>;
|
|
33
39
|
MAINTENANCE_ENABLED: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
34
40
|
AUTO_UPDATE_POLICY: z.ZodDefault<z.ZodEnum<["patch", "minor", "off"]>>;
|
|
35
41
|
AUTO_UPDATE_SCHEDULE: z.ZodDefault<z.ZodString>;
|
|
42
|
+
INBOX_WATCH_FALLBACK_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
|
43
|
+
INBOX_WATCH_DEBOUNCE_MS: z.ZodDefault<z.ZodNumber>;
|
|
36
44
|
}, "strip", z.ZodTypeAny, {
|
|
37
45
|
AWS_REGION: string;
|
|
38
46
|
TEAMVIBE_API_URL: string;
|
|
@@ -45,6 +53,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
45
53
|
LOCK_RETRY_INTERVAL_MS: number;
|
|
46
54
|
IDLE_WATCHDOG_WARN_MS: number;
|
|
47
55
|
IDLE_WATCHDOG_KILL_MS: number;
|
|
56
|
+
IDLE_WATCHDOG_COMPACTION_GRACE_MS: number;
|
|
48
57
|
NETWORK_ERROR_KILL_MS: number;
|
|
49
58
|
MAX_CONSECUTIVE_NET_FAILURES: number;
|
|
50
59
|
TEAMVIBE_DATA_DIR: string;
|
|
@@ -58,10 +67,17 @@ declare const configSchema: z.ZodObject<{
|
|
|
58
67
|
BASE_BRAIN_PATH: string;
|
|
59
68
|
BRAIN_AUTO_UPDATE: boolean;
|
|
60
69
|
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
70
|
+
BRAIN_MAX_BLOB_BYTES: number;
|
|
71
|
+
BRAIN_PUSH_RETRY_MAX: number;
|
|
72
|
+
BRAIN_PUSH_RETRY_BACKOFF_MS: number;
|
|
73
|
+
GIT_LOCK_MAX_AGE_MS: number;
|
|
74
|
+
GIT_SUBMODULE_LOCK_MAX_AGE_MS: number;
|
|
61
75
|
MAINTENANCE_CRON: string;
|
|
62
76
|
MAINTENANCE_ENABLED: boolean;
|
|
63
77
|
AUTO_UPDATE_POLICY: "patch" | "minor" | "off";
|
|
64
78
|
AUTO_UPDATE_SCHEDULE: string;
|
|
79
|
+
INBOX_WATCH_FALLBACK_INTERVAL_MS: number;
|
|
80
|
+
INBOX_WATCH_DEBOUNCE_MS: number;
|
|
65
81
|
SQS_QUEUE_URL?: string | undefined;
|
|
66
82
|
SESSIONS_TABLE?: string | undefined;
|
|
67
83
|
TEAMVIBE_POLLER_TOKEN?: string | undefined;
|
|
@@ -81,6 +97,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
81
97
|
LOCK_RETRY_INTERVAL_MS?: number | undefined;
|
|
82
98
|
IDLE_WATCHDOG_WARN_MS?: number | undefined;
|
|
83
99
|
IDLE_WATCHDOG_KILL_MS?: number | undefined;
|
|
100
|
+
IDLE_WATCHDOG_COMPACTION_GRACE_MS?: number | undefined;
|
|
84
101
|
NETWORK_ERROR_KILL_MS?: number | undefined;
|
|
85
102
|
MAX_CONSECUTIVE_NET_FAILURES?: number | undefined;
|
|
86
103
|
TEAMVIBE_DATA_DIR?: string | undefined;
|
|
@@ -94,11 +111,18 @@ declare const configSchema: z.ZodObject<{
|
|
|
94
111
|
BASE_BRAIN_PATH?: string | undefined;
|
|
95
112
|
BRAIN_AUTO_UPDATE?: string | undefined;
|
|
96
113
|
BRAIN_UPDATE_COOLDOWN_MS?: number | undefined;
|
|
114
|
+
BRAIN_MAX_BLOB_BYTES?: number | undefined;
|
|
115
|
+
BRAIN_PUSH_RETRY_MAX?: number | undefined;
|
|
116
|
+
BRAIN_PUSH_RETRY_BACKOFF_MS?: number | undefined;
|
|
117
|
+
GIT_LOCK_MAX_AGE_MS?: number | undefined;
|
|
118
|
+
GIT_SUBMODULE_LOCK_MAX_AGE_MS?: number | undefined;
|
|
97
119
|
POLLER_TZ?: string | undefined;
|
|
98
120
|
MAINTENANCE_CRON?: string | undefined;
|
|
99
121
|
MAINTENANCE_ENABLED?: string | undefined;
|
|
100
122
|
AUTO_UPDATE_POLICY?: "patch" | "minor" | "off" | undefined;
|
|
101
123
|
AUTO_UPDATE_SCHEDULE?: string | undefined;
|
|
124
|
+
INBOX_WATCH_FALLBACK_INTERVAL_MS?: number | undefined;
|
|
125
|
+
INBOX_WATCH_DEBOUNCE_MS?: number | undefined;
|
|
102
126
|
}>;
|
|
103
127
|
export type Config = z.infer<typeof configSchema>;
|
|
104
128
|
export declare function loadConfig(): Config;
|
|
@@ -114,6 +138,7 @@ export declare const config: {
|
|
|
114
138
|
LOCK_RETRY_INTERVAL_MS: number;
|
|
115
139
|
IDLE_WATCHDOG_WARN_MS: number;
|
|
116
140
|
IDLE_WATCHDOG_KILL_MS: number;
|
|
141
|
+
IDLE_WATCHDOG_COMPACTION_GRACE_MS: number;
|
|
117
142
|
NETWORK_ERROR_KILL_MS: number;
|
|
118
143
|
MAX_CONSECUTIVE_NET_FAILURES: number;
|
|
119
144
|
TEAMVIBE_DATA_DIR: string;
|
|
@@ -127,10 +152,17 @@ export declare const config: {
|
|
|
127
152
|
BASE_BRAIN_PATH: string;
|
|
128
153
|
BRAIN_AUTO_UPDATE: boolean;
|
|
129
154
|
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
155
|
+
BRAIN_MAX_BLOB_BYTES: number;
|
|
156
|
+
BRAIN_PUSH_RETRY_MAX: number;
|
|
157
|
+
BRAIN_PUSH_RETRY_BACKOFF_MS: number;
|
|
158
|
+
GIT_LOCK_MAX_AGE_MS: number;
|
|
159
|
+
GIT_SUBMODULE_LOCK_MAX_AGE_MS: number;
|
|
130
160
|
MAINTENANCE_CRON: string;
|
|
131
161
|
MAINTENANCE_ENABLED: boolean;
|
|
132
162
|
AUTO_UPDATE_POLICY: "patch" | "minor" | "off";
|
|
133
163
|
AUTO_UPDATE_SCHEDULE: string;
|
|
164
|
+
INBOX_WATCH_FALLBACK_INTERVAL_MS: number;
|
|
165
|
+
INBOX_WATCH_DEBOUNCE_MS: number;
|
|
134
166
|
SQS_QUEUE_URL?: string | undefined;
|
|
135
167
|
SESSIONS_TABLE?: string | undefined;
|
|
136
168
|
TEAMVIBE_POLLER_TOKEN?: string | undefined;
|
package/dist/config.js
CHANGED
|
@@ -20,10 +20,11 @@ const configSchema = z.object({
|
|
|
20
20
|
VISIBILITY_TIMEOUT_SECONDS: z.coerce.number().default(60),
|
|
21
21
|
HEARTBEAT_INTERVAL_MS: z.coerce.number().default(40000), // 40 seconds
|
|
22
22
|
CLAUDE_TIMEOUT_MS: z.coerce.number().default(3600000), // 60 minutes
|
|
23
|
-
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(
|
|
23
|
+
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(1800000), // 30 minutes (comfortable margin above the ~15min worst-case compaction kill deadline below)
|
|
24
24
|
LOCK_RETRY_INTERVAL_MS: z.coerce.number().default(30000), // 30 seconds
|
|
25
25
|
IDLE_WATCHDOG_WARN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
26
|
-
IDLE_WATCHDOG_KILL_MS: z.coerce.number().default(600000), // 10 minutes
|
|
26
|
+
IDLE_WATCHDOG_KILL_MS: z.coerce.number().default(600000), // 10 minutes (default; on a compact_boundary event, becomes max(this-from-silence-start, event-time + IDLE_WATCHDOG_COMPACTION_GRACE_MS) — tv#32)
|
|
27
|
+
IDLE_WATCHDOG_COMPACTION_GRACE_MS: z.coerce.number().default(300000), // 5 minutes — fixed grace from compact_boundary arrival, not a large speculative ceiling (compact_boundary only proves compaction STARTED, never that it finishes in bounded time)
|
|
27
28
|
NETWORK_ERROR_KILL_MS: z.coerce.number().default(120000), // Per-session: kill stuck session after 2 min of network errors
|
|
28
29
|
MAX_CONSECUTIVE_NET_FAILURES: z.coerce.number().default(5), // Poll loop: DNS flush at N, exit(1) at 2×N consecutive failures
|
|
29
30
|
// Paths
|
|
@@ -43,6 +44,18 @@ const configSchema = z.object({
|
|
|
43
44
|
.default('true')
|
|
44
45
|
.transform((v) => v.toLowerCase() === 'true'),
|
|
45
46
|
BRAIN_UPDATE_COOLDOWN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
47
|
+
// Push guard: refuse to push unpushed commits that carry a blob over this size.
|
|
48
|
+
// Prevents a single oversized scratch file (e.g. agent .local/tmp/ writes) from
|
|
49
|
+
// getting permanently rejected by GitHub's pre-receive hook (tv#246).
|
|
50
|
+
BRAIN_MAX_BLOB_BYTES: z.coerce.number().default(50 * 1024 * 1024), // 50 MB
|
|
51
|
+
// Brain push-race retry (tv#232): fetch+rebase+retry on non-fast-forward push rejection
|
|
52
|
+
BRAIN_PUSH_RETRY_MAX: z.coerce.number().default(3),
|
|
53
|
+
BRAIN_PUSH_RETRY_BACKOFF_MS: z.coerce.number().default(500), // linear backoff: backoffMs * attempt
|
|
54
|
+
// Stale git lock cleanup (tv#76): orphaned locks from killed processes
|
|
55
|
+
// (e.g. the 2026-05-01 outage) block all future brain updates indefinitely
|
|
56
|
+
// unless removed. Distinct from STALE_LOCK_TIMEOUT_MS above (DDB session lock).
|
|
57
|
+
GIT_LOCK_MAX_AGE_MS: z.coerce.number().default(120000), // 2 minutes — .git/index.lock
|
|
58
|
+
GIT_SUBMODULE_LOCK_MAX_AGE_MS: z.coerce.number().default(300000), // 5 minutes — .git/modules/*/config.lock
|
|
46
59
|
// Brain maintenance (cron-based)
|
|
47
60
|
// POLLER_TZ: optional IANA timezone for MAINTENANCE_CRON.
|
|
48
61
|
// When unset, croner uses the host system timezone (e.g. `Europe/Prague` on
|
|
@@ -55,6 +68,14 @@ const configSchema = z.object({
|
|
|
55
68
|
// Auto-update settings
|
|
56
69
|
AUTO_UPDATE_POLICY: z.enum(['patch', 'minor', 'off']).default('patch'),
|
|
57
70
|
AUTO_UPDATE_SCHEDULE: z.string().default('03:00'), // HH:MM in UTC — daily check time
|
|
71
|
+
// Inbox watcher (teamvibe.ai#242): fs.watch()'s .inbox/ per tracked brain so a
|
|
72
|
+
// file dropped while no session is running (e.g. a background-task result)
|
|
73
|
+
// still starts one, instead of waiting inert for the next SQS message.
|
|
74
|
+
// The fallback sweep re-scans on an interval regardless of fs events — it
|
|
75
|
+
// catches missed/coalesced events, picks up brains whose .inbox/ didn't
|
|
76
|
+
// exist at watcher startup, and prunes empty thread dirs.
|
|
77
|
+
INBOX_WATCH_FALLBACK_INTERVAL_MS: z.coerce.number().default(300_000), // 5 minutes
|
|
78
|
+
INBOX_WATCH_DEBOUNCE_MS: z.coerce.number().default(300), // coalesce bursty fs events into one scan
|
|
58
79
|
});
|
|
59
80
|
export function loadConfig() {
|
|
60
81
|
const result = configSchema.safeParse(process.env);
|
package/dist/cron-telemetry.d.ts
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function markMaintenanceJobFired(at?: number): void;
|
|
14
14
|
export declare function markMaintenanceJobSuccess(at?: number): void;
|
|
15
|
+
export declare function markMaintenanceJobSkipped(at?: number): void;
|
|
15
16
|
export declare function markAutoUpdateJobFired(at?: number): void;
|
|
16
17
|
export declare function markAutoUpdateJobSuccess(at?: number): void;
|
|
17
18
|
export interface CronJobTimestamps {
|
|
18
19
|
lastMaintenanceJobFiredAt: number | null;
|
|
19
20
|
lastMaintenanceJobSuccessAt: number | null;
|
|
21
|
+
lastMaintenanceJobSkippedAt: number | null;
|
|
22
|
+
maintenanceSkippedCountLastRun: number;
|
|
20
23
|
lastAutoUpdateJobFiredAt: number | null;
|
|
21
24
|
lastAutoUpdateJobSuccessAt: number | null;
|
|
22
25
|
}
|
package/dist/cron-telemetry.js
CHANGED
|
@@ -14,12 +14,24 @@ let lastMaintenanceJobFiredAt = null;
|
|
|
14
14
|
let lastMaintenanceJobSuccessAt = null;
|
|
15
15
|
let lastAutoUpdateJobFiredAt = null;
|
|
16
16
|
let lastAutoUpdateJobSuccessAt = null;
|
|
17
|
+
// teamvibeai/teamvibe.ai#131 — activity-based skip observability. Count
|
|
18
|
+
// resets on each markMaintenanceJobFired() (i.e. once per cron run), so it
|
|
19
|
+
// reflects "brains skipped in the most recently started run", not a
|
|
20
|
+
// lifetime total. Without this, a `shouldSkipMaintenance` edge-case bug
|
|
21
|
+
// that over-skips would be invisible short of manual per-brain digging.
|
|
22
|
+
let lastMaintenanceJobSkippedAt = null;
|
|
23
|
+
let maintenanceSkippedCountLastRun = 0;
|
|
17
24
|
export function markMaintenanceJobFired(at = Date.now()) {
|
|
18
25
|
lastMaintenanceJobFiredAt = at;
|
|
26
|
+
maintenanceSkippedCountLastRun = 0;
|
|
19
27
|
}
|
|
20
28
|
export function markMaintenanceJobSuccess(at = Date.now()) {
|
|
21
29
|
lastMaintenanceJobSuccessAt = at;
|
|
22
30
|
}
|
|
31
|
+
export function markMaintenanceJobSkipped(at = Date.now()) {
|
|
32
|
+
lastMaintenanceJobSkippedAt = at;
|
|
33
|
+
maintenanceSkippedCountLastRun += 1;
|
|
34
|
+
}
|
|
23
35
|
export function markAutoUpdateJobFired(at = Date.now()) {
|
|
24
36
|
lastAutoUpdateJobFiredAt = at;
|
|
25
37
|
}
|
|
@@ -30,6 +42,8 @@ export function getCronJobTimestamps() {
|
|
|
30
42
|
return {
|
|
31
43
|
lastMaintenanceJobFiredAt,
|
|
32
44
|
lastMaintenanceJobSuccessAt,
|
|
45
|
+
lastMaintenanceJobSkippedAt,
|
|
46
|
+
maintenanceSkippedCountLastRun,
|
|
33
47
|
lastAutoUpdateJobFiredAt,
|
|
34
48
|
lastAutoUpdateJobSuccessAt,
|
|
35
49
|
};
|
|
@@ -41,6 +55,8 @@ export function getCronJobTimestamps() {
|
|
|
41
55
|
export function __resetCronTelemetryForTest() {
|
|
42
56
|
lastMaintenanceJobFiredAt = null;
|
|
43
57
|
lastMaintenanceJobSuccessAt = null;
|
|
58
|
+
lastMaintenanceJobSkippedAt = null;
|
|
59
|
+
maintenanceSkippedCountLastRun = 0;
|
|
44
60
|
lastAutoUpdateJobFiredAt = null;
|
|
45
61
|
lastAutoUpdateJobSuccessAt = null;
|
|
46
62
|
}
|
package/dist/event-queue.d.ts
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
export type ErrorCategory = 'sqs' | 'brain' | 'auth' | 'session' | 'slack' | 'maintenance';
|
|
2
2
|
export type ErrorSeverity = 'warn' | 'error';
|
|
3
|
+
interface ErrorEvent {
|
|
4
|
+
eventType: 'poller.error';
|
|
5
|
+
channelId: string;
|
|
6
|
+
metadata: {
|
|
7
|
+
category: ErrorCategory;
|
|
8
|
+
severity: ErrorSeverity;
|
|
9
|
+
message: string;
|
|
10
|
+
count: number;
|
|
11
|
+
context?: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
error: {
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
timestamp: string;
|
|
18
|
+
}
|
|
19
|
+
interface GenericEvent {
|
|
20
|
+
eventType: string;
|
|
21
|
+
channelId: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
error?: {
|
|
24
|
+
code: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
timestamp: string;
|
|
28
|
+
}
|
|
29
|
+
type QueuedEvent = ErrorEvent | GenericEvent;
|
|
3
30
|
export declare function emitEvent(eventType: string, channelId: string, metadata?: Record<string, unknown>): void;
|
|
4
31
|
export declare function flush(): Promise<void>;
|
|
5
32
|
export declare function startEventQueue(): void;
|
|
@@ -11,5 +38,8 @@ export declare function emitError(category: ErrorCategory, message: string, opti
|
|
|
11
38
|
}): void;
|
|
12
39
|
/** For testing: get current queue length */
|
|
13
40
|
export declare function getQueueLength(): number;
|
|
41
|
+
/** For testing: inspect queued events (e.g. assert on errorCode/severity) without flushing */
|
|
42
|
+
export declare function peekQueue(): QueuedEvent[];
|
|
14
43
|
/** For testing: clear the queue */
|
|
15
44
|
export declare function clearQueue(): void;
|
|
45
|
+
export {};
|
package/dist/event-queue.js
CHANGED
|
@@ -124,6 +124,10 @@ export function emitError(category, message, options) {
|
|
|
124
124
|
export function getQueueLength() {
|
|
125
125
|
return queue.length;
|
|
126
126
|
}
|
|
127
|
+
/** For testing: inspect queued events (e.g. assert on errorCode/severity) without flushing */
|
|
128
|
+
export function peekQueue() {
|
|
129
|
+
return [...queue];
|
|
130
|
+
}
|
|
127
131
|
/** For testing: clear the queue */
|
|
128
132
|
export function clearQueue() {
|
|
129
133
|
queue = [];
|
package/dist/inbox-manager.d.ts
CHANGED
|
@@ -20,6 +20,31 @@ export declare function markFailed(brainPath: string, threadId: string, filename
|
|
|
20
20
|
* Delete a message file from processing/ (marks it as done).
|
|
21
21
|
*/
|
|
22
22
|
export declare function markProcessed(brainPath: string, threadId: string, filename: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Move a message from processing/ back to new/.
|
|
25
|
+
*
|
|
26
|
+
* Used specifically for a poller-initiated *shutdown* kill (teamvibe.ai#73,
|
|
27
|
+
* PR#308 round-5): pickNextMessage() above moves a file to processing/ as
|
|
28
|
+
* soon as it's picked up, not once it completes, so a message killed
|
|
29
|
+
* mid-flight by shutdown() is left sitting in processing/. The inbox
|
|
30
|
+
* watcher's wake condition (hasNewMessage(), inbox-watcher.ts) only scans
|
|
31
|
+
* new/ — a thread whose only pending message is in processing/ has nothing
|
|
32
|
+
* to start a fresh runInboxLoop() for it on the next boot, so it could sit
|
|
33
|
+
* stuck indefinitely. Moving it back to new/ here is what makes the
|
|
34
|
+
* shutdown-kill notice text ("your message is saved and will resume
|
|
35
|
+
* automatically", classify-failed-iteration.ts) literally true.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately NOT used for timeout/idle/network/zombie kills — those retry
|
|
38
|
+
* immediately within the same still-running loop iteration (see
|
|
39
|
+
* isPollerInitiatedFailure() / the loop condition in poller.ts), so the
|
|
40
|
+
* existing processing/ crash-recovery branch in pickNextMessage() already
|
|
41
|
+
* covers them; requeueing those too would be redundant.
|
|
42
|
+
*
|
|
43
|
+
* Tolerant of the source file already being gone (e.g. a concurrent
|
|
44
|
+
* markProcessed/markFailed pass, or this being invoked twice for the same
|
|
45
|
+
* iteration) — that's a no-op, not an error.
|
|
46
|
+
*/
|
|
47
|
+
export declare function requeueMessage(brainPath: string, threadId: string, filename: string): void;
|
|
23
48
|
/**
|
|
24
49
|
* Check if there are unprocessed messages in new/ or processing/.
|
|
25
50
|
*/
|
package/dist/inbox-manager.js
CHANGED
|
@@ -93,6 +93,53 @@ export function markProcessed(brainPath, threadId, filename) {
|
|
|
93
93
|
logger.warn(`[Inbox] Failed to delete ${filename}: ${error}`);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Move a message from processing/ back to new/.
|
|
98
|
+
*
|
|
99
|
+
* Used specifically for a poller-initiated *shutdown* kill (teamvibe.ai#73,
|
|
100
|
+
* PR#308 round-5): pickNextMessage() above moves a file to processing/ as
|
|
101
|
+
* soon as it's picked up, not once it completes, so a message killed
|
|
102
|
+
* mid-flight by shutdown() is left sitting in processing/. The inbox
|
|
103
|
+
* watcher's wake condition (hasNewMessage(), inbox-watcher.ts) only scans
|
|
104
|
+
* new/ — a thread whose only pending message is in processing/ has nothing
|
|
105
|
+
* to start a fresh runInboxLoop() for it on the next boot, so it could sit
|
|
106
|
+
* stuck indefinitely. Moving it back to new/ here is what makes the
|
|
107
|
+
* shutdown-kill notice text ("your message is saved and will resume
|
|
108
|
+
* automatically", classify-failed-iteration.ts) literally true.
|
|
109
|
+
*
|
|
110
|
+
* Deliberately NOT used for timeout/idle/network/zombie kills — those retry
|
|
111
|
+
* immediately within the same still-running loop iteration (see
|
|
112
|
+
* isPollerInitiatedFailure() / the loop condition in poller.ts), so the
|
|
113
|
+
* existing processing/ crash-recovery branch in pickNextMessage() already
|
|
114
|
+
* covers them; requeueing those too would be redundant.
|
|
115
|
+
*
|
|
116
|
+
* Tolerant of the source file already being gone (e.g. a concurrent
|
|
117
|
+
* markProcessed/markFailed pass, or this being invoked twice for the same
|
|
118
|
+
* iteration) — that's a no-op, not an error.
|
|
119
|
+
*/
|
|
120
|
+
export function requeueMessage(brainPath, threadId, filename) {
|
|
121
|
+
const inbox = getInboxPath(brainPath, threadId);
|
|
122
|
+
const srcPath = join(inbox, PROCESSING_DIR, filename);
|
|
123
|
+
const newDir = join(inbox, NEW_DIR);
|
|
124
|
+
mkdirSync(newDir, { recursive: true });
|
|
125
|
+
const dstPath = join(newDir, filename);
|
|
126
|
+
try {
|
|
127
|
+
if (existsSync(srcPath)) {
|
|
128
|
+
renameSync(srcPath, dstPath);
|
|
129
|
+
logger.info(`[Inbox] Requeued shutdown-killed message to new/: ${filename}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
logger.info(`[Inbox] Requeue skipped, ${filename} already gone from processing/`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
// e.g. ENOENT from a race between the existsSync check above and the
|
|
137
|
+
// rename itself — same tolerance as the existsSync guard, just covering
|
|
138
|
+
// the narrower window. Log and continue; never let a requeue failure
|
|
139
|
+
// crash the shutdown path.
|
|
140
|
+
logger.warn(`[Inbox] Failed to requeue ${filename} to new/: ${error}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
96
143
|
/**
|
|
97
144
|
* Check if there are unprocessed messages in new/ or processing/.
|
|
98
145
|
*/
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { SessionLogger } from './logger.js';
|
|
2
|
+
import { type TrackedBrainInfo } from './maintenance-cron.js';
|
|
3
|
+
import type { TeamVibeQueueMessage } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Everything checkInboxForNewSessions/pruneEmptyInboxDirs read from other
|
|
6
|
+
* modules, gathered behind one interface so tests can pass plain fakes
|
|
7
|
+
* instead of mocking poller.js / maintenance-cron.js / claude-spawner.js
|
|
8
|
+
* (those pull in AWS clients and process-spawning at import time, and Bun's
|
|
9
|
+
* mock.module() is process-global — it would leak into every other test
|
|
10
|
+
* file in the run).
|
|
11
|
+
*/
|
|
12
|
+
export interface InboxWatcherDeps {
|
|
13
|
+
brainsPath: string;
|
|
14
|
+
getTrackedBrains: () => ReadonlyMap<string, TrackedBrainInfo>;
|
|
15
|
+
isAtCapacity: () => boolean;
|
|
16
|
+
isInboxLoopActive: (threadId: string) => boolean;
|
|
17
|
+
runInboxLoop: (threadId: string, kbPath: string, queueMessage: TeamVibeQueueMessage, sessionLog: SessionLogger) => Promise<void>;
|
|
18
|
+
rmThreadDir: (path: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* threadId is always `<botId>:<channel>:<threadTs>` for slack-originated
|
|
22
|
+
* threads (see modules-teamvibe/hooks/slack-events.ts). Synthetic threadIds
|
|
23
|
+
* used elsewhere (`maintenance_<brainId>_<ts>`, and
|
|
24
|
+
* `scheduled:<scheduleId>:<ts>` from
|
|
25
|
+
* modules-teamvibe/src/functions/scheduler.ts:86) never sit idle waiting on
|
|
26
|
+
* a background-task drop, so they're skipped rather than guessed at.
|
|
27
|
+
* `scheduled:*:*` has the same 3-colon-segment shape as a real Slack
|
|
28
|
+
* threadId, so it's rejected explicitly rather than by segment count alone
|
|
29
|
+
* — otherwise a leftover unprocessed file in a stale scheduled-session inbox
|
|
30
|
+
* dir would make this parse `scheduleId` as a Slack channel.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseSlackThreadId(threadId: string): {
|
|
33
|
+
channel: string;
|
|
34
|
+
threadTs: string;
|
|
35
|
+
} | null;
|
|
36
|
+
export declare function listThreadDirs(inboxRoot: string): string[];
|
|
37
|
+
export declare function hasNewMessage(inboxRoot: string, threadId: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Scan every tracked brain's .inbox/ for a thread that has a new message but
|
|
40
|
+
* no active runInboxLoop, and start one. This is the only config-only path
|
|
41
|
+
* that can wake a fully idle session (teamvibe.ai#242) — a file dropped into
|
|
42
|
+
* .inbox/ by e.g. a background task otherwise sits inert until the next SQS
|
|
43
|
+
* message for that thread, which may never come.
|
|
44
|
+
*/
|
|
45
|
+
export declare function checkInboxForNewSessions(deps?: InboxWatcherDeps): void;
|
|
46
|
+
export declare function isThreadDirEmpty(inboxRoot: string, threadId: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Remove thread dirs whose new/processing/failed subdirs are all empty (or
|
|
49
|
+
* absent) and which have no active loop right now. Safe because
|
|
50
|
+
* writeMessage() always recreates the directory tree via a recursive
|
|
51
|
+
* mkdirSync before writing (inbox-manager.ts) — deleting an empty thread dir
|
|
52
|
+
* loses nothing, and a later message for the same thread just recreates it.
|
|
53
|
+
* Bounds the number of directories fs.watch has to track, which would
|
|
54
|
+
* otherwise only grow: thread dirs are never pruned anywhere else once
|
|
55
|
+
* created.
|
|
56
|
+
*/
|
|
57
|
+
export declare function pruneEmptyInboxDirs(deps?: InboxWatcherDeps): void;
|
|
58
|
+
/**
|
|
59
|
+
* Starts the fs.watch-driven wake path plus a sparse fallback sweep
|
|
60
|
+
* (default 5min) that (a) catches anything a missed/coalesced fs event
|
|
61
|
+
* dropped, (b) picks up brains whose .inbox/ didn't exist at watcher
|
|
62
|
+
* startup, and (c) prunes empty thread dirs. fs.watch gives near-instant
|
|
63
|
+
* wake; the fallback exists because relying on it exclusively would be a
|
|
64
|
+
* silent single point of failure with no observable "did it fire" signal.
|
|
65
|
+
*/
|
|
66
|
+
export declare function startInboxWatcher(deps?: InboxWatcherDeps): void;
|
|
67
|
+
export declare function stopInboxWatcher(): void;
|