@agfpd/iapeer 0.4.93 → 0.4.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,10 +117,10 @@ resets, the plan) — data, never our interpretation of the cause.
117
117
  | `summary` | string | one line, e.g. `boris · claude — rate_limit (Fable 5)` |
118
118
  | `content` | string | verbatim (claude) / rendered from typed fields (codex) |
119
119
  | `sessionId` | string? | correlates with the on-disk transcript |
120
- | `createdMs` | number | first detection |
121
- | `lastMs` | number | most recent detection folded in |
120
+ | `createdMs` | number | first occurrence |
121
+ | `lastMs` | number | latest OCCURRENCE (not the latest sweep that re-read it) |
122
122
  | `expiresMs` | number | TTL boundary |
123
- | `count` | number | detections folded into this notice (≥1) — render as `×N` |
123
+ | `count` | number | distinct OCCURRENCES folded in (≥1) — how many times the peer actually hit the wall. Render as `×N`. The sweep window overlaps on purpose, so the same runtime event is re-read across passes; the board discriminates on the event's own timestamp, and a re-read never bumps this. |
124
124
 
125
125
  ### Snapshot
126
126
 
@@ -156,6 +156,12 @@ A mute peer re-emits its error on **every** attempted turn. So a notice carries
156
156
  a repeat detection only bumps `count` and `lastMs`: **no new id, no new log line, no new SSE
157
157
  event**. The owner sees one card saying `×7`, not seven cards.
158
158
 
159
+ `count` means **occurrences, not sweeps**. The sweep window deliberately overlaps (so an event
160
+ landing between two passes is never missed), which means the same transcript line is re-read on
161
+ the next pass. The board therefore discriminates on the runtime event's OWN timestamp: a re-read
162
+ of an already-counted event folds silently and bumps nothing. Without that, two real refusals
163
+ would render as `×3` — a number the owner cannot check and that we would have made up.
164
+
159
165
  When the TTL passes (`IAPEER_NOTICE_TTL_MS`, default 1 h) the notice expires silently. If the
160
166
  peer is still broken, the next detection raises a **fresh** notice — a deliberate periodic
161
167
  reminder rather than one card that scrolls away forever. Expiry is not an event: either the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.93",
3
+ "version": "0.4.94",
4
4
  "description": "Run Claude Code and Codex CLI as local background agents that message each other, wake on demand, and share one memory.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -367,6 +367,9 @@ export function muteWatchTick(deps: MuteWatchDeps, sinceMs: number): { raised: n
367
367
  resetsAtMs: d.resetsAtMs,
368
368
  content: d.content,
369
369
  sessionId: d.sessionId,
370
+ // The runtime event's OWN timestamp — lets the board tell a new occurrence from the
371
+ // same line re-read by the deliberately-overlapping sweep window.
372
+ eventAtMs: d.atMs,
370
373
  summary: `${d.personality} · ${d.runtime} — ${d.errorType}${d.model ? ` (${d.model})` : ''}`,
371
374
  })
372
375
  if (!deduped) raised += 1
@@ -51,18 +51,27 @@ export interface NoticeInput {
51
51
  content: string
52
52
  /** The session that hit it (correlation with the transcript on disk). */
53
53
  sessionId?: string
54
+ /** When the underlying runtime event happened (the transcript line's own timestamp) —
55
+ * NOT when we swept. This is what makes `count` mean OCCURRENCES rather than sweeps:
56
+ * the sweep window deliberately overlaps, so the same line is re-read across passes.
57
+ * Absent → every raise counts (the caller has no event clock to dedup against). */
58
+ eventAtMs?: number
54
59
  }
55
60
 
56
61
  export interface Notice extends NoticeInput {
57
62
  id: string
58
63
  summary: string
59
64
  dedupKey: string
65
+ /** First occurrence. */
60
66
  createdMs: number
61
- /** Last time this same condition was re-detected (dedup bump). */
67
+ /** Latest OCCURRENCE (not the latest sweep that re-read it). */
62
68
  lastMs: number
63
69
  expiresMs: number
64
- /** How many times the condition was detected while this notice stayed live (≥1). */
70
+ /** Distinct occurrences folded into this notice (≥1) — how many times the peer actually
71
+ * hit the wall, which is what "×N" claims to the owner. */
65
72
  count: number
73
+ /** The newest event timestamp already counted — the overlap discriminator. */
74
+ lastEventMs?: number
66
75
  }
67
76
 
68
77
  export interface NoticeBoardOptions {
@@ -122,8 +131,16 @@ export class NoticeBoard {
122
131
  const dedupKey = noticeDedupKey(input)
123
132
  const existing = this.live.get(dedupKey)
124
133
  if (existing) {
125
- existing.count += 1
126
- existing.lastMs = nowMs
134
+ // Same condition. Is this a NEW occurrence, or the same runtime event re-read by an
135
+ // overlapping sweep? Counting the latter would inflate "×N" into a claim the owner
136
+ // cannot check — 2 real refusals rendering as ×3. Only a strictly newer event counts.
137
+ const at = input.eventAtMs
138
+ const isNewOccurrence = at === undefined || existing.lastEventMs === undefined || at > existing.lastEventMs
139
+ if (isNewOccurrence) {
140
+ existing.count += 1
141
+ existing.lastMs = nowMs
142
+ if (at !== undefined) existing.lastEventMs = at
143
+ }
127
144
  return { notice: existing, deduped: true }
128
145
  }
129
146
  const id = `n${++this.counter}`
@@ -137,6 +154,7 @@ export class NoticeBoard {
137
154
  lastMs: nowMs,
138
155
  expiresMs: nowMs + this.ttlMs,
139
156
  count: 1,
157
+ lastEventMs: input.eventAtMs,
140
158
  }
141
159
  this.live.set(dedupKey, notice)
142
160
  appendNoticeEvent(