@nanobpm/urban 0.92.0 → 0.92.1

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.
@@ -41,7 +41,6 @@ import { resolveBindMode } from "../manifest.js";
41
41
  import { json, makeRouter } from "../router.js";
42
42
  import { API_BASE, readApiBinding } from "./api.js";
43
43
  import { resolveAppPath } from "./datasource.js";
44
- import { InstanceStateStore } from "./instance-state-store.js";
45
44
  import { OpenUserTasksStore } from "./open-user-tasks-store.js";
46
45
  /** The mount path of the MCP endpoint — a sibling of `/app/api` / `/app/agent`, mounted
47
46
  * unconditionally so an MCP client always has a stable address to connect to. */
@@ -682,18 +681,19 @@ function buildDebugTools(app) {
682
681
  },
683
682
  {
684
683
  name: `${DEBUG_PREFIX}instance_state`,
685
- description: "Projection (urban_instance_state, ADR 0065): the canonical engine-lifecycle state for an instance.",
684
+ description: "Engine truth: the live lifecycle state of a process instance (ACTIVE/COMPLETED/TERMINATED) via searchProcessInstances, plus `waitingOnHuman` from its open user tasks. `null` means the engine has no such instance (not merely 'not yet terminal').",
686
685
  inputSchema: {
687
686
  type: "object",
688
687
  properties: { processInstanceKey: OPTIONAL_STRING },
689
688
  required: ["processInstanceKey"],
690
689
  },
691
- run: (args) => {
690
+ run: async (args) => {
692
691
  const key = requireString(args, "processInstanceKey");
693
- const db = projectionDb();
694
- if (!db)
695
- return Promise.resolve(null);
696
- return Promise.resolve(new InstanceStateStore(db).getState(key) ?? null);
692
+ const [snapshot] = await app.engine.searchProcessInstances({ processInstanceKeys: [key] });
693
+ if (!snapshot)
694
+ return null;
695
+ const openTasks = await app.engine.openUserTasks({ processInstanceKey: key });
696
+ return { processInstanceKey: key, state: snapshot.state, waitingOnHuman: openTasks.length > 0 };
697
697
  },
698
698
  },
699
699
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.92.0",
3
+ "version": "0.92.1",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,7 +29,6 @@ import { makeRouter } from "../router.ts";
29
29
  import { mountApi } from "./api.ts";
30
30
  import { makeGateway } from "./gateway.ts";
31
31
  import { DataLayer, type ProvisionedSource } from "./datasource.ts";
32
- import { InstanceStateStore } from "./instance-state-store.ts";
33
32
  import { OpenUserTasksStore } from "./open-user-tasks-store.ts";
34
33
  import { evictExcessSessions, isLoopbackRequest, missingRequiredArgs, mountMcp, newSessionId, readHeader, readMcpConfig } from "./mcp.ts";
35
34
  import { collectMcpToolProjection, diffMcpToolProjection, parseSpec } from "../../../openapi/spec.ts";
@@ -769,13 +768,11 @@ test("the reserved-namespace drop warns once, not on every tools/list and tools/
769
768
  assert.equal(drops.length, 1, "the reserved-namespace drop must warn exactly once across many requests");
770
769
  });
771
770
 
772
- test("the projection debug tools read the ADR 0065 canonical stores", async () => {
771
+ test("urban_debug_open_user_tasks reads the ADR 0065 canonical projection", async () => {
773
772
  const dir = await mkdtemp(join(tmpdir(), "urban-mcp-proj-"));
774
773
  const host = createNodeHost({ cwd: dir, log: () => {} });
775
774
  const db: SqliteDb = host.openSqlite(join(dir, "test.db"));
776
775
  try {
777
- new InstanceStateStore(db, { clock: { now: () => 0 } }).ensureSchema();
778
- new InstanceStateStore(db, { clock: { now: () => 0 } }).recordState("pi-7", "ACTIVE");
779
776
  const openStore = new OpenUserTasksStore(db, { clock: { now: () => 0 } });
780
777
  openStore.ensureSchema();
781
778
  openStore.recordOpenTask("pi-7", { userTaskKey: "ut-1", elementId: "Task_A" });
@@ -792,13 +789,6 @@ test("the projection debug tools read the ADR 0065 canonical stores", async () =
792
789
  const { router } = buildHarness({ data });
793
790
  const session = await connect(router);
794
791
 
795
- const stateCall = await rpc(router, session, "tools/call", {
796
- name: "urban_debug_instance_state",
797
- arguments: { processInstanceKey: "pi-7" },
798
- });
799
- const state = JSON.parse(toolContentText(stateCall.result));
800
- assert.equal(Reflect.get(state, "state"), "ACTIVE");
801
-
802
792
  const tasksCall = await rpc(router, session, "tools/call", {
803
793
  name: "urban_debug_open_user_tasks",
804
794
  arguments: { processInstanceKey: "pi-7" },
@@ -812,6 +802,66 @@ test("the projection debug tools read the ADR 0065 canonical stores", async () =
812
802
  }
813
803
  });
814
804
 
805
+ test("urban_debug_instance_state returns engine truth for a live (ACTIVE) instance", async () => {
806
+ const snapshot: ProcessInstanceSnapshot = { processInstanceKey: "pi-7", state: "ACTIVE" };
807
+ const engine = fakeEngine({
808
+ searchProcessInstances: async (filter) => {
809
+ assert.deepEqual(filter?.processInstanceKeys, ["pi-7"]);
810
+ return [snapshot];
811
+ },
812
+ openUserTasks: async () => [],
813
+ });
814
+ const { router } = buildHarness({ engine });
815
+ const session = await connect(router);
816
+
817
+ const call = await rpc(router, session, "tools/call", {
818
+ name: "urban_debug_instance_state",
819
+ arguments: { processInstanceKey: "pi-7" },
820
+ });
821
+ const state = JSON.parse(toolContentText(call.result));
822
+ assert.deepEqual(state, { processInstanceKey: "pi-7", state: "ACTIVE", waitingOnHuman: false });
823
+ });
824
+
825
+ test("urban_debug_instance_state reports waitingOnHuman from open user tasks", async () => {
826
+ const snapshot: ProcessInstanceSnapshot = { processInstanceKey: "pi-7", state: "ACTIVE" };
827
+ const engine = fakeEngine({
828
+ searchProcessInstances: async () => [snapshot],
829
+ openUserTasks: async (filter) => {
830
+ assert.equal(filter?.processInstanceKey, "pi-7");
831
+ return [{ userTaskKey: "ut-1", processInstanceKey: "pi-7" }];
832
+ },
833
+ });
834
+ const { router } = buildHarness({ engine });
835
+ const session = await connect(router);
836
+
837
+ const call = await rpc(router, session, "tools/call", {
838
+ name: "urban_debug_instance_state",
839
+ arguments: { processInstanceKey: "pi-7" },
840
+ });
841
+ const state = JSON.parse(toolContentText(call.result));
842
+ assert.deepEqual(state, { processInstanceKey: "pi-7", state: "ACTIVE", waitingOnHuman: true });
843
+ });
844
+
845
+ test("urban_debug_instance_state returns null when the engine has no such instance", async () => {
846
+ let openUserTasksCalled = false;
847
+ const engine = fakeEngine({
848
+ searchProcessInstances: async () => [],
849
+ openUserTasks: async () => {
850
+ openUserTasksCalled = true;
851
+ return [];
852
+ },
853
+ });
854
+ const { router } = buildHarness({ engine });
855
+ const session = await connect(router);
856
+
857
+ const call = await rpc(router, session, "tools/call", {
858
+ name: "urban_debug_instance_state",
859
+ arguments: { processInstanceKey: "nope" },
860
+ });
861
+ assert.equal(toolContentText(call.result), "null");
862
+ assert.equal(openUserTasksCalled, false, "must not probe user tasks for an instance the engine doesn't know");
863
+ });
864
+
815
865
  // ---- mutation guard (Slice 3) -----------------------------------------------------------------
816
866
 
817
867
  // A repeated credential header must read the SAME under the MCP guard as under OpenAPI route
@@ -82,7 +82,6 @@ import { resolveBindMode } from "../manifest.ts";
82
82
  import { json, makeRouter, type Route } from "../router.ts";
83
83
  import { API_BASE, readApiBinding } from "./api.ts";
84
84
  import { resolveAppPath } from "./datasource.ts";
85
- import { InstanceStateStore } from "./instance-state-store.ts";
86
85
  import { OpenUserTasksStore } from "./open-user-tasks-store.ts";
87
86
 
88
87
  /** The mount path of the MCP endpoint — a sibling of `/app/api` / `/app/agent`, mounted
@@ -785,17 +784,19 @@ function buildDebugTools(app: AppApi): DebugTool[] {
785
784
  },
786
785
  {
787
786
  name: `${DEBUG_PREFIX}instance_state`,
788
- description: "Projection (urban_instance_state, ADR 0065): the canonical engine-lifecycle state for an instance.",
787
+ description:
788
+ "Engine truth: the live lifecycle state of a process instance (ACTIVE/COMPLETED/TERMINATED) via searchProcessInstances, plus `waitingOnHuman` from its open user tasks. `null` means the engine has no such instance (not merely 'not yet terminal').",
789
789
  inputSchema: {
790
790
  type: "object",
791
791
  properties: { processInstanceKey: OPTIONAL_STRING },
792
792
  required: ["processInstanceKey"],
793
793
  },
794
- run: (args) => {
794
+ run: async (args) => {
795
795
  const key = requireString(args, "processInstanceKey");
796
- const db = projectionDb();
797
- if (!db) return Promise.resolve(null);
798
- return Promise.resolve(new InstanceStateStore(db).getState(key) ?? null);
796
+ const [snapshot] = await app.engine.searchProcessInstances({ processInstanceKeys: [key] });
797
+ if (!snapshot) return null;
798
+ const openTasks = await app.engine.openUserTasks({ processInstanceKey: key });
799
+ return { processInstanceKey: key, state: snapshot.state, waitingOnHuman: openTasks.length > 0 };
799
800
  },
800
801
  },
801
802
  {