@kontextmind/kxm 0.7.78 → 0.7.79

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.
@@ -14781,9 +14781,9 @@ var require_dist = __commonJS({
14781
14781
  // plugins/kxm/src/runtime-supervisor.ts
14782
14782
  import { spawn as spawn3 } from "node:child_process";
14783
14783
  import { createHash as createHash12, createHmac, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
14784
- import { chmodSync as chmodSync2, existsSync as existsSync12, lstatSync as lstatSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
14784
+ import { chmodSync as chmodSync2, existsSync as existsSync13, lstatSync as lstatSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
14785
14785
  import { createServer } from "node:http";
14786
- import { dirname as dirname8, isAbsolute as isAbsolute6, join as join15 } from "node:path";
14786
+ import { dirname as dirname9, isAbsolute as isAbsolute6, join as join15 } from "node:path";
14787
14787
 
14788
14788
  // plugins/kxm/src/repo-root.ts
14789
14789
  import { existsSync } from "node:fs";
@@ -18280,7 +18280,7 @@ function verifyKxmDriveReceipt(receipt, events, foldedStatus, binding) {
18280
18280
  if (reasons.length === 0) return { verified: true };
18281
18281
  return { verified: false, divergence: reasons.join("; ") };
18282
18282
  }
18283
- var KXM_EVENT_STORE_SCHEMA_VERSION = 6;
18283
+ var KXM_EVENT_STORE_SCHEMA_VERSION = 7;
18284
18284
  var KXM_DRIVE_RECEIPT_SCHEMA = "kxm.drive-receipt.v1";
18285
18285
  var DRIVE_RECEIPT_MAX_BYTES = 8 * 1024;
18286
18286
  var EVENT_STORE_TABLES = {
@@ -18386,7 +18386,17 @@ var EVENT_STORE_TABLES = {
18386
18386
  "record"
18387
18387
  ],
18388
18388
  project_controls: ["project_id", "paused", "reason", "updated_at", "actor", "schema", "record"],
18389
- outbox: ["seq", "run_id", "sequence", "sync_event", "attempted_at", "acked_at"]
18389
+ outbox: [
18390
+ "seq",
18391
+ "run_id",
18392
+ "sequence",
18393
+ "sync_event",
18394
+ "attempted_at",
18395
+ "attempt_count",
18396
+ "acked_at",
18397
+ "refused_code",
18398
+ "refused_at"
18399
+ ]
18390
18400
  };
18391
18401
  var KXM_EVENT_STORE_TABLE_NAMES = Object.keys(EVENT_STORE_TABLES).sort();
18392
18402
  var EVENT_STORE_SCHEMA = `
@@ -18580,10 +18590,14 @@ CREATE TABLE IF NOT EXISTS outbox (
18580
18590
  sequence INTEGER NOT NULL,
18581
18591
  sync_event TEXT NOT NULL,
18582
18592
  attempted_at TEXT,
18593
+ attempt_count INTEGER NOT NULL DEFAULT 0,
18583
18594
  acked_at TEXT,
18595
+ refused_code TEXT,
18596
+ refused_at TEXT,
18584
18597
  UNIQUE (run_id, sequence)
18585
18598
  ) STRICT;
18586
- CREATE INDEX outbox_pending ON outbox(seq) WHERE acked_at IS NULL;
18599
+ CREATE INDEX outbox_pending ON outbox(seq) WHERE acked_at IS NULL AND refused_code IS NULL;
18600
+ CREATE INDEX outbox_refused ON outbox(seq) WHERE refused_code IS NOT NULL;
18587
18601
  `;
18588
18602
  var KxmRunEventStore = class {
18589
18603
  path;
@@ -18723,37 +18737,108 @@ var KxmRunEventStore = class {
18723
18737
  const syncEvent = deriveKxmSyncEvent(event, { redactor: this.syncRedactor });
18724
18738
  this.database.prepare("INSERT INTO outbox (run_id, sequence, sync_event) VALUES (?, ?, ?)").run(event.runId, event.sequence, kxmSyncEventBytes(syncEvent));
18725
18739
  }
18726
- /** Unacknowledged outbox rows after `afterSeq`, in outbox order, oldest first. */
18740
+ /**
18741
+ * Retryable outbox rows after `afterSeq`, in outbox order, oldest first.
18742
+ * Acked and durably refused rows are never returned: a refused row is not
18743
+ * waiting for a retry, it is waiting for an operator.
18744
+ */
18727
18745
  pendingOutbox(limit = 100, afterSeq = 0) {
18728
18746
  const rows = this.database.prepare(`
18729
- SELECT seq, run_id, sequence, sync_event, attempted_at, acked_at
18730
- FROM outbox WHERE acked_at IS NULL AND seq > ? ORDER BY seq ASC LIMIT ?
18747
+ SELECT seq, run_id, sequence, sync_event, attempt_count, attempted_at, acked_at, refused_code, refused_at
18748
+ FROM outbox WHERE acked_at IS NULL AND refused_code IS NULL AND seq > ? ORDER BY seq ASC LIMIT ?
18731
18749
  `).all(afterSeq, limit);
18732
18750
  return rows.map(outboxFromRow);
18733
18751
  }
18734
- /** Every outbox row of one run, acknowledged or not, in sequence order. */
18752
+ /** Every outbox row of one run, in any state, in sequence order. */
18735
18753
  outboxForRun(runId) {
18736
18754
  const rows = this.database.prepare(`
18737
- SELECT seq, run_id, sequence, sync_event, attempted_at, acked_at
18755
+ SELECT seq, run_id, sequence, sync_event, attempt_count, attempted_at, acked_at, refused_code, refused_at
18738
18756
  FROM outbox WHERE run_id = ? ORDER BY sequence ASC
18739
18757
  `).all(runId);
18740
18758
  return rows.map(outboxFromRow);
18741
18759
  }
18760
+ /**
18761
+ * Count one attempt against each row. Refused rows are never re-counted: they
18762
+ * are out of the queue, and a tick that reaches them again would mean the
18763
+ * refusal was cleared (which resets the count).
18764
+ */
18742
18765
  markOutboxAttempted(seqs, at) {
18743
- const statement = this.database.prepare("UPDATE outbox SET attempted_at = ? WHERE seq = ? AND acked_at IS NULL");
18766
+ const statement = this.database.prepare(`
18767
+ UPDATE outbox SET attempted_at = ?, attempt_count = attempt_count + 1
18768
+ WHERE seq = ? AND acked_at IS NULL AND refused_code IS NULL
18769
+ `);
18744
18770
  this.transaction(() => {
18745
18771
  for (const seq of seqs) statement.run(at, seq);
18746
18772
  });
18747
18773
  }
18748
18774
  /** Advance the cursor: the hub holds these rows now. Returns rows newly acked. */
18749
18775
  ackOutbox(seqs, at) {
18750
- const statement = this.database.prepare("UPDATE outbox SET acked_at = ? WHERE seq = ? AND acked_at IS NULL");
18776
+ const statement = this.database.prepare("UPDATE outbox SET acked_at = ?, refused_code = NULL, refused_at = NULL WHERE seq = ? AND acked_at IS NULL");
18751
18777
  return this.transaction(() => {
18752
18778
  let changed = 0;
18753
18779
  for (const seq of seqs) changed += Number(statement.run(at, seq).changes);
18754
18780
  return changed;
18755
18781
  });
18756
18782
  }
18783
+ /**
18784
+ * Record a durable hub refusal: the answer will not change if the same bytes
18785
+ * are sent again. Returns rows newly moved out of the pending queue.
18786
+ */
18787
+ refuseOutbox(refusals, at) {
18788
+ const statement = this.database.prepare(`
18789
+ UPDATE outbox SET refused_code = ?, refused_at = ?
18790
+ WHERE seq = ? AND acked_at IS NULL AND refused_code IS NULL
18791
+ `);
18792
+ return this.transaction(() => {
18793
+ let changed = 0;
18794
+ for (const refusal of refusals) changed += Number(statement.run(refusal.code, at, refusal.seq).changes);
18795
+ return changed;
18796
+ });
18797
+ }
18798
+ /**
18799
+ * Operator revive: put refused rows back in the pending queue with a fresh
18800
+ * attempt budget. Used after the hub-side state is corrected — the Runtime
18801
+ * must not decide on its own that a refusal has become retryable.
18802
+ */
18803
+ retryRefusedOutbox() {
18804
+ return Number(this.database.prepare(`
18805
+ UPDATE outbox SET refused_code = NULL, refused_at = NULL, attempted_at = NULL, attempt_count = 0
18806
+ WHERE acked_at IS NULL AND refused_code IS NOT NULL
18807
+ `).run().changes);
18808
+ }
18809
+ /**
18810
+ * The outbox as one read model: what is still retryable, what the hub holds,
18811
+ * and what the hub refused with which codes. This is what makes a stalled
18812
+ * sync answerable without opening SQLite.
18813
+ */
18814
+ outboxStatus() {
18815
+ const counts = this.database.prepare(`
18816
+ SELECT
18817
+ COALESCE(SUM(acked_at IS NULL AND refused_code IS NULL), 0) AS pending,
18818
+ COALESCE(SUM(acked_at IS NOT NULL), 0) AS acked,
18819
+ COALESCE(SUM(acked_at IS NULL AND refused_code IS NOT NULL), 0) AS refused,
18820
+ MAX(attempted_at) AS last_attempt_at
18821
+ FROM outbox
18822
+ `).get();
18823
+ const refusals = this.database.prepare(`
18824
+ SELECT refused_code AS code, COUNT(*) AS count
18825
+ FROM outbox WHERE acked_at IS NULL AND refused_code IS NOT NULL
18826
+ GROUP BY refused_code ORDER BY count DESC, code ASC
18827
+ `).all();
18828
+ const oldest = this.database.prepare(`
18829
+ SELECT seq FROM outbox WHERE acked_at IS NULL AND refused_code IS NULL ORDER BY seq ASC LIMIT 1
18830
+ `).get();
18831
+ return {
18832
+ pending: counts.pending,
18833
+ acked: counts.acked,
18834
+ refused: counts.refused,
18835
+ ...counts.last_attempt_at !== null ? { lastAttemptAt: counts.last_attempt_at } : {},
18836
+ ...oldest ? { oldestPendingSeq: oldest.seq } : {},
18837
+ // Mapped, not spread: node:sqlite rows are null-prototype objects, and this
18838
+ // read model is handed straight to the CLI and the supervisor's JSON answer.
18839
+ refusals: refusals.map((refusal) => ({ code: refusal.code, count: refusal.count }))
18840
+ };
18841
+ }
18757
18842
  events(runId, afterSequence = 0, limit = 200) {
18758
18843
  const rows = this.database.prepare(`
18759
18844
  SELECT project_id, run_id, sequence, event_id, event_type, command_id, occurred_at, recorded_at, monotonic_ns, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, payload, schema, home_runtime_id
@@ -19275,8 +19360,11 @@ function outboxFromRow(row) {
19275
19360
  runId: row.run_id,
19276
19361
  sequence: row.sequence,
19277
19362
  syncEvent: row.sync_event,
19363
+ attemptCount: row.attempt_count,
19278
19364
  ...row.attempted_at !== null ? { attemptedAt: row.attempted_at } : {},
19279
- ...row.acked_at !== null ? { ackedAt: row.acked_at } : {}
19365
+ ...row.acked_at !== null ? { ackedAt: row.acked_at } : {},
19366
+ ...row.refused_code !== null ? { refusedCode: row.refused_code } : {},
19367
+ ...row.refused_at !== null ? { refusedAt: row.refused_at } : {}
19280
19368
  };
19281
19369
  }
19282
19370
  function runFromRow(row) {
@@ -27473,13 +27561,162 @@ function resolveClientHubAuthToken(env, project) {
27473
27561
  return record2?.projectTokens?.[project]?.trim() || record2?.authToken?.trim() || void 0;
27474
27562
  }
27475
27563
 
27564
+ // plugins/kxm/src/logger.ts
27565
+ import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync6, renameSync as renameSync3, statSync as statSync3, unlinkSync as unlinkSync2 } from "node:fs";
27566
+ import { dirname as dirname8 } from "node:path";
27567
+ var LOG_LEVEL_PRIORITY = {
27568
+ debug: 10,
27569
+ info: 20,
27570
+ warn: 30,
27571
+ error: 40
27572
+ };
27573
+ var DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024;
27574
+ var DEFAULT_LOG_MAX_FILES = 3;
27575
+ var SENSITIVE_KEY_PATTERN = /(?:^|_)(?:token|secret|password|apiKey|api_key|authorization|bearer)(?:$|_)/i;
27576
+ var ALLOWED_EXACT_KEYS = /* @__PURE__ */ new Set(["auth", "authType", "authMethod", "authArgs", "canUpdate", "status"]);
27577
+ function redactLogValue(val, key) {
27578
+ if (val === null || val === void 0) return val;
27579
+ if (typeof val === "string") {
27580
+ if (key && SENSITIVE_KEY_PATTERN.test(key) && !ALLOWED_EXACT_KEYS.has(key)) {
27581
+ return "[redacted]";
27582
+ }
27583
+ return redactSecrets(val);
27584
+ }
27585
+ if (typeof val === "number" || typeof val === "boolean") {
27586
+ return val;
27587
+ }
27588
+ if (Array.isArray(val)) {
27589
+ return val.map((item) => redactLogValue(item, key));
27590
+ }
27591
+ if (typeof val === "object") {
27592
+ const out = {};
27593
+ for (const [k, v] of Object.entries(val)) {
27594
+ out[k] = redactLogValue(v, k);
27595
+ }
27596
+ return out;
27597
+ }
27598
+ return String(val);
27599
+ }
27600
+ function rotateLogFiles(filePath, maxFiles) {
27601
+ for (let i = maxFiles; i >= 1; i--) {
27602
+ const current = `${filePath}.${i}`;
27603
+ if (existsSync12(current)) {
27604
+ if (i >= maxFiles) {
27605
+ try {
27606
+ unlinkSync2(current);
27607
+ } catch {
27608
+ }
27609
+ } else {
27610
+ try {
27611
+ renameSync3(current, `${filePath}.${i + 1}`);
27612
+ } catch {
27613
+ }
27614
+ }
27615
+ }
27616
+ }
27617
+ if (existsSync12(filePath)) {
27618
+ try {
27619
+ renameSync3(filePath, `${filePath}.1`);
27620
+ } catch {
27621
+ }
27622
+ }
27623
+ }
27624
+ function createLogger(options) {
27625
+ const component = options.component;
27626
+ const filePath = options.path;
27627
+ const maxBytes = Math.max(100, options.maxBytes ?? DEFAULT_LOG_MAX_BYTES);
27628
+ const maxFiles = Math.max(1, options.maxFiles ?? DEFAULT_LOG_MAX_FILES);
27629
+ const configuredLevel = options.level ?? "info";
27630
+ const isDaemon = Boolean(options.daemon ?? (process.env.KXM_DAEMON === "1" || process.env.KXM_DAEMON === "true"));
27631
+ const shouldStdout = options.stdout ?? !isDaemon;
27632
+ const correlationDefaults = options.correlation ?? {};
27633
+ let currentSize = 0;
27634
+ if (filePath && existsSync12(filePath)) {
27635
+ try {
27636
+ currentSize = statSync3(filePath).size;
27637
+ } catch {
27638
+ currentSize = 0;
27639
+ }
27640
+ }
27641
+ function emit(level, entryOrEvent, extra) {
27642
+ const minPriority = LOG_LEVEL_PRIORITY[configuredLevel] ?? LOG_LEVEL_PRIORITY.info;
27643
+ const currentPriority = LOG_LEVEL_PRIORITY[level] ?? LOG_LEVEL_PRIORITY.info;
27644
+ if (currentPriority < minPriority) return;
27645
+ let base;
27646
+ if (typeof entryOrEvent === "string") {
27647
+ base = { event: entryOrEvent, ...extra };
27648
+ } else {
27649
+ base = { ...entryOrEvent, ...extra };
27650
+ }
27651
+ const timestamp = typeof base.timestamp === "string" ? base.timestamp : (/* @__PURE__ */ new Date()).toISOString();
27652
+ delete base.timestamp;
27653
+ delete base.level;
27654
+ delete base.component;
27655
+ const payload = {
27656
+ timestamp,
27657
+ level,
27658
+ component,
27659
+ ...correlationDefaults,
27660
+ ...base
27661
+ };
27662
+ const sanitized = redactLogValue(payload);
27663
+ const line = `${JSON.stringify(sanitized)}
27664
+ `;
27665
+ if (filePath) {
27666
+ const lineBytes = Buffer.byteLength(line, "utf8");
27667
+ if (currentSize + lineBytes > maxBytes) {
27668
+ rotateLogFiles(filePath, maxFiles);
27669
+ currentSize = 0;
27670
+ }
27671
+ try {
27672
+ mkdirSync6(dirname8(filePath), { recursive: true });
27673
+ appendFileSync(filePath, line, { encoding: "utf8", mode: 384 });
27674
+ currentSize += lineBytes;
27675
+ } catch {
27676
+ }
27677
+ }
27678
+ if (shouldStdout) {
27679
+ process.stdout.write(line);
27680
+ }
27681
+ }
27682
+ const logFn = ((entryOrEvent, extra) => {
27683
+ let lvl = "info";
27684
+ if (typeof entryOrEvent === "object" && entryOrEvent !== null && typeof entryOrEvent.level === "string") {
27685
+ const candidate = entryOrEvent.level.toLowerCase();
27686
+ if (candidate === "debug" || candidate === "info" || candidate === "warn" || candidate === "error") {
27687
+ lvl = candidate;
27688
+ }
27689
+ }
27690
+ emit(lvl, entryOrEvent, extra);
27691
+ });
27692
+ logFn.info = (entryOrEvent, extra) => emit("info", entryOrEvent, extra);
27693
+ logFn.warn = (entryOrEvent, extra) => emit("warn", entryOrEvent, extra);
27694
+ logFn.error = (entryOrEvent, extra) => emit("error", entryOrEvent, extra);
27695
+ logFn.debug = (entryOrEvent, extra) => emit("debug", entryOrEvent, extra);
27696
+ logFn.child = (sub) => {
27697
+ return createLogger({
27698
+ ...options,
27699
+ component: sub.component ? `${component}.${sub.component}` : component,
27700
+ correlation: { ...correlationDefaults, ...sub.correlation }
27701
+ });
27702
+ };
27703
+ logFn.close = () => {
27704
+ };
27705
+ Object.defineProperty(logFn, "options", {
27706
+ value: Object.freeze({ ...options }),
27707
+ writable: false,
27708
+ enumerable: true
27709
+ });
27710
+ return logFn;
27711
+ }
27712
+
27476
27713
  // plugins/kxm/src/runtime-supervisor.ts
27477
27714
  function kxmSupervisorTokenFile(paths) {
27478
27715
  return join15(paths.runtimeDir, "supervisor.token");
27479
27716
  }
27480
27717
  function publishKxmSupervisorToken(paths, token) {
27481
27718
  const file = kxmSupervisorTokenFile(paths);
27482
- mkdirSync6(dirname8(file), { recursive: true, mode: 448 });
27719
+ mkdirSync7(dirname9(file), { recursive: true, mode: 448 });
27483
27720
  const temp = `${file}.${process.pid}.tmp`;
27484
27721
  writeFileSync6(temp, `${token}
27485
27722
  `, { encoding: "utf8", mode: 384 });
@@ -27487,7 +27724,7 @@ function publishKxmSupervisorToken(paths, token) {
27487
27724
  chmodSync2(temp, 384);
27488
27725
  } catch {
27489
27726
  }
27490
- renameSync3(temp, file);
27727
+ renameSync4(temp, file);
27491
27728
  try {
27492
27729
  chmodSync2(file, 384);
27493
27730
  } catch {
@@ -27521,11 +27758,11 @@ function supervisorErrorFile(paths) {
27521
27758
  }
27522
27759
  function clearSupervisorError(paths) {
27523
27760
  const file = supervisorErrorFile(paths);
27524
- if (existsSync12(file)) rmSync3(file, { force: true });
27761
+ if (existsSync13(file)) rmSync3(file, { force: true });
27525
27762
  }
27526
27763
  function recordSupervisorError(paths, message) {
27527
27764
  try {
27528
- mkdirSync6(paths.runtimeDir, { recursive: true, mode: 448 });
27765
+ mkdirSync7(paths.runtimeDir, { recursive: true, mode: 448 });
27529
27766
  writeFileSync6(supervisorErrorFile(paths), `${message}
27530
27767
  `, { encoding: "utf8", mode: 384 });
27531
27768
  } catch {
@@ -27544,7 +27781,7 @@ function readRecentSupervisorError(paths) {
27544
27781
  }
27545
27782
  }
27546
27783
  function kxmSupervisorStatus(paths) {
27547
- if (!existsSync12(paths.registryDb)) return { running: false };
27784
+ if (!existsSync13(paths.registryDb)) return { running: false };
27548
27785
  const registry = new KxmRuntimeRegistry(paths.registryDb);
27549
27786
  try {
27550
27787
  const record2 = registry.supervisor();
@@ -27725,6 +27962,8 @@ var DEFAULT_RUNTIME_SYNC_INTERVAL_MS = 1e4;
27725
27962
  var MIN_RUNTIME_SYNC_INTERVAL_MS = 250;
27726
27963
  var MAX_RUNTIME_SYNC_INTERVAL_MS = 6e4;
27727
27964
  var OUTBOX_PUSH_BATCH = 32;
27965
+ var OUTBOX_PUSH_BATCH_BYTES = 2e5;
27966
+ var RUNTIME_SYNC_MAX_BACKOFF_MS = 5 * 6e4;
27728
27967
  function runtimeSyncIntervalMs(env = process.env) {
27729
27968
  const raw = env.KXM_RUNTIME_SYNC_INTERVAL_MS?.trim();
27730
27969
  if (!raw) return DEFAULT_RUNTIME_SYNC_INTERVAL_MS;
@@ -27732,16 +27971,72 @@ function runtimeSyncIntervalMs(env = process.env) {
27732
27971
  if (!Number.isInteger(parsed)) return DEFAULT_RUNTIME_SYNC_INTERVAL_MS;
27733
27972
  return Math.min(MAX_RUNTIME_SYNC_INTERVAL_MS, Math.max(MIN_RUNTIME_SYNC_INTERVAL_MS, parsed));
27734
27973
  }
27974
+ function isDurableRefusal(outcome) {
27975
+ return outcome === "conflict" || outcome === "rejected";
27976
+ }
27977
+ function isOversizeRefusal(error) {
27978
+ return error instanceof HubHttpError && (error.statusCode === 413 || error.code === "sync_batch_too_large" || error.code === "payload_too_large");
27979
+ }
27735
27980
  async function syncKxmOutbox(eventStore, client, options = {}) {
27736
27981
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
27737
- const batchSize = options.batchSize ?? OUTBOX_PUSH_BATCH;
27738
- const result = { pushed: 0, acked: 0, conflicts: 0, rejected: 0 };
27982
+ const batchSize = Math.max(1, options.batchSize ?? OUTBOX_PUSH_BATCH);
27983
+ const result = { pushed: 0, acked: 0, refused: 0, refusals: [], unconfirmed: 0, blocked: false };
27984
+ const refuse = (row, code) => {
27985
+ if (eventStore.refuseOutbox([{ seq: row.seq, code }], now()) === 0) return;
27986
+ result.refused += 1;
27987
+ result.refusals.push({ runId: row.runId, sequence: row.sequence, code });
27988
+ };
27989
+ const deliver = async (batch) => {
27990
+ const payload = [];
27991
+ const sendable = [];
27992
+ for (const row of batch) {
27993
+ try {
27994
+ payload.push(JSON.parse(row.syncEvent));
27995
+ sendable.push(row);
27996
+ } catch {
27997
+ refuse(row, "sync_row_unreadable");
27998
+ }
27999
+ }
28000
+ if (sendable.length === 0) return true;
28001
+ eventStore.markOutboxAttempted(sendable.map((row) => row.seq), now());
28002
+ let response;
28003
+ try {
28004
+ response = await client.pushSyncEvents(payload);
28005
+ } catch (error) {
28006
+ if (isOversizeRefusal(error)) {
28007
+ if (sendable.length > 1) {
28008
+ for (const row of sendable) {
28009
+ if (!await deliver([row])) return false;
28010
+ }
28011
+ return true;
28012
+ }
28013
+ refuse(sendable[0], "sync_row_too_large");
28014
+ return true;
28015
+ }
28016
+ result.blockedReason = syncFailureText(error);
28017
+ return false;
28018
+ }
28019
+ result.pushed += sendable.length;
28020
+ const acked = [];
28021
+ sendable.forEach((row, index) => {
28022
+ const entry = response.results[index];
28023
+ const describesRow = entry !== void 0 && (entry.runId === void 0 || entry.runId === row.runId) && (entry.sequence === void 0 || entry.sequence === row.sequence);
28024
+ if (!entry || !describesRow) {
28025
+ result.unconfirmed += 1;
28026
+ return;
28027
+ }
28028
+ if (entry.outcome === "accepted" || entry.outcome === "duplicate") acked.push(row.seq);
28029
+ else if (isDurableRefusal(entry.outcome)) refuse(row, entry.code ?? `sync_${entry.outcome}`);
28030
+ else result.unconfirmed += 1;
28031
+ });
28032
+ result.acked += eventStore.ackOutbox(acked, now());
28033
+ return true;
28034
+ };
27739
28035
  let afterSeq = 0;
27740
28036
  for (; ; ) {
27741
28037
  const rows = eventStore.pendingOutbox(batchSize, afterSeq);
27742
28038
  if (rows.length === 0) return result;
27743
- const MAX_BATCH_BYTES = 2e5;
27744
- let byteBudget = MAX_BATCH_BYTES;
28039
+ let byteBudget = OUTBOX_PUSH_BATCH_BYTES;
27745
28040
  let sendCount = 0;
27746
28041
  for (const row of rows) {
27747
28042
  const rowBytes = Buffer.byteLength(row.syncEvent, "utf8") + 64;
@@ -27749,21 +28044,12 @@ async function syncKxmOutbox(eventStore, client, options = {}) {
27749
28044
  byteBudget -= rowBytes;
27750
28045
  sendCount += 1;
27751
28046
  }
27752
- const batch = rows.slice(0, sendCount);
27753
- if (batch.length === 0) batch.push(rows[0]);
28047
+ const batch = rows.slice(0, Math.max(1, sendCount));
27754
28048
  afterSeq = batch[batch.length - 1].seq;
27755
- eventStore.markOutboxAttempted(batch.map((row) => row.seq), now());
27756
- const response = await client.pushSyncEvents(batch.map((row) => JSON.parse(row.syncEvent)));
27757
- result.pushed += batch.length;
27758
- const outcomes = new Map(response.results.map((entry) => [`${entry.runId}\0${entry.sequence}`, entry.outcome]));
27759
- const acked = [];
27760
- for (const row of batch) {
27761
- const outcome = outcomes.get(`${row.runId}\0${row.sequence}`);
27762
- if (outcome === "accepted" || outcome === "duplicate") acked.push(row.seq);
27763
- else if (outcome === "conflict") result.conflicts += 1;
27764
- else result.rejected += 1;
28049
+ if (!await deliver(batch)) {
28050
+ result.blocked = true;
28051
+ return result;
27765
28052
  }
27766
- result.acked += eventStore.ackOutbox(acked, now());
27767
28053
  }
27768
28054
  }
27769
28055
  function runtimeHubClientFor(context, env) {
@@ -27778,11 +28064,24 @@ function runtimeHubClientFor(context, env) {
27778
28064
  ...authToken ? { authToken } : {}
27779
28065
  });
27780
28066
  }
28067
+ function syncBackoffMs(consecutiveStalls, intervalMs) {
28068
+ if (consecutiveStalls <= 0) return 0;
28069
+ return Math.min(RUNTIME_SYNC_MAX_BACKOFF_MS, intervalMs * 2 ** Math.min(5, consecutiveStalls - 1));
28070
+ }
28071
+ function syncFailureText(error) {
28072
+ const code = error instanceof HubHttpError ? `${error.statusCode} ${error.code ?? "error"}` : void 0;
28073
+ const message = error instanceof Error ? error.message : String(error);
28074
+ return `${code ? `${code}: ` : ""}${message}`.replace(/(kxm_[A-Za-z0-9_]+|[A-Za-z0-9._-]{40,})/g, "[redacted]").slice(0, 300);
28075
+ }
28076
+ function runtimeSupervisorLogFile(paths) {
28077
+ return join15(paths.runtimeDir, "logs", "kxm-runtime.jsonl");
28078
+ }
27781
28079
  async function startKxmRuntimeSupervisor(options = {}) {
27782
28080
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
28081
+ const env = options.env ?? process.env;
27783
28082
  const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : {});
27784
28083
  try {
27785
- return await startKxmRuntimeSupervisorInner(paths, options.port, now);
28084
+ return await startKxmRuntimeSupervisorInner(paths, options.port, now, env);
27786
28085
  } catch (error) {
27787
28086
  if (!(error instanceof KxmConfigError && error.issues.some((issue3) => issue3.code === "runtime_supervisor_conflict"))) {
27788
28087
  recordSupervisorError(paths, error.message);
@@ -27790,9 +28089,15 @@ async function startKxmRuntimeSupervisor(options = {}) {
27790
28089
  throw error;
27791
28090
  }
27792
28091
  }
27793
- async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
27794
- mkdirSync6(paths.runtimeDir, { recursive: true, mode: 448 });
27795
- mkdirSync6(paths.projectsDir, { recursive: true, mode: 448 });
28092
+ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, env) {
28093
+ mkdirSync7(paths.runtimeDir, { recursive: true, mode: 448 });
28094
+ mkdirSync7(paths.projectsDir, { recursive: true, mode: 448 });
28095
+ const logger = createLogger({
28096
+ component: "runtime",
28097
+ path: runtimeSupervisorLogFile(paths),
28098
+ stdout: false,
28099
+ maxBytes: 2 * 1024 * 1024
28100
+ });
27796
28101
  const token = randomBytes2(32).toString("hex");
27797
28102
  const tokenHash = hashKxmSupervisorToken(token);
27798
28103
  const registry = new KxmRuntimeRegistry(paths.registryDb);
@@ -27800,12 +28105,13 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
27800
28105
  const requestedPort = requestedPortOption ?? 0;
27801
28106
  let activeRuntimeId = runtimeId;
27802
28107
  const contexts = /* @__PURE__ */ new Map();
28108
+ const syncStatuses = /* @__PURE__ */ new Map();
27803
28109
  const registerSyncCredentials = (context) => {
27804
- const hubToken = resolveClientHubAuthToken(process.env, context.projectId);
28110
+ const hubToken = resolveClientHubAuthToken(env, context.projectId);
27805
28111
  if (hubToken) context.eventStore.syncRedactor.register(hubToken);
27806
- for (const key of Object.keys(process.env)) {
28112
+ for (const key of Object.keys(env)) {
27807
28113
  if (key.startsWith("KXM_") && (key.endsWith("_TOKEN") || key.endsWith("_KEY")) || key.endsWith("_API_KEY") || key.endsWith("_SECRET")) {
27808
- const value = process.env[key]?.trim();
28114
+ const value = env[key]?.trim();
27809
28115
  if (value) context.eventStore.syncRedactor.register(value);
27810
28116
  }
27811
28117
  }
@@ -27855,6 +28161,28 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
27855
28161
  });
27856
28162
  return;
27857
28163
  }
28164
+ if (request.method === "GET" && url.pathname === "/v1/sync/status") {
28165
+ const projectRoot = url.searchParams.get("projectRoot");
28166
+ const statuses = [...syncStatuses.values()].filter((status) => projectRoot === null || status.projectRoot === projectRoot).sort((left, right) => left.projectId.localeCompare(right.projectId));
28167
+ sendJson(response, 200, { ok: true, runtimeId: activeRuntimeId, projects: statuses });
28168
+ return;
28169
+ }
28170
+ if (request.method === "POST" && url.pathname === "/v1/sync/retry") {
28171
+ const body = await readJsonBody(request);
28172
+ const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
28173
+ const context = contextFor(projectRoot);
28174
+ const key = projectRuntimeKey(context.projectRoot);
28175
+ const retried = context.eventStore.retryRefusedOutbox();
28176
+ const status = syncStatuses.get(key);
28177
+ if (status) {
28178
+ const cleared = { ...status, outbox: context.eventStore.outboxStatus(), consecutiveFailures: 0 };
28179
+ delete cleared.nextAttemptAt;
28180
+ syncStatuses.set(key, cleared);
28181
+ }
28182
+ logger({ event: "runtime_sync_retry", projectId: context.projectId, retried });
28183
+ sendJson(response, 200, { ok: true, projectId: context.projectId, retried, outbox: context.eventStore.outboxStatus() });
28184
+ return;
28185
+ }
27858
28186
  if (request.method === "POST" && url.pathname === "/v1/runs") {
27859
28187
  const body = await readJsonBody(request);
27860
28188
  const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
@@ -28154,26 +28482,93 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
28154
28482
  try {
28155
28483
  contextFor(reg.projectRoot);
28156
28484
  } catch {
28485
+ logger.warn({
28486
+ event: "runtime_sync_context_unavailable",
28487
+ projectId: reg.projectId,
28488
+ message: `cannot reopen ${reg.projectRoot}: its outbox rows stay pending`
28489
+ });
28157
28490
  }
28158
28491
  }
28492
+ const recordSyncStatus = (context, next) => {
28493
+ const key = projectRuntimeKey(context.projectRoot);
28494
+ const previous = syncStatuses.get(key);
28495
+ syncStatuses.set(key, next);
28496
+ const changed = previous === void 0 || previous.state !== next.state || next.lastError !== void 0 && previous?.lastError !== next.lastError || JSON.stringify(next.outbox.refusals) !== JSON.stringify(previous?.outbox.refusals ?? []);
28497
+ if (!changed) return;
28498
+ const stalledState = next.state === "blocked" || next.state === "refusing";
28499
+ const entry = {
28500
+ event: stalledState ? "runtime_sync_stalled" : "runtime_sync_state",
28501
+ projectId: next.projectId,
28502
+ runtimeId: next.homeRuntimeId,
28503
+ state: next.state,
28504
+ pending: next.outbox.pending,
28505
+ acked: next.outbox.acked,
28506
+ refused: next.outbox.refused,
28507
+ ...next.outbox.refusals.length > 0 ? { refusals: next.outbox.refusals } : {},
28508
+ ...next.hubUrl ? { hubUrl: next.hubUrl } : {},
28509
+ ...next.lastError ? { reason: next.lastError } : {},
28510
+ ...next.nextAttemptAt ? { nextAttemptAt: next.nextAttemptAt } : {}
28511
+ };
28512
+ if (stalledState) logger.warn(entry);
28513
+ else logger.info(entry);
28514
+ };
28159
28515
  let syncing = false;
28160
28516
  const syncTimer = setInterval(() => {
28161
28517
  if (syncing || stopping) return;
28162
28518
  syncing = true;
28163
28519
  void (async () => {
28164
28520
  for (const context of [...contexts.values()]) {
28521
+ const key = projectRuntimeKey(context.projectRoot);
28522
+ const prior = syncStatuses.get(key);
28523
+ if (prior?.nextAttemptAt && Date.parse(prior.nextAttemptAt) > Date.parse(now())) continue;
28524
+ const base = {
28525
+ projectId: context.projectId,
28526
+ projectRoot: context.projectRoot,
28527
+ homeRuntimeId: context.homeRuntimeId,
28528
+ outbox: context.eventStore.outboxStatus(),
28529
+ consecutiveFailures: 0
28530
+ };
28165
28531
  try {
28166
- const client = runtimeHubClientFor(context, process.env);
28167
- if (!client) continue;
28532
+ const client = runtimeHubClientFor(context, env);
28533
+ if (!client) {
28534
+ recordSyncStatus(context, { ...base, state: "no_hub" });
28535
+ continue;
28536
+ }
28168
28537
  await client.heartbeat();
28169
- await syncKxmOutbox(context.eventStore, client, { now });
28170
- } catch {
28538
+ const pass = await syncKxmOutbox(context.eventStore, client, { now });
28539
+ const outbox = context.eventStore.outboxStatus();
28540
+ const failures = pass.blocked || pass.unconfirmed > 0 ? (prior?.consecutiveFailures ?? 0) + 1 : 0;
28541
+ const delayMs = syncBackoffMs(failures, runtimeSyncIntervalMs(env));
28542
+ recordSyncStatus(context, {
28543
+ ...base,
28544
+ hubUrl: client.options.serverUrl,
28545
+ outbox,
28546
+ state: pass.blocked ? "blocked" : outbox.refused > 0 || pass.unconfirmed > 0 ? "refusing" : "ok",
28547
+ lastCompletedAt: now(),
28548
+ lastPushed: pass.pushed,
28549
+ lastAcked: pass.acked,
28550
+ lastRefused: pass.refused,
28551
+ lastUnconfirmed: pass.unconfirmed,
28552
+ consecutiveFailures: failures,
28553
+ ...pass.blockedReason !== void 0 ? { lastError: pass.blockedReason } : {},
28554
+ ...delayMs > 0 ? { nextAttemptAt: new Date(Date.parse(now()) + delayMs).toISOString() } : {}
28555
+ });
28556
+ } catch (error) {
28557
+ const failures = (prior?.consecutiveFailures ?? 0) + 1;
28558
+ const delayMs = syncBackoffMs(failures, runtimeSyncIntervalMs(env));
28559
+ recordSyncStatus(context, {
28560
+ ...base,
28561
+ state: "blocked",
28562
+ consecutiveFailures: failures,
28563
+ lastError: syncFailureText(error),
28564
+ nextAttemptAt: new Date(Date.parse(now()) + delayMs).toISOString()
28565
+ });
28171
28566
  }
28172
28567
  }
28173
28568
  })().finally(() => {
28174
28569
  syncing = false;
28175
28570
  });
28176
- }, runtimeSyncIntervalMs());
28571
+ }, runtimeSyncIntervalMs(env));
28177
28572
  syncTimer.unref();
28178
28573
  let stopping = false;
28179
28574
  const stop = async () => {
@@ -28208,6 +28603,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
28208
28603
  }
28209
28604
  for (const context of contexts.values()) closeKxmRuntimeContext(context);
28210
28605
  contexts.clear();
28606
+ logger.close();
28211
28607
  const closed = new Promise((resolveStop) => server.close(() => resolveStop()));
28212
28608
  server.closeIdleConnections?.();
28213
28609
  await closed;