yt-briefing 0.12.2 → 0.12.3

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.
@@ -87,6 +87,16 @@ export function bumpStatePointer(content, handle, type, videoId, date) {
87
87
  throw new Error(`bumpStatePointer: row for ${handle} not found`);
88
88
  return bumpStateFrontmatterDate(lines.join('\n'), date);
89
89
  }
90
+ /**
91
+ * Is this video already resolved for the current run? True when the per-(channel,type)
92
+ * state pointer has landed on it, OR it is in the run's `seen` set. The `seen` arm is the
93
+ * race-proof guard: `bumpStatePointer` is not monotonic, so a detached `--fill` re-bumping
94
+ * a stale pointer can transiently regress it and make a pointer-only check re-emit an
95
+ * already-rated video. `seen` is authoritative and pointer-independent.
96
+ */
97
+ export function isResolved(pointer, videoId, seen) {
98
+ return pointer === videoId || seen.includes(videoId);
99
+ }
90
100
  // ---------- channel profile: durable signal writes ----------
91
101
  /**
92
102
  * Append `line` under `## <section>`, creating the section if missing (before `## Notes`
package/dist/yt-rating.js CHANGED
@@ -19,7 +19,7 @@
19
19
  import { readFileSync, writeFileSync, existsSync } from 'fs';
20
20
  import { loadEnv } from "./lib/env.js";
21
21
  import { parseChannels, appendSkipTitle, appendNote, bumpStatePointer } from "./lib/yt-lib.js";
22
- import { CHANNELS_MD, STATE_MD, PENDING_FILE, profilePath } from "./lib/paths.js";
22
+ import { CHANNELS_MD, STATE_MD, PENDING_FILE, QUEUE_FILE, profilePath } from "./lib/paths.js";
23
23
  loadEnv();
24
24
  function getArg(args, name) {
25
25
  const idx = args.indexOf(name);
@@ -103,6 +103,20 @@ if (!args.noState) {
103
103
  stateBumped = true;
104
104
  }
105
105
  }
106
+ // 3. Mark the rated video resolved in the run queue (`seen`) — independent of the state
107
+ // pointer, so a cursor-regression race in yt-sweep (a detached `--fill` re-bumping a
108
+ // stale pointer) can't re-emit an already-rated video. Best-effort: the queue is a
109
+ // same-day throwaway and no sweep process writes it while the user rates.
110
+ if (!args.noState && existsSync(QUEUE_FILE)) {
111
+ try {
112
+ const q = JSON.parse(readFileSync(QUEUE_FILE, 'utf8'));
113
+ if (q?.built_at === date && Array.isArray(q.seen) && !q.seen.includes(args.id)) {
114
+ q.seen.push(args.id);
115
+ writeFileSync(QUEUE_FILE, JSON.stringify(q), 'utf8');
116
+ }
117
+ }
118
+ catch { /* corrupt / foreign queue → ignore; the next sweep rebuilds it */ }
119
+ }
106
120
  console.log(JSON.stringify({
107
121
  ok: true,
108
122
  profile: `channels/${ch.slug}.md`,
package/dist/yt-sweep.js CHANGED
@@ -50,7 +50,7 @@
50
50
  import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, renameSync, appendFileSync } from 'node:fs';
51
51
  import { spawn } from 'node:child_process';
52
52
  import { loadEnv, missingEnv, missingEnvMessage, REQUIRED_LLM, REQUIRED_YOUTUBE } from "./lib/env.js";
53
- import { parseChannels, parseState, bumpStatePointer } from "./lib/yt-lib.js";
53
+ import { parseChannels, parseState, bumpStatePointer, isResolved } from "./lib/yt-lib.js";
54
54
  import { chat, getModel } from "./lib/llm.js";
55
55
  import { outputLang } from "./lib/config.js";
56
56
  import { PKG_ROOT, CHANNELS_MD, STATE_MD, CACHE_DIR, QUEUE_FILE, REST_FILE, PENDING_FILE, PREFETCH_FILE, LOG_FILE, profilePath, script, } from "./lib/paths.js";
@@ -437,8 +437,9 @@ async function advance(queue) {
437
437
  if (queue.items.length === 0)
438
438
  break;
439
439
  const item = queue.items[0];
440
- // Head already resolved last round (rated/skipped → pointer landed on it) → drop.
441
- if (statePointer(item) === item.videoId) {
440
+ // Head already resolved last round (rated/skipped → pointer landed on it, or it's in
441
+ // `seen`) → drop. The `seen` arm survives a --fill pointer-regression race (isResolved).
442
+ if (isResolved(statePointer(item), item.videoId, queue.seen)) {
442
443
  dropHead(queue);
443
444
  continue;
444
445
  }
@@ -489,7 +490,7 @@ async function runPrefetch(videoId) {
489
490
  const item = queue.items.find(i => i.videoId === videoId);
490
491
  if (!item)
491
492
  process.exit(0);
492
- if (statePointer(item) === item.videoId)
493
+ if (isResolved(statePointer(item), item.videoId, queue.seen))
493
494
  process.exit(0); // already resolved
494
495
  if (loadPrefetch(item.videoId))
495
496
  process.exit(0); // already warm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-briefing",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "A self-learning YouTube briefing engine: it sweeps the channels you follow, filters noise in two stages (title, then transcript), summarizes the rest in your language, and adapts to your ratings — one video at a time.",
5
5
  "type": "module",
6
6
  "bin": {