yt-briefing 0.14.1 → 0.14.2

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.
@@ -38,3 +38,31 @@ export function summaryWasPasted(transcript, videoId) {
38
38
  }
39
39
  return false;
40
40
  }
41
+ /**
42
+ * Same question as `summaryWasPasted`, but tolerant of the harness writing the transcript late.
43
+ *
44
+ * The agent pastes the summary and calls the popup in one turn, so the text block and the tool
45
+ * call are separate JSONL entries written by the harness asynchronously — the hook can run
46
+ * while only the tool call has reached disk. A single read then reports "not pasted" for a turn
47
+ * that did paste, and the block fires on every single video (observed 2026-08-30: the same
48
+ * transcript and pending file that blocked, replayed seconds later, allowed).
49
+ *
50
+ * So poll instead of guessing: re-read until the id shows up or the window closes. A turn that
51
+ * genuinely skipped the paste never produces the line, so the gate still blocks — just later.
52
+ * `read` returning null (a torn read mid-write) counts as "not yet", not as proof of absence.
53
+ */
54
+ export async function waitForSummary(read, videoId, opts = {}) {
55
+ const timeoutMs = opts.timeoutMs ?? 5000;
56
+ const intervalMs = opts.intervalMs ?? 200;
57
+ const now = opts.now ?? (() => Date.now());
58
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
59
+ const deadline = now() + timeoutMs;
60
+ for (;;) {
61
+ const transcript = read();
62
+ if (transcript !== null && summaryWasPasted(transcript, videoId))
63
+ return true;
64
+ if (now() >= deadline)
65
+ return false;
66
+ await sleep(intervalMs);
67
+ }
68
+ }
@@ -14,12 +14,16 @@
14
14
  * `pending.json`, and neither can see the conversation. The transcript is the one place the
15
15
  * evidence exists and only the agent harness exposes it, so the check has to run as a hook.
16
16
  *
17
+ * The transcript is polled for a few seconds rather than read once: the paste and the popup are
18
+ * a single turn and the harness flushes the JSONL asynchronously, so a single read can miss a
19
+ * text block that is merely still in flight.
20
+ *
17
21
  * Reads the PreToolUse payload on stdin; exit 0 allows, exit 2 blocks and feeds stderr back to
18
22
  * the agent. Anything unexpected — foreign tool, unreadable transcript, no pending video —
19
23
  * allows: a gate that misfires on unrelated work would be worse than the bug it guards.
20
24
  */
21
25
  import { readFileSync, existsSync } from 'node:fs';
22
- import { isRatingPopup, summaryWasPasted } from "./lib/summary-gate.js";
26
+ import { isRatingPopup, waitForSummary } from "./lib/summary-gate.js";
23
27
  const ALLOW = 0;
24
28
  const BLOCK = 2;
25
29
  const allow = () => process.exit(ALLOW);
@@ -49,13 +53,16 @@ if (!pending.videoId)
49
53
  const transcript = payload.transcript_path;
50
54
  if (!transcript || !existsSync(transcript))
51
55
  allow();
52
- let seen = false;
53
- try {
54
- seen = summaryWasPasted(readFileSync(transcript, 'utf8'), pending.videoId);
55
- }
56
- catch {
57
- allow();
58
- }
56
+ // Poll rather than read once: the paste and this popup are one turn, and the harness writes the
57
+ // transcript asynchronously, so the text block can still be in flight while the hook runs.
58
+ const seen = await waitForSummary(() => {
59
+ try {
60
+ return readFileSync(transcript, 'utf8');
61
+ }
62
+ catch {
63
+ return null; // transient (file being written) — treat as "not yet", keep waiting
64
+ }
65
+ }, pending.videoId);
59
66
  if (seen)
60
67
  allow();
61
68
  process.stderr.write(`/yt step B not done: the summary for ${pending.videoId} ("${pending.title ?? '?'}") is not in your chat text.\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-briefing",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
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": {