@teamvibe/poller 0.8.1 → 0.8.2-rc.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/auth-provider.d.ts +6 -0
- package/dist/auth-provider.js +52 -7
- package/dist/base-brain-metadata.js +6 -1
- package/dist/brain-manager.js +4 -3
- package/dist/claude-spawner.js +20 -26
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/maintenance-reporter.d.ts +8 -0
- package/dist/maintenance-reporter.js +20 -1
- package/dist/poller.js +24 -4
- package/dist/slack-client.d.ts +3 -1
- package/dist/slack-client.js +102 -9
- package/dist/startup-auth.d.ts +15 -0
- package/dist/startup-auth.js +137 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/auth-provider.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ interface AuthConfig {
|
|
|
9
9
|
sessionsTable: string;
|
|
10
10
|
region: string;
|
|
11
11
|
}
|
|
12
|
+
export declare class AuthHttpError extends Error {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
constructor(status: number, _body?: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class StartupConfigurationError extends Error {
|
|
17
|
+
}
|
|
12
18
|
export declare function initAuth(apiUrl: string, token: string): Promise<void>;
|
|
13
19
|
export declare function getCredentials(): AuthCredentials;
|
|
14
20
|
export declare function getAuthConfig(): AuthConfig;
|
package/dist/auth-provider.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { POLLER_VERSION } from './config.js';
|
|
1
|
+
import { config, POLLER_VERSION } from './config.js';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
3
|
import { emitError } from './event-queue.js';
|
|
4
|
+
import { getBaseBrainCommitSha } from './base-brain-metadata.js';
|
|
5
|
+
export class AuthHttpError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
constructor(status, _body) {
|
|
8
|
+
super(`Auth failed (${status})`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = 'AuthHttpError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class StartupConfigurationError extends Error {
|
|
14
|
+
}
|
|
4
15
|
let currentCredentials = null;
|
|
5
16
|
let currentConfig = null;
|
|
6
17
|
let currentPollerId = null;
|
|
@@ -13,13 +24,16 @@ const REFRESH_INTERVAL_MS = 50 * 60 * 1000; // 50 minutes (before 1hr expiry)
|
|
|
13
24
|
const HEARTBEAT_INTERVAL_MS = 60 * 1000; // 1 minute
|
|
14
25
|
async function sendHeartbeat(apiUrl, token, version) {
|
|
15
26
|
try {
|
|
27
|
+
const baseBrainSha = await getBaseBrainCommitSha(config.BASE_BRAIN_PATH);
|
|
28
|
+
if (!baseBrainSha)
|
|
29
|
+
logger.warn('[Heartbeat] Could not read base-brain HEAD for pin readback');
|
|
16
30
|
const response = await fetch(`${apiUrl}/heartbeat`, {
|
|
17
31
|
method: 'POST',
|
|
18
32
|
headers: {
|
|
19
33
|
Authorization: `Bearer ${token}`,
|
|
20
34
|
'Content-Type': 'application/json',
|
|
21
35
|
},
|
|
22
|
-
body: JSON.stringify({ version }),
|
|
36
|
+
body: JSON.stringify({ version, baseBrainSha, baseBrainBranch: config.BASE_BRAIN_BRANCH }),
|
|
23
37
|
});
|
|
24
38
|
if (!response.ok) {
|
|
25
39
|
logger.warn(`Heartbeat failed (${response.status})`);
|
|
@@ -31,25 +45,56 @@ async function sendHeartbeat(apiUrl, token, version) {
|
|
|
31
45
|
emitError('auth', `Heartbeat error: ${error instanceof Error ? error.message : String(error)}`, { severity: 'warn' });
|
|
32
46
|
}
|
|
33
47
|
}
|
|
34
|
-
async function fetchCredentials(apiUrl, token) {
|
|
48
|
+
async function fetchCredentials(apiUrl, token, signal) {
|
|
35
49
|
const response = await fetch(`${apiUrl}/auth`, {
|
|
36
50
|
method: 'POST',
|
|
51
|
+
signal,
|
|
37
52
|
headers: {
|
|
38
53
|
Authorization: `Bearer ${token}`,
|
|
39
54
|
'Content-Type': 'application/json',
|
|
40
55
|
},
|
|
41
56
|
});
|
|
42
57
|
if (!response.ok) {
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
throw new AuthHttpError(response.status);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await response.json();
|
|
45
62
|
}
|
|
46
|
-
|
|
63
|
+
catch {
|
|
64
|
+
throw new StartupConfigurationError('Auth response is not valid JSON');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function validateStartupAuthResponse(auth) {
|
|
68
|
+
const value = auth;
|
|
69
|
+
const authRecord = value;
|
|
70
|
+
const credentials = authRecord?.credentials;
|
|
71
|
+
const authConfig = authRecord?.config;
|
|
72
|
+
if (typeof authRecord?.pollerId !== 'string' || !authRecord.pollerId ||
|
|
73
|
+
typeof credentials?.accessKeyId !== 'string' || !credentials.accessKeyId ||
|
|
74
|
+
typeof credentials?.secretAccessKey !== 'string' || !credentials.secretAccessKey ||
|
|
75
|
+
typeof credentials?.sessionToken !== 'string' || !credentials.sessionToken ||
|
|
76
|
+
typeof credentials?.expiration !== 'string' || !credentials.expiration ||
|
|
77
|
+
typeof authConfig?.sqsQueueUrl !== 'string' || !authConfig.sqsQueueUrl ||
|
|
78
|
+
typeof authConfig?.sessionsTable !== 'string' || !authConfig.sessionsTable ||
|
|
79
|
+
typeof authConfig?.region !== 'string' || !authConfig.region)
|
|
80
|
+
throw new StartupConfigurationError('Auth response is missing or has malformed credentials/config');
|
|
47
81
|
}
|
|
48
82
|
export async function initAuth(apiUrl, token) {
|
|
83
|
+
if (!token.trim())
|
|
84
|
+
throw new StartupConfigurationError('TEAMVIBE_POLLER_TOKEN is missing or empty');
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(apiUrl);
|
|
87
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
88
|
+
throw new Error('unsupported protocol');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new StartupConfigurationError('TEAMVIBE_API_URL is malformed');
|
|
92
|
+
}
|
|
49
93
|
logger.info('Authenticating with TeamVibe API...');
|
|
50
94
|
storedApiUrl = apiUrl;
|
|
51
95
|
storedToken = token;
|
|
52
|
-
const auth = await fetchCredentials(apiUrl, token);
|
|
96
|
+
const auth = await fetchCredentials(apiUrl, token, AbortSignal.timeout(15_000));
|
|
97
|
+
validateStartupAuthResponse(auth);
|
|
53
98
|
currentCredentials = auth.credentials;
|
|
54
99
|
currentConfig = auth.config;
|
|
55
100
|
currentPollerId = auth.pollerId;
|
|
@@ -3,6 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { execFile } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
|
+
const GIT_HEAD_TIMEOUT_MS = 2_000;
|
|
6
7
|
/**
|
|
7
8
|
* Read the current HEAD commit SHA of a git repo. Returns null on any error
|
|
8
9
|
* (not a git repo, git missing, dir missing, etc.) so callers can attach a
|
|
@@ -10,7 +11,11 @@ const execFileAsync = promisify(execFile);
|
|
|
10
11
|
*/
|
|
11
12
|
export async function getBaseBrainCommitSha(repoDir) {
|
|
12
13
|
try {
|
|
13
|
-
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
14
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
15
|
+
cwd: repoDir,
|
|
16
|
+
timeout: GIT_HEAD_TIMEOUT_MS,
|
|
17
|
+
killSignal: 'SIGKILL',
|
|
18
|
+
});
|
|
14
19
|
const sha = stdout.trim();
|
|
15
20
|
return sha || null;
|
|
16
21
|
}
|
package/dist/brain-manager.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join, basename } from 'path';
|
|
|
6
6
|
import { config } from './config.js';
|
|
7
7
|
import { logger } from './logger.js';
|
|
8
8
|
import { emitError } from './event-queue.js';
|
|
9
|
+
import { startupFailureDetail } from './startup-auth.js';
|
|
9
10
|
const execAsync = promisify(exec);
|
|
10
11
|
// Clean env for git commands:
|
|
11
12
|
// - npm/npx sets GIT_ASKPASS which prevents credential helpers from working
|
|
@@ -257,7 +258,7 @@ async function updateBrainLocked(brainDir, brainId, branch) {
|
|
|
257
258
|
catch (error) {
|
|
258
259
|
// Pre-check itself failed (e.g. brainDir not a git repo). Fail-safe to skip;
|
|
259
260
|
// the next iteration will retry.
|
|
260
|
-
const errorMessage = error
|
|
261
|
+
const errorMessage = startupFailureDetail(error, 'BaseBrain', [config.TEAMVIBE_POLLER_TOKEN ?? '', config.BASE_BRAIN_REPO]);
|
|
261
262
|
logger.warn(`Brain ${brainId} pre-check failed, skipping update: ${errorMessage}`);
|
|
262
263
|
return;
|
|
263
264
|
}
|
|
@@ -271,7 +272,7 @@ async function updateBrainLocked(brainDir, brainId, branch) {
|
|
|
271
272
|
logger.info(`Brain ${brainId} updated successfully`);
|
|
272
273
|
}
|
|
273
274
|
catch (error) {
|
|
274
|
-
const errorMessage = error
|
|
275
|
+
const errorMessage = startupFailureDetail(error, 'BaseBrain', [config.TEAMVIBE_POLLER_TOKEN ?? '', config.BASE_BRAIN_REPO]);
|
|
275
276
|
logger.error(`Failed to update brain ${brainId}: ${errorMessage}`);
|
|
276
277
|
emitError('brain', `Failed to update brain ${brainId}: ${errorMessage}`, { context: { brainId, brainDir } });
|
|
277
278
|
}
|
|
@@ -283,7 +284,7 @@ async function updateBrainLocked(brainDir, brainId, branch) {
|
|
|
283
284
|
export async function ensureBaseBrain() {
|
|
284
285
|
const brainDir = config.BASE_BRAIN_PATH;
|
|
285
286
|
if (!existsSync(brainDir)) {
|
|
286
|
-
logger.info(`Cloning base brain
|
|
287
|
+
logger.info(`Cloning base brain (branch: ${config.BASE_BRAIN_BRANCH})...`);
|
|
287
288
|
await gitExec(`git clone --recurse-submodules --branch ${config.BASE_BRAIN_BRANCH} ${config.BASE_BRAIN_REPO} ${brainDir}`);
|
|
288
289
|
lastUpdateTimes.set('__base_brain__', Date.now());
|
|
289
290
|
logger.info('Base brain cloned successfully');
|
package/dist/claude-spawner.js
CHANGED
|
@@ -702,22 +702,13 @@ export function assistantCalledTool(event, toolName) {
|
|
|
702
702
|
// Status text is a one-line UI label, not a chat message — cap it well short
|
|
703
703
|
// of a full sentence.
|
|
704
704
|
const TYPING_STATUS_MAX_LEN = 100;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
// `event.type === 'tool_use'` check — see assistantCalledTool's comment
|
|
713
|
-
// above for why the latter never matches the real stream shape.
|
|
714
|
-
//
|
|
715
|
-
// `assistantText`, when passed, is the caller's already-extracted
|
|
716
|
-
// extractAssistantText(event) result (the stdout handler computes it a few
|
|
717
|
-
// lines above for the end-of-turn guard) — avoids re-parsing the same event
|
|
718
|
-
// twice per line. Falls back to extracting it internally so the function
|
|
719
|
-
// stays a self-contained, directly-testable unit.
|
|
720
|
-
export function resolveTypingStatus(event, assistantText) {
|
|
705
|
+
function oneLineStatus(text) {
|
|
706
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
707
|
+
return oneLine.length > TYPING_STATUS_MAX_LEN
|
|
708
|
+
? oneLine.slice(0, TYPING_STATUS_MAX_LEN - 1) + '…'
|
|
709
|
+
: oneLine;
|
|
710
|
+
}
|
|
711
|
+
function resolveTypingStatusDetail(event, assistantText) {
|
|
721
712
|
if (event?.type !== 'assistant' || !Array.isArray(event.message?.content)) {
|
|
722
713
|
return null;
|
|
723
714
|
}
|
|
@@ -729,19 +720,22 @@ export function resolveTypingStatus(event, assistantText) {
|
|
|
729
720
|
if (text) {
|
|
730
721
|
// Collapse newlines/whitespace runs so a multi-line narration doesn't
|
|
731
722
|
// land a literal `\n` in the one-line status surface.
|
|
732
|
-
|
|
733
|
-
return oneLine.length > TYPING_STATUS_MAX_LEN
|
|
734
|
-
? oneLine.slice(0, TYPING_STATUS_MAX_LEN - 1) + '…'
|
|
735
|
-
: oneLine;
|
|
723
|
+
return { text: oneLineStatus(text), source: 'comment' };
|
|
736
724
|
}
|
|
737
725
|
for (const block of event.message.content) {
|
|
738
726
|
if (block?.type !== 'tool_use' || typeof block.name !== 'string')
|
|
739
727
|
continue;
|
|
740
728
|
const status = getStatusForTool(block.name);
|
|
741
|
-
if (status)
|
|
742
|
-
|
|
729
|
+
if (status) {
|
|
730
|
+
const safeToolName = /^(?:Bash|Read|Edit|Write|WebFetch|WebSearch|Grep|Glob|Agent)$/.test(block.name)
|
|
731
|
+
? block.name : 'other';
|
|
732
|
+
return { text: oneLineStatus(status), source: `tool:${safeToolName}` };
|
|
733
|
+
}
|
|
743
734
|
}
|
|
744
|
-
return 'Thinking...';
|
|
735
|
+
return { text: 'Thinking...', source: 'default' };
|
|
736
|
+
}
|
|
737
|
+
export function resolveTypingStatus(event, assistantText) {
|
|
738
|
+
return resolveTypingStatusDetail(event, assistantText)?.text ?? null;
|
|
745
739
|
}
|
|
746
740
|
async function runClaudeCode(msg, sessionLog, cwd, brainPath, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
|
|
747
741
|
const slackContext = msg.response_context.slack;
|
|
@@ -1050,9 +1044,9 @@ async function runClaudeCode(msg, sessionLog, cwd, brainPath, sessionId, isFirst
|
|
|
1050
1044
|
}
|
|
1051
1045
|
// Auto-update typing indicator status based on tool usage (tv#369, tv#291)
|
|
1052
1046
|
if (typingIndicator) {
|
|
1053
|
-
const status =
|
|
1047
|
+
const status = resolveTypingStatusDetail(event, assistantText);
|
|
1054
1048
|
if (status) {
|
|
1055
|
-
typingIndicator.updateStatus(status);
|
|
1049
|
+
typingIndicator.updateStatus(status.text, status.source);
|
|
1056
1050
|
}
|
|
1057
1051
|
}
|
|
1058
1052
|
}
|
|
@@ -1143,7 +1137,7 @@ async function runClaudeCode(msg, sessionLog, cwd, brainPath, sessionId, isFirst
|
|
|
1143
1137
|
});
|
|
1144
1138
|
// Surface a visible status so the user isn't left with a stale/generic
|
|
1145
1139
|
// indicator during long silent stretches (e.g. context compaction).
|
|
1146
|
-
typingIndicator?.updateStatus('Still working on a long response...');
|
|
1140
|
+
typingIndicator?.updateStatus('Still working on a long response...', 'default');
|
|
1147
1141
|
idleWarned = true;
|
|
1148
1142
|
}
|
|
1149
1143
|
}, 30000); // Check every 30 seconds
|
package/dist/config.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
18
18
|
IDLE_WATCHDOG_COMPACTION_GRACE_MS: z.ZodDefault<z.ZodNumber>;
|
|
19
19
|
NETWORK_ERROR_KILL_MS: z.ZodDefault<z.ZodNumber>;
|
|
20
20
|
MAX_CONSECUTIVE_NET_FAILURES: z.ZodDefault<z.ZodNumber>;
|
|
21
|
+
TYPING_COMMENT_MIN_HOLD_MS: z.ZodDefault<z.ZodNumber>;
|
|
21
22
|
TEAMVIBE_DATA_DIR: z.ZodDefault<z.ZodString>;
|
|
22
23
|
BRAINS_PATH: z.ZodDefault<z.ZodString>;
|
|
23
24
|
DEFAULT_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
@@ -60,6 +61,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
60
61
|
IDLE_WATCHDOG_COMPACTION_GRACE_MS: number;
|
|
61
62
|
NETWORK_ERROR_KILL_MS: number;
|
|
62
63
|
MAX_CONSECUTIVE_NET_FAILURES: number;
|
|
64
|
+
TYPING_COMMENT_MIN_HOLD_MS: number;
|
|
63
65
|
TEAMVIBE_DATA_DIR: string;
|
|
64
66
|
BRAINS_PATH: string;
|
|
65
67
|
DEFAULT_BRAIN_PATH: string;
|
|
@@ -108,6 +110,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
108
110
|
IDLE_WATCHDOG_COMPACTION_GRACE_MS?: number | undefined;
|
|
109
111
|
NETWORK_ERROR_KILL_MS?: number | undefined;
|
|
110
112
|
MAX_CONSECUTIVE_NET_FAILURES?: number | undefined;
|
|
113
|
+
TYPING_COMMENT_MIN_HOLD_MS?: number | undefined;
|
|
111
114
|
TEAMVIBE_DATA_DIR?: string | undefined;
|
|
112
115
|
BRAINS_PATH?: string | undefined;
|
|
113
116
|
DEFAULT_BRAIN_PATH?: string | undefined;
|
|
@@ -153,6 +156,7 @@ export declare const config: {
|
|
|
153
156
|
IDLE_WATCHDOG_COMPACTION_GRACE_MS: number;
|
|
154
157
|
NETWORK_ERROR_KILL_MS: number;
|
|
155
158
|
MAX_CONSECUTIVE_NET_FAILURES: number;
|
|
159
|
+
TYPING_COMMENT_MIN_HOLD_MS: number;
|
|
156
160
|
TEAMVIBE_DATA_DIR: string;
|
|
157
161
|
BRAINS_PATH: string;
|
|
158
162
|
DEFAULT_BRAIN_PATH: string;
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,7 @@ const configSchema = z.object({
|
|
|
27
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)
|
|
28
28
|
NETWORK_ERROR_KILL_MS: z.coerce.number().default(120000), // Per-session: kill stuck session after 2 min of network errors
|
|
29
29
|
MAX_CONSECUTIVE_NET_FAILURES: z.coerce.number().default(5), // Poll loop: DNS flush at N, exit(1) at 2×N consecutive failures
|
|
30
|
+
TYPING_COMMENT_MIN_HOLD_MS: z.coerce.number().default(4500), // Minimum time to display agent narration before a canned status
|
|
30
31
|
// Paths
|
|
31
32
|
TEAMVIBE_DATA_DIR: z.string().default(DEFAULT_DATA_DIR),
|
|
32
33
|
BRAINS_PATH: z.string().default(''),
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** Best-effort runtime identity, sampled when each report is posted. */
|
|
2
|
+
export declare function collectReportProvenance(brainPath: string): Promise<{
|
|
3
|
+
pollerId: string | null;
|
|
4
|
+
pollerVersion: string;
|
|
5
|
+
baseBrainSha: string | null;
|
|
6
|
+
baseBrainBranch: string;
|
|
7
|
+
brainSha: string | null;
|
|
8
|
+
}>;
|
|
1
9
|
/**
|
|
2
10
|
* Scan for JSON maintenance reports in the brain's reports/ directory
|
|
3
11
|
* and POST each to the /events endpoint. Fire-and-forget — never throws.
|
|
@@ -3,10 +3,29 @@ import { join } from 'path';
|
|
|
3
3
|
import { execFile } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { config, POLLER_VERSION } from './config.js';
|
|
6
|
+
import { getPollerId } from './auth-provider.js';
|
|
6
7
|
import { logger } from './logger.js';
|
|
7
8
|
import { emitError } from './event-queue.js';
|
|
8
9
|
import { getBaseBrainCommitSha, getBaseBrainAgeHours, } from './base-brain-metadata.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
+
/** Best-effort runtime identity, sampled when each report is posted. */
|
|
12
|
+
export async function collectReportProvenance(brainPath) {
|
|
13
|
+
const [baseBrainSha, brainSha] = await Promise.all([
|
|
14
|
+
getBaseBrainCommitSha(config.BASE_BRAIN_PATH),
|
|
15
|
+
getBaseBrainCommitSha(brainPath),
|
|
16
|
+
]);
|
|
17
|
+
if (!baseBrainSha)
|
|
18
|
+
logger.warn('[MaintenanceReporter] Could not read base-brain HEAD for report provenance');
|
|
19
|
+
if (!brainSha)
|
|
20
|
+
logger.warn('[MaintenanceReporter] Could not read channel-brain HEAD for report provenance');
|
|
21
|
+
return {
|
|
22
|
+
pollerId: getPollerId(),
|
|
23
|
+
pollerVersion: POLLER_VERSION,
|
|
24
|
+
baseBrainSha,
|
|
25
|
+
baseBrainBranch: config.BASE_BRAIN_BRANCH,
|
|
26
|
+
brainSha,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
10
29
|
const OPERATION_TO_EVENT_TYPE = {
|
|
11
30
|
consolidation: 'maintenance.consolidation',
|
|
12
31
|
reflection: 'maintenance.reflection',
|
|
@@ -131,7 +150,7 @@ export async function postMaintenanceReports(brainPath, channelId) {
|
|
|
131
150
|
const payload = {
|
|
132
151
|
eventType,
|
|
133
152
|
channelId,
|
|
134
|
-
|
|
153
|
+
...(await collectReportProvenance(brainPath)),
|
|
135
154
|
baseBrainCommitSha,
|
|
136
155
|
baseBrainAgeHours,
|
|
137
156
|
metadata: report,
|
package/dist/poller.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getBrainPath, ensureDirectories, ensureBaseBrain, pushBrainChanges } fr
|
|
|
9
9
|
import { cleanupOrphanWorktreesForAllBrains, isWorktreeIsolationEnabled } from './worktree-manager.js';
|
|
10
10
|
import { postMaintenanceReports } from './maintenance-reporter.js';
|
|
11
11
|
import { initAuth, stopRefresh, getPollerId } from './auth-provider.js';
|
|
12
|
+
import { retryStartupAuth, retryStartupOperation, startupFailureDetail } from './startup-auth.js';
|
|
12
13
|
import { registerBrain, startMaintenanceLoop, stopMaintenanceLoop } from './maintenance-cron.js';
|
|
13
14
|
import { bootstrapTrackedBrains } from './brains-bootstrap.js';
|
|
14
15
|
import { startEventQueue, stopEventQueue, flush as flushEventQueue, emitEvent, emitError } from './event-queue.js';
|
|
@@ -870,8 +871,9 @@ export async function startPoller() {
|
|
|
870
871
|
logger.info(`TeamVibe Poller v${POLLER_VERSION} starting...`);
|
|
871
872
|
logger.info(` Max concurrent: ${config.MAX_CONCURRENT_SESSIONS}`);
|
|
872
873
|
logger.info(` Brains path: ${config.BRAINS_PATH}`);
|
|
873
|
-
|
|
874
|
-
process.on('
|
|
874
|
+
const startupAbort = new AbortController();
|
|
875
|
+
process.on('SIGINT', () => { startupAbort.abort(); void shutdown('SIGINT'); });
|
|
876
|
+
process.on('SIGTERM', () => { startupAbort.abort(); void shutdown('SIGTERM'); });
|
|
875
877
|
// Last-resort handlers so a hard crash produces one tagged ERROR line (+ a
|
|
876
878
|
// best-effort event flush) instead of an untagged default Node stack trace —
|
|
877
879
|
// these are exactly the failures that never reach /events on their own.
|
|
@@ -899,7 +901,15 @@ export async function startPoller() {
|
|
|
899
901
|
});
|
|
900
902
|
// Token-based auth: fetch credentials before starting
|
|
901
903
|
if (config.TEAMVIBE_API_URL && config.TEAMVIBE_POLLER_TOKEN) {
|
|
902
|
-
|
|
904
|
+
try {
|
|
905
|
+
const ready = await retryStartupAuth(() => initAuth(config.TEAMVIBE_API_URL, config.TEAMVIBE_POLLER_TOKEN), startupAbort.signal, logger, Math.random, [config.TEAMVIBE_POLLER_TOKEN ?? '', config.BASE_BRAIN_REPO]);
|
|
906
|
+
if (!ready)
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
logger.error(`Startup authentication failed permanently: ${startupFailureDetail(error, 'StartupAuth', [config.TEAMVIBE_POLLER_TOKEN ?? '', config.BASE_BRAIN_REPO])}`);
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
903
913
|
const pollerId = getPollerId();
|
|
904
914
|
if (pollerId) {
|
|
905
915
|
setLogContext({ pollerId });
|
|
@@ -910,9 +920,19 @@ export async function startPoller() {
|
|
|
910
920
|
logger.info(` Queue: ${config.SQS_QUEUE_URL}`);
|
|
911
921
|
logger.info(` Sessions table: ${config.SESSIONS_TABLE}`);
|
|
912
922
|
}
|
|
923
|
+
if (startupAbort.signal.aborted)
|
|
924
|
+
return;
|
|
913
925
|
startEventQueue();
|
|
914
926
|
await ensureDirectories();
|
|
915
|
-
|
|
927
|
+
try {
|
|
928
|
+
const ready = await retryStartupOperation('BaseBrain', ensureBaseBrain, startupAbort.signal, logger, Math.random, [config.TEAMVIBE_POLLER_TOKEN ?? '', config.BASE_BRAIN_REPO]);
|
|
929
|
+
if (!ready)
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
logger.error(`Startup base brain failed permanently: ${startupFailureDetail(error, 'BaseBrain', [config.TEAMVIBE_POLLER_TOKEN ?? '', config.BASE_BRAIN_REPO])}`);
|
|
934
|
+
process.exit(1);
|
|
935
|
+
}
|
|
916
936
|
reconcileBgTasksOnBoot();
|
|
917
937
|
if (config.IDLE_RESTART_ENABLED) {
|
|
918
938
|
// Defaults off (tv#317 follow-up, DevGuru review): a silent disabled gate and an
|
package/dist/slack-client.d.ts
CHANGED
|
@@ -7,11 +7,13 @@ export declare function sendSlackMessage(msg: TeamVibeQueueMessage, text: string
|
|
|
7
7
|
export declare function sendSlackError(msg: TeamVibeQueueMessage, error: string): Promise<void>;
|
|
8
8
|
export declare function addReaction(msg: TeamVibeQueueMessage, emoji: string): Promise<void>;
|
|
9
9
|
export declare function setThreadStatus(msg: TeamVibeQueueMessage, status: string): Promise<void>;
|
|
10
|
+
type TypingStatusSource = 'comment' | 'default' | `tool:${string}`;
|
|
10
11
|
export interface TypingIndicator {
|
|
11
12
|
/** Update the displayed status text */
|
|
12
|
-
updateStatus: (newStatus: string) => void;
|
|
13
|
+
updateStatus: (newStatus: string, source?: TypingStatusSource) => void;
|
|
13
14
|
/** Stop the typing indicator and clear the status */
|
|
14
15
|
stop: () => void;
|
|
15
16
|
}
|
|
16
17
|
export declare function startTypingIndicator(msg: TeamVibeQueueMessage, statusText?: string): TypingIndicator;
|
|
17
18
|
export declare function removeReaction(msg: TeamVibeQueueMessage, emoji: string): Promise<void>;
|
|
19
|
+
export {};
|
package/dist/slack-client.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { WebClient } from '@slack/web-api';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
3
|
import { emitError } from './event-queue.js';
|
|
4
|
+
import { config } from './config.js';
|
|
4
5
|
// Cache user info to avoid repeated API calls
|
|
5
6
|
const userCache = new Map();
|
|
6
7
|
// Per-token Slack client cache
|
|
7
8
|
const slackClients = new Map();
|
|
9
|
+
// Slack: "must be less than 51 characters [json-pointer:/loading_messages/0]"
|
|
10
|
+
const SLACK_LOADING_MESSAGE_MAX = 50;
|
|
8
11
|
function getSlackClient(botToken) {
|
|
9
12
|
let client = slackClients.get(botToken);
|
|
10
13
|
if (!client) {
|
|
@@ -94,22 +97,87 @@ export async function setThreadStatus(msg, status) {
|
|
|
94
97
|
return;
|
|
95
98
|
const { channel, thread_ts } = msg.response_context.slack;
|
|
96
99
|
const slack = getSlackClient(msg.teamvibe.botToken);
|
|
100
|
+
const safeStatus = neutralizeSlackMarkup(status);
|
|
101
|
+
const payload = {
|
|
102
|
+
channel_id: channel,
|
|
103
|
+
thread_ts,
|
|
104
|
+
status: safeStatus,
|
|
105
|
+
};
|
|
97
106
|
try {
|
|
98
107
|
await slack.apiCall('assistant.threads.setStatus', {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
status,
|
|
108
|
+
...payload,
|
|
109
|
+
...(safeStatus ? { loading_messages: [truncateLoadingMessage(safeStatus)] } : {}),
|
|
102
110
|
});
|
|
103
111
|
}
|
|
104
112
|
catch (error) {
|
|
105
|
-
|
|
113
|
+
const messages = error?.data?.response_metadata?.messages;
|
|
114
|
+
const details = [error?.message, ...(Array.isArray(messages) ? messages : [])].join(' ');
|
|
115
|
+
if (safeStatus && error?.data?.error === 'invalid_arguments' && details.includes('loading_messages')) {
|
|
116
|
+
// API errors may echo request fields; never log the error or status.
|
|
117
|
+
logger.warn('Slack rejected loading messages; retrying thread status');
|
|
118
|
+
try {
|
|
119
|
+
await slack.apiCall('assistant.threads.setStatus', payload);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
emitError('slack', 'Failed to set thread status', { severity: 'warn' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// API errors may echo request fields; status can contain private narration.
|
|
128
|
+
logger.warn('Failed to set thread status');
|
|
106
129
|
emitError('slack', `Failed to set thread status`, { severity: 'warn' });
|
|
107
130
|
}
|
|
108
131
|
}
|
|
109
|
-
|
|
132
|
+
function truncateLoadingMessage(text) {
|
|
133
|
+
// Treat escaped markup as whole tokens so truncation never leaves a partial entity.
|
|
134
|
+
const tokens = text.match(/&(?:lt|gt|amp);|[\s\S]/gu) ?? [];
|
|
135
|
+
if (text.length <= SLACK_LOADING_MESSAGE_MAX)
|
|
136
|
+
return text;
|
|
137
|
+
let result = '';
|
|
138
|
+
for (const token of tokens) {
|
|
139
|
+
if (result.length + token.length >= SLACK_LOADING_MESSAGE_MAX)
|
|
140
|
+
break;
|
|
141
|
+
result += token;
|
|
142
|
+
}
|
|
143
|
+
return result + '…';
|
|
144
|
+
}
|
|
145
|
+
function neutralizeSlackMarkup(text) {
|
|
146
|
+
return text.replace(/[<>&]/g, (character) => ({ '<': '<', '>': '>', '&': '&' })[character]);
|
|
147
|
+
}
|
|
148
|
+
export function startTypingIndicator(msg, statusText = 'Thinking...') {
|
|
110
149
|
let currentStatus = statusText;
|
|
150
|
+
let currentSource = statusText === 'Thinking...' ? 'default' : 'comment';
|
|
151
|
+
let commentShownAt = currentSource === 'comment' ? Date.now() : 0;
|
|
152
|
+
let pendingCanned = null;
|
|
153
|
+
let holdTimer;
|
|
154
|
+
let stopped = false;
|
|
155
|
+
const logStatus = (status, source) => {
|
|
156
|
+
if (!status)
|
|
157
|
+
logger.info('[Typing] status cleared');
|
|
158
|
+
else
|
|
159
|
+
logger.info(`[Typing] status -> ${source} (${status.length} chars)`);
|
|
160
|
+
};
|
|
111
161
|
// Set initial status
|
|
162
|
+
logStatus(currentStatus, currentSource);
|
|
112
163
|
setThreadStatus(msg, currentStatus);
|
|
164
|
+
const cancelHold = () => {
|
|
165
|
+
if (holdTimer !== undefined)
|
|
166
|
+
clearTimeout(holdTimer);
|
|
167
|
+
holdTimer = undefined;
|
|
168
|
+
pendingCanned = null;
|
|
169
|
+
};
|
|
170
|
+
const showStatus = (text, source) => {
|
|
171
|
+
const changed = text !== currentStatus;
|
|
172
|
+
currentStatus = text;
|
|
173
|
+
currentSource = text ? source : 'default';
|
|
174
|
+
if (text && source === 'comment')
|
|
175
|
+
commentShownAt = Date.now();
|
|
176
|
+
if (changed) {
|
|
177
|
+
logStatus(text, source);
|
|
178
|
+
setThreadStatus(msg, text);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
113
181
|
// Keepalive every 3 seconds (Slack clears after ~2 min timeout)
|
|
114
182
|
let failures = 0;
|
|
115
183
|
const interval = setInterval(async () => {
|
|
@@ -125,14 +193,39 @@ export function startTypingIndicator(msg, statusText = 'is thinking...') {
|
|
|
125
193
|
}
|
|
126
194
|
}, 3000);
|
|
127
195
|
return {
|
|
128
|
-
updateStatus: (newStatus) => {
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
196
|
+
updateStatus: (newStatus, source = 'comment') => {
|
|
197
|
+
if (stopped)
|
|
198
|
+
return;
|
|
199
|
+
if (!newStatus || source === 'comment') {
|
|
200
|
+
cancelHold();
|
|
201
|
+
showStatus(newStatus, source);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const remainingHold = config.TYPING_COMMENT_MIN_HOLD_MS - (Date.now() - commentShownAt);
|
|
205
|
+
if (currentSource === 'comment' && remainingHold > 0) {
|
|
206
|
+
pendingCanned = { text: newStatus, source };
|
|
207
|
+
if (holdTimer === undefined) {
|
|
208
|
+
holdTimer = setTimeout(() => {
|
|
209
|
+
holdTimer = undefined;
|
|
210
|
+
const pending = pendingCanned;
|
|
211
|
+
pendingCanned = null;
|
|
212
|
+
if (!stopped && pending)
|
|
213
|
+
showStatus(pending.text, pending.source);
|
|
214
|
+
}, remainingHold);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
132
217
|
}
|
|
218
|
+
cancelHold();
|
|
219
|
+
showStatus(newStatus, source);
|
|
133
220
|
},
|
|
134
221
|
stop: () => {
|
|
222
|
+
if (stopped)
|
|
223
|
+
return;
|
|
224
|
+
stopped = true;
|
|
225
|
+
cancelHold();
|
|
135
226
|
clearInterval(interval);
|
|
227
|
+
if (currentStatus)
|
|
228
|
+
logStatus('', 'default');
|
|
136
229
|
setThreadStatus(msg, '');
|
|
137
230
|
},
|
|
138
231
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface StartupAuthLogger {
|
|
2
|
+
warn(message: string): void;
|
|
3
|
+
error(message: string): void;
|
|
4
|
+
}
|
|
5
|
+
export declare class UnknownStartupFailureError extends Error {
|
|
6
|
+
readonly attempts: number;
|
|
7
|
+
constructor(attempts: number);
|
|
8
|
+
}
|
|
9
|
+
/** The only formatter used for startup retry and terminal failure logs. */
|
|
10
|
+
export declare function redactStartupText(text: string, configuredTokens?: readonly string[]): string;
|
|
11
|
+
export declare function startupFailureDetail(error: unknown, operation: string, configuredTokens?: readonly string[]): string;
|
|
12
|
+
export declare function isPermanentStartupFailure(error: unknown): boolean;
|
|
13
|
+
/** Returns false when shutdown cancels startup. Permanent errors propagate to the caller. */
|
|
14
|
+
export declare function retryStartupOperation(operation: string, run: () => Promise<void>, signal: AbortSignal, log: StartupAuthLogger, random?: () => number, configuredTokens?: readonly string[]): Promise<boolean>;
|
|
15
|
+
export declare function retryStartupAuth(authenticate: () => Promise<void>, signal: AbortSignal, log: StartupAuthLogger, random?: () => number, configuredTokens?: readonly string[]): Promise<boolean>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { AuthHttpError, StartupConfigurationError } from './auth-provider.js';
|
|
2
|
+
export class UnknownStartupFailureError extends Error {
|
|
3
|
+
attempts;
|
|
4
|
+
constructor(attempts) {
|
|
5
|
+
super(`unclassified startup failure after ${attempts} attempts`);
|
|
6
|
+
this.attempts = attempts;
|
|
7
|
+
this.name = 'UnknownStartupFailureError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const NETWORK_CODES = new Set([
|
|
11
|
+
'EAI_AGAIN', 'ENOTFOUND', 'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT',
|
|
12
|
+
'EHOSTUNREACH', 'ENETUNREACH', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT',
|
|
13
|
+
]);
|
|
14
|
+
function isTransient(error) {
|
|
15
|
+
if (error instanceof AuthHttpError)
|
|
16
|
+
return error.status === 429 || error.status >= 500;
|
|
17
|
+
if (error instanceof StartupConfigurationError)
|
|
18
|
+
return false;
|
|
19
|
+
if (!(error instanceof Error))
|
|
20
|
+
return false;
|
|
21
|
+
if (error.name === 'AbortError' || error.name === 'TimeoutError')
|
|
22
|
+
return true;
|
|
23
|
+
if (error instanceof TypeError && /fetch failed|network|timeout/i.test(error.message))
|
|
24
|
+
return true;
|
|
25
|
+
if (/could not resolve host|could not resolve hostname|temporary failure in name resolution|failed to connect|connection (timed out|refused|reset)|network is unreachable|(?:HTTP|response|returned error:?)\s*(?:429|5\d\d)\b/i.test(error.message))
|
|
26
|
+
return true;
|
|
27
|
+
let cause = error;
|
|
28
|
+
for (let depth = 0; depth < 4 && cause && typeof cause === 'object'; depth++) {
|
|
29
|
+
const code = cause.code;
|
|
30
|
+
if (typeof code === 'string' && NETWORK_CODES.has(code))
|
|
31
|
+
return true;
|
|
32
|
+
cause = cause.cause;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/** The only formatter used for startup retry and terminal failure logs. */
|
|
37
|
+
export function redactStartupText(text, configuredTokens = []) {
|
|
38
|
+
let safe = text
|
|
39
|
+
.replace(/https?:\/\/[^\s/@]+@/gi, 'https://[REDACTED]@')
|
|
40
|
+
.replace(/\b(?:Authorization\s*:\s*)?Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
|
|
41
|
+
.replace(/\bx-access-token\s*:\s*[^\s/@,;]+/gi, 'x-access-token:[REDACTED]');
|
|
42
|
+
for (const token of configuredTokens) {
|
|
43
|
+
if (token)
|
|
44
|
+
safe = safe.split(token).join('[REDACTED]');
|
|
45
|
+
}
|
|
46
|
+
return safe;
|
|
47
|
+
}
|
|
48
|
+
export function startupFailureDetail(error, operation, configuredTokens = []) {
|
|
49
|
+
let detail;
|
|
50
|
+
if (error instanceof AuthHttpError)
|
|
51
|
+
detail = `HTTP ${error.status}`;
|
|
52
|
+
else if (error instanceof StartupConfigurationError)
|
|
53
|
+
detail = 'invalid startup configuration';
|
|
54
|
+
else if (error instanceof UnknownStartupFailureError)
|
|
55
|
+
detail = error.message;
|
|
56
|
+
else if (operation === 'BaseBrain') {
|
|
57
|
+
const code = error && typeof error === 'object' ? error.code : undefined;
|
|
58
|
+
const exit = typeof code === 'number' ? code : 'unknown';
|
|
59
|
+
const message = error instanceof Error ? error.message : '';
|
|
60
|
+
const category = /could not resolve|name resolution|ENOTFOUND|EAI_AGAIN/i.test(message) ? 'DNS failure'
|
|
61
|
+
: /timed out|timeout|ETIMEDOUT/i.test(message) ? 'connection timeout'
|
|
62
|
+
: /failed to connect|connection refused|ECONNREFUSED/i.test(message) ? 'connection failure'
|
|
63
|
+
: /authentication failed|permission denied|403|401/i.test(message) ? 'authentication failure'
|
|
64
|
+
: 'git failure';
|
|
65
|
+
detail = `${category} (exit ${exit})`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
let code = error;
|
|
69
|
+
for (let depth = 0; depth < 4 && code && typeof code === 'object'; depth++) {
|
|
70
|
+
const value = code.code;
|
|
71
|
+
if (typeof value === 'string' && NETWORK_CODES.has(value)) {
|
|
72
|
+
detail = value;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
code = code.cause;
|
|
76
|
+
}
|
|
77
|
+
detail ??= error instanceof TypeError && /fetch failed/i.test(error.message) ? 'fetch failed'
|
|
78
|
+
: error instanceof Error && ['AbortError', 'TimeoutError'].includes(error.name) ? error.name
|
|
79
|
+
: 'unclassified error';
|
|
80
|
+
}
|
|
81
|
+
return redactStartupText(detail ?? 'unclassified error', configuredTokens);
|
|
82
|
+
}
|
|
83
|
+
export function isPermanentStartupFailure(error) {
|
|
84
|
+
return error instanceof StartupConfigurationError ||
|
|
85
|
+
(error instanceof AuthHttpError && error.status !== 429 && error.status < 500);
|
|
86
|
+
}
|
|
87
|
+
function waitForBackoff(ms, signal) {
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
return Promise.resolve(false);
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const finish = (ready) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
signal.removeEventListener('abort', onAbort);
|
|
94
|
+
resolve(ready);
|
|
95
|
+
};
|
|
96
|
+
const onAbort = () => finish(false);
|
|
97
|
+
const timer = setTimeout(() => finish(true), ms);
|
|
98
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
99
|
+
if (signal.aborted)
|
|
100
|
+
onAbort();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Returns false when shutdown cancels startup. Permanent errors propagate to the caller. */
|
|
104
|
+
export async function retryStartupOperation(operation, run, signal, log, random = Math.random, configuredTokens = []) {
|
|
105
|
+
let attempt = 0;
|
|
106
|
+
while (!signal.aborted) {
|
|
107
|
+
attempt++;
|
|
108
|
+
try {
|
|
109
|
+
await run();
|
|
110
|
+
return !signal.aborted;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (signal.aborted)
|
|
114
|
+
return false;
|
|
115
|
+
if (isPermanentStartupFailure(error))
|
|
116
|
+
throw error;
|
|
117
|
+
const transient = isTransient(error);
|
|
118
|
+
if (!transient && attempt >= 5) {
|
|
119
|
+
log.error(`[${operation}] attempt ${attempt} failed: ${startupFailureDetail(error, operation, configuredTokens)}; unclassified failure limit reached`);
|
|
120
|
+
throw new UnknownStartupFailureError(attempt);
|
|
121
|
+
}
|
|
122
|
+
const baseMs = Math.min(1000 * 2 ** Math.min(attempt - 1, 6), 60_000);
|
|
123
|
+
const delayMs = Math.min(60_000, Math.round(baseMs * (1 + 0.2 * random())));
|
|
124
|
+
const message = `[${operation}] attempt ${attempt} failed: ${startupFailureDetail(error, operation, configuredTokens)}; retrying in ${delayMs}ms`;
|
|
125
|
+
if (transient)
|
|
126
|
+
log.warn(message);
|
|
127
|
+
else
|
|
128
|
+
log.error(message);
|
|
129
|
+
if (!await waitForBackoff(delayMs, signal))
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
export function retryStartupAuth(authenticate, signal, log, random = Math.random, configuredTokens = []) {
|
|
136
|
+
return retryStartupOperation('StartupAuth', authenticate, signal, log, random, configuredTokens);
|
|
137
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamvibe/poller",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2-rc.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@teamvibe/poller",
|
|
9
|
-
"version": "0.8.
|
|
9
|
+
"version": "0.8.2-rc.2",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-dynamodb": "3.1108.0",
|
|
12
12
|
"@aws-sdk/client-sqs": "3.1108.0",
|