@kontextmind/kxm 0.7.77 → 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.
@@ -18441,7 +18441,7 @@ function restoreBackup(manifestPathOrDir, options = {}) {
18441
18441
  if (store.storeId === "registry" || store.storeId === "binding-store") {
18442
18442
  maxSupported = 1;
18443
18443
  } else if (store.storeId.startsWith("events:")) {
18444
- maxSupported = 6;
18444
+ maxSupported = 7;
18445
18445
  }
18446
18446
  let targetPath = store.sourcePath;
18447
18447
  if (options.projectRoot && manifest.projectRoot && targetPath.startsWith(manifest.projectRoot)) {
@@ -18720,7 +18720,7 @@ function verifyKxmDriveReceipt(receipt, events, foldedStatus, binding) {
18720
18720
  if (reasons.length === 0) return { verified: true };
18721
18721
  return { verified: false, divergence: reasons.join("; ") };
18722
18722
  }
18723
- var KXM_EVENT_STORE_SCHEMA_VERSION = 6;
18723
+ var KXM_EVENT_STORE_SCHEMA_VERSION = 7;
18724
18724
  var KXM_DRIVE_RECEIPT_SCHEMA = "kxm.drive-receipt.v1";
18725
18725
  var DRIVE_RECEIPT_MAX_BYTES = 8 * 1024;
18726
18726
  var EVENT_STORE_TABLES = {
@@ -18826,7 +18826,17 @@ var EVENT_STORE_TABLES = {
18826
18826
  "record"
18827
18827
  ],
18828
18828
  project_controls: ["project_id", "paused", "reason", "updated_at", "actor", "schema", "record"],
18829
- outbox: ["seq", "run_id", "sequence", "sync_event", "attempted_at", "acked_at"]
18829
+ outbox: [
18830
+ "seq",
18831
+ "run_id",
18832
+ "sequence",
18833
+ "sync_event",
18834
+ "attempted_at",
18835
+ "attempt_count",
18836
+ "acked_at",
18837
+ "refused_code",
18838
+ "refused_at"
18839
+ ]
18830
18840
  };
18831
18841
  var KXM_EVENT_STORE_TABLE_NAMES = Object.keys(EVENT_STORE_TABLES).sort();
18832
18842
  var EVENT_STORE_SCHEMA = `
@@ -19020,10 +19030,14 @@ CREATE TABLE IF NOT EXISTS outbox (
19020
19030
  sequence INTEGER NOT NULL,
19021
19031
  sync_event TEXT NOT NULL,
19022
19032
  attempted_at TEXT,
19033
+ attempt_count INTEGER NOT NULL DEFAULT 0,
19023
19034
  acked_at TEXT,
19035
+ refused_code TEXT,
19036
+ refused_at TEXT,
19024
19037
  UNIQUE (run_id, sequence)
19025
19038
  ) STRICT;
19026
- CREATE INDEX outbox_pending ON outbox(seq) WHERE acked_at IS NULL;
19039
+ CREATE INDEX outbox_pending ON outbox(seq) WHERE acked_at IS NULL AND refused_code IS NULL;
19040
+ CREATE INDEX outbox_refused ON outbox(seq) WHERE refused_code IS NOT NULL;
19027
19041
  `;
19028
19042
  var KxmRunEventStore = class {
19029
19043
  path;
@@ -19163,37 +19177,108 @@ var KxmRunEventStore = class {
19163
19177
  const syncEvent = deriveKxmSyncEvent(event, { redactor: this.syncRedactor });
19164
19178
  this.database.prepare("INSERT INTO outbox (run_id, sequence, sync_event) VALUES (?, ?, ?)").run(event.runId, event.sequence, kxmSyncEventBytes(syncEvent));
19165
19179
  }
19166
- /** Unacknowledged outbox rows after `afterSeq`, in outbox order, oldest first. */
19180
+ /**
19181
+ * Retryable outbox rows after `afterSeq`, in outbox order, oldest first.
19182
+ * Acked and durably refused rows are never returned: a refused row is not
19183
+ * waiting for a retry, it is waiting for an operator.
19184
+ */
19167
19185
  pendingOutbox(limit = 100, afterSeq = 0) {
19168
19186
  const rows = this.database.prepare(`
19169
- SELECT seq, run_id, sequence, sync_event, attempted_at, acked_at
19170
- FROM outbox WHERE acked_at IS NULL AND seq > ? ORDER BY seq ASC LIMIT ?
19187
+ SELECT seq, run_id, sequence, sync_event, attempt_count, attempted_at, acked_at, refused_code, refused_at
19188
+ FROM outbox WHERE acked_at IS NULL AND refused_code IS NULL AND seq > ? ORDER BY seq ASC LIMIT ?
19171
19189
  `).all(afterSeq, limit);
19172
19190
  return rows.map(outboxFromRow);
19173
19191
  }
19174
- /** Every outbox row of one run, acknowledged or not, in sequence order. */
19192
+ /** Every outbox row of one run, in any state, in sequence order. */
19175
19193
  outboxForRun(runId) {
19176
19194
  const rows = this.database.prepare(`
19177
- SELECT seq, run_id, sequence, sync_event, attempted_at, acked_at
19195
+ SELECT seq, run_id, sequence, sync_event, attempt_count, attempted_at, acked_at, refused_code, refused_at
19178
19196
  FROM outbox WHERE run_id = ? ORDER BY sequence ASC
19179
19197
  `).all(runId);
19180
19198
  return rows.map(outboxFromRow);
19181
19199
  }
19200
+ /**
19201
+ * Count one attempt against each row. Refused rows are never re-counted: they
19202
+ * are out of the queue, and a tick that reaches them again would mean the
19203
+ * refusal was cleared (which resets the count).
19204
+ */
19182
19205
  markOutboxAttempted(seqs, at) {
19183
- const statement = this.database.prepare("UPDATE outbox SET attempted_at = ? WHERE seq = ? AND acked_at IS NULL");
19206
+ const statement = this.database.prepare(`
19207
+ UPDATE outbox SET attempted_at = ?, attempt_count = attempt_count + 1
19208
+ WHERE seq = ? AND acked_at IS NULL AND refused_code IS NULL
19209
+ `);
19184
19210
  this.transaction(() => {
19185
19211
  for (const seq of seqs) statement.run(at, seq);
19186
19212
  });
19187
19213
  }
19188
19214
  /** Advance the cursor: the hub holds these rows now. Returns rows newly acked. */
19189
19215
  ackOutbox(seqs, at) {
19190
- const statement = this.database.prepare("UPDATE outbox SET acked_at = ? WHERE seq = ? AND acked_at IS NULL");
19216
+ const statement = this.database.prepare("UPDATE outbox SET acked_at = ?, refused_code = NULL, refused_at = NULL WHERE seq = ? AND acked_at IS NULL");
19191
19217
  return this.transaction(() => {
19192
19218
  let changed = 0;
19193
19219
  for (const seq of seqs) changed += Number(statement.run(at, seq).changes);
19194
19220
  return changed;
19195
19221
  });
19196
19222
  }
19223
+ /**
19224
+ * Record a durable hub refusal: the answer will not change if the same bytes
19225
+ * are sent again. Returns rows newly moved out of the pending queue.
19226
+ */
19227
+ refuseOutbox(refusals, at) {
19228
+ const statement = this.database.prepare(`
19229
+ UPDATE outbox SET refused_code = ?, refused_at = ?
19230
+ WHERE seq = ? AND acked_at IS NULL AND refused_code IS NULL
19231
+ `);
19232
+ return this.transaction(() => {
19233
+ let changed = 0;
19234
+ for (const refusal of refusals) changed += Number(statement.run(refusal.code, at, refusal.seq).changes);
19235
+ return changed;
19236
+ });
19237
+ }
19238
+ /**
19239
+ * Operator revive: put refused rows back in the pending queue with a fresh
19240
+ * attempt budget. Used after the hub-side state is corrected — the Runtime
19241
+ * must not decide on its own that a refusal has become retryable.
19242
+ */
19243
+ retryRefusedOutbox() {
19244
+ return Number(this.database.prepare(`
19245
+ UPDATE outbox SET refused_code = NULL, refused_at = NULL, attempted_at = NULL, attempt_count = 0
19246
+ WHERE acked_at IS NULL AND refused_code IS NOT NULL
19247
+ `).run().changes);
19248
+ }
19249
+ /**
19250
+ * The outbox as one read model: what is still retryable, what the hub holds,
19251
+ * and what the hub refused with which codes. This is what makes a stalled
19252
+ * sync answerable without opening SQLite.
19253
+ */
19254
+ outboxStatus() {
19255
+ const counts = this.database.prepare(`
19256
+ SELECT
19257
+ COALESCE(SUM(acked_at IS NULL AND refused_code IS NULL), 0) AS pending,
19258
+ COALESCE(SUM(acked_at IS NOT NULL), 0) AS acked,
19259
+ COALESCE(SUM(acked_at IS NULL AND refused_code IS NOT NULL), 0) AS refused,
19260
+ MAX(attempted_at) AS last_attempt_at
19261
+ FROM outbox
19262
+ `).get();
19263
+ const refusals = this.database.prepare(`
19264
+ SELECT refused_code AS code, COUNT(*) AS count
19265
+ FROM outbox WHERE acked_at IS NULL AND refused_code IS NOT NULL
19266
+ GROUP BY refused_code ORDER BY count DESC, code ASC
19267
+ `).all();
19268
+ const oldest = this.database.prepare(`
19269
+ SELECT seq FROM outbox WHERE acked_at IS NULL AND refused_code IS NULL ORDER BY seq ASC LIMIT 1
19270
+ `).get();
19271
+ return {
19272
+ pending: counts.pending,
19273
+ acked: counts.acked,
19274
+ refused: counts.refused,
19275
+ ...counts.last_attempt_at !== null ? { lastAttemptAt: counts.last_attempt_at } : {},
19276
+ ...oldest ? { oldestPendingSeq: oldest.seq } : {},
19277
+ // Mapped, not spread: node:sqlite rows are null-prototype objects, and this
19278
+ // read model is handed straight to the CLI and the supervisor's JSON answer.
19279
+ refusals: refusals.map((refusal) => ({ code: refusal.code, count: refusal.count }))
19280
+ };
19281
+ }
19197
19282
  events(runId, afterSequence = 0, limit = 200) {
19198
19283
  const rows = this.database.prepare(`
19199
19284
  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
@@ -19715,8 +19800,11 @@ function outboxFromRow(row) {
19715
19800
  runId: row.run_id,
19716
19801
  sequence: row.sequence,
19717
19802
  syncEvent: row.sync_event,
19803
+ attemptCount: row.attempt_count,
19718
19804
  ...row.attempted_at !== null ? { attemptedAt: row.attempted_at } : {},
19719
- ...row.acked_at !== null ? { ackedAt: row.acked_at } : {}
19805
+ ...row.acked_at !== null ? { ackedAt: row.acked_at } : {},
19806
+ ...row.refused_code !== null ? { refusedCode: row.refused_code } : {},
19807
+ ...row.refused_at !== null ? { refusedAt: row.refused_at } : {}
19720
19808
  };
19721
19809
  }
19722
19810
  function runFromRow(row) {
@@ -23356,9 +23444,9 @@ function assertKxmConfigError(error) {
23356
23444
  // plugins/kxm/src/runtime-supervisor.ts
23357
23445
  import { spawn as spawn3 } from "node:child_process";
23358
23446
  import { createHash as createHash13, createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
23359
- 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";
23447
+ 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";
23360
23448
  import { createServer } from "node:http";
23361
- import { dirname as dirname8, isAbsolute as isAbsolute6, join as join15 } from "node:path";
23449
+ import { dirname as dirname9, isAbsolute as isAbsolute6, join as join15 } from "node:path";
23362
23450
 
23363
23451
  // plugins/kxm/src/oneshot-producer.ts
23364
23452
  import { join as join12 } from "node:path";
@@ -27988,13 +28076,162 @@ function resolveClientHubAuthToken(env, project) {
27988
28076
  return record2?.projectTokens?.[project]?.trim() || record2?.authToken?.trim() || void 0;
27989
28077
  }
27990
28078
 
28079
+ // plugins/kxm/src/logger.ts
28080
+ import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync6, renameSync as renameSync3, statSync as statSync3, unlinkSync as unlinkSync2 } from "node:fs";
28081
+ import { dirname as dirname8 } from "node:path";
28082
+ var LOG_LEVEL_PRIORITY = {
28083
+ debug: 10,
28084
+ info: 20,
28085
+ warn: 30,
28086
+ error: 40
28087
+ };
28088
+ var DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024;
28089
+ var DEFAULT_LOG_MAX_FILES = 3;
28090
+ var SENSITIVE_KEY_PATTERN = /(?:^|_)(?:token|secret|password|apiKey|api_key|authorization|bearer)(?:$|_)/i;
28091
+ var ALLOWED_EXACT_KEYS = /* @__PURE__ */ new Set(["auth", "authType", "authMethod", "authArgs", "canUpdate", "status"]);
28092
+ function redactLogValue(val, key) {
28093
+ if (val === null || val === void 0) return val;
28094
+ if (typeof val === "string") {
28095
+ if (key && SENSITIVE_KEY_PATTERN.test(key) && !ALLOWED_EXACT_KEYS.has(key)) {
28096
+ return "[redacted]";
28097
+ }
28098
+ return redactSecrets(val);
28099
+ }
28100
+ if (typeof val === "number" || typeof val === "boolean") {
28101
+ return val;
28102
+ }
28103
+ if (Array.isArray(val)) {
28104
+ return val.map((item) => redactLogValue(item, key));
28105
+ }
28106
+ if (typeof val === "object") {
28107
+ const out = {};
28108
+ for (const [k, v] of Object.entries(val)) {
28109
+ out[k] = redactLogValue(v, k);
28110
+ }
28111
+ return out;
28112
+ }
28113
+ return String(val);
28114
+ }
28115
+ function rotateLogFiles(filePath, maxFiles) {
28116
+ for (let i = maxFiles; i >= 1; i--) {
28117
+ const current = `${filePath}.${i}`;
28118
+ if (existsSync12(current)) {
28119
+ if (i >= maxFiles) {
28120
+ try {
28121
+ unlinkSync2(current);
28122
+ } catch {
28123
+ }
28124
+ } else {
28125
+ try {
28126
+ renameSync3(current, `${filePath}.${i + 1}`);
28127
+ } catch {
28128
+ }
28129
+ }
28130
+ }
28131
+ }
28132
+ if (existsSync12(filePath)) {
28133
+ try {
28134
+ renameSync3(filePath, `${filePath}.1`);
28135
+ } catch {
28136
+ }
28137
+ }
28138
+ }
28139
+ function createLogger(options) {
28140
+ const component = options.component;
28141
+ const filePath = options.path;
28142
+ const maxBytes = Math.max(100, options.maxBytes ?? DEFAULT_LOG_MAX_BYTES);
28143
+ const maxFiles = Math.max(1, options.maxFiles ?? DEFAULT_LOG_MAX_FILES);
28144
+ const configuredLevel = options.level ?? "info";
28145
+ const isDaemon = Boolean(options.daemon ?? (process.env.KXM_DAEMON === "1" || process.env.KXM_DAEMON === "true"));
28146
+ const shouldStdout = options.stdout ?? !isDaemon;
28147
+ const correlationDefaults = options.correlation ?? {};
28148
+ let currentSize = 0;
28149
+ if (filePath && existsSync12(filePath)) {
28150
+ try {
28151
+ currentSize = statSync3(filePath).size;
28152
+ } catch {
28153
+ currentSize = 0;
28154
+ }
28155
+ }
28156
+ function emit(level, entryOrEvent, extra) {
28157
+ const minPriority = LOG_LEVEL_PRIORITY[configuredLevel] ?? LOG_LEVEL_PRIORITY.info;
28158
+ const currentPriority = LOG_LEVEL_PRIORITY[level] ?? LOG_LEVEL_PRIORITY.info;
28159
+ if (currentPriority < minPriority) return;
28160
+ let base;
28161
+ if (typeof entryOrEvent === "string") {
28162
+ base = { event: entryOrEvent, ...extra };
28163
+ } else {
28164
+ base = { ...entryOrEvent, ...extra };
28165
+ }
28166
+ const timestamp = typeof base.timestamp === "string" ? base.timestamp : (/* @__PURE__ */ new Date()).toISOString();
28167
+ delete base.timestamp;
28168
+ delete base.level;
28169
+ delete base.component;
28170
+ const payload = {
28171
+ timestamp,
28172
+ level,
28173
+ component,
28174
+ ...correlationDefaults,
28175
+ ...base
28176
+ };
28177
+ const sanitized = redactLogValue(payload);
28178
+ const line = `${JSON.stringify(sanitized)}
28179
+ `;
28180
+ if (filePath) {
28181
+ const lineBytes = Buffer.byteLength(line, "utf8");
28182
+ if (currentSize + lineBytes > maxBytes) {
28183
+ rotateLogFiles(filePath, maxFiles);
28184
+ currentSize = 0;
28185
+ }
28186
+ try {
28187
+ mkdirSync6(dirname8(filePath), { recursive: true });
28188
+ appendFileSync(filePath, line, { encoding: "utf8", mode: 384 });
28189
+ currentSize += lineBytes;
28190
+ } catch {
28191
+ }
28192
+ }
28193
+ if (shouldStdout) {
28194
+ process.stdout.write(line);
28195
+ }
28196
+ }
28197
+ const logFn = ((entryOrEvent, extra) => {
28198
+ let lvl = "info";
28199
+ if (typeof entryOrEvent === "object" && entryOrEvent !== null && typeof entryOrEvent.level === "string") {
28200
+ const candidate = entryOrEvent.level.toLowerCase();
28201
+ if (candidate === "debug" || candidate === "info" || candidate === "warn" || candidate === "error") {
28202
+ lvl = candidate;
28203
+ }
28204
+ }
28205
+ emit(lvl, entryOrEvent, extra);
28206
+ });
28207
+ logFn.info = (entryOrEvent, extra) => emit("info", entryOrEvent, extra);
28208
+ logFn.warn = (entryOrEvent, extra) => emit("warn", entryOrEvent, extra);
28209
+ logFn.error = (entryOrEvent, extra) => emit("error", entryOrEvent, extra);
28210
+ logFn.debug = (entryOrEvent, extra) => emit("debug", entryOrEvent, extra);
28211
+ logFn.child = (sub) => {
28212
+ return createLogger({
28213
+ ...options,
28214
+ component: sub.component ? `${component}.${sub.component}` : component,
28215
+ correlation: { ...correlationDefaults, ...sub.correlation }
28216
+ });
28217
+ };
28218
+ logFn.close = () => {
28219
+ };
28220
+ Object.defineProperty(logFn, "options", {
28221
+ value: Object.freeze({ ...options }),
28222
+ writable: false,
28223
+ enumerable: true
28224
+ });
28225
+ return logFn;
28226
+ }
28227
+
27991
28228
  // plugins/kxm/src/runtime-supervisor.ts
27992
28229
  function kxmSupervisorTokenFile(paths) {
27993
28230
  return join15(paths.runtimeDir, "supervisor.token");
27994
28231
  }
27995
28232
  function publishKxmSupervisorToken(paths, token) {
27996
28233
  const file = kxmSupervisorTokenFile(paths);
27997
- mkdirSync6(dirname8(file), { recursive: true, mode: 448 });
28234
+ mkdirSync7(dirname9(file), { recursive: true, mode: 448 });
27998
28235
  const temp = `${file}.${process.pid}.tmp`;
27999
28236
  writeFileSync6(temp, `${token}
28000
28237
  `, { encoding: "utf8", mode: 384 });
@@ -28002,7 +28239,7 @@ function publishKxmSupervisorToken(paths, token) {
28002
28239
  chmodSync2(temp, 384);
28003
28240
  } catch {
28004
28241
  }
28005
- renameSync3(temp, file);
28242
+ renameSync4(temp, file);
28006
28243
  try {
28007
28244
  chmodSync2(file, 384);
28008
28245
  } catch {
@@ -28036,11 +28273,11 @@ function supervisorErrorFile(paths) {
28036
28273
  }
28037
28274
  function clearSupervisorError(paths) {
28038
28275
  const file = supervisorErrorFile(paths);
28039
- if (existsSync12(file)) rmSync3(file, { force: true });
28276
+ if (existsSync13(file)) rmSync3(file, { force: true });
28040
28277
  }
28041
28278
  function recordSupervisorError(paths, message) {
28042
28279
  try {
28043
- mkdirSync6(paths.runtimeDir, { recursive: true, mode: 448 });
28280
+ mkdirSync7(paths.runtimeDir, { recursive: true, mode: 448 });
28044
28281
  writeFileSync6(supervisorErrorFile(paths), `${message}
28045
28282
  `, { encoding: "utf8", mode: 384 });
28046
28283
  } catch {
@@ -28059,7 +28296,7 @@ function readRecentSupervisorError(paths) {
28059
28296
  }
28060
28297
  }
28061
28298
  function kxmSupervisorStatus(paths) {
28062
- if (!existsSync12(paths.registryDb)) return { running: false };
28299
+ if (!existsSync13(paths.registryDb)) return { running: false };
28063
28300
  const registry = new KxmRuntimeRegistry(paths.registryDb);
28064
28301
  try {
28065
28302
  const record2 = registry.supervisor();
@@ -28240,6 +28477,8 @@ var DEFAULT_RUNTIME_SYNC_INTERVAL_MS = 1e4;
28240
28477
  var MIN_RUNTIME_SYNC_INTERVAL_MS = 250;
28241
28478
  var MAX_RUNTIME_SYNC_INTERVAL_MS = 6e4;
28242
28479
  var OUTBOX_PUSH_BATCH = 32;
28480
+ var OUTBOX_PUSH_BATCH_BYTES = 2e5;
28481
+ var RUNTIME_SYNC_MAX_BACKOFF_MS = 5 * 6e4;
28243
28482
  function runtimeSyncIntervalMs(env = process.env) {
28244
28483
  const raw = env.KXM_RUNTIME_SYNC_INTERVAL_MS?.trim();
28245
28484
  if (!raw) return DEFAULT_RUNTIME_SYNC_INTERVAL_MS;
@@ -28247,16 +28486,72 @@ function runtimeSyncIntervalMs(env = process.env) {
28247
28486
  if (!Number.isInteger(parsed)) return DEFAULT_RUNTIME_SYNC_INTERVAL_MS;
28248
28487
  return Math.min(MAX_RUNTIME_SYNC_INTERVAL_MS, Math.max(MIN_RUNTIME_SYNC_INTERVAL_MS, parsed));
28249
28488
  }
28489
+ function isDurableRefusal(outcome) {
28490
+ return outcome === "conflict" || outcome === "rejected";
28491
+ }
28492
+ function isOversizeRefusal(error) {
28493
+ return error instanceof HubHttpError && (error.statusCode === 413 || error.code === "sync_batch_too_large" || error.code === "payload_too_large");
28494
+ }
28250
28495
  async function syncKxmOutbox(eventStore, client, options = {}) {
28251
28496
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
28252
- const batchSize = options.batchSize ?? OUTBOX_PUSH_BATCH;
28253
- const result = { pushed: 0, acked: 0, conflicts: 0, rejected: 0 };
28497
+ const batchSize = Math.max(1, options.batchSize ?? OUTBOX_PUSH_BATCH);
28498
+ const result = { pushed: 0, acked: 0, refused: 0, refusals: [], unconfirmed: 0, blocked: false };
28499
+ const refuse = (row, code) => {
28500
+ if (eventStore.refuseOutbox([{ seq: row.seq, code }], now()) === 0) return;
28501
+ result.refused += 1;
28502
+ result.refusals.push({ runId: row.runId, sequence: row.sequence, code });
28503
+ };
28504
+ const deliver = async (batch) => {
28505
+ const payload = [];
28506
+ const sendable = [];
28507
+ for (const row of batch) {
28508
+ try {
28509
+ payload.push(JSON.parse(row.syncEvent));
28510
+ sendable.push(row);
28511
+ } catch {
28512
+ refuse(row, "sync_row_unreadable");
28513
+ }
28514
+ }
28515
+ if (sendable.length === 0) return true;
28516
+ eventStore.markOutboxAttempted(sendable.map((row) => row.seq), now());
28517
+ let response;
28518
+ try {
28519
+ response = await client.pushSyncEvents(payload);
28520
+ } catch (error) {
28521
+ if (isOversizeRefusal(error)) {
28522
+ if (sendable.length > 1) {
28523
+ for (const row of sendable) {
28524
+ if (!await deliver([row])) return false;
28525
+ }
28526
+ return true;
28527
+ }
28528
+ refuse(sendable[0], "sync_row_too_large");
28529
+ return true;
28530
+ }
28531
+ result.blockedReason = syncFailureText(error);
28532
+ return false;
28533
+ }
28534
+ result.pushed += sendable.length;
28535
+ const acked = [];
28536
+ sendable.forEach((row, index) => {
28537
+ const entry = response.results[index];
28538
+ const describesRow = entry !== void 0 && (entry.runId === void 0 || entry.runId === row.runId) && (entry.sequence === void 0 || entry.sequence === row.sequence);
28539
+ if (!entry || !describesRow) {
28540
+ result.unconfirmed += 1;
28541
+ return;
28542
+ }
28543
+ if (entry.outcome === "accepted" || entry.outcome === "duplicate") acked.push(row.seq);
28544
+ else if (isDurableRefusal(entry.outcome)) refuse(row, entry.code ?? `sync_${entry.outcome}`);
28545
+ else result.unconfirmed += 1;
28546
+ });
28547
+ result.acked += eventStore.ackOutbox(acked, now());
28548
+ return true;
28549
+ };
28254
28550
  let afterSeq = 0;
28255
28551
  for (; ; ) {
28256
28552
  const rows = eventStore.pendingOutbox(batchSize, afterSeq);
28257
28553
  if (rows.length === 0) return result;
28258
- const MAX_BATCH_BYTES = 2e5;
28259
- let byteBudget = MAX_BATCH_BYTES;
28554
+ let byteBudget = OUTBOX_PUSH_BATCH_BYTES;
28260
28555
  let sendCount = 0;
28261
28556
  for (const row of rows) {
28262
28557
  const rowBytes = Buffer.byteLength(row.syncEvent, "utf8") + 64;
@@ -28264,21 +28559,12 @@ async function syncKxmOutbox(eventStore, client, options = {}) {
28264
28559
  byteBudget -= rowBytes;
28265
28560
  sendCount += 1;
28266
28561
  }
28267
- const batch = rows.slice(0, sendCount);
28268
- if (batch.length === 0) batch.push(rows[0]);
28562
+ const batch = rows.slice(0, Math.max(1, sendCount));
28269
28563
  afterSeq = batch[batch.length - 1].seq;
28270
- eventStore.markOutboxAttempted(batch.map((row) => row.seq), now());
28271
- const response = await client.pushSyncEvents(batch.map((row) => JSON.parse(row.syncEvent)));
28272
- result.pushed += batch.length;
28273
- const outcomes = new Map(response.results.map((entry) => [`${entry.runId}\0${entry.sequence}`, entry.outcome]));
28274
- const acked = [];
28275
- for (const row of batch) {
28276
- const outcome = outcomes.get(`${row.runId}\0${row.sequence}`);
28277
- if (outcome === "accepted" || outcome === "duplicate") acked.push(row.seq);
28278
- else if (outcome === "conflict") result.conflicts += 1;
28279
- else result.rejected += 1;
28564
+ if (!await deliver(batch)) {
28565
+ result.blocked = true;
28566
+ return result;
28280
28567
  }
28281
- result.acked += eventStore.ackOutbox(acked, now());
28282
28568
  }
28283
28569
  }
28284
28570
  function runtimeHubClientFor(context, env) {
@@ -28293,11 +28579,24 @@ function runtimeHubClientFor(context, env) {
28293
28579
  ...authToken ? { authToken } : {}
28294
28580
  });
28295
28581
  }
28582
+ function syncBackoffMs(consecutiveStalls, intervalMs) {
28583
+ if (consecutiveStalls <= 0) return 0;
28584
+ return Math.min(RUNTIME_SYNC_MAX_BACKOFF_MS, intervalMs * 2 ** Math.min(5, consecutiveStalls - 1));
28585
+ }
28586
+ function syncFailureText(error) {
28587
+ const code = error instanceof HubHttpError ? `${error.statusCode} ${error.code ?? "error"}` : void 0;
28588
+ const message = error instanceof Error ? error.message : String(error);
28589
+ return `${code ? `${code}: ` : ""}${message}`.replace(/(kxm_[A-Za-z0-9_]+|[A-Za-z0-9._-]{40,})/g, "[redacted]").slice(0, 300);
28590
+ }
28591
+ function runtimeSupervisorLogFile(paths) {
28592
+ return join15(paths.runtimeDir, "logs", "kxm-runtime.jsonl");
28593
+ }
28296
28594
  async function startKxmRuntimeSupervisor(options = {}) {
28297
28595
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
28596
+ const env = options.env ?? process.env;
28298
28597
  const paths = kxmRuntimePaths(options.stateRoot !== void 0 ? { stateRoot: options.stateRoot } : {});
28299
28598
  try {
28300
- return await startKxmRuntimeSupervisorInner(paths, options.port, now);
28599
+ return await startKxmRuntimeSupervisorInner(paths, options.port, now, env);
28301
28600
  } catch (error) {
28302
28601
  if (!(error instanceof KxmConfigError && error.issues.some((issue3) => issue3.code === "runtime_supervisor_conflict"))) {
28303
28602
  recordSupervisorError(paths, error.message);
@@ -28305,9 +28604,15 @@ async function startKxmRuntimeSupervisor(options = {}) {
28305
28604
  throw error;
28306
28605
  }
28307
28606
  }
28308
- async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
28309
- mkdirSync6(paths.runtimeDir, { recursive: true, mode: 448 });
28310
- mkdirSync6(paths.projectsDir, { recursive: true, mode: 448 });
28607
+ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, env) {
28608
+ mkdirSync7(paths.runtimeDir, { recursive: true, mode: 448 });
28609
+ mkdirSync7(paths.projectsDir, { recursive: true, mode: 448 });
28610
+ const logger = createLogger({
28611
+ component: "runtime",
28612
+ path: runtimeSupervisorLogFile(paths),
28613
+ stdout: false,
28614
+ maxBytes: 2 * 1024 * 1024
28615
+ });
28311
28616
  const token = randomBytes3(32).toString("hex");
28312
28617
  const tokenHash = hashKxmSupervisorToken(token);
28313
28618
  const registry = new KxmRuntimeRegistry(paths.registryDb);
@@ -28315,12 +28620,13 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
28315
28620
  const requestedPort = requestedPortOption ?? 0;
28316
28621
  let activeRuntimeId = runtimeId;
28317
28622
  const contexts = /* @__PURE__ */ new Map();
28623
+ const syncStatuses = /* @__PURE__ */ new Map();
28318
28624
  const registerSyncCredentials = (context) => {
28319
- const hubToken = resolveClientHubAuthToken(process.env, context.projectId);
28625
+ const hubToken = resolveClientHubAuthToken(env, context.projectId);
28320
28626
  if (hubToken) context.eventStore.syncRedactor.register(hubToken);
28321
- for (const key of Object.keys(process.env)) {
28627
+ for (const key of Object.keys(env)) {
28322
28628
  if (key.startsWith("KXM_") && (key.endsWith("_TOKEN") || key.endsWith("_KEY")) || key.endsWith("_API_KEY") || key.endsWith("_SECRET")) {
28323
- const value = process.env[key]?.trim();
28629
+ const value = env[key]?.trim();
28324
28630
  if (value) context.eventStore.syncRedactor.register(value);
28325
28631
  }
28326
28632
  }
@@ -28370,6 +28676,28 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
28370
28676
  });
28371
28677
  return;
28372
28678
  }
28679
+ if (request.method === "GET" && url.pathname === "/v1/sync/status") {
28680
+ const projectRoot = url.searchParams.get("projectRoot");
28681
+ const statuses = [...syncStatuses.values()].filter((status) => projectRoot === null || status.projectRoot === projectRoot).sort((left, right) => left.projectId.localeCompare(right.projectId));
28682
+ sendJson(response, 200, { ok: true, runtimeId: activeRuntimeId, projects: statuses });
28683
+ return;
28684
+ }
28685
+ if (request.method === "POST" && url.pathname === "/v1/sync/retry") {
28686
+ const body = await readJsonBody(request);
28687
+ const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
28688
+ const context = contextFor(projectRoot);
28689
+ const key = projectRuntimeKey(context.projectRoot);
28690
+ const retried = context.eventStore.retryRefusedOutbox();
28691
+ const status = syncStatuses.get(key);
28692
+ if (status) {
28693
+ const cleared = { ...status, outbox: context.eventStore.outboxStatus(), consecutiveFailures: 0 };
28694
+ delete cleared.nextAttemptAt;
28695
+ syncStatuses.set(key, cleared);
28696
+ }
28697
+ logger({ event: "runtime_sync_retry", projectId: context.projectId, retried });
28698
+ sendJson(response, 200, { ok: true, projectId: context.projectId, retried, outbox: context.eventStore.outboxStatus() });
28699
+ return;
28700
+ }
28373
28701
  if (request.method === "POST" && url.pathname === "/v1/runs") {
28374
28702
  const body = await readJsonBody(request);
28375
28703
  const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
@@ -28669,26 +28997,93 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
28669
28997
  try {
28670
28998
  contextFor(reg.projectRoot);
28671
28999
  } catch {
29000
+ logger.warn({
29001
+ event: "runtime_sync_context_unavailable",
29002
+ projectId: reg.projectId,
29003
+ message: `cannot reopen ${reg.projectRoot}: its outbox rows stay pending`
29004
+ });
28672
29005
  }
28673
29006
  }
29007
+ const recordSyncStatus = (context, next) => {
29008
+ const key = projectRuntimeKey(context.projectRoot);
29009
+ const previous = syncStatuses.get(key);
29010
+ syncStatuses.set(key, next);
29011
+ 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 ?? []);
29012
+ if (!changed) return;
29013
+ const stalledState = next.state === "blocked" || next.state === "refusing";
29014
+ const entry = {
29015
+ event: stalledState ? "runtime_sync_stalled" : "runtime_sync_state",
29016
+ projectId: next.projectId,
29017
+ runtimeId: next.homeRuntimeId,
29018
+ state: next.state,
29019
+ pending: next.outbox.pending,
29020
+ acked: next.outbox.acked,
29021
+ refused: next.outbox.refused,
29022
+ ...next.outbox.refusals.length > 0 ? { refusals: next.outbox.refusals } : {},
29023
+ ...next.hubUrl ? { hubUrl: next.hubUrl } : {},
29024
+ ...next.lastError ? { reason: next.lastError } : {},
29025
+ ...next.nextAttemptAt ? { nextAttemptAt: next.nextAttemptAt } : {}
29026
+ };
29027
+ if (stalledState) logger.warn(entry);
29028
+ else logger.info(entry);
29029
+ };
28674
29030
  let syncing = false;
28675
29031
  const syncTimer = setInterval(() => {
28676
29032
  if (syncing || stopping) return;
28677
29033
  syncing = true;
28678
29034
  void (async () => {
28679
29035
  for (const context of [...contexts.values()]) {
29036
+ const key = projectRuntimeKey(context.projectRoot);
29037
+ const prior = syncStatuses.get(key);
29038
+ if (prior?.nextAttemptAt && Date.parse(prior.nextAttemptAt) > Date.parse(now())) continue;
29039
+ const base = {
29040
+ projectId: context.projectId,
29041
+ projectRoot: context.projectRoot,
29042
+ homeRuntimeId: context.homeRuntimeId,
29043
+ outbox: context.eventStore.outboxStatus(),
29044
+ consecutiveFailures: 0
29045
+ };
28680
29046
  try {
28681
- const client = runtimeHubClientFor(context, process.env);
28682
- if (!client) continue;
29047
+ const client = runtimeHubClientFor(context, env);
29048
+ if (!client) {
29049
+ recordSyncStatus(context, { ...base, state: "no_hub" });
29050
+ continue;
29051
+ }
28683
29052
  await client.heartbeat();
28684
- await syncKxmOutbox(context.eventStore, client, { now });
28685
- } catch {
29053
+ const pass = await syncKxmOutbox(context.eventStore, client, { now });
29054
+ const outbox = context.eventStore.outboxStatus();
29055
+ const failures = pass.blocked || pass.unconfirmed > 0 ? (prior?.consecutiveFailures ?? 0) + 1 : 0;
29056
+ const delayMs = syncBackoffMs(failures, runtimeSyncIntervalMs(env));
29057
+ recordSyncStatus(context, {
29058
+ ...base,
29059
+ hubUrl: client.options.serverUrl,
29060
+ outbox,
29061
+ state: pass.blocked ? "blocked" : outbox.refused > 0 || pass.unconfirmed > 0 ? "refusing" : "ok",
29062
+ lastCompletedAt: now(),
29063
+ lastPushed: pass.pushed,
29064
+ lastAcked: pass.acked,
29065
+ lastRefused: pass.refused,
29066
+ lastUnconfirmed: pass.unconfirmed,
29067
+ consecutiveFailures: failures,
29068
+ ...pass.blockedReason !== void 0 ? { lastError: pass.blockedReason } : {},
29069
+ ...delayMs > 0 ? { nextAttemptAt: new Date(Date.parse(now()) + delayMs).toISOString() } : {}
29070
+ });
29071
+ } catch (error) {
29072
+ const failures = (prior?.consecutiveFailures ?? 0) + 1;
29073
+ const delayMs = syncBackoffMs(failures, runtimeSyncIntervalMs(env));
29074
+ recordSyncStatus(context, {
29075
+ ...base,
29076
+ state: "blocked",
29077
+ consecutiveFailures: failures,
29078
+ lastError: syncFailureText(error),
29079
+ nextAttemptAt: new Date(Date.parse(now()) + delayMs).toISOString()
29080
+ });
28686
29081
  }
28687
29082
  }
28688
29083
  })().finally(() => {
28689
29084
  syncing = false;
28690
29085
  });
28691
- }, runtimeSyncIntervalMs());
29086
+ }, runtimeSyncIntervalMs(env));
28692
29087
  syncTimer.unref();
28693
29088
  let stopping = false;
28694
29089
  const stop = async () => {
@@ -28723,6 +29118,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now) {
28723
29118
  }
28724
29119
  for (const context of contexts.values()) closeKxmRuntimeContext(context);
28725
29120
  contexts.clear();
29121
+ logger.close();
28726
29122
  const closed = new Promise((resolveStop) => server.close(() => resolveStop()));
28727
29123
  server.closeIdleConnections?.();
28728
29124
  await closed;
@@ -29261,155 +29657,6 @@ function createKxmPiProducer(options = {}) {
29261
29657
  return producer;
29262
29658
  }
29263
29659
 
29264
- // plugins/kxm/src/logger.ts
29265
- import { appendFileSync, existsSync as existsSync13, mkdirSync as mkdirSync7, renameSync as renameSync4, statSync as statSync3, unlinkSync as unlinkSync2 } from "node:fs";
29266
- import { dirname as dirname9 } from "node:path";
29267
- var LOG_LEVEL_PRIORITY = {
29268
- debug: 10,
29269
- info: 20,
29270
- warn: 30,
29271
- error: 40
29272
- };
29273
- var DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024;
29274
- var DEFAULT_LOG_MAX_FILES = 3;
29275
- var SENSITIVE_KEY_PATTERN = /(?:^|_)(?:token|secret|password|apiKey|api_key|authorization|bearer)(?:$|_)/i;
29276
- var ALLOWED_EXACT_KEYS = /* @__PURE__ */ new Set(["auth", "authType", "authMethod", "authArgs", "canUpdate", "status"]);
29277
- function redactLogValue(val, key) {
29278
- if (val === null || val === void 0) return val;
29279
- if (typeof val === "string") {
29280
- if (key && SENSITIVE_KEY_PATTERN.test(key) && !ALLOWED_EXACT_KEYS.has(key)) {
29281
- return "[redacted]";
29282
- }
29283
- return redactSecrets(val);
29284
- }
29285
- if (typeof val === "number" || typeof val === "boolean") {
29286
- return val;
29287
- }
29288
- if (Array.isArray(val)) {
29289
- return val.map((item) => redactLogValue(item, key));
29290
- }
29291
- if (typeof val === "object") {
29292
- const out = {};
29293
- for (const [k, v] of Object.entries(val)) {
29294
- out[k] = redactLogValue(v, k);
29295
- }
29296
- return out;
29297
- }
29298
- return String(val);
29299
- }
29300
- function rotateLogFiles(filePath, maxFiles) {
29301
- for (let i = maxFiles; i >= 1; i--) {
29302
- const current = `${filePath}.${i}`;
29303
- if (existsSync13(current)) {
29304
- if (i >= maxFiles) {
29305
- try {
29306
- unlinkSync2(current);
29307
- } catch {
29308
- }
29309
- } else {
29310
- try {
29311
- renameSync4(current, `${filePath}.${i + 1}`);
29312
- } catch {
29313
- }
29314
- }
29315
- }
29316
- }
29317
- if (existsSync13(filePath)) {
29318
- try {
29319
- renameSync4(filePath, `${filePath}.1`);
29320
- } catch {
29321
- }
29322
- }
29323
- }
29324
- function createLogger(options) {
29325
- const component = options.component;
29326
- const filePath = options.path;
29327
- const maxBytes = Math.max(100, options.maxBytes ?? DEFAULT_LOG_MAX_BYTES);
29328
- const maxFiles = Math.max(1, options.maxFiles ?? DEFAULT_LOG_MAX_FILES);
29329
- const configuredLevel = options.level ?? "info";
29330
- const isDaemon = Boolean(options.daemon ?? (process.env.KXM_DAEMON === "1" || process.env.KXM_DAEMON === "true"));
29331
- const shouldStdout = options.stdout ?? !isDaemon;
29332
- const correlationDefaults = options.correlation ?? {};
29333
- let currentSize = 0;
29334
- if (filePath && existsSync13(filePath)) {
29335
- try {
29336
- currentSize = statSync3(filePath).size;
29337
- } catch {
29338
- currentSize = 0;
29339
- }
29340
- }
29341
- function emit(level, entryOrEvent, extra) {
29342
- const minPriority = LOG_LEVEL_PRIORITY[configuredLevel] ?? LOG_LEVEL_PRIORITY.info;
29343
- const currentPriority = LOG_LEVEL_PRIORITY[level] ?? LOG_LEVEL_PRIORITY.info;
29344
- if (currentPriority < minPriority) return;
29345
- let base;
29346
- if (typeof entryOrEvent === "string") {
29347
- base = { event: entryOrEvent, ...extra };
29348
- } else {
29349
- base = { ...entryOrEvent, ...extra };
29350
- }
29351
- const timestamp = typeof base.timestamp === "string" ? base.timestamp : (/* @__PURE__ */ new Date()).toISOString();
29352
- delete base.timestamp;
29353
- delete base.level;
29354
- delete base.component;
29355
- const payload = {
29356
- timestamp,
29357
- level,
29358
- component,
29359
- ...correlationDefaults,
29360
- ...base
29361
- };
29362
- const sanitized = redactLogValue(payload);
29363
- const line = `${JSON.stringify(sanitized)}
29364
- `;
29365
- if (filePath) {
29366
- const lineBytes = Buffer.byteLength(line, "utf8");
29367
- if (currentSize + lineBytes > maxBytes) {
29368
- rotateLogFiles(filePath, maxFiles);
29369
- currentSize = 0;
29370
- }
29371
- try {
29372
- mkdirSync7(dirname9(filePath), { recursive: true });
29373
- appendFileSync(filePath, line, { encoding: "utf8", mode: 384 });
29374
- currentSize += lineBytes;
29375
- } catch {
29376
- }
29377
- }
29378
- if (shouldStdout) {
29379
- process.stdout.write(line);
29380
- }
29381
- }
29382
- const logFn = ((entryOrEvent, extra) => {
29383
- let lvl = "info";
29384
- if (typeof entryOrEvent === "object" && entryOrEvent !== null && typeof entryOrEvent.level === "string") {
29385
- const candidate = entryOrEvent.level.toLowerCase();
29386
- if (candidate === "debug" || candidate === "info" || candidate === "warn" || candidate === "error") {
29387
- lvl = candidate;
29388
- }
29389
- }
29390
- emit(lvl, entryOrEvent, extra);
29391
- });
29392
- logFn.info = (entryOrEvent, extra) => emit("info", entryOrEvent, extra);
29393
- logFn.warn = (entryOrEvent, extra) => emit("warn", entryOrEvent, extra);
29394
- logFn.error = (entryOrEvent, extra) => emit("error", entryOrEvent, extra);
29395
- logFn.debug = (entryOrEvent, extra) => emit("debug", entryOrEvent, extra);
29396
- logFn.child = (sub) => {
29397
- return createLogger({
29398
- ...options,
29399
- component: sub.component ? `${component}.${sub.component}` : component,
29400
- correlation: { ...correlationDefaults, ...sub.correlation }
29401
- });
29402
- };
29403
- logFn.close = () => {
29404
- };
29405
- Object.defineProperty(logFn, "options", {
29406
- value: Object.freeze({ ...options }),
29407
- writable: false,
29408
- enumerable: true
29409
- });
29410
- return logFn;
29411
- }
29412
-
29413
29660
  // plugins/kxm/src/improve.ts
29414
29661
  import { createHash as createHash14 } from "node:crypto";
29415
29662
  import { existsSync as existsSync14, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "node:fs";