ticketlens 0.21.5 → 0.21.6

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.
@@ -37,6 +37,7 @@ import { checkForUpdate, getUpdateHint } from '../skills/jtb/scripts/lib/update-
37
37
  import { incrementInvocation, incrementCommand } from '../skills/jtb/scripts/lib/activity-counter.mjs';
38
38
  import { DEFAULT_CONFIG_DIR } from '../skills/jtb/scripts/lib/config.mjs';
39
39
  import { checkTeamJiraConfigUpdate } from '../skills/jtb/scripts/lib/team-jira-sync.mjs';
40
+ import { maybeAutoFlush } from '../skills/jtb/scripts/lib/recall-queue.mjs';
40
41
 
41
42
  const TRACKED_COMMANDS = new Set([
42
43
  'triage', 'fetch', 'get', 'compliance', 'review', 'standup',
@@ -60,6 +61,26 @@ revalidateIfStale();
60
61
  // Fire-and-forget: refresh the cached latest npm version once per 24h
61
62
  checkForUpdate();
62
63
 
64
+ // Best-effort: retry any Recall notes still queued from a prior failed push —
65
+ // on every command now, not just note add/recall, so a backlog from a burst
66
+ // of transient failures doesn't just sit until the user happens to run one
67
+ // of those two again. Cheap no-op when the queue is empty (one local file
68
+ // read); maybeAutoFlush's own cooldown still protects a down backend from
69
+ // being hit by every single command.
70
+ try {
71
+ const cliToken = readCliToken(DEFAULT_CONFIG_DIR);
72
+ if (cliToken) {
73
+ // Short timeout — this runs before every command, unrelated to Recall or
74
+ // not, so it must never be the reason an unrelated command feels slow.
75
+ const result = await maybeAutoFlush({ cliToken, configDir: DEFAULT_CONFIG_DIR, timeoutMs: 4000 });
76
+ if (result?.flushed) {
77
+ process.stderr.write(` ✔ Synced ${result.flushed} pending Recall note${result.flushed === 1 ? '' : 's'}.\n`);
78
+ }
79
+ }
80
+ } catch {
81
+ // Never let a background sync check break the actual command.
82
+ }
83
+
63
84
  // Show a one-line update hint on stderr after the command exits (never blocks stdout)
64
85
  process.on('exit', () => {
65
86
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.21.5",
3
+ "version": "0.21.6",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -141,6 +141,7 @@ export async function flushQueue({
141
141
  isRetryableFailureFn = isRetryableFailure,
142
142
  warn = () => {},
143
143
  now = () => Date.now(),
144
+ timeoutMs,
144
145
  } = {}) {
145
146
  const nowMs = now();
146
147
  const currentHash = hashToken(cliToken);
@@ -154,7 +155,9 @@ export async function flushQueue({
154
155
  continue;
155
156
  }
156
157
 
157
- const result = await pushNoteFn(entry.notePayload, { cliToken, configDir, warn });
158
+ const result = await pushNoteFn(entry.notePayload, {
159
+ cliToken, configDir, warn, ...(timeoutMs !== undefined ? { timeoutMs } : {}),
160
+ });
158
161
  if (result.ok) {
159
162
  flushed++;
160
163
  continue;
@@ -191,36 +194,52 @@ function writeLastFlushAttemptAt(configDir, isoTimestamp) {
191
194
  }
192
195
 
193
196
  /**
194
- * Time-gated background flush, attempted from the CLI's existing
195
- * Recall-touching entry points (note add's push, recall's pull) rather than
196
- * on every invocation — a no-op unless the queue is non-empty AND at least
197
- * AUTO_FLUSH_INTERVAL_MS has passed since the last attempt. The attempt
198
- * timestamp is recorded even on failure, so a down backend can't be hammered
199
- * once per command within the window.
197
+ * Time-gated background flush. Called from every command's entry point
198
+ * (bin/ticketlens.mjs), not just Recall-specific ones — a note added during
199
+ * a burst of failures (e.g. a debugging session) would otherwise sit queued
200
+ * until the user happens to run `note add`/`recall` again, or runs
201
+ * `recall sync` by hand. A no-op unless the queue is non-empty AND at least
202
+ * AUTO_FLUSH_INTERVAL_MS has passed since the last attempt — the interval is
203
+ * a single global cooldown, not per-entry, so a burst of failures in one
204
+ * short window still only gets one automatic retry pass, by design: this
205
+ * runs on every command now, so the next opportunity is never far away, and
206
+ * the cooldown exists specifically to stop a down backend from being hit by
207
+ * every single command in the meantime. The attempt timestamp is recorded
208
+ * even on failure, so a down backend can't be hammered once per command
209
+ * within the window.
200
210
  *
201
211
  * @param {object} opts
202
212
  * @param {string} opts.cliToken
203
213
  * @param {string} [opts.configDir]
204
214
  * @param {() => number} [opts.now]
205
215
  * @param {Function} [opts.flushQueueFn]
216
+ * @param {number} [opts.timeoutMs] - per-request timeout, passed through to
217
+ * pushNote. Callers running this unconditionally on every command (as
218
+ * opposed to an explicit `recall sync`) should pass something short — this
219
+ * must feel instant, not stall an unrelated command behind a slow network.
220
+ * @returns {Promise<{flushed: number, remaining: number}|null>} null if skipped
221
+ * (empty queue or still cooling down) — distinguishes "nothing to report"
222
+ * from "attempted, flushed 0" for a caller that wants to print a summary.
206
223
  */
207
224
  export async function maybeAutoFlush({
208
225
  cliToken,
209
226
  configDir = DEFAULT_CONFIG_DIR,
210
227
  now = () => Date.now(),
211
228
  flushQueueFn = flushQueue,
229
+ timeoutMs,
212
230
  } = {}) {
213
- if (readQueue(configDir).length === 0) return;
231
+ if (readQueue(configDir).length === 0) return null;
214
232
 
215
233
  const lastAttemptAt = readLastFlushAttemptAt(configDir);
216
234
  const nowMs = now();
217
- if (lastAttemptAt && nowMs - new Date(lastAttemptAt).getTime() < AUTO_FLUSH_INTERVAL_MS) return;
235
+ if (lastAttemptAt && nowMs - new Date(lastAttemptAt).getTime() < AUTO_FLUSH_INTERVAL_MS) return null;
218
236
 
219
237
  try {
220
- await flushQueueFn({ cliToken, configDir, now });
238
+ return await flushQueueFn({ cliToken, configDir, now, ...(timeoutMs !== undefined ? { timeoutMs } : {}) });
221
239
  } catch {
222
240
  // A down backend or a thrown network error must never crash the command
223
241
  // that opportunistically triggered this background attempt.
242
+ return null;
224
243
  } finally {
225
244
  writeLastFlushAttemptAt(configDir, new Date(nowMs).toISOString());
226
245
  }