@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/plugins/kxm/package.json
CHANGED
|
@@ -81,10 +81,10 @@ kxm workflow record run_12345 decision "Serialize the fixture setup" --stage-id
|
|
|
81
81
|
does not read project `.kxm/workflows/*.yaml`; validate those with
|
|
82
82
|
`kxm init --dry-run --json`.
|
|
83
83
|
|
|
84
|
-
`kxm gate signal` on a
|
|
85
|
-
Runtime and passes `--recovery-action` through; on a hub webhook run
|
|
86
|
-
|
|
87
|
-
secret. Reuse `--delivery-id` to retry one unchanged callback.
|
|
84
|
+
`kxm gate signal` on a run this project's Runtime store holds goes to the
|
|
85
|
+
Runtime and passes `--recovery-action` through; on a hub webhook run, including
|
|
86
|
+
one with the same `run_` + 32-hex shape, it posts a signed callback that needs
|
|
87
|
+
`KXM_WORKFLOW_ID` and the definition's signal secret. Reuse `--delivery-id` to retry one unchanged callback.
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
90
|
kxm gate validate --file workflows.json --json
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "../role.ts";
|
|
20
20
|
import { discoverKxmProjectRoot } from "../project-config.ts";
|
|
21
21
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
22
|
+
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
22
23
|
import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
|
|
23
24
|
import { print, printPlan, type CliIo, type Runtime } from "./types.ts";
|
|
24
25
|
|
|
@@ -547,9 +548,9 @@ export async function cmdRoleResume(
|
|
|
547
548
|
|
|
548
549
|
const effectiveRuling = ruling?.trim() || "operator_ruling: waived and resumed";
|
|
549
550
|
|
|
550
|
-
//
|
|
551
|
+
// A run this project's Runtime owns; a hub workflow run of the same id shape falls through.
|
|
551
552
|
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
552
|
-
if (projectRoot &&
|
|
553
|
+
if (projectRoot && projectRuntimeOwnsRun(projectRoot, runId, runtime.env)) {
|
|
553
554
|
if (runtime.dryRun) {
|
|
554
555
|
printPlan(
|
|
555
556
|
runtime,
|
package/plugins/kxm/src/inbox.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
/** Announce an inbox request at most once per process. The id is claimed before the send, so a
|
|
2
|
+
* request read back from the hub and the same request arriving on the event stream cannot both
|
|
3
|
+
* announce it; a failed send releases the claim, so the request stays retryable. */
|
|
1
4
|
export async function deliverInboxNotification(
|
|
2
5
|
messageId: string,
|
|
3
6
|
delivered: Set<string>,
|
|
4
7
|
notify: () => Promise<void>,
|
|
5
8
|
): Promise<boolean> {
|
|
6
9
|
if (delivered.has(messageId)) return false;
|
|
7
|
-
await notify();
|
|
8
10
|
delivered.add(messageId);
|
|
11
|
+
try {
|
|
12
|
+
await notify();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
delivered.delete(messageId);
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
9
17
|
return true;
|
|
10
18
|
}
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.102";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -55,33 +55,24 @@ function sessionIdentity(): { projectDir: string; project: string; serverUrl: st
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
inbox.delete(event.message.id);
|
|
61
|
-
notifiedInbox.delete(event.message.id);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (event.type !== "message") return;
|
|
65
|
-
const client = meshClient;
|
|
66
|
-
if (!client) return;
|
|
67
|
-
if (event.message.status === "queued") await client.acknowledge(event.message.id);
|
|
68
|
-
inbox.set(event.message.id, event.message);
|
|
58
|
+
/** Tell the session about an inbox request on the channel, at most once per process. */
|
|
59
|
+
async function announce(message: MessageRecord): Promise<void> {
|
|
69
60
|
const meta: Record<string, string> = {
|
|
70
|
-
message_id:
|
|
71
|
-
from_agent:
|
|
72
|
-
delivery:
|
|
61
|
+
message_id: message.id,
|
|
62
|
+
from_agent: message.fromName,
|
|
63
|
+
delivery: message.delivery,
|
|
73
64
|
};
|
|
74
|
-
if (
|
|
75
|
-
await deliverInboxNotification(
|
|
65
|
+
if (message.correlationId) meta.correlation_id = message.correlationId;
|
|
66
|
+
await deliverInboxNotification(message.id, notifiedInbox, async () => {
|
|
76
67
|
await mcp.notification({
|
|
77
68
|
method: "notifications/claude/channel",
|
|
78
69
|
params: {
|
|
79
70
|
content: [
|
|
80
|
-
`Peer request from ${
|
|
71
|
+
`Peer request from ${message.fromName}:`,
|
|
81
72
|
"",
|
|
82
|
-
|
|
73
|
+
message.content,
|
|
83
74
|
"",
|
|
84
|
-
`When complete, call kxm_reply with messageId ${
|
|
75
|
+
`When complete, call kxm_reply with messageId ${message.id}.`,
|
|
85
76
|
].join("\n"),
|
|
86
77
|
meta,
|
|
87
78
|
},
|
|
@@ -89,6 +80,36 @@ async function onHubEvent(event: HubEvent): Promise<void> {
|
|
|
89
80
|
});
|
|
90
81
|
}
|
|
91
82
|
|
|
83
|
+
async function onHubEvent(client: HubClient, event: HubEvent): Promise<void> {
|
|
84
|
+
if (event.type === "cancelled" || event.type === "expired") {
|
|
85
|
+
inbox.delete(event.message.id);
|
|
86
|
+
notifiedInbox.delete(event.message.id);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (event.type !== "message") return;
|
|
90
|
+
if (event.message.status === "queued") await client.acknowledge(event.message.id);
|
|
91
|
+
inbox.set(event.message.id, event.message);
|
|
92
|
+
await announce(event.message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A durable KXM_AGENT_NAME resumes its agent id, but the event stream replays only requests
|
|
96
|
+
* that agent has not acknowledged. One an earlier process acknowledged and never answered is
|
|
97
|
+
* still open on the hub, so it is read back here and announced like a pushed request: this
|
|
98
|
+
* session has not been told about it. The inbox is reconciled before anything is announced,
|
|
99
|
+
* so a request cancelled or expired while the list was in flight is dropped, not announced;
|
|
100
|
+
* its event may have gone to a stream that was not connected yet. Queued requests are left
|
|
101
|
+
* to the stream, which acknowledges them in order. */
|
|
102
|
+
async function seedInbox(client: HubClient): Promise<void> {
|
|
103
|
+
for (const message of await client.listInbox()) {
|
|
104
|
+
if (message.status === "delivered" && !inbox.has(message.id)) inbox.set(message.id, message);
|
|
105
|
+
}
|
|
106
|
+
await reconcileInbox(client, inbox, notifiedInbox);
|
|
107
|
+
for (const messageId of [...inbox.keys()]) {
|
|
108
|
+
const message = inbox.get(messageId);
|
|
109
|
+
if (message) await announce(message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
async function startClient(project: string, serverUrl: string, name: string, authToken: string): Promise<HubClient> {
|
|
93
114
|
const candidate = new HubClient({
|
|
94
115
|
serverUrl,
|
|
@@ -99,7 +120,9 @@ async function startClient(project: string, serverUrl: string, name: string, aut
|
|
|
99
120
|
authToken,
|
|
100
121
|
});
|
|
101
122
|
try {
|
|
102
|
-
await candidate.start(onHubEvent);
|
|
123
|
+
await candidate.start((event) => onHubEvent(candidate, event));
|
|
124
|
+
// A tool call waits for the seeded inbox: `starting` stays pending until this returns.
|
|
125
|
+
await seedInbox(candidate);
|
|
103
126
|
meshClient = candidate;
|
|
104
127
|
return candidate;
|
|
105
128
|
} catch (error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "./sqlite.ts";
|
|
4
|
+
import { DatabaseSync, openReadOnlyDatabase } from "./sqlite.ts";
|
|
5
5
|
import { KxmConfigError, validateCoordinator, validateDriveReceipt, validateIntakeMessage, validateRunEvent, kxmCanonicalJson, type JsonValue, type KxmConfigIssue, type KxmConfigOptions } from "./project-config.ts";
|
|
6
6
|
import { kxmUserStateRoot } from "./bindings.ts";
|
|
7
7
|
import { deriveKxmSyncEvent, kxmSyncEventBytes, KxmSyncRedactor } from "./sync-transform.ts";
|
|
@@ -59,13 +59,13 @@ export function kxmProjectRunEventsPath(projectRoot: string, env: NodeJS.Process
|
|
|
59
59
|
/**
|
|
60
60
|
* Whether this project's Runtime event store holds `runId`. Hub workflow runs and Runtime
|
|
61
61
|
* runs share the `run_<32 hex>` shape, so a command addressed by run id asks the owning
|
|
62
|
-
* store instead of guessing from the id. Read-only
|
|
63
|
-
* owns no runs.
|
|
62
|
+
* store instead of guessing from the id. Read-only and leaves no sidecars, so a dry run
|
|
63
|
+
* can ask; a project whose Runtime never ran owns no runs.
|
|
64
64
|
*/
|
|
65
65
|
export function projectRuntimeOwnsRun(projectRoot: string, runId: string, env: NodeJS.ProcessEnv): boolean {
|
|
66
66
|
const path = kxmProjectRunEventsPath(projectRoot, env);
|
|
67
67
|
if (!existsSync(path)) return false;
|
|
68
|
-
const database =
|
|
68
|
+
const database = openReadOnlyDatabase(path);
|
|
69
69
|
try {
|
|
70
70
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
71
71
|
return database.prepare("SELECT 1 FROM runs WHERE run_id = ?").get(runId) !== undefined;
|