@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/docs/contracts/synchronization.md +31 -3
- package/docs/operations.md +27 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +63 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +443 -47
- package/plugins/kxm/dist/runtime.js +444 -197
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/project.ts +57 -3
- package/plugins/kxm/src/cli.ts +4 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime-store.ts +121 -10
- package/plugins/kxm/src/runtime-supervisor.ts +323 -42
|
@@ -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
|
|
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
|
|
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 =
|
|
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: [
|
|
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
|
-
/**
|
|
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,
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
27761
|
+
if (existsSync13(file)) rmSync3(file, { force: true });
|
|
27525
27762
|
}
|
|
27526
27763
|
function recordSupervisorError(paths, message) {
|
|
27527
27764
|
try {
|
|
27528
|
-
|
|
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 (!
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
27756
|
-
|
|
27757
|
-
|
|
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
|
-
|
|
27795
|
-
|
|
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(
|
|
28110
|
+
const hubToken = resolveClientHubAuthToken(env, context.projectId);
|
|
27805
28111
|
if (hubToken) context.eventStore.syncRedactor.register(hubToken);
|
|
27806
|
-
for (const key of Object.keys(
|
|
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 =
|
|
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,
|
|
28167
|
-
if (!client)
|
|
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
|
-
|
|
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;
|