@plannotator/pi-extension 0.28.38 → 0.28.39

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.
@@ -165,6 +165,21 @@ export interface NotesFetchRecord {
165
165
  * try waits RELEASE_NOTES_PENDING_RETRY_MS instead of a day.
166
166
  */
167
167
  publishedAt?: number;
168
+ /**
169
+ * Set on the claim, gone once GitHub answers: a claim still marked was
170
+ * abandoned (its process quit mid-request), and is asked again after
171
+ * RELEASE_NOTES_PENDING_RETRY_MS, not read as an empty answer (24 h).
172
+ */
173
+ inFlight?: boolean;
174
+ /**
175
+ * Set on a failure that may have been the network (not a rate limit, and
176
+ * no release check answered between the failure before it and this one):
177
+ * asked again RELEASE_NOTES_PENDING_RETRY_MS after it once a release check
178
+ * has answered since (the network is back), not after 6 h. One quick retry,
179
+ * then 6 h: a source that keeps failing while the network is up (or a
180
+ * network that drops again before the quick retry) waits 6 h.
181
+ */
182
+ retryOnline?: boolean;
168
183
  }
169
184
 
170
185
  export interface UpdateState {
@@ -239,6 +254,13 @@ export interface UpdateState {
239
254
  * skipped from it, so "Also in the releases since X" survives the update.
240
255
  */
241
256
  inboxPreviousVersion?: string;
257
+ /**
258
+ * The version an Inbox start found newer than `inboxVersion`, kept until its
259
+ * "What's new" link was offered with readable notes: a start on that same
260
+ * version shows the link too, so a restart before the notes were readable
261
+ * does not lose it.
262
+ */
263
+ whatsNewPending?: string;
242
264
  /**
243
265
  * One release's notes fetched by its tag (fetchReleaseNotesFor), per
244
266
  * version: claimed before the request, so concurrent processes ask once.
@@ -246,10 +268,10 @@ export interface UpdateState {
246
268
  notesFetch?: Record<string, NotesFetchRecord>;
247
269
  /**
248
270
  * The Inbox's fetch of one release's images (packages/server/inbox-whats-new-images.ts),
249
- * per version, with the same claim and back-off as `notesFetch`. Only the
250
- * Inbox writes it; nothing here fetches an image.
271
+ * per version, with the same claim and back-off as `notesFetch` (its
272
+ * `inFlight` mark too). Only the Inbox writes it; nothing here fetches an image.
251
273
  */
252
- releaseImagesFetch?: Record<string, { attemptedAt: number; failedAt?: number; error?: string }>;
274
+ releaseImagesFetch?: Record<string, Pick<NotesFetchRecord, "attemptedAt" | "failedAt" | "error" | "inFlight">>;
253
275
  /**
254
276
  * Safe updates: installs of `version` whose download did not match the
255
277
  * checksum Plannotator fetched (install.sh exit 113), counted once each.
@@ -1073,19 +1095,32 @@ function isYoungRelease(record: NotesFetchRecord, now: number): boolean {
1073
1095
  return typeof published === "number" && Number.isFinite(published) && Math.abs(now - published) < RELEASE_NOTES_YOUNG_MS;
1074
1096
  }
1075
1097
 
1098
+ /**
1099
+ * A release check answered after `at` (the network is up again since then).
1100
+ * An answer dated after `now` (the clock moved back) does not count.
1101
+ */
1102
+ function releaseCheckAnsweredAfter(state: UpdateState, at: number, now: number): boolean {
1103
+ const answered = state.latestRelease?.fetchedAt;
1104
+ return typeof answered === "number" && answered > at && answered <= now;
1105
+ }
1106
+
1076
1107
  /**
1077
1108
  * Whether one version's notes may be asked for now: once, then after a
1078
- * failure no sooner than 6 h, and after an answer that carried no "What's
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.
1109
+ * failure no sooner than 6 h (15 minutes once the network is back, see
1110
+ * `retryOnline`), after an answer that carried no "What's New" part no
1111
+ * sooner than 15 minutes while the release is under 2 days old (the release
1112
+ * body is edited after publishing), else 24 h. A claim nobody answered (its
1113
+ * process quit) waits 15 minutes. Never sooner than 15 minutes, never in a loop.
1081
1114
  */
1082
1115
  export function isNotesFetchDue(state: UpdateState, version: string, now: number): boolean {
1083
1116
  const record = state.notesFetch?.[version];
1084
1117
  const at = record?.attemptedAt;
1085
1118
  if (!record || typeof at !== "number" || !Number.isFinite(at) || at > now) return true;
1086
1119
  const wait = record.failedAt
1087
- ? RELEASE_ASSETS_FETCH_RETRY_MS
1088
- : isYoungRelease(record, now)
1120
+ ? record.retryOnline && releaseCheckAnsweredAfter(state, record.failedAt, now)
1121
+ ? RELEASE_NOTES_PENDING_RETRY_MS
1122
+ : RELEASE_ASSETS_FETCH_RETRY_MS
1123
+ : record.inFlight || isYoungRelease(record, now)
1089
1124
  ? RELEASE_NOTES_PENDING_RETRY_MS
1090
1125
  : RELEASE_ASSETS_FETCH_INTERVAL_MS;
1091
1126
  return now - at >= wait;
@@ -1130,15 +1165,17 @@ export async function fetchReleaseNotesFor(deps: ReleaseNotesFetchDeps): Promise
1130
1165
  return { ...s, notesFetch: Object.fromEntries(entries) };
1131
1166
  }, dataDir);
1132
1167
  let pendingBefore = false;
1168
+ let previous: NotesFetchRecord | undefined;
1169
+ let state: UpdateState;
1133
1170
  try {
1134
- const state = readUpdateState(dataDir);
1171
+ state = readUpdateState(dataDir);
1135
1172
  if (!isNotesFetchDue(state, version, now)) return { outcome: "not-due" };
1136
1173
  // 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];
1174
+ // once. Marked in flight, so an attempt that never finished (the process
1175
+ // quit) is tried again on the quick cadence, not in a day.
1176
+ previous = state.notesFetch?.[version];
1140
1177
  pendingBefore = !!previous && !previous.failedAt && isYoungRelease(previous, now);
1141
- record({ attemptedAt: now, ...(pendingBefore && { publishedAt: previous!.publishedAt }) });
1178
+ record({ attemptedAt: now, inFlight: true, ...(pendingBefore && { publishedAt: previous!.publishedAt }) });
1142
1179
  } catch (err) {
1143
1180
  const error = err instanceof Error ? err.message : String(err);
1144
1181
  updateLog(`could not write update-state.json: ${error}`, dataDir);
@@ -1148,9 +1185,14 @@ export async function fetchReleaseNotesFor(deps: ReleaseNotesFetchDeps): Promise
1148
1185
  const tag = result.ok ? normalizeStableVersion(result.release.tag) : null;
1149
1186
  if (!result.ok || tag !== version) {
1150
1187
  const error = result.ok ? `the release answered for ${JSON.stringify(result.release.tag).slice(0, 80)}` : result.error;
1151
- updateLog(`could not read the release notes for ${version} (${error}); next try in 6h`, dataDir);
1188
+ // Maybe the network (a laptop waking, a VPN): asked again 15 minutes after,
1189
+ // once a release check answers. Not after a rate limit, and not when this
1190
+ // source failed before with a release check answering since (it keeps
1191
+ // failing while the network is up: 6 h).
1192
+ const retryOnline = !(!result.ok && result.rateLimited) && !(previous?.failedAt && releaseCheckAnsweredAfter(state, previous.failedAt, now));
1193
+ updateLog(`could not read the release notes for ${version} (${error}); next try in ${retryOnline ? "15m once online, else 6h" : "6h"}`, dataDir);
1152
1194
  try {
1153
- record({ attemptedAt: now, failedAt: now, error: error.slice(0, 200) });
1195
+ record({ attemptedAt: now, failedAt: now, error: error.slice(0, 200), ...(retryOnline && { retryOnline }) });
1154
1196
  } catch {}
1155
1197
  return { outcome: "failed", error };
1156
1198
  }
@@ -1174,6 +1216,11 @@ export async function fetchReleaseNotesFor(deps: ReleaseNotesFetchDeps): Promise
1174
1216
  if (isYoungRelease(pending, now) && !pendingBefore) {
1175
1217
  updateLog(`the release notes for ${version} have no "What's New" part yet; next try in ${formatWait(RELEASE_NOTES_PENDING_RETRY_MS)}`, dataDir);
1176
1218
  }
1219
+ } else {
1220
+ // Answered: no longer in flight (it matters only when the notes could not be saved).
1221
+ try {
1222
+ record({ attemptedAt: now });
1223
+ } catch {}
1177
1224
  }
1178
1225
  // The release page names no publish date: take this answer's for the latest release.
1179
1226
  if (release.publishedAt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.28.38",
3
+ "version": "0.28.39",
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",