You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two hub-adjacent code paths swallow errors silently with no logging surface. Operators can't tell whether a session-start hub sync succeeded, partially failed, or never reached the network at all — only the count of successfully-fetched entries shows up in the nudge. Same story for the cluster-mode replication loop: dial, stream, send, close, and receive failures all return into nothing.
This was tracked in TASKS.md (sub-items 2 and 3 of a broader "fix silent error suppression in hub" entry, added 2026-04-08). Sub-item 1 from that entry (ctx add --share publish failure warning) is already done at internal/cli/add/core/run/run.go:105-107 via writeConnect.PublishFailed. This issue covers only the two remaining sub-items.
The package doc at internal/cli/system/core/hubsync/doc.go:26 is explicit about the design intent:
Every error is silently swallowed so the hook never blocks the session start.
The "never block" constraint is correct. The "silently" part is the bug — the hook can preserve its non-blocking behavior while still emitting structured warnings that operators (and CI / agents reading the event log) can see.
Replication loop — internal/hub/replicate.go
Five silent return sites in replicateOnce plus a discarded store.Append result:
Note: startReplication is currently held behind a var _ = startReplication sentinel at line 22 (the function is not wired into Server.Start yet, awaiting cluster-mode work). Logging needs to land regardless — the function will be hot the moment cluster mode activates, and silent failures will be impossible to debug.
Proposed Fix
Use the existing internal/log/warn package — warn.Warn(format, args...) writes to stderr with a ctx: prefix, is intentionally best-effort (silent on sink failures), and is the documented pattern for "errors not actionable by the caller but should not be silently swallowed."
Note the un-conflation of syncErr and len(entries) == 0 — the current code treats them as the same condition, so a real sync error looks identical to "nothing new". Split them; only the error case warns.
Replication loop
Same pattern — wrap each silent return with warn.Warn including enough context to debug (masterAddr, sequence number where relevant). The discarded store.Append result at line 121 becomes:
if_, appendErr:=store.Append([]Entry{entry}); appendErr!=nil {
warn.Warn("replication: append entry %s (seq=%d): %v", entry.ID, entry.Sequence, appendErr)
// Don't return — keep consuming the stream; the next entry might succeed.
}
Update internal/cli/system/core/hubsync/doc.go to replace "Every error is silently swallowed" with the new contract: "Every error is warned via warn.Warn but does not block the session start."
Tests Required
TestSync_WarnsOnLoadError — wire a sink-replacement helper (the warn package's sink is a package-level io.Writer; tests can swap it for a bytes.Buffer via an unexported setter, similar to how existing warn tests likely work). Call Sync with a broken config and assert the buffer contains the expected hubsync: load connection config: prefix.
TestSync_WarnsOnDialError — same shape with an unreachable HubAddr.
TestSync_NoWarnOnEmptyResult — assert the buffer stays empty when the hub returns zero entries (the un-conflation case).
TestReplicateOnce_WarnsOnDialError — same shape for the replication loop.
TestReplicateOnce_KeepsConsumingAfterAppendError — the only behavior change beyond logging: append failures don't abort the receive loop. Inject a store that fails on the first append, succeeds on the second; assert both entries were attempted.
Out of Scope
Structured (JSON) event log emission. The task description mentions "event system"; for now, stderr via warn.Warn is the established pattern. If a separate JSON event log becomes a thing (the internal/log/event package exists but its surface is rotate/query, not emit-warn), a follow-up can route through both.
Making Sync return an error. The non-blocking-hook contract is a hard requirement; the function must continue returning string. Logging is the visibility mechanism.
Both halves are mechanical: find the silent return, add one warn.Warn call, repeat. The pattern is identical (existing internal/write/warn.Warn calls elsewhere in the codebase give a clear example). The only design judgment needed is the un-conflation of syncErr and empty-result in the hubsync hook — well-flagged above. A newcomer can land this in ~50 lines across two files plus tests.
🤖 Filed via gh by Claude Code on behalf of @v0lkan. Discovered during stale-task triage; companion to #93, #94, #95, #96.
Summary
Two hub-adjacent code paths swallow errors silently with no logging surface. Operators can't tell whether a session-start hub sync succeeded, partially failed, or never reached the network at all — only the count of successfully-fetched entries shows up in the nudge. Same story for the cluster-mode replication loop: dial, stream, send, close, and receive failures all return into nothing.
This was tracked in TASKS.md (sub-items 2 and 3 of a broader "fix silent error suppression in hub" entry, added 2026-04-08). Sub-item 1 from that entry (
ctx add --sharepublish failure warning) is already done atinternal/cli/add/core/run/run.go:105-107viawriteConnect.PublishFailed. This issue covers only the two remaining sub-items.Locations
Hubsync hook —
internal/cli/system/core/hubsync/sync.goFour silent return sites in
Sync:The package doc at
internal/cli/system/core/hubsync/doc.go:26is explicit about the design intent:The "never block" constraint is correct. The "silently" part is the bug — the hook can preserve its non-blocking behavior while still emitting structured warnings that operators (and CI / agents reading the event log) can see.
Replication loop —
internal/hub/replicate.goFive silent return sites in
replicateOnceplus a discardedstore.Appendresult:Note:
startReplicationis currently held behind avar _ = startReplicationsentinel at line 22 (the function is not wired into Server.Start yet, awaiting cluster-mode work). Logging needs to land regardless — the function will be hot the moment cluster mode activates, and silent failures will be impossible to debug.Proposed Fix
Use the existing
internal/log/warnpackage —warn.Warn(format, args...)writes to stderr with actx:prefix, is intentionally best-effort (silent on sink failures), and is the documented pattern for "errors not actionable by the caller but should not be silently swallowed."Hubsync hook
Note the un-conflation of
syncErrandlen(entries) == 0— the current code treats them as the same condition, so a real sync error looks identical to "nothing new". Split them; only the error case warns.Replication loop
Same pattern — wrap each silent return with
warn.Warnincluding enough context to debug (masterAddr, sequence number where relevant). The discardedstore.Appendresult at line 121 becomes:Update
internal/cli/system/core/hubsync/doc.goto replace "Every error is silently swallowed" with the new contract: "Every error is warned viawarn.Warnbut does not block the session start."Tests Required
TestSync_WarnsOnLoadError— wire a sink-replacement helper (the warn package'ssinkis a package-levelio.Writer; tests can swap it for abytes.Buffervia an unexported setter, similar to how existing warn tests likely work). CallSyncwith a broken config and assert the buffer contains the expectedhubsync: load connection config:prefix.TestSync_WarnsOnDialError— same shape with an unreachable HubAddr.TestSync_NoWarnOnEmptyResult— assert the buffer stays empty when the hub returns zero entries (the un-conflation case).TestReplicateOnce_WarnsOnDialError— same shape for the replication loop.TestReplicateOnce_KeepsConsumingAfterAppendError— the only behavior change beyond logging: append failures don't abort the receive loop. Inject a store that fails on the first append, succeeds on the second; assert both entries were attempted.Out of Scope
warn.Warnis the established pattern. If a separate JSON event log becomes a thing (theinternal/log/eventpackage exists but its surface is rotate/query, not emit-warn), a follow-up can route through both.Syncreturn an error. The non-blocking-hook contract is a hard requirement; the function must continue returningstring. Logging is the visibility mechanism.startReplicationinto the active cluster path. Separate issue (Hub status: surface cluster leadership (IsLeader, LeaderAddr) in Status RPC + ctx hub status #96 covers cluster-leadership wiring; this issue is logging-only).Scope for "good first issue"
Both halves are mechanical: find the silent return, add one
warn.Warncall, repeat. The pattern is identical (existinginternal/write/warn.Warncalls elsewhere in the codebase give a clear example). The only design judgment needed is the un-conflation ofsyncErrand empty-result in the hubsync hook — well-flagged above. A newcomer can land this in ~50 lines across two files plus tests.🤖 Filed via gh by Claude Code on behalf of @v0lkan. Discovered during stale-task triage; companion to #93, #94, #95, #96.