@teamvibe/poller 0.7.0 → 0.7.2
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 +8 -0
- package/dist/brain-manager.js +132 -6
- package/dist/brains-bootstrap.d.ts +45 -0
- package/dist/brains-bootstrap.js +217 -0
- package/dist/claude-spawner.d.ts +1 -0
- package/dist/claude-spawner.js +10 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +17 -0
- package/dist/idle-restart.d.ts +12 -0
- package/dist/idle-restart.js +200 -0
- package/dist/maintenance-cron.d.ts +66 -0
- package/dist/maintenance-cron.js +69 -0
- package/dist/poller.d.ts +3 -0
- package/dist/poller.js +204 -21
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { appendFileSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { config } from './config.js';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
import { getActiveProcessCount, getLastSessionEndTs } from './claude-spawner.js';
|
|
6
|
+
import { emitEvent } from './event-queue.js';
|
|
7
|
+
// tv#317: self-restart once genuinely idle, to let Railway reclaim page-cache RAM that
|
|
8
|
+
// Linux never proactively frees under our large memory limit. See config.ts for the
|
|
9
|
+
// IDLE_RESTART_* thresholds this checks against.
|
|
10
|
+
const processStartTime = Date.now();
|
|
11
|
+
// cgroup v2 only. Railway containers run cgroup v2; on anything else (local dev, a
|
|
12
|
+
// future cgroup v1 host) this just returns null and the gate never fires — the memory
|
|
13
|
+
// pressure this exists to relieve is Railway-specific, so failing closed is correct
|
|
14
|
+
// rather than guessing at a v1 path/format.
|
|
15
|
+
const MEMORY_STAT_PATH = '/sys/fs/cgroup/memory.stat';
|
|
16
|
+
export function readReclaimableFileBytes(path = MEMORY_STAT_PATH) {
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = readFileSync(path, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
for (const line of text.split('\n')) {
|
|
25
|
+
const spaceIdx = line.indexOf(' ');
|
|
26
|
+
if (spaceIdx > 0 && line.slice(0, spaceIdx) === 'file') {
|
|
27
|
+
const value = Number(line.slice(spaceIdx + 1));
|
|
28
|
+
return Number.isFinite(value) ? value : null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// --- bg-task liveness ---------------------------------------------------------------
|
|
34
|
+
//
|
|
35
|
+
// Reimplemented (not imported): bg-task.mjs lives in poller-brain and runs inside a
|
|
36
|
+
// Claude session, not the poller process — there's no shared module to import here.
|
|
37
|
+
// This mirrors its parseStatus/lifecycleOf classification (poller-brain
|
|
38
|
+
// skills/background-task/scripts/bg-task.mjs) rather than a raw /proc scan, which
|
|
39
|
+
// false-negatives on interpreter/path variations and can't distinguish "stuck past TTL"
|
|
40
|
+
// from "healthy" the way the status file's own started/ttl fields can.
|
|
41
|
+
const ABANDON_GRACE_SEC = 60; // mirrors bg-task.mjs's ABANDON_GRACE_SEC
|
|
42
|
+
function parseStatus(text) {
|
|
43
|
+
const kv = {};
|
|
44
|
+
for (const line of text.split('\n')) {
|
|
45
|
+
const i = line.indexOf('=');
|
|
46
|
+
if (i > 0)
|
|
47
|
+
kv[line.slice(0, i)] = line.slice(i + 1);
|
|
48
|
+
}
|
|
49
|
+
return kv;
|
|
50
|
+
}
|
|
51
|
+
function pidAlive(pid) {
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Same four-way classification as bg-task.mjs's lifecycleOf, minus the tests-only
|
|
61
|
+
// `alive` injection point (this module has its own dedicated tests instead).
|
|
62
|
+
function lifecycleOf(kv, now) {
|
|
63
|
+
// Trust the runner's own claim, whatever it says (bg-task.mjs:139) — not just the two
|
|
64
|
+
// values the runner writes today (`finished`/`timed-out`, bg-task-runner.mjs:403).
|
|
65
|
+
// Mirroring the check itself, not just its current output, means the two
|
|
66
|
+
// implementations of this format can't quietly diverge if the runner ever starts
|
|
67
|
+
// writing a state value this list doesn't happen to enumerate — hardcoding a closed
|
|
68
|
+
// set here would silently fall through to the pid/TTL heuristics below instead.
|
|
69
|
+
if (kv.state)
|
|
70
|
+
return kv.state;
|
|
71
|
+
if (kv.runner_crashed)
|
|
72
|
+
return 'crashed';
|
|
73
|
+
if (Object.keys(kv).length === 0)
|
|
74
|
+
return 'unknown';
|
|
75
|
+
const pid = Number(kv.pid);
|
|
76
|
+
if (!Number.isInteger(pid) || pid <= 0 || !pidAlive(pid))
|
|
77
|
+
return 'abandoned';
|
|
78
|
+
const started = kv.started ? Date.parse(kv.started) : NaN;
|
|
79
|
+
const ttl = Number(String(kv.ttl ?? '').replace(/s$/, ''));
|
|
80
|
+
if (Number.isFinite(started) && Number.isFinite(ttl) && now - started > (ttl + ABANDON_GRACE_SEC) * 1000) {
|
|
81
|
+
return 'abandoned';
|
|
82
|
+
}
|
|
83
|
+
return 'running';
|
|
84
|
+
}
|
|
85
|
+
function bgTaskRoot() {
|
|
86
|
+
// Mirrors bg-task.mjs's taskRoot() default exactly, minus the per-channel
|
|
87
|
+
// namespace — the poller checks liveness across every channel it serves, not just
|
|
88
|
+
// one, so it walks the parent directory instead of joining a single channel ID.
|
|
89
|
+
return process.env['BG_TASK_ROOT'] || join(config.PERSISTENT_STORAGE_PATH || '/tmp', 'bg-tasks');
|
|
90
|
+
}
|
|
91
|
+
function listDirs(path) {
|
|
92
|
+
try {
|
|
93
|
+
return readdirSync(path, { withFileTypes: true })
|
|
94
|
+
.filter((entry) => entry.isDirectory())
|
|
95
|
+
.map((entry) => entry.name);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Returns a "channel/taskId" label for the first live (running or unknown-state) task
|
|
102
|
+
// found, or null if none. `unknown` (no status written yet) is treated as live —
|
|
103
|
+
// deliberately conservative, per tv#317: a task a few milliseconds from its first
|
|
104
|
+
// status line looks identical to an orphaned dir, and "skip this idle cycle" is cheap
|
|
105
|
+
// while restarting mid-launch is not recoverable.
|
|
106
|
+
export function findLiveBgTask(now = Date.now()) {
|
|
107
|
+
const root = bgTaskRoot();
|
|
108
|
+
for (const channel of listDirs(root)) {
|
|
109
|
+
const channelPath = join(root, channel);
|
|
110
|
+
for (const taskDir of listDirs(channelPath)) {
|
|
111
|
+
let statusText = '';
|
|
112
|
+
try {
|
|
113
|
+
statusText = readFileSync(join(channelPath, taskDir, 'status'), 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// No status file at all — same "unknown" treatment as an empty one below.
|
|
117
|
+
}
|
|
118
|
+
const state = lifecycleOf(parseStatus(statusText), now);
|
|
119
|
+
if (state === 'running' || state === 'unknown') {
|
|
120
|
+
return `${channel}/${taskDir}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
// tv#317 "Startup reconciliation": bg-tasks don't survive a poller restart (their
|
|
127
|
+
// runner is a child of the container that just went away), but a status file on a
|
|
128
|
+
// persistent volume has no way to know that on its own. Without this, a task dir
|
|
129
|
+
// whose runner never wrote a terminal line looks identical to a live one — worse,
|
|
130
|
+
// after a restart the fresh container's own low PIDs can coincidentally collide with
|
|
131
|
+
// a stale recorded `pid=`, so pidAlive() alone isn't reliable evidence either. Called
|
|
132
|
+
// once at boot so every pre-restart dir is unambiguously stamped before the idle-restart
|
|
133
|
+
// gate (or `--list`) ever reads it again.
|
|
134
|
+
export function reconcileBgTasksOnBoot() {
|
|
135
|
+
const root = bgTaskRoot();
|
|
136
|
+
let stamped = 0;
|
|
137
|
+
for (const channel of listDirs(root)) {
|
|
138
|
+
const channelPath = join(root, channel);
|
|
139
|
+
for (const taskDir of listDirs(channelPath)) {
|
|
140
|
+
const statusPath = join(channelPath, taskDir, 'status');
|
|
141
|
+
let statusText;
|
|
142
|
+
try {
|
|
143
|
+
statusText = readFileSync(statusPath, 'utf-8');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
continue; // no status file yet — nothing to stamp
|
|
147
|
+
}
|
|
148
|
+
const kv = parseStatus(statusText);
|
|
149
|
+
if (kv.state || kv.runner_crashed)
|
|
150
|
+
continue; // already terminal
|
|
151
|
+
try {
|
|
152
|
+
appendFileSync(statusPath, 'runner_crashed=container_restart\n');
|
|
153
|
+
stamped++;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
logger.warn(`[IdleRestart] Failed to stamp stale bg-task ${channel}/${taskDir}: ${err instanceof Error ? err.message : err}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (stamped > 0) {
|
|
161
|
+
logger.info(`[IdleRestart] Reconciled ${stamped} bg-task dir(s) left over from before this boot`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function checkIdleRestart(deps = {}) {
|
|
165
|
+
const now = deps.now ?? Date.now();
|
|
166
|
+
const startTime = deps.startTime ?? processStartTime;
|
|
167
|
+
const activeProcessCount = deps.activeProcessCount ?? getActiveProcessCount;
|
|
168
|
+
const lastSessionEndTs = deps.lastSessionEndTs ?? getLastSessionEndTs;
|
|
169
|
+
const reclaimableBytesFn = deps.reclaimableBytes ?? readReclaimableFileBytes;
|
|
170
|
+
const liveBgTask = deps.liveBgTask ?? findLiveBgTask;
|
|
171
|
+
if (!config.IDLE_RESTART_ENABLED)
|
|
172
|
+
return false;
|
|
173
|
+
const uptimeMs = now - startTime;
|
|
174
|
+
if (uptimeMs < config.IDLE_RESTART_MIN_UPTIME_MS)
|
|
175
|
+
return false;
|
|
176
|
+
if (activeProcessCount() > 0)
|
|
177
|
+
return false;
|
|
178
|
+
const idleMs = now - lastSessionEndTs();
|
|
179
|
+
if (idleMs < config.IDLE_RESTART_IDLE_MS)
|
|
180
|
+
return false;
|
|
181
|
+
const reclaimableBytes = reclaimableBytesFn();
|
|
182
|
+
if (reclaimableBytes === null || reclaimableBytes < config.IDLE_RESTART_MEMORY_FILE_THRESHOLD_BYTES)
|
|
183
|
+
return false;
|
|
184
|
+
if (liveBgTask())
|
|
185
|
+
return false; // skip this cycle, recheck next time — never cut a bg-task short just to free memory sooner
|
|
186
|
+
// Re-verify the two liveness conditions right before signaling restart — a
|
|
187
|
+
// session or bg-task could have started in the time it took to reach this point.
|
|
188
|
+
if (activeProcessCount() > 0)
|
|
189
|
+
return false;
|
|
190
|
+
if (liveBgTask())
|
|
191
|
+
return false;
|
|
192
|
+
logger.info(`[IdleRestart] Idle ${Math.round(idleMs / 60000)}min, reclaimable page cache ` +
|
|
193
|
+
`${Math.round(reclaimableBytes / 1024 / 1024)}MB — restarting to let Railway reclaim it`);
|
|
194
|
+
emitEvent('poller.idle_restart', 'system', {
|
|
195
|
+
idleMs,
|
|
196
|
+
uptimeMs,
|
|
197
|
+
reclaimableBytes,
|
|
198
|
+
});
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
@@ -21,6 +21,72 @@ export declare function getTrackedBrains(): ReadonlyMap<string, TrackedBrainInfo
|
|
|
21
21
|
* @internal
|
|
22
22
|
*/
|
|
23
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;
|
|
24
90
|
/**
|
|
25
91
|
* Register a brain from a real message so the maintenance cron knows about it.
|
|
26
92
|
* Called from processMessage() after brain path is resolved.
|
package/dist/maintenance-cron.js
CHANGED
|
@@ -27,6 +27,75 @@ export function __resetTrackedBrainsForTest() {
|
|
|
27
27
|
trackedBrains.clear();
|
|
28
28
|
}
|
|
29
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
|
+
}
|
|
30
99
|
/**
|
|
31
100
|
* Register a brain from a real message so the maintenance cron knows about it.
|
|
32
101
|
* Called from processMessage() after brain path is resolved.
|
package/dist/poller.d.ts
CHANGED
|
@@ -29,8 +29,11 @@ export declare function isPollerInitiatedFailure(result: {
|
|
|
29
29
|
exitCode: number | null;
|
|
30
30
|
killedByPoller?: KilledByPoller;
|
|
31
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";
|
|
32
34
|
export declare function endOfTurnFallbackText(hasSlackContext: boolean, result: Pick<SpawnResult, 'success' | 'sawSendMessageCall' | 'lastPlainTextContent'>): string | undefined;
|
|
33
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;
|
|
34
37
|
export declare function __setShuttingDownForTest(value: boolean): void;
|
|
35
38
|
export declare function startPoller(): Promise<void>;
|
|
36
39
|
export {};
|