@kontextmind/kxm 0.7.100 → 0.7.102
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 +11 -2
- package/docs/concepts/architecture.md +1 -1
- package/docs/contributing/test-matrix.md +1 -0
- package/docs/guides/peer-messaging.md +2 -2
- package/docs/reference/cli-reference.md +4 -2
- package/docs/reference/tools.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +2 -2
- package/plugins/kxm/dist/mcp-server.js +39 -22
- package/plugins/kxm/dist/runtime.js +136 -127
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +4 -4
- package/plugins/kxm/src/cli/roles.ts +3 -2
- package/plugins/kxm/src/inbox.ts +9 -1
- package/plugins/kxm/src/mcp-server.ts +44 -21
- package/plugins/kxm/src/runtime-store.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -375,8 +375,9 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
375
375
|
or the Claude MCP server send one hop past the inbound request being handled, so a
|
|
376
376
|
chain of agents forwarding to each other stops at `hop_limit_reached`.
|
|
377
377
|
- **Workflow prompts no longer point agents at `.kxm/config`**, a path KXM refuses.
|
|
378
|
-
- **`kxm gate signal` and `kxm
|
|
379
|
-
runs.** They go to the local Runtime only for a run its store holds
|
|
378
|
+
- **`kxm gate signal`, `kxm workflow wait` and `kxm role resume` inside a KXM project reach
|
|
379
|
+
the hub for hub runs.** They go to the local Runtime only for a run its store holds, and
|
|
380
|
+
the lookup leaves no files behind, so `--dry-run` changes nothing.
|
|
380
381
|
- **`kxm peer inbox` lists the requests waiting for a named CLI agent.** It returned
|
|
381
382
|
`{"messages":[]}` every time. The hub now serves `GET /v1/agents/:id/inbox`
|
|
382
383
|
(agent-authenticated, project-scoped): the caller's queued and delivered requests,
|
|
@@ -384,6 +385,14 @@ All notable user-facing changes are documented here. The project follows [Semant
|
|
|
384
385
|
`codex`) to list requests peers queued for it while it was offline, then answer them
|
|
385
386
|
with `kxm peer reply`. The Pi extension's `kxm_inbox` tool now refuses instead of
|
|
386
387
|
returning an empty list, because Pi activates each inbound request as a turn itself.
|
|
388
|
+
- **A restarted Claude Code session keeps the requests it acknowledged but never answered.**
|
|
389
|
+
The MCP server acknowledges a request on arrival, and the hub pushes only unacknowledged
|
|
390
|
+
requests again on reconnect, so a session that restarted under the same agent name (the
|
|
391
|
+
plugin's `agent_name`) resumed its agent id but lost those requests from `kxm_inbox` and
|
|
392
|
+
the channel until they expired. After it registers, the server now reads them back from
|
|
393
|
+
the hub into `kxm_inbox` and announces each once as a channel event; one cancelled or
|
|
394
|
+
expired meanwhile is dropped, not announced. A tool call waits for that read, and a failed
|
|
395
|
+
read fails the call instead of listing a partial inbox.
|
|
387
396
|
|
|
388
397
|
- **The Claude plugin's SessionStart hook is one bundled, read-only, project-scoped
|
|
389
398
|
script.** The two shell hooks it replaces (`kxm session brief --status` and
|
|
@@ -90,7 +90,7 @@ stateDiagram-v2
|
|
|
90
90
|
|
|
91
91
|
- The sender gets the message ID at once. The recipient may reply straight from `queued` without acknowledging first.
|
|
92
92
|
- `error` is declared in the protocol, but the hub never sets it. Treat it as reserved.
|
|
93
|
-
- Open messages survive a hub restart. When an agent reconnects under the same project and name, the hub rotates its key and pushes every `queued` message again with its original ID. A `delivered` message is not pushed again; it stays open until a reply, cancellation, or expiry. Clients suppress a second turn for an ID they already handle.
|
|
93
|
+
- Open messages survive a hub restart. When an agent reconnects under the same project and name, the hub rotates its key and pushes every `queued` message again with its original ID. A `delivered` message is not pushed again; it stays open until a reply, cancellation, or expiry. The Claude Code MCP server reads its agent's `delivered` messages back from the hub when it registers, so a session restarted under the same name still lists and announces them. Clients suppress a second turn for an ID they already handle.
|
|
94
94
|
- An idempotency key deduplicates an exact retry by the same sender. It does not stop the recipient from repeating a side effect, so handlers must be safe to repeat.
|
|
95
95
|
- The default TTL is 24 hours (at most 7 days). Terminal messages are purged after the retention window, 7 days by default.
|
|
96
96
|
- If a workflow coordinator's prompt expires before its run finishes, the run fails.
|
|
@@ -20,6 +20,7 @@ to run one file is in [Develop KXM](development.md#run-one-file-or-one-test).
|
|
|
20
20
|
| Delivery modes, message fields, hop limits and validation | `hub-api.test.ts`, `protocol.test.ts` |
|
|
21
21
|
| Queue, acknowledgement, visibility, reply and authorization | `hub-api.test.ts`, `hub.test.ts` |
|
|
22
22
|
| An unacknowledged (queued) message replays after a recipient restart as the same record | `hub.test.ts`, `extension.test.ts`, `mcp.test.ts` |
|
|
23
|
+
| An acknowledged, unanswered request survives a Claude Code restart under the same agent name and is announced once; one cancelled during the restart is not announced | `mcp.test.ts`, `inbox.test.ts` |
|
|
23
24
|
| An `allowOffline` send queues, delivers once on resumption, and expires unread by TTL | `hub-api.test.ts` |
|
|
24
25
|
| TTL expiry, sender cancellation and terminal retention | `hub-api.test.ts` |
|
|
25
26
|
| Exact-retry idempotency, and rejection of a reused key with different content | `hub-api.test.ts` |
|
|
@@ -37,7 +37,7 @@ stateDiagram-v2
|
|
|
37
37
|
| State | Meaning |
|
|
38
38
|
|---|---|
|
|
39
39
|
| `queued` | Accepted and stored. Survives hub and agent restarts. The hub pushes it again each time the recipient reconnects, until the recipient acknowledges it. |
|
|
40
|
-
| `delivered` | Acknowledged: by Pi when the model turn starts, by the Claude Code plugin on arrival. It is not pushed again after a reconnect. |
|
|
40
|
+
| `delivered` | Acknowledged: by Pi when the model turn starts, by the Claude Code plugin on arrival. It is not pushed again after a reconnect; a Claude Code session that restarts under the same agent name reads it back from the hub. |
|
|
41
41
|
| `replied`, `cancelled`, `expired` | Terminal. A later reply or cancel is refused. |
|
|
42
42
|
| `error` | Declared in the protocol but never set by the hub. Treat it as reserved. |
|
|
43
43
|
|
|
@@ -288,7 +288,7 @@ Errors about `workflowContext` are covered in [Peer provenance and quorum gates]
|
|
|
288
288
|
| Symptom | Cause | Fix |
|
|
289
289
|
|---|---|---|
|
|
290
290
|
| A request stays `queued` | The recipient is offline, busy with earlier work, or swapping its Pi session for a workflow run. | Check `kxm_list` and the recipient's log. Do not send a duplicate. |
|
|
291
|
-
| A request stays `delivered` | The recipient's turn, tool, or provider call is still running, or
|
|
291
|
+
| A request stays `delivered` | The recipient's turn, tool, or provider call is still running, or the Claude Code session that acknowledged it came back under another agent name: `claude-<pid>` when `agent_name` is empty, or `<name>-<pid>` while the old session was still online. | Wait, or cancel and send it again with a new idempotency key. Keep the plugin's `agent_name` set, so a restarted session reads its requests back. |
|
|
292
292
|
| `kxm_fanout` returns `pending` | The local wait ended before a reply. | Use the returned message IDs with `kxm_get`, or repeat the exact call. |
|
|
293
293
|
| Claude Code never sees requests | Channel mode is off or blocked by policy. | Use `kxm_inbox` and `kxm_reply`. |
|
|
294
294
|
| `kxm peer inbox` is always empty | `KXM_AGENT_NAME` is unset, so each call registers a new `cli-<pid>` agent that nobody has addressed. | Set `KXM_AGENT_NAME` to the name peers send to. |
|
|
@@ -1351,11 +1351,13 @@ kxm role resume <runId> [ruling]
|
|
|
1351
1351
|
Resumes an audit-escalated run with an operator directive. The default ruling is `operator_ruling: waived and resumed`.
|
|
1352
1352
|
|
|
1353
1353
|
- Arguments: `<runId>`; `[ruling]`, free text recorded with the decision.
|
|
1354
|
-
-
|
|
1355
|
-
-
|
|
1354
|
+
- Inside a project, a run that this project's Runtime store holds gets an `audit_escalation` signal with action `unblock` in the Runtime, starting the supervisor if needed. Hub workflow runs share the `run_` + 32-hex shape, so the store, not the ID, decides. `--dry-run` plans the request without starting the supervisor. JSON keys: `runId`, `ruling`, `unblocked`.
|
|
1355
|
+
- Any other run ID, including a hub workflow run of the same shape, updates the hub store at `.kxm/state/kxm.db` in the current directory directly (ignoring `--workspace` and `KXM_DATA_PATH`) and adds a `decision` journal entry. `--dry-run` reads the store read-only, reports the stage it would resume and the resulting `status`, and plans the write. JSON keys: `runId`, `stageId`, `ruling`, `status`.
|
|
1356
1356
|
- The hub-run path bypasses the hub even while one is running: it writes SQLite directly, without authentication, in two statements outside one transaction. A running hub keeps runs in memory, so it does not see the change until it restarts, and its next write to that run overwrites it; it also pushes no event and sends the coordinator no resume message. Stop the hub first, or resume a live hub's run with a signed `audit_escalation` signal (see [Waits, signals and escalation](workflow-definitions.md#waits-signals-and-escalation)).
|
|
1357
1357
|
- Errors: `resume_failed` (exit 1), or a plain `not found` line (exit 1).
|
|
1358
1358
|
|
|
1359
|
+
A run the project's Runtime holds:
|
|
1360
|
+
|
|
1359
1361
|
```bash
|
|
1360
1362
|
kxm role resume run_0123456789abcdef0123456789abcdef "waive the audit" --dry-run --json
|
|
1361
1363
|
```
|
package/docs/reference/tools.md
CHANGED
|
@@ -161,7 +161,7 @@ Returns the message record with `status: "cancelled"`. Cancelling an already can
|
|
|
161
161
|
|
|
162
162
|
Lists inbound requests that still need a reply. It takes no parameters.
|
|
163
163
|
|
|
164
|
-
- **Claude Code:** returns `{ "messages": [...] }` from the session's inbox, which fills from the hub's event stream while the session is registered. Before returning, it re-reads each request and drops those already answered, cancelled or expired. This is pull mode; see [Pushed channel mode and pull mode](../../plugins/kxm/README.md#pushed-channel-mode-and-pull-mode).
|
|
164
|
+
- **Claude Code:** returns `{ "messages": [...] }` from the session's inbox, which fills from the hub's event stream while the session is registered. A session that restarts under the same agent name also reads back the requests the previous session acknowledged but never answered. Before returning, it re-reads each request and drops those already answered, cancelled or expired. This is pull mode; see [Pushed channel mode and pull mode](../../plugins/kxm/README.md#pushed-channel-mode-and-pull-mode).
|
|
165
165
|
- **Pi:** refuses. The extension turns each inbound request into a model turn itself, and that turn's final response is the reply, so listing would offer requests its own queue is about to activate.
|
|
166
166
|
- **CLI:** `kxm peer inbox` reads the agent's open requests from the hub (`GET /v1/agents/<agentId>/inbox`), acknowledging nothing. Run it with a stable `KXM_AGENT_NAME` to see requests queued for that name while it was offline; the default `cli-<pid>` is a new agent on every call, so its list is empty.
|
|
167
167
|
|
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.102",
|
|
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/README.md
CHANGED
|
@@ -160,7 +160,7 @@ Workflow authors can require replies from a snapshotted set of eligible peers. T
|
|
|
160
160
|
|
|
161
161
|
Peer requests reach Claude in one of two ways. Everything else, including `kxm_send`, `kxm_fanout`, the workflow tools and the context tools, works the same in both.
|
|
162
162
|
|
|
163
|
-
**Pull mode** is the default. Claude checks for work with `kxm_inbox`, handles one request, and answers it with `kxm_reply` and the request's message ID. Nothing arrives on its own: ask Claude to check the inbox, or to poll it with a backoff while it waits. The MCP server keeps the inbox, filling it from the hub's event stream while it is registered; in a KXM project with a project token that starts with the session.
|
|
163
|
+
**Pull mode** is the default. Claude checks for work with `kxm_inbox`, handles one request, and answers it with `kxm_reply` and the request's message ID. Nothing arrives on its own: ask Claude to check the inbox, or to poll it with a backoff while it waits. The MCP server keeps the inbox, filling it from the hub's event stream while it is registered; in a KXM project with a project token that starts with the session. A session that restarts under the same `agent_name` also reads back the requests the previous session acknowledged but never answered, and announces each once on the channel.
|
|
164
164
|
|
|
165
165
|
**Pushed channel mode** uses Claude Code channels to inject each peer request into the running session as a `<channel source="kxm" message_id="...">` event, which Claude handles and answers with `kxm_reply`. During the channels research preview, start Claude Code with the community channel explicitly and review the trust prompt:
|
|
166
166
|
|
package/plugins/kxm/dist/cli.js
CHANGED
|
@@ -24677,7 +24677,7 @@ function kxmProjectRunEventsPath(projectRoot, env) {
|
|
|
24677
24677
|
function projectRuntimeOwnsRun(projectRoot, runId, env) {
|
|
24678
24678
|
const path4 = kxmProjectRunEventsPath(projectRoot, env);
|
|
24679
24679
|
if (!existsSync10(path4)) return false;
|
|
24680
|
-
const database =
|
|
24680
|
+
const database = openReadOnlyDatabase(path4);
|
|
24681
24681
|
try {
|
|
24682
24682
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
24683
24683
|
return database.prepare("SELECT 1 FROM runs WHERE run_id = ?").get(runId) !== void 0;
|
|
@@ -28834,7 +28834,7 @@ async function cmdRoleResume(runtime, runId, ruling) {
|
|
|
28834
28834
|
}
|
|
28835
28835
|
const effectiveRuling = ruling?.trim() || "operator_ruling: waived and resumed";
|
|
28836
28836
|
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
28837
|
-
if (projectRoot &&
|
|
28837
|
+
if (projectRoot && projectRuntimeOwnsRun(projectRoot, runId, runtime.env)) {
|
|
28838
28838
|
if (runtime.dryRun) {
|
|
28839
28839
|
printPlan(
|
|
28840
28840
|
runtime,
|
|
@@ -17294,8 +17294,13 @@ function enforceToolPolicy(commandName, env = process.env, options) {
|
|
|
17294
17294
|
// plugins/kxm/src/inbox.ts
|
|
17295
17295
|
async function deliverInboxNotification(messageId, delivered, notify) {
|
|
17296
17296
|
if (delivered.has(messageId)) return false;
|
|
17297
|
-
await notify();
|
|
17298
17297
|
delivered.add(messageId);
|
|
17298
|
+
try {
|
|
17299
|
+
await notify();
|
|
17300
|
+
} catch (error2) {
|
|
17301
|
+
delivered.delete(messageId);
|
|
17302
|
+
throw error2;
|
|
17303
|
+
}
|
|
17299
17304
|
return true;
|
|
17300
17305
|
}
|
|
17301
17306
|
|
|
@@ -17308,7 +17313,7 @@ function sessionTokenFixHint(policy) {
|
|
|
17308
17313
|
}
|
|
17309
17314
|
|
|
17310
17315
|
// plugins/kxm/src/mcp-server.ts
|
|
17311
|
-
var VERSION = "0.7.
|
|
17316
|
+
var VERSION = "0.7.102";
|
|
17312
17317
|
var CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
17313
17318
|
var inbox = /* @__PURE__ */ new Map();
|
|
17314
17319
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
@@ -17344,39 +17349,50 @@ function sessionIdentity() {
|
|
|
17344
17349
|
serverUrl: process.env.KXM_SERVER_URL?.trim() || "http://127.0.0.1:7331"
|
|
17345
17350
|
};
|
|
17346
17351
|
}
|
|
17347
|
-
async function
|
|
17348
|
-
if (event.type === "cancelled" || event.type === "expired") {
|
|
17349
|
-
inbox.delete(event.message.id);
|
|
17350
|
-
notifiedInbox.delete(event.message.id);
|
|
17351
|
-
return;
|
|
17352
|
-
}
|
|
17353
|
-
if (event.type !== "message") return;
|
|
17354
|
-
const client = meshClient;
|
|
17355
|
-
if (!client) return;
|
|
17356
|
-
if (event.message.status === "queued") await client.acknowledge(event.message.id);
|
|
17357
|
-
inbox.set(event.message.id, event.message);
|
|
17352
|
+
async function announce(message) {
|
|
17358
17353
|
const meta2 = {
|
|
17359
|
-
message_id:
|
|
17360
|
-
from_agent:
|
|
17361
|
-
delivery:
|
|
17354
|
+
message_id: message.id,
|
|
17355
|
+
from_agent: message.fromName,
|
|
17356
|
+
delivery: message.delivery
|
|
17362
17357
|
};
|
|
17363
|
-
if (
|
|
17364
|
-
await deliverInboxNotification(
|
|
17358
|
+
if (message.correlationId) meta2.correlation_id = message.correlationId;
|
|
17359
|
+
await deliverInboxNotification(message.id, notifiedInbox, async () => {
|
|
17365
17360
|
await mcp.notification({
|
|
17366
17361
|
method: "notifications/claude/channel",
|
|
17367
17362
|
params: {
|
|
17368
17363
|
content: [
|
|
17369
|
-
`Peer request from ${
|
|
17364
|
+
`Peer request from ${message.fromName}:`,
|
|
17370
17365
|
"",
|
|
17371
|
-
|
|
17366
|
+
message.content,
|
|
17372
17367
|
"",
|
|
17373
|
-
`When complete, call kxm_reply with messageId ${
|
|
17368
|
+
`When complete, call kxm_reply with messageId ${message.id}.`
|
|
17374
17369
|
].join("\n"),
|
|
17375
17370
|
meta: meta2
|
|
17376
17371
|
}
|
|
17377
17372
|
});
|
|
17378
17373
|
});
|
|
17379
17374
|
}
|
|
17375
|
+
async function onHubEvent(client, event) {
|
|
17376
|
+
if (event.type === "cancelled" || event.type === "expired") {
|
|
17377
|
+
inbox.delete(event.message.id);
|
|
17378
|
+
notifiedInbox.delete(event.message.id);
|
|
17379
|
+
return;
|
|
17380
|
+
}
|
|
17381
|
+
if (event.type !== "message") return;
|
|
17382
|
+
if (event.message.status === "queued") await client.acknowledge(event.message.id);
|
|
17383
|
+
inbox.set(event.message.id, event.message);
|
|
17384
|
+
await announce(event.message);
|
|
17385
|
+
}
|
|
17386
|
+
async function seedInbox(client) {
|
|
17387
|
+
for (const message of await client.listInbox()) {
|
|
17388
|
+
if (message.status === "delivered" && !inbox.has(message.id)) inbox.set(message.id, message);
|
|
17389
|
+
}
|
|
17390
|
+
await reconcileInbox(client, inbox, notifiedInbox);
|
|
17391
|
+
for (const messageId of [...inbox.keys()]) {
|
|
17392
|
+
const message = inbox.get(messageId);
|
|
17393
|
+
if (message) await announce(message);
|
|
17394
|
+
}
|
|
17395
|
+
}
|
|
17380
17396
|
async function startClient(project, serverUrl, name, authToken) {
|
|
17381
17397
|
const candidate = new HubClient({
|
|
17382
17398
|
serverUrl,
|
|
@@ -17387,7 +17403,8 @@ async function startClient(project, serverUrl, name, authToken) {
|
|
|
17387
17403
|
authToken
|
|
17388
17404
|
});
|
|
17389
17405
|
try {
|
|
17390
|
-
await candidate.start(onHubEvent);
|
|
17406
|
+
await candidate.start((event) => onHubEvent(candidate, event));
|
|
17407
|
+
await seedInbox(candidate);
|
|
17391
17408
|
meshClient = candidate;
|
|
17392
17409
|
return candidate;
|
|
17393
17410
|
} catch (error2) {
|