@kontextmind/kxm 0.7.70 → 0.7.71
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/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +41 -17
- package/plugins/kxm/dist/client.js +16 -3
- package/plugins/kxm/dist/core.js +23 -4
- package/plugins/kxm/dist/extension.js +25 -7
- package/plugins/kxm/dist/mcp-server.js +26 -8
- package/plugins/kxm/dist/server.js +42 -14
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-peer/SKILL.md +3 -1
- package/plugins/kxm/src/cli.ts +2 -1
- package/plugins/kxm/src/client.ts +23 -2
- package/plugins/kxm/src/commands.ts +10 -4
- package/plugins/kxm/src/hub.ts +34 -11
- package/plugins/kxm/src/local-snapshot.ts +10 -2
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/protocol.ts +42 -1
- package/plugins/kxm/src/store.ts +5 -2
- package/plugins/kxm/src/tui.ts +20 -4
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.71",
|
|
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,7 @@ 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;
|
|
18364
18363
|
var ProtocolError = class extends Error {
|
|
18365
18364
|
statusCode;
|
|
18366
18365
|
code;
|
|
@@ -18453,6 +18452,7 @@ function validateTerminalReceipt(value) {
|
|
|
18453
18452
|
}
|
|
18454
18453
|
|
|
18455
18454
|
// plugins/kxm/src/workflow.ts
|
|
18455
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
18456
18456
|
var PROMOTABLE_JOURNAL_CATEGORIES = ["skill-candidate", "hypothesis", "experiment"];
|
|
18457
18457
|
var WORKFLOW_TERMINAL_TARGET = "$terminal";
|
|
18458
18458
|
function normalizeOutcomeValue(value, field) {
|
|
@@ -19200,6 +19200,13 @@ var MeshWaitError = class extends Error {
|
|
|
19200
19200
|
this.waitStatus = waitStatus;
|
|
19201
19201
|
}
|
|
19202
19202
|
};
|
|
19203
|
+
function defaultHostLabel() {
|
|
19204
|
+
try {
|
|
19205
|
+
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || void 0;
|
|
19206
|
+
} catch {
|
|
19207
|
+
return void 0;
|
|
19208
|
+
}
|
|
19209
|
+
}
|
|
19203
19210
|
function completedFanoutResult(target, message) {
|
|
19204
19211
|
if (message.status === "queued" || message.status === "delivered") {
|
|
19205
19212
|
throw new Error(`message ${message.id} is not complete`);
|
|
@@ -19289,8 +19296,11 @@ var HubClient = class {
|
|
|
19289
19296
|
this.onEvent = void 0;
|
|
19290
19297
|
this.eventLoop = void 0;
|
|
19291
19298
|
}
|
|
19292
|
-
|
|
19293
|
-
|
|
19299
|
+
/** Online peers of this client's project. `includeOffline` also returns
|
|
19300
|
+
* registered members whose lease the hub has already retired. */
|
|
19301
|
+
async listAgents(options = {}) {
|
|
19302
|
+
const path4 = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
|
|
19303
|
+
const result = await this.request(path4);
|
|
19294
19304
|
return result.agents;
|
|
19295
19305
|
}
|
|
19296
19306
|
async send(options) {
|
|
@@ -19530,7 +19540,8 @@ var HubClient = class {
|
|
|
19530
19540
|
name: this.options.name,
|
|
19531
19541
|
purpose: this.options.purpose,
|
|
19532
19542
|
project: this.options.project,
|
|
19533
|
-
model: this.options.model
|
|
19543
|
+
model: this.options.model,
|
|
19544
|
+
host: this.options.host ?? defaultHostLabel()
|
|
19534
19545
|
})
|
|
19535
19546
|
}, false);
|
|
19536
19547
|
this.agent = registration.agent;
|
|
@@ -19770,14 +19781,19 @@ var AGENT_COMMANDS = [
|
|
|
19770
19781
|
group: "peer",
|
|
19771
19782
|
verb: "list",
|
|
19772
19783
|
label: "List hub peers",
|
|
19773
|
-
description: "List
|
|
19784
|
+
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
19785
|
parameters: {
|
|
19775
19786
|
type: "object",
|
|
19776
|
-
properties: {
|
|
19787
|
+
properties: {
|
|
19788
|
+
includeOffline: {
|
|
19789
|
+
type: "boolean",
|
|
19790
|
+
description: "Also list registered peers whose hub lease has expired"
|
|
19791
|
+
}
|
|
19792
|
+
},
|
|
19777
19793
|
additionalProperties: false
|
|
19778
19794
|
},
|
|
19779
|
-
async execute(client) {
|
|
19780
|
-
return { agents: await client.listAgents() };
|
|
19795
|
+
async execute(client, args) {
|
|
19796
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
19781
19797
|
}
|
|
19782
19798
|
},
|
|
19783
19799
|
{
|
|
@@ -26188,8 +26204,8 @@ function printWorker(runtime, worker, payload, text, outcome) {
|
|
|
26188
26204
|
}
|
|
26189
26205
|
function hostMode(runtime) {
|
|
26190
26206
|
try {
|
|
26191
|
-
const
|
|
26192
|
-
if (
|
|
26207
|
+
const hostname2 = new URL(runtime.serverUrl).hostname;
|
|
26208
|
+
if (hostname2 === "127.0.0.1" || hostname2 === "localhost" || hostname2 === "::1") return "local";
|
|
26193
26209
|
} catch {
|
|
26194
26210
|
}
|
|
26195
26211
|
return "hub";
|
|
@@ -43087,6 +43103,13 @@ function meshTuiTheme(color) {
|
|
|
43087
43103
|
function visibleAgents(snapshot) {
|
|
43088
43104
|
return snapshot.agents.filter((agent) => agent.model !== "tui");
|
|
43089
43105
|
}
|
|
43106
|
+
function presenceCell(agent, theme, width = 7) {
|
|
43107
|
+
const presence = agent.presence ?? "n/a";
|
|
43108
|
+
const cell = pad(presence, width);
|
|
43109
|
+
if (presence === "online") return theme.success(cell);
|
|
43110
|
+
if (presence === "stale") return theme.warning(cell);
|
|
43111
|
+
return theme.dim(cell);
|
|
43112
|
+
}
|
|
43090
43113
|
function panelMetric(snapshot, panel) {
|
|
43091
43114
|
if (panel === "agents") {
|
|
43092
43115
|
const agents = visibleAgents(snapshot);
|
|
@@ -43137,7 +43160,7 @@ function listLines(snapshot, view, theme) {
|
|
|
43137
43160
|
const now = Date.parse(snapshot.fetchedAt);
|
|
43138
43161
|
const mark = (index) => cursor(theme, index === view.selected, "list", view.pane);
|
|
43139
43162
|
if (view.tab === "agents") {
|
|
43140
|
-
return visibleAgents(snapshot).map((agent, index) => `${mark(index)}${pad(agent.name, 14)} ${agent
|
|
43163
|
+
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
43164
|
}
|
|
43142
43165
|
if (view.tab === "tasks" || view.tab === "workflows") {
|
|
43143
43166
|
const runs = view.tab === "tasks" ? snapshot.runs.filter((run) => run.status === "running" || run.status === "waiting") : snapshot.runs;
|
|
@@ -43175,9 +43198,10 @@ function detailLines(snapshot, view, theme) {
|
|
|
43175
43198
|
const related = snapshot.openMessages.filter((message) => message.fromName === agent.name || message.toName === agent.name);
|
|
43176
43199
|
return [
|
|
43177
43200
|
theme.accent(agent.name),
|
|
43178
|
-
`${agent
|
|
43201
|
+
`${presenceCell(agent, theme, (agent.presence ?? "n/a").length)} ${agent.model ?? "-"}`,
|
|
43202
|
+
`host ${agent.host ?? "-"}`,
|
|
43179
43203
|
agent.purpose,
|
|
43180
|
-
`seen ${age(agent.lastSeenAt, now)} ago`,
|
|
43204
|
+
`seen ${age(agent.lastSeenAt, now)} ago \xB7 lease ${agent.leaseExpiresAt?.slice(11, 19) ?? "n/a"} UTC`,
|
|
43181
43205
|
"",
|
|
43182
43206
|
theme.dim("Open work"),
|
|
43183
43207
|
...related.length === 0 ? [theme.dim("none")] : related.map((message) => `${message.status} ${message.fromName} \u2192 ${message.toName} ${age(message.createdAt, now)}`)
|
|
@@ -43486,7 +43510,7 @@ async function runMeshTui(input) {
|
|
|
43486
43510
|
}
|
|
43487
43511
|
if (!useOpsStream && identity) {
|
|
43488
43512
|
try {
|
|
43489
|
-
const listed = await input.fetchImpl(`${base}/v1/agents`, { headers: headers(identity) });
|
|
43513
|
+
const listed = await input.fetchImpl(`${base}/v1/agents?includeOffline=true`, { headers: headers(identity) });
|
|
43490
43514
|
if (listed.ok) {
|
|
43491
43515
|
const body = await readJson(listed);
|
|
43492
43516
|
const byId = new Map(local.agents.map((agent) => [agent.id, agent]));
|
|
@@ -47744,7 +47768,7 @@ function createProgram(ctx, result) {
|
|
|
47744
47768
|
});
|
|
47745
47769
|
const peer = addGlobalOptions(program2.command("peer").description("Peer agent messaging and coordination"));
|
|
47746
47770
|
peer.helpCommand("help", "Show peer help");
|
|
47747
|
-
addGlobalOptions(peer.command("list").description("List
|
|
47771
|
+
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
47772
|
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_list", opts ?? {});
|
|
47749
47773
|
});
|
|
47750
47774
|
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,13 @@
|
|
|
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;
|
|
9
11
|
|
|
10
12
|
// plugins/kxm/src/workflow.ts
|
|
11
13
|
function canonicalWorkflowEvidenceKey(value) {
|
|
@@ -21,6 +23,13 @@ var MeshWaitError = class extends Error {
|
|
|
21
23
|
this.waitStatus = waitStatus;
|
|
22
24
|
}
|
|
23
25
|
};
|
|
26
|
+
function defaultHostLabel() {
|
|
27
|
+
try {
|
|
28
|
+
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || void 0;
|
|
29
|
+
} catch {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
24
33
|
function completedFanoutResult(target, message) {
|
|
25
34
|
if (message.status === "queued" || message.status === "delivered") {
|
|
26
35
|
throw new Error(`message ${message.id} is not complete`);
|
|
@@ -110,8 +119,11 @@ var HubClient = class {
|
|
|
110
119
|
this.onEvent = void 0;
|
|
111
120
|
this.eventLoop = void 0;
|
|
112
121
|
}
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
/** Online peers of this client's project. `includeOffline` also returns
|
|
123
|
+
* registered members whose lease the hub has already retired. */
|
|
124
|
+
async listAgents(options = {}) {
|
|
125
|
+
const path = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
|
|
126
|
+
const result = await this.request(path);
|
|
115
127
|
return result.agents;
|
|
116
128
|
}
|
|
117
129
|
async send(options) {
|
|
@@ -351,7 +363,8 @@ var HubClient = class {
|
|
|
351
363
|
name: this.options.name,
|
|
352
364
|
purpose: this.options.purpose,
|
|
353
365
|
project: this.options.project,
|
|
354
|
-
model: this.options.model
|
|
366
|
+
model: this.options.model,
|
|
367
|
+
host: this.options.host ?? defaultHostLabel()
|
|
355
368
|
})
|
|
356
369
|
}, false);
|
|
357
370
|
this.agent = registration.agent;
|
package/plugins/kxm/dist/core.js
CHANGED
|
@@ -12,6 +12,17 @@ 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
|
+
function agentPresenceView(agent, staleAfterMs = DEFAULT_STALE_AFTER_MS, now = Date.now()) {
|
|
17
|
+
const lastSeenMs = Date.parse(agent.lastSeenAt);
|
|
18
|
+
const leaseExpiresAtMs = (Number.isFinite(lastSeenMs) ? lastSeenMs : 0) + staleAfterMs;
|
|
19
|
+
const leaseExpiresAt = new Date(leaseExpiresAtMs).toISOString();
|
|
20
|
+
if (!agent.online) return { leaseExpiresAt, presence: "offline" };
|
|
21
|
+
return { leaseExpiresAt, presence: now < leaseExpiresAtMs ? "online" : "stale" };
|
|
22
|
+
}
|
|
23
|
+
function toAgentRecord(agent, staleAfterMs, now) {
|
|
24
|
+
return { ...agent, ...agentPresenceView(agent, staleAfterMs, now) };
|
|
25
|
+
}
|
|
15
26
|
var ProtocolError = class extends Error {
|
|
16
27
|
statusCode;
|
|
17
28
|
code;
|
|
@@ -945,14 +956,19 @@ var AGENT_COMMANDS = [
|
|
|
945
956
|
group: "peer",
|
|
946
957
|
verb: "list",
|
|
947
958
|
label: "List hub peers",
|
|
948
|
-
description: "List
|
|
959
|
+
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
960
|
parameters: {
|
|
950
961
|
type: "object",
|
|
951
|
-
properties: {
|
|
962
|
+
properties: {
|
|
963
|
+
includeOffline: {
|
|
964
|
+
type: "boolean",
|
|
965
|
+
description: "Also list registered peers whose hub lease has expired"
|
|
966
|
+
}
|
|
967
|
+
},
|
|
952
968
|
additionalProperties: false
|
|
953
969
|
},
|
|
954
|
-
async execute(client) {
|
|
955
|
-
return { agents: await client.listAgents() };
|
|
970
|
+
async execute(client, args) {
|
|
971
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
956
972
|
}
|
|
957
973
|
},
|
|
958
974
|
{
|
|
@@ -2011,6 +2027,7 @@ export {
|
|
|
2011
2027
|
DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
2012
2028
|
DEFAULT_STALE_AFTER_MS,
|
|
2013
2029
|
LOG_LEVEL_PRIORITY,
|
|
2030
|
+
MAX_AGENT_HOST_CHARS,
|
|
2014
2031
|
MAX_BODY_BYTES,
|
|
2015
2032
|
MAX_CONTENT_CHARS,
|
|
2016
2033
|
MAX_CONTEXT_ITEM_IDS,
|
|
@@ -2026,6 +2043,7 @@ export {
|
|
|
2026
2043
|
TERMINAL_RECEIPT_SCHEMA,
|
|
2027
2044
|
WORKER_RESULT_SCHEMA,
|
|
2028
2045
|
WORKER_SCHEMA,
|
|
2046
|
+
agentPresenceView,
|
|
2029
2047
|
agentWorker,
|
|
2030
2048
|
behavioralConfigHash,
|
|
2031
2049
|
clearSessionTokenFromDisk,
|
|
@@ -2066,6 +2084,7 @@ export {
|
|
|
2066
2084
|
rotateLogFiles,
|
|
2067
2085
|
sessionTokenPath,
|
|
2068
2086
|
timingSafeStringCompare,
|
|
2087
|
+
toAgentRecord,
|
|
2069
2088
|
validateTerminalReceipt,
|
|
2070
2089
|
workerResult
|
|
2071
2090
|
};
|
|
@@ -14534,6 +14534,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
14534
14534
|
|
|
14535
14535
|
// plugins/kxm/src/client.ts
|
|
14536
14536
|
import { createHash } from "node:crypto";
|
|
14537
|
+
import { hostname } from "node:os";
|
|
14537
14538
|
|
|
14538
14539
|
// plugins/kxm/src/protocol.ts
|
|
14539
14540
|
var DEFAULT_MESSAGE_TTL_MS = 24 * 60 * 6e4;
|
|
@@ -14541,6 +14542,7 @@ var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
|
14541
14542
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
14542
14543
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
14543
14544
|
var MAX_CONTENT_CHARS = 32e3;
|
|
14545
|
+
var MAX_AGENT_HOST_CHARS = 64;
|
|
14544
14546
|
|
|
14545
14547
|
// plugins/kxm/src/workflow.ts
|
|
14546
14548
|
function canonicalWorkflowEvidenceKey(value) {
|
|
@@ -14556,6 +14558,13 @@ var MeshWaitError = class extends Error {
|
|
|
14556
14558
|
this.waitStatus = waitStatus;
|
|
14557
14559
|
}
|
|
14558
14560
|
};
|
|
14561
|
+
function defaultHostLabel() {
|
|
14562
|
+
try {
|
|
14563
|
+
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || void 0;
|
|
14564
|
+
} catch {
|
|
14565
|
+
return void 0;
|
|
14566
|
+
}
|
|
14567
|
+
}
|
|
14559
14568
|
function completedFanoutResult(target, message) {
|
|
14560
14569
|
if (message.status === "queued" || message.status === "delivered") {
|
|
14561
14570
|
throw new Error(`message ${message.id} is not complete`);
|
|
@@ -14645,8 +14654,11 @@ var HubClient = class {
|
|
|
14645
14654
|
this.onEvent = void 0;
|
|
14646
14655
|
this.eventLoop = void 0;
|
|
14647
14656
|
}
|
|
14648
|
-
|
|
14649
|
-
|
|
14657
|
+
/** Online peers of this client's project. `includeOffline` also returns
|
|
14658
|
+
* registered members whose lease the hub has already retired. */
|
|
14659
|
+
async listAgents(options = {}) {
|
|
14660
|
+
const path = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
|
|
14661
|
+
const result2 = await this.request(path);
|
|
14650
14662
|
return result2.agents;
|
|
14651
14663
|
}
|
|
14652
14664
|
async send(options) {
|
|
@@ -14886,7 +14898,8 @@ var HubClient = class {
|
|
|
14886
14898
|
name: this.options.name,
|
|
14887
14899
|
purpose: this.options.purpose,
|
|
14888
14900
|
project: this.options.project,
|
|
14889
|
-
model: this.options.model
|
|
14901
|
+
model: this.options.model,
|
|
14902
|
+
host: this.options.host ?? defaultHostLabel()
|
|
14890
14903
|
})
|
|
14891
14904
|
}, false);
|
|
14892
14905
|
this.agent = registration.agent;
|
|
@@ -15014,14 +15027,19 @@ var AGENT_COMMANDS = [
|
|
|
15014
15027
|
group: "peer",
|
|
15015
15028
|
verb: "list",
|
|
15016
15029
|
label: "List hub peers",
|
|
15017
|
-
description: "List
|
|
15030
|
+
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.",
|
|
15018
15031
|
parameters: {
|
|
15019
15032
|
type: "object",
|
|
15020
|
-
properties: {
|
|
15033
|
+
properties: {
|
|
15034
|
+
includeOffline: {
|
|
15035
|
+
type: "boolean",
|
|
15036
|
+
description: "Also list registered peers whose hub lease has expired"
|
|
15037
|
+
}
|
|
15038
|
+
},
|
|
15021
15039
|
additionalProperties: false
|
|
15022
15040
|
},
|
|
15023
|
-
async execute(client) {
|
|
15024
|
-
return { agents: await client.listAgents() };
|
|
15041
|
+
async execute(client, args) {
|
|
15042
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
15025
15043
|
}
|
|
15026
15044
|
},
|
|
15027
15045
|
{
|
|
@@ -15755,12 +15755,14 @@ var StdioServerTransport = class {
|
|
|
15755
15755
|
|
|
15756
15756
|
// plugins/kxm/src/client.ts
|
|
15757
15757
|
import { createHash } from "node:crypto";
|
|
15758
|
+
import { hostname } from "node:os";
|
|
15758
15759
|
|
|
15759
15760
|
// plugins/kxm/src/protocol.ts
|
|
15760
15761
|
var DEFAULT_MESSAGE_TTL_MS = 24 * 60 * 6e4;
|
|
15761
15762
|
var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
15762
15763
|
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
|
|
15763
15764
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
15765
|
+
var MAX_AGENT_HOST_CHARS = 64;
|
|
15764
15766
|
|
|
15765
15767
|
// plugins/kxm/src/workflow.ts
|
|
15766
15768
|
function canonicalWorkflowEvidenceKey(value) {
|
|
@@ -15776,6 +15778,13 @@ var MeshWaitError = class extends Error {
|
|
|
15776
15778
|
this.waitStatus = waitStatus;
|
|
15777
15779
|
}
|
|
15778
15780
|
};
|
|
15781
|
+
function defaultHostLabel() {
|
|
15782
|
+
try {
|
|
15783
|
+
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || void 0;
|
|
15784
|
+
} catch {
|
|
15785
|
+
return void 0;
|
|
15786
|
+
}
|
|
15787
|
+
}
|
|
15779
15788
|
function completedFanoutResult(target, message) {
|
|
15780
15789
|
if (message.status === "queued" || message.status === "delivered") {
|
|
15781
15790
|
throw new Error(`message ${message.id} is not complete`);
|
|
@@ -15865,8 +15874,11 @@ var HubClient = class {
|
|
|
15865
15874
|
this.onEvent = void 0;
|
|
15866
15875
|
this.eventLoop = void 0;
|
|
15867
15876
|
}
|
|
15868
|
-
|
|
15869
|
-
|
|
15877
|
+
/** Online peers of this client's project. `includeOffline` also returns
|
|
15878
|
+
* registered members whose lease the hub has already retired. */
|
|
15879
|
+
async listAgents(options = {}) {
|
|
15880
|
+
const path = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
|
|
15881
|
+
const result = await this.request(path);
|
|
15870
15882
|
return result.agents;
|
|
15871
15883
|
}
|
|
15872
15884
|
async send(options) {
|
|
@@ -16106,7 +16118,8 @@ var HubClient = class {
|
|
|
16106
16118
|
name: this.options.name,
|
|
16107
16119
|
purpose: this.options.purpose,
|
|
16108
16120
|
project: this.options.project,
|
|
16109
|
-
model: this.options.model
|
|
16121
|
+
model: this.options.model,
|
|
16122
|
+
host: this.options.host ?? defaultHostLabel()
|
|
16110
16123
|
})
|
|
16111
16124
|
}, false);
|
|
16112
16125
|
this.agent = registration.agent;
|
|
@@ -16325,14 +16338,19 @@ var AGENT_COMMANDS = [
|
|
|
16325
16338
|
group: "peer",
|
|
16326
16339
|
verb: "list",
|
|
16327
16340
|
label: "List hub peers",
|
|
16328
|
-
description: "List
|
|
16341
|
+
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.",
|
|
16329
16342
|
parameters: {
|
|
16330
16343
|
type: "object",
|
|
16331
|
-
properties: {
|
|
16344
|
+
properties: {
|
|
16345
|
+
includeOffline: {
|
|
16346
|
+
type: "boolean",
|
|
16347
|
+
description: "Also list registered peers whose hub lease has expired"
|
|
16348
|
+
}
|
|
16349
|
+
},
|
|
16332
16350
|
additionalProperties: false
|
|
16333
16351
|
},
|
|
16334
|
-
async execute(client) {
|
|
16335
|
-
return { agents: await client.listAgents() };
|
|
16352
|
+
async execute(client, args) {
|
|
16353
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
16336
16354
|
}
|
|
16337
16355
|
},
|
|
16338
16356
|
{
|
|
@@ -17127,7 +17145,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
|
|
|
17127
17145
|
}
|
|
17128
17146
|
|
|
17129
17147
|
// plugins/kxm/src/mcp-server.ts
|
|
17130
|
-
var VERSION = "0.7.
|
|
17148
|
+
var VERSION = "0.7.71";
|
|
17131
17149
|
var inbox = /* @__PURE__ */ new Map();
|
|
17132
17150
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17133
17151
|
var meshClient;
|
|
@@ -7387,6 +7387,17 @@ var DEFAULT_RATE_LIMIT_MAX = 600;
|
|
|
7387
7387
|
var DEFAULT_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
7388
7388
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
7389
7389
|
var MAX_CONTENT_CHARS = 32e3;
|
|
7390
|
+
var MAX_AGENT_HOST_CHARS = 64;
|
|
7391
|
+
function agentPresenceView(agent, staleAfterMs = DEFAULT_STALE_AFTER_MS, now = Date.now()) {
|
|
7392
|
+
const lastSeenMs = Date.parse(agent.lastSeenAt);
|
|
7393
|
+
const leaseExpiresAtMs = (Number.isFinite(lastSeenMs) ? lastSeenMs : 0) + staleAfterMs;
|
|
7394
|
+
const leaseExpiresAt = new Date(leaseExpiresAtMs).toISOString();
|
|
7395
|
+
if (!agent.online) return { leaseExpiresAt, presence: "offline" };
|
|
7396
|
+
return { leaseExpiresAt, presence: now < leaseExpiresAtMs ? "online" : "stale" };
|
|
7397
|
+
}
|
|
7398
|
+
function toAgentRecord(agent, staleAfterMs, now) {
|
|
7399
|
+
return { ...agent, ...agentPresenceView(agent, staleAfterMs, now) };
|
|
7400
|
+
}
|
|
7390
7401
|
var ProtocolError = class extends Error {
|
|
7391
7402
|
statusCode;
|
|
7392
7403
|
code;
|
|
@@ -8575,14 +8586,19 @@ var AGENT_COMMANDS = [
|
|
|
8575
8586
|
group: "peer",
|
|
8576
8587
|
verb: "list",
|
|
8577
8588
|
label: "List hub peers",
|
|
8578
|
-
description: "List
|
|
8589
|
+
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.",
|
|
8579
8590
|
parameters: {
|
|
8580
8591
|
type: "object",
|
|
8581
|
-
properties: {
|
|
8592
|
+
properties: {
|
|
8593
|
+
includeOffline: {
|
|
8594
|
+
type: "boolean",
|
|
8595
|
+
description: "Also list registered peers whose hub lease has expired"
|
|
8596
|
+
}
|
|
8597
|
+
},
|
|
8582
8598
|
additionalProperties: false
|
|
8583
8599
|
},
|
|
8584
|
-
async execute(client) {
|
|
8585
|
-
return { agents: await client.listAgents() };
|
|
8600
|
+
async execute(client, args) {
|
|
8601
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
8586
8602
|
}
|
|
8587
8603
|
},
|
|
8588
8604
|
{
|
|
@@ -12138,9 +12154,9 @@ function isLoopback(host2) {
|
|
|
12138
12154
|
if (host2 === "localhost" || host2 === "::1") return true;
|
|
12139
12155
|
return isIP(host2) === 4 && host2.startsWith("127.");
|
|
12140
12156
|
}
|
|
12141
|
-
function publicAgent(agent) {
|
|
12142
|
-
const { key: _key, ...
|
|
12143
|
-
return
|
|
12157
|
+
function publicAgent(agent, staleAfterMs, now = Date.now()) {
|
|
12158
|
+
const { key: _key, ...identity } = agent;
|
|
12159
|
+
return toAgentRecord(identity, staleAfterMs, now);
|
|
12144
12160
|
}
|
|
12145
12161
|
function safeTokenEqual(actual, expected) {
|
|
12146
12162
|
return timingSafeStringCompare(actual, expected);
|
|
@@ -12560,7 +12576,8 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
|
|
|
12560
12576
|
}
|
|
12561
12577
|
}
|
|
12562
12578
|
function opsSnapshot(project) {
|
|
12563
|
-
const
|
|
12579
|
+
const snapshotAt = Date.now();
|
|
12580
|
+
const projectAgents = [...agents.values()].filter((agent) => agent.project === project).map((agent) => publicAgent(agent, staleAfterMs, snapshotAt)).sort((left, right) => Number(right.online) - Number(left.online) || left.name.localeCompare(right.name));
|
|
12564
12581
|
const open = store.getOpenMessages(project);
|
|
12565
12582
|
const projectRuns = [...workflowRuns.values()].filter((run) => run.project === project).sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
12566
12583
|
return {
|
|
@@ -12610,7 +12627,7 @@ data: ${JSON.stringify({ type: "ops", project, topic, at: nowIso() })}
|
|
|
12610
12627
|
function broadcastPresence(agent) {
|
|
12611
12628
|
for (const candidate of agents.values()) {
|
|
12612
12629
|
if (candidate.project === agent.project && candidate.id !== agent.id && candidate.online) {
|
|
12613
|
-
publish(candidate.id, { type: "presence", agent: publicAgent(agent) });
|
|
12630
|
+
publish(candidate.id, { type: "presence", agent: publicAgent(agent, staleAfterMs) });
|
|
12614
12631
|
}
|
|
12615
12632
|
}
|
|
12616
12633
|
publishOps(agent.project, "agents");
|
|
@@ -13912,6 +13929,7 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
|
|
|
13912
13929
|
const project = requireString(body.project, "project", { max: 128 });
|
|
13913
13930
|
requireProjectAuth(request, project);
|
|
13914
13931
|
const model = optionalString(body.model, "model", 128);
|
|
13932
|
+
const hostLabel = optionalString(body.host, "host", MAX_AGENT_HOST_CHARS);
|
|
13915
13933
|
const existing = [...agents.values()].find(
|
|
13916
13934
|
(agent2) => agent2.project === project && agent2.name.toLowerCase() === name.toLowerCase()
|
|
13917
13935
|
);
|
|
@@ -13938,17 +13956,27 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
|
|
|
13938
13956
|
agent.online = true;
|
|
13939
13957
|
if (model) agent.model = model;
|
|
13940
13958
|
else delete agent.model;
|
|
13959
|
+
if (hostLabel) agent.host = hostLabel;
|
|
13960
|
+
else delete agent.host;
|
|
13941
13961
|
store.saveAgent(agent);
|
|
13942
13962
|
counters.registrations += 1;
|
|
13943
|
-
logger({
|
|
13963
|
+
logger({
|
|
13964
|
+
event: existing ? "agent_resumed" : "agent_registered",
|
|
13965
|
+
agentId: agent.id,
|
|
13966
|
+
name,
|
|
13967
|
+
project,
|
|
13968
|
+
...hostLabel ? { host: hostLabel } : {}
|
|
13969
|
+
});
|
|
13944
13970
|
broadcastPresence(agent);
|
|
13945
|
-
json(response, existing ? 200 : 201, { agent: publicAgent(agent), agentKey: agent.key, resumed: Boolean(existing) });
|
|
13971
|
+
json(response, existing ? 200 : 201, { agent: publicAgent(agent, staleAfterMs), agentKey: agent.key, resumed: Boolean(existing) });
|
|
13946
13972
|
return;
|
|
13947
13973
|
}
|
|
13948
13974
|
if (method === "GET" && url.pathname === "/v1/agents") {
|
|
13949
13975
|
const current = requireAgent(request);
|
|
13950
13976
|
requireProjectAuth(request, current.project);
|
|
13951
|
-
const
|
|
13977
|
+
const includeOffline = url.searchParams.get("includeOffline") === "true";
|
|
13978
|
+
const listedAt = Date.now();
|
|
13979
|
+
const result = [...agents.values()].filter((agent) => agent.project === current.project && (agent.online || includeOffline)).map((agent) => publicAgent(agent, staleAfterMs, listedAt));
|
|
13952
13980
|
json(response, 200, { agents: result });
|
|
13953
13981
|
return;
|
|
13954
13982
|
}
|
|
@@ -13957,7 +13985,7 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
|
|
|
13957
13985
|
const current = requireAgent(request, decodeURIComponent(heartbeatMatch[1]));
|
|
13958
13986
|
requireProjectAuth(request, current.project);
|
|
13959
13987
|
await readJson(request);
|
|
13960
|
-
json(response, 200, { agent: publicAgent(current) });
|
|
13988
|
+
json(response, 200, { agent: publicAgent(current, staleAfterMs) });
|
|
13961
13989
|
return;
|
|
13962
13990
|
}
|
|
13963
13991
|
const agentMatch = url.pathname.match(/^\/v1\/agents\/([^/]+)$/);
|
|
@@ -13989,7 +14017,7 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
|
|
|
13989
14017
|
...presenceOnly ? { "x-kxm-events-mode": "presence" } : {}
|
|
13990
14018
|
});
|
|
13991
14019
|
response.write(`event: ready
|
|
13992
|
-
data: ${JSON.stringify({ agent: publicAgent(current) })}
|
|
14020
|
+
data: ${JSON.stringify({ agent: publicAgent(current, staleAfterMs) })}
|
|
13993
14021
|
|
|
13994
14022
|
`);
|
|
13995
14023
|
const client = {
|
package/plugins/kxm/package.json
CHANGED
|
@@ -15,7 +15,9 @@ All commands support `--json` for machine-readable output.
|
|
|
15
15
|
|
|
16
16
|
| Command | Purpose | Key Options |
|
|
17
17
|
|---|---|---|
|
|
18
|
-
| `kxm peer list` | List
|
|
18
|
+
| `kxm peer list` | List peer agents with purpose, host label, and presence | `--json`, `--include-offline` |
|
|
19
|
+
|
|
20
|
+
Every listed peer carries `host` (the box it declared at registration), `lastSeenAt`, `leaseExpiresAt`, and `presence`. Presence is the hub's own reading of its heartbeat lease: `online` holds the lease, `stale` has passed `leaseExpiresAt` but has not been swept yet, and `offline` is a registered peer the hub has retired. Offline peers are listed only with `--include-offline`. The host label is a reading aid, never a permission.
|
|
19
21
|
|
|
20
22
|
### Sending Requests (`kxm peer send`)
|
|
21
23
|
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -533,7 +533,8 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
|
|
|
533
533
|
const peer = addGlobalOptions(program.command("peer").description("Peer agent messaging and coordination"));
|
|
534
534
|
peer.helpCommand("help", "Show peer help");
|
|
535
535
|
|
|
536
|
-
addGlobalOptions(peer.command("list").description("List
|
|
536
|
+
addGlobalOptions(peer.command("list").description("List peer agents in this project's hub pool with host and presence"))
|
|
537
|
+
.option("--include-offline", "Also list registered peers whose hub lease has expired")
|
|
537
538
|
.option("--payload <json>", "JSON payload")
|
|
538
539
|
.action(async function peerListAction(this: Command, opts?: Record<string, unknown>) {
|
|
539
540
|
result.code = await dispatchAgentCliCommand(runtimeFrom(ctx, this), "kxm_list", opts ?? {});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { MAX_AGENT_HOST_CHARS } from "./protocol.ts";
|
|
2
4
|
import type { AgentRecord, DeliveryMode, HubEvent, MessageRecord, WorkflowMessageContext } from "./protocol.ts";
|
|
3
5
|
import type { ContextAuthority, ContextConfidence, ContextItem, ContextItemAuditMetadata, ContextItemKind, ContextPacket } from "./context.ts";
|
|
4
6
|
import {
|
|
@@ -52,6 +54,10 @@ export interface HubClientOptions {
|
|
|
52
54
|
purpose: string;
|
|
53
55
|
project: string;
|
|
54
56
|
model?: string;
|
|
57
|
+
/** Label for the box this client runs on, declared at registration.
|
|
58
|
+
* Defaults to the machine hostname; the hub records it for readers and
|
|
59
|
+
* never uses it to authorize anything. */
|
|
60
|
+
host?: string;
|
|
55
61
|
heartbeatMs?: number;
|
|
56
62
|
reconnectMs?: number;
|
|
57
63
|
requestTimeoutMs?: number;
|
|
@@ -94,6 +100,17 @@ class MeshWaitError extends Error {
|
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
/** The box label a client declares when it registers. Bounded to the hub's
|
|
104
|
+
* limit so a long hostname cannot turn registration into a 400, and dropped
|
|
105
|
+
* entirely when the platform cannot report one. */
|
|
106
|
+
function defaultHostLabel(): string | undefined {
|
|
107
|
+
try {
|
|
108
|
+
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || undefined;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
function completedFanoutResult(target: string, message: MessageRecord): FanoutResult {
|
|
98
115
|
if (message.status === "queued" || message.status === "delivered") {
|
|
99
116
|
throw new Error(`message ${message.id} is not complete`);
|
|
@@ -208,8 +225,11 @@ export class HubClient {
|
|
|
208
225
|
this.eventLoop = undefined;
|
|
209
226
|
}
|
|
210
227
|
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
/** Online peers of this client's project. `includeOffline` also returns
|
|
229
|
+
* registered members whose lease the hub has already retired. */
|
|
230
|
+
async listAgents(options: { includeOffline?: boolean } = {}): Promise<AgentRecord[]> {
|
|
231
|
+
const path = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
|
|
232
|
+
const result = await this.request<{ agents: AgentRecord[] }>(path);
|
|
213
233
|
return result.agents;
|
|
214
234
|
}
|
|
215
235
|
|
|
@@ -555,6 +575,7 @@ export class HubClient {
|
|
|
555
575
|
purpose: this.options.purpose,
|
|
556
576
|
project: this.options.project,
|
|
557
577
|
model: this.options.model,
|
|
578
|
+
host: this.options.host ?? defaultHostLabel(),
|
|
558
579
|
}),
|
|
559
580
|
}, false);
|
|
560
581
|
this.agent = registration.agent;
|
|
@@ -194,14 +194,20 @@ export const AGENT_COMMANDS: readonly AgentCommand[] = [
|
|
|
194
194
|
group: "peer",
|
|
195
195
|
verb: "list",
|
|
196
196
|
label: "List hub peers",
|
|
197
|
-
description:
|
|
197
|
+
description:
|
|
198
|
+
"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.",
|
|
198
199
|
parameters: {
|
|
199
200
|
type: "object",
|
|
200
|
-
properties: {
|
|
201
|
+
properties: {
|
|
202
|
+
includeOffline: {
|
|
203
|
+
type: "boolean",
|
|
204
|
+
description: "Also list registered peers whose hub lease has expired",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
201
207
|
additionalProperties: false,
|
|
202
208
|
},
|
|
203
|
-
async execute(client) {
|
|
204
|
-
return { agents: await client.listAgents() };
|
|
209
|
+
async execute(client, args) {
|
|
210
|
+
return { agents: await client.listAgents({ includeOffline: args.includeOffline === true }) };
|
|
205
211
|
},
|
|
206
212
|
},
|
|
207
213
|
{
|
package/plugins/kxm/src/hub.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DEFAULT_RATE_LIMIT_MAX,
|
|
11
11
|
DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
12
12
|
DEFAULT_STALE_AFTER_MS,
|
|
13
|
+
MAX_AGENT_HOST_CHARS,
|
|
13
14
|
MAX_BODY_BYTES,
|
|
14
15
|
MAX_CONTENT_CHARS,
|
|
15
16
|
MAX_MESSAGE_TTL_MS,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
parseBoundedInteger,
|
|
23
24
|
parseDeliveryMode,
|
|
24
25
|
requireString,
|
|
26
|
+
toAgentRecord,
|
|
25
27
|
type AgentRecord,
|
|
26
28
|
type DeliveryMode,
|
|
27
29
|
type HubEvent,
|
|
@@ -118,9 +120,12 @@ function isLoopback(host: string): boolean {
|
|
|
118
120
|
return isIP(host) === 4 && host.startsWith("127.");
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
/** Drop the agent key and attach hub-clocked presence. Every caller passes the
|
|
124
|
+
* hub's own `staleAfterMs` and clock, so no reader ever sees a lease derived
|
|
125
|
+
* from a client timestamp. */
|
|
126
|
+
function publicAgent(agent: StoredAgent, staleAfterMs: number, now = Date.now()): AgentRecord {
|
|
127
|
+
const { key: _key, ...identity } = agent;
|
|
128
|
+
return toAgentRecord(identity, staleAfterMs, now);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
function safeTokenEqual(actual: string | undefined, expected: string): boolean {
|
|
@@ -623,9 +628,10 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
623
628
|
}
|
|
624
629
|
|
|
625
630
|
function opsSnapshot(project: string) {
|
|
631
|
+
const snapshotAt = Date.now();
|
|
626
632
|
const projectAgents = [...agents.values()]
|
|
627
633
|
.filter((agent) => agent.project === project)
|
|
628
|
-
.map(publicAgent)
|
|
634
|
+
.map((agent) => publicAgent(agent, staleAfterMs, snapshotAt))
|
|
629
635
|
.sort((left, right) => Number(right.online) - Number(left.online) || left.name.localeCompare(right.name));
|
|
630
636
|
const open = store.getOpenMessages(project);
|
|
631
637
|
const projectRuns = [...workflowRuns.values()]
|
|
@@ -683,7 +689,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
683
689
|
function broadcastPresence(agent: StoredAgent): void {
|
|
684
690
|
for (const candidate of agents.values()) {
|
|
685
691
|
if (candidate.project === agent.project && candidate.id !== agent.id && candidate.online) {
|
|
686
|
-
publish(candidate.id, { type: "presence", agent: publicAgent(agent) });
|
|
692
|
+
publish(candidate.id, { type: "presence", agent: publicAgent(agent, staleAfterMs) });
|
|
687
693
|
}
|
|
688
694
|
}
|
|
689
695
|
publishOps(agent.project, "agents");
|
|
@@ -2109,6 +2115,9 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2109
2115
|
const project = requireString(body.project, "project", { max: 128 });
|
|
2110
2116
|
requireProjectAuth(request, project);
|
|
2111
2117
|
const model = optionalString(body.model, "model", 128);
|
|
2118
|
+
// A client declares which box it runs on. The label is a reading aid
|
|
2119
|
+
// only: the project token is still the whole of admission.
|
|
2120
|
+
const hostLabel = optionalString(body.host, "host", MAX_AGENT_HOST_CHARS);
|
|
2112
2121
|
const existing = [...agents.values()].find(
|
|
2113
2122
|
(agent) => agent.project === project && agent.name.toLowerCase() === name.toLowerCase(),
|
|
2114
2123
|
);
|
|
@@ -2135,20 +2144,34 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2135
2144
|
agent.online = true;
|
|
2136
2145
|
if (model) agent.model = model;
|
|
2137
2146
|
else delete agent.model;
|
|
2147
|
+
if (hostLabel) agent.host = hostLabel;
|
|
2148
|
+
else delete agent.host;
|
|
2138
2149
|
store.saveAgent(agent);
|
|
2139
2150
|
counters.registrations += 1;
|
|
2140
|
-
logger({
|
|
2151
|
+
logger({
|
|
2152
|
+
event: existing ? "agent_resumed" : "agent_registered",
|
|
2153
|
+
agentId: agent.id,
|
|
2154
|
+
name,
|
|
2155
|
+
project,
|
|
2156
|
+
...(hostLabel ? { host: hostLabel } : {}),
|
|
2157
|
+
});
|
|
2141
2158
|
broadcastPresence(agent);
|
|
2142
|
-
json(response, existing ? 200 : 201, { agent: publicAgent(agent), agentKey: agent.key, resumed: Boolean(existing) });
|
|
2159
|
+
json(response, existing ? 200 : 201, { agent: publicAgent(agent, staleAfterMs), agentKey: agent.key, resumed: Boolean(existing) });
|
|
2143
2160
|
return;
|
|
2144
2161
|
}
|
|
2145
2162
|
|
|
2146
2163
|
if (method === "GET" && url.pathname === "/v1/agents") {
|
|
2147
2164
|
const current = requireAgent(request);
|
|
2148
2165
|
requireProjectAuth(request, current.project);
|
|
2166
|
+
// Offline members are registered peers of the caller's own project
|
|
2167
|
+
// whose lease the sweep has already retired. They stay out of the
|
|
2168
|
+
// list unless a caller asks for them, so discovery keeps meaning
|
|
2169
|
+
// "who can answer right now".
|
|
2170
|
+
const includeOffline = url.searchParams.get("includeOffline") === "true";
|
|
2171
|
+
const listedAt = Date.now();
|
|
2149
2172
|
const result = [...agents.values()]
|
|
2150
|
-
.filter((agent) => agent.project === current.project && agent.online)
|
|
2151
|
-
.map(publicAgent);
|
|
2173
|
+
.filter((agent) => agent.project === current.project && (agent.online || includeOffline))
|
|
2174
|
+
.map((agent) => publicAgent(agent, staleAfterMs, listedAt));
|
|
2152
2175
|
json(response, 200, { agents: result });
|
|
2153
2176
|
return;
|
|
2154
2177
|
}
|
|
@@ -2158,7 +2181,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2158
2181
|
const current = requireAgent(request, decodeURIComponent(heartbeatMatch[1]!));
|
|
2159
2182
|
requireProjectAuth(request, current.project);
|
|
2160
2183
|
await readJson(request);
|
|
2161
|
-
json(response, 200, { agent: publicAgent(current) });
|
|
2184
|
+
json(response, 200, { agent: publicAgent(current, staleAfterMs) });
|
|
2162
2185
|
return;
|
|
2163
2186
|
}
|
|
2164
2187
|
|
|
@@ -2191,7 +2214,7 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2191
2214
|
"x-content-type-options": "nosniff",
|
|
2192
2215
|
...(presenceOnly ? { "x-kxm-events-mode": "presence" } : {}),
|
|
2193
2216
|
});
|
|
2194
|
-
response.write(`event: ready\ndata: ${JSON.stringify({ agent: publicAgent(current) })}\n\n`);
|
|
2217
|
+
response.write(`event: ready\ndata: ${JSON.stringify({ agent: publicAgent(current, staleAfterMs) })}\n\n`);
|
|
2195
2218
|
const client: SseClient = {
|
|
2196
2219
|
response,
|
|
2197
2220
|
heartbeat: setInterval(() => response.write(": heartbeat\n\n"), 15_000),
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "./sqlite.ts";
|
|
5
|
-
import type { AgentRecord, MessageRecord } from "./protocol.ts";
|
|
5
|
+
import type { AgentIdentity, AgentRecord, MessageRecord } from "./protocol.ts";
|
|
6
6
|
import type { WorkflowRun } from "./workflow.ts";
|
|
7
7
|
import { readRoutingRecords } from "./telemetry.ts";
|
|
8
8
|
import type { RoutingRecord, RoutingRecordV2 } from "./routing.ts";
|
|
@@ -253,7 +253,15 @@ export function loadLocalMeshSnapshot(
|
|
|
253
253
|
const database = new DatabaseSync(dataPath, { readOnly: true });
|
|
254
254
|
try {
|
|
255
255
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
256
|
-
|
|
256
|
+
// Stored rows carry identity only. Presence is derived here against the
|
|
257
|
+
// default lease window, because the hub's configured `staleAfterMs` is
|
|
258
|
+
// not in the file; a reachable hub's own records replace these.
|
|
259
|
+
// Stored rows carry identity only. Presence is NOT computed here: the hub's
|
|
260
|
+
// configured staleAfterMs and its clock are not in the file, so a reader-side
|
|
261
|
+
// lease would mislabel agents against the hub's own projection. The stored
|
|
262
|
+
// `online` boolean is the only durable truth; the hub's /v1/agents or the
|
|
263
|
+
// ops snapshot is the authoritative presence source.
|
|
264
|
+
agents = readJsonRows<AgentIdentity>(database, "SELECT record FROM agents");
|
|
257
265
|
openMessages = readOpenMessageMetadata(database);
|
|
258
266
|
openMessageTotal = countRows(database, "messages", " WHERE json_extract(record, '$.status') IN ('queued', 'delivered')");
|
|
259
267
|
legacyRuns = readJsonRows<WorkflowRun>(database, "SELECT record FROM workflow_runs ORDER BY rowid DESC LIMIT 8");
|
|
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
8
8
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
9
9
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.7.
|
|
11
|
+
const VERSION = "0.7.71";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -12,21 +12,62 @@ export const DEFAULT_RATE_LIMIT_MAX = 600;
|
|
|
12
12
|
export const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
13
13
|
export const MAX_BODY_BYTES = 256 * 1024;
|
|
14
14
|
export const MAX_CONTENT_CHARS = 32_000;
|
|
15
|
+
export const MAX_AGENT_HOST_CHARS = 64;
|
|
15
16
|
|
|
16
17
|
export type DeliveryMode = "steer" | "followUp" | "nextTurn";
|
|
17
18
|
export type MessageStatus = "queued" | "delivered" | "replied" | "cancelled" | "expired" | "error";
|
|
19
|
+
export type AgentPresence = "online" | "stale" | "offline";
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
/** The durable half of an agent: exactly the fields the hub persists in the
|
|
22
|
+
* agents record JSON. Presence is never stored, so a restored database can
|
|
23
|
+
* never claim an agent was alive. */
|
|
24
|
+
export interface AgentIdentity {
|
|
20
25
|
id: string;
|
|
21
26
|
name: string;
|
|
22
27
|
purpose: string;
|
|
23
28
|
project: string;
|
|
24
29
|
model?: string;
|
|
30
|
+
/** Client-declared label for the box this agent runs on. It exists so a
|
|
31
|
+
* reader can tell two boxes apart; the hub never reads it for
|
|
32
|
+
* authorization, project scope, or target resolution. */
|
|
33
|
+
host?: string;
|
|
25
34
|
connectedAt: string;
|
|
26
35
|
lastSeenAt: string;
|
|
27
36
|
online: boolean;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
/** The public projection of an agent: durable identity plus presence derived
|
|
40
|
+
* from the hub clock at read time. */
|
|
41
|
+
export interface AgentRecord extends AgentIdentity {
|
|
42
|
+
/** `lastSeenAt + staleAfterMs`: the heartbeat lease the stale sweep enforces.
|
|
43
|
+
* Absent when projected from the local store without the hub's clock or lease. */
|
|
44
|
+
leaseExpiresAt?: string;
|
|
45
|
+
/** Hub-clocked presence. Absent when the reader has no hub projection. */
|
|
46
|
+
presence?: AgentPresence;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Presence is hub-clocked and never client-reported. An agent holds its lease
|
|
50
|
+
* until `lastSeenAt + staleAfterMs`; between lease expiry and the sweep that
|
|
51
|
+
* retires it, it reads `stale`; once retired it reads `offline`. An
|
|
52
|
+
* unparseable `lastSeenAt` leaves the lease at the epoch, which reads as an
|
|
53
|
+
* expired lease rather than as a live agent. */
|
|
54
|
+
export function agentPresenceView(
|
|
55
|
+
agent: AgentIdentity,
|
|
56
|
+
staleAfterMs: number = DEFAULT_STALE_AFTER_MS,
|
|
57
|
+
now: number = Date.now(),
|
|
58
|
+
): { leaseExpiresAt: string; presence: AgentPresence } {
|
|
59
|
+
const lastSeenMs = Date.parse(agent.lastSeenAt);
|
|
60
|
+
const leaseExpiresAtMs = (Number.isFinite(lastSeenMs) ? lastSeenMs : 0) + staleAfterMs;
|
|
61
|
+
const leaseExpiresAt = new Date(leaseExpiresAtMs).toISOString();
|
|
62
|
+
if (!agent.online) return { leaseExpiresAt, presence: "offline" };
|
|
63
|
+
return { leaseExpiresAt, presence: now < leaseExpiresAtMs ? "online" : "stale" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Project a durable identity onto the wire shape readers consume. */
|
|
67
|
+
export function toAgentRecord(agent: AgentIdentity, staleAfterMs?: number, now?: number): AgentRecord {
|
|
68
|
+
return { ...agent, ...agentPresenceView(agent, staleAfterMs, now) };
|
|
69
|
+
}
|
|
70
|
+
|
|
30
71
|
export interface MessageReply {
|
|
31
72
|
content: string;
|
|
32
73
|
createdAt: string;
|
package/plugins/kxm/src/store.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type { DatabaseSync } from "./sqlite.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentIdentity, MessageRecord } from "./protocol.ts";
|
|
4
4
|
import type { ContextItem } from "./context.ts";
|
|
5
5
|
import type { WorkflowJournalEntry, WorkflowRun } from "./workflow.ts";
|
|
6
6
|
import {
|
|
@@ -78,7 +78,10 @@ export const HUB_STORE_SCHEMA_SPEC: DatabaseSchemaSpec = Object.freeze({
|
|
|
78
78
|
tables: HUB_STORE_TABLES,
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/** The stored shape stays the durable identity plus the agent key: presence
|
|
82
|
+
* and the lease are derived on read, so the agents record JSON grows only by
|
|
83
|
+
* the additive `host` label. */
|
|
84
|
+
export interface StoredAgent extends AgentIdentity {
|
|
82
85
|
key: string;
|
|
83
86
|
}
|
|
84
87
|
|
package/plugins/kxm/src/tui.ts
CHANGED
|
@@ -310,6 +310,19 @@ function visibleAgents(snapshot: MeshTuiSnapshot): AgentRecord[] {
|
|
|
310
310
|
return snapshot.agents.filter((agent) => agent.model !== "tui");
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
/** Presence comes from the hub clock, so the dash colours it rather than
|
|
314
|
+
* recomputing it: green holds a lease, amber has lost it but has not been
|
|
315
|
+
* swept, dim is a registered peer that is gone. */
|
|
316
|
+
function presenceCell(agent: AgentRecord, theme: MeshTuiTheme, width = 7): string {
|
|
317
|
+
// Local-store projections carry no hub-clocked presence; the dash shows "n/a"
|
|
318
|
+
// rather than inventing a lease from the reader's clock.
|
|
319
|
+
const presence = agent.presence ?? "n/a";
|
|
320
|
+
const cell = pad(presence, width);
|
|
321
|
+
if (presence === "online") return theme.success(cell);
|
|
322
|
+
if (presence === "stale") return theme.warning(cell);
|
|
323
|
+
return theme.dim(cell);
|
|
324
|
+
}
|
|
325
|
+
|
|
313
326
|
function panelMetric(snapshot: MeshTuiSnapshot, panel: MeshTuiPanel): string {
|
|
314
327
|
if (panel === "agents") {
|
|
315
328
|
const agents = visibleAgents(snapshot);
|
|
@@ -441,7 +454,7 @@ function listLines(snapshot: MeshTuiSnapshot, view: MeshTuiView, theme: MeshTuiT
|
|
|
441
454
|
const mark = (index: number) => cursor(theme, index === view.selected, "list", view.pane);
|
|
442
455
|
if (view.tab === "agents") {
|
|
443
456
|
return visibleAgents(snapshot).map((agent, index) => (
|
|
444
|
-
`${mark(index)}${pad(agent.name, 14)} ${agent
|
|
457
|
+
`${mark(index)}${pad(agent.name, 14)} ${presenceCell(agent, theme)} ${pad(agent.host ?? "-", 12)} ${pad(agent.model ?? "-", 14)} ${pad(age(agent.lastSeenAt, now), 4)}`
|
|
445
458
|
));
|
|
446
459
|
}
|
|
447
460
|
if (view.tab === "tasks" || view.tab === "workflows") {
|
|
@@ -489,9 +502,10 @@ function detailLines(snapshot: MeshTuiSnapshot, view: MeshTuiView, theme: MeshTu
|
|
|
489
502
|
const related = snapshot.openMessages.filter((message) => message.fromName === agent.name || message.toName === agent.name);
|
|
490
503
|
return [
|
|
491
504
|
theme.accent(agent.name),
|
|
492
|
-
`${agent
|
|
505
|
+
`${presenceCell(agent, theme, (agent.presence ?? "n/a").length)} ${agent.model ?? "-"}`,
|
|
506
|
+
`host ${agent.host ?? "-"}`,
|
|
493
507
|
agent.purpose,
|
|
494
|
-
`seen ${age(agent.lastSeenAt, now)} ago`,
|
|
508
|
+
`seen ${age(agent.lastSeenAt, now)} ago · lease ${agent.leaseExpiresAt?.slice(11, 19) ?? "n/a"} UTC`,
|
|
495
509
|
"",
|
|
496
510
|
theme.dim("Open work"),
|
|
497
511
|
...(related.length === 0 ? [theme.dim("none")] : related.map((message) => `${message.status} ${message.fromName} → ${message.toName} ${age(message.createdAt, now)}`)),
|
|
@@ -875,7 +889,9 @@ export async function runMeshTui(input: {
|
|
|
875
889
|
}
|
|
876
890
|
if (!useOpsStream && identity) {
|
|
877
891
|
try {
|
|
878
|
-
|
|
892
|
+
// Offline members too: the ops snapshot lists the whole project, and
|
|
893
|
+
// the legacy path is a fallback for the same screen, not a narrower one.
|
|
894
|
+
const listed = await input.fetchImpl(`${base}/v1/agents?includeOffline=true`, { headers: headers(identity) });
|
|
879
895
|
if (listed.ok) {
|
|
880
896
|
const body = await readJson<{ agents: AgentRecord[] }>(listed);
|
|
881
897
|
const byId = new Map(local.agents.map((agent) => [agent.id, agent]));
|