@teamvibe/poller 0.7.2 → 0.8.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.
@@ -72,10 +72,17 @@ export async function checkForUpdate() {
72
72
  logger.info(`[AutoUpdate] Already on latest version (${POLLER_VERSION})`);
73
73
  return false;
74
74
  }
75
- // Check if update type is within policy
76
- const policyAllows = config.AUTO_UPDATE_POLICY === 'minor'
77
- ? updateType === 'patch' || updateType === 'minor'
78
- : updateType === 'patch'; // policy === 'patch'
75
+ // Check if update type is within policy. Explicit 'off' branch (tv#312):
76
+ // startAutoUpdateCron() never schedules this job at all when policy is
77
+ // 'off', so this path is currently unreachable in practice — kept
78
+ // explicit so a future caller of checkForUpdate() outside the cron
79
+ // (manual trigger, control message) can't silently apply patch updates
80
+ // to a poller configured not to auto-update.
81
+ const policyAllows = config.AUTO_UPDATE_POLICY === 'off'
82
+ ? false
83
+ : config.AUTO_UPDATE_POLICY === 'minor'
84
+ ? updateType === 'patch' || updateType === 'minor'
85
+ : updateType === 'patch'; // policy === 'patch'
79
86
  if (!policyAllows) {
80
87
  logger.info(`[AutoUpdate] ${updateType} update available (${POLLER_VERSION} → ${latestVersion}) but policy is "${config.AUTO_UPDATE_POLICY}", skipping`);
81
88
  return false;
@@ -212,6 +212,7 @@ export interface SpawnStdinPlan {
212
212
  export declare function buildSpawnStdinPlan(prompt: string, inlineImages: TeamVibeQueueMessage['inlineImages'], now?: number): SpawnStdinPlan;
213
213
  export declare function extractAssistantText(event: any): string | undefined;
214
214
  export declare function assistantCalledTool(event: any, toolName: string): boolean;
215
+ export declare function resolveTypingStatus(event: any, assistantText?: string): string | null;
215
216
  /**
216
217
  * Decides whether a spawn attempt should report its session ID back to the
217
218
  * caller (poller.ts) for persistence to DynamoDB via updateSessionId().
@@ -699,6 +699,50 @@ export function assistantCalledTool(event, toolName) {
699
699
  }
700
700
  return event.message.content.some((c) => c?.type === 'tool_use' && c.name === toolName);
701
701
  }
702
+ // Status text is a one-line UI label, not a chat message — cap it well short
703
+ // of a full sentence.
704
+ const TYPING_STATUS_MAX_LEN = 100;
705
+ // tv#369: decide what the typing indicator should show for a given
706
+ // stream-json event. Prefers Claude's own narration text over a generic
707
+ // placeholder — surfaces the same "what I'm doing / found" signal the CLI's
708
+ // own transcript view already shows. Falls back to a per-tool canned status
709
+ // (tv#291) when the assistant event carries a tool_use block but no
710
+ // narration text, and to 'Thinking...' when neither is present. Reads
711
+ // tool_use blocks from `event.message.content[]` rather than a top-level
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) {
721
+ if (event?.type !== 'assistant' || !Array.isArray(event.message?.content)) {
722
+ return null;
723
+ }
724
+ // The caller clears the indicator when this tool sends a reply. Do not
725
+ // restore a status from that same event, even if it also contains narration.
726
+ if (assistantCalledTool(event, 'mcp__slack__send_message'))
727
+ return null;
728
+ const text = assistantText ?? extractAssistantText(event);
729
+ if (text) {
730
+ // Collapse newlines/whitespace runs so a multi-line narration doesn't
731
+ // land a literal `\n` in the one-line status surface.
732
+ const oneLine = text.replace(/\s+/g, ' ').trim();
733
+ return oneLine.length > TYPING_STATUS_MAX_LEN
734
+ ? oneLine.slice(0, TYPING_STATUS_MAX_LEN - 1) + '…'
735
+ : oneLine;
736
+ }
737
+ for (const block of event.message.content) {
738
+ if (block?.type !== 'tool_use' || typeof block.name !== 'string')
739
+ continue;
740
+ const status = getStatusForTool(block.name);
741
+ if (status)
742
+ return status;
743
+ }
744
+ return 'Thinking...';
745
+ }
702
746
  async function runClaudeCode(msg, sessionLog, cwd, brainPath, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
703
747
  const slackContext = msg.response_context.slack;
704
748
  const isCronMessage = msg.source === 'cron';
@@ -1004,18 +1048,11 @@ async function runClaudeCode(msg, sessionLog, cwd, brainPath, sessionId, isFirst
1004
1048
  scheduleProgressNotifications(progressNotificationMarks(minutes));
1005
1049
  }
1006
1050
  }
1007
- // Auto-update typing indicator status based on tool usage
1051
+ // Auto-update typing indicator status based on tool usage (tv#369, tv#291)
1008
1052
  if (typingIndicator) {
1009
- if (event.type === 'tool_use' && event.name) {
1010
- const status = getStatusForTool(event.name);
1011
- if (status !== null && status !== '') {
1012
- typingIndicator.updateStatus(status);
1013
- }
1014
- // When status is '' (e.g. set_status tool), don't override — Claude is managing it
1015
- }
1016
- else if (event.type === 'assistant') {
1017
- // Reset to "Thinking..." when Claude is reasoning between tool calls
1018
- typingIndicator.updateStatus('Thinking...');
1053
+ const status = resolveTypingStatus(event, assistantText);
1054
+ if (status) {
1055
+ typingIndicator.updateStatus(status);
1019
1056
  }
1020
1057
  }
1021
1058
  }
package/dist/config.js CHANGED
@@ -65,8 +65,12 @@ const configSchema = z.object({
65
65
  POLLER_TZ: z.string().optional(),
66
66
  MAINTENANCE_CRON: z.string().default('30 3 * * *'), // Daily at 03:30 in POLLER_TZ (or host TZ)
67
67
  MAINTENANCE_ENABLED: z.string().default('true').transform((v) => v.toLowerCase() === 'true'),
68
- // Auto-update settings
69
- AUTO_UPDATE_POLICY: z.enum(['patch', 'minor', 'off']).default('patch'),
68
+ // Auto-update settings — defaults to 'off': most pollers run as pinned
69
+ // Railway container images (see Dockerfile), where an in-process
70
+ // `npm install -g` update can't survive the next redeploy anyway. Set
71
+ // explicitly (e.g. AUTO_UPDATE_POLICY=patch) for bare-metal/VM installs
72
+ // that actually benefit from self-updating.
73
+ AUTO_UPDATE_POLICY: z.enum(['patch', 'minor', 'off']).default('off'),
70
74
  AUTO_UPDATE_SCHEDULE: z.string().default('03:00'), // HH:MM in UTC — daily check time
71
75
  // Inbox watcher (teamvibe.ai#242): fs.watch()'s .inbox/ per tracked brain so a
72
76
  // file dropped while no session is running (e.g. a background-task result)
package/dist/poller.js CHANGED
@@ -219,6 +219,14 @@ export function describeFailureReason(killedByPoller, exitCode) {
219
219
  // content an agent forgot to send via send_message) still delivers as
220
220
  // before, so this can't silently drop a forgotten reply.
221
221
  export const END_OF_TURN_NO_FURTHER_RESPONSE_SENTINEL = 'NO_FURTHER_RESPONSE_NEEDED';
222
+ // tv#358 (revised): the sentinel agents emit and the guard matches is
223
+ // unchanged — still the exact ALL_CAPS literal above, still an exact-match
224
+ // suppression. What changes is purely cosmetic: poller-brain#428's leak
225
+ // failure mode (a sentence written before the sentinel breaks the exact
226
+ // match, so the whole thing delivers as-is) showed the raw literal reads as
227
+ // jarring when it does surface. Swap it for a quieter display token
228
+ // wherever it appears in text that's actually being delivered.
229
+ const END_OF_TURN_SENTINEL_DISPLAY = '[silent]';
222
230
  export function endOfTurnFallbackText(hasSlackContext, result) {
223
231
  if (!hasSlackContext || !result.success || result.sawSendMessageCall) {
224
232
  return undefined;
@@ -227,7 +235,7 @@ export function endOfTurnFallbackText(hasSlackContext, result) {
227
235
  if (!text || text === END_OF_TURN_NO_FURTHER_RESPONSE_SENTINEL) {
228
236
  return undefined;
229
237
  }
230
- return text;
238
+ return text.split(END_OF_TURN_NO_FURTHER_RESPONSE_SENTINEL).join(END_OF_TURN_SENTINEL_DISPLAY);
231
239
  }
232
240
  export async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
233
241
  if (activeLoops.has(threadId)) {