Skip to content

feat(hn-monitor): unify Slack + Telegram into single dual-transport agent#89

Merged
khaliqgant merged 8 commits into
mainfrom
feat/unified-hn-monitor
Jun 23, 2026
Merged

feat(hn-monitor): unify Slack + Telegram into single dual-transport agent#89
khaliqgant merged 8 commits into
mainfrom
feat/unified-hn-monitor

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the separate hn-monitor and hn-monitor-telegram agents with a single dual-transport agent. Delivers to Slack, Telegram, or both — configuration-driven via SLACK_CHANNEL and TELEGRAM_CHAT inputs (both optional).

Depends on workforce#250 for @agentworkforce/delivery (now published as ^0.1.0).

Changes

  • Single agent.ts — uses createDelivery from @agentworkforce/delivery, auto-detects configured transports
  • Non-blocking parentRef threading — header published with 0ms timeout, body threaded via parentRef (x-reply-radar pattern, zero receipt round-trips)
  • Backported Telegram improvements: fetchWithTimeout (8s AbortController), withTimeout for LLM, pending thread body recovery with headerRefs for proper retry threading
  • Shared helpers (input, list, withTimeout, fetchWithTimeout) from @agentworkforce/delivery
  • Persona declares both slack and telegram integrations, both inputs optional: true
  • Q&A routes relay inbox DMs and Telegram messages through shared handler, replies only to origin transport
  • Recovery tests verify pending body retry preserves threading context
  • Removed hn-monitor-telegram/ directory (now superseded)
  • 161/161 tests pass, typecheck clean

Summary by cubic

Unifies the Slack and Telegram HN monitors into one agent that posts to Slack, Telegram, or both via @agentworkforce/delivery. Adds non-blocking threading with robust recovery and prevents cross-post or partial-publish failures.

  • New Features

    • Config via SLACK_CHANNEL and TELEGRAM_CHAT (optional) with unified, non-blocking parentRef threading; adds fetchWithTimeout (8s) and withTimeout to prevent hangs.
    • Durable recovery: saves headerRefs and retries threaded bodies on the next scan; unified Q&A for relay DMs and Telegram, replying only on the origin transport.
  • Bug Fixes

    • Treats partial header publishes as failures; builds pending state before sending the body; clears orphaned pending bodies when targets change; fixes cross-post leakage with transport-scoped delivery.
    • Removes hn-monitor-telegram/ and superseded Telegram tests; updates imports; uses published @agentworkforce/delivery@^0.1.0 (matches main).

Written for commit 2784722. Summary will update on new commits.

Review in cubic

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The hn-monitor agent is refactored from a Slack-only implementation to a unified multi-transport agent using a new @agentworkforce/delivery package. The separate hn-monitor-telegram agent (persona, agent, README) is deleted and its behaviors are consolidated into hn-monitor. The persona gains optional Telegram inputs, handleQaMessage replaces handleInboxMessage, and digest posting uses delivery targets with durable pending-thread retry.

Changes

Unified HN monitor transport flow

Layer / File(s) Summary
Dependency, persona, and module transport setup
package.json, hn-monitor/persona.ts
Adds the @agentworkforce/delivery workspace package, expands persona integrations to mount both Slack and Telegram routing, makes SLACK_CHANNEL optional and adds a new optional TELEGRAM_CHAT input, generalizes the systemPrompt, and replaces Slack-only imports in the agent with delivery utilities and Telegram parsing helpers.
Unified handler dispatch and Q&A routing
hn-monitor/agent.ts
Introduces PendingThreadBody and ParsedMessage types, relay message parsing helpers, and a Telegram message trigger. Implements handleQaMessage with provider-specific payload extraction, digest recall from durable memory, LLM prompt assembly, withTimeout-wrapped completion with fallback, and reply scoped to the origin transport's delivery targets.
Delivery-based digest posting and pending-thread capture
hn-monitor/agent.ts
Rewrites postFreshStories to publish a header across all delivery targets then thread the digest body non-blockingly via replyTo. On partial failure, serializes headerRefs into a PendingThreadBody durable record. Adds retryPendingThreadBody with target validation and header-ref reconstruction. Adjusts summarize, removes locally defined helpers in favor of imported ones, fixes loadPosts to skip malformed records and sort by postedAt, and adds loadPendingThreadBody/savePendingThreadBody/clearPendingThreadBody.
Test rewrite and hn-monitor-telegram removal
tests/hn-monitor.test.mjs, tests/telegram-agents.test.mjs, hn-monitor-telegram/*
Replaces the hn-monitor test harness with fakeCtx/fakeDelivery, rewrites posting tests for success/LLM-fallback/header-failure/partial-failure paths, migrates Q&A tests to handleQaMessage, and adds retryPendingThreadBody recovery tests. Deletes hn-monitor-telegram/agent.ts, hn-monitor-telegram/persona.ts, hn-monitor-telegram/README.md, and removes the hn-monitor-telegram test block from telegram-agents.test.mjs.

Sequence Diagram

sequenceDiagram
  actor User
  participant Trigger as Telegram/Relay Trigger
  participant handleQaMessage
  participant memory as Durable Memory
  participant LLM as LLM withTimeout
  participant delivery as DeliveryClient

  User->>Trigger: sends question
  Trigger->>handleQaMessage: event with provider context
  handleQaMessage->>memory: recall hn-monitor:post (limit 60)
  memory-->>handleQaMessage: recent digests
  handleQaMessage->>LLM: prompt built from digests + question
  LLM-->>handleQaMessage: answer text (or fallback on timeout)
  handleQaMessage->>delivery: send answer to origin-provider targets only

  rect rgba(100, 180, 100, 0.5)
    Note over delivery,memory: Cron posting path
    delivery->>memory: save hn-monitor:seen
    delivery->>delivery: publish header to all targets
    delivery->>delivery: send threaded body (replyTo headerRefs)
    alt all succeed
      delivery->>memory: save hn-monitor:post
    else partial failure
      delivery->>memory: save hn-monitor:pending-thread-body
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/agents#51: Both PRs modify hn-monitor/agent.ts cron posting flow to claim/persist "seen" story IDs before publishing the digest.
  • AgentWorkforce/agents#86: This PR removes the hn-monitor-telegram persona and agent that was originally added in the referenced PR, directly reversing those additions.
  • AgentWorkforce/agents#88: Overlaps directly with the core hn-monitor/agent.ts refactor adding handleQaMessage and switching postFreshStories/retryPendingThreadBody to DeliveryClient-based dual-transport threading.

Poem

🐇 Hoppin' through the code with care,
Two transports merged beyond compare,
Slack and Telegram, now as one,
The telegram agent's work is done!
Durable threads that never break—
This bunny baked a unified cake. 🎂

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: unifying Slack and Telegram agents into a single dual-transport agent.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description clearly explains the unification of two agents into a single dual-transport agent with detailed changes and objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/unified-hn-monitor

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6108a7e4c3

ℹ️ 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".

Comment thread hn-monitor/agent.ts
// Retain the digest so a user can DM the agent and ask about recent posts.
// ttlDays (30) on memory ages these out, giving a rolling ~30-day window.
// Thread the body under each header, also non-blocking.
const bodyResult = await delivery.send(body, { replyTo: heads, nonBlocking: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Save pending state before the body send can throw

When delivery.send rejects after the header has already been published, pending is still null, so the catch block logs hn-monitor.thread-incomplete but does not save hn-monitor:pending-thread-body; the stories remain claimed as seen and the threaded body is never retried. This affects hard delivery failures, which the comment immediately above this block explicitly expects to recover from.

Useful? React with 👍 / 👎.

Comment thread hn-monitor/agent.ts Outdated
Comment on lines +265 to +267
if (heads.refs.length === 0) {
throw new Error(`Header publish failed across all targets`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat partial header publishes as incomplete

When both Slack and Telegram are configured and delivery.publish(header) returns a ref for only one target, this check accepts the header as successful. The saved headerRefs then only contain the successful target, and recovery only retries the body, so the other target cannot receive a threaded digest under its missing header; the body path already treats refs.length < delivery.targets.length as a partial failure and the header needs the same handling.

Useful? React with 👍 / 👎.

Comment thread package.json
"evals:live": "npm run compile && node scripts/evals/run-evals.mjs --live --judge"
},
"dependencies": {
"@agentworkforce/delivery": "^0.1.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the lockfile with the new dependency

This adds @agentworkforce/delivery to package.json but leaves package-lock.json unchanged (rg '@agentworkforce/delivery' package-lock.json finds nothing). The npm-ci docs state that if lockfile dependencies do not match package.json, npm ci exits with an error instead of updating the lock, so CI/deploy installs that use the checked-in lockfile will fail until the lockfile is regenerated and committed.

Useful? React with 👍 / 👎.

@@ -1,353 +0,0 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the stale hn-monitor-telegram test imports

Deleting this module leaves tests/telegram-agents.test.mjs:20 importing ../.test-build/hn-monitor-telegram/agent.js, while npm test runs every tests/*.test.mjs after compiling only the remaining */agent.ts files. In any environment with dependencies installed, the test suite will fail with a module-not-found error because this deleted agent is no longer emitted.

Useful? React with 👍 / 👎.

agent-relay-code Bot added a commit that referenced this pull request Jun 23, 2026
@agent-relay-code

Copy link
Copy Markdown
Contributor

Review: PR #89feat/unified-hn-monitor

Summary of the PR

This PR finishes consolidating the standalone hn-monitor-telegram agent into the unified dual-transport hn-monitor agent:

  • Deletes hn-monitor-telegram/ (README, agent.ts, persona.ts, 3 PNGs).
  • Removes the hn-monitor-telegram test block from tests/telegram-agents.test.mjs and repoints its import to the unified hn-monitor.
  • Tightens one guard in hn-monitor/agent.ts:265: the header-publish check now also fails when heads.refs.length < delivery.targets.length (partial-target failure), not just on total failure (=== 0).

Note: .workforce/pr.diff was truncated at 180000 bytes by the deleted PNG binary patches, so I traced the real text changes via the checked-out repo (baseSha..headSha).

Findings

1. Correctness of the header-guard change (hn-monitor/agent.ts:265) — sound, no change needed.
delivery.targets is ReadonlyArray<'slack'|'telegram'>, so heads.refs.length < delivery.targets.length correctly detects when the header reached fewer transports than configured. This makes the header check symmetric with the existing body check at agent.ts:288. It moves in the fail-closed direction (it errors/recovers more often), so I did not modify it.

2. Behavioral observation on partial header failure (advisory — left as comment, not changed).
With multiple targets, a partial header success now throws into the !headerPosted branch (agent.ts:301), which releases the seen-claim and rethrows for a full retry. On retry the header re-publishes to all targets, so a target that already received the header gets a duplicate. This is the same fail-closed tradeoff the code already made for total failure, and it is the explicit intent of the commit ("catch partial header failures") — it prefers re-posting over dropping stories. This is a deliberate safety default; I left it unchanged and flag it only for human awareness. (Contrast: the body partial-failure path at agent.ts:288 saves pending state for targeted recovery instead of full retry.)

3. Dead import (mechanical) — fixed.
The PR repointed tests/telegram-agents.test.mjs:20 to import postFreshStories, retryPendingThreadBody from hn-monitor, but it deleted every test that used them, leaving the import unused. Removed it (non-semantic; the proper tests live in tests/hn-monitor.test.mjs). Fixed in tests/telegram-agents.test.mjs:19 (line removed).

4. No stale references. Grep for hn-monitor-telegram across the repo returns nothing — the deletion is complete.

Verification

  • Build: tsc compile and tsc --noEmit typecheck both pass clean.
  • tests/hn-monitor.test.mjs: 8/8 pass (covers the changed partial-failure path).
  • tests/telegram-agents.test.mjs: 14/15 pass after my edit (unchanged by my edit).
  • Full npm test: 156/160 pass. The 4 failures are all inbox-buddy / inbox-buddy-telegram mount-reading tests ("threads from the mount", threadsLoaded 0 vs 4). The PR touches no inbox-buddy code; these fail because the installed @relayfile/relay-helpers@0.4.2 mount-reader doesn't see the seeded temp mount in this sandbox — environmental/pre-existing, not caused by this PR or my edit.
  • Install note: npm install fails out-of-the-box because the published @agentworkforce/delivery@0.1.0 declares "@agentworkforce/runtime": "workspace:*" (a broken publish). This is pre-existing (base SHA depends on the same ^0.1.0) and unrelated to this PR. I worked around it locally to run the suite and restored package-lock.json so no install artifact is left in the tree.

Addressed comments

  • No reviewer or bot comments were provided. .workforce/ contains only pr.diff, changed-files.txt, and context.json, and context.json carries no review threads — nothing to reconcile.

Advisory Notes

  • The published @agentworkforce/delivery@0.1.0 package ships a workspace:* runtime dependency, which breaks npm install in any non-workspace consumer. This is outside this PR's scope (dependency was already present at base) — worth a follow-up republish of that package, but it does not belong in this PR.
  • The 4 inbox-buddy mount-reading test failures appear environment-specific to this sandbox. If they also fail in real CI, that's a separate issue from this PR and warrants its own investigation; folding a fix here would be out of scope.

Working tree

Only one change remains staged for the push: the dead-import removal in tests/telegram-agents.test.mjs. package-lock.json was restored; no build artifacts left behind.

I'm not printing READY: the full suite is not green in this environment (4 unrelated inbox-buddy mount failures), and I cannot confirm the real CI/merge state from here — that requires human/post-harness verification of whether those failures reproduce in CI.

…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.
- 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
- 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).
… tests

- Check heads.refs.length < targets.length for partial header publishes
- Remove hn-monitor-telegram tests (superseded by hn-monitor.test.mjs)
- Fix stale import in telegram-agents.test.mjs
@khaliqgant khaliqgant force-pushed the feat/unified-hn-monitor branch from 6f6e881 to a618a40 Compare June 23, 2026 20:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hn-monitor/agent.ts (1)

462-470: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate parsed post records before sorting.

A valid JSON value like null, [], or a string bypasses the catch at Line 466 and can still crash Line 470 when sorting by postedAt. Skip non-record values the same way malformed JSON is skipped.

Proposed shape guard
   for (const item of items) {
     try {
-      posts.push(JSON.parse(item.content) as PostRecord);
+      const parsed = JSON.parse(item.content);
+      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+        posts.push(parsed as PostRecord);
+      }
     } catch {
       // skip malformed records
     }
   }
🤖 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 462 - 470, After successfully parsing JSON
in the try block within the loop that processes items, add a validation check to
ensure the parsed value is actually a valid PostRecord object before pushing it
to the posts array. Currently, valid JSON values like null, empty arrays, or
strings can pass the try-catch block but will cause issues when the sort
operation on line 470 attempts to access the postedAt property. Add a type guard
or shape validation that skips any parsed values that don't match the expected
PostRecord structure, treating them the same way malformed JSON is handled in
the catch block.
🧹 Nitpick comments (2)
tests/hn-monitor.test.mjs (2)

282-349: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cover retry partial-failure behavior explicitly.

Please add a test where delivery.send returns ok: false or fewer refs than targets, then assert pending state is not cleared and hn-monitor:post is not saved.

Example test shape
+test('retryPendingThreadBody keeps pending when retry send fails', async () => {
+  // arrange pending recall
+  // delivery.send -> { ok: false, refs: [] }
+  const result = await retryPendingThreadBody(ctx, delivery);
+  assert.equal(result, true);
+  assert.equal(
+    saved.some((s) => s.content === 'null' && s.opts?.tags?.includes('hn-monitor:pending-thread-body')),
+    false
+  );
+  assert.equal(saved.some((s) => s.opts?.tags?.includes('hn-monitor:post')), false);
+});
🤖 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 282 - 349, Add a new test case after
the existing retryPendingThreadBody test that covers the partial-failure
scenario. In this new test, configure the delivery.send method to return ok:
false or return fewer refs than expected targets, then assert that the pending
state is NOT cleared (i.e., no saved call with content 'null' and tag
'hn-monitor:pending-thread-body') and the post is NOT saved (i.e., no saved call
with tag 'hn-monitor:post'). Use the same context and memory setup structure as
the existing test, but modify the delivery.send response to simulate a failure
condition.

183-279: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add Telegram-path Q&A regression coverage.

The Q&A suite only exercises the relay branch. The Telegram branch has separate parsing/gating and should have at least one happy-path test plus one skip-path test (e.g., wrong chat or bot echo) to protect unified transport behavior.

🤖 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 183 - 279, Add two new regression
tests for the Telegram branch of the Q&A functionality to match the existing
relay coverage. First, create a happy-path test for Telegram similar to the
existing relay test that calls handleQaMessage with 'telegram' as the channel
parameter instead of 'relay', using a mock delivery configured for Telegram
targets. Second, add a skip-path test for Telegram (such as a message from the
bot itself or a message in the wrong chat) that verifies the function logs and
returns early without calling memory.recall or llm.complete, using the same
pattern as the existing "no text" skip-path test but with Telegram-specific
conditions to trigger the early exit.
🤖 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 264-268: The error handling for the delivery.publish call does not
distinguish between complete failure and partial success. Currently, when
heads.refs.length is greater than zero but less than delivery.targets.length
(partial publish), the code throws an error before setting headerPosted to true,
causing the catch block to roll back the seen state and rethrow as if no refs
were published. This leads to duplicate header publishes on retry. Split the
condition to only throw an error when heads.refs.length equals zero (complete
failure), and set headerPosted to true whenever at least one header ref exists
(partial or complete success). This prevents the rollback/rethrow logic from
undoing progress on targets that already received the header. Also apply the
same fix to the equivalent code block that also appears around lines 301-305.
- Around line 357-362: The delivery.send call for pending.body is not wrapped in
exception handling, allowing transient failures to escape and terminate the cron
invocation instead of treating them as recoverable pending state. Wrap the const
bodyResult = await delivery.send(pending.body, bodyOpts) call in a try-catch
block to catch any exceptions thrown during the send operation. When an
exception is caught, log the error appropriately and return true to mark the
body as still pending for the next scheduled retry attempt, consistent with how
postFreshStories handles send failures.
- Around line 220-229: The relay DM handler currently falls back to all
configured delivery targets when Slack is not available, which violates
origin-transport isolation by potentially sending relay-origin questions to
Telegram. In the provider === 'relay' block, the targets assignment on line 223
should not spread all delivery targets as a fallback when Slack is absent.
Instead, ensure relay DMs are only delivered via Slack by keeping targets as
['slack'] regardless of what other targets are configured, or skip delivery
entirely if Slack is not available. Remove the fallback behavior
`[...delivery.targets]` and maintain strict origin-scoped routing for relay
messages.
- Around line 159-169: The code does not validate that TELEGRAM_CHAT is
configured before processing Telegram messages. When TELEGRAM_CHAT is empty or
undefined, telegramSkipReason is called with an undefined value, which fails to
properly filter wrong chat messages. Add an early return check after getting the
TELEGRAM_CHAT value with input(ctx, 'TELEGRAM_CHAT') to verify it is configured;
if it is not configured (empty or falsy), return immediately to skip the entire
Telegram Q&A processing path, ensuring the persona behavior of skipping Telegram
delivery when the config is not set.

In `@package.json`:
- Line 17: The dependency entry for `@agentworkforce/delivery` in package.json is
using a file protocol pointing to a relative sibling directory
(../workforce/packages/delivery), which breaks in CI/cloud environments and
contradicts the migration to a published package. Replace the file protocol
reference with a proper published semver version range (such as "^X.Y.Z") for
the `@agentworkforce/delivery` package, or use the workspace protocol if this
repository manages that package. Ensure the dependency resolves to the published
package rather than a local file path.

---

Outside diff comments:
In `@hn-monitor/agent.ts`:
- Around line 462-470: After successfully parsing JSON in the try block within
the loop that processes items, add a validation check to ensure the parsed value
is actually a valid PostRecord object before pushing it to the posts array.
Currently, valid JSON values like null, empty arrays, or strings can pass the
try-catch block but will cause issues when the sort operation on line 470
attempts to access the postedAt property. Add a type guard or shape validation
that skips any parsed values that don't match the expected PostRecord structure,
treating them the same way malformed JSON is handled in the catch block.

---

Nitpick comments:
In `@tests/hn-monitor.test.mjs`:
- Around line 282-349: Add a new test case after the existing
retryPendingThreadBody test that covers the partial-failure scenario. In this
new test, configure the delivery.send method to return ok: false or return fewer
refs than expected targets, then assert that the pending state is NOT cleared
(i.e., no saved call with content 'null' and tag
'hn-monitor:pending-thread-body') and the post is NOT saved (i.e., no saved call
with tag 'hn-monitor:post'). Use the same context and memory setup structure as
the existing test, but modify the delivery.send response to simulate a failure
condition.
- Around line 183-279: Add two new regression tests for the Telegram branch of
the Q&A functionality to match the existing relay coverage. First, create a
happy-path test for Telegram similar to the existing relay test that calls
handleQaMessage with 'telegram' as the channel parameter instead of 'relay',
using a mock delivery configured for Telegram targets. Second, add a skip-path
test for Telegram (such as a message from the bot itself or a message in the
wrong chat) that verifies the function logs and returns early without calling
memory.recall or llm.complete, using the same pattern as the existing "no text"
skip-path test but with Telegram-specific conditions to trigger the early exit.
🪄 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: 96a0c56e-9818-43eb-b1ac-703d6b1b99bc

📥 Commits

Reviewing files that changed from the base of the PR and between a0577f5 and 6f6e881.

⛔ Files ignored due to path filters (3)
  • hn-monitor-telegram/avatar.png is excluded by !**/*.png
  • hn-monitor-telegram/banner.png is excluded by !**/*.png
  • hn-monitor-telegram/card.png is excluded by !**/*.png
📒 Files selected for processing (8)
  • hn-monitor-telegram/README.md
  • hn-monitor-telegram/agent.ts
  • hn-monitor-telegram/persona.ts
  • hn-monitor/agent.ts
  • hn-monitor/persona.ts
  • package.json
  • tests/hn-monitor.test.mjs
  • tests/telegram-agents.test.mjs
💤 Files with no reviewable changes (4)
  • hn-monitor-telegram/README.md
  • hn-monitor-telegram/agent.ts
  • hn-monitor-telegram/persona.ts
  • tests/telegram-agents.test.mjs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
hn-monitor/agent.ts (1)

462-470: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate parsed post records before sorting.

A valid JSON value like null, [], or a string bypasses the catch at Line 466 and can still crash Line 470 when sorting by postedAt. Skip non-record values the same way malformed JSON is skipped.

Proposed shape guard
   for (const item of items) {
     try {
-      posts.push(JSON.parse(item.content) as PostRecord);
+      const parsed = JSON.parse(item.content);
+      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+        posts.push(parsed as PostRecord);
+      }
     } catch {
       // skip malformed records
     }
   }
🤖 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 462 - 470, After successfully parsing JSON
in the try block within the loop that processes items, add a validation check to
ensure the parsed value is actually a valid PostRecord object before pushing it
to the posts array. Currently, valid JSON values like null, empty arrays, or
strings can pass the try-catch block but will cause issues when the sort
operation on line 470 attempts to access the postedAt property. Add a type guard
or shape validation that skips any parsed values that don't match the expected
PostRecord structure, treating them the same way malformed JSON is handled in
the catch block.
🧹 Nitpick comments (2)
tests/hn-monitor.test.mjs (2)

282-349: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cover retry partial-failure behavior explicitly.

Please add a test where delivery.send returns ok: false or fewer refs than targets, then assert pending state is not cleared and hn-monitor:post is not saved.

Example test shape
+test('retryPendingThreadBody keeps pending when retry send fails', async () => {
+  // arrange pending recall
+  // delivery.send -> { ok: false, refs: [] }
+  const result = await retryPendingThreadBody(ctx, delivery);
+  assert.equal(result, true);
+  assert.equal(
+    saved.some((s) => s.content === 'null' && s.opts?.tags?.includes('hn-monitor:pending-thread-body')),
+    false
+  );
+  assert.equal(saved.some((s) => s.opts?.tags?.includes('hn-monitor:post')), false);
+});
🤖 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 282 - 349, Add a new test case after
the existing retryPendingThreadBody test that covers the partial-failure
scenario. In this new test, configure the delivery.send method to return ok:
false or return fewer refs than expected targets, then assert that the pending
state is NOT cleared (i.e., no saved call with content 'null' and tag
'hn-monitor:pending-thread-body') and the post is NOT saved (i.e., no saved call
with tag 'hn-monitor:post'). Use the same context and memory setup structure as
the existing test, but modify the delivery.send response to simulate a failure
condition.

183-279: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add Telegram-path Q&A regression coverage.

The Q&A suite only exercises the relay branch. The Telegram branch has separate parsing/gating and should have at least one happy-path test plus one skip-path test (e.g., wrong chat or bot echo) to protect unified transport behavior.

🤖 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 183 - 279, Add two new regression
tests for the Telegram branch of the Q&A functionality to match the existing
relay coverage. First, create a happy-path test for Telegram similar to the
existing relay test that calls handleQaMessage with 'telegram' as the channel
parameter instead of 'relay', using a mock delivery configured for Telegram
targets. Second, add a skip-path test for Telegram (such as a message from the
bot itself or a message in the wrong chat) that verifies the function logs and
returns early without calling memory.recall or llm.complete, using the same
pattern as the existing "no text" skip-path test but with Telegram-specific
conditions to trigger the early exit.
🤖 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 264-268: The error handling for the delivery.publish call does not
distinguish between complete failure and partial success. Currently, when
heads.refs.length is greater than zero but less than delivery.targets.length
(partial publish), the code throws an error before setting headerPosted to true,
causing the catch block to roll back the seen state and rethrow as if no refs
were published. This leads to duplicate header publishes on retry. Split the
condition to only throw an error when heads.refs.length equals zero (complete
failure), and set headerPosted to true whenever at least one header ref exists
(partial or complete success). This prevents the rollback/rethrow logic from
undoing progress on targets that already received the header. Also apply the
same fix to the equivalent code block that also appears around lines 301-305.
- Around line 357-362: The delivery.send call for pending.body is not wrapped in
exception handling, allowing transient failures to escape and terminate the cron
invocation instead of treating them as recoverable pending state. Wrap the const
bodyResult = await delivery.send(pending.body, bodyOpts) call in a try-catch
block to catch any exceptions thrown during the send operation. When an
exception is caught, log the error appropriately and return true to mark the
body as still pending for the next scheduled retry attempt, consistent with how
postFreshStories handles send failures.
- Around line 220-229: The relay DM handler currently falls back to all
configured delivery targets when Slack is not available, which violates
origin-transport isolation by potentially sending relay-origin questions to
Telegram. In the provider === 'relay' block, the targets assignment on line 223
should not spread all delivery targets as a fallback when Slack is absent.
Instead, ensure relay DMs are only delivered via Slack by keeping targets as
['slack'] regardless of what other targets are configured, or skip delivery
entirely if Slack is not available. Remove the fallback behavior
`[...delivery.targets]` and maintain strict origin-scoped routing for relay
messages.
- Around line 159-169: The code does not validate that TELEGRAM_CHAT is
configured before processing Telegram messages. When TELEGRAM_CHAT is empty or
undefined, telegramSkipReason is called with an undefined value, which fails to
properly filter wrong chat messages. Add an early return check after getting the
TELEGRAM_CHAT value with input(ctx, 'TELEGRAM_CHAT') to verify it is configured;
if it is not configured (empty or falsy), return immediately to skip the entire
Telegram Q&A processing path, ensuring the persona behavior of skipping Telegram
delivery when the config is not set.

In `@package.json`:
- Line 17: The dependency entry for `@agentworkforce/delivery` in package.json is
using a file protocol pointing to a relative sibling directory
(../workforce/packages/delivery), which breaks in CI/cloud environments and
contradicts the migration to a published package. Replace the file protocol
reference with a proper published semver version range (such as "^X.Y.Z") for
the `@agentworkforce/delivery` package, or use the workspace protocol if this
repository manages that package. Ensure the dependency resolves to the published
package rather than a local file path.

---

Outside diff comments:
In `@hn-monitor/agent.ts`:
- Around line 462-470: After successfully parsing JSON in the try block within
the loop that processes items, add a validation check to ensure the parsed value
is actually a valid PostRecord object before pushing it to the posts array.
Currently, valid JSON values like null, empty arrays, or strings can pass the
try-catch block but will cause issues when the sort operation on line 470
attempts to access the postedAt property. Add a type guard or shape validation
that skips any parsed values that don't match the expected PostRecord structure,
treating them the same way malformed JSON is handled in the catch block.

---

Nitpick comments:
In `@tests/hn-monitor.test.mjs`:
- Around line 282-349: Add a new test case after the existing
retryPendingThreadBody test that covers the partial-failure scenario. In this
new test, configure the delivery.send method to return ok: false or return fewer
refs than expected targets, then assert that the pending state is NOT cleared
(i.e., no saved call with content 'null' and tag
'hn-monitor:pending-thread-body') and the post is NOT saved (i.e., no saved call
with tag 'hn-monitor:post'). Use the same context and memory setup structure as
the existing test, but modify the delivery.send response to simulate a failure
condition.
- Around line 183-279: Add two new regression tests for the Telegram branch of
the Q&A functionality to match the existing relay coverage. First, create a
happy-path test for Telegram similar to the existing relay test that calls
handleQaMessage with 'telegram' as the channel parameter instead of 'relay',
using a mock delivery configured for Telegram targets. Second, add a skip-path
test for Telegram (such as a message from the bot itself or a message in the
wrong chat) that verifies the function logs and returns early without calling
memory.recall or llm.complete, using the same pattern as the existing "no text"
skip-path test but with Telegram-specific conditions to trigger the early exit.
🪄 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: 96a0c56e-9818-43eb-b1ac-703d6b1b99bc

📥 Commits

Reviewing files that changed from the base of the PR and between a0577f5 and 6f6e881.

⛔ Files ignored due to path filters (3)
  • hn-monitor-telegram/avatar.png is excluded by !**/*.png
  • hn-monitor-telegram/banner.png is excluded by !**/*.png
  • hn-monitor-telegram/card.png is excluded by !**/*.png
📒 Files selected for processing (8)
  • hn-monitor-telegram/README.md
  • hn-monitor-telegram/agent.ts
  • hn-monitor-telegram/persona.ts
  • hn-monitor/agent.ts
  • hn-monitor/persona.ts
  • package.json
  • tests/hn-monitor.test.mjs
  • tests/telegram-agents.test.mjs
💤 Files with no reviewable changes (4)
  • hn-monitor-telegram/README.md
  • hn-monitor-telegram/agent.ts
  • hn-monitor-telegram/persona.ts
  • tests/telegram-agents.test.mjs
🛑 Comments failed to post (5)
hn-monitor/agent.ts (4)

159-169: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Skip Telegram Q&A when TELEGRAM_CHAT is not configured.

With TELEGRAM_CHAT empty, telegramSkipReason(msg, undefined) does not reject wrong chats, so any non-bot Telegram message can be loaded into the LLM path even though the persona says empty Telegram config skips Telegram delivery.

Proposed gating fix
     const msg = readTelegramMessage(payload.data);
     if (!msg) return;
+    const telegramChat = input(ctx, 'TELEGRAM_CHAT');
+    if (!telegramChat) {
+      ctx.log('info', 'hn-monitor.qa.skip', { reason: 'telegram not configured' });
+      return;
+    }
     // Gate: skip bot echoes, wrong chat, empty text
-    const reason = telegramSkipReason(msg, input(ctx, 'TELEGRAM_CHAT'));
+    const reason = telegramSkipReason(msg, telegramChat);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  if (provider === 'telegram') {
    const payload = expanded as { data?: unknown };
    if (!payload.data) return;
    const msg = readTelegramMessage(payload.data);
    if (!msg) return;
    const telegramChat = input(ctx, 'TELEGRAM_CHAT');
    if (!telegramChat) {
      ctx.log('info', 'hn-monitor.qa.skip', { reason: 'telegram not configured' });
      return;
    }
    // Gate: skip bot echoes, wrong chat, empty text
    const reason = telegramSkipReason(msg, telegramChat);
    if (reason) {
      ctx.log('info', `hn-monitor.qa.skip reason=${reason.replace(/\s+/g, '-')}`);
      return;
    }
🤖 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 159 - 169, The code does not validate that
TELEGRAM_CHAT is configured before processing Telegram messages. When
TELEGRAM_CHAT is empty or undefined, telegramSkipReason is called with an
undefined value, which fails to properly filter wrong chat messages. Add an
early return check after getting the TELEGRAM_CHAT value with input(ctx,
'TELEGRAM_CHAT') to verify it is configured; if it is not configured (empty or
falsy), return immediately to skip the entire Telegram Q&A processing path,
ensuring the persona behavior of skipping Telegram delivery when the config is
not set.

220-229: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not fall back from relay DMs to Telegram.

Line 223 sends relay-origin Q&A replies to every configured target when Slack is absent, so a private relay inbox question can be answered in Telegram. That breaks the origin-transport isolation called out by this PR.

Proposed origin-scoped fix
     if (provider === 'relay') {
       // Relay DMs: reply to Slack if configured (legacy behavior).
-      // If only Telegram is configured, reply there instead.
-      const targets: Array<'slack' | 'telegram'> = delivery.targets.includes('slack') ? ['slack'] : [...delivery.targets];
+      // Do not mirror relay-origin questions into Telegram.
+      if (!delivery.targets.includes('slack')) {
+        ctx.log('warn', 'hn-monitor.qa.no-origin-target', { provider: 'relay' });
+        return;
+      }
+      const targets: Array<'slack'> = ['slack'];
🤖 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 220 - 229, The relay DM handler currently
falls back to all configured delivery targets when Slack is not available, which
violates origin-transport isolation by potentially sending relay-origin
questions to Telegram. In the provider === 'relay' block, the targets assignment
on line 223 should not spread all delivery targets as a fallback when Slack is
absent. Instead, ensure relay DMs are only delivered via Slack by keeping
targets as ['slack'] regardless of what other targets are configured, or skip
delivery entirely if Slack is not available. Remove the fallback behavior
`[...delivery.targets]` and maintain strict origin-scoped routing for relay
messages.

264-268: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Handle partial header publishes as landed state.

Line 265 throws on a partial header publish (refs.length > 0 && refs.length < targets.length) before headerPosted is set. The catch block then restores seen and rethrows as if nothing landed, so a runtime retry can duplicate headers on targets that already returned refs. Split zero-ref failure from partial-ref failure, and avoid the rollback/rethrow path once any header ref exists.

Also applies to: 301-305

🤖 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 264 - 268, The error handling for the
delivery.publish call does not distinguish between complete failure and partial
success. Currently, when heads.refs.length is greater than zero but less than
delivery.targets.length (partial publish), the code throws an error before
setting headerPosted to true, causing the catch block to roll back the seen
state and rethrow as if no refs were published. This leads to duplicate header
publishes on retry. Split the condition to only throw an error when
heads.refs.length equals zero (complete failure), and set headerPosted to true
whenever at least one header ref exists (partial or complete success). This
prevents the rollback/rethrow logic from undoing progress on targets that
already received the header. Also apply the same fix to the equivalent code
block that also appears around lines 301-305.

357-362: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Catch pending body retry send exceptions.

postFreshStories treats thrown delivery.send() calls as recoverable pending state, but the retry path lets the same exception escape. A transient send failure can fail the whole cron invocation and trigger runtime retries instead of leaving the pending body for the next scheduled attempt.

Proposed retry guard
-  const bodyResult = await delivery.send(pending.body, bodyOpts);
+  let bodyResult: DeliveryResult;
+  try {
+    bodyResult = await delivery.send(pending.body, bodyOpts);
+  } catch (error) {
+    ctx.log('error', 'hn-monitor.pending-body-retry-failed', {
+      targets: configuredTargets,
+      error: String(error)
+    });
+    return true;
+  }
   // Match postFreshStories: ALL targets must receive refs for success.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  let bodyResult: DeliveryResult;
  try {
    bodyResult = await delivery.send(pending.body, bodyOpts);
  } catch (error) {
    ctx.log('error', 'hn-monitor.pending-body-retry-failed', {
      targets: configuredTargets,
      error: String(error)
    });
    return true;
  }
  // Match postFreshStories: ALL targets must receive refs for success.
  if (!bodyResult.ok || bodyResult.refs.length < delivery.targets.length) {
    ctx.log('error', 'hn-monitor.pending-body-retry-failed', { targets: configuredTargets });
    return true;
  }
🤖 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 357 - 362, The delivery.send call for
pending.body is not wrapped in exception handling, allowing transient failures
to escape and terminate the cron invocation instead of treating them as
recoverable pending state. Wrap the const bodyResult = await
delivery.send(pending.body, bodyOpts) call in a try-catch block to catch any
exceptions thrown during the send operation. When an exception is caught, log
the error appropriately and return true to mark the body as still pending for
the next scheduled retry attempt, consistent with how postFreshStories handles
send failures.
package.json (1)

17-17: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the delivery dependency is not pinned to a sibling checkout.
node - <<'NODE'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const dep = pkg.dependencies?.['`@agentworkforce/delivery`'] ?? pkg.devDependencies?.['`@agentworkforce/delivery`'];
console.log(`@agentworkforce/delivery=${dep ?? '<missing>'}`);
if (typeof dep === 'string' && dep.startsWith('file:../')) {
  process.exitCode = 1;
}
NODE

Repository: AgentWorkforce/agents

Length of output: 220


Use the published delivery package instead of a sibling file: link.

Line 17 points outside this repository (../workforce/packages/delivery), which causes dependency resolution to fail in CI/cloud environments without the adjacent ../workforce checkout. This also contradicts the PR objective of migrating to the published package. Use the published semver range, or a workspace protocol if this repository owns that package.

Proposed fix
-    "`@agentworkforce/delivery`": "file:../workforce/packages/delivery",
+    "`@agentworkforce/delivery`": "^0.1.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    "`@agentworkforce/delivery`": "^0.1.0",
🤖 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 `@package.json` at line 17, The dependency entry for `@agentworkforce/delivery`
in package.json is using a file protocol pointing to a relative sibling
directory (../workforce/packages/delivery), which breaks in CI/cloud
environments and contradicts the migration to a published package. Replace the
file protocol reference with a proper published semver version range (such as
"^X.Y.Z") for the `@agentworkforce/delivery` package, or use the workspace
protocol if this repository manages that package. Ensure the dependency resolves
to the published package rather than a local file path.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

Working tree is clean. No source edits made (no mechanical-only fix was safe/appropriate). Here is my review.


Review: PR #89feat/unified-hn-monitor

Summary of the change

The PR retires the standalone hn-monitor-telegram agent and folds its behavior into the unified, transport-agnostic hn-monitor. Concretely it:

  • Deletes hn-monitor-telegram/ (README, agent.ts, avatar.png, banner.png, card.png).
  • Repoints the test import from ../.test-build/hn-monitor-telegram/agent.js to ../.test-build/hn-monitor/agent.js and removes the four hn-monitor-telegram tests.
  • Tightens the header-publish guard in hn-monitor/agent.ts (line 265).
  • Switches @agentworkforce/delivery from ^0.1.0 to file:../workforce/packages/delivery in package.json (and the matching package-lock.json).

Note: .workforce/pr.diff was truncated at 180 KB — entirely consumed by the deleted PNG binary patches — so the textual changes weren't in it. I reconstructed them from the checkout (git diff base..head for the three text files) before reviewing.

Blocking finding — build is red (delivery dependency)

package.json:17 pins @agentworkforce/delivery to file:../workforce/packages/delivery. That path does not exist in the checkout, so the dependency resolves to a dangling symlink:

node_modules/@agentworkforce/delivery -> ../../../workforce/packages/delivery   (broken)

npm install silently "succeeds" (npm doesn't validate file: targets), but the canonical CI command fails. I ran it:

$ npx tsc --noEmit
hn-monitor/agent.ts(33,8): error TS2307: Cannot find module '@agentworkforce/delivery' ...
hn-monitor/agent.ts(324,52): error TS2307: Cannot find module '@agentworkforce/delivery' ...
(+ knock-on TS7006 implicit-any errors)

npm test (tsc --outDir .test-build then node --test) fails identically — the test build can't compile hn-monitor/agent.ts, so the tests cannot run at all.

I did not auto-edit this. It's a deliberate staging choice (head commit a618a40: "use file: link for @agentworkforce/delivery until 0.1.1 published"), the registry currently only has 0.1.0, and reverting to ^0.1.0 is a semantic dependency decision that could break the new unified code if it depends on unpublished 0.1.1 API. Human action required: either publish @agentworkforce/delivery@0.1.1 and pin a registry version before merge, or confirm the 0.1.0 API is sufficient. As-is this PR is not mergeable green.

Review note — header-publish guard (hn-monitor/agent.ts:265)

-    if (heads.refs.length === 0) {
+    if (heads.refs.length === 0 || heads.refs.length < delivery.targets.length) {
       throw new Error(`Header publish failed across all targets`);

This makes the header check as strict as the existing body check at line 288 (bodyResult.refs.length < delivery.targets.length) — symmetric and reasonable. The direction is fail-closed-leaning (more likely to throw), not fail-open, so it doesn't weaken a safety default. Since the throw happens while headerPosted === false, the catch releases the seen-claim and rethrows for a clean next-tick retry.

One thing for the author to confirm (recovery/in-flight semantics — I'm flagging, not editing): in a partial header publish (e.g. 1 of 2 targets got a draft ref), this now throws and releases the claim, so the next tick re-publishes the header to all targets — including the one that already got a draft. Whether that double-posts depends on whether a draft ref without a later body is materialized server-side. If draft refs are inert until the body threads under them, this is harmless; if a header draft can surface on its own, the retry could duplicate it on the already-succeeded target. Worth a one-line confirmation from someone who owns the delivery parentRef semantics.

Other observations

  • Deletion is clean: no remaining references to hn-monitor-telegram anywhere in the repo (searched .ts/.mjs/.json/.md).
  • tests/telegram-agents.test.mjs:20 now imports postFreshStories, retryPendingThreadBody from hn-monitor but no longer uses them (the tests that used them were removed). Both symbols are still exported by hn-monitor/agent.ts, so the import resolves; noUnusedLocals is not enabled, so it won't fail the build. Left as advisory below rather than auto-removed — the build is blocked upstream by the delivery issue regardless, and trimming a test import is a change I'd rather leave to the author.

Addressed comments

  • No bot or human reviewer comments were provided in the harness context (.workforce/context.json contains only PR metadata; no review/comment payload). Nothing to reconcile against the current checkout.

Advisory Notes

  • tests/telegram-agents.test.mjs:20: the postFreshStories, retryPendingThreadBody import is now unused after the hn-monitor-telegram tests were deleted. Consider either removing it or adding a smoke assertion that exercises hn-monitor's post path, so the import earns its place. (Left unchanged — not required for correctness and orthogonal to the blocking build issue.)

Verification

  • Ran npx tsc --noEmit (CI's typecheck) → fails on unresolved @agentworkforce/delivery; same root cause blocks npm test's build step.
  • npm install created an incidental package-lock.json modification and a broken delivery symlink; I restored package-lock.json with git restore so nothing unintended is committed. Working tree is clean — no file edits were made.

No mechanical-only fix was available that wouldn't change semantics or infra decisions, so I made no edits. The PR is blocked on the @agentworkforce/delivery packaging decision (human/release action), so I'm not printing READY.

@khaliqgant khaliqgant merged commit 922f760 into main Jun 23, 2026
2 checks passed
@khaliqgant khaliqgant deleted the feat/unified-hn-monitor branch June 23, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant