@plannotator/pi-extension 0.28.37 → 0.28.38

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.
@@ -6,9 +6,9 @@
6
6
  import type { GuideViewerAssets } from "./guide-format.ts";
7
7
 
8
8
  export const GUIDE_VIEWER_MANIFEST: Omit<GuideViewerAssets, "baseUrl"> = {
9
- js: "viewer.SgPt8W-S.js",
9
+ js: "viewer.BqcO9v_d.js",
10
10
  css: "viewer.DejL7_DK.css",
11
- jsIntegrity: "sha384-P3KhhgeAgJGa3CTHbXg0ZuIN5F703EFwTG7ont4K08zzCQtJS6EP55W2uRIsdItJ",
11
+ jsIntegrity: "sha384-hv0Y+2gJFlY9p99fic5BMf0H8suh2cpCqppFI1GPYYBfCbMLs/4fTmf3astg5o8b",
12
12
  cssIntegrity: "sha384-RC1v6OjikD4krGxtTP5dn1SNTtuT4wQWdRSS5HSVrmAdJk97gNs5wbKca+3/0Hdl",
13
13
  langs: {
14
14
  "astro": "chunks/astro.Ts5EKq2l.js",
@@ -22,8 +22,9 @@ export function sanitizeTag(name: string): string | null {
22
22
  .replace(/[\s_]+/g, "-") // spaces/underscores -> hyphens
23
23
  .replace(/[^a-z0-9-]/g, "") // remove special chars
24
24
  .replace(/-+/g, "-") // collapse multiple hyphens
25
- .replace(/^-|-$/g, "") // trim leading/trailing hyphens
26
- .slice(0, 30); // max 30 chars
25
+ .replace(/^-/, "") // trim leading hyphen
26
+ .slice(0, 30) // max 30 chars
27
+ .replace(/-$/, ""); // trim trailing hyphen, after the cut so the result is idempotent
27
28
 
28
29
  return sanitized.length >= 2 ? sanitized : null;
29
30
  }
@@ -84,6 +84,15 @@ const RELEASE_CHECK_TIMEOUT_MS = 8_000;
84
84
  */
85
85
  export const RELEASE_ASSETS_FETCH_INTERVAL_MS = 24 * 60 * 60 * 1000;
86
86
  export const RELEASE_ASSETS_FETCH_RETRY_MS = 6 * 60 * 60 * 1000;
87
+ /**
88
+ * A release's notes answered with no "What's New" part while the release is
89
+ * young (published under RELEASE_NOTES_YOUNG_MS ago): GitHub's generated
90
+ * body, read in the minutes before the written notes replaced it. Asked again
91
+ * after this instead of RELEASE_ASSETS_FETCH_INTERVAL_MS, so the "What's new"
92
+ * link shows within the hour, not a day later.
93
+ */
94
+ export const RELEASE_NOTES_PENDING_RETRY_MS = 15 * 60 * 1000;
95
+ export const RELEASE_NOTES_YOUNG_MS = 2 * 24 * 60 * 60 * 1000;
87
96
  /** "Remind me tomorrow" in the Inbox. */
88
97
  export const UPDATE_SNOOZE_MS = 24 * 60 * 60 * 1000;
89
98
  /** The terminal hint shows at most this often. */
@@ -145,6 +154,19 @@ export function releaseUrlFor(version: string): string {
145
154
  // State (update-state.json, shared with auto-update)
146
155
  // ---------------------------------------------------------------------------
147
156
 
157
+ /** One version's notes fetch (`notesFetch[<version>]`). */
158
+ export interface NotesFetchRecord {
159
+ attemptedAt: number;
160
+ failedAt?: number;
161
+ error?: string;
162
+ /**
163
+ * The release's `published_at` (epoch ms), kept when its answer carried no
164
+ * "What's New" part: while it is under RELEASE_NOTES_YOUNG_MS old, the next
165
+ * try waits RELEASE_NOTES_PENDING_RETRY_MS instead of a day.
166
+ */
167
+ publishedAt?: number;
168
+ }
169
+
148
170
  export interface UpdateState {
149
171
  /** Auto-update's install window (epoch ms of the last check that got past its gates). */
150
172
  lastCheckAt?: number;
@@ -221,7 +243,7 @@ export interface UpdateState {
221
243
  * One release's notes fetched by its tag (fetchReleaseNotesFor), per
222
244
  * version: claimed before the request, so concurrent processes ask once.
223
245
  */
224
- notesFetch?: Record<string, { attemptedAt: number; failedAt?: number; error?: string }>;
246
+ notesFetch?: Record<string, NotesFetchRecord>;
225
247
  /**
226
248
  * The Inbox's fetch of one release's images (packages/server/inbox-whats-new-images.ts),
227
249
  * per version, with the same claim and back-off as `notesFetch`. Only the
@@ -1045,17 +1067,28 @@ export function scheduleReleaseCheck(deps: ReleaseCheckDeps, then?: (outcome: Re
1045
1067
  /** How many versions `notesFetch` remembers. */
1046
1068
  const NOTES_FETCH_REMEMBERED = 10;
1047
1069
 
1070
+ /** The record names a release young enough for the quick retry (a clock far off either way does not). */
1071
+ function isYoungRelease(record: NotesFetchRecord, now: number): boolean {
1072
+ const published = record.publishedAt;
1073
+ return typeof published === "number" && Number.isFinite(published) && Math.abs(now - published) < RELEASE_NOTES_YOUNG_MS;
1074
+ }
1075
+
1048
1076
  /**
1049
1077
  * Whether one version's notes may be asked for now: once, then after a
1050
1078
  * failure no sooner than 6 h, and after an answer that carried no "What's
1051
- * New" part no sooner than 24 h (the release body is edited after publishing).
1052
- * Never in a loop.
1079
+ * New" part no sooner than 15 minutes while the release is under 2 days old
1080
+ * (the release body is edited after publishing), else 24 h. Never in a loop.
1053
1081
  */
1054
1082
  export function isNotesFetchDue(state: UpdateState, version: string, now: number): boolean {
1055
1083
  const record = state.notesFetch?.[version];
1056
1084
  const at = record?.attemptedAt;
1057
1085
  if (!record || typeof at !== "number" || !Number.isFinite(at) || at > now) return true;
1058
- return now - at >= (record.failedAt ? RELEASE_ASSETS_FETCH_RETRY_MS : RELEASE_ASSETS_FETCH_INTERVAL_MS);
1086
+ const wait = record.failedAt
1087
+ ? RELEASE_ASSETS_FETCH_RETRY_MS
1088
+ : isYoungRelease(record, now)
1089
+ ? RELEASE_NOTES_PENDING_RETRY_MS
1090
+ : RELEASE_ASSETS_FETCH_INTERVAL_MS;
1091
+ return now - at >= wait;
1059
1092
  }
1060
1093
 
1061
1094
  export interface ReleaseNotesFetchDeps {
@@ -1088,7 +1121,7 @@ export async function fetchReleaseNotesFor(deps: ReleaseNotesFetchDeps): Promise
1088
1121
  if (hasReleaseNotesContent(stored)) return { outcome: "present", notes: stored };
1089
1122
  if (!deps.enabled()) return { outcome: "disabled" };
1090
1123
  const now = (deps.now ?? Date.now)();
1091
- const record = (patch: { attemptedAt: number; failedAt?: number; error?: string }) =>
1124
+ const record = (patch: NotesFetchRecord) =>
1092
1125
  patchUpdateState((s) => {
1093
1126
  const entries = Object.entries({ ...(s.notesFetch ?? {}), [version]: patch })
1094
1127
  .filter(([, r]) => r && typeof r.attemptedAt === "number")
@@ -1096,10 +1129,16 @@ export async function fetchReleaseNotesFor(deps: ReleaseNotesFetchDeps): Promise
1096
1129
  .slice(0, NOTES_FETCH_REMEMBERED);
1097
1130
  return { ...s, notesFetch: Object.fromEntries(entries) };
1098
1131
  }, dataDir);
1132
+ let pendingBefore = false;
1099
1133
  try {
1100
- if (!isNotesFetchDue(readUpdateState(dataDir), version, now)) return { outcome: "not-due" };
1101
- // Claim the attempt before the network call so concurrent processes ask once.
1102
- record({ attemptedAt: now });
1134
+ const state = readUpdateState(dataDir);
1135
+ if (!isNotesFetchDue(state, version, now)) return { outcome: "not-due" };
1136
+ // Claim the attempt before the network call so concurrent processes ask
1137
+ // once. A young release's date is kept, so an attempt that never finished
1138
+ // (the process quit) is tried again on the quick cadence, not in a day.
1139
+ const previous = state.notesFetch?.[version];
1140
+ pendingBefore = !!previous && !previous.failedAt && isYoungRelease(previous, now);
1141
+ record({ attemptedAt: now, ...(pendingBefore && { publishedAt: previous!.publishedAt }) });
1103
1142
  } catch (err) {
1104
1143
  const error = err instanceof Error ? err.message : String(err);
1105
1144
  updateLog(`could not write update-state.json: ${error}`, dataDir);
@@ -1123,6 +1162,19 @@ export async function fetchReleaseNotesFor(deps: ReleaseNotesFetchDeps): Promise
1123
1162
  } catch (err) {
1124
1163
  updateLog(`could not save the release notes: ${err instanceof Error ? err.message : String(err)}`, dataDir);
1125
1164
  }
1165
+ if (!notes) {
1166
+ // GitHub's generated body, read before the written notes replaced it (or
1167
+ // a release with none): a young release is asked again in 15 minutes.
1168
+ const published = release.publishedAt ? Date.parse(release.publishedAt) : NaN;
1169
+ const pending: NotesFetchRecord = { attemptedAt: now, ...(Number.isFinite(published) && { publishedAt: published }) };
1170
+ try {
1171
+ record(pending);
1172
+ } catch {}
1173
+ // One line per version, not one per retry.
1174
+ if (isYoungRelease(pending, now) && !pendingBefore) {
1175
+ updateLog(`the release notes for ${version} have no "What's New" part yet; next try in ${formatWait(RELEASE_NOTES_PENDING_RETRY_MS)}`, dataDir);
1176
+ }
1177
+ }
1126
1178
  // The release page names no publish date: take this answer's for the latest release.
1127
1179
  if (release.publishedAt) {
1128
1180
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.28.37",
3
+ "version": "0.28.38",
4
4
  "type": "module",
5
5
  "description": "Plannotator Pi extension - interactive plan review with annotations, annotate agent messages, and review code/PRs",
6
6
  "author": "backnotprop",