feat(hn-monitor): unify Slack + Telegram into single dual-transport agent#88
Conversation
…gent Replaces the separate hn-monitor and hn-monitor-telegram directories with a single agent that delivers to Slack, Telegram, or both — configuration- driven via SLACK_CHANNEL and TELEGRAM_CHAT inputs (both optional). Uses the new @agentworkforce/delivery package for unified messaging with non-blocking parentRef threading (zero receipt round-trips, cloud-side ordering). Backports Telegram-specific improvements: - fetchWithTimeout (8s AbortController) - withTimeout for LLM calls - Pending thread body recovery on partial failure Shared helpers (input, list, withTimeout, fetchWithTimeout) now come from @agentworkforce/delivery instead of being copy-pasted.
📝 WalkthroughWalkthroughThe HN monitor now uses a transport-agnostic delivery abstraction to send digests and answers to multiple Slack and/or Telegram targets simultaneously. Q&A is handled via provider-specific message parsing (Telegram events and relay inbox DMs), with answers routed only to the originating transport. Fresh story posting uses non-blocking delivery to thread digests under published headers, and stores pending thread bodies in durable state when sends partially fail, enabling retry on subsequent cron ticks. Digest generation is wrapped with timeout-backed LLM calls that fall back to plain story titles on error. Tests now cover success, fallback, header-failure rollback, partial-failure recovery, and Q&A routing across both posting and Q&A flows. ChangesHN monitor multi-target delivery
Sequence Diagram(s)sequenceDiagram
participant Cron
participant Agent
participant Memory
participant LLM
participant Delivery
Cron->>Agent: scheduled scan
Agent->>Memory: load pending thread body
Agent->>Delivery: retry pending body if present
Agent->>Memory: claim seen IDs before publish
Agent->>LLM: summarize fresh stories with timeout
LLM-->>Agent: header and body or fallback
Agent->>Delivery: publish header
Delivery-->>Agent: header ref
Agent->>Delivery: send body replyTo header (nonBlocking)
alt all targets succeed
Agent->>Memory: persist post digest
else partial failure
Agent->>Memory: save pending thread body record
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request refactors the Hacker News monitor to support multi-platform delivery (Slack, Telegram, and Relay) using the @agentworkforce/delivery package, and introduces a pending thread body recovery mechanism to handle partial delivery failures. Feedback on the changes highlights a bug in parseRelayMessage where accessing payload.data is redundant and breaks Relay Q&A, and notes that routing for Relay inbox DMs is missing from the main handler. Additionally, it is recommended to store and pass the replyTo references in the pending thread body state to ensure retried bodies are correctly threaded under their original headers.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function parseRelayMessage(payload: unknown): ParsedMessage | null { | ||
| const data = asRecord((payload as { data?: Record<string, unknown> })?.data); | ||
| const nested = (data?.message && typeof data.message === 'object' ? data.message : {}) as Record<string, unknown>; | ||
| const text = str(data?.text) ?? str(nested.text) ?? ''; | ||
| if (!text.trim()) return null; | ||
| return { text: text.trim(), provider: 'relay' }; | ||
| } |
There was a problem hiding this comment.
In handleQaMessage, payload.data is passed directly to parseRelayMessage. Therefore, the payload parameter in parseRelayMessage is already the inner data record, not the outer event payload. Accessing payload.data again results in undefined, which completely breaks the Relay Q&A path. We should parse payload directly as the record.
| function parseRelayMessage(payload: unknown): ParsedMessage | null { | |
| const data = asRecord((payload as { data?: Record<string, unknown> })?.data); | |
| const nested = (data?.message && typeof data.message === 'object' ? data.message : {}) as Record<string, unknown>; | |
| const text = str(data?.text) ?? str(nested.text) ?? ''; | |
| if (!text.trim()) return null; | |
| return { text: text.trim(), provider: 'relay' }; | |
| } | |
| function parseRelayMessage(payload: unknown): ParsedMessage | null { | |
| const data = asRecord(payload); | |
| if (!data) return null; | |
| const nested = asRecord(data.message) ?? {}; | |
| const text = str(data.text) ?? str(nested.text) ?? ''; | |
| if (!text.trim()) return null; | |
| return { text: text.trim(), provider: 'relay' }; | |
| } |
| // Q&A path: telegram message | ||
| if (typeof event.type === 'string' && event.type.startsWith('telegram.')) { | ||
| await handleQaMessage(ctx, event as unknown as AgentEvent, 'telegram'); | ||
| return; | ||
| } | ||
| // Cron path | ||
| if (!isCronTickEvent(event as unknown as AgentEvent)) return; |
There was a problem hiding this comment.
The routing for Relay inbox DM messages (relaycast.message) is missing from the handler, meaning the agent will silently ignore any incoming Relay DMs. We should route relaycast.message events to handleQaMessage with the 'relay' provider.
// Q&A path: telegram message or relay DM
if (typeof event.type === 'string' && event.type.startsWith('telegram.')) {
await handleQaMessage(ctx, event as unknown as AgentEvent, 'telegram');
return;
}
if (event.type === 'relaycast.message') {
await handleQaMessage(ctx, event as unknown as AgentEvent, 'relay');
return;
}
// Cron path
if (!isCronTickEvent(event as unknown as AgentEvent)) return;| interface PendingThreadBody { | ||
| targets: string; | ||
| header: string; | ||
| body: string; | ||
| createdAt: string; | ||
| stories: Array<{ title: string; url: string; points: number }>; | ||
| } |
There was a problem hiding this comment.
To ensure that the retried body is correctly threaded under the original header on recovery, we should store the header's delivery references (replyTo) in the PendingThreadBody state.
| interface PendingThreadBody { | |
| targets: string; | |
| header: string; | |
| body: string; | |
| createdAt: string; | |
| stories: Array<{ title: string; url: string; points: number }>; | |
| } | |
| interface PendingThreadBody { | |
| targets: string; | |
| header: string; | |
| body: string; | |
| createdAt: string; | |
| stories: Array<{ title: string; url: string; points: number }>; | |
| replyTo?: any; | |
| } |
| pending = { | ||
| targets: delivery.targets.join(','), | ||
| header, | ||
| body, | ||
| createdAt: new Date().toISOString(), | ||
| stories: fresh.map((s) => ({ title: s.title, url: s.url, points: s.points })) | ||
| }; |
There was a problem hiding this comment.
Include the heads delivery result as replyTo in the saved pending state so that the retry attempt can thread the body correctly.
| pending = { | |
| targets: delivery.targets.join(','), | |
| header, | |
| body, | |
| createdAt: new Date().toISOString(), | |
| stories: fresh.map((s) => ({ title: s.title, url: s.url, points: s.points })) | |
| }; | |
| pending = { | |
| targets: delivery.targets.join(','), | |
| header, | |
| body, | |
| createdAt: new Date().toISOString(), | |
| stories: fresh.map((s) => ({ title: s.title, url: s.url, points: s.points })), | |
| replyTo: heads | |
| }; |
| const configuredTargets = delivery.targets.join(','); | ||
| if (pending.targets !== configuredTargets) return false; | ||
|
|
||
| const bodyResult = await delivery.send(pending.body, { nonBlocking: true }); |
There was a problem hiding this comment.
Pass the stored replyTo reference to delivery.send so that the retried body is threaded under the original header instead of being posted as a standalone message.
| const bodyResult = await delivery.send(pending.body, { nonBlocking: true }); | |
| const bodyResult = await delivery.send(pending.body, { replyTo: pending.replyTo, nonBlocking: true }); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 48da94e8b6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "evals:live": "npm run compile && node scripts/evals/run-evals.mjs --live --judge" | ||
| }, | ||
| "dependencies": { | ||
| "@agentworkforce/delivery": "file:../workforce/packages/delivery", |
There was a problem hiding this comment.
Depend on a resolvable delivery package
This adds a dependency on a sibling directory outside the repository. In a fresh checkout of this agents repo, ../workforce/packages/delivery is not present, so installs/CI/deploys cannot resolve @agentworkforce/delivery and hn-monitor/agent.ts cannot compile. Please depend on a published package version or include the package in this workspace instead of a local file path.
Useful? React with 👍 / 👎.
| return; | ||
| } | ||
| // Cron path | ||
| if (!isCronTickEvent(event as unknown as AgentEvent)) return; |
There was a problem hiding this comment.
Route relay inbox messages before dropping non-cron events
When this persona receives a relay inbox DM, the event is a relaycast.message while hn-monitor/persona.ts still declares relay: { inbox: ['@self'] }. The new handler only routes telegram.* events and then silently returns for every non-cron event here, so the existing DM Q&A path advertised by the persona stops working, especially for Slack-only deployments. Please keep a relay inbox branch before this guard.
Useful? React with 👍 / 👎.
| // Deliver answer to all configured targets (non-blocking for Q&A replies) | ||
| const delivery = createDelivery(ctx); | ||
| if (delivery.targets.length > 0) { | ||
| await delivery.publish(answer.trim() || 'No answer available.'); |
There was a problem hiding this comment.
Reply only to the Telegram chat that asked
For a Telegram message from any chat in the mounted /telegram/chats/** scope, this publishes the generated answer to all configured delivery targets instead of verifying the message came from TELEGRAM_CHAT and replying to that source message. Because parseTelegramMessage drops the chat/message id, a question in an unrelated Telegram chat can be copied into the configured Slack/Telegram digest destinations. Please parse the chat id/message id, apply the TELEGRAM_CHAT guard, and reply to that message rather than broadcasting.
Useful? React with 👍 / 👎.
|
pr-reviewer could not complete review for #88 in AgentWorkforce/agents. |
|
pr-reviewer could not complete review for #88 in AgentWorkforce/agents. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
tests/hn-monitor.test.mjs (1)
139-173: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftAdd a recovery-path test that asserts threaded retry uses preserved
replyTo.Current suite stops at pending-state persistence. Please add a follow-up test for retry behavior (same targets) that verifies the body is re-sent under the original header and that pending state is cleared only after successful retry.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/hn-monitor.test.mjs` around lines 139 - 173, The current tests cover only saving pending state after a partial failure, but they do not verify the recovery path for a retry. Add a follow-up test around postFreshStories using the same delivery target flow to assert that the threaded body resend preserves the original replyTo from the published header, and that the pending-thread-body state is removed only after the retry succeeds. Use the existing fakeCtx, delivery.publish, and delivery.send patterns to verify the retry behavior end to end.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hn-monitor/agent.ts`:
- Around line 108-119: The Agent handler in agent.ts only routes Q&A traffic
when event.type starts with telegram., so relay/slack Q&A events never reach
handleQaMessage or the later parsing branches. Update the dispatcher in handler
to recognize the relay/slack event types alongside telegram before the cron
check, and make sure the existing Q&A parsing path in handleQaMessage is invoked
for those events rather than being dropped as non-cron noise.
- Around line 202-206: The Q&A reply path in createDelivery currently publishes
to every configured target, which causes in-origin questions to be mirrored
elsewhere. Update the answer delivery flow so the publish call only sends back
to the originating transport/context for the current request, and keep the
non-blocking behavior for replies; use createDelivery and delivery.publish as
the main places to adjust the routing logic.
- Around line 247-248: The pending-vs-configured target comparison is
order-sensitive because `delivery.targets.join(',')` preserves array order,
which can cause false retry mismatches. Update the target key generation in the
relevant `agent.ts` flow (where `targets` and `header` are assembled, and the
same logic is reused later) to canonicalize the targets before storing or
comparing them, such as by sorting or otherwise normalizing the collection
first. Ensure both the pending and configured target keys use the same
normalized representation so `slack,telegram` and `telegram,slack` compare
equal.
- Around line 47-53: The PendingThreadBody interface is missing the thread
linkage references needed for recovery. Add `heads` and `replyTo` fields to the
PendingThreadBody interface to preserve the thread context when pending body
state is persisted after header success. Then update the pending body state
creation (in the sections around lines 243-253 and 292-303) to include these
thread references when saving, and ensure that when delivery.send(...) is called
during retry, it receives the replyTo information from the recovered pending
body so the body can be properly attached to the original header message.
---
Nitpick comments:
In `@tests/hn-monitor.test.mjs`:
- Around line 139-173: The current tests cover only saving pending state after a
partial failure, but they do not verify the recovery path for a retry. Add a
follow-up test around postFreshStories using the same delivery target flow to
assert that the threaded body resend preserves the original replyTo from the
published header, and that the pending-thread-body state is removed only after
the retry succeeds. Use the existing fakeCtx, delivery.publish, and
delivery.send patterns to verify the retry behavior end to end.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6c7e6275-27be-4797-ad94-87fad5ad4958
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (4)
hn-monitor/agent.tshn-monitor/persona.tspackage.jsontests/hn-monitor.test.mjs
| triggers: { | ||
| telegram: [{ on: 'message' }] | ||
| }, | ||
| handler: async (ctx, event) => { | ||
| // Chat path: a relay DM arrived — answer questions about what we've posted. | ||
| if (isRelaycastMessageEvent(event)) { | ||
| await handleInboxMessage(ctx, event); | ||
| // Q&A path: telegram message | ||
| if (typeof event.type === 'string' && event.type.startsWith('telegram.')) { | ||
| await handleQaMessage(ctx, event as unknown as AgentEvent, 'telegram'); | ||
| return; | ||
| } | ||
| // Cron path | ||
| if (!isCronTickEvent(event as unknown as AgentEvent)) return; | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Relay/Slack Q&A events are currently unreachable.
The handler only dispatches handleQaMessage(...) for telegram.*; any non-telegram, non-cron event is dropped. That makes the relay/slack parsing branches dead for live traffic.
Suggested fix
handler: async (ctx, event) => {
- // Q&A path: telegram message
- if (typeof event.type === 'string' && event.type.startsWith('telegram.')) {
- await handleQaMessage(ctx, event as unknown as AgentEvent, 'telegram');
- return;
- }
+ if (typeof event.type === 'string') {
+ if (event.type.startsWith('telegram.')) {
+ await handleQaMessage(ctx, event as unknown as AgentEvent, 'telegram');
+ return;
+ }
+ if (event.type.startsWith('relay.')) {
+ await handleQaMessage(ctx, event as unknown as AgentEvent, 'relay');
+ return;
+ }
+ if (event.type.startsWith('slack.')) {
+ await handleQaMessage(ctx, event as unknown as AgentEvent, 'slack');
+ return;
+ }
+ }
// Cron path
if (!isCronTickEvent(event as unknown as AgentEvent)) return;Also applies to: 151-163
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hn-monitor/agent.ts` around lines 108 - 119, The Agent handler in agent.ts
only routes Q&A traffic when event.type starts with telegram., so relay/slack
Q&A events never reach handleQaMessage or the later parsing branches. Update the
dispatcher in handler to recognize the relay/slack event types alongside
telegram before the cron check, and make sure the existing Q&A parsing path in
handleQaMessage is invoked for those events rather than being dropped as
non-cron noise.
|
Clean — Review of PR #88 —
|
|
I made no edits to the working tree (nothing to discard — git status is clean). Here is my review. PR #88 Review —
|
f23b1d4 to
48da94e
Compare
- Fix sendToTargets cross-post leakage: now uses createDelivery(onlyTargets) - Fix threaded body permanently lost on hard throw (build pending before send) - Fix retryPendingThreadBody partial-failure inconsistency (check refs.length) - Fix orphaned pending body (clear on targets mismatch) - Add createDelivery(onlyTargets) parameter for transport-scoped delivery
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
shared/delivery/delivery.ts (1)
121-136: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNon-blocking partial failures are silently swallowed.
In non-blocking mode
ok = refs.length > 0, so when one target succeeds and another throws (errorspopulated,ok=true), the warning at line 127 (if (!ok && errors.length > 0)) never fires. The consumer inpostFreshStoriesdetects the missing ref viarefs.length < targets.length, but the underlying error reason is never logged anywhere, making partial-delivery debugging difficult.Consider emitting the warning whenever
errors.length > 0, independent ofok.♻️ Proposed change
- if (!ok && errors.length > 0) { + if (errors.length > 0) { this.ctx.log?.('warn', 'delivery.partial-failure', { errors, nonBlocking }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@shared/delivery/delivery.ts` around lines 121 - 136, In non-blocking mode, partial failures where some targets succeed and others throw are not being logged because the warning condition checks both !ok and errors.length > 0, but in non-blocking mode ok evaluates to true when refs.length > 0 regardless of errors. Remove the !ok condition from the warning check on the line with this.ctx.log?.('warn', 'delivery.partial-failure', { errors, nonBlocking }) so that the warning is emitted whenever errors.length > 0, independent of the ok flag, ensuring partial delivery failures are always logged for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@shared/delivery/delivery.ts`:
- Around line 218-243: In the sendTelegram method, the condition on line 233
skips error handling for non-blocking sends, allowing execution to continue to
line 241 where messageId becomes an empty string when result.ok is false (which
occurs in non-blocking mode due to writebackTimeoutMs: 0). This breaks reply
threading when the returned ref is used as a parent. Unlike the Slack
implementation (lines 191-217) which embeds parentRef.draftRef directly in the
message body to ensure threading works independently of receipt timing, Telegram
relies entirely on messageId from the receipt. You need to either embed the
parent reference information directly in the message text similar to the Slack
approach, or ensure that a valid messageId is returned for non-blocking sends
that can be used for subsequent reply threading instead of returning an empty
string.
In `@shared/delivery/types.ts`:
- Around line 17-22: The TelegramRef interface is missing a draftRef field that
is needed for cloud-side threading support in non-blocking mode. Add a draftRef
field to the TelegramRef interface (optional string type similar to how SlackRef
has it), then update the sendTelegram function to populate this draftRef field
when creating the TelegramRef object in the same way that sendSlackNonBlocking
populates draftRef. This will ensure that threading works correctly in
non-blocking mode when messageId is initially empty, by allowing the draftRef to
be used as the parent reference instead of relying on the empty messageId.
---
Nitpick comments:
In `@shared/delivery/delivery.ts`:
- Around line 121-136: In non-blocking mode, partial failures where some targets
succeed and others throw are not being logged because the warning condition
checks both !ok and errors.length > 0, but in non-blocking mode ok evaluates to
true when refs.length > 0 regardless of errors. Remove the !ok condition from
the warning check on the line with this.ctx.log?.('warn',
'delivery.partial-failure', { errors, nonBlocking }) so that the warning is
emitted whenever errors.length > 0, independent of the ok flag, ensuring partial
delivery failures are always logged for debugging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a538aedf-060e-4355-8f3e-9d1af58af628
📒 Files selected for processing (6)
hn-monitor/agent.tsshared/delivery/delivery.tsshared/delivery/helpers.tsshared/delivery/index.tsshared/delivery/types.tstests/hn-monitor.test.mjs
✅ Files skipped from review due to trivial changes (2)
- shared/delivery/index.ts
- shared/delivery/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- hn-monitor/agent.ts
| export interface TelegramRef { | ||
| provider: 'telegram'; | ||
| chatId: string; | ||
| /** Delivered message id (set after the writeback receipt arrives). */ | ||
| messageId: string; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
fd -t f "types.ts" | grep -i deliveryRepository: AgentWorkforce/agents
Length of output: 184
🏁 Script executed:
fd -t f "delivery.ts"Repository: AgentWorkforce/agents
Length of output: 187
🏁 Script executed:
# Find the shared directory structure
git ls-files shared/delivery/Repository: AgentWorkforce/agents
Length of output: 264
🏁 Script executed:
cat -n shared/delivery/types.tsRepository: AgentWorkforce/agents
Length of output: 5161
🏁 Script executed:
cat -n shared/delivery/delivery.ts | head -300Repository: AgentWorkforce/agents
Length of output: 9816
🏁 Script executed:
# Check for replyTo and parentRef usage
rg -n "replyTo|parentRef" shared/delivery/ -A 2 -B 2Repository: AgentWorkforce/agents
Length of output: 7011
TelegramRef lacks a draftRef field for cloud-side threading in non-blocking mode.
Unlike SlackRef (which carries draftRef for embedding in the message body as parentRef), TelegramRef only has messageId. In non-blocking mode, messageId is set to an empty string (see line 241: messageId: result.ok ? result.messageId : ''). When this empty messageId is later used as a parent ref, the condition at line 229 (parentRef?.messageId) fails, and replyToMessageId is never set. This breaks threading for Telegram in the non-blocking publish pattern, whereas Slack threading works correctly because it relies on draftRef embedded in the message body.
Add a draftRef field to TelegramRef and populate it in sendTelegram (similar to how sendSlackNonBlocking populates it) to support the documented non-blocking threading pattern.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@shared/delivery/types.ts` around lines 17 - 22, The TelegramRef interface is
missing a draftRef field that is needed for cloud-side threading support in
non-blocking mode. Add a draftRef field to the TelegramRef interface (optional
string type similar to how SlackRef has it), then update the sendTelegram
function to populate this draftRef field when creating the TelegramRef object in
the same way that sendSlackNonBlocking populates draftRef. This will ensure that
threading works correctly in non-blocking mode when messageId is initially
empty, by allowing the draftRef to be used as the parent reference instead of
relying on the empty messageId.
- Remove @agentworkforce/delivery file: dependency from package.json (now vendored in shared/delivery/ — no external dep needed) - Remove pnpm-lock.yaml (was added by earlier commit, not tracked on main) - Export retryPendingThreadBody for testing - Add recovery-path tests: verify retry threads under original headerRefs, verifies orphaned pending body is cleared on targets mismatch
Replace vendored shared/delivery/ with npm dependency on @agentworkforce/delivery ^0.1.0 (now published).
Summary
Replaces the separate
hn-monitorandhn-monitor-telegramdirectories with a single agent that delivers to Slack, Telegram, or both — configuration-driven viaSLACK_CHANNELandTELEGRAM_CHATinputs (both optional).Depends on workforce#250 for the
@agentworkforce/deliverypackage.Changes
createDeliveryfrom@agentworkforce/delivery, auto-detects configured transportsfetchWithTimeout(8s AbortController),withTimeoutfor LLM, pending thread body recoveryinput,list,withTimeout,fetchWithTimeout) now imported from@agentworkforce/deliveryslackandtelegramintegrations, both inputsoptional: trueWhat this enables
Once
@agentworkforce/deliveryis published, the same unification pattern can be applied to the remaining 3 paired agents (inbox-buddy, joke-bot, spotify-releases).Summary by cubic
Unifies
hn-monitorinto one agent that sends Hacker News digests to Slack, Telegram, or both using the published@agentworkforce/delivery. Adds non‑blocking header publish, transport‑scoped Q&A (relay and Telegram), and reliable recovery for threaded posts.New Features
agent.tsusingcreateDeliveryfrom@agentworkforce/delivery; auto-detectsSLACK_CHANNEL/TELEGRAM_CHAT.replyTo; bounded timeouts for HN fetch (8s) and LLM.Bug Fixes
createDelivery(onlyTargets)to prevent cross‑post leakage.retryPendingThreadBody.Written for commit 05d6135. Summary will update on new commits.