@nanobpm/nano-workforce 0.178.3 → 0.178.4

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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.178.4](https://github.com/nanobpm/nano-workforce/compare/v0.178.3...v0.178.4) (2026-09-03)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **mcp:** align session-selfheal e2e with urban 0.90.1 spec-conformant 404/-32001 ([#724](https://github.com/nanobpm/nano-workforce/issues/724)) ([5f54f3c](https://github.com/nanobpm/nano-workforce/commit/5f54f3ce42bbcbf30715bf17829e3f9ddd9cf793)), closes [#715](https://github.com/nanobpm/nano-workforce/issues/715)
6
+
1
7
  ## [0.178.3](https://github.com/nanobpm/nano-workforce/compare/v0.178.2...v0.178.3) (2026-09-03)
2
8
 
3
9
  ### Bug Fixes
@@ -3,20 +3,25 @@
3
3
  // WHAT THIS PINS
4
4
  // ==============
5
5
  // The runtime-served MCP surface (`/app/mcp`, ADR 0067) is a **stateful streamable-HTTP** transport:
6
- // every `tools/call` MUST carry a valid `mcp-session-id`, and a call with a missing / stale / evicted
7
- // / deleted session id is refused with JSON-RPC `-32000 "Bad Request: no valid session id, and not an
8
- // initialize request."` (mcp.ts). In the field (issue #715) a single hiccup a heavy-tool timeout,
9
- // an idle drop, a proxy reset, or LRU eviction (`MAX_SESSIONS`) loses the session and then bricks
10
- // the ENTIRE surface for a client that does not re-`initialize`: every subsequent tool reads as
11
- // "tool does not exist". The stateless/resumable transport that would remove the session dependency
12
- // lives in the urban runtime and is tracked upstream (nano-ide#488); until it lands, the
13
- // **workforce-visible requirement** (this issue) is that the surface is RECOVERABLE — a client that
14
- // re-`initialize`s after a `-32000` gets the WHOLE surface back in one round trip, not a degraded one.
6
+ // every `tools/call` MUST carry a valid `mcp-session-id`. Per the MCP Streamable-HTTP spec the runtime
7
+ // distinguishes two refusals (urban 0.90.1): a session id that is SUPPLIED but unknown / stale /
8
+ // evicted / deleted is a terminated session, answered `404` with JSON-RPC `-32001 "Session not found:
9
+ // unknown or expired mcp-session-id."` so the client transparently re-initializes; only a GENUINELY
10
+ // MISSING id (no header at all) is the `400` / `-32000 "Bad Request: no valid session id, and not an
11
+ // initialize request."` bad-client case (mcp.ts). In the field (issue #715) a single hiccup — a
12
+ // heavy-tool timeout, an idle drop, a proxy reset, or LRU eviction (`MAX_SESSIONS`) loses the
13
+ // session and then bricks the ENTIRE surface for a client that does not re-`initialize`: every
14
+ // subsequent tool reads as "tool does not exist". The stateless/resumable transport that would remove
15
+ // the session dependency lives in the urban runtime and is tracked upstream (nano-ide#488); until it
16
+ // lands, the **workforce-visible requirement** (this issue) is that the surface is RECOVERABLE — a
17
+ // client that re-`initialize`s after a gone-session refusal gets the WHOLE surface back in one round
18
+ // trip, not a degraded one.
15
19
  //
16
20
  // This is the acceptance regression: "kill the session mid-flight and assert the next call still
17
21
  // works." It kills the session two faithful ways — an unknown/stale id, and a server-side `DELETE`
18
- // (the spec session-termination verb) of a live id — asserts each bricks a call with the exact
19
- // `-32000` signature, then asserts a single client `reinitialize()` fully restores the surface
22
+ // (the spec session-termination verb) of a live id — asserts each bricks a call with a session-gone
23
+ // signature (a supplied-unknown id `404` / `-32001`, a DELETEd id → the SDK transport's `Session
24
+ // not found`), then asserts a single client `reinitialize()` fully restores the surface
20
25
  // (a working `tools/call` AND the complete `tools/list`). If a future runtime makes the transport
21
26
  // stateless/resumable (nano-ide#488), the stale-id call simply stops erroring — this test then
22
27
  // tightens to that stronger contract with a one-line change, never silently passing on a regression.
@@ -27,14 +32,12 @@ import { randomUUID } from "node:crypto";
27
32
  import { after, before, describe, test } from "node:test";
28
33
  import { bootMcpHarness, type McpHarness } from "./support/mcp-harness.ts";
29
34
 
30
- /** The exact runtime signature of a lost/absent session (mcp.ts). A recovered surface must NOT
31
- * answer with this after a re-`initialize`. */
32
- const NO_SESSION_SIGNATURE = "no valid session id";
33
-
34
- /** Any "the session is gone" refusal: the runtime's own `-32000` "no valid session id" (unknown id)
35
- * OR the SDK transport's "Session not found" (a terminated/DELETEd id). Either proves the call was
36
- * refused because the session no longer exists — the field failure #715 gap 1 is about. */
37
- const SESSION_GONE = /no valid session id|session not found/i;
35
+ /** Any "the session is gone" refusal, in either faithful form: the runtime's `-32001` "Session not
36
+ * found: unknown or expired …" for a SUPPLIED but unknown / stale / evicted / DELETEd id (urban
37
+ * ≥ 0.90.1), OR the SDK transport's own "Session not found" for a terminated id. Either proves the
38
+ * call was refused because the session no longer exists — the field failure #715 gap 1 is about. A
39
+ * recovered surface must NOT answer with this after a re-`initialize`. */
40
+ const SESSION_GONE = /session not found|unknown or expired|no valid session id/i;
38
41
 
39
42
  /** A safe, side-effect-free read used as the "does the surface answer?" probe. */
40
43
  const PROBE_TOOL = "getVersion";
@@ -53,14 +56,22 @@ describe("#715 gap 1 — a lost MCP session self-heals on client re-initialize",
53
56
  assert(!res.isError, `baseline ${PROBE_TOOL} should succeed on a live session: ${res.text}`);
54
57
  });
55
58
 
56
- test("an unknown/stale session id bricks a call with -32000, and re-initialize restores the surface", async () => {
59
+ test("an unknown/stale session id bricks a call with 404 / -32001, and re-initialize restores the surface", async () => {
57
60
  // A stale id models an evicted (LRU / idle-dropped) or proxy-reset session the client still holds.
58
61
  const staleId = `stale-${randomUUID()}`;
59
62
  const bricked = await h.callToolAs(staleId, PROBE_TOOL);
60
63
  assert(bricked.isError, "a call carrying a stale session id must be refused, not answered");
64
+ // A SUPPLIED-but-unknown id is a terminated session: per the MCP Streamable-HTTP spec the runtime
65
+ // answers 404 / -32001 "Session not found: unknown or expired …" (urban ≥ 0.90.1) so the client
66
+ // transparently re-initializes, rather than the 400 / -32000 reserved for a genuinely missing id.
67
+ assert.equal(
68
+ bricked.httpStatus,
69
+ 404,
70
+ `a stale (supplied-but-unknown) session id must be refused 404 so the client re-initializes, got HTTP ${bricked.httpStatus}: ${bricked.text}`,
71
+ );
61
72
  assert(
62
- bricked.text.includes(NO_SESSION_SIGNATURE),
63
- `a stale-session call must fail with the "${NO_SESSION_SIGNATURE}" signature, got: ${bricked.text}`,
73
+ SESSION_GONE.test(bricked.text),
74
+ `a stale-session call must fail with a session-gone signature, got: ${bricked.text}`,
64
75
  );
65
76
 
66
77
  // Client self-heal: re-run the handshake. The pinned runtime is stateful, so this is how a real
@@ -72,8 +83,8 @@ describe("#715 gap 1 — a lost MCP session self-heals on client re-initialize",
72
83
  const healed = await h.callTool(PROBE_TOOL);
73
84
  assert(!healed.isError, `after reinitialize the surface must answer again: ${healed.text}`);
74
85
  assert(
75
- !healed.text.includes(NO_SESSION_SIGNATURE),
76
- "a healed call must not still report a missing session",
86
+ !SESSION_GONE.test(healed.text),
87
+ "a healed call must not still report a gone session",
77
88
  );
78
89
 
79
90
  // And the FULL projected catalogue is restored — the field failure was "every tool vanished".
@@ -95,9 +106,10 @@ describe("#715 gap 1 — a lost MCP session self-heals on client re-initialize",
95
106
  const status = await h.deleteSession(killedId);
96
107
  assert(status < 500, `DELETE session should not server-error, got ${status}`);
97
108
 
98
- // The now-terminated id bricks a call — the mid-flight hiccup. A terminated session surfaces the
99
- // SDK transport's "Session not found"; an unknown id surfaces the runtime's "no valid session id"
100
- // — both mean the session is gone and the call was refused, not answered.
109
+ // The now-terminated id bricks a call — the mid-flight hiccup. Both a DELETEd/terminated id and a
110
+ // supplied-but-unknown id now surface a "Session not found" gone-signal (the SDK transport's own,
111
+ // or the runtime's 404 / -32001 "unknown or expired") either means the session is gone and the
112
+ // call was refused, not answered.
101
113
  const bricked = await h.callToolAs(killedId, PROBE_TOOL);
102
114
  assert(bricked.isError, "a call on a DELETEd session id must be refused");
103
115
  assert(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.178.3",
3
+ "version": "0.178.4",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@nanobpm/agentic": "^0.12.0",
68
- "@nanobpm/urban": "^0.90.0",
68
+ "@nanobpm/urban": "^0.90.1",
69
69
  "bpmn-auto-layout": "^2.0.0-alpha.2"
70
70
  },
71
71
  "devDependencies": {