@kontextmind/kxm 0.7.71 → 0.7.72
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/CHANGELOG.md +22 -0
- package/docs/operations.md +37 -5
- package/docs/test-matrix.md +1 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +42 -2
- package/plugins/kxm/dist/client.js +34 -0
- package/plugins/kxm/dist/core.js +8 -0
- package/plugins/kxm/dist/extension.js +34 -0
- package/plugins/kxm/dist/mcp-server.js +35 -1
- package/plugins/kxm/dist/runtime.js +4 -2
- package/plugins/kxm/dist/server.js +275 -6
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm/references/protocol.md +18 -0
- package/plugins/kxm/src/client.ts +39 -1
- package/plugins/kxm/src/database.ts +6 -2
- package/plugins/kxm/src/external-effects.ts +390 -66
- package/plugins/kxm/src/hub.ts +137 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/protocol.ts +25 -0
- package/plugins/kxm/src/store.ts +213 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
6
6
|
|
|
7
7
|
### Added
|
|
8
8
|
|
|
9
|
+
- **Fenced hub leases, and shared external effects that will not run without one.**
|
|
10
|
+
`POST /v1/leases/:resource/acquire|renew|release` are agent-authenticated and
|
|
11
|
+
project-scoped (the hub prefixes the caller's project onto the resource name). Each call
|
|
12
|
+
is a compare-and-set inside one store transaction on the **hub clock**, with TTLs bounded
|
|
13
|
+
to 5 s–10 min. The fencing token starts at 1, survives renewal unchanged, and increments
|
|
14
|
+
only when a new holder takes over an expired lease, so a holder that returns after its
|
|
15
|
+
deadline is told its token was superseded instead of writing behind its replacement.
|
|
16
|
+
Shared external effects (`git-push` to a ref the run does not own, `pr-create`,
|
|
17
|
+
`tracker-issue`, `webhook`) acquire a lease keyed by `targetRef` before executing, record
|
|
18
|
+
`{leaseResource, fencingToken}` in the receipt, renew on the existing Q6 heartbeat, and
|
|
19
|
+
re-present the token at commit; a superseded token leaves the effect `in-flight` and the
|
|
20
|
+
attempt `blocked_uncertain` with nothing retrying. An unreachable hub refuses the effect
|
|
21
|
+
(`effect_lease_unavailable`) rather than executing it unfenced. Unique-namespace kinds
|
|
22
|
+
(`git-branch`, `git-commit`, and a push to the run's own branch) stay lease-free.
|
|
23
|
+
|
|
9
24
|
- **`kxm peer send --allow-offline` queues to a registered offline peer.**
|
|
10
25
|
`POST /v1/messages` accepts `allowOffline: true` (also `kxm_send.allowOffline`): a
|
|
11
26
|
registered agent in the same project is stored `queued` instead of `target_not_found`,
|
|
@@ -42,6 +57,13 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
42
57
|
|
|
43
58
|
### Changed
|
|
44
59
|
|
|
60
|
+
- **Hub store schema v3 → v4, external-effects ledger v1 → v2.** The hub store gains a
|
|
61
|
+
`leases` table and the ledger gains `lease_resource`/`fencing_token` columns. Neither has
|
|
62
|
+
a migration lane: an older file is refused at open with `runtime_schema_outdated`, and the
|
|
63
|
+
hub backup/restore ceiling in `database.ts` moves to 4 with the bump, so a v3 backup must
|
|
64
|
+
be restored with the release that produced it. Delete the state file to start fresh and
|
|
65
|
+
let `kxm hub start` recreate it.
|
|
66
|
+
|
|
45
67
|
- **`kxm tenant status`: one composed read for the portal, with the authorities labelled.**
|
|
46
68
|
The portal needs hub metadata (roster, message queue, the hub's run projection) *and* the
|
|
47
69
|
Runtime's authoritative run state, and the failure mode S2 exists to prevent is rendering
|
package/docs/operations.md
CHANGED
|
@@ -315,7 +315,10 @@ restore lands somewhere the running service will not look.
|
|
|
315
315
|
copies, not the file copy**: configuration, repository bindings, prompt sidecars,
|
|
316
316
|
routing manifests and update configuration are not databases, so a snapshot-only backup
|
|
317
317
|
reproduces exactly the failure this section exists to remove. The hub's own backup path already writes a hashed manifest and records
|
|
318
|
-
a schema version ceiling; keep that manifest with the files.
|
|
318
|
+
a schema version ceiling; keep that manifest with the files. That ceiling is
|
|
319
|
+
**hub store v4** as of the fenced-lease release: a backup taken by an earlier
|
|
320
|
+
build records v3 and is refused by this one, because there is no migration lane.
|
|
321
|
+
Restore such a backup with the release that produced it, or start fresh.
|
|
319
322
|
3. Record the package version, configuration revision and schema versions beside the copy.
|
|
320
323
|
A restore that cannot state which release produced it is not a restore path.
|
|
321
324
|
4. Keep at least one rotation, and bound retention explicitly — run events and prompt
|
|
@@ -392,10 +395,10 @@ Broader deployments need shared state and coordination, external identity and fi
|
|
|
392
395
|
|
|
393
396
|
## v0.5 context/state storage
|
|
394
397
|
|
|
395
|
-
The hub database (schema version
|
|
396
|
-
agents, messages, workflow runs, and the journal. Temporal state,
|
|
397
|
-
records, and their audit trails live in the same SQLite file and
|
|
398
|
-
place from v0.4 databases.
|
|
398
|
+
The hub database (schema version 4) carries `context_items` and `leases`
|
|
399
|
+
alongside agents, messages, workflow runs, and the journal. Temporal state,
|
|
400
|
+
knowledge records, and their audit trails live in the same SQLite file and
|
|
401
|
+
upgrade in place from v0.4 databases.
|
|
399
402
|
|
|
400
403
|
- **Backup and restore**: include the hub database file and, if used, the
|
|
401
404
|
`.kxm/skills/` and `.kxm/knowledge/` trees. The wiki is a compiled view and
|
|
@@ -412,6 +415,35 @@ place from v0.4 databases.
|
|
|
412
415
|
to open on an older runtime). Restore a database backup taken before the
|
|
413
416
|
upgrade instead.
|
|
414
417
|
|
|
418
|
+
### Fenced leases over shared resources
|
|
419
|
+
|
|
420
|
+
`leases` holds one row per project-scoped resource: the holder, a monotonic
|
|
421
|
+
fencing token, and a deadline. `POST /v1/leases/:resource/acquire|renew|release`
|
|
422
|
+
are agent-authenticated and scoped to the caller's project, which the hub
|
|
423
|
+
prefixes onto the resource name — two projects naming the same branch never
|
|
424
|
+
contend. TTLs are bounded to 5 s–10 min and every decision is made on the **hub
|
|
425
|
+
clock** inside one store transaction, so a skewed client cannot extend its own
|
|
426
|
+
grip.
|
|
427
|
+
|
|
428
|
+
The token is the safety property. It starts at 1, stays put across renewals, and
|
|
429
|
+
increments only when a new holder takes over an expired lease. A holder that
|
|
430
|
+
comes back after its deadline is therefore told its token was superseded rather
|
|
431
|
+
than allowed to write behind whoever replaced it. Shared external effects
|
|
432
|
+
(`git-push` to a ref the run does not own, `pr-create`, `tracker-issue`,
|
|
433
|
+
`webhook`) take a lease before executing and re-present the token at commit; a
|
|
434
|
+
superseded token leaves the effect `in-flight` and the attempt
|
|
435
|
+
`blocked_uncertain` for an operator to resolve, and nothing retries it. An
|
|
436
|
+
unreachable hub refuses the effect (`effect_lease_unavailable`) rather than
|
|
437
|
+
running it unfenced.
|
|
438
|
+
|
|
439
|
+
Expired rows are **not** reaped immediately — their token is what the next
|
|
440
|
+
takeover has to increment past. The retention sweep drops rows whose deadline is
|
|
441
|
+
older than the run-retention window (7 days by default), far beyond any live
|
|
442
|
+
holder. `kxm_leases_granted_total`, `kxm_leases_refused_total` and
|
|
443
|
+
`kxm_leases_released_total` in `/metrics` report contention;
|
|
444
|
+
`lease_acquired`, `lease_renewed`, `lease_released`, `lease_denied` and
|
|
445
|
+
`lease_purged` are the structured log events.
|
|
446
|
+
|
|
415
447
|
## Hub Q&A / knowledge base
|
|
416
448
|
|
|
417
449
|
- [What is all stored on the hub?](kb/qa-what-the-hub-stores.md)
|
package/docs/test-matrix.md
CHANGED
|
@@ -23,6 +23,7 @@ npm run verify
|
|
|
23
23
|
| Queue, acknowledgement, visibility, reply, and authorization | `test/core/hub-api.test.ts`, `test/core/hub.test.ts` |
|
|
24
24
|
| Queued/delivered replay after recipient restart reuses one message record | `test/core/hub.test.ts`, `test/core/extension.test.ts`, `test/core/mcp.test.ts` |
|
|
25
25
|
| Known-offline peer send with `allowOffline` queues, delivers once on resumption, and expires unread by TTL | `test/core/hub-api.test.ts` |
|
|
26
|
+
| Fenced hub leases: CAS acquire/renew/release, monotonic token on takeover only, hub-clocked expiry, and a shared external effect refused at commit under a superseded token | `test/core/hub-api.test.ts`, `test/core/store.test.ts`, `test/core/external-effects.test.ts` |
|
|
26
27
|
| One-to-three-peer fanout, recoverable local timeouts/aborts, exact retries, and partial-error collection | `test/core/client.test.ts`, `test/core/hub-api.test.ts`, `test/core/extension.test.ts`, `test/core/mcp.test.ts` |
|
|
27
28
|
| TTL expiry, sender cancellation, and terminal retention | `test/core/hub-api.test.ts` |
|
|
28
29
|
| Terminal inbound cleanup and next-request activation | `test/core/extension.test.ts`, `test/core/mcp.test.ts` |
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.72",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -18360,6 +18360,8 @@ var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
|
18360
18360
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
18361
18361
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
18362
18362
|
var MAX_AGENT_HOST_CHARS = 64;
|
|
18363
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
18364
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
18363
18365
|
var ProtocolError = class extends Error {
|
|
18364
18366
|
statusCode;
|
|
18365
18367
|
code;
|
|
@@ -19396,6 +19398,37 @@ var HubClient = class {
|
|
|
19396
19398
|
);
|
|
19397
19399
|
return result.message;
|
|
19398
19400
|
}
|
|
19401
|
+
// ----- Fenced leases over shared resources (P3) -----
|
|
19402
|
+
/**
|
|
19403
|
+
* Take or extend the lease over `resource` inside this client's project.
|
|
19404
|
+
*
|
|
19405
|
+
* The returned `fencingToken` is the whole point: hold it, present it on every
|
|
19406
|
+
* renewal, and present it again before committing anything shared. A hub that
|
|
19407
|
+
* has moved past it refuses, and the caller must stop rather than retry —
|
|
19408
|
+
* another holder owns the resource now. Rejects `HubHttpError` with code
|
|
19409
|
+
* `lease_held` when a live holder has it.
|
|
19410
|
+
*/
|
|
19411
|
+
async acquireLease(resource, ttlMs) {
|
|
19412
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/acquire`, {
|
|
19413
|
+
method: "POST",
|
|
19414
|
+
body: JSON.stringify(ttlMs === void 0 ? {} : { ttlMs })
|
|
19415
|
+
});
|
|
19416
|
+
}
|
|
19417
|
+
/** Extend a lease this client holds. The token never changes on renewal; a
|
|
19418
|
+
* `lease_superseded` or `lease_expired` refusal means it is gone. */
|
|
19419
|
+
async renewLease(resource, fencingToken, ttlMs) {
|
|
19420
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/renew`, {
|
|
19421
|
+
method: "POST",
|
|
19422
|
+
body: JSON.stringify(ttlMs === void 0 ? { fencingToken } : { fencingToken, ttlMs })
|
|
19423
|
+
});
|
|
19424
|
+
}
|
|
19425
|
+
/** Give the resource back. */
|
|
19426
|
+
async releaseLease(resource, fencingToken) {
|
|
19427
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/release`, {
|
|
19428
|
+
method: "POST",
|
|
19429
|
+
body: JSON.stringify({ fencingToken })
|
|
19430
|
+
});
|
|
19431
|
+
}
|
|
19399
19432
|
async listWorkflows() {
|
|
19400
19433
|
const result = await this.request("/v1/workflows");
|
|
19401
19434
|
return result.runs;
|
|
@@ -19589,6 +19622,7 @@ var HubClient = class {
|
|
|
19589
19622
|
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
|
|
19590
19623
|
if (typeof body[key] === "string") extras[key] = body[key];
|
|
19591
19624
|
}
|
|
19625
|
+
if (body.lease && typeof body.lease === "object") extras.lease = body.lease;
|
|
19592
19626
|
throw new HubHttpError(
|
|
19593
19627
|
response.status,
|
|
19594
19628
|
String(body.error ?? `HTTP ${response.status}`),
|
|
@@ -23862,7 +23896,7 @@ function discoverProjectStores(projectRoot, options = {}) {
|
|
|
23862
23896
|
const stores = [];
|
|
23863
23897
|
const hubPath = options.hubDataPath ? resolve5(options.hubDataPath) : join8(root, ".kxm", "state", "kxm.db");
|
|
23864
23898
|
if (existsSync8(hubPath)) {
|
|
23865
|
-
stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion:
|
|
23899
|
+
stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion: 4 });
|
|
23866
23900
|
}
|
|
23867
23901
|
const registryPath = join8(root, ".kxm", "runtime", "registry.db");
|
|
23868
23902
|
if (existsSync8(registryPath)) {
|
|
@@ -23968,7 +24002,7 @@ function restoreBackup(manifestPathOrDir, options = {}) {
|
|
|
23968
24002
|
`backup file ${store.backupFile} sha256 ${actualSha256} does not match manifest hash ${store.sha256}`
|
|
23969
24003
|
);
|
|
23970
24004
|
}
|
|
23971
|
-
let maxSupported =
|
|
24005
|
+
let maxSupported = 4;
|
|
23972
24006
|
if (store.storeId === "registry" || store.storeId === "binding-store") {
|
|
23973
24007
|
maxSupported = 1;
|
|
23974
24008
|
} else if (store.storeId.startsWith("events:")) {
|
|
@@ -42603,6 +42637,12 @@ function slugifyBranchPart(text, maxLength = 40) {
|
|
|
42603
42637
|
if (cleaned.length <= maxLength) return cleaned;
|
|
42604
42638
|
return cleaned.slice(0, maxLength).replace(/-+$/, "");
|
|
42605
42639
|
}
|
|
42640
|
+
var SHARED_EFFECT_KINDS = Object.freeze([
|
|
42641
|
+
"git-push",
|
|
42642
|
+
"pr-create",
|
|
42643
|
+
"tracker-issue",
|
|
42644
|
+
"webhook"
|
|
42645
|
+
]);
|
|
42606
42646
|
|
|
42607
42647
|
// plugins/kxm/src/local-snapshot.ts
|
|
42608
42648
|
import { existsSync as existsSync26, readdirSync as readdirSync11, readFileSync as readFileSync26 } from "node:fs";
|
|
@@ -8,6 +8,8 @@ var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
|
8
8
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
9
9
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
10
10
|
var MAX_AGENT_HOST_CHARS = 64;
|
|
11
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
12
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
11
13
|
|
|
12
14
|
// plugins/kxm/src/workflow.ts
|
|
13
15
|
function canonicalWorkflowEvidenceKey(value) {
|
|
@@ -219,6 +221,37 @@ var HubClient = class {
|
|
|
219
221
|
);
|
|
220
222
|
return result.message;
|
|
221
223
|
}
|
|
224
|
+
// ----- Fenced leases over shared resources (P3) -----
|
|
225
|
+
/**
|
|
226
|
+
* Take or extend the lease over `resource` inside this client's project.
|
|
227
|
+
*
|
|
228
|
+
* The returned `fencingToken` is the whole point: hold it, present it on every
|
|
229
|
+
* renewal, and present it again before committing anything shared. A hub that
|
|
230
|
+
* has moved past it refuses, and the caller must stop rather than retry —
|
|
231
|
+
* another holder owns the resource now. Rejects `HubHttpError` with code
|
|
232
|
+
* `lease_held` when a live holder has it.
|
|
233
|
+
*/
|
|
234
|
+
async acquireLease(resource, ttlMs) {
|
|
235
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/acquire`, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
body: JSON.stringify(ttlMs === void 0 ? {} : { ttlMs })
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/** Extend a lease this client holds. The token never changes on renewal; a
|
|
241
|
+
* `lease_superseded` or `lease_expired` refusal means it is gone. */
|
|
242
|
+
async renewLease(resource, fencingToken, ttlMs) {
|
|
243
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/renew`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
body: JSON.stringify(ttlMs === void 0 ? { fencingToken } : { fencingToken, ttlMs })
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** Give the resource back. */
|
|
249
|
+
async releaseLease(resource, fencingToken) {
|
|
250
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/release`, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
body: JSON.stringify({ fencingToken })
|
|
253
|
+
});
|
|
254
|
+
}
|
|
222
255
|
async listWorkflows() {
|
|
223
256
|
const result = await this.request("/v1/workflows");
|
|
224
257
|
return result.runs;
|
|
@@ -412,6 +445,7 @@ var HubClient = class {
|
|
|
412
445
|
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
|
|
413
446
|
if (typeof body[key] === "string") extras[key] = body[key];
|
|
414
447
|
}
|
|
448
|
+
if (body.lease && typeof body.lease === "object") extras.lease = body.lease;
|
|
415
449
|
throw new HubHttpError(
|
|
416
450
|
response.status,
|
|
417
451
|
String(body.error ?? `HTTP ${response.status}`),
|
package/plugins/kxm/dist/core.js
CHANGED
|
@@ -13,6 +13,10 @@ var DEFAULT_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
|
13
13
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
14
14
|
var MAX_CONTENT_CHARS = 32e3;
|
|
15
15
|
var MAX_AGENT_HOST_CHARS = 64;
|
|
16
|
+
var MIN_LEASE_TTL_MS = 5e3;
|
|
17
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
18
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
19
|
+
var MAX_LEASE_RESOURCE_CHARS = 200;
|
|
16
20
|
function agentPresenceView(agent, staleAfterMs = DEFAULT_STALE_AFTER_MS, now = Date.now()) {
|
|
17
21
|
const lastSeenMs = Date.parse(agent.lastSeenAt);
|
|
18
22
|
const leaseExpiresAtMs = (Number.isFinite(lastSeenMs) ? lastSeenMs : 0) + staleAfterMs;
|
|
@@ -2017,6 +2021,7 @@ export {
|
|
|
2017
2021
|
AGENT_COMMANDS,
|
|
2018
2022
|
AGENT_COMMANDS_MAP,
|
|
2019
2023
|
BEHAVIORAL_HASH_VERSION,
|
|
2024
|
+
DEFAULT_LEASE_TTL_MS,
|
|
2020
2025
|
DEFAULT_LOG_MAX_BYTES,
|
|
2021
2026
|
DEFAULT_LOG_MAX_FILES,
|
|
2022
2027
|
DEFAULT_MAX_HOPS,
|
|
@@ -2031,9 +2036,12 @@ export {
|
|
|
2031
2036
|
MAX_BODY_BYTES,
|
|
2032
2037
|
MAX_CONTENT_CHARS,
|
|
2033
2038
|
MAX_CONTEXT_ITEM_IDS,
|
|
2039
|
+
MAX_LEASE_RESOURCE_CHARS,
|
|
2040
|
+
MAX_LEASE_TTL_MS,
|
|
2034
2041
|
MAX_MESSAGE_TTL_MS,
|
|
2035
2042
|
MAX_PROVIDER_METADATA_FIELDS,
|
|
2036
2043
|
MAX_SKILL_REFS,
|
|
2044
|
+
MIN_LEASE_TTL_MS,
|
|
2037
2045
|
MIN_MESSAGE_RETENTION_MS,
|
|
2038
2046
|
MIN_MESSAGE_TTL_MS,
|
|
2039
2047
|
ProtocolError,
|
|
@@ -14543,6 +14543,8 @@ var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
|
14543
14543
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
14544
14544
|
var MAX_CONTENT_CHARS = 32e3;
|
|
14545
14545
|
var MAX_AGENT_HOST_CHARS = 64;
|
|
14546
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
14547
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
14546
14548
|
|
|
14547
14549
|
// plugins/kxm/src/workflow.ts
|
|
14548
14550
|
function canonicalWorkflowEvidenceKey(value) {
|
|
@@ -14754,6 +14756,37 @@ var HubClient = class {
|
|
|
14754
14756
|
);
|
|
14755
14757
|
return result2.message;
|
|
14756
14758
|
}
|
|
14759
|
+
// ----- Fenced leases over shared resources (P3) -----
|
|
14760
|
+
/**
|
|
14761
|
+
* Take or extend the lease over `resource` inside this client's project.
|
|
14762
|
+
*
|
|
14763
|
+
* The returned `fencingToken` is the whole point: hold it, present it on every
|
|
14764
|
+
* renewal, and present it again before committing anything shared. A hub that
|
|
14765
|
+
* has moved past it refuses, and the caller must stop rather than retry —
|
|
14766
|
+
* another holder owns the resource now. Rejects `HubHttpError` with code
|
|
14767
|
+
* `lease_held` when a live holder has it.
|
|
14768
|
+
*/
|
|
14769
|
+
async acquireLease(resource, ttlMs) {
|
|
14770
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/acquire`, {
|
|
14771
|
+
method: "POST",
|
|
14772
|
+
body: JSON.stringify(ttlMs === void 0 ? {} : { ttlMs })
|
|
14773
|
+
});
|
|
14774
|
+
}
|
|
14775
|
+
/** Extend a lease this client holds. The token never changes on renewal; a
|
|
14776
|
+
* `lease_superseded` or `lease_expired` refusal means it is gone. */
|
|
14777
|
+
async renewLease(resource, fencingToken, ttlMs) {
|
|
14778
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/renew`, {
|
|
14779
|
+
method: "POST",
|
|
14780
|
+
body: JSON.stringify(ttlMs === void 0 ? { fencingToken } : { fencingToken, ttlMs })
|
|
14781
|
+
});
|
|
14782
|
+
}
|
|
14783
|
+
/** Give the resource back. */
|
|
14784
|
+
async releaseLease(resource, fencingToken) {
|
|
14785
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/release`, {
|
|
14786
|
+
method: "POST",
|
|
14787
|
+
body: JSON.stringify({ fencingToken })
|
|
14788
|
+
});
|
|
14789
|
+
}
|
|
14757
14790
|
async listWorkflows() {
|
|
14758
14791
|
const result2 = await this.request("/v1/workflows");
|
|
14759
14792
|
return result2.runs;
|
|
@@ -14947,6 +14980,7 @@ var HubClient = class {
|
|
|
14947
14980
|
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
|
|
14948
14981
|
if (typeof body[key] === "string") extras[key] = body[key];
|
|
14949
14982
|
}
|
|
14983
|
+
if (body.lease && typeof body.lease === "object") extras.lease = body.lease;
|
|
14950
14984
|
throw new HubHttpError(
|
|
14951
14985
|
response.status,
|
|
14952
14986
|
String(body.error ?? `HTTP ${response.status}`),
|
|
@@ -15763,6 +15763,8 @@ var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
|
15763
15763
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
15764
15764
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
15765
15765
|
var MAX_AGENT_HOST_CHARS = 64;
|
|
15766
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
15767
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
15766
15768
|
|
|
15767
15769
|
// plugins/kxm/src/workflow.ts
|
|
15768
15770
|
function canonicalWorkflowEvidenceKey(value) {
|
|
@@ -15974,6 +15976,37 @@ var HubClient = class {
|
|
|
15974
15976
|
);
|
|
15975
15977
|
return result.message;
|
|
15976
15978
|
}
|
|
15979
|
+
// ----- Fenced leases over shared resources (P3) -----
|
|
15980
|
+
/**
|
|
15981
|
+
* Take or extend the lease over `resource` inside this client's project.
|
|
15982
|
+
*
|
|
15983
|
+
* The returned `fencingToken` is the whole point: hold it, present it on every
|
|
15984
|
+
* renewal, and present it again before committing anything shared. A hub that
|
|
15985
|
+
* has moved past it refuses, and the caller must stop rather than retry —
|
|
15986
|
+
* another holder owns the resource now. Rejects `HubHttpError` with code
|
|
15987
|
+
* `lease_held` when a live holder has it.
|
|
15988
|
+
*/
|
|
15989
|
+
async acquireLease(resource, ttlMs) {
|
|
15990
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/acquire`, {
|
|
15991
|
+
method: "POST",
|
|
15992
|
+
body: JSON.stringify(ttlMs === void 0 ? {} : { ttlMs })
|
|
15993
|
+
});
|
|
15994
|
+
}
|
|
15995
|
+
/** Extend a lease this client holds. The token never changes on renewal; a
|
|
15996
|
+
* `lease_superseded` or `lease_expired` refusal means it is gone. */
|
|
15997
|
+
async renewLease(resource, fencingToken, ttlMs) {
|
|
15998
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/renew`, {
|
|
15999
|
+
method: "POST",
|
|
16000
|
+
body: JSON.stringify(ttlMs === void 0 ? { fencingToken } : { fencingToken, ttlMs })
|
|
16001
|
+
});
|
|
16002
|
+
}
|
|
16003
|
+
/** Give the resource back. */
|
|
16004
|
+
async releaseLease(resource, fencingToken) {
|
|
16005
|
+
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/release`, {
|
|
16006
|
+
method: "POST",
|
|
16007
|
+
body: JSON.stringify({ fencingToken })
|
|
16008
|
+
});
|
|
16009
|
+
}
|
|
15977
16010
|
async listWorkflows() {
|
|
15978
16011
|
const result = await this.request("/v1/workflows");
|
|
15979
16012
|
return result.runs;
|
|
@@ -16167,6 +16200,7 @@ var HubClient = class {
|
|
|
16167
16200
|
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
|
|
16168
16201
|
if (typeof body[key] === "string") extras[key] = body[key];
|
|
16169
16202
|
}
|
|
16203
|
+
if (body.lease && typeof body.lease === "object") extras.lease = body.lease;
|
|
16170
16204
|
throw new HubHttpError(
|
|
16171
16205
|
response.status,
|
|
16172
16206
|
String(body.error ?? `HTTP ${response.status}`),
|
|
@@ -17145,7 +17179,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
|
|
|
17145
17179
|
}
|
|
17146
17180
|
|
|
17147
17181
|
// plugins/kxm/src/mcp-server.ts
|
|
17148
|
-
var VERSION = "0.7.
|
|
17182
|
+
var VERSION = "0.7.72";
|
|
17149
17183
|
var inbox = /* @__PURE__ */ new Map();
|
|
17150
17184
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17151
17185
|
var meshClient;
|
|
@@ -17989,7 +17989,7 @@ function discoverProjectStores(projectRoot, options = {}) {
|
|
|
17989
17989
|
const stores = [];
|
|
17990
17990
|
const hubPath = options.hubDataPath ? resolve3(options.hubDataPath) : join4(root, ".kxm", "state", "kxm.db");
|
|
17991
17991
|
if (existsSync4(hubPath)) {
|
|
17992
|
-
stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion:
|
|
17992
|
+
stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion: 4 });
|
|
17993
17993
|
}
|
|
17994
17994
|
const registryPath = join4(root, ".kxm", "runtime", "registry.db");
|
|
17995
17995
|
if (existsSync4(registryPath)) {
|
|
@@ -18095,7 +18095,7 @@ function restoreBackup(manifestPathOrDir, options = {}) {
|
|
|
18095
18095
|
`backup file ${store.backupFile} sha256 ${actualSha256} does not match manifest hash ${store.sha256}`
|
|
18096
18096
|
);
|
|
18097
18097
|
}
|
|
18098
|
-
let maxSupported =
|
|
18098
|
+
let maxSupported = 4;
|
|
18099
18099
|
if (store.storeId === "registry" || store.storeId === "binding-store") {
|
|
18100
18100
|
maxSupported = 1;
|
|
18101
18101
|
} else if (store.storeId.startsWith("events:")) {
|
|
@@ -28684,6 +28684,8 @@ var DEFAULT_MESSAGE_TTL_MS = 24 * 60 * 6e4;
|
|
|
28684
28684
|
var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
28685
28685
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
28686
28686
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
28687
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
28688
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
28687
28689
|
function nowIso() {
|
|
28688
28690
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
28689
28691
|
}
|