monday-cli 0.3.0 → 0.4.0
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/CHANGELOG.md +351 -0
- package/README.md +130 -36
- package/dist/api/assets.d.ts +326 -0
- package/dist/api/assets.d.ts.map +1 -0
- package/dist/api/assets.js +519 -0
- package/dist/api/assets.js.map +1 -0
- package/dist/api/column-types.d.ts +11 -7
- package/dist/api/column-types.d.ts.map +1 -1
- package/dist/api/column-types.js +5 -3
- package/dist/api/column-types.js.map +1 -1
- package/dist/api/column-values.d.ts +7 -1
- package/dist/api/column-values.d.ts.map +1 -1
- package/dist/api/column-values.js +15 -6
- package/dist/api/column-values.js.map +1 -1
- package/dist/api/documents.d.ts +519 -0
- package/dist/api/documents.d.ts.map +1 -0
- package/dist/api/documents.js +586 -0
- package/dist/api/documents.js.map +1 -0
- package/dist/api/item-watch.d.ts +263 -0
- package/dist/api/item-watch.d.ts.map +1 -0
- package/dist/api/item-watch.js +709 -0
- package/dist/api/item-watch.js.map +1 -0
- package/dist/api/multipart-transport.d.ts +223 -0
- package/dist/api/multipart-transport.d.ts.map +1 -0
- package/dist/api/multipart-transport.js +274 -0
- package/dist/api/multipart-transport.js.map +1 -0
- package/dist/api/parallel-dispatch.d.ts +155 -0
- package/dist/api/parallel-dispatch.d.ts.map +1 -0
- package/dist/api/parallel-dispatch.js +243 -0
- package/dist/api/parallel-dispatch.js.map +1 -0
- package/dist/api/partial-success-bulk.d.ts +118 -60
- package/dist/api/partial-success-bulk.d.ts.map +1 -1
- package/dist/api/partial-success-bulk.js +137 -79
- package/dist/api/partial-success-bulk.js.map +1 -1
- package/dist/api/partial-success-mutation.d.ts +13 -1
- package/dist/api/partial-success-mutation.d.ts.map +1 -1
- package/dist/api/partial-success-mutation.js +5 -1
- package/dist/api/partial-success-mutation.js.map +1 -1
- package/dist/api/raw-write.d.ts +12 -4
- package/dist/api/raw-write.d.ts.map +1 -1
- package/dist/api/raw-write.js +21 -11
- package/dist/api/raw-write.js.map +1 -1
- package/dist/api/resolve-client.d.ts +11 -0
- package/dist/api/resolve-client.d.ts.map +1 -1
- package/dist/api/resolve-client.js +9 -1
- package/dist/api/resolve-client.js.map +1 -1
- package/dist/cli/run.d.ts +20 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +1 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/commands/board/column-create.d.ts +6 -5
- package/dist/commands/board/column-create.d.ts.map +1 -1
- package/dist/commands/board/column-create.js +9 -6
- package/dist/commands/board/column-create.js.map +1 -1
- package/dist/commands/completion.d.ts +188 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +418 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/doc/get.d.ts +46 -0
- package/dist/commands/doc/get.d.ts.map +1 -0
- package/dist/commands/doc/get.js +95 -0
- package/dist/commands/doc/get.js.map +1 -0
- package/dist/commands/doc/list.d.ts +83 -0
- package/dist/commands/doc/list.d.ts.map +1 -0
- package/dist/commands/doc/list.js +248 -0
- package/dist/commands/doc/list.js.map +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +46 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/item/create.js +2 -2
- package/dist/commands/item/update.d.ts +1 -0
- package/dist/commands/item/update.d.ts.map +1 -1
- package/dist/commands/item/update.js +61 -0
- package/dist/commands/item/update.js.map +1 -1
- package/dist/commands/item/upload.d.ts +108 -0
- package/dist/commands/item/upload.d.ts.map +1 -0
- package/dist/commands/item/upload.js +370 -0
- package/dist/commands/item/upload.js.map +1 -0
- package/dist/commands/item/watch.d.ts +90 -0
- package/dist/commands/item/watch.d.ts.map +1 -0
- package/dist/commands/item/watch.js +342 -0
- package/dist/commands/item/watch.js.map +1 -0
- package/dist/commands/update/upload.d.ts +69 -0
- package/dist/commands/update/upload.d.ts.map +1 -0
- package/dist/commands/update/upload.js +235 -0
- package/dist/commands/update/upload.js.map +1 -0
- package/dist/types/ids.d.ts +2 -0
- package/dist/types/ids.d.ts.map +1 -1
- package/dist/types/ids.js +9 -2
- package/dist/types/ids.js.map +1 -1
- package/dist/utils/mime.d.ts +24 -0
- package/dist/utils/mime.d.ts.map +1 -0
- package/dist/utils/mime.js +64 -0
- package/dist/utils/mime.js.map +1 -0
- package/dist/utils/output/envelope.d.ts +30 -0
- package/dist/utils/output/envelope.d.ts.map +1 -1
- package/dist/utils/output/envelope.js +26 -0
- package/dist/utils/output/envelope.js.map +1 -1
- package/dist/utils/output/ndjson.d.ts +25 -0
- package/dist/utils/output/ndjson.d.ts.map +1 -1
- package/dist/utils/output/ndjson.js +12 -0
- package/dist/utils/output/ndjson.js.map +1 -1
- package/dist/utils/signal.d.ts +42 -0
- package/dist/utils/signal.d.ts.map +1 -0
- package/dist/utils/signal.js +45 -0
- package/dist/utils/signal.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polling-based watch surface for the v0.4-M29 `monday item watch <iid>`
|
|
3
|
+
* verb (`cli-design.md` §13 v0.4 entry + §14.4 closure;
|
|
4
|
+
* `v0.4-plan.md` §3 M29).
|
|
5
|
+
*
|
|
6
|
+
* **What `monday item watch` answers:** "wait for changes on this item
|
|
7
|
+
* and emit them as they arrive, without me scripting a polling loop
|
|
8
|
+
* around `monday item history`". A single CLI invocation polls Monday's
|
|
9
|
+
* `boards(ids:){ activity_logs(item_ids:, from:, limit:) }` surface on
|
|
10
|
+
* each tick, projects new events through the M24 `item-history-
|
|
11
|
+
* projection.ts` projector (reused verbatim), and emits one NDJSON
|
|
12
|
+
* record per new event plus a session-summary trailer on exit.
|
|
13
|
+
*
|
|
14
|
+
* **Status: M29 IMPL.** Runtime body of {@link watchItem} shipped
|
|
15
|
+
* at `7b83a3a` + Codex impl review round-1 fix-ups (signal-aware
|
|
16
|
+
* catches across the bootstrap / `--once` / polling-loop sites,
|
|
17
|
+
* epoch-floor backlog drain, since-boundary skip, deadline-aware
|
|
18
|
+
* cadence/backoff). The module surface (input + result types,
|
|
19
|
+
* projector import, named-operation pin) was finalised at the
|
|
20
|
+
* pre-flight contract diff so the cli-design §4.3 row + v0.4-plan
|
|
21
|
+
* §3 M29 deliverables list cite real signatures.
|
|
22
|
+
*
|
|
23
|
+
* **Pinned design clearances (v0.4-plan §3 M29 + cli-design §14.4
|
|
24
|
+
* closure at `31713fb`).**
|
|
25
|
+
*
|
|
26
|
+
* - **Default cadence: 30s** (`DEFAULT_WATCH_INTERVAL_MS`). Override
|
|
27
|
+
* range 1000ms–3600000ms (`MIN_WATCH_INTERVAL_MS` /
|
|
28
|
+
* `MAX_WATCH_INTERVAL_MS`). Per the empirical probe
|
|
29
|
+
* (`scripts/probe/m29-polling-burn.ts`, 2026-05-13, API `2026-01`)
|
|
30
|
+
* each poll costs 10 complexity points against a 1,000,000/min
|
|
31
|
+
* budget — 30s cadence burns 0.002% of the per-minute budget;
|
|
32
|
+
* politeness + Monday's >30s `activity_logs` propagation lag (M24
|
|
33
|
+
* probe) are the binding constraints, NOT budget.
|
|
34
|
+
* - **Reactive circuit breaker** (`CIRCUIT_BREAKER_CONSECUTIVE_FAILS`
|
|
35
|
+
* = 5). On `complexity_exceeded` / `concurrency_exceeded` /
|
|
36
|
+
* `rate_limited` wire errors the loop backs off respecting
|
|
37
|
+
* `reset_in_x_seconds` (60s default cap when absent; 300s
|
|
38
|
+
* ceiling per `MAX_BACKOFF_SECONDS`); after N consecutive failed
|
|
39
|
+
* polls the session trips to a failure envelope carrying
|
|
40
|
+
* `circuit_broken_at` + `failed_polls` in trailer-meta. Each
|
|
41
|
+
* failure APPENDS a `WatchSessionWarning` to
|
|
42
|
+
* {@link WatchItemResult}.warnings; the action body folds the
|
|
43
|
+
* accumulated warnings into the trailer-meta's `_meta.warnings`
|
|
44
|
+
* slot at session end per cli-design §6.3 (resource lines +
|
|
45
|
+
* final `_meta`; warnings are NOT interleaved with event lines).
|
|
46
|
+
* - **No new ERROR_CODE.** `complexity_exceeded` /
|
|
47
|
+
* `concurrency_exceeded` / `rate_limited` already in §6.5's
|
|
48
|
+
* 29-code registry cover the circuit-breaker exit. The trailer-
|
|
49
|
+
* meta `circuit_broken_at` slot discriminates which Monday code
|
|
50
|
+
* tripped the session.
|
|
51
|
+
* - **Each invocation independent.** No shared registry between
|
|
52
|
+
* concurrent `monday item watch` invocations; the last-seen-
|
|
53
|
+
* event-id watermark lives in-memory only. Aligns with cli-design
|
|
54
|
+
* §3.1 #5.
|
|
55
|
+
* - **`--since <event-id>` is a one-shot bootstrap watermark, not a
|
|
56
|
+
* state-machine resume.** The runtime looks up the event's
|
|
57
|
+
* `created_at` once at startup, sets the initial poll-from
|
|
58
|
+
* timestamp, and emits any backlog from that point before
|
|
59
|
+
* entering the polling loop. Distinct from a full `--resume
|
|
60
|
+
* <token>` mechanism (still open per cli-design §14.6).
|
|
61
|
+
* - **`--once` vs `--max-events 1` are DISTINCT.** `--once` drains
|
|
62
|
+
* the backlog from `--since` (or the most-recent N events if no
|
|
63
|
+
* `--since`) and exits without polling. `--max-events 1` waits
|
|
64
|
+
* for the NEXT event.
|
|
65
|
+
*
|
|
66
|
+
* **Why polling activity_logs ONLY (not the M24 two-source merge).**
|
|
67
|
+
* The M29 pre-flight probe (`scripts/probe/m29-polling-burn.ts`,
|
|
68
|
+
* 2026-05-13) measured the merged (activity_logs + updates) shape at
|
|
69
|
+
* 20 complexity points per poll vs 10 for activity_logs alone. Polling
|
|
70
|
+
* BOTH sources every tick doubles the burn while emitting comments
|
|
71
|
+
* agents could already poll via `monday update list` on a slower
|
|
72
|
+
* cadence. v0.4-M29 ships activity_logs-only; a `--include-comments`
|
|
73
|
+
* flag that adds a separate slower-cadence updates poll is a v0.4-
|
|
74
|
+
* stretch / v0.5 candidate. `--include update_posted` /
|
|
75
|
+
* `--include update_replied` is accepted at the argv boundary for
|
|
76
|
+
* forward-compat but returns no events at v0.4-M29 (the projector
|
|
77
|
+
* variants are valid but the activity_logs source doesn't surface
|
|
78
|
+
* comment events).
|
|
79
|
+
*/
|
|
80
|
+
import { z } from 'zod';
|
|
81
|
+
import { ApiError, UsageError } from '../utils/errors.js';
|
|
82
|
+
import { unwrapOrThrow } from '../utils/parse-boundary.js';
|
|
83
|
+
import { extractSignalReason } from '../utils/signal.js';
|
|
84
|
+
import { ITEM_SCOPED_ENTITY, buildUnknownEventKindWarning, projectActivityLogRow, rawActivityLogRowSchema, } from './item-history-projection.js';
|
|
85
|
+
/**
|
|
86
|
+
* Default polling interval (30,000ms / 30s) per cli-design §14.4
|
|
87
|
+
* closure. Pinned by the M29 pre-flight empirical probe — burns
|
|
88
|
+
* ~0.002% of the per-minute complexity budget at this cadence, and
|
|
89
|
+
* matches Monday's documented >30s `activity_logs` propagation lag so
|
|
90
|
+
* faster polling would just generate polls against unpropagated data.
|
|
91
|
+
*/
|
|
92
|
+
export const DEFAULT_WATCH_INTERVAL_MS = 30_000;
|
|
93
|
+
/**
|
|
94
|
+
* Floor on `--interval <ms>` per cli-design §14.4 closure. Faster than
|
|
95
|
+
* 1s would generate Monday request-rate concerns + crosses the
|
|
96
|
+
* propagation-lag threshold where successive polls would see the same
|
|
97
|
+
* window of unpropagated events.
|
|
98
|
+
*/
|
|
99
|
+
export const MIN_WATCH_INTERVAL_MS = 1_000;
|
|
100
|
+
/**
|
|
101
|
+
* Ceiling on `--interval <ms>` per cli-design §14.4 closure. Beyond
|
|
102
|
+
* 1h the verb crosses the "no longer a watch, just a poll" boundary —
|
|
103
|
+
* agents should use `cron + monday item history` for hourly+ cadences
|
|
104
|
+
* rather than a long-running CLI session.
|
|
105
|
+
*/
|
|
106
|
+
export const MAX_WATCH_INTERVAL_MS = 3_600_000;
|
|
107
|
+
/**
|
|
108
|
+
* Number of consecutive failed polls before the circuit breaker trips
|
|
109
|
+
* per cli-design §14.4 closure. Each prior failure APPENDS a
|
|
110
|
+
* `WatchSessionWarning` to {@link WatchItemResult}.warnings (folded
|
|
111
|
+
* into the trailer's `_meta.warnings[]` slot at session end per §6.3
|
|
112
|
+
* — NOT interleaved with event lines); the Nth (default 5) failure
|
|
113
|
+
* trips the session to a failure envelope.
|
|
114
|
+
*/
|
|
115
|
+
export const CIRCUIT_BREAKER_CONSECUTIVE_FAILS = 5;
|
|
116
|
+
/**
|
|
117
|
+
* Default `reset_in_x_seconds` fallback when Monday's wire error
|
|
118
|
+
* doesn't carry the field. 60s mirrors Monday's per-minute complexity
|
|
119
|
+
* window so the backoff aligns with the budget-reset cycle.
|
|
120
|
+
*/
|
|
121
|
+
export const DEFAULT_BACKOFF_SECONDS = 60;
|
|
122
|
+
/**
|
|
123
|
+
* Cap on `reset_in_x_seconds` backoff per cli-design §14.4 closure.
|
|
124
|
+
* Beyond 5min the session may as well exit + let an agent re-invoke
|
|
125
|
+
* later — sleeping a watch loop for >5min defeats the "react quickly"
|
|
126
|
+
* purpose.
|
|
127
|
+
*/
|
|
128
|
+
export const MAX_BACKOFF_SECONDS = 300;
|
|
129
|
+
/**
|
|
130
|
+
* Default number of recent events to drain at session startup when
|
|
131
|
+
* `--once` is set without `--since`. Mirrors the M24 `--limit` default
|
|
132
|
+
* so an agent calling `monday item watch <iid> --once` without bounds
|
|
133
|
+
* sees the same backlog `monday item history <iid> --limit 100` would
|
|
134
|
+
* (just streamed via the NDJSON envelope).
|
|
135
|
+
*/
|
|
136
|
+
export const DEFAULT_ONCE_BACKLOG_LIMIT = 100;
|
|
137
|
+
/**
|
|
138
|
+
* Pinned GraphQL document for the per-tick poll. Mirrors M24's
|
|
139
|
+
* `ACTIVITY_LOGS_QUERY` verbatim — M29's runtime body reuses M24's
|
|
140
|
+
* projector (`projectActivityLogRow`) on the same wire-shape rows.
|
|
141
|
+
* The `complexity` selection is co-located so each poll's response
|
|
142
|
+
* carries per-call cost + remaining-budget for circuit-breaker
|
|
143
|
+
* decisions (mirrors the M29 pre-flight probe shape).
|
|
144
|
+
*
|
|
145
|
+
* **R-NEW-37 W2 audit-point: operationName is `ItemWatchPoll`.**
|
|
146
|
+
* Pinned literal here + used by {@link fetchPoll}'s
|
|
147
|
+
* `client.raw(..., { operationName: 'ItemWatchPoll' })` call.
|
|
148
|
+
* Safely-by-construction per the M27 round-1 P2-1 precedent
|
|
149
|
+
* (`6f59a83`): no caller-overridable operationName input slot on
|
|
150
|
+
* {@link WatchItemInputs}.
|
|
151
|
+
*/
|
|
152
|
+
export const WATCH_POLL_QUERY = `
|
|
153
|
+
query ItemWatchPoll(
|
|
154
|
+
$bid: [ID!]!,
|
|
155
|
+
$iid: [ID!]!,
|
|
156
|
+
$from: ISO8601DateTime!,
|
|
157
|
+
$limit: Int!
|
|
158
|
+
) {
|
|
159
|
+
complexity { before after query reset_in_x_seconds }
|
|
160
|
+
boards(ids: $bid) {
|
|
161
|
+
id
|
|
162
|
+
activity_logs(
|
|
163
|
+
item_ids: $iid,
|
|
164
|
+
from: $from,
|
|
165
|
+
limit: $limit
|
|
166
|
+
) {
|
|
167
|
+
id
|
|
168
|
+
event
|
|
169
|
+
entity
|
|
170
|
+
user_id
|
|
171
|
+
created_at
|
|
172
|
+
data
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
/**
|
|
178
|
+
* Wire-codes that arm the circuit breaker per cli-design §14.4
|
|
179
|
+
* closure D1. These are Monday's rate-limit codes already in §6.5's
|
|
180
|
+
* 29-code registry; the breaker reuses them rather than introducing
|
|
181
|
+
* a new ERROR_CODE (count stays at 29). Non-matching wire errors
|
|
182
|
+
* (`not_found` from a deleted item mid-watch, `unauthorized` from a
|
|
183
|
+
* revoked token, `internal_error` from a parse boundary) propagate
|
|
184
|
+
* unchanged — they're not "Monday is asking us to slow down" signals.
|
|
185
|
+
*/
|
|
186
|
+
const CIRCUIT_BREAKER_CODES = [
|
|
187
|
+
'complexity_exceeded',
|
|
188
|
+
'concurrency_exceeded',
|
|
189
|
+
'rate_limited',
|
|
190
|
+
];
|
|
191
|
+
/**
|
|
192
|
+
* Wire-shape schema for the per-tick {@link WATCH_POLL_QUERY}
|
|
193
|
+
* response. Mirrors M24's `activityLogsResponseSchema` for the
|
|
194
|
+
* `boards.activity_logs` sub-tree; `.loose()` so forward-compat
|
|
195
|
+
* Monday surface extensions don't break the parse.
|
|
196
|
+
*/
|
|
197
|
+
const watchPollResponseSchema = z
|
|
198
|
+
.object({
|
|
199
|
+
boards: z
|
|
200
|
+
.array(z
|
|
201
|
+
.object({
|
|
202
|
+
id: z.string().min(1),
|
|
203
|
+
activity_logs: z.array(rawActivityLogRowSchema).nullable(),
|
|
204
|
+
})
|
|
205
|
+
.loose()
|
|
206
|
+
.nullable())
|
|
207
|
+
.nullable(),
|
|
208
|
+
})
|
|
209
|
+
.loose();
|
|
210
|
+
/**
|
|
211
|
+
* Promise that resolves after `ms` milliseconds OR rejects when the
|
|
212
|
+
* supplied {@link AbortSignal} fires. Mirrors `src/api/retry.ts`'s
|
|
213
|
+
* `defaultSleep` + the R-NEW-26 race-window guard: a sync
|
|
214
|
+
* `signal.aborted` check BEFORE listener registration handles the
|
|
215
|
+
* case where the abort fires synchronously between the caller's last
|
|
216
|
+
* `signal.aborted` check and our `addEventListener` call (Node's
|
|
217
|
+
* AbortSignal does NOT replay 'abort' for listeners attached after
|
|
218
|
+
* the event dispatched).
|
|
219
|
+
*/
|
|
220
|
+
const sleepWithSignal = (ms, signal) => new Promise((resolve, reject) => {
|
|
221
|
+
// Defensive race-window guard (R-NEW-26): handles the narrow case
|
|
222
|
+
// where the abort fires synchronously between the caller's last
|
|
223
|
+
// `signal.aborted` check and our `addEventListener` registration.
|
|
224
|
+
// Not reachable from the integration tests since the runner
|
|
225
|
+
// checks `signal.aborted` after every await; production-only.
|
|
226
|
+
/* c8 ignore start */
|
|
227
|
+
if (signal.aborted) {
|
|
228
|
+
reject(extractSignalReason(signal));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
/* c8 ignore stop */
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
signal.removeEventListener('abort', onAbort);
|
|
234
|
+
resolve();
|
|
235
|
+
}, ms);
|
|
236
|
+
const onAbort = () => {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
reject(extractSignalReason(signal));
|
|
239
|
+
};
|
|
240
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
241
|
+
});
|
|
242
|
+
/**
|
|
243
|
+
* Whether an error represents Monday signalling rate-limit /
|
|
244
|
+
* complexity-budget exhaustion (matched by ErrorCode rather than
|
|
245
|
+
* English message per `.claude/rules/security.md` + cli-design §6.5
|
|
246
|
+
* agent-keys-off-code discipline).
|
|
247
|
+
*/
|
|
248
|
+
const isCircuitBreakerError = (err) => err instanceof ApiError &&
|
|
249
|
+
CIRCUIT_BREAKER_CODES.includes(err.code);
|
|
250
|
+
/**
|
|
251
|
+
* Computes the per-failure backoff in seconds. Monday's wire error
|
|
252
|
+
* may carry `retry_after_seconds` (mapped through `api/errors.ts`'s
|
|
253
|
+
* `extractRetryInSeconds`); when absent we default to
|
|
254
|
+
* {@link DEFAULT_BACKOFF_SECONDS}. Either way we clamp at
|
|
255
|
+
* {@link MAX_BACKOFF_SECONDS} so the loop doesn't sleep past the
|
|
256
|
+
* 5-min ceiling (cli-design §14.4 closure: "beyond 5min the session
|
|
257
|
+
* may as well exit + let an agent re-invoke later").
|
|
258
|
+
*/
|
|
259
|
+
const backoffSecondsFrom = (err) => {
|
|
260
|
+
const seconds = err.retryAfterSeconds ?? DEFAULT_BACKOFF_SECONDS;
|
|
261
|
+
return Math.min(seconds, MAX_BACKOFF_SECONDS);
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Loads one page of `activity_logs` against the watch surface,
|
|
265
|
+
* filtered to the target item via the `iid` arg + the
|
|
266
|
+
* `from: <watermark>` ISO-timestamp pin. Reuses M24's wire shape
|
|
267
|
+
* (same `ACTIVITY_LOGS_QUERY` selection set, minus the per-page
|
|
268
|
+
* pagination args that watch doesn't paginate). Returns the raw
|
|
269
|
+
* rows so the caller can apply walker-side entity filter +
|
|
270
|
+
* projection + dedup.
|
|
271
|
+
*/
|
|
272
|
+
const fetchPoll = async (args) => {
|
|
273
|
+
const response = await args.client.raw(WATCH_POLL_QUERY, {
|
|
274
|
+
bid: [args.boardId],
|
|
275
|
+
iid: [args.itemId],
|
|
276
|
+
from: args.from,
|
|
277
|
+
limit: args.limit,
|
|
278
|
+
}, { operationName: 'ItemWatchPoll' });
|
|
279
|
+
const parsed = unwrapOrThrow(watchPollResponseSchema.safeParse(response.data), {
|
|
280
|
+
context: 'Monday `boards.activity_logs` watch-poll response',
|
|
281
|
+
details: { item_id: args.itemId, board_id: args.boardId },
|
|
282
|
+
hint: 'Monday may have amended the `boards(ids:) { activity_logs }` surface — re-probe via `scripts/probe/m29-polling-burn.ts` and amend cli-design §14.4 closure if so',
|
|
283
|
+
});
|
|
284
|
+
const rows = [];
|
|
285
|
+
for (const board of parsed.boards ?? []) {
|
|
286
|
+
if (board === null)
|
|
287
|
+
continue;
|
|
288
|
+
rows.push(...(board.activity_logs ?? []));
|
|
289
|
+
}
|
|
290
|
+
return rows;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Looks up the `--since <event-id>` watermark by scanning a recent
|
|
294
|
+
* window of activity_logs for the matching id. Monday's GraphQL
|
|
295
|
+
* surface has no `activity_log(id:)` resolver; we fetch a generous
|
|
296
|
+
* recent slice and search client-side. If the id isn't in the
|
|
297
|
+
* window, throws `usage_error` so an agent passing a stale id sees
|
|
298
|
+
* a clear cause (not a silent no-op session).
|
|
299
|
+
*/
|
|
300
|
+
const resolveSinceWatermark = async (args) => {
|
|
301
|
+
// Use a unix-epoch floor so we get the full recent backlog; the
|
|
302
|
+
// 500-row limit caps how far back resumption reaches in a single
|
|
303
|
+
// call (sufficient for "resume from a recent session" — the
|
|
304
|
+
// documented intent per cli-design §14.4 closure).
|
|
305
|
+
const rows = await fetchPoll({
|
|
306
|
+
client: args.client,
|
|
307
|
+
boardId: args.boardId,
|
|
308
|
+
itemId: args.itemId,
|
|
309
|
+
from: '1970-01-01T00:00:00Z',
|
|
310
|
+
limit: 500,
|
|
311
|
+
});
|
|
312
|
+
const match = rows.find((r) => r.id === args.sinceEventId);
|
|
313
|
+
if (match === undefined) {
|
|
314
|
+
throw new UsageError(`--since event-id ${args.sinceEventId} not found in the recent activity-log window for item ${args.itemId}`, {
|
|
315
|
+
details: {
|
|
316
|
+
item_id: args.itemId,
|
|
317
|
+
since_event_id: args.sinceEventId,
|
|
318
|
+
window_size: rows.length,
|
|
319
|
+
hint: 'Monday\'s activity_logs has no direct event-id resolver; the CLI scans the 500 most recent rows. Pass a more recent event-id, or omit --since to start the watch session from now.',
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return match;
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Sorts raw activity-log rows chronologically ascending so the polling
|
|
327
|
+
* loop emits in real-time order. Tie-breaks on `id` (numeric compare
|
|
328
|
+
* via BigInt — Monday's ids can exceed `Number.MAX_SAFE_INTEGER`).
|
|
329
|
+
*/
|
|
330
|
+
const sortChronological = (rows) => {
|
|
331
|
+
const copy = [...rows];
|
|
332
|
+
copy.sort((a, b) => {
|
|
333
|
+
if (a.created_at !== b.created_at) {
|
|
334
|
+
return a.created_at < b.created_at ? -1 : 1;
|
|
335
|
+
}
|
|
336
|
+
return compareBigIntStrings(a.id, b.id);
|
|
337
|
+
});
|
|
338
|
+
return copy;
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Numeric compare for Monday's id strings without loss of precision.
|
|
342
|
+
* Activity-log ids can be 13+ digits, exceeding the JS Number safe
|
|
343
|
+
* range; BigInt parsing avoids the lossy `Number(...)` shape.
|
|
344
|
+
* Lexicographic compare would mis-order ids of different lengths
|
|
345
|
+
* (`"9"` > `"10"` lex; `9n < 10n` numerically).
|
|
346
|
+
*/
|
|
347
|
+
const compareBigIntStrings = (a, b) => {
|
|
348
|
+
try {
|
|
349
|
+
const ba = BigInt(a);
|
|
350
|
+
const bb = BigInt(b);
|
|
351
|
+
if (ba === bb)
|
|
352
|
+
return 0;
|
|
353
|
+
return ba < bb ? -1 : 1;
|
|
354
|
+
} /* c8 ignore start */
|
|
355
|
+
catch {
|
|
356
|
+
// Defensive: non-numeric id shouldn't happen (Monday's wire
|
|
357
|
+
// schema is `String!` but always digits per the M24 probe);
|
|
358
|
+
// fall back to lex compare so we never throw out of a sort.
|
|
359
|
+
if (a === b)
|
|
360
|
+
return 0;
|
|
361
|
+
return a < b ? -1 : 1;
|
|
362
|
+
} /* c8 ignore stop */
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Polling-based event-stream walker. Reuses M24's
|
|
366
|
+
* `projectActivityLogRow` for per-event projection; the polling
|
|
367
|
+
* loop owns the cadence + circuit-breaker + watermark state.
|
|
368
|
+
*
|
|
369
|
+
* 1. Resolves the initial poll-from timestamp from `inputs.since`
|
|
370
|
+
* (look up event-id → created_at) or from now.
|
|
371
|
+
* 2. If `inputs.once === true`: drains backlog through `onEvent`,
|
|
372
|
+
* exits with `exit_reason: 'once_complete'`.
|
|
373
|
+
* 3. Otherwise enters the polling loop: each tick fires
|
|
374
|
+
* `WATCH_POLL_QUERY`, filters newly-seen events (dedup Set +
|
|
375
|
+
* walker-side `entity === 'pulse'`), projects via
|
|
376
|
+
* `projectActivityLogRow`, applies the `includeKinds` filter,
|
|
377
|
+
* emits via `onEvent`, advances the watermark.
|
|
378
|
+
* 4. After each poll awaits the cadence interval as a Promise
|
|
379
|
+
* racing the {@link AbortSignal}. On signal: graceful exit
|
|
380
|
+
* `exit_reason: 'signal'`.
|
|
381
|
+
* 5. On Monday rate-limit wire errors (`complexity_exceeded` /
|
|
382
|
+
* `concurrency_exceeded` / `rate_limited`): appends a
|
|
383
|
+
* `poll_failed` warning to the in-flight accumulator (folded
|
|
384
|
+
* into the trailer's `_meta.warnings[]` at session end — NOT
|
|
385
|
+
* emitted as an interleaved NDJSON line per §6.3), backs off
|
|
386
|
+
* `retry_after_seconds` (60s default cap; 300s ceiling per
|
|
387
|
+
* {@link MAX_BACKOFF_SECONDS}), increments failed-poll counter.
|
|
388
|
+
* After {@link CIRCUIT_BREAKER_CONSECUTIVE_FAILS} consecutive
|
|
389
|
+
* failures trips with `exit_reason: 'circuit_broken'` and
|
|
390
|
+
* `circuit_broken_at` set; the action body inspects the result
|
|
391
|
+
* after the trailer emits and re-throws an `ApiError` so the
|
|
392
|
+
* runner emits a §6.5 failure envelope on stderr.
|
|
393
|
+
* 6. On `--max-events` / `--max-duration` ceiling reached: clean
|
|
394
|
+
* exit with the matching `exit_reason`.
|
|
395
|
+
*/
|
|
396
|
+
export const watchItem = async (inputs) => {
|
|
397
|
+
const sessionStartMs = Date.now();
|
|
398
|
+
const sessionStartIso = new Date(sessionStartMs).toISOString();
|
|
399
|
+
// Epoch floor sentinel reused for "give me the recent backlog"
|
|
400
|
+
// fetches (`--once` without `--since`, `--since` lookup). Monday's
|
|
401
|
+
// `activity_logs(from:, limit:)` returns most-recent-first up to
|
|
402
|
+
// `limit`; with an epoch `from:` the slice covers the entire
|
|
403
|
+
// recent window the caller asked for. The chronological sort
|
|
404
|
+
// inside `processRows` then re-orders for emission.
|
|
405
|
+
const EPOCH_FLOOR = '1970-01-01T00:00:00Z';
|
|
406
|
+
const includeKinds = inputs.includeKinds === undefined
|
|
407
|
+
? undefined
|
|
408
|
+
: new Set(inputs.includeKinds);
|
|
409
|
+
const warnings = [];
|
|
410
|
+
const unknownTracker = new Map();
|
|
411
|
+
let eventsEmitted = 0;
|
|
412
|
+
let pollsMade = 0;
|
|
413
|
+
let failedPolls = 0;
|
|
414
|
+
let consecutiveFailures = 0;
|
|
415
|
+
let armedFor; // monday_code that armed; cleared on success
|
|
416
|
+
let lastSeenEventId = null;
|
|
417
|
+
let circuitBrokenAt = null;
|
|
418
|
+
// Read `signal.aborted` via a function so TS narrowing on the
|
|
419
|
+
// top-of-loop `if (inputs.signal.aborted)` doesn't lock the type
|
|
420
|
+
// into `false` for subsequent post-await reads — the runtime value
|
|
421
|
+
// can flip between awaits when the thunk's own abort fires mid-
|
|
422
|
+
// call. Mirrors `src/api/retry.ts:withRetry`'s `isAborted` shape.
|
|
423
|
+
const isAborted = () => inputs.signal.aborted;
|
|
424
|
+
/**
|
|
425
|
+
* Builds the immutable result snapshot. Defined upfront so the
|
|
426
|
+
* pre-loop bootstrap (`resolveSinceWatermark`) can also exit via
|
|
427
|
+
* `signal` cleanly with a trailer (Codex impl review round-1 P1-1
|
|
428
|
+
* fix: aborts during in-flight wire calls must surface the
|
|
429
|
+
* `exit_reason: 'signal'` trailer rather than rethrowing past the
|
|
430
|
+
* action body's `stream.writeTrailer`).
|
|
431
|
+
*/
|
|
432
|
+
const buildResult = (exitReason) => {
|
|
433
|
+
const finalUnknownWarnings = Array.from(unknownTracker.values())
|
|
434
|
+
.sort((a, b) =>
|
|
435
|
+
/* c8 ignore next */
|
|
436
|
+
a.event < b.event ? -1 : a.event > b.event ? 1 : 0)
|
|
437
|
+
.map((entry) => buildUnknownEventKindWarning(entry.event, entry.entity, entry.count));
|
|
438
|
+
return {
|
|
439
|
+
events_emitted: eventsEmitted,
|
|
440
|
+
polls_made: pollsMade,
|
|
441
|
+
failed_polls: failedPolls,
|
|
442
|
+
watch_duration_seconds: (Date.now() - sessionStartMs) / 1000,
|
|
443
|
+
last_seen_event_id: lastSeenEventId,
|
|
444
|
+
circuit_broken_at: circuitBrokenAt,
|
|
445
|
+
exit_reason: exitReason,
|
|
446
|
+
warnings: [...warnings, ...finalUnknownWarnings],
|
|
447
|
+
source: 'live',
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
// Initial poll-from watermark + the optional `--since` boundary
|
|
451
|
+
// that excludes already-seen events. `--since`: look up the
|
|
452
|
+
// event's created_at; absent → now (only events strictly after
|
|
453
|
+
// session start surface in the polling loop; `--once` overrides
|
|
454
|
+
// with the epoch floor below to drain the recent backlog).
|
|
455
|
+
let watermark;
|
|
456
|
+
// `sinceBoundary` excludes events <= the boundary (Codex impl
|
|
457
|
+
// review round-1 P2-1 fix): without it, a `--since` resume across
|
|
458
|
+
// a same-timestamp tuple re-emits events the prior session already
|
|
459
|
+
// emitted. Compared via `compareBigIntStrings` (numeric, not lex).
|
|
460
|
+
let sinceBoundary;
|
|
461
|
+
const seenEventIds = new Set();
|
|
462
|
+
if (inputs.since !== undefined) {
|
|
463
|
+
try {
|
|
464
|
+
const sinceRow = await resolveSinceWatermark({
|
|
465
|
+
client: inputs.client,
|
|
466
|
+
boardId: inputs.boardId,
|
|
467
|
+
itemId: inputs.itemId,
|
|
468
|
+
sinceEventId: inputs.since,
|
|
469
|
+
});
|
|
470
|
+
sinceBoundary = { createdAt: sinceRow.created_at, id: sinceRow.id };
|
|
471
|
+
watermark = sinceRow.created_at;
|
|
472
|
+
seenEventIds.add(sinceRow.id);
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
// P1-1 fix scope: if SIGINT fired mid-lookup, emit a clean
|
|
476
|
+
// signal trailer instead of letting the transport's abort
|
|
477
|
+
// wrap propagate as `internal_error`. UsageError (unknown
|
|
478
|
+
// event-id) propagates unchanged.
|
|
479
|
+
if (isAborted() && !(err instanceof UsageError)) {
|
|
480
|
+
return buildResult('signal');
|
|
481
|
+
}
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
watermark = sessionStartIso;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Per-tick processor: chronological sort, walker-side entity
|
|
490
|
+
* filter, dedup via the Set, projection, `--include` filter, and
|
|
491
|
+
* per-event emit via `onEvent`. Returns the early-exit reason when
|
|
492
|
+
* a ceiling fires mid-poll; otherwise undefined (continue looping).
|
|
493
|
+
*/
|
|
494
|
+
const processRows = async (rows) => {
|
|
495
|
+
const sorted = sortChronological(rows);
|
|
496
|
+
for (const row of sorted) {
|
|
497
|
+
if (seenEventIds.has(row.id))
|
|
498
|
+
continue;
|
|
499
|
+
// P2-1 fix: skip rows at-or-before the `--since` boundary
|
|
500
|
+
// (by created_at, with BigInt id tie-break). Without this,
|
|
501
|
+
// a resumed session re-emits events sharing a created_at
|
|
502
|
+
// tuple with the bootstrap event.
|
|
503
|
+
if (sinceBoundary !== undefined) {
|
|
504
|
+
if (row.created_at < sinceBoundary.createdAt)
|
|
505
|
+
continue;
|
|
506
|
+
if (row.created_at === sinceBoundary.createdAt &&
|
|
507
|
+
compareBigIntStrings(row.id, sinceBoundary.id) <= 0) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
seenEventIds.add(row.id);
|
|
512
|
+
// Always advance the wall-clock watermark — even for filtered
|
|
513
|
+
// rows — so the next poll's `from:` doesn't re-fetch them.
|
|
514
|
+
if (row.created_at > watermark) {
|
|
515
|
+
watermark = row.created_at;
|
|
516
|
+
}
|
|
517
|
+
// Walker-side entity filter per M24 Decision 2 closure (single
|
|
518
|
+
// source of truth at the walker layer; projector does NOT
|
|
519
|
+
// re-filter). Drops board-scoped events that leak through the
|
|
520
|
+
// `iid` arg.
|
|
521
|
+
if (row.entity !== ITEM_SCOPED_ENTITY)
|
|
522
|
+
continue;
|
|
523
|
+
const event = projectActivityLogRow({ row });
|
|
524
|
+
// Track unknown event kinds for warning aggregation; one
|
|
525
|
+
// warning per unique kind at session end (matches M24's
|
|
526
|
+
// unknownByKey shape so re-walks against the same stream
|
|
527
|
+
// produce identical envelopes).
|
|
528
|
+
if (event.kind === 'unknown') {
|
|
529
|
+
const key = `${event.event}\x00${event.entity}`;
|
|
530
|
+
const entry = unknownTracker.get(key);
|
|
531
|
+
if (entry === undefined) {
|
|
532
|
+
unknownTracker.set(key, {
|
|
533
|
+
event: event.event,
|
|
534
|
+
entity: event.entity,
|
|
535
|
+
count: 1,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
entry.count++;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// `--include` filter applied AFTER projection so unknown-event-
|
|
543
|
+
// kind aggregation still surfaces (mirrors M24's filter
|
|
544
|
+
// semantics).
|
|
545
|
+
if (includeKinds !== undefined && !includeKinds.has(event.kind)) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
await inputs.onEvent(event);
|
|
549
|
+
eventsEmitted++;
|
|
550
|
+
lastSeenEventId = row.id;
|
|
551
|
+
if (inputs.maxEvents !== undefined &&
|
|
552
|
+
eventsEmitted >= inputs.maxEvents) {
|
|
553
|
+
return 'max_events';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return undefined;
|
|
557
|
+
};
|
|
558
|
+
// --once short-circuit: one poll, drain backlog, exit. P1-2 fix:
|
|
559
|
+
// without `--since`, the backlog drain pulls from the epoch floor
|
|
560
|
+
// (Monday returns the most-recent N events; chronological sort
|
|
561
|
+
// re-orders for emission), NOT from session start — the contract
|
|
562
|
+
// is "drain the recent N events", not "wait for new events". With
|
|
563
|
+
// `--since`, the `--since` row's created_at is the lower bound;
|
|
564
|
+
// limit 500 covers the resumption window.
|
|
565
|
+
if (inputs.once === true) {
|
|
566
|
+
try {
|
|
567
|
+
const rows = await fetchPoll({
|
|
568
|
+
client: inputs.client,
|
|
569
|
+
boardId: inputs.boardId,
|
|
570
|
+
itemId: inputs.itemId,
|
|
571
|
+
from: inputs.since === undefined ? EPOCH_FLOOR : watermark,
|
|
572
|
+
limit: inputs.since === undefined ? DEFAULT_ONCE_BACKLOG_LIMIT : 500,
|
|
573
|
+
});
|
|
574
|
+
pollsMade++;
|
|
575
|
+
const early = await processRows(rows);
|
|
576
|
+
return buildResult(early ?? 'once_complete');
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
// P1-1 fix scope: SIGINT mid-once-poll → trailer with signal
|
|
580
|
+
// exit rather than rethrow.
|
|
581
|
+
if (isAborted()) {
|
|
582
|
+
return buildResult('signal');
|
|
583
|
+
}
|
|
584
|
+
throw err;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// P2-2 fix: compute a deadline once so cadence/backoff sleeps can
|
|
588
|
+
// shrink to `min(intervalMs, remainingMs)` and don't overshoot
|
|
589
|
+
// `--max-duration`. Without the deadline-aware sleep, a
|
|
590
|
+
// `--max-duration 5` with default 30s cadence would exit at ~30s
|
|
591
|
+
// (full cadence then top-of-loop check), and a circuit-breaker
|
|
592
|
+
// backoff could overshoot by up to `MAX_BACKOFF_SECONDS` (300s).
|
|
593
|
+
const deadlineMs = inputs.maxDurationSeconds === undefined
|
|
594
|
+
? Number.POSITIVE_INFINITY
|
|
595
|
+
: sessionStartMs + inputs.maxDurationSeconds * 1000;
|
|
596
|
+
const remainingMs = () => Math.max(0, deadlineMs - Date.now());
|
|
597
|
+
// Polling loop. Each iteration: signal check → ceiling check →
|
|
598
|
+
// poll → process events → cadence wait. Early exits route through
|
|
599
|
+
// `buildResult` for uniform shape.
|
|
600
|
+
for (;;) {
|
|
601
|
+
// Defensive: in practice the cadence-wait below catches the
|
|
602
|
+
// abort signal and exits via the catch-and-return-signal branch;
|
|
603
|
+
// this top-of-loop check covers the narrow race where the
|
|
604
|
+
// cadence completed cleanly but the signal aborted between then
|
|
605
|
+
// and the next iteration. Hard to drive deterministically from
|
|
606
|
+
// an integration test (the cadence catch wins almost always).
|
|
607
|
+
/* c8 ignore start */
|
|
608
|
+
if (isAborted()) {
|
|
609
|
+
return buildResult('signal');
|
|
610
|
+
}
|
|
611
|
+
/* c8 ignore stop */
|
|
612
|
+
if (remainingMs() <= 0) {
|
|
613
|
+
return buildResult('max_duration');
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const rows = await fetchPoll({
|
|
617
|
+
client: inputs.client,
|
|
618
|
+
boardId: inputs.boardId,
|
|
619
|
+
itemId: inputs.itemId,
|
|
620
|
+
from: watermark,
|
|
621
|
+
limit: 100,
|
|
622
|
+
});
|
|
623
|
+
pollsMade++;
|
|
624
|
+
consecutiveFailures = 0;
|
|
625
|
+
armedFor = undefined;
|
|
626
|
+
const early = await processRows(rows);
|
|
627
|
+
if (early !== undefined) {
|
|
628
|
+
return buildResult(early);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
// P1-1 fix: SIGINT mid-poll surfaces as a non-circuit-breaker
|
|
633
|
+
// error from the transport's abort wrap. Detect via
|
|
634
|
+
// `isAborted()` and exit with `signal` trailer instead of
|
|
635
|
+
// rethrowing past the action body's `stream.writeTrailer`.
|
|
636
|
+
if (isAborted()) {
|
|
637
|
+
return buildResult('signal');
|
|
638
|
+
}
|
|
639
|
+
if (!isCircuitBreakerError(err))
|
|
640
|
+
throw err;
|
|
641
|
+
failedPolls++;
|
|
642
|
+
consecutiveFailures++;
|
|
643
|
+
const backoffSeconds = backoffSecondsFrom(err);
|
|
644
|
+
warnings.push({
|
|
645
|
+
code: 'poll_failed',
|
|
646
|
+
message: `poll ${String(pollsMade + failedPolls)} failed with ${err.code} (${String(consecutiveFailures)}/${String(CIRCUIT_BREAKER_CONSECUTIVE_FAILS)} consecutive); backing off ${String(backoffSeconds)}s`,
|
|
647
|
+
details: {
|
|
648
|
+
consecutive_failures: consecutiveFailures,
|
|
649
|
+
monday_code: err.code,
|
|
650
|
+
backoff_seconds: backoffSeconds,
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
// Trip on the Nth consecutive failure — circuit_broken exit;
|
|
654
|
+
// the action body re-throws after the trailer emits so a §6.5
|
|
655
|
+
// failure envelope surfaces on stderr.
|
|
656
|
+
if (consecutiveFailures >= CIRCUIT_BREAKER_CONSECUTIVE_FAILS) {
|
|
657
|
+
circuitBrokenAt = new Date().toISOString();
|
|
658
|
+
return buildResult('circuit_broken');
|
|
659
|
+
}
|
|
660
|
+
// Arm at the N-1 boundary (once per arming window) — surfaces
|
|
661
|
+
// a single `circuit_breaker_armed` warning per arming so the
|
|
662
|
+
// accumulator stays bounded even on prolonged bursts.
|
|
663
|
+
if (consecutiveFailures === CIRCUIT_BREAKER_CONSECUTIVE_FAILS - 1 &&
|
|
664
|
+
armedFor !== err.code) {
|
|
665
|
+
armedFor = err.code;
|
|
666
|
+
warnings.push({
|
|
667
|
+
code: 'circuit_breaker_armed',
|
|
668
|
+
message: `circuit breaker armed: one more ${err.code} failure trips the session`,
|
|
669
|
+
details: {
|
|
670
|
+
polls_until_trip: 1,
|
|
671
|
+
monday_code: err.code,
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
// Backoff sleep — capped at the `--max-duration` remaining
|
|
676
|
+
// window so the breaker can't overshoot the wall-clock
|
|
677
|
+
// ceiling. A 0ms sleep (`retry_in_seconds: 0` from Monday's
|
|
678
|
+
// hint) is fine — the loop top picks up immediately. Signal-
|
|
679
|
+
// driven graceful exit interrupts via the sleep's rejection.
|
|
680
|
+
const backoffMs = Math.min(backoffSeconds * 1000, remainingMs());
|
|
681
|
+
if (backoffMs > 0) {
|
|
682
|
+
try {
|
|
683
|
+
await sleepWithSignal(backoffMs, inputs.signal);
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
return buildResult('signal');
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
// Cadence wait between successful polls — capped at the
|
|
692
|
+
// `--max-duration` remaining window so the next iteration's
|
|
693
|
+
// ceiling-check exits with `max_duration` precisely rather than
|
|
694
|
+
// overshooting by up to a full interval. `intervalMs >= 1000`
|
|
695
|
+
// per the argv schema so the only way `cadenceMs` is 0 is when
|
|
696
|
+
// the deadline already elapsed; the top-of-loop check catches
|
|
697
|
+
// that on the next iteration.
|
|
698
|
+
const cadenceMs = Math.min(inputs.intervalMs, remainingMs());
|
|
699
|
+
if (cadenceMs > 0) {
|
|
700
|
+
try {
|
|
701
|
+
await sleepWithSignal(cadenceMs, inputs.signal);
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return buildResult('signal');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
//# sourceMappingURL=item-watch.js.map
|