@teamvibe/poller 0.6.2 → 0.7.1
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/brains-bootstrap.d.ts +45 -0
- package/dist/brains-bootstrap.js +217 -0
- package/dist/classify-failed-iteration.d.ts +1 -1
- package/dist/classify-failed-iteration.js +33 -1
- package/dist/claude-spawner.d.ts +179 -1
- package/dist/claude-spawner.js +697 -92
- package/dist/config.d.ts +40 -0
- package/dist/config.js +32 -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/idle-restart.d.ts +12 -0
- package/dist/idle-restart.js +200 -0
- package/dist/inbox-manager.d.ts +25 -0
- package/dist/inbox-manager.js +47 -0
- package/dist/maintenance-activity.d.ts +18 -0
- package/dist/maintenance-activity.js +28 -0
- package/dist/maintenance-cron.d.ts +73 -0
- package/dist/maintenance-cron.js +85 -1
- package/dist/metrics/collector.d.ts +2 -0
- package/dist/metrics/collector.js +7 -0
- package/dist/poller.d.ts +12 -0
- package/dist/poller.js +345 -8
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ export interface TrackedBrainInfo {
|
|
|
7
7
|
botId: string;
|
|
8
8
|
botToken: string;
|
|
9
9
|
channelId: string;
|
|
10
|
+
lastRealMessageAt?: number;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Read-only view of brains seen so far (via registerBrain). Used by the inbox
|
|
@@ -14,6 +15,78 @@ export interface TrackedBrainInfo {
|
|
|
14
15
|
* workspace/bot credentials to use when starting a session for an idle thread.
|
|
15
16
|
*/
|
|
16
17
|
export declare function getTrackedBrains(): ReadonlyMap<string, TrackedBrainInfo>;
|
|
18
|
+
/**
|
|
19
|
+
* Test-only helper to clear tracked brains between tests (module-scoped
|
|
20
|
+
* state persists across tests in the same file otherwise).
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function __resetTrackedBrainsForTest(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Shape returned by GET /poller/brains (teamvibe.ai#127 Option B) — the
|
|
26
|
+
* fields registerBrain() needs to build a synthetic maintenance message,
|
|
27
|
+
* minus `lastRealMessageAt` (see seedTrackedBrains below for why that's
|
|
28
|
+
* deliberately not part of the bootstrap payload).
|
|
29
|
+
*/
|
|
30
|
+
export interface BootstrapBrainInfo {
|
|
31
|
+
brainId: string;
|
|
32
|
+
gitRepoUrl: string;
|
|
33
|
+
branch: string;
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
botId: string;
|
|
36
|
+
botToken: string;
|
|
37
|
+
channelId: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Seed trackedBrains at startup from the backend's authoritative brain list
|
|
41
|
+
* (teamvibe.ai#127) so a "quiet" brain — one that hasn't received a real
|
|
42
|
+
* message since this poller process started — still gets included in the
|
|
43
|
+
* maintenance/heartbeat cron, instead of being silently invisible until a
|
|
44
|
+
* human happens to message it.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately does NOT set `lastRealMessageAt` (left undefined). That
|
|
47
|
+
* field specifically means "we've seen a real message since our own last
|
|
48
|
+
* restart" — shouldSkipMaintenance (maintenance-activity.ts, #131) treats
|
|
49
|
+
* `undefined` as fail-open (never skip) for exactly this reason. Bootstrap
|
|
50
|
+
* seeding isn't a real message; claiming one occurred here would risk the
|
|
51
|
+
* opposite bug — a genuinely-quiet brain wrongly evaluated as "nothing new,
|
|
52
|
+
* skip" on its very first post-restart cron tick.
|
|
53
|
+
*
|
|
54
|
+
* Skips (rather than overwrites) any brainId already present in
|
|
55
|
+
* trackedBrains. This guard is load-bearing, not just defensive (DevGuru
|
|
56
|
+
* review, round 4): since round 3's critical-path detachment
|
|
57
|
+
* (brains-bootstrap.ts's `void (async () => {...})()` retry), the retry
|
|
58
|
+
* branch can finish AFTER startPoller() has already returned and real user
|
|
59
|
+
* messages have started arriving. That means a real registerBrain() call
|
|
60
|
+
* for a brain (which sets `lastRealMessageAt`) can race ahead of — and
|
|
61
|
+
* complete before — the detached bootstrap retry's seedTrackedBrains()
|
|
62
|
+
* call for that same brain. Without this skip, the late-arriving bootstrap
|
|
63
|
+
* seed would silently overwrite the real registration back to a fresh
|
|
64
|
+
* entry with `lastRealMessageAt: undefined`, losing real activity data
|
|
65
|
+
* (and, per the fail-open contract above, artificially reopening the
|
|
66
|
+
* skip-maintenance window for a brain that's actually been active).
|
|
67
|
+
*
|
|
68
|
+
* DOES also register each seeded brain for hourly metrics (see
|
|
69
|
+
* `registerBrainForMetrics` below) — DevGuru review caught that this
|
|
70
|
+
* module's original "metrics has its own disk-walk path" reasoning was
|
|
71
|
+
* wrong: `metrics/scheduler.ts`'s `registered` Map has exactly one
|
|
72
|
+
* production caller (`registerBrain()` below), with no disk-walk discovery
|
|
73
|
+
* of its own. Disk reads only happen in `collector.ts`, and only for a
|
|
74
|
+
* brain already present in that Map. Skipping the metrics seed here would
|
|
75
|
+
* reproduce the exact same restart-amnesia bug for metrics that this whole
|
|
76
|
+
* change exists to fix for maintenance: a quiet brain would get maintenance
|
|
77
|
+
* triggers again, but zero metrics until someone messages it.
|
|
78
|
+
*
|
|
79
|
+
* Deliberately does NOT replicate `registerBrain()`'s *immediate*
|
|
80
|
+
* collect+send side effect (the `if (isNew) { void (async () => ... })()`
|
|
81
|
+
* block below) — only the Map insert. Firing an immediate collect+send for
|
|
82
|
+
* every brain this poller owns, all at once, on every restart would be a
|
|
83
|
+
* fleet-wide burst; the existing hourly cron with its per-brain jitter
|
|
84
|
+
* (`collectAndSendAll`, `jitterMsFor`) already picks up newly-registered
|
|
85
|
+
* brains on its own schedule without a startup spike.
|
|
86
|
+
*
|
|
87
|
+
* Returns the number of brains actually seeded (for startup logging).
|
|
88
|
+
*/
|
|
89
|
+
export declare function seedTrackedBrains(brains: readonly BootstrapBrainInfo[]): number;
|
|
17
90
|
/**
|
|
18
91
|
* Register a brain from a real message so the maintenance cron knows about it.
|
|
19
92
|
* Called from processMessage() after brain path is resolved.
|
package/dist/maintenance-cron.js
CHANGED
|
@@ -6,7 +6,8 @@ import { emitError } from './event-queue.js';
|
|
|
6
6
|
import { registerBrainForMetrics } from './metrics/scheduler.js';
|
|
7
7
|
import { collectMetrics } from './metrics/collector.js';
|
|
8
8
|
import { sendMetrics } from './metrics/sender.js';
|
|
9
|
-
import { markMaintenanceJobFired, markMaintenanceJobSuccess, } from './cron-telemetry.js';
|
|
9
|
+
import { markMaintenanceJobFired, markMaintenanceJobSuccess, markMaintenanceJobSkipped, } from './cron-telemetry.js';
|
|
10
|
+
import { shouldSkipMaintenance } from './maintenance-activity.js';
|
|
10
11
|
import { join } from 'path';
|
|
11
12
|
const trackedBrains = new Map();
|
|
12
13
|
/**
|
|
@@ -17,7 +18,84 @@ const trackedBrains = new Map();
|
|
|
17
18
|
export function getTrackedBrains() {
|
|
18
19
|
return trackedBrains;
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Test-only helper to clear tracked brains between tests (module-scoped
|
|
23
|
+
* state persists across tests in the same file otherwise).
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export function __resetTrackedBrainsForTest() {
|
|
27
|
+
trackedBrains.clear();
|
|
28
|
+
}
|
|
20
29
|
let maintenanceJob = null;
|
|
30
|
+
/**
|
|
31
|
+
* Seed trackedBrains at startup from the backend's authoritative brain list
|
|
32
|
+
* (teamvibe.ai#127) so a "quiet" brain — one that hasn't received a real
|
|
33
|
+
* message since this poller process started — still gets included in the
|
|
34
|
+
* maintenance/heartbeat cron, instead of being silently invisible until a
|
|
35
|
+
* human happens to message it.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately does NOT set `lastRealMessageAt` (left undefined). That
|
|
38
|
+
* field specifically means "we've seen a real message since our own last
|
|
39
|
+
* restart" — shouldSkipMaintenance (maintenance-activity.ts, #131) treats
|
|
40
|
+
* `undefined` as fail-open (never skip) for exactly this reason. Bootstrap
|
|
41
|
+
* seeding isn't a real message; claiming one occurred here would risk the
|
|
42
|
+
* opposite bug — a genuinely-quiet brain wrongly evaluated as "nothing new,
|
|
43
|
+
* skip" on its very first post-restart cron tick.
|
|
44
|
+
*
|
|
45
|
+
* Skips (rather than overwrites) any brainId already present in
|
|
46
|
+
* trackedBrains. This guard is load-bearing, not just defensive (DevGuru
|
|
47
|
+
* review, round 4): since round 3's critical-path detachment
|
|
48
|
+
* (brains-bootstrap.ts's `void (async () => {...})()` retry), the retry
|
|
49
|
+
* branch can finish AFTER startPoller() has already returned and real user
|
|
50
|
+
* messages have started arriving. That means a real registerBrain() call
|
|
51
|
+
* for a brain (which sets `lastRealMessageAt`) can race ahead of — and
|
|
52
|
+
* complete before — the detached bootstrap retry's seedTrackedBrains()
|
|
53
|
+
* call for that same brain. Without this skip, the late-arriving bootstrap
|
|
54
|
+
* seed would silently overwrite the real registration back to a fresh
|
|
55
|
+
* entry with `lastRealMessageAt: undefined`, losing real activity data
|
|
56
|
+
* (and, per the fail-open contract above, artificially reopening the
|
|
57
|
+
* skip-maintenance window for a brain that's actually been active).
|
|
58
|
+
*
|
|
59
|
+
* DOES also register each seeded brain for hourly metrics (see
|
|
60
|
+
* `registerBrainForMetrics` below) — DevGuru review caught that this
|
|
61
|
+
* module's original "metrics has its own disk-walk path" reasoning was
|
|
62
|
+
* wrong: `metrics/scheduler.ts`'s `registered` Map has exactly one
|
|
63
|
+
* production caller (`registerBrain()` below), with no disk-walk discovery
|
|
64
|
+
* of its own. Disk reads only happen in `collector.ts`, and only for a
|
|
65
|
+
* brain already present in that Map. Skipping the metrics seed here would
|
|
66
|
+
* reproduce the exact same restart-amnesia bug for metrics that this whole
|
|
67
|
+
* change exists to fix for maintenance: a quiet brain would get maintenance
|
|
68
|
+
* triggers again, but zero metrics until someone messages it.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately does NOT replicate `registerBrain()`'s *immediate*
|
|
71
|
+
* collect+send side effect (the `if (isNew) { void (async () => ... })()`
|
|
72
|
+
* block below) — only the Map insert. Firing an immediate collect+send for
|
|
73
|
+
* every brain this poller owns, all at once, on every restart would be a
|
|
74
|
+
* fleet-wide burst; the existing hourly cron with its per-brain jitter
|
|
75
|
+
* (`collectAndSendAll`, `jitterMsFor`) already picks up newly-registered
|
|
76
|
+
* brains on its own schedule without a startup spike.
|
|
77
|
+
*
|
|
78
|
+
* Returns the number of brains actually seeded (for startup logging).
|
|
79
|
+
*/
|
|
80
|
+
export function seedTrackedBrains(brains) {
|
|
81
|
+
let seeded = 0;
|
|
82
|
+
for (const b of brains) {
|
|
83
|
+
if (trackedBrains.has(b.brainId))
|
|
84
|
+
continue;
|
|
85
|
+
trackedBrains.set(b.brainId, {
|
|
86
|
+
brainId: b.brainId,
|
|
87
|
+
gitRepoUrl: b.gitRepoUrl,
|
|
88
|
+
branch: b.branch,
|
|
89
|
+
workspaceId: b.workspaceId,
|
|
90
|
+
botId: b.botId,
|
|
91
|
+
botToken: b.botToken,
|
|
92
|
+
channelId: b.channelId,
|
|
93
|
+
});
|
|
94
|
+
registerBrainForMetrics(b.brainId);
|
|
95
|
+
seeded++;
|
|
96
|
+
}
|
|
97
|
+
return seeded;
|
|
98
|
+
}
|
|
21
99
|
/**
|
|
22
100
|
* Register a brain from a real message so the maintenance cron knows about it.
|
|
23
101
|
* Called from processMessage() after brain path is resolved.
|
|
@@ -38,6 +116,7 @@ export function registerBrain(msg) {
|
|
|
38
116
|
botId: msg.teamvibe.botId,
|
|
39
117
|
botToken: msg.teamvibe.botToken,
|
|
40
118
|
channelId: msg.teamvibe.channelId,
|
|
119
|
+
lastRealMessageAt: Date.now(),
|
|
41
120
|
});
|
|
42
121
|
logger.debug(`[Maintenance] Registered brain: ${brain.brainId}`);
|
|
43
122
|
// Metrics pipeline (teamvibe.ai#110): register for hourly collection. On the
|
|
@@ -68,6 +147,11 @@ async function triggerAllBrains(processMessageFn) {
|
|
|
68
147
|
logger.debug(`[Maintenance] At capacity, skipping ${brainId}`);
|
|
69
148
|
continue;
|
|
70
149
|
}
|
|
150
|
+
if (await shouldSkipMaintenance(config.BRAINS_PATH, brainId, info.lastRealMessageAt)) {
|
|
151
|
+
markMaintenanceJobSkipped();
|
|
152
|
+
logger.debug(`[Maintenance] Skipping ${brainId} — no real activity since last maintenance (#131)`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
71
155
|
logger.info(`[Maintenance] Triggering maintenance for brain ${brainId}`);
|
|
72
156
|
const syntheticMessage = {
|
|
73
157
|
id: `maintenance_${brainId}_${now}`,
|
|
@@ -21,6 +21,8 @@ export interface MetricsPayload {
|
|
|
21
21
|
daysSinceMaintenance?: number;
|
|
22
22
|
lastMaintenanceJobMs?: number;
|
|
23
23
|
lastMaintenanceJobSuccessMs?: number;
|
|
24
|
+
maintenanceSkippedCountLastRun?: number;
|
|
25
|
+
lastMaintenanceJobSkippedMs?: number;
|
|
24
26
|
lastAutoUpdateJobMs?: number;
|
|
25
27
|
lastAutoUpdateJobSuccessMs?: number;
|
|
26
28
|
timestamp?: string;
|
|
@@ -228,6 +228,13 @@ export async function collectMetrics(brainPath, brainId) {
|
|
|
228
228
|
if (cron.lastMaintenanceJobSuccessAt !== null) {
|
|
229
229
|
payload.lastMaintenanceJobSuccessMs = now - cron.lastMaintenanceJobSuccessAt;
|
|
230
230
|
}
|
|
231
|
+
// #131 — activity-based skip counter. Always sent (0 is a real "no skips
|
|
232
|
+
// this run" value, not an omission) so a stuck-high count is visible
|
|
233
|
+
// fleet-wide without per-brain digging.
|
|
234
|
+
payload.maintenanceSkippedCountLastRun = cron.maintenanceSkippedCountLastRun;
|
|
235
|
+
if (cron.lastMaintenanceJobSkippedAt !== null) {
|
|
236
|
+
payload.lastMaintenanceJobSkippedMs = now - cron.lastMaintenanceJobSkippedAt;
|
|
237
|
+
}
|
|
231
238
|
if (cron.lastAutoUpdateJobFiredAt !== null) {
|
|
232
239
|
payload.lastAutoUpdateJobMs = now - cron.lastAutoUpdateJobFiredAt;
|
|
233
240
|
}
|
package/dist/poller.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
|
+
import { type SpawnResult } from './claude-spawner.js';
|
|
3
|
+
import { type KilledByPoller } from './classify-failed-iteration.js';
|
|
2
4
|
import type { TeamVibeQueueMessage } from './types.js';
|
|
3
5
|
interface ReleaseLogger {
|
|
4
6
|
info: (msg: string) => void;
|
|
@@ -22,6 +24,16 @@ export declare function runAutoUpdateJob(checkForUpdateFn?: () => Promise<boolea
|
|
|
22
24
|
* will pick up any new file on its own.
|
|
23
25
|
*/
|
|
24
26
|
export declare function isInboxLoopActive(threadId: string): boolean;
|
|
27
|
+
export declare function isIntentionalKillExitCode(exitCode: number | null): boolean;
|
|
28
|
+
export declare function isPollerInitiatedFailure(result: {
|
|
29
|
+
exitCode: number | null;
|
|
30
|
+
killedByPoller?: KilledByPoller;
|
|
31
|
+
}): boolean;
|
|
32
|
+
export declare function describeFailureReason(killedByPoller: KilledByPoller | undefined, exitCode: number | null): string;
|
|
33
|
+
export declare const END_OF_TURN_NO_FURTHER_RESPONSE_SENTINEL = "NO_FURTHER_RESPONSE_NEEDED";
|
|
34
|
+
export declare function endOfTurnFallbackText(hasSlackContext: boolean, result: Pick<SpawnResult, 'success' | 'sawSendMessageCall' | 'lastPlainTextContent'>): string | undefined;
|
|
25
35
|
export declare function runInboxLoop(threadId: string, kbPath: string, queueMessage: TeamVibeQueueMessage, sessionLog: ReturnType<typeof logger.createSession>): Promise<void>;
|
|
36
|
+
export declare function __setSleepForTest(impl: (ms: number) => Promise<void>): void;
|
|
37
|
+
export declare function __setShuttingDownForTest(value: boolean): void;
|
|
26
38
|
export declare function startPoller(): Promise<void>;
|
|
27
39
|
export {};
|