@trawlme/cli 1.18.5 → 1.18.7

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.
@@ -1,10 +1,21 @@
1
1
  import { Command } from 'commander';
2
2
  export declare const scraps: Command;
3
3
  /** The top-of-history snapshot pollRunProgress needs to identify which run
4
- * it's watching — see captureBeforeRunState. */
4
+ * it's watching — see captureBeforeRunState.
5
+ *
6
+ * #97 — `captured` discriminates WHY `id` is undefined: `true` means the GET
7
+ * succeeded and the scrap genuinely has no history yet (an honest "never
8
+ * run" signal pollRunProgress can trust immediately); `false` means the GET
9
+ * itself threw, so `id`/`alreadyInFlight` carry NO information at all — the
10
+ * scrap could easily have prior (possibly terminal) history that this
11
+ * lookup simply never saw. Before this field existed, both cases produced
12
+ * the identical `{id: undefined, alreadyInFlight: false}` shape, so
13
+ * pollRunProgress could not tell them apart (see its own #97 comment).
14
+ */
5
15
  interface BeforeRunState {
6
16
  id?: string;
7
17
  alreadyInFlight: boolean;
18
+ captured: boolean;
8
19
  }
9
20
  /**
10
21
  * #91 P1 — replaces "await the run to completion, THEN open the activities
@@ -38,6 +49,35 @@ interface BeforeRunState {
38
49
  * (the dedup case) — a same-id row that was already TERMINAL at capture is
39
50
  * neither, and must not be latched onto as "done" (it's just the previous
40
51
  * run, still sitting there until a genuinely new run supersedes it).
52
+ *
53
+ * #97 — capture-failed fallback. The dedup-race fix above assumes `before`
54
+ * is trustworthy. When `captureBeforeRunState`'s own GET threw,
55
+ * `before.id` is `undefined` — and that is INDISTINGUISHABLE from "the
56
+ * scrap has genuinely never run" (also `id: undefined`), which is exactly
57
+ * the case `isNewRun` above is designed to match on the very first row that
58
+ * ever appears. So on the very first poll, ANY pre-existing history row —
59
+ * even the STALE PREVIOUS run, already terminal — satisfied
60
+ * `last._id !== undefined` and got reported as "the run we just launched"
61
+ * finishing, when it was really just whatever ran before.
62
+ *
63
+ * Fix: `before.captured === false` defers trusting a baseline at all.
64
+ * Instead of comparing against the (unknown) `beforeId` from the start, the
65
+ * FIRST successful poll read is treated as the deferred capture itself —
66
+ * exactly what `captureBeforeRunState` would have returned had its GET
67
+ * succeeded — and only READS from that point on are compared against it,
68
+ * via the exact same `isNewRun` / `isDedupOntoInFlight` logic above. A
69
+ * pre-existing terminal row observed on that first read becomes `beforeId`
70
+ * (not a match for itself), so it correctly falls into "still the stale
71
+ * previous run" below and the poll keeps waiting; a row still in flight
72
+ * becomes the `alreadyInFlight` baseline, exactly like a successful capture
73
+ * would have recorded. Either way this costs at most one extra poll
74
+ * interval, bounded by the same deadline as everything else. The one
75
+ * remaining edge case — capture failed AND the scrap never ran before AND
76
+ * the triggered run already finished by the very first poll — is genuinely
77
+ * undecidable from "stale pre-existing row" with no more information than
78
+ * this function has, so it resolves to the same honest timeout rather than
79
+ * risk reporting a possibly-wrong outcome (never a lie, at worst a timeout
80
+ * telling the caller to check `doctor`).
41
81
  */
42
82
  export declare function pollRunProgress(id: string, before: BeforeRunState | undefined, opts?: {
43
83
  intervalMs?: number;
@@ -96,17 +96,21 @@ async function watchActivities(id) {
96
96
  * recognize that dedup case too, instead of waiting forever for an `_id`
97
97
  * that will never arrive (#93 item 1).
98
98
  *
99
- * Best-effort: a failed lookup falls back to `{alreadyInFlight:false}`,
100
- * which is still correct for a scrap that has never run (id undefined).
99
+ * #97 a failed lookup used to fall back to `{alreadyInFlight:false}` with
100
+ * no `id`, which LOOKED identical to "scrap has never run" but is not:
101
+ * the scrap may well have prior (possibly terminal) history this GET simply
102
+ * never observed. `captured:false` flags that distinction explicitly so
103
+ * pollRunProgress can defer trusting a baseline instead of risking a stale
104
+ * pre-existing row being reported as the run just launched.
101
105
  */
102
106
  async function captureBeforeRunState(id) {
103
107
  try {
104
108
  const scrap = await api.get(`/api/scraps/${id}`);
105
109
  const top = scrap.history?.[0];
106
- return { id: top?._id, alreadyInFlight: top?.status === null };
110
+ return { id: top?._id, alreadyInFlight: top?.status === null, captured: true };
107
111
  }
108
112
  catch {
109
- return { id: undefined, alreadyInFlight: false };
113
+ return { id: undefined, alreadyInFlight: false, captured: false };
110
114
  }
111
115
  }
112
116
  function sleep(ms) {
@@ -148,12 +152,46 @@ const POLL_TIMEOUT_MS = LONG_RUN_TIMEOUT_MS;
148
152
  * (the dedup case) — a same-id row that was already TERMINAL at capture is
149
153
  * neither, and must not be latched onto as "done" (it's just the previous
150
154
  * run, still sitting there until a genuinely new run supersedes it).
155
+ *
156
+ * #97 — capture-failed fallback. The dedup-race fix above assumes `before`
157
+ * is trustworthy. When `captureBeforeRunState`'s own GET threw,
158
+ * `before.id` is `undefined` — and that is INDISTINGUISHABLE from "the
159
+ * scrap has genuinely never run" (also `id: undefined`), which is exactly
160
+ * the case `isNewRun` above is designed to match on the very first row that
161
+ * ever appears. So on the very first poll, ANY pre-existing history row —
162
+ * even the STALE PREVIOUS run, already terminal — satisfied
163
+ * `last._id !== undefined` and got reported as "the run we just launched"
164
+ * finishing, when it was really just whatever ran before.
165
+ *
166
+ * Fix: `before.captured === false` defers trusting a baseline at all.
167
+ * Instead of comparing against the (unknown) `beforeId` from the start, the
168
+ * FIRST successful poll read is treated as the deferred capture itself —
169
+ * exactly what `captureBeforeRunState` would have returned had its GET
170
+ * succeeded — and only READS from that point on are compared against it,
171
+ * via the exact same `isNewRun` / `isDedupOntoInFlight` logic above. A
172
+ * pre-existing terminal row observed on that first read becomes `beforeId`
173
+ * (not a match for itself), so it correctly falls into "still the stale
174
+ * previous run" below and the poll keeps waiting; a row still in flight
175
+ * becomes the `alreadyInFlight` baseline, exactly like a successful capture
176
+ * would have recorded. Either way this costs at most one extra poll
177
+ * interval, bounded by the same deadline as everything else. The one
178
+ * remaining edge case — capture failed AND the scrap never ran before AND
179
+ * the triggered run already finished by the very first poll — is genuinely
180
+ * undecidable from "stale pre-existing row" with no more information than
181
+ * this function has, so it resolves to the same honest timeout rather than
182
+ * risk reporting a possibly-wrong outcome (never a lie, at worst a timeout
183
+ * telling the caller to check `doctor`).
151
184
  */
152
185
  export async function pollRunProgress(id, before, opts = {}) {
153
186
  const intervalMs = opts.intervalMs ?? POLL_INTERVAL_MS;
154
187
  const timeoutMs = opts.timeoutMs ?? POLL_TIMEOUT_MS;
155
- const beforeId = before?.id;
156
- const beforeAlreadyInFlight = before?.alreadyInFlight ?? false;
188
+ let beforeId = before?.id;
189
+ let beforeAlreadyInFlight = before?.alreadyInFlight ?? false;
190
+ // #97 — only an EXPLICIT captured:false (capture's GET actually threw)
191
+ // defers the baseline. `before` itself being undefined (callers that skip
192
+ // capture entirely) or `captured` being true/absent both mean "trust
193
+ // beforeId as given", preserving every existing call site's behavior.
194
+ let baselineEstablished = before?.captured ?? true;
157
195
  console.log(chalk.dim('Live activity streaming has no signal for this run (async/cross-pod) — polling for progress instead…\n'));
158
196
  const deadline = Date.now() + timeoutMs;
159
197
  const seen = new Set();
@@ -172,6 +210,16 @@ export async function pollRunProgress(id, before, opts = {}) {
172
210
  const last = scrap.history?.[0];
173
211
  if (!last?._id)
174
212
  continue; // no history row recorded yet
213
+ if (!baselineEstablished) {
214
+ // #97 — deferred capture: whatever we see on this first successful
215
+ // read becomes the reference point, BEFORE the isNewRun check below
216
+ // ever runs against it. This must happen before that check, not
217
+ // after, so a pre-existing terminal row is recognized as stale on
218
+ // this very same iteration rather than one poll late.
219
+ beforeId = last._id;
220
+ beforeAlreadyInFlight = last.status === null;
221
+ baselineEstablished = true;
222
+ }
175
223
  const isNewRun = last._id !== beforeId;
176
224
  const isDedupOntoInFlight = last._id === beforeId && beforeAlreadyInFlight;
177
225
  if (!isNewRun && !isDedupOntoInFlight)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "1.18.5",
3
+ "version": "1.18.7",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,7 @@
40
40
  "url": "https://github.com/comes-io/trawl_cli/issues"
41
41
  },
42
42
  "dependencies": {
43
- "@trawlme/skills": "1.3.3",
43
+ "@trawlme/skills": "1.3.4",
44
44
  "chalk": "^5.6.2",
45
45
  "commander": "^14.0.3",
46
46
  "conf": "^15.1.0",