@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.100",
3
+ "version": "0.7.102",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -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 KXM run ID (`run_` plus 32 hex digits) goes to the
85
- Runtime and passes `--recovery-action` through; on a hub webhook run it posts a
86
- signed callback that needs `KXM_WORKFLOW_ID` and the definition's signal
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
- // Check if it's a KXM run
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 && /^run_[a-f0-9]{32}$/i.test(runId)) {
553
+ if (projectRoot && projectRuntimeOwnsRun(projectRoot, runId, runtime.env)) {
553
554
  if (runtime.dryRun) {
554
555
  printPlan(
555
556
  runtime,
@@ -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.100";
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
- async function onHubEvent(event: HubEvent): Promise<void> {
59
- if (event.type === "cancelled" || event.type === "expired") {
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: event.message.id,
71
- from_agent: event.message.fromName,
72
- delivery: event.message.delivery,
61
+ message_id: message.id,
62
+ from_agent: message.fromName,
63
+ delivery: message.delivery,
73
64
  };
74
- if (event.message.correlationId) meta.correlation_id = event.message.correlationId;
75
- await deliverInboxNotification(event.message.id, notifiedInbox, async () => {
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 ${event.message.fromName}:`,
71
+ `Peer request from ${message.fromName}:`,
81
72
  "",
82
- event.message.content,
73
+ message.content,
83
74
  "",
84
- `When complete, call kxm_reply with messageId ${event.message.id}.`,
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; a project whose Runtime never ran
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 = new DatabaseSync(path, { readOnly: true });
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;