@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.
- package/docs/19-notices.md +9 -3
- package/package.json +1 -1
- package/src/daemon/mutewatch.ts +3 -0
- package/src/daemon/notices.ts +22 -4
package/docs/19-notices.md
CHANGED
|
@@ -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
|
|
121
|
-
| `lastMs` | number |
|
|
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 |
|
|
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
package/src/daemon/mutewatch.ts
CHANGED
|
@@ -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
|
package/src/daemon/notices.ts
CHANGED
|
@@ -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
|
-
/**
|
|
67
|
+
/** Latest OCCURRENCE (not the latest sweep that re-read it). */
|
|
62
68
|
lastMs: number
|
|
63
69
|
expiresMs: number
|
|
64
|
-
/**
|
|
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
|
-
|
|
126
|
-
|
|
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(
|