@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/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(900000), // 15 minutes (above idle watchdog kill time of 10 min)
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);
@@ -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
  }
@@ -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
  }
@@ -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 {};
@@ -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 = [];
@@ -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
  */
@@ -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;