@odla-ai/cli 0.25.9 → 0.25.11

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
@@ -72,10 +72,13 @@ snapshot instead of scraping Studio or composing collector routes themselves:
72
72
  npx odla-ai o11y status --app <appId> --env prod --minutes 60 --json
73
73
  ```
74
74
 
75
- Schema v4 keeps application RED, reconciled live-sync load/freshness and
75
+ Schema v5 keeps application RED, reconciled live-sync load/freshness and
76
76
  subscription recompute/fanout/payload/commit-to-send performance, the exact
77
77
  latest protected commit-to-visible canary, collector ingest/scheduler trust,
78
- and Cloudflare-owned Worker runtime evidence separate. It derives one
78
+ the live Cloudflare Worker read, and 30-day-retained provider snapshot history
79
+ with a seven-day maximum per read separate. Scheduled collection failures,
80
+ source age, expected cadence, collection lag, and history truncation remain
81
+ machine-readable. It derives one
79
82
  `healthy`, `degraded`, or `unhealthy` verdict with stable reason codes while
80
83
  retaining every source response for diagnosis. Provider unavailability,
81
84
  adaptive sampling, publication delay, collector loss, quota rejection, stale
@@ -238,6 +241,14 @@ advancing the cursor. A restore or truncated history exits 3 and emits an
238
241
  explicit replacement checkpoint; an explicit timeout exits 75, and an
239
242
  exhausted remote retry exits 6.
240
243
 
244
+ `discuss read <topic> --json` follows the server's bounded forward post pages
245
+ before emitting one complete document. A long-lived agent can therefore watch
246
+ an event and then read the full topic rather than receiving an apparently
247
+ successful response capped to its oldest 200 posts. Use `--limit <n>
248
+ --offset <n>` to request one bounded page instead; complete reads fail closed
249
+ after 10,000 posts or three continuously changing scans rather than consuming
250
+ memory forever or claiming an inconsistent result.
251
+
241
252
  `code connect` is the first-party personal Code terminal runtime. Run the exact
242
253
  command copied from **Studio → Code → Terminal** while your shell is in the
243
254
  local Git checkout. `--platform`, `--app-id`, and `--env` make the requested
package/dist/bin.cjs CHANGED
@@ -6563,7 +6563,7 @@ Usage:
6563
6563
  odla-ai pm handoff --app <id> [--json]
6564
6564
  odla-ai discuss groups [--json]
6565
6565
  odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
6566
- odla-ai discuss read <topic> [--json]
6566
+ odla-ai discuss read <topic> [--limit <n> --offset <n>] [--json]
6567
6567
  odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"] [--mutation-id <id>]
6568
6568
  odla-ai discuss reply <topic> --body "..." [--markup "..."] [--mutation-id <id>]
6569
6569
  odla-ai discuss resolve <topic> [--reopen] [--mutation-id <id>]
@@ -7409,22 +7409,59 @@ async function discussList(ctx, parsed) {
7409
7409
  }
7410
7410
  });
7411
7411
  }
7412
- async function discussRead(ctx, id) {
7413
- const result = await request(
7414
- ctx,
7415
- "GET",
7416
- `/topics/${encodeURIComponent(id)}`
7417
- );
7418
- emit(ctx, result, () => {
7419
- ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
7420
- for (const post of result.posts) {
7421
- const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7422
- ctx.out.log(`
7423
- \u2014 ${who}`);
7424
- ctx.out.log(bodyWithRefs(post));
7425
- for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
7412
+ async function discussRead(ctx, id, parsed) {
7413
+ const requestedLimit = stringOpt(parsed.options.limit);
7414
+ const requestedOffset = stringOpt(parsed.options.offset);
7415
+ if (requestedLimit !== void 0 || requestedOffset !== void 0) {
7416
+ const query = new URLSearchParams({
7417
+ limit: requestedLimit ?? "200",
7418
+ offset: requestedOffset ?? "0"
7419
+ });
7420
+ const page = await request(ctx, "GET", `/topics/${encodeURIComponent(id)}?${query}`);
7421
+ emit(ctx, page, () => renderRead(ctx, page.topic, page.posts));
7422
+ return;
7423
+ }
7424
+ for (let scan = 0; scan < 3; scan++) {
7425
+ const posts = /* @__PURE__ */ new Map();
7426
+ let topic = null;
7427
+ let offset = 0;
7428
+ for (; ; ) {
7429
+ const page = await request(
7430
+ ctx,
7431
+ "GET",
7432
+ `/topics/${encodeURIComponent(id)}?limit=200&offset=${offset}`
7433
+ );
7434
+ topic = page.topic;
7435
+ for (const post of page.posts) posts.set(post.id, post);
7436
+ if (posts.size > 1e4) throw new Error("discuss read failed: conversation exceeds 10000 posts");
7437
+ if (!page.page?.hasMore) break;
7438
+ if (page.page.nextOffset === null || page.page.nextOffset <= offset) {
7439
+ throw new Error("discuss read failed: registry returned a non-advancing post page");
7440
+ }
7441
+ offset = page.page.nextOffset;
7426
7442
  }
7427
- });
7443
+ const ordered = [...posts.values()].sort(
7444
+ (a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
7445
+ );
7446
+ const expected = Number.isInteger(topic.replyCount) ? topic.replyCount + 1 : ordered.length;
7447
+ if (ordered.length === expected) {
7448
+ const result = { topic, posts: ordered };
7449
+ emit(ctx, result, () => renderRead(ctx, result.topic, result.posts));
7450
+ return;
7451
+ }
7452
+ if (offset === 0) throw new Error("discuss read failed: registry did not provide forward post pages");
7453
+ }
7454
+ throw new Error("discuss read failed: conversation changed during every complete-read attempt");
7455
+ }
7456
+ function renderRead(ctx, topic, posts) {
7457
+ ctx.out.log(`${topic.subject} [${state(topic)}] ${topic.appId ?? ""}`);
7458
+ for (const post of posts) {
7459
+ const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
7460
+ ctx.out.log(`
7461
+ \u2014 ${who}`);
7462
+ ctx.out.log(bodyWithRefs(post));
7463
+ for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
7464
+ }
7428
7465
  }
7429
7466
  async function discussPost(ctx, parsed) {
7430
7467
  const appId = stringOpt(parsed.options.app);
@@ -7751,7 +7788,7 @@ async function discussCommand(parsed, deps = {}) {
7751
7788
  case "topics":
7752
7789
  return discussList(ctx, parsed);
7753
7790
  case "read":
7754
- return discussRead(ctx, requireId(id, "read"));
7791
+ return discussRead(ctx, requireId(id, "read"), parsed);
7755
7792
  case "post":
7756
7793
  return discussPost(ctx, parsed);
7757
7794
  case "reply":
@@ -8078,6 +8115,7 @@ function statusVerdict(reads) {
8078
8115
  sourceHttp(reasons, "canary", reads.canary);
8079
8116
  sourceHttp(reasons, "collector", reads.collector);
8080
8117
  sourceHttp(reasons, "provider", reads.provider);
8118
+ sourceHttp(reasons, "provider", reads.providerHistory, "provider_history_");
8081
8119
  const liveStatus = String(reads.liveSync.body.status ?? "");
8082
8120
  if (liveStatus === "partial") {
8083
8121
  reasons.push({
@@ -8149,6 +8187,14 @@ function statusVerdict(reads) {
8149
8187
  severity: "degraded"
8150
8188
  });
8151
8189
  }
8190
+ const providerHistoryStatus = String(reads.providerHistory.body.status ?? "");
8191
+ if (providerHistoryStatus && providerHistoryStatus !== "observed" && providerHistoryStatus !== "no_data") {
8192
+ reasons.push({
8193
+ source: "provider",
8194
+ code: `provider_history_${providerHistoryStatus}`,
8195
+ severity: "degraded"
8196
+ });
8197
+ }
8152
8198
  return {
8153
8199
  status: reasons.some((reason) => reason.severity === "unhealthy") ? "unhealthy" : reasons.length > 0 ? "degraded" : "healthy",
8154
8200
  reasons
@@ -8168,11 +8214,11 @@ function collectorReason(read3, fallback) {
8168
8214
  );
8169
8215
  return typeof first?.code === "string" && first.code ? first.code : fallback;
8170
8216
  }
8171
- function sourceHttp(reasons, source, read3) {
8217
+ function sourceHttp(reasons, source, read3, prefix = "") {
8172
8218
  if (read3.httpStatus >= 200 && read3.httpStatus < 300) return;
8173
8219
  reasons.push({
8174
8220
  source,
8175
- code: `http_${read3.httpStatus}`,
8221
+ code: `${prefix}http_${read3.httpStatus}`,
8176
8222
  severity: read3.httpStatus >= 500 && read3.httpStatus !== 501 ? "unhealthy" : "degraded"
8177
8223
  });
8178
8224
  }
@@ -8214,7 +8260,7 @@ async function o11yCommand(parsed, deps = {}) {
8214
8260
  const base = `${cfg.platformUrl}/o11y/${encodeURIComponent(appId)}`;
8215
8261
  const query = new URLSearchParams({ env, minutes: String(minutes) });
8216
8262
  const headers = { authorization: `Bearer ${token}` };
8217
- const [application, liveSync, canary, collector, provider] = await Promise.all([
8263
+ const [application, liveSync, canary, collector, provider, providerHistory] = await Promise.all([
8218
8264
  read2(`${base}/metrics/red?${query}`, headers, doFetch),
8219
8265
  read2(
8220
8266
  `${base}/live-sync/health?${query}`,
@@ -8227,17 +8273,19 @@ async function o11yCommand(parsed, deps = {}) {
8227
8273
  doFetch
8228
8274
  ),
8229
8275
  read2(`${base}/self-health?${query}`, headers, doFetch),
8230
- read2(`${base}/provider/cloudflare?${query}`, headers, doFetch)
8276
+ read2(`${base}/provider/cloudflare?${query}`, headers, doFetch),
8277
+ read2(`${base}/provider/cloudflare/history?${query}`, headers, doFetch)
8231
8278
  ]);
8232
8279
  const verdict = statusVerdict({
8233
8280
  application,
8234
8281
  liveSync,
8235
8282
  canary,
8236
8283
  collector,
8237
- provider
8284
+ provider,
8285
+ providerHistory
8238
8286
  });
8239
8287
  const status = {
8240
- schemaVersion: 4,
8288
+ schemaVersion: 5,
8241
8289
  scope: { appId, env, minutes },
8242
8290
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
8243
8291
  verdict,
@@ -8245,7 +8293,8 @@ async function o11yCommand(parsed, deps = {}) {
8245
8293
  liveSync,
8246
8294
  canary,
8247
8295
  collector,
8248
- provider
8296
+ provider,
8297
+ providerHistory
8249
8298
  };
8250
8299
  if (parsed.options.json === true) {
8251
8300
  out.log(JSON.stringify(status, null, 2));
@@ -8305,6 +8354,11 @@ function printStatus2(status, out) {
8305
8354
  out.log(
8306
8355
  `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors`
8307
8356
  );
8357
+ const providerPoints = Array.isArray(status.providerHistory.body.points) ? status.providerHistory.body.points.length : 0;
8358
+ const providerFreshness = record6(status.providerHistory.body.freshness) ? status.providerHistory.body.freshness : {};
8359
+ out.log(
8360
+ `cloudflare-history ${status.providerHistory.httpStatus} ${String(status.providerHistory.body.status ?? status.providerHistory.body.error ?? "unavailable")} ${providerPoints} snapshots ${optionalAge(providerFreshness.ageMs)} old`
8361
+ );
8308
8362
  out.log(
8309
8363
  `verdict ${status.verdict.status} ${status.verdict.reasons.map((reason) => `${reason.source}:${reason.code}`).join(", ") || "all required signals healthy"}`
8310
8364
  );
@@ -8323,6 +8377,10 @@ function numeric3(value) {
8323
8377
  function optionalNumeric(value) {
8324
8378
  return typeof value === "number" && Number.isFinite(value) ? `${value} ms` : "\u2014";
8325
8379
  }
8380
+ function optionalAge(value) {
8381
+ if (typeof value !== "number" || !Number.isFinite(value)) return "unknown";
8382
+ return `${Math.max(0, Math.round(value / 1e3))}s`;
8383
+ }
8326
8384
 
8327
8385
  // src/provision.ts
8328
8386
  var import_apps5 = require("@odla-ai/apps");