convene-cli 1.4.3 → 1.4.4

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.
package/dist/cache.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OVERRIDE_TTL_MS = exports.writeWatchHighWater = void 0;
6
+ exports.OVERRIDE_TTL_MS = exports.writeWatchHighWater = exports.LIVE_SESSION_WINDOW_SEC = void 0;
7
7
  exports.readCache = readCache;
8
8
  exports.writeCache = writeCache;
9
9
  exports.ageSeconds = ageSeconds;
@@ -140,17 +140,46 @@ function ensureSessionInstance(slug) {
140
140
  return readSessionInstance(slug) || mintSessionInstance(slug);
141
141
  }
142
142
  /**
143
- * Count DISTINCT sessions that have touched this checkout within `maxAgeSec`, by
144
- * scanning the per-session local-state files for `<slug>` (`.json` cache, refreshed
145
- * each active prompt, + `.instance`). A bare slug and each `#<disc>` scope counts
146
- * once. `doctor` uses this to nudge toward one-worktree-per-session when several
147
- * agents share a checkout. Best-effort: any error 0 (no nudge). The slug is
148
- * sanitized to `[A-Za-z0-9_-]` so it is already regex-safe to anchor.
143
+ * "Live session" window for the share-this-checkout nudge. It measures RECENT
144
+ * concurrent activity, not history: a session counts only if it fetched the feed
145
+ * within this window (see liveSessionCount). Tighter than the prior 15 min
146
+ * (practice-guard) / 30 min (doctor) windows so a long-closed session no longer
147
+ * lingers in the count, but long enough to still catch two agents both actively
148
+ * driving a shared checkout across multi-minute turns. 10 min is the balance
149
+ * point: shorter risks missing a concurrent agent mid-long-turn (its `.json`
150
+ * pulse only refreshes on a prompt); longer keeps just-closed/relaunched sessions
151
+ * counted longer (the relaunch-ghost below).
152
+ */
153
+ exports.LIVE_SESSION_WINDOW_SEC = 10 * 60;
154
+ /**
155
+ * Count DISTINCT *live* sessions sharing this checkout within `maxAgeSec`, where
156
+ * "live" = the session refreshed its feed cache (`<slug>[_<disc>].json`) within
157
+ * the window. The feed cache is rewritten by `convene fetch` on every active
158
+ * prompt, so its mtime is a per-session liveness pulse.
159
+ *
160
+ * We count ONLY `.json`, never `.instance`: the instance file is stamped ONCE at
161
+ * SessionStart and never refreshed, so a long-dead boot's `.instance` would
162
+ * otherwise masquerade as a live co-tenant. Excluding it (plus the tighter
163
+ * window) removes the STRUCTURAL onboarding false positives — an instance-ghost
164
+ * from a prior boot, and a one-off terminal `convene doctor`/`post` (which never
165
+ * writes a feed cache) — while a genuinely concurrent session (which fetches each
166
+ * prompt) is still counted.
167
+ *
168
+ * Two residual limits are inherent to a filesystem-only signal, not bugs: (1) the
169
+ * RELAUNCH-GHOST — a session closed then relaunched within the window still shows
170
+ * until the prior session's `.json` ages out, because a closed session's last
171
+ * `.json` is indistinguishable from an idle-but-live one (the window is the only
172
+ * lever); (2) an agent MID-LONG-TURN (> window since its last prompt, no
173
+ * intervening fetch) ages out of the count — a future refinement could also pulse
174
+ * the `.json` mtime on edit-hook activity. A bare slug and each `#<disc>` scope
175
+ * counts once. `doctor` / `practice-guard` use this to nudge toward one-worktree-
176
+ * per-session ONLY when sessions are concurrently active. Best-effort: any error →
177
+ * 0 (no nudge). The slug is sanitized to `[A-Za-z0-9_-]` so it is regex-safe.
149
178
  */
150
179
  function liveSessionCount(slug, maxAgeSec) {
151
180
  try {
152
181
  const san = slug.replace(/[^a-zA-Z0-9_-]/g, '_');
153
- const re = new RegExp(`^${san}(_[a-z0-9-]+)?\\.(json|instance)$`);
182
+ const re = new RegExp(`^${san}(_[a-z0-9-]+)?\\.json$`);
154
183
  const cutoff = Date.now() - maxAgeSec * 1000;
155
184
  const scopes = new Set();
156
185
  for (const f of node_fs_1.default.readdirSync(config_1.CACHE_DIR)) {
@@ -260,14 +260,18 @@ async function doctor(opts) {
260
260
  // working tree clobber each other's uncommitted files and (absent the
261
261
  // discriminator) collapse to one bus identity. Convene now auto-disambiguates
262
262
  // them, but a worktree apiece is the cleaner default — so nudge when ≥2 sessions
263
- // have recently touched this checkout. Purely informational (never fails doctor).
263
+ // are CONCURRENTLY ACTIVE in this checkout (each has fetched within the short
264
+ // liveness window; see liveSessionCount). Purely informational (never fails
265
+ // doctor) — and deliberately NOT a one-off terminal command or a just-closed
266
+ // session, so it doesn't false-fire during onboarding.
264
267
  if (proj?.slug) {
265
- const n = (0, cache_1.liveSessionCount)(proj.slug, 30 * 60); // active within the last 30 min
268
+ const n = (0, cache_1.liveSessionCount)(proj.slug, cache_1.LIVE_SESSION_WINDOW_SEC);
266
269
  if (n >= 2) {
270
+ const mins = Math.round(cache_1.LIVE_SESSION_WINDOW_SEC / 60);
267
271
  checks.push({
268
272
  name: 'sessions',
269
273
  ok: true,
270
- detail: `${n} sessions share this checkout (last 30m) — prefer one git worktree each: \`convene worktree <branch>\``,
274
+ detail: `${n} sessions active in this checkout (last ${mins}m) — prefer one git worktree each: \`convene worktree <branch>\``,
271
275
  });
272
276
  }
273
277
  }
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FEED_FAST_FAIL_MS = void 0;
7
+ exports.fetchFeedResilient = fetchFeedResilient;
6
8
  exports.runFetch = runFetch;
7
9
  /**
8
10
  * `convene fetch` — the UserPromptSubmit hook.
@@ -31,6 +33,57 @@ const FETCH_TIMEOUT_MS = 4000;
31
33
  const WATCHDOG_MS = 6000;
32
34
  /** Catalog-version cache TTL for the behind-nudge — long enough to stay off the hot path. */
33
35
  const CATALOG_VERSION_TTL_SEC = 3600;
36
+ // ── feed-fetch resilience (single transient blip → no loud DEGRADED) ──────────
37
+ // The bus runs a SINGLE web task; a deploy rollout, task restart, or RDS
38
+ // backup-window connection blip leaves a brief window where one feed fetch fails.
39
+ // Without a retry that one failure renders the loud "DEGRADED — could not reach"
40
+ // line on the very next prompt — alarming during onboarding even though the bus is
41
+ // fine a second later. So we retry ONCE, but only when the first attempt failed
42
+ // FAST with a likely-TRANSIENT class (a network error or a 5xx, NOT a full timeout
43
+ // and NOT a 4xx — neither is cured by retrying), and ALWAYS within the SAME total
44
+ // network budget. A genuine outage (a black-hole) still hits the budget and
45
+ // surfaces DEGRADED just as fast. The WORST-CASE wall time is unchanged
46
+ // (<= FEED_BUDGET_MS); the one deliberate trade is that the first-attempt timeout
47
+ // is ~500ms tighter than the old single 4000ms attempt, so a pathologically
48
+ // slow-but-alive server (responding in the 3.5–4s band) now degrades where it
49
+ // previously squeaked through — an acceptable price for absorbing the common
50
+ // fast blip, given the bus normally answers in ~0.15s.
51
+ /** Total network budget for the feed fetch across all attempts (== FETCH_TIMEOUT_MS). */
52
+ const FEED_BUDGET_MS = FETCH_TIMEOUT_MS;
53
+ /** First-attempt timeout. ~500ms under FETCH_TIMEOUT_MS, leaving room for a retry. */
54
+ const FEED_ATTEMPT_MS = 3500;
55
+ /** A failure faster than this is treated as a transient blip worth one retry. */
56
+ exports.FEED_FAST_FAIL_MS = 1200;
57
+ /** Brief pause before the retry, to let a restarting task come back. */
58
+ const FEED_RETRY_BACKOFF_MS = 250;
59
+ /** Skip the retry if less than this much budget remains (defensive; binds only if FAST_FAIL is raised). */
60
+ const FEED_MIN_RETRY_MS = 750;
61
+ function sleep(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+ /**
65
+ * Fetch the feed with one within-budget retry for a FAST transient failure.
66
+ * Returns the (possibly still-failed) ApiResult — the caller renders DEGRADED on a
67
+ * non-ok result exactly as before. Worst-case wall time is FEED_BUDGET_MS, so a
68
+ * black-hole / sustained outage is surfaced no slower than the old single attempt.
69
+ */
70
+ async function fetchFeedResilient(api, slug, q) {
71
+ const started = Date.now();
72
+ let res = await api.feed(slug, q, FEED_ATTEMPT_MS);
73
+ if (res.ok)
74
+ return res;
75
+ const elapsed = Date.now() - started;
76
+ // Retry ONLY a fast, likely-transient failure — a network error (status 0) or a
77
+ // 5xx (server momentarily down). A 4xx (auth / not-found) won't be cured by a
78
+ // retry, so don't burn the round-trip. Always within whatever budget remains.
79
+ const transient = res.status === 0 || res.status >= 500;
80
+ const retryMs = FEED_BUDGET_MS - elapsed - FEED_RETRY_BACKOFF_MS;
81
+ if (transient && elapsed < exports.FEED_FAST_FAIL_MS && retryMs >= FEED_MIN_RETRY_MS) {
82
+ await sleep(FEED_RETRY_BACKOFF_MS);
83
+ res = await api.feed(slug, q, retryMs);
84
+ }
85
+ return res;
86
+ }
34
87
  /**
35
88
  * A single fail-soft nudge line iff the repo's adopted-practices manifest trails a
36
89
  * CACHED live catalog version. Reads only local state (the manifest + the catalog-
@@ -170,9 +223,10 @@ async function runFetch(opts = {}) {
170
223
  emitNudge();
171
224
  return done(0);
172
225
  }
173
- // Slow path: bounded network fetch.
226
+ // Slow path: bounded network fetch, with one within-budget retry so a single
227
+ // transient blip doesn't render the loud DEGRADED line on the next prompt.
174
228
  const api = new api_1.ConveneApi(cfg.baseUrl, cfg.apiKey, session, cfg.tool);
175
- const res = await api.feed(slug, { lookbackMin: lookback, max }, FETCH_TIMEOUT_MS);
229
+ const res = await fetchFeedResilient(api, slug, { lookbackMin: lookback, max });
176
230
  if (res.ok && res.json) {
177
231
  (0, cache_1.writeCache)(slug, res.json);
178
232
  renderData(res.json, { slug, member, session, lookback, syncedAgoSec: 0, json: opts.json });
@@ -263,7 +263,7 @@ async function run(opts, id) {
263
263
  case 'worktree-per-session': {
264
264
  // hook-soft: if >1 live session shares this checkout, nudge toward a worktree
265
265
  // (reuse the doctor signal). Best-effort; any uncertainty → no nudge.
266
- const live = (0, cache_1.liveSessionCount)(slug, 15 * 60);
266
+ const live = (0, cache_1.liveSessionCount)(slug, cache_1.LIVE_SESSION_WINDOW_SEC);
267
267
  if (live > 1) {
268
268
  note(`convene: ${live} live sessions share this checkout — worktree-per-session. ` +
269
269
  `Run each concurrent agent in its own worktree (\`convene worktree <branch>\`) to avoid silent clobbering.` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "convene-cli",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "Convene CLI — AI development coordination bus client + UserPromptSubmit hook. Install: npm i -g convene-cli; then `convene setup`.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://dev.convene.live",