@trawlme/cli 1.18.4 → 1.18.5
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/dist/commands/scraps.d.ts +20 -1
- package/dist/commands/scraps.js +74 -21
- package/dist/commands/skills.js +19 -1
- package/package.json +1 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
export declare const scraps: Command;
|
|
3
|
+
/** The top-of-history snapshot pollRunProgress needs to identify which run
|
|
4
|
+
* it's watching — see captureBeforeRunState. */
|
|
5
|
+
interface BeforeRunState {
|
|
6
|
+
id?: string;
|
|
7
|
+
alreadyInFlight: boolean;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* #91 P1 — replaces "await the run to completion, THEN open the activities
|
|
5
11
|
* SSE stream" (which showed NOTHING: the activities SSE
|
|
@@ -20,8 +26,21 @@ export declare const scraps: Command;
|
|
|
20
26
|
* - GET /api/scraps/:id — history[0].status/statusDetail, the SAME
|
|
21
27
|
* terminal-status signal `lastStatus()` above already trusts (status:null
|
|
22
28
|
* === in flight, #88 item 1) to know when the run is done.
|
|
29
|
+
*
|
|
30
|
+
* #93 item 1 — dedup-race fix. "Is this the run we're watching?" used to be
|
|
31
|
+
* a single check: `history[0]._id !== beforeHistoryId`. That's wrong when
|
|
32
|
+
* `before.alreadyInFlight` is true (a `trigger` call deduped onto a worker
|
|
33
|
+
* job that was ALREADY pending/running at capture time): the top row IS the
|
|
34
|
+
* run we're watching, but its `_id` never changes, so the old guard never
|
|
35
|
+
* released and the poll ran the full timeout to a false "Timed out". The run
|
|
36
|
+
* we're watching is now EITHER a brand-new id (fresh trigger, the common
|
|
37
|
+
* case) OR the same id that was already in-flight (status:null) at capture
|
|
38
|
+
* (the dedup case) — a same-id row that was already TERMINAL at capture is
|
|
39
|
+
* neither, and must not be latched onto as "done" (it's just the previous
|
|
40
|
+
* run, still sitting there until a genuinely new run supersedes it).
|
|
23
41
|
*/
|
|
24
|
-
export declare function pollRunProgress(id: string,
|
|
42
|
+
export declare function pollRunProgress(id: string, before: BeforeRunState | undefined, opts?: {
|
|
25
43
|
intervalMs?: number;
|
|
26
44
|
timeoutMs?: number;
|
|
27
45
|
}): Promise<void>;
|
|
46
|
+
export {};
|
package/dist/commands/scraps.js
CHANGED
|
@@ -83,21 +83,30 @@ async function watchActivities(id) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* #91 P1 — snapshot the current top history entry BEFORE
|
|
87
|
-
* so pollRunProgress can later
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
86
|
+
* #91 P1 / #93 item 1 — snapshot the current top history entry BEFORE
|
|
87
|
+
* triggering a run, so pollRunProgress can later tell "the run we just
|
|
88
|
+
* launched" apart from whatever the last run happened to be. Node persists a
|
|
89
|
+
* fresh {status:null, inFlight:true} row the moment a run starts (see
|
|
90
|
+
* HistorysService.create in trawl_node), so a changed history[0]._id is
|
|
91
|
+
* normally an honest "the new run has begun" signal — EXCEPT `trigger`
|
|
92
|
+
* (method:'worker') dedups onto an already-pending/running worker job
|
|
93
|
+
* (ScrapJobsService, LIVE_STATUSES) instead of creating a new row: the top
|
|
94
|
+
* row's `_id` never changes even though this launch IS that run. Recording
|
|
95
|
+
* `alreadyInFlight` (status===null at capture time) lets pollRunProgress
|
|
96
|
+
* recognize that dedup case too, instead of waiting forever for an `_id`
|
|
97
|
+
* that will never arrive (#93 item 1).
|
|
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).
|
|
93
101
|
*/
|
|
94
|
-
async function
|
|
102
|
+
async function captureBeforeRunState(id) {
|
|
95
103
|
try {
|
|
96
104
|
const scrap = await api.get(`/api/scraps/${id}`);
|
|
97
|
-
|
|
105
|
+
const top = scrap.history?.[0];
|
|
106
|
+
return { id: top?._id, alreadyInFlight: top?.status === null };
|
|
98
107
|
}
|
|
99
108
|
catch {
|
|
100
|
-
return undefined;
|
|
109
|
+
return { id: undefined, alreadyInFlight: false };
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
function sleep(ms) {
|
|
@@ -127,10 +136,24 @@ const POLL_TIMEOUT_MS = LONG_RUN_TIMEOUT_MS;
|
|
|
127
136
|
* - GET /api/scraps/:id — history[0].status/statusDetail, the SAME
|
|
128
137
|
* terminal-status signal `lastStatus()` above already trusts (status:null
|
|
129
138
|
* === in flight, #88 item 1) to know when the run is done.
|
|
139
|
+
*
|
|
140
|
+
* #93 item 1 — dedup-race fix. "Is this the run we're watching?" used to be
|
|
141
|
+
* a single check: `history[0]._id !== beforeHistoryId`. That's wrong when
|
|
142
|
+
* `before.alreadyInFlight` is true (a `trigger` call deduped onto a worker
|
|
143
|
+
* job that was ALREADY pending/running at capture time): the top row IS the
|
|
144
|
+
* run we're watching, but its `_id` never changes, so the old guard never
|
|
145
|
+
* released and the poll ran the full timeout to a false "Timed out". The run
|
|
146
|
+
* we're watching is now EITHER a brand-new id (fresh trigger, the common
|
|
147
|
+
* case) OR the same id that was already in-flight (status:null) at capture
|
|
148
|
+
* (the dedup case) — a same-id row that was already TERMINAL at capture is
|
|
149
|
+
* neither, and must not be latched onto as "done" (it's just the previous
|
|
150
|
+
* run, still sitting there until a genuinely new run supersedes it).
|
|
130
151
|
*/
|
|
131
|
-
export async function pollRunProgress(id,
|
|
152
|
+
export async function pollRunProgress(id, before, opts = {}) {
|
|
132
153
|
const intervalMs = opts.intervalMs ?? POLL_INTERVAL_MS;
|
|
133
154
|
const timeoutMs = opts.timeoutMs ?? POLL_TIMEOUT_MS;
|
|
155
|
+
const beforeId = before?.id;
|
|
156
|
+
const beforeAlreadyInFlight = before?.alreadyInFlight ?? false;
|
|
134
157
|
console.log(chalk.dim('Live activity streaming has no signal for this run (async/cross-pod) — polling for progress instead…\n'));
|
|
135
158
|
const deadline = Date.now() + timeoutMs;
|
|
136
159
|
const seen = new Set();
|
|
@@ -147,8 +170,12 @@ export async function pollRunProgress(id, beforeHistoryId, opts = {}) {
|
|
|
147
170
|
continue; // transient — keep polling rather than aborting the wait
|
|
148
171
|
}
|
|
149
172
|
const last = scrap.history?.[0];
|
|
150
|
-
if (!last?._id
|
|
151
|
-
continue; //
|
|
173
|
+
if (!last?._id)
|
|
174
|
+
continue; // no history row recorded yet
|
|
175
|
+
const isNewRun = last._id !== beforeId;
|
|
176
|
+
const isDedupOntoInFlight = last._id === beforeId && beforeAlreadyInFlight;
|
|
177
|
+
if (!isNewRun && !isDedupOntoInFlight)
|
|
178
|
+
continue; // still the stale previous run
|
|
152
179
|
try {
|
|
153
180
|
const activities = await api.get(`/api/scraps/${id}/activities?history=${last._id}&limit=20`);
|
|
154
181
|
// Server returns newest-first — print unseen ones oldest-first.
|
|
@@ -534,9 +561,10 @@ scraps
|
|
|
534
561
|
.option('-w, --watch', 'Show progress after launching (polls — see `trawl scraps trigger --watch`, #91)')
|
|
535
562
|
.action(async (id, opts) => {
|
|
536
563
|
validateObjectId(id);
|
|
537
|
-
// #91 P1 — captured BEFORE launching so pollRunProgress can
|
|
538
|
-
// that's about to finish" apart from whatever the last run
|
|
539
|
-
|
|
564
|
+
// #91 P1 / #93 item 1 — captured BEFORE launching so pollRunProgress can
|
|
565
|
+
// tell "the run that's about to finish" apart from whatever the last run
|
|
566
|
+
// happened to be (including a dedup onto an already-in-flight run).
|
|
567
|
+
const beforeRun = opts.watch ? await captureBeforeRunState(id) : undefined;
|
|
540
568
|
// #91 P0 — GET /api/scraps/load/:id runs the scrap synchronously
|
|
541
569
|
// server-side (30-250s); the 30s default was aborting it mid-flight.
|
|
542
570
|
await oraPromise(() => api.get(`/api/scraps/load/${id}`, { timeoutMs: LONG_RUN_TIMEOUT_MS }), {
|
|
@@ -544,7 +572,7 @@ scraps
|
|
|
544
572
|
successText: 'Scrap launched',
|
|
545
573
|
});
|
|
546
574
|
if (opts.watch) {
|
|
547
|
-
await pollRunProgress(id,
|
|
575
|
+
await pollRunProgress(id, beforeRun);
|
|
548
576
|
}
|
|
549
577
|
});
|
|
550
578
|
// #70 — render an items array either as a table summary or --json. Shared by
|
|
@@ -641,6 +669,27 @@ scraps
|
|
|
641
669
|
console.log(chalk.dim('No data yet. Run the scrap first.'));
|
|
642
670
|
return;
|
|
643
671
|
}
|
|
672
|
+
// #93 item 2 — --fresh used to render items without ever checking for a
|
|
673
|
+
// regression, unlike the persisted `data` path below (#88 item 2). The
|
|
674
|
+
// load() response's OWN embedded `scrap.history[0]` can't be trusted for
|
|
675
|
+
// this (see the ScrapLoadResult comment above): it may be the previous
|
|
676
|
+
// run's row, and even the right row never carries the regression flip.
|
|
677
|
+
// `--fresh` runs synchronously to completion server-side though — by the
|
|
678
|
+
// time this call returns, node has already awaited the regression patch
|
|
679
|
+
// — so a fresh GET /api/scraps/:id (the SAME read the persisted path
|
|
680
|
+
// below already trusts) reliably observes the finalized DB state.
|
|
681
|
+
// Best-effort: never fail --fresh's real output over this side check,
|
|
682
|
+
// and never gate on it — the items returned ARE this run's real,
|
|
683
|
+
// synchronously-computed data regardless of what this check finds.
|
|
684
|
+
try {
|
|
685
|
+
const fresh = await api.get(`/api/scraps/${id}`);
|
|
686
|
+
if (fresh.history?.[0]?.statusDetail === 'regression') {
|
|
687
|
+
console.error(chalk.yellow(`⚠ Item count regressed vs baseline for the last run of ${id} — see: trawl scraps doctor ${id}`));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
// best-effort — the fresh run's items are still valid without this check
|
|
692
|
+
}
|
|
644
693
|
renderScrapItems(items, opts.json);
|
|
645
694
|
return;
|
|
646
695
|
}
|
|
@@ -862,9 +911,13 @@ scraps
|
|
|
862
911
|
.option('--wait', 'Run synchronously and wait for the result (legacy behaviour)')
|
|
863
912
|
.action(async (id, opts) => {
|
|
864
913
|
validateObjectId(id);
|
|
865
|
-
// #91 P1 — captured BEFORE triggering so pollRunProgress can
|
|
866
|
-
// run we just triggered" apart from whatever the last run
|
|
867
|
-
|
|
914
|
+
// #91 P1 / #93 item 1 — captured BEFORE triggering so pollRunProgress can
|
|
915
|
+
// tell "the run we just triggered" apart from whatever the last run
|
|
916
|
+
// happened to be. This is the dedup-prone path: `trigger`'s method:'worker'
|
|
917
|
+
// collapses onto an already-pending/running worker job for the same scrap
|
|
918
|
+
// (ScrapJobsService, LIVE_STATUSES) instead of creating a new history row —
|
|
919
|
+
// captureBeforeRunState records that so pollRunProgress can still track it.
|
|
920
|
+
const beforeRun = opts.watch ? await captureBeforeRunState(id) : undefined;
|
|
868
921
|
// #50 — default async: the backend (#1313) kicks off the run and returns a
|
|
869
922
|
// 'queued' envelope immediately instead of holding the connection for the
|
|
870
923
|
// whole run. --wait restores the old synchronous round-trip.
|
|
@@ -878,7 +931,7 @@ scraps
|
|
|
878
931
|
successText: opts.wait ? 'Worker run complete' : 'Worker triggered',
|
|
879
932
|
});
|
|
880
933
|
if (opts.watch)
|
|
881
|
-
await pollRunProgress(id,
|
|
934
|
+
await pollRunProgress(id, beforeRun);
|
|
882
935
|
});
|
|
883
936
|
// account subcommand group
|
|
884
937
|
const account = scraps
|
package/dist/commands/skills.js
CHANGED
|
@@ -34,13 +34,31 @@ skills
|
|
|
34
34
|
// #91 — same EISDIR class as autoUpdateInstalledSkills/installSkill: an
|
|
35
35
|
// unreadable `.version` marker on one skill must not crash the whole
|
|
36
36
|
// listing before the other skills are shown.
|
|
37
|
+
//
|
|
38
|
+
// #93 item 3 — the two scope reads must be guarded INDEPENDENTLY. A
|
|
39
|
+
// single try/catch wrapped around `getInstalledVersion(name, 'user') ??
|
|
40
|
+
// getInstalledVersion(name, 'local')` still throws the whole expression
|
|
41
|
+
// the moment the 'user' read throws (e.g. a corrupt/EISDIR `.version`
|
|
42
|
+
// marker) — `??` never gets a chance to evaluate the 'local' fallback,
|
|
43
|
+
// so a healthy local install gets masked as "not installed" too. Every
|
|
44
|
+
// other marker read in this codebase (installSkill, removeOrphanedSkills,
|
|
45
|
+
// autoUpdateInstalledSkills — all in lib/skills.ts) already guards each
|
|
46
|
+
// scope on its own; this was the one chained exception.
|
|
37
47
|
let installedVersion;
|
|
38
48
|
try {
|
|
39
|
-
installedVersion = getInstalledVersion(name, 'user')
|
|
49
|
+
installedVersion = getInstalledVersion(name, 'user');
|
|
40
50
|
}
|
|
41
51
|
catch {
|
|
42
52
|
installedVersion = null;
|
|
43
53
|
}
|
|
54
|
+
if (installedVersion === null) {
|
|
55
|
+
try {
|
|
56
|
+
installedVersion = getInstalledVersion(name, 'local');
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
installedVersion = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
44
62
|
const stale = installedVersion && installedVersion !== version;
|
|
45
63
|
const tag = userInstalled
|
|
46
64
|
? localInstalled
|