@kontextmind/kxm 0.7.97 → 0.7.99

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.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/workflows/default.yaml +2 -0
  3. package/CHANGELOG.md +46 -2
  4. package/docs/concepts/architecture.md +1 -1
  5. package/docs/concepts/data-and-storage.md +1 -1
  6. package/docs/contracts/routing.md +1 -1
  7. package/docs/contributing/test-matrix.md +6 -5
  8. package/docs/guides/peer-messaging.md +2 -2
  9. package/docs/operations/backup-and-restore.md +43 -24
  10. package/docs/operations/deploy.md +1 -1
  11. package/docs/reference/cli-reference.md +65 -28
  12. package/docs/reference/config-reference.md +24 -13
  13. package/docs/reference/harness-routing.md +3 -3
  14. package/docs/reference/http-api.md +2 -1
  15. package/docs/reference/tools.md +3 -3
  16. package/docs/start/first-workflow.md +4 -4
  17. package/docs/start/quickstart-claude-code.md +2 -2
  18. package/package.json +1 -1
  19. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  20. package/plugins/kxm/dist/claude-hook.js +4 -1
  21. package/plugins/kxm/dist/cli.js +407 -156
  22. package/plugins/kxm/dist/client.js +9 -0
  23. package/plugins/kxm/dist/core.js +9 -3
  24. package/plugins/kxm/dist/extension.js +13 -1
  25. package/plugins/kxm/dist/mcp-server.js +14 -2
  26. package/plugins/kxm/dist/runtime-supervisor.js +231 -48
  27. package/plugins/kxm/dist/runtime.js +415 -92
  28. package/plugins/kxm/dist/server.js +19 -1
  29. package/plugins/kxm/package.json +1 -1
  30. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +4 -3
  31. package/plugins/kxm/skills/kxm-peer/SKILL.md +5 -3
  32. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -2
  33. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +3 -0
  34. package/plugins/kxm/skills/kxm-runs/SKILL.md +8 -7
  35. package/plugins/kxm/src/cli/project.ts +22 -13
  36. package/plugins/kxm/src/cli/system.ts +22 -1
  37. package/plugins/kxm/src/cli.ts +14 -4
  38. package/plugins/kxm/src/client.ts +10 -0
  39. package/plugins/kxm/src/commands.ts +10 -1
  40. package/plugins/kxm/src/database.ts +210 -36
  41. package/plugins/kxm/src/engine.ts +117 -2
  42. package/plugins/kxm/src/extension.ts +1 -0
  43. package/plugins/kxm/src/harness.ts +29 -0
  44. package/plugins/kxm/src/hub.ts +12 -0
  45. package/plugins/kxm/src/init-guide-setup.ts +43 -28
  46. package/plugins/kxm/src/mcp-server.ts +1 -1
  47. package/plugins/kxm/src/oneshot-producer.ts +16 -8
  48. package/plugins/kxm/src/prices.ts +33 -2
  49. package/plugins/kxm/src/routing.ts +13 -7
  50. package/plugins/kxm/src/studio-layout.ts +5 -4
  51. package/plugins/kxm/src/template.ts +31 -0
  52. package/plugins/kxm/src/worktree-witness.ts +71 -0
  53. package/schemas/backup-manifest.schema.json +33 -0
@@ -15271,6 +15271,10 @@ var READ_ONLY_ONESHOT_ARGS = Object.freeze({
15271
15271
  function oneShotReadOnlyArgs(harness) {
15272
15272
  return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness] : void 0;
15273
15273
  }
15274
+ var WRITER_ONESHOT_ARGS = Object.freeze({
15275
+ pi: Object.freeze(["-a", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session"]),
15276
+ grok: Object.freeze(["--always-approve", "--no-subagents", "--disable-web-search"])
15277
+ });
15274
15278
  var BUILTIN_HARNESSES = Object.freeze([
15275
15279
  {
15276
15280
  id: "pi",
@@ -17219,7 +17223,10 @@ var AGENT_COMMANDS = [
17219
17223
  await reconcileInbox(client, context.inbox, context.notifiedInbox);
17220
17224
  return { messages: [...context.inbox.values()] };
17221
17225
  }
17222
- return { messages: [] };
17226
+ if (context?.hubInbox) return { messages: await client.listInbox() };
17227
+ throw new Error(
17228
+ "kxm_inbox is not available in this session: it activates each inbound request as a turn, and that turn's final response is the reply"
17229
+ );
17223
17230
  }
17224
17231
  },
17225
17232
  {
@@ -19418,6 +19425,9 @@ var DatabaseSync = class {
19418
19425
  }
19419
19426
  };
19420
19427
 
19428
+ // plugins/kxm/src/bindings.ts
19429
+ var MAX_BINDING_RECORD_BYTES = 256 * 1024;
19430
+
19421
19431
  // plugins/kxm/src/database.ts
19422
19432
  function databaseError(code, file, message) {
19423
19433
  const issue2 = { phase: "semantic", code, file, message };
@@ -22519,6 +22529,14 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
22519
22529
  json(response, 200, { agent: publicAgent(current, staleAfterMs) });
22520
22530
  return;
22521
22531
  }
22532
+ const inboxMatch = url.pathname.match(/^\/v1\/agents\/([^/]+)\/inbox$/);
22533
+ if (method === "GET" && inboxMatch) {
22534
+ const current = requireAgent(request, decodeURIComponent(inboxMatch[1]));
22535
+ requireProjectAuth(request, current.project);
22536
+ expireMessages();
22537
+ json(response, 200, { messages: store.getPendingMessages(current.id) });
22538
+ return;
22539
+ }
22522
22540
  const agentMatch = url.pathname.match(/^\/v1\/agents\/([^/]+)$/);
22523
22541
  if (method === "DELETE" && agentMatch) {
22524
22542
  const current = requireAgent(request, decodeURIComponent(agentMatch[1]));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.97",
3
+ "version": "0.7.99",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -15,7 +15,7 @@ invent restart or status subcommands.
15
15
  |---|---|---|
16
16
  | `kxm hub view` | Check `/health` and `/ready`; exits 1 when the hub is down | `--json` |
17
17
  | `kxm tenant status` | Hub metadata and Runtime run state as one labeled view; reads only and never starts the supervisor | `--json` |
18
- | `kxm backup` | Verified SQLite backup of the hub stores with a hashed manifest | `--out <dir>`, `--json` |
18
+ | `kxm backup` | Verified SQLite backup of the hub store and the Runtime stores with a hashed manifest; exits 1 when the backup is incomplete | `--out <dir>`, `--json` |
19
19
 
20
20
  ```bash
21
21
  kxm hub view --json
@@ -30,8 +30,9 @@ runs have separate ID spaces, so a comparison with no shared ID is
30
30
  `unverified`, never agreement.
31
31
 
32
32
  The durable hub store defaults to `.kxm/state/kxm.db` (`KXM_DATA_PATH`). Do not
33
- hand-edit it. `kxm backup` does not include the Runtime supervisor's stores
34
- under the user state root.
33
+ hand-edit it. `kxm backup` also copies the Runtime registry and every
34
+ project's run event store and prompt sidecar under the user state root, so a
35
+ `kxm restore` rolls back every project on the machine, not only this one.
35
36
 
36
37
  ## Operator steps
37
38
 
@@ -42,9 +42,11 @@ reading of its heartbeat lease: `online` holds the lease, `stale` has passed
42
42
  peer the hub has retired. Offline peers are listed only with
43
43
  `--include-offline`. The host label is a reading aid, never a permission.
44
44
 
45
- `kxm peer inbox` from a one-shot CLI call always returns an empty list,
46
- because the inbox lives in a long-running harness session. Use `kxm_inbox` in
47
- that session or `kxm dash --screen inbox`.
45
+ `kxm peer inbox` lists the requests addressed to the CLI agent's own name, so
46
+ run it with a stable `KXM_AGENT_NAME` and answer each with `kxm peer reply`
47
+ under that name. The default `cli-<pid>` is a new agent on every call, and its
48
+ inbox is always empty. In Claude Code, `kxm_inbox` lists the session's inbox; a
49
+ Pi session receives each request as a turn instead, and its `kxm_inbox` refuses.
48
50
 
49
51
  ## Examples
50
52
 
@@ -122,8 +122,9 @@ or store the admin or project token in the conversation. The user enters
122
122
  in a workflow step, then run `kxm init` again. Only when
123
123
  `.kxm/project.yaml` does not exist yet, ask the user to follow the README's
124
124
  move-aside workaround; never do that in a committed project.
125
- - The template `default` workflow declares `limits.maxAgentTimeMs`, so
126
- `kxm runs drive` on it fails with `run_handoff_required`. Use `first`.
125
+ - The template `default` workflow can be driven, but even a simulated drive
126
+ runs its `npm test` gate, and a live drive spends Claude and Grok and lets
127
+ Grok edit the checkout. Use `first` for the first run.
127
128
  - `kxm run` starts the Runtime supervisor. Stop it with `kxm runtime stop`.
128
129
  - Only run workflow IDs that `kxm workflow definitions` lists; any other ID
129
130
  fails with `run_workflow_unknown`.
@@ -73,11 +73,14 @@ Never apply a candidate diff yourself or treat `readyForReview` as approval.
73
73
  |---|---|---|
74
74
  | `kxm routing report` | Compare verified completion, cost, and rework per behavioral configuration | `-f/--file`, `-l/--equivalent-list-cost`, `--list-prices`, `--prices <path>` |
75
75
  | `kxm routing benchmark` | Placeholder side-by-side comparison | `--task`, `--arms`, `--runs` |
76
+ | `kxm prices acknowledge` | Stamp the existing `.kxm/prices.yaml` list as today's estimate without fetching vendor rates | `--json` |
76
77
 
77
78
  `kxm routing report` reads the same sources as `kxm improve`, and
78
79
  `routing report --json` includes the `sources`. `kxm routing benchmark` prints
79
80
  fixed placeholder figures in this build; never cite them as measured cost or
80
81
  quality. Use `kxm routing report` for recorded spend.
82
+ Estimates stay unknown until the catalog carries today's stamp, and a routing
83
+ total is null when any attempt has no cost.
81
84
 
82
85
  `--equivalent-list-cost` loads the price catalog (`.kxm/prices.yaml`, or
83
86
  `--prices <path>`) without a freshness check, so an old catalog quotes old
@@ -7,8 +7,9 @@ description: Create, drive, and inspect local KXM runs. kxm runs drive with --si
7
7
 
8
8
  `kxm run` creates a run of a project workflow in the local Runtime and starts
9
9
  the Runtime supervisor. A created run stays `created` until
10
- `kxm runs drive` executes it. Do not invent get, create, or logs verbs under
11
- `runs`.
10
+ `kxm runs drive` executes the pinned plan. Live drive is the default and spends
11
+ an admitted model; `--simulated` is the model-free producer. Do not invent get,
12
+ create, or logs verbs under `runs`.
12
13
 
13
14
  ## Commands
14
15
 
@@ -58,11 +59,11 @@ the receipt's settlement is terminal `completed`.
58
59
  - `run_workflow_unknown`: the workflow ID is not a project workflow. Run only
59
60
  IDs that `kxm workflow definitions` lists.
60
61
  - `run_handoff_required`: the Runtime does not execute a field the workflow
61
- uses, so drive fails with `runtime request failed with HTTP 409` and the run
62
- stays `preparing`. The template `default` workflow declares
63
- `limits.maxAgentTimeMs`, which is one such field. Cancel the run with
64
- `kxm runs cancel <runId>` and drive a workflow without the field, such as
65
- `first`.
62
+ uses (such as `limits.maxAgentTimeMs`, which a fresh `kxm init` no longer
63
+ writes), so drive fails with `runtime request failed with HTTP 409` and the
64
+ run stays `preparing`. Cancel the run with `kxm runs cancel <runId>` and drive
65
+ a workflow without the field, such as `first`.
66
66
 
67
67
  Listing or status alone is not proof that steps ran; a verified receipt is.
68
+ A read-only step that settles `passed` did not author a checkout change.
68
69
  `kxm workflow list` shows hub webhook runs, not these runs.
@@ -185,36 +185,41 @@ export async function cmdBackup(runtime: Runtime, options: { out?: string | unde
185
185
  try {
186
186
  const backupOptions = {
187
187
  projectRoot: runtime.cwd,
188
+ env: runtime.env,
188
189
  ...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
189
190
  };
190
191
  if (runtime.dryRun) {
191
192
  const plan = planBackup(backupOptions);
192
193
  printPlan(
193
194
  runtime,
194
- { command: "backup", outDir: plan.outDir, stores: plan.stores },
195
+ { command: "backup", outDir: plan.outDir, stores: plan.stores, files: plan.files },
195
196
  [
196
- ...plan.stores.map((store) => ({ action: "write" as const, target: join(plan.outDir, store.backupFile) })),
197
+ ...[...plan.stores, ...plan.files].map((entry) => ({ action: "write" as const, target: join(plan.outDir, entry.backupFile) })),
197
198
  { action: "write", target: join(plan.outDir, "manifest.json") },
198
199
  ],
199
- `back up ${plan.stores.length} store(s) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
200
+ `back up ${plan.stores.length} store(s) and ${plan.files.length} file(s) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
200
201
  );
201
202
  return 0;
202
203
  }
203
204
  const { manifest, outDir } = createBackup(backupOptions);
205
+ const complete = manifest.complete === true;
204
206
  const payload = {
205
- ok: true,
207
+ ok: complete,
206
208
  command: "backup",
207
209
  backupId: manifest.backupId,
208
210
  outDir,
209
211
  manifest,
210
212
  };
211
213
  const summary = [
212
- `Created SQLite backup with ${manifest.stores.length} store(s):`,
214
+ complete
215
+ ? `Created SQLite backup with ${manifest.stores.length} store(s):`
216
+ : `Backup is incomplete (${manifest.omitted?.length ?? 0} omitted); not ok:`,
213
217
  ...manifest.stores.map((s) => ` - ${s.storeId}: ${s.sourcePath} -> ${s.backupFile} (schema v${s.schemaVersion}, ${s.bytes} bytes, sha256 ${s.sha256.slice(0, 12)}...)`),
218
+ ...(manifest.omitted ?? []).map((id) => ` - omitted ${id}`),
214
219
  `Manifest: ${join(outDir, "manifest.json")}`,
215
220
  ].join("\n");
216
221
  print(runtime.io, runtime.json, payload, summary);
217
- return 0;
222
+ return complete ? 0 : 1;
218
223
  } catch (error) {
219
224
  if (error instanceof KxmConfigError) {
220
225
  print(runtime.io, runtime.json, { ok: false, command: "backup", error: "backup_failed", issues: error.issues }, `backup failed: ${error.message}`);
@@ -236,14 +241,18 @@ export async function cmdRestore(runtime: Runtime, manifestArg: string): Promise
236
241
  backupId: plan.backupId,
237
242
  manifestPath: plan.manifestPath,
238
243
  stores: plan.stores.map(({ storeId, targetPath, schemaVersion }) => ({ storeId, targetPath, schemaVersion })),
244
+ files: plan.files.map(({ id, targetPath }) => ({ id, targetPath })),
239
245
  },
240
- plan.stores.flatMap((store) => [
241
- { action: "write" as const, target: store.targetPath },
242
- ...[`${store.targetPath}-wal`, `${store.targetPath}-shm`]
243
- .filter((file) => existsSync(file))
244
- .map((file) => ({ action: "delete" as const, target: file })),
245
- ]),
246
- `restore ${plan.stores.length} store(s) from ${plan.manifestPath}; digests verified against the manifest`,
246
+ [
247
+ ...plan.stores.flatMap((store) => [
248
+ { action: "write" as const, target: store.targetPath },
249
+ ...[`${store.targetPath}-wal`, `${store.targetPath}-shm`]
250
+ .filter((file) => existsSync(file))
251
+ .map((file) => ({ action: "delete" as const, target: file })),
252
+ ]),
253
+ ...plan.files.map((file) => ({ action: "write" as const, target: file.targetPath })),
254
+ ],
255
+ `restore ${plan.stores.length} store(s) and ${plan.files.length} file(s) from ${plan.manifestPath}; digests verified against the manifest`,
247
256
  );
248
257
  return 0;
249
258
  }
@@ -20,7 +20,7 @@ import {
20
20
  type RoutingRecord,
21
21
  type RoutingRecordV2,
22
22
  } from "../routing.ts";
23
- import { loadPriceCatalog, type PriceCatalog } from "../prices.ts";
23
+ import { acknowledgePriceCatalog, loadPriceCatalog, type PriceCatalog } from "../prices.ts";
24
24
  import {
25
25
  buildImprovementReport,
26
26
  formatImprovementReport,
@@ -59,6 +59,7 @@ import {
59
59
  import {
60
60
  GUIDE_WORKFLOWS,
61
61
  parseGuideSelection,
62
+ mergeGuideRouteAdmission,
62
63
  planGuideSetup,
63
64
  renderGuideSetupFiles,
64
65
  writeGuideSetupFiles,
@@ -867,8 +868,10 @@ export async function maybeOfferGuideSetup(runtime: Runtime): Promise<void> {
867
868
  const plan = planGuideSetup({ inventory, selected });
868
869
  const files = renderGuideSetupFiles(runtime.cwd, plan);
869
870
  const report = writeGuideSetupFiles(files);
871
+ const admitted = mergeGuideRouteAdmission(runtime.cwd, plan);
870
872
  for (const file of report.written) runtime.io.stdout(`wrote ${file}\n`);
871
873
  for (const file of report.existed) runtime.io.stdout(`kept existing ${file} (not overwritten)\n`);
874
+ for (const selector of admitted) runtime.io.stdout(`admitted route ${selector}\n`);
872
875
  for (const skip of plan.skipped) {
873
876
  runtime.io.stdout(`skipped ${skip.workflow}/${skip.role}: ${skip.reason}\n`);
874
877
  }
@@ -877,6 +880,24 @@ export async function maybeOfferGuideSetup(runtime: Runtime): Promise<void> {
877
880
  }
878
881
  }
879
882
 
883
+ export async function cmdPricesAcknowledge(runtime: Runtime): Promise<number> {
884
+ try {
885
+ const catalog = acknowledgePriceCatalog(runtime.cwd);
886
+ print(runtime.io, runtime.json, {
887
+ ok: true,
888
+ command: "prices acknowledge",
889
+ date: catalog.date,
890
+ sha256: catalog.sha256,
891
+ note: "stamped the existing list as today's estimate; vendor rates were not fetched",
892
+ }, `price catalog stamped ${catalog.date} (list estimate only; vendor rates were not fetched)`);
893
+ return 0;
894
+ } catch (error) {
895
+ const message = error instanceof Error ? error.message : String(error);
896
+ print(runtime.io, runtime.json, { ok: false, command: "prices acknowledge", error: "prices_acknowledge_failed", message }, `prices acknowledge failed: ${message}`);
897
+ return 1;
898
+ }
899
+ }
900
+
880
901
  export async function cmdRoutingReport(
881
902
  runtime: Runtime,
882
903
  options: { file?: string | undefined; equivalentListCost?: boolean | undefined; listPrices?: boolean | undefined; prices?: string | undefined },
@@ -149,6 +149,7 @@ import {
149
149
  cmdConfigList,
150
150
  cmdCompletion,
151
151
  cmdCompletionInstall,
152
+ cmdPricesAcknowledge,
152
153
  cmdRoutingReport,
153
154
  cmdRoutingBenchmark,
154
155
  maybeOfferCompletionInstall,
@@ -348,7 +349,9 @@ async function dispatchAgentCliCommand(
348
349
  let client: HubClient | undefined;
349
350
  try {
350
351
  client = await ensureCliClient(runtime);
351
- const output = await cmd.execute(client, args);
352
+ // A one-shot call keeps no inbox, so `peer inbox` reads this agent's open requests from
353
+ // the hub; with a stable KXM_AGENT_NAME those include ones queued while it was offline.
354
+ const output = await cmd.execute(client, args, { hubInbox: true });
352
355
  const payload = (output && typeof output === "object" ? output : { result: output }) as object;
353
356
  print(runtime.io, runtime.json, payload, JSON.stringify(output, null, 2));
354
357
  return 0;
@@ -416,7 +419,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
416
419
  });
417
420
  });
418
421
 
419
- addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of the project hub store with a hashed manifest (Runtime stores under the user state root are not included)"))
422
+ addGlobalOptions(program.command("backup").description("Create a verified SQLite backup of the project hub store and the Runtime stores under the user state root, with a hashed manifest"))
420
423
  .option("--out <dir>", "Directory to write backup and manifest")
421
424
  .action(async function backupAction(this: Command, options: { out?: string }) {
422
425
  result.code = await cmdBackup(runtimeFrom(ctx, this), options);
@@ -442,7 +445,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
442
445
  });
443
446
  addGlobalOptions(runCmd.command("drive").description("Drive a run with live harness calls, or with the model-free simulation when --simulated is passed"))
444
447
  .argument("<runId>", "Run id")
445
- .option("--simulated", "Use the model-free simulation producer")
448
+ .option("--simulated", "Use the model-free simulation producer instead of a live model")
446
449
  .option("--wait", "Wait until a drive receipt is recorded; exits 0 only for a VERIFIED COMPLETED settlement")
447
450
  .option("--timeout-ms <n>", "Wait timeout in milliseconds (default 60000, max 600000)")
448
451
  .action(async function runDriveAction(this: Command, runId: string, options: { simulated?: boolean; wait?: boolean; timeoutMs?: string }) {
@@ -474,6 +477,13 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
474
477
  result.code = await cmdTenantStatus(runtimeFrom(ctx, this));
475
478
  });
476
479
 
480
+ const pricesCmd = addGlobalOptions(program.command("prices").description("Stamp the local list-price catalog. Estimates stay unknown until today's stamp"));
481
+ pricesCmd.helpCommand("help", "Show prices help");
482
+ addGlobalOptions(pricesCmd.command("acknowledge").description("Stamp the existing .kxm/prices.yaml list as today's estimate without fetching vendor rates"))
483
+ .action(async function pricesAcknowledgeAction(this: Command) {
484
+ result.code = await cmdPricesAcknowledge(runtimeFrom(ctx, this));
485
+ });
486
+
477
487
  const modelsCmd = addGlobalOptions(program.command("models").description("Manage model catalogs, roles, and route state"));
478
488
  modelsCmd.action(async function modelsScreenAction(this: Command) { result.code = await cmdModelsScreen(runtimeFrom(ctx, this)); });
479
489
  modelsCmd.helpCommand("help", "Show models help");
@@ -928,7 +938,7 @@ function createProgram(ctx: CliContext, result: { code: number }): Command {
928
938
  result.code = await cmdGithubWatch(runtimeFrom(ctx, this), options);
929
939
  });
930
940
 
931
- const improve = addGlobalOptions(program.command("improve").description("Propose coded-repeat candidates from this project's Runtime routing records and telemetry"));
941
+ const improve = addGlobalOptions(program.command("improve").description("Propose coded-repeat candidates from this project's Runtime routing records and telemetry. Proposals are not applied to the next run"));
932
942
  improve.helpCommand("help", "Show improve help");
933
943
  addGlobalOptions(improve.command("report", { isDefault: true }).description("Generate improvement report and candidates from routing records"))
934
944
  .option("--file <path>", "Read only this routing-record JSONL instead of the project's Runtime store and telemetry")
@@ -325,6 +325,16 @@ export class HubClient {
325
325
  }));
326
326
  }
327
327
 
328
+ /** This agent's open inbound requests (queued or delivered), oldest first. A read: nothing
329
+ * is acknowledged. */
330
+ async listInbox(): Promise<MessageRecord[]> {
331
+ if (!this.agent) throw new Error("hub client is not registered");
332
+ const result = await this.request<{ messages: MessageRecord[] }>(
333
+ `/v1/agents/${encodeURIComponent(this.agent.id)}/inbox`,
334
+ );
335
+ return result.messages;
336
+ }
337
+
328
338
  async getMessage(messageId: string): Promise<MessageRecord> {
329
339
  const result = await this.request<{ message: MessageRecord }>(`/v1/messages/${encodeURIComponent(messageId)}`);
330
340
  return result.message;
@@ -72,8 +72,12 @@ export interface SessionTokenPayload {
72
72
 
73
73
  export interface CommandExecutionContext {
74
74
  signal?: AbortSignal | undefined;
75
+ /** An event-fed inbox the caller keeps (the MCP server); `kxm_inbox` reconciles it. */
75
76
  inbox?: Map<string, MessageRecord> | undefined;
76
77
  notifiedInbox?: Set<string> | undefined;
78
+ /** The caller keeps no inbox and reads its open requests from the hub (a one-shot CLI
79
+ * call). A caller with neither this nor `inbox` activates inbound requests itself (Pi). */
80
+ hubInbox?: boolean | undefined;
77
81
  /** Inbound requests this session is handling: the Pi extension's active request, the MCP
78
82
  * server's open inbox. A request sent meanwhile is one more hop along their chain. */
79
83
  handling?: readonly Pick<MessageRecord, "hops" | "maxHops">[] | undefined;
@@ -452,7 +456,12 @@ export const AGENT_COMMANDS: readonly AgentCommand[] = [
452
456
  await reconcileInbox(client, context.inbox, context.notifiedInbox);
453
457
  return { messages: [...context.inbox.values()] };
454
458
  }
455
- return { messages: [] };
459
+ if (context?.hubInbox) return { messages: await client.listInbox() };
460
+ // Listing would offer requests this session's own activation queue is about to hand
461
+ // it as turns, and an empty list would hide them, so it refuses.
462
+ throw new Error(
463
+ "kxm_inbox is not available in this session: it activates each inbound request as a turn, and that turn's final response is the reply",
464
+ );
456
465
  },
457
466
  },
458
467
  {