@odla-ai/cli 0.25.8 → 0.25.9

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/README.md CHANGED
@@ -195,6 +195,8 @@ npx odla-ai provision --email owner@example.com --write-dev-vars --push-secrets
195
195
  npx odla-ai smoke --env dev
196
196
  npx odla-ai agent jobs --env dev --state dead_letter --json
197
197
  npx odla-ai agent retry <job-id> --env dev --json
198
+ npx odla-ai discuss watch <topic-id> --jsonl
199
+ npx odla-ai discuss watch <topic-id> --cursor <saved-cursor> --jsonl
198
200
  npx odla-ai secrets push --env dev
199
201
  npx odla-ai secrets set clerk_webhook_secret --env dev --stdin
200
202
  npx odla-ai secrets set-clerk-key --env dev --from-env CLERK_SECRET_KEY
@@ -225,6 +227,17 @@ currently running work. Successful receipts are retained for seven days (and
225
227
  bounded to the newest 1,000 per tenant); unresolved dead letters remain until
226
228
  an operator requeues them.
227
229
 
230
+ `discuss watch` waits at a server-issued snapshot boundary instead of comparing
231
+ client clocks or repeatedly reading a topic's oldest 200 posts. It is unbounded
232
+ unless `--timeout <seconds>` is supplied. `--jsonl` emits versioned
233
+ `checkpoint`, `event`, `heartbeat`, and degraded `status` records; persist the
234
+ latest cursor only after consuming its record and pass it back with `--cursor`
235
+ after a restart. Event IDs are stable for at-least-once deduplication. Transient
236
+ 429/5xx/network failures retry with bounded exponential backoff without
237
+ advancing the cursor. A restore or truncated history exits 3 and emits an
238
+ explicit replacement checkpoint; an explicit timeout exits 75, and an
239
+ exhausted remote retry exits 6.
240
+
228
241
  `code connect` is the first-party personal Code terminal runtime. Run the exact
229
242
  command copied from **Studio → Code → Terminal** while your shell is in the
230
243
  local Git checkout. `--platform`, `--app-id`, and `--env` make the requested
package/dist/bin.cjs CHANGED
@@ -4608,13 +4608,13 @@ async function createConversionRegistry(config) {
4608
4608
  policies.set(policy.conversionId, Object.freeze(policy));
4609
4609
  }
4610
4610
  const outputCounts = /* @__PURE__ */ new Map();
4611
- const get2 = (id, kind) => {
4611
+ const get = (id, kind) => {
4612
4612
  const policy = policies.get(id);
4613
4613
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
4614
4614
  return policy;
4615
4615
  };
4616
4616
  const checked = (source, id, kind) => {
4617
- const policy = get2(id, kind);
4617
+ const policy = get(id, kind);
4618
4618
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
4619
4619
  return policy;
4620
4620
  };
@@ -6568,7 +6568,7 @@ Usage:
6568
6568
  odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6569
6569
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
6570
6570
  odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
6571
- odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
6571
+ odla-ai discuss watch [<topic>] [--cursor <cursor>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json|--jsonl]
6572
6572
  odla-ai agent jobs [--env dev] [--state pending|running|succeeded|dead_letter] [--limit 50] [--email <email>] [--json]
6573
6573
  odla-ai agent retry <job-id> [--env dev] [--email <email>] [--json]
6574
6574
  odla-ai o11y status [--app <id>] [--env prod] [--minutes 60] [--json]
@@ -7473,61 +7473,214 @@ async function discussWho(ctx, parsed) {
7473
7473
  });
7474
7474
  }
7475
7475
 
7476
+ // src/discuss-watch-http.ts
7477
+ var WatchRequestError = class extends Error {
7478
+ constructor(message2, retryable, status, code) {
7479
+ super(message2);
7480
+ this.retryable = retryable;
7481
+ this.status = status;
7482
+ this.code = code;
7483
+ this.name = "WatchRequestError";
7484
+ }
7485
+ retryable;
7486
+ status;
7487
+ code;
7488
+ };
7489
+ var WatchCheckpointError = class extends Error {
7490
+ constructor(cursor, streamId) {
7491
+ super("discussion cursor requires a new checkpoint");
7492
+ this.cursor = cursor;
7493
+ this.streamId = streamId;
7494
+ this.name = "WatchCheckpointError";
7495
+ }
7496
+ cursor;
7497
+ streamId;
7498
+ code = "checkpoint_required";
7499
+ };
7500
+ var WatchRemoteError = class extends Error {
7501
+ constructor(cursor, cause) {
7502
+ super("discussion watch dependency remained unavailable", { cause });
7503
+ this.cursor = cursor;
7504
+ this.name = "WatchRemoteError";
7505
+ }
7506
+ cursor;
7507
+ code = "remote_unavailable";
7508
+ };
7509
+ var WatchTimeoutError = class extends Error {
7510
+ constructor(cursor) {
7511
+ super("nothing new before the timeout");
7512
+ this.cursor = cursor;
7513
+ this.name = "WatchTimeoutError";
7514
+ }
7515
+ cursor;
7516
+ code = "watch_timeout";
7517
+ };
7518
+ function requestPath(topicId, app, cursor) {
7519
+ const params = new URLSearchParams();
7520
+ if (topicId) params.set("topic", topicId);
7521
+ if (app) params.set("app", app);
7522
+ if (cursor) params.set("cursor", cursor);
7523
+ return `/watch?${params}`;
7524
+ }
7525
+ async function getWatchPage(ctx, path) {
7526
+ let res;
7527
+ try {
7528
+ res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7529
+ headers: { authorization: `Bearer ${ctx.token}` }
7530
+ });
7531
+ } catch (error) {
7532
+ throw new WatchRequestError(
7533
+ `discuss watch request failed: ${error instanceof Error ? error.message : String(error)}`,
7534
+ true
7535
+ );
7536
+ }
7537
+ const data = await res.json().catch(() => ({}));
7538
+ if (res.status === 409 && data.code === "checkpoint_required") {
7539
+ throw new WatchCheckpointError(data.cursor, data.streamId);
7540
+ }
7541
+ if (!res.ok) {
7542
+ throw new WatchRequestError(
7543
+ `discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`,
7544
+ res.status === 429 || res.status >= 500,
7545
+ res.status,
7546
+ data.code ?? (res.status === 401 || res.status === 403 ? "auth_failed" : void 0)
7547
+ );
7548
+ }
7549
+ return data;
7550
+ }
7551
+
7476
7552
  // src/discuss-watch.ts
7477
7553
  var DEFAULT_INTERVAL_MS = 15e3;
7478
- var DEFAULT_TIMEOUT_MS = 15 * 6e4;
7554
+ var MAX_CONSECUTIVE_FAILURES = 5;
7555
+ var MAX_BACKOFF_MS = 3e4;
7479
7556
  function numberOpt2(parsed, flag, fallback) {
7480
7557
  const raw = stringOpt(parsed.options[flag]);
7481
7558
  const value = raw == null ? NaN : Number(raw);
7482
7559
  return Number.isFinite(value) && value > 0 ? value : fallback;
7483
7560
  }
7484
- async function get(ctx, path) {
7485
- const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
7486
- headers: { authorization: `Bearer ${ctx.token}` }
7487
- });
7488
- const data = await res.json().catch(() => ({}));
7489
- if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
7490
- return data;
7561
+ function jsonl(ctx, parsed, value) {
7562
+ if (parsed.options.jsonl === true) ctx.out.log(JSON.stringify({ v: 1, ...value }));
7491
7563
  }
7492
7564
  async function discussWatch(ctx, topicId, parsed) {
7565
+ if (ctx.json && parsed.options.jsonl === true) throw new Error("--json and --jsonl cannot be combined");
7493
7566
  const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
7494
7567
  const now = ctx.now ?? Date.now;
7495
- const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7496
- const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
7568
+ const intervalMs = (numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) ?? DEFAULT_INTERVAL_MS / 1e3) * 1e3;
7569
+ const timeoutSeconds = numberOpt2(parsed, "timeout");
7570
+ const deadline = timeoutSeconds === void 0 ? void 0 : now() + timeoutSeconds * 1e3;
7497
7571
  const by = stringOpt(parsed.options.by);
7498
7572
  const self = stringOpt(parsed.options.self);
7499
- const deadline = now() + timeoutMs;
7500
- const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
7501
- let seen = /* @__PURE__ */ new Set();
7502
- let since = now();
7503
- if (topicId) {
7504
- const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7505
- seen = new Set(first.posts.map((post) => post.id));
7506
- }
7573
+ const app = stringOpt(parsed.options.app);
7574
+ let cursor = stringOpt(parsed.options.cursor);
7575
+ let firstSuccess = true;
7576
+ let consecutiveFailures = 0;
7507
7577
  for (; ; ) {
7508
- if (topicId) {
7509
- const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
7510
- const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
7511
- if (added.length > 0) return report2(ctx, { found: true, posts: added });
7512
- for (const post of posts) seen.add(post.id);
7513
- } else {
7514
- const app = stringOpt(parsed.options.app);
7515
- const { topics } = await get(
7516
- ctx,
7517
- `/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
7518
- );
7519
- const active = topics.filter((topic) => topic.lastActivityAt > since);
7520
- if (active.length > 0) return report2(ctx, { found: true, topics: active });
7521
- since = now();
7578
+ let page;
7579
+ try {
7580
+ page = await getWatchPage(ctx, requestPath(topicId, app, cursor));
7581
+ consecutiveFailures = 0;
7582
+ } catch (error) {
7583
+ if (!(error instanceof WatchRequestError) || !error.retryable) {
7584
+ if (error instanceof WatchCheckpointError) {
7585
+ jsonl(ctx, parsed, {
7586
+ type: "status",
7587
+ state: "checkpoint_required",
7588
+ retryable: false,
7589
+ ...error.cursor ? { cursor: error.cursor } : {},
7590
+ ...error.streamId ? { streamId: error.streamId } : {}
7591
+ });
7592
+ }
7593
+ throw error;
7594
+ }
7595
+ consecutiveFailures++;
7596
+ jsonl(ctx, parsed, {
7597
+ type: "status",
7598
+ state: "degraded",
7599
+ retryable: true,
7600
+ attempt: consecutiveFailures,
7601
+ ...cursor ? { cursor } : {},
7602
+ ...error.status ? { status: error.status } : {}
7603
+ });
7604
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
7605
+ throw new WatchRemoteError(cursor, error);
7606
+ }
7607
+ if (deadline !== void 0 && now() >= deadline) {
7608
+ const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
7609
+ throw new WatchTimeoutError(result.cursor);
7610
+ }
7611
+ const base = Math.min(intervalMs, 1e3);
7612
+ const backoff = Math.min(MAX_BACKOFF_MS, base * 2 ** (consecutiveFailures - 1));
7613
+ await sleep(deadline === void 0 ? backoff : Math.min(backoff, Math.max(0, deadline - now())));
7614
+ continue;
7615
+ }
7616
+ cursor = page.cursor;
7617
+ const baseline = firstSuccess && page.events.length === 0;
7618
+ if (baseline) {
7619
+ jsonl(ctx, parsed, {
7620
+ type: "checkpoint",
7621
+ streamId: page.streamId,
7622
+ cursor,
7623
+ serverTime: page.serverTime
7624
+ });
7522
7625
  }
7523
- if (now() >= deadline) return report2(ctx, { found: false });
7524
- await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
7626
+ firstSuccess = false;
7627
+ const matching = page.events.filter((event) => {
7628
+ if (topicId && (event.type !== "message" || event.action !== "created")) return false;
7629
+ return (!by || event.actor.id === by) && (!self || event.actor.id !== self);
7630
+ });
7631
+ for (const event of matching) {
7632
+ jsonl(ctx, parsed, {
7633
+ type: "event",
7634
+ streamId: page.streamId,
7635
+ eventId: event.id,
7636
+ cursor: event.cursor,
7637
+ event
7638
+ });
7639
+ }
7640
+ if (matching.length > 0) {
7641
+ jsonl(ctx, parsed, {
7642
+ type: "checkpoint",
7643
+ streamId: page.streamId,
7644
+ cursor,
7645
+ serverTime: page.serverTime
7646
+ });
7647
+ const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
7648
+ const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
7649
+ return report2(ctx, parsed, {
7650
+ found: true,
7651
+ cursor,
7652
+ events: matching,
7653
+ ...posts && posts.length > 0 ? { posts } : {},
7654
+ ...topics && topics.length > 0 ? { topics } : {}
7655
+ });
7656
+ }
7657
+ if (page.events.length > 0) {
7658
+ jsonl(ctx, parsed, {
7659
+ type: "checkpoint",
7660
+ streamId: page.streamId,
7661
+ cursor,
7662
+ serverTime: page.serverTime
7663
+ });
7664
+ } else if (!baseline) {
7665
+ jsonl(ctx, parsed, {
7666
+ type: "heartbeat",
7667
+ streamId: page.streamId,
7668
+ cursor,
7669
+ serverTime: page.serverTime
7670
+ });
7671
+ }
7672
+ if (page.hasMore) continue;
7673
+ if (deadline !== void 0 && now() >= deadline) {
7674
+ return report2(ctx, parsed, { found: false, cursor });
7675
+ }
7676
+ const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
7677
+ await sleep(wait2);
7525
7678
  }
7526
7679
  }
7527
- function report2(ctx, result) {
7680
+ function report2(ctx, parsed, result) {
7528
7681
  if (ctx.json) {
7529
7682
  ctx.out.log(JSON.stringify(result, null, 2));
7530
- } else if (result.found) {
7683
+ } else if (parsed.options.jsonl !== true && result.found) {
7531
7684
  for (const post of result.posts ?? []) {
7532
7685
  const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7533
7686
  ctx.out.log(`\u2014 ${who}
@@ -7539,13 +7692,6 @@ ${post.body}`);
7539
7692
  }
7540
7693
  return result;
7541
7694
  }
7542
- var WatchTimeoutError = class extends Error {
7543
- code = "watch_timeout";
7544
- constructor() {
7545
- super("nothing new before the timeout");
7546
- this.name = "WatchTimeoutError";
7547
- }
7548
- };
7549
7695
 
7550
7696
  // src/discuss-command.ts
7551
7697
  var ALLOWED = [
@@ -7567,6 +7713,8 @@ var ALLOWED = [
7567
7713
  "self",
7568
7714
  "interval",
7569
7715
  "timeout",
7716
+ "cursor",
7717
+ "jsonl",
7570
7718
  "mutation-id"
7571
7719
  ];
7572
7720
  function requireId(id, action) {
@@ -7614,7 +7762,7 @@ async function discussCommand(parsed, deps = {}) {
7614
7762
  return discussWho(ctx, parsed);
7615
7763
  case "watch": {
7616
7764
  const result = await discussWatch(ctx, id, parsed);
7617
- if (!result.found) throw new WatchTimeoutError();
7765
+ if (!result.found) throw new WatchTimeoutError(result.cursor);
7618
7766
  return;
7619
7767
  }
7620
7768
  default:
@@ -10378,7 +10526,12 @@ async function securityStatus(parsed, dependencies) {
10378
10526
  // src/cli.ts
10379
10527
  function exitCodeFor(err) {
10380
10528
  const code = err?.code;
10381
- return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
10529
+ if (code === "handshake_pending" || code === "watch_timeout") return 75;
10530
+ if (code === "checkpoint_required") return 3;
10531
+ if (code === "remote_unavailable") return 6;
10532
+ if (code === "auth_failed") return 5;
10533
+ if (code === "invalid_cursor") return 2;
10534
+ return 1;
10382
10535
  }
10383
10536
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
10384
10537
  const runtime = {