@kontextmind/kxm 0.7.70 → 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 +83 -19
- package/plugins/kxm/dist/client.js +50 -3
- package/plugins/kxm/dist/core.js +31 -4
- package/plugins/kxm/dist/extension.js +59 -7
- package/plugins/kxm/dist/mcp-server.js +60 -8
- package/plugins/kxm/dist/runtime.js +4 -2
- package/plugins/kxm/dist/server.js +317 -20
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm/references/protocol.md +18 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +3 -1
- package/plugins/kxm/src/cli.ts +2 -1
- package/plugins/kxm/src/client.ts +62 -3
- package/plugins/kxm/src/commands.ts +10 -4
- package/plugins/kxm/src/database.ts +6 -2
- package/plugins/kxm/src/external-effects.ts +390 -66
- package/plugins/kxm/src/hub.ts +171 -12
- package/plugins/kxm/src/local-snapshot.ts +10 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/protocol.ts +67 -1
- package/plugins/kxm/src/store.ts +217 -6
- package/plugins/kxm/src/tui.ts +20 -4
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
|
@@ -18350,9 +18350,7 @@ function findKxmRepoRoot(fromUrl = import.meta.url) {
|
|
|
18350
18350
|
|
|
18351
18351
|
// plugins/kxm/src/client.ts
|
|
18352
18352
|
import { createHash as createHash3 } from "node:crypto";
|
|
18353
|
-
|
|
18354
|
-
// plugins/kxm/src/workflow.ts
|
|
18355
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
18353
|
+
import { hostname } from "node:os";
|
|
18356
18354
|
|
|
18357
18355
|
// plugins/kxm/src/protocol.ts
|
|
18358
18356
|
import { randomUUID } from "node:crypto";
|
|
@@ -18361,6 +18359,9 @@ var MIN_MESSAGE_TTL_MS = 1e3;
|
|
|
18361
18359
|
var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
18362
18360
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
18363
18361
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
18362
|
+
var MAX_AGENT_HOST_CHARS = 64;
|
|
18363
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
18364
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
18364
18365
|
var ProtocolError = class extends Error {
|
|
18365
18366
|
statusCode;
|
|
18366
18367
|
code;
|
|
@@ -18453,6 +18454,7 @@ function validateTerminalReceipt(value) {
|
|
|
18453
18454
|
}
|
|
18454
18455
|
|
|
18455
18456
|
// plugins/kxm/src/workflow.ts
|
|
18457
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
18456
18458
|
var PROMOTABLE_JOURNAL_CATEGORIES = ["skill-candidate", "hypothesis", "experiment"];
|
|
18457
18459
|
var WORKFLOW_TERMINAL_TARGET = "$terminal";
|
|
18458
18460
|
function normalizeOutcomeValue(value, field) {
|
|
@@ -19200,6 +19202,13 @@ var MeshWaitError = class extends Error {
|
|
|
19200
19202
|
this.waitStatus = waitStatus;
|
|
19201
19203
|
}
|
|
19202
19204
|
};
|
|
19205
|
+
function defaultHostLabel() {
|
|
19206
|
+
try {
|
|
19207
|
+
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || void 0;
|
|
19208
|
+
} catch {
|
|
19209
|
+
return void 0;
|
|
19210
|
+
}
|
|
19211
|
+
}
|
|
19203
19212
|
function completedFanoutResult(target, message) {
|
|
19204
19213
|
if (message.status === "queued" || message.status === "delivered") {
|
|
19205
19214
|
throw new Error(`message ${message.id} is not complete`);
|
|
@@ -19289,8 +19298,11 @@ var HubClient = class {
|
|
|
19289
19298
|
this.onEvent = void 0;
|
|
19290
19299
|
this.eventLoop = void 0;
|
|
19291
19300
|
}
|
|
19292
|
-
|
|
19293
|
-
|
|
19301
|
+
/** Online peers of this client's project. `includeOffline` also returns
|
|
19302
|
+
* registered members whose lease the hub has already retired. */
|
|
19303
|
+
async listAgents(options = {}) {
|
|
19304
|
+
const path4 = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
|
|
19305
|
+
const result = await this.request(path4);
|
|
19294
19306
|
return result.agents;
|
|
19295
19307
|
}
|
|
19296
19308
|
async send(options) {
|
|
@@ -19386,6 +19398,37 @@ var HubClient = class {
|
|
|
19386
19398
|
);
|
|
19387
19399
|
return result.message;
|
|
19388
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
|
+
}
|
|
19389
19432
|
async listWorkflows() {
|
|
19390
19433
|
const result = await this.request("/v1/workflows");
|
|
19391
19434
|
return result.runs;
|
|
@@ -19530,7 +19573,8 @@ var HubClient = class {
|
|
|
19530
19573
|
name: this.options.name,
|
|
19531
19574
|
purpose: this.options.purpose,
|
|
19532
19575
|
project: this.options.project,
|
|
19533
|
-
model: this.options.model
|
|
19576
|
+
model: this.options.model,
|
|
19577
|
+
host: this.options.host ?? defaultHostLabel()
|
|
19534
19578
|
})
|
|
19535
19579
|
}, false);
|
|
19536
19580
|
this.agent = registration.agent;
|
|
@@ -19578,6 +19622,7 @@ var HubClient = class {
|
|
|
19578
19622
|
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
|
|
19579
19623
|
if (typeof body[key] === "string") extras[key] = body[key];
|
|
19580
19624
|
}
|
|
19625
|
+
if (body.lease && typeof body.lease === "object") extras.lease = body.lease;
|
|
19581
19626
|
throw new HubHttpError(
|
|
19582
19627
|
response.status,
|
|
19583
19628
|
String(body.error ?? `HTTP ${response.status}`),
|
|
@@ -19770,14 +19815,19 @@ var AGENT_COMMANDS = [
|
|
|
19770
19815
|
group: "peer",
|
|
19771
19816
|
verb: "list",
|
|
19772
19817
|
label: "List hub peers",
|
|
19773
|
-
description: "List
|
|
19818
|
+
description: "List peer agents in this project's hub pool with their names, purposes, host label, and hub-clocked presence (online, stale, offline). Registered offline peers are listed only when includeOffline is set.",
|
|
19774
19819
|
parameters: {
|
|
19775
19820
|
type: "object",
|
|
19776
|
-
properties: {
|
|
19821
|
+
properties: {
|
|
19822
|
+
includeOffline: {
|
|
19823
|
+
type: "boolean",
|
|
19824
|
+
description: "Also list registered peers whose hub lease has expired"
|
|
19825
|
+
}
|
|
19826
|
+
},
|
|
19777
19827
|
additionalProperties: false
|
|
19778
19828
|
},
|
|
19779
|
-
async execute(client) {
|
|
19780
|
-
return { agents: await client.listAgents() };
|
|
19829
|
+
async execute(client, args) {
|
|
19830
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
19781
19831
|
}
|
|
19782
19832
|
},
|
|
19783
19833
|
{
|
|
@@ -23846,7 +23896,7 @@ function discoverProjectStores(projectRoot, options = {}) {
|
|
|
23846
23896
|
const stores = [];
|
|
23847
23897
|
const hubPath = options.hubDataPath ? resolve5(options.hubDataPath) : join8(root, ".kxm", "state", "kxm.db");
|
|
23848
23898
|
if (existsSync8(hubPath)) {
|
|
23849
|
-
stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion:
|
|
23899
|
+
stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion: 4 });
|
|
23850
23900
|
}
|
|
23851
23901
|
const registryPath = join8(root, ".kxm", "runtime", "registry.db");
|
|
23852
23902
|
if (existsSync8(registryPath)) {
|
|
@@ -23952,7 +24002,7 @@ function restoreBackup(manifestPathOrDir, options = {}) {
|
|
|
23952
24002
|
`backup file ${store.backupFile} sha256 ${actualSha256} does not match manifest hash ${store.sha256}`
|
|
23953
24003
|
);
|
|
23954
24004
|
}
|
|
23955
|
-
let maxSupported =
|
|
24005
|
+
let maxSupported = 4;
|
|
23956
24006
|
if (store.storeId === "registry" || store.storeId === "binding-store") {
|
|
23957
24007
|
maxSupported = 1;
|
|
23958
24008
|
} else if (store.storeId.startsWith("events:")) {
|
|
@@ -26188,8 +26238,8 @@ function printWorker(runtime, worker, payload, text, outcome) {
|
|
|
26188
26238
|
}
|
|
26189
26239
|
function hostMode(runtime) {
|
|
26190
26240
|
try {
|
|
26191
|
-
const
|
|
26192
|
-
if (
|
|
26241
|
+
const hostname2 = new URL(runtime.serverUrl).hostname;
|
|
26242
|
+
if (hostname2 === "127.0.0.1" || hostname2 === "localhost" || hostname2 === "::1") return "local";
|
|
26193
26243
|
} catch {
|
|
26194
26244
|
}
|
|
26195
26245
|
return "hub";
|
|
@@ -42587,6 +42637,12 @@ function slugifyBranchPart(text, maxLength = 40) {
|
|
|
42587
42637
|
if (cleaned.length <= maxLength) return cleaned;
|
|
42588
42638
|
return cleaned.slice(0, maxLength).replace(/-+$/, "");
|
|
42589
42639
|
}
|
|
42640
|
+
var SHARED_EFFECT_KINDS = Object.freeze([
|
|
42641
|
+
"git-push",
|
|
42642
|
+
"pr-create",
|
|
42643
|
+
"tracker-issue",
|
|
42644
|
+
"webhook"
|
|
42645
|
+
]);
|
|
42590
42646
|
|
|
42591
42647
|
// plugins/kxm/src/local-snapshot.ts
|
|
42592
42648
|
import { existsSync as existsSync26, readdirSync as readdirSync11, readFileSync as readFileSync26 } from "node:fs";
|
|
@@ -43087,6 +43143,13 @@ function meshTuiTheme(color) {
|
|
|
43087
43143
|
function visibleAgents(snapshot) {
|
|
43088
43144
|
return snapshot.agents.filter((agent) => agent.model !== "tui");
|
|
43089
43145
|
}
|
|
43146
|
+
function presenceCell(agent, theme, width = 7) {
|
|
43147
|
+
const presence = agent.presence ?? "n/a";
|
|
43148
|
+
const cell = pad(presence, width);
|
|
43149
|
+
if (presence === "online") return theme.success(cell);
|
|
43150
|
+
if (presence === "stale") return theme.warning(cell);
|
|
43151
|
+
return theme.dim(cell);
|
|
43152
|
+
}
|
|
43090
43153
|
function panelMetric(snapshot, panel) {
|
|
43091
43154
|
if (panel === "agents") {
|
|
43092
43155
|
const agents = visibleAgents(snapshot);
|
|
@@ -43137,7 +43200,7 @@ function listLines(snapshot, view, theme) {
|
|
|
43137
43200
|
const now = Date.parse(snapshot.fetchedAt);
|
|
43138
43201
|
const mark = (index) => cursor(theme, index === view.selected, "list", view.pane);
|
|
43139
43202
|
if (view.tab === "agents") {
|
|
43140
|
-
return visibleAgents(snapshot).map((agent, index) => `${mark(index)}${pad(agent.name, 14)} ${agent
|
|
43203
|
+
return visibleAgents(snapshot).map((agent, index) => `${mark(index)}${pad(agent.name, 14)} ${presenceCell(agent, theme)} ${pad(agent.host ?? "-", 12)} ${pad(agent.model ?? "-", 14)} ${pad(age(agent.lastSeenAt, now), 4)}`);
|
|
43141
43204
|
}
|
|
43142
43205
|
if (view.tab === "tasks" || view.tab === "workflows") {
|
|
43143
43206
|
const runs = view.tab === "tasks" ? snapshot.runs.filter((run) => run.status === "running" || run.status === "waiting") : snapshot.runs;
|
|
@@ -43175,9 +43238,10 @@ function detailLines(snapshot, view, theme) {
|
|
|
43175
43238
|
const related = snapshot.openMessages.filter((message) => message.fromName === agent.name || message.toName === agent.name);
|
|
43176
43239
|
return [
|
|
43177
43240
|
theme.accent(agent.name),
|
|
43178
|
-
`${agent
|
|
43241
|
+
`${presenceCell(agent, theme, (agent.presence ?? "n/a").length)} ${agent.model ?? "-"}`,
|
|
43242
|
+
`host ${agent.host ?? "-"}`,
|
|
43179
43243
|
agent.purpose,
|
|
43180
|
-
`seen ${age(agent.lastSeenAt, now)} ago`,
|
|
43244
|
+
`seen ${age(agent.lastSeenAt, now)} ago \xB7 lease ${agent.leaseExpiresAt?.slice(11, 19) ?? "n/a"} UTC`,
|
|
43181
43245
|
"",
|
|
43182
43246
|
theme.dim("Open work"),
|
|
43183
43247
|
...related.length === 0 ? [theme.dim("none")] : related.map((message) => `${message.status} ${message.fromName} \u2192 ${message.toName} ${age(message.createdAt, now)}`)
|
|
@@ -43486,7 +43550,7 @@ async function runMeshTui(input) {
|
|
|
43486
43550
|
}
|
|
43487
43551
|
if (!useOpsStream && identity) {
|
|
43488
43552
|
try {
|
|
43489
|
-
const listed = await input.fetchImpl(`${base}/v1/agents`, { headers: headers(identity) });
|
|
43553
|
+
const listed = await input.fetchImpl(`${base}/v1/agents?includeOffline=true`, { headers: headers(identity) });
|
|
43490
43554
|
if (listed.ok) {
|
|
43491
43555
|
const body = await readJson(listed);
|
|
43492
43556
|
const byId = new Map(local.agents.map((agent) => [agent.id, agent]));
|
|
@@ -47744,7 +47808,7 @@ function createProgram(ctx, result) {
|
|
|
47744
47808
|
});
|
|
47745
47809
|
const peer = addGlobalOptions(program2.command("peer").description("Peer agent messaging and coordination"));
|
|
47746
47810
|
peer.helpCommand("help", "Show peer help");
|
|
47747
|
-
addGlobalOptions(peer.command("list").description("List
|
|
47811
|
+
addGlobalOptions(peer.command("list").description("List peer agents in this project's hub pool with host and presence")).option("--include-offline", "Also list registered peers whose hub lease has expired").option("--payload <json>", "JSON payload").action(async function peerListAction(opts) {
|
|
47748
47812
|
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_list", opts ?? {});
|
|
47749
47813
|
});
|
|
47750
47814
|
addGlobalOptions(peer.command("send [target] [content]").description("Send a focused request to a peer agent")).option("--target <name>", "Peer name or agent ID").option("--content <text>", "Focused request content").option("--delivery <mode>", "steer, followUp, or nextTurn").option("--correlation-id <id>", "Task grouping ID").option("--idempotency-key <key>", "Deduplication key").option("--workflow-context <json>", "Workflow context JSON").option("--ttl-ms <ms>", "Message TTL in milliseconds").option("--allow-offline", "Queue the request if the target is registered but offline").option("--payload <json>", "JSON payload").action(async function peerSendAction(target, content, opts) {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// plugins/kxm/src/client.ts
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { hostname } from "node:os";
|
|
3
4
|
|
|
4
5
|
// plugins/kxm/src/protocol.ts
|
|
5
6
|
var DEFAULT_MESSAGE_TTL_MS = 24 * 60 * 6e4;
|
|
6
7
|
var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
7
8
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
8
9
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
10
|
+
var MAX_AGENT_HOST_CHARS = 64;
|
|
11
|
+
var MAX_LEASE_TTL_MS = 10 * 6e4;
|
|
12
|
+
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
|
|
9
13
|
|
|
10
14
|
// plugins/kxm/src/workflow.ts
|
|
11
15
|
function canonicalWorkflowEvidenceKey(value) {
|
|
@@ -21,6 +25,13 @@ var MeshWaitError = class extends Error {
|
|
|
21
25
|
this.waitStatus = waitStatus;
|
|
22
26
|
}
|
|
23
27
|
};
|
|
28
|
+
function defaultHostLabel() {
|
|
29
|
+
try {
|
|
30
|
+
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || void 0;
|
|
31
|
+
} catch {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
function completedFanoutResult(target, message) {
|
|
25
36
|
if (message.status === "queued" || message.status === "delivered") {
|
|
26
37
|
throw new Error(`message ${message.id} is not complete`);
|
|
@@ -110,8 +121,11 @@ var HubClient = class {
|
|
|
110
121
|
this.onEvent = void 0;
|
|
111
122
|
this.eventLoop = void 0;
|
|
112
123
|
}
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
/** Online peers of this client's project. `includeOffline` also returns
|
|
125
|
+
* registered members whose lease the hub has already retired. */
|
|
126
|
+
async listAgents(options = {}) {
|
|
127
|
+
const path = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
|
|
128
|
+
const result = await this.request(path);
|
|
115
129
|
return result.agents;
|
|
116
130
|
}
|
|
117
131
|
async send(options) {
|
|
@@ -207,6 +221,37 @@ var HubClient = class {
|
|
|
207
221
|
);
|
|
208
222
|
return result.message;
|
|
209
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
|
+
}
|
|
210
255
|
async listWorkflows() {
|
|
211
256
|
const result = await this.request("/v1/workflows");
|
|
212
257
|
return result.runs;
|
|
@@ -351,7 +396,8 @@ var HubClient = class {
|
|
|
351
396
|
name: this.options.name,
|
|
352
397
|
purpose: this.options.purpose,
|
|
353
398
|
project: this.options.project,
|
|
354
|
-
model: this.options.model
|
|
399
|
+
model: this.options.model,
|
|
400
|
+
host: this.options.host ?? defaultHostLabel()
|
|
355
401
|
})
|
|
356
402
|
}, false);
|
|
357
403
|
this.agent = registration.agent;
|
|
@@ -399,6 +445,7 @@ var HubClient = class {
|
|
|
399
445
|
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
|
|
400
446
|
if (typeof body[key] === "string") extras[key] = body[key];
|
|
401
447
|
}
|
|
448
|
+
if (body.lease && typeof body.lease === "object") extras.lease = body.lease;
|
|
402
449
|
throw new HubHttpError(
|
|
403
450
|
response.status,
|
|
404
451
|
String(body.error ?? `HTTP ${response.status}`),
|
package/plugins/kxm/dist/core.js
CHANGED
|
@@ -12,6 +12,21 @@ var DEFAULT_RATE_LIMIT_MAX = 600;
|
|
|
12
12
|
var DEFAULT_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
13
13
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
14
14
|
var MAX_CONTENT_CHARS = 32e3;
|
|
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;
|
|
20
|
+
function agentPresenceView(agent, staleAfterMs = DEFAULT_STALE_AFTER_MS, now = Date.now()) {
|
|
21
|
+
const lastSeenMs = Date.parse(agent.lastSeenAt);
|
|
22
|
+
const leaseExpiresAtMs = (Number.isFinite(lastSeenMs) ? lastSeenMs : 0) + staleAfterMs;
|
|
23
|
+
const leaseExpiresAt = new Date(leaseExpiresAtMs).toISOString();
|
|
24
|
+
if (!agent.online) return { leaseExpiresAt, presence: "offline" };
|
|
25
|
+
return { leaseExpiresAt, presence: now < leaseExpiresAtMs ? "online" : "stale" };
|
|
26
|
+
}
|
|
27
|
+
function toAgentRecord(agent, staleAfterMs, now) {
|
|
28
|
+
return { ...agent, ...agentPresenceView(agent, staleAfterMs, now) };
|
|
29
|
+
}
|
|
15
30
|
var ProtocolError = class extends Error {
|
|
16
31
|
statusCode;
|
|
17
32
|
code;
|
|
@@ -945,14 +960,19 @@ var AGENT_COMMANDS = [
|
|
|
945
960
|
group: "peer",
|
|
946
961
|
verb: "list",
|
|
947
962
|
label: "List hub peers",
|
|
948
|
-
description: "List
|
|
963
|
+
description: "List peer agents in this project's hub pool with their names, purposes, host label, and hub-clocked presence (online, stale, offline). Registered offline peers are listed only when includeOffline is set.",
|
|
949
964
|
parameters: {
|
|
950
965
|
type: "object",
|
|
951
|
-
properties: {
|
|
966
|
+
properties: {
|
|
967
|
+
includeOffline: {
|
|
968
|
+
type: "boolean",
|
|
969
|
+
description: "Also list registered peers whose hub lease has expired"
|
|
970
|
+
}
|
|
971
|
+
},
|
|
952
972
|
additionalProperties: false
|
|
953
973
|
},
|
|
954
|
-
async execute(client) {
|
|
955
|
-
return { agents: await client.listAgents() };
|
|
974
|
+
async execute(client, args) {
|
|
975
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
956
976
|
}
|
|
957
977
|
},
|
|
958
978
|
{
|
|
@@ -2001,6 +2021,7 @@ export {
|
|
|
2001
2021
|
AGENT_COMMANDS,
|
|
2002
2022
|
AGENT_COMMANDS_MAP,
|
|
2003
2023
|
BEHAVIORAL_HASH_VERSION,
|
|
2024
|
+
DEFAULT_LEASE_TTL_MS,
|
|
2004
2025
|
DEFAULT_LOG_MAX_BYTES,
|
|
2005
2026
|
DEFAULT_LOG_MAX_FILES,
|
|
2006
2027
|
DEFAULT_MAX_HOPS,
|
|
@@ -2011,12 +2032,16 @@ export {
|
|
|
2011
2032
|
DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
2012
2033
|
DEFAULT_STALE_AFTER_MS,
|
|
2013
2034
|
LOG_LEVEL_PRIORITY,
|
|
2035
|
+
MAX_AGENT_HOST_CHARS,
|
|
2014
2036
|
MAX_BODY_BYTES,
|
|
2015
2037
|
MAX_CONTENT_CHARS,
|
|
2016
2038
|
MAX_CONTEXT_ITEM_IDS,
|
|
2039
|
+
MAX_LEASE_RESOURCE_CHARS,
|
|
2040
|
+
MAX_LEASE_TTL_MS,
|
|
2017
2041
|
MAX_MESSAGE_TTL_MS,
|
|
2018
2042
|
MAX_PROVIDER_METADATA_FIELDS,
|
|
2019
2043
|
MAX_SKILL_REFS,
|
|
2044
|
+
MIN_LEASE_TTL_MS,
|
|
2020
2045
|
MIN_MESSAGE_RETENTION_MS,
|
|
2021
2046
|
MIN_MESSAGE_TTL_MS,
|
|
2022
2047
|
ProtocolError,
|
|
@@ -2026,6 +2051,7 @@ export {
|
|
|
2026
2051
|
TERMINAL_RECEIPT_SCHEMA,
|
|
2027
2052
|
WORKER_RESULT_SCHEMA,
|
|
2028
2053
|
WORKER_SCHEMA,
|
|
2054
|
+
agentPresenceView,
|
|
2029
2055
|
agentWorker,
|
|
2030
2056
|
behavioralConfigHash,
|
|
2031
2057
|
clearSessionTokenFromDisk,
|
|
@@ -2066,6 +2092,7 @@ export {
|
|
|
2066
2092
|
rotateLogFiles,
|
|
2067
2093
|
sessionTokenPath,
|
|
2068
2094
|
timingSafeStringCompare,
|
|
2095
|
+
toAgentRecord,
|
|
2069
2096
|
validateTerminalReceipt,
|
|
2070
2097
|
workerResult
|
|
2071
2098
|
};
|