@namzu/sdk 13.1.0 → 14.0.0

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 (98) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/agents/runAgent.d.ts +9 -0
  3. package/dist/agents/runAgent.d.ts.map +1 -1
  4. package/dist/agents/runAgent.js.map +1 -1
  5. package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts +2 -0
  6. package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts.map +1 -0
  7. package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js +101 -0
  8. package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js.map +1 -0
  9. package/dist/connector/mcp/http-sse.d.ts +10 -0
  10. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  11. package/dist/connector/mcp/http-sse.js +16 -0
  12. package/dist/connector/mcp/http-sse.js.map +1 -1
  13. package/dist/connector/mcp/stdio.d.ts +12 -0
  14. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  15. package/dist/connector/mcp/stdio.js +33 -1
  16. package/dist/connector/mcp/stdio.js.map +1 -1
  17. package/dist/connector/mcp/streamable-http.d.ts +2 -0
  18. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  19. package/dist/connector/mcp/streamable-http.js +13 -1
  20. package/dist/connector/mcp/streamable-http.js.map +1 -1
  21. package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts +2 -0
  22. package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts.map +1 -0
  23. package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js +73 -0
  24. package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js.map +1 -0
  25. package/dist/contracts/schemas.d.ts +15 -0
  26. package/dist/contracts/schemas.d.ts.map +1 -1
  27. package/dist/contracts/schemas.js +18 -1
  28. package/dist/contracts/schemas.js.map +1 -1
  29. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  30. package/dist/manager/agent/lifecycle.js +6 -4
  31. package/dist/manager/agent/lifecycle.js.map +1 -1
  32. package/dist/manager/index.d.ts +2 -0
  33. package/dist/manager/index.d.ts.map +1 -1
  34. package/dist/manager/index.js +1 -0
  35. package/dist/manager/index.js.map +1 -1
  36. package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts +2 -0
  37. package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts.map +1 -0
  38. package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js +208 -0
  39. package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js.map +1 -0
  40. package/dist/manager/project/lifecycle.d.ts +66 -0
  41. package/dist/manager/project/lifecycle.d.ts.map +1 -0
  42. package/dist/manager/project/lifecycle.js +125 -0
  43. package/dist/manager/project/lifecycle.js.map +1 -0
  44. package/dist/public-runtime.d.ts +2 -2
  45. package/dist/public-runtime.d.ts.map +1 -1
  46. package/dist/public-runtime.js +7 -1
  47. package/dist/public-runtime.js.map +1 -1
  48. package/dist/session/errors.d.ts +71 -1
  49. package/dist/session/errors.d.ts.map +1 -1
  50. package/dist/session/errors.js +50 -0
  51. package/dist/session/errors.js.map +1 -1
  52. package/dist/session/handoff/broadcast.d.ts.map +1 -1
  53. package/dist/session/handoff/broadcast.js +4 -5
  54. package/dist/session/handoff/broadcast.js.map +1 -1
  55. package/dist/session/handoff/single.d.ts.map +1 -1
  56. package/dist/session/handoff/single.js +5 -4
  57. package/dist/session/handoff/single.js.map +1 -1
  58. package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts +2 -0
  59. package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts.map +1 -0
  60. package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js +75 -0
  61. package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js.map +1 -0
  62. package/dist/session/migration/filesystem.d.ts.map +1 -1
  63. package/dist/session/migration/filesystem.js +24 -0
  64. package/dist/session/migration/filesystem.js.map +1 -1
  65. package/dist/store/session/disk.d.ts +3 -1
  66. package/dist/store/session/disk.d.ts.map +1 -1
  67. package/dist/store/session/disk.js +64 -1
  68. package/dist/store/session/disk.js.map +1 -1
  69. package/dist/store/session/memory.d.ts +3 -1
  70. package/dist/store/session/memory.d.ts.map +1 -1
  71. package/dist/store/session/memory.js +39 -1
  72. package/dist/store/session/memory.js.map +1 -1
  73. package/dist/types/project/entity.d.ts +17 -0
  74. package/dist/types/project/entity.d.ts.map +1 -1
  75. package/dist/types/session/store.d.ts +28 -1
  76. package/dist/types/session/store.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/agents/runAgent.ts +9 -0
  79. package/src/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.ts +127 -0
  80. package/src/connector/mcp/http-sse.ts +17 -0
  81. package/src/connector/mcp/stdio.ts +34 -1
  82. package/src/connector/mcp/streamable-http.ts +15 -1
  83. package/src/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.ts +84 -0
  84. package/src/contracts/schemas.ts +18 -1
  85. package/src/manager/agent/lifecycle.ts +6 -6
  86. package/src/manager/index.ts +3 -0
  87. package/src/manager/project/__tests__/a-closed-workspace-takes-no-work.test.ts +268 -0
  88. package/src/manager/project/lifecycle.ts +160 -0
  89. package/src/public-runtime.ts +9 -0
  90. package/src/session/errors.ts +85 -1
  91. package/src/session/handoff/broadcast.ts +4 -5
  92. package/src/session/handoff/single.ts +5 -4
  93. package/src/session/migration/__tests__/a-migrated-project-id-is-an-id.test.ts +92 -0
  94. package/src/session/migration/filesystem.ts +28 -0
  95. package/src/store/session/disk.ts +79 -2
  96. package/src/store/session/memory.ts +49 -2
  97. package/src/types/project/entity.ts +18 -0
  98. package/src/types/session/store.ts +35 -1
@@ -25,15 +25,32 @@ export interface ProjectConfig {
25
25
  sharedDeliverables?: boolean;
26
26
  retentionPolicy?: RetentionPolicy;
27
27
  }
28
+ /**
29
+ * Whether the workspace accepts new work.
30
+ *
31
+ * Owner-managed, like the status it replaces: a project does not derive
32
+ * `archived` from having no live sessions, because "empty right now" and
33
+ * "closed" are different facts and only one of them is a decision.
34
+ */
35
+ export type ProjectStatus = 'open' | 'archived';
28
36
  /**
29
37
  * Long-lived goal scope that owns shared memory, vaults, knowledge bases,
30
38
  * and deliverables across sessions. See session-hierarchy.md §4.2.
39
+ *
40
+ * `status` is the gate the ingress paths read: an archived project accepts no
41
+ * new session and no handoff. It lives here rather than on Thread because the
42
+ * project is the thing a tenant actually closes — a workspace with its own
43
+ * limits, its own environment, and its own memory — and closing it has to mean
44
+ * something to the code, not only to a listing.
31
45
  */
32
46
  export interface Project {
33
47
  id: ProjectId;
34
48
  tenantId: TenantId;
35
49
  name: string;
36
50
  config: ProjectConfig;
51
+ status: ProjectStatus;
52
+ /** CAS counter for status transitions. Mirrors `Session.ownerVersion`. */
53
+ ownerVersion: number;
37
54
  createdAt: Date;
38
55
  updatedAt: Date;
39
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../../../src/types/project/entity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC3F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC7B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,kBAAkB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IAC9C,YAAY,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAA;IAClC,oBAAoB,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAA;IAClD,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAA;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,SAAS,CAAA;IACb,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,CAAA;IACrB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;CACf"}
1
+ {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../../../src/types/project/entity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC3F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC7B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,kBAAkB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IAC9C,YAAY,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAA;IAClC,oBAAoB,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAA;IAClD,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAA;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,UAAU,CAAA;AAE/C;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,SAAS,CAAA;IACb,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,aAAa,CAAA;IACrB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;CACf"}
@@ -5,7 +5,7 @@
5
5
  * access rejects with `TenantIsolationError`. Convention #0: no speculative
6
6
  * API — extensions land alongside their consumers.
7
7
  */
8
- import type { Project } from '../../types/project/entity.js';
8
+ import type { Project, ProjectStatus } from '../../types/project/entity.js';
9
9
  import type { ActorRef } from '../../types/session/actor.js';
10
10
  import type { Session } from '../../types/session/entity.js';
11
11
  import type { SessionMessage } from '../../types/session/messages.js';
@@ -136,6 +136,23 @@ export interface SessionStore {
136
136
  * nothing else — there is no level above Project to filter by.
137
137
  */
138
138
  listProjects?(tenantId: TenantId): Promise<readonly Project[]>;
139
+ /**
140
+ * Open or close a workspace. OPTIONAL, same reasoning as the two above.
141
+ *
142
+ * Compare-and-set on {@link Project.ownerVersion}: pass the version you
143
+ * read, and a concurrent writer makes this throw `StaleProjectError`
144
+ * instead of silently winning. On success the stored version is bumped.
145
+ *
146
+ * Both directions, because a workspace is long-lived and closing one by
147
+ * mistake should not be permanent — unlike the Thread status this replaces,
148
+ * which only ever went one way. Returns `null` if the project does not
149
+ * exist; writing to another tenant's project throws.
150
+ *
151
+ * This is the store-level write. The precondition that no live session is
152
+ * attached belongs to {@link import('../../manager/project/lifecycle.js').ProjectManager},
153
+ * because the store deliberately holds no view of what is running.
154
+ */
155
+ setProjectStatus?(projectId: ProjectId, status: ProjectStatus, tenantId: TenantId, expectedOwnerVersion: number): Promise<Project | null>;
139
156
  createSession(params: CreateSessionParams, tenantId: TenantId): Promise<Session>;
140
157
  getSession(sessionId: SessionId, tenantId: TenantId): Promise<Session | null>;
141
158
  /**
@@ -184,6 +201,16 @@ export interface SessionStore {
184
201
  * ThreadStore stays unaware of session layout (Convention #0).
185
202
  */
186
203
  listSessions(threadId: ThreadId, tenantId: TenantId): Promise<readonly Session[]>;
204
+ /**
205
+ * Every Session attached to a workspace, oldest first. OPTIONAL.
206
+ *
207
+ * The project-scoped sibling of {@link SessionStore.listSessions}, and the
208
+ * one the archive precondition reads: closing a workspace has to know what
209
+ * is still running in it, and "what is running in this thread" was never
210
+ * the question — a project can hold sessions across many threads, and after
211
+ * the Thread level is removed it is the only grouping left.
212
+ */
213
+ listSessionsByProject?(projectId: ProjectId, tenantId: TenantId): Promise<readonly Session[]>;
187
214
  /**
188
215
  * Hard-delete a session. Idempotent — absent sessions succeed as a no-op.
189
216
  * Rejects with `TenantIsolationError` on cross-tenant access.
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/types/session/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/types/session/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "13.1.0",
3
+ "version": "14.0.0",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -22,6 +22,15 @@ import {
22
22
  * point: auto-generating alone would make each call its own session, which is
23
23
  * right for a one-shot and silently wrong for a conversation — the second turn
24
24
  * would start with no history and no shared budget, and nothing would say so.
25
+ *
26
+ * **A generated id is a correlation label, not a store handle.** `runAgent`
27
+ * takes no `SessionStore` and creates no records, so a `projectId` it mints
28
+ * names no `Project`. Carrying one into a store-backed `AgentManager` is
29
+ * refused at the first delegation with `Project <id> not found for tenant
30
+ * <id> — spawn rejected`, which is the enforcement site behaving correctly:
31
+ * delegation limits live on the project, and a missing project has no limits
32
+ * to read. A run that has to delegate should be given the id returned by
33
+ * `store.createProject()`.
25
34
  */
26
35
  export interface AgentIdentity {
27
36
  sessionId?: SessionId
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { MCPJsonRpcMessage } from '../../../types/connector/index.js'
4
+ import { HttpSseTransport } from '../http-sse.js'
5
+ import { StreamableHttpTransport } from '../streamable-http.js'
6
+
7
+ /**
8
+ * Handler registration was append-only and nothing ever dropped it.
9
+ *
10
+ * `MCPClient.connect()` calls `onMessage`, `onClose` and `onError` once each,
11
+ * and it is reachable again after `disconnect()` — the guard only refuses when
12
+ * the status is already `connected`. So every reconnect stacked another set on
13
+ * the last: after n cycles one inbound message dispatched to n handlers, n-1
14
+ * of them closures over dead sessions. `rejectAllPending` and `emitLifecycle`
15
+ * fired n times per close, and the stale closures held their old client state
16
+ * alive for as long as the transport object did.
17
+ *
18
+ * The two HTTP transports are exercised here because they notify inside
19
+ * `close()`. `StdioTransport` notifies from the child process's own `close`
20
+ * event, so its handlers deliberately outlive the `close()` call and are
21
+ * dropped after that event fires — clearing them earlier would leave the
22
+ * client believing it was still connected, and its next `connect()` would be
23
+ * refused with "already connected". That ordering is the reason the obvious
24
+ * one-line fix is wrong there.
25
+ */
26
+
27
+ const MESSAGE = { jsonrpc: '2.0', method: 'ping' } as MCPJsonRpcMessage
28
+
29
+ interface Transport {
30
+ close(): Promise<void>
31
+ onMessage(handler: (message: MCPJsonRpcMessage) => void): void
32
+ onClose(handler: () => void): void
33
+ }
34
+
35
+ /**
36
+ * `open` puts the transport in the state where `close()` notifies.
37
+ *
38
+ * They differ, and the difference is real rather than a test artefact:
39
+ * `StreamableHttpTransport.close()` returns early when it was never connected,
40
+ * `HttpSseTransport.close()` notifies regardless. Streamable's `connect()`
41
+ * does no I/O — it sets a flag — so opening it here reaches no network. The
42
+ * divergence itself is left alone; making the two agree changes what a host
43
+ * observes and deserves its own decision.
44
+ */
45
+ const IMPLEMENTATIONS: ReadonlyArray<
46
+ readonly [string, () => Transport, (t: Transport) => Promise<void>]
47
+ > = [
48
+ [
49
+ 'http-sse',
50
+ () => new HttpSseTransport({ url: 'http://127.0.0.1:1/mcp' } as never),
51
+ async () => {},
52
+ ],
53
+ [
54
+ 'streamable-http',
55
+ () => new StreamableHttpTransport({ url: 'http://127.0.0.1:1/mcp' } as never),
56
+ async (t) => {
57
+ await (t as unknown as { connect(): Promise<void> }).connect()
58
+ },
59
+ ],
60
+ ]
61
+
62
+ describe.each(IMPLEMENTATIONS)(
63
+ 'a %s transport forgets its listeners on close',
64
+ (_n, build, open) => {
65
+ it('notifies each close handler exactly once per registration', async () => {
66
+ const transport = build()
67
+ await open(transport)
68
+ let closes = 0
69
+ transport.onClose(() => {
70
+ closes++
71
+ })
72
+
73
+ await transport.close()
74
+ // A second registration is what a reconnect does. If the first
75
+ // survived the close, this close fires twice.
76
+ await open(transport)
77
+ transport.onClose(() => {
78
+ closes++
79
+ })
80
+ await transport.close()
81
+
82
+ expect(closes).toBe(2)
83
+ })
84
+
85
+ it('does not dispatch a message to a handler from a closed session', async () => {
86
+ // The consequence a host actually sees: work done twice, by a
87
+ // closure that belongs to a session that ended.
88
+ const seen: string[] = []
89
+ const transport = build()
90
+ await open(transport)
91
+ transport.onMessage(() => seen.push('first'))
92
+
93
+ await transport.close()
94
+ await open(transport)
95
+ transport.onMessage(() => seen.push('second'))
96
+ dispatch(transport, MESSAGE)
97
+
98
+ expect(seen).toEqual(['second'])
99
+ })
100
+
101
+ it('still notifies the handlers registered for the session being closed', async () => {
102
+ // The failure mode of clearing too eagerly: close arrives, nobody is
103
+ // told, and the client believes it is still connected.
104
+ const transport = build()
105
+ await open(transport)
106
+ const order: string[] = []
107
+ transport.onClose(() => order.push('notified'))
108
+
109
+ await transport.close()
110
+
111
+ expect(order).toEqual(['notified'])
112
+ })
113
+ },
114
+ )
115
+
116
+ /**
117
+ * Reach the private dispatch the way an inbound frame does. Both transports
118
+ * loop `messageHandlers` from their own parse path; there is no public method
119
+ * that pushes a message in, and adding one for a test would be a surface
120
+ * nobody asked for.
121
+ */
122
+ function dispatch(transport: Transport, message: MCPJsonRpcMessage): void {
123
+ const handlers = (
124
+ transport as unknown as { messageHandlers: Array<(m: MCPJsonRpcMessage) => void> }
125
+ ).messageHandlers
126
+ for (const handler of handlers) handler(message)
127
+ }
@@ -51,6 +51,23 @@ export class HttpSseTransport implements MCPTransport {
51
51
  this.abortController?.abort()
52
52
  this.abortController = null
53
53
  for (const handler of this.closeHandlers) handler()
54
+ // After the notification, never before it.
55
+ this.clearHandlers()
56
+ }
57
+
58
+ /**
59
+ * Drop every registered handler.
60
+ *
61
+ * `onMessage`/`onClose`/`onError` append, and `MCPClient.connect()` calls
62
+ * all three every time — and is reachable again after `disconnect()`. So
63
+ * without this each reconnect duplicated the set, and after n cycles one
64
+ * inbound message dispatched to n handlers, n-1 of them closures over dead
65
+ * sessions that kept their old client state alive.
66
+ */
67
+ private clearHandlers(): void {
68
+ this.messageHandlers = []
69
+ this.closeHandlers = []
70
+ this.errorHandlers = []
54
71
  }
55
72
 
56
73
  async send(message: MCPJsonRpcMessage): Promise<void> {
@@ -12,6 +12,8 @@ export class StdioTransport implements MCPTransport {
12
12
  private closeHandlers: Array<() => void> = []
13
13
  private errorHandlers: Array<(error: Error) => void> = []
14
14
  private connected = false
15
+ /** True between kill and the process's own `close` event. */
16
+ private exitPending = false
15
17
  private buffer = ''
16
18
  private log: Logger
17
19
 
@@ -41,6 +43,10 @@ export class StdioTransport implements MCPTransport {
41
43
  this.connected = false
42
44
  this.log.info(`MCP server process exited with code ${code}`)
43
45
  for (const handler of this.closeHandlers) handler()
46
+ // After the notification, not before it: this event is what tells
47
+ // `MCPClient` the session ended.
48
+ this.exitPending = false
49
+ this.clearHandlers()
44
50
  })
45
51
 
46
52
  this.process.on('error', (err) => {
@@ -55,11 +61,38 @@ export class StdioTransport implements MCPTransport {
55
61
  }
56
62
 
57
63
  async close(): Promise<void> {
58
- if (!this.process) return
64
+ if (!this.process) {
65
+ // Nothing was spawned, or the exit already ran. No `close` event is
66
+ // coming, so this is the only chance to drop what `connect()`
67
+ // registered — and without it a retry after a failed spawn stacks a
68
+ // second set of handlers on the first.
69
+ if (!this.exitPending) this.clearHandlers()
70
+ return
71
+ }
59
72
  this.connected = false
73
+ this.exitPending = true
60
74
  this.process.kill('SIGTERM')
61
75
  this.process = null
62
76
  this.buffer = ''
77
+ // The handlers deliberately survive this call. `process.on('close')`
78
+ // fires them when the process actually exits, and dropping them here
79
+ // would leave `MCPClient` believing it is still connected — so its next
80
+ // `connect()` would be refused with "already connected".
81
+ }
82
+
83
+ /**
84
+ * Drop every registered handler.
85
+ *
86
+ * `onMessage`/`onClose`/`onError` append, and `MCPClient.connect()` calls
87
+ * all three every time — and is reachable again after `disconnect()`. So
88
+ * without this each reconnect duplicated the set, and after n cycles one
89
+ * inbound message dispatched to n handlers, n-1 of them closures over dead
90
+ * sessions that kept their old client state alive.
91
+ */
92
+ private clearHandlers(): void {
93
+ this.messageHandlers = []
94
+ this.closeHandlers = []
95
+ this.errorHandlers = []
63
96
  }
64
97
 
65
98
  async send(message: MCPJsonRpcMessage): Promise<void> {
@@ -26,9 +26,23 @@ export class StreamableHttpTransport implements MCPTransport {
26
26
  }
27
27
 
28
28
  async close(): Promise<void> {
29
- if (!this.connected) return
29
+ if (!this.connected) {
30
+ // Never connected, or already closed: nothing will notify, so this is
31
+ // the only chance to drop what `connect()` registered before it failed.
32
+ this.clearHandlers()
33
+ return
34
+ }
30
35
  this.connected = false
31
36
  for (const handler of this.closeHandlers) handler()
37
+ // After the notification, never before it.
38
+ this.clearHandlers()
39
+ }
40
+
41
+ /** See {@link StdioTransport} — the same append-only handler leak. */
42
+ private clearHandlers(): void {
43
+ this.messageHandlers = []
44
+ this.closeHandlers = []
45
+ this.errorHandlers = []
32
46
  }
33
47
 
34
48
  async send(message: MCPJsonRpcMessage): Promise<void> {
@@ -0,0 +1,84 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+
6
+ import {
7
+ DefaultFilesystemMigrator,
8
+ NOOP_FILESYSTEM_MIGRATION_SINK,
9
+ } from '../../session/migration/filesystem.js'
10
+ import { generateProjectId } from '../../utils/id.js'
11
+ import { ProjectIdSchema } from '../schemas.js'
12
+
13
+ /**
14
+ * `ProjectIdSchema` was `/^prj_[a-z0-9]+$/` while the v0.2.0 filesystem
15
+ * migration minted `prj_legacy_<suffix>`. So the SDK's own public validator
16
+ * rejected ids the SDK itself had written to disk: a host that validated an
17
+ * inbound project id — the reason the schema is exported at all — refused
18
+ * every project it had migrated, with "Invalid project ID format" and no hint
19
+ * that the id came from the SDK.
20
+ *
21
+ * Both minters are driven here rather than restated. A test that spells out
22
+ * `prj_legacy_abc` as a literal would still pass if the migration changed its
23
+ * shape tomorrow; running the migration means the two cannot drift apart
24
+ * without this failing.
25
+ */
26
+
27
+ const dirs: string[] = []
28
+ afterEach(async () => {
29
+ await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true })))
30
+ dirs.length = 0
31
+ })
32
+
33
+ describe('every project id the SDK mints passes the schema the SDK exports', () => {
34
+ it('accepts what the id generator produces', () => {
35
+ // 200 draws, because the generator is random over [0-9a-z] and one
36
+ // sample proves nothing about the alphabet it can reach.
37
+ for (let i = 0; i < 200; i++) {
38
+ const id = generateProjectId()
39
+ const parsed = ProjectIdSchema.safeParse(id)
40
+ expect({ id, ok: parsed.success }).toEqual({ id, ok: true })
41
+ }
42
+ })
43
+
44
+ it('accepts what the filesystem migration produces', async () => {
45
+ const root = await mkdtemp(join(tmpdir(), 'namzu-idschema-'))
46
+ dirs.push(root)
47
+ const runDir = join(root, 'threads', 'thd_a1b2c3d4e5f6', 'runs', 'run_seed')
48
+ await mkdir(runDir, { recursive: true })
49
+ await writeFile(join(runDir, 'run.json'), JSON.stringify({ id: 'run_seed' }), 'utf-8')
50
+
51
+ const result = await new DefaultFilesystemMigrator(NOOP_FILESYSTEM_MIGRATION_SINK).migrate(root)
52
+
53
+ expect(result.kind).toBe('migrated')
54
+ expect(result.migratedThreads).toHaveLength(1)
55
+ for (const { newProjectId } of result.migratedThreads) {
56
+ const parsed = ProjectIdSchema.safeParse(newProjectId)
57
+ expect({ newProjectId, ok: parsed.success }).toEqual({ newProjectId, ok: true })
58
+ }
59
+ })
60
+
61
+ it('still refuses what no minter produces, because the id is also a directory name', () => {
62
+ // Widening to accept the legacy form must not widen to accept a path.
63
+ // Everything here would be joined onto the store root if it got through.
64
+ const refused = [
65
+ 'prj_../../etc',
66
+ 'prj_..',
67
+ 'prj_a/b',
68
+ 'prj_a\\b',
69
+ 'prj_',
70
+ 'prj_legacy_',
71
+ 'prj_ABC',
72
+ 'prj_a-b',
73
+ 'prj_a b',
74
+ 'proj_abc',
75
+ 'thd_abc',
76
+ '',
77
+ ]
78
+
79
+ for (const candidate of refused) {
80
+ const parsed = ProjectIdSchema.safeParse(candidate)
81
+ expect({ candidate, ok: parsed.success }).toEqual({ candidate, ok: false })
82
+ }
83
+ })
84
+ })
@@ -1,6 +1,23 @@
1
1
  import { z } from 'zod'
2
2
 
3
- export const ProjectIdSchema = z.string().regex(/^prj_[a-z0-9]+$/, 'Invalid project ID format')
3
+ /**
4
+ * The two shapes the SDK mints, and nothing else.
5
+ *
6
+ * `prj_<12 lowercase alphanumerics>` comes from `generateProjectId()`.
7
+ * `prj_legacy_<suffix>` comes from the v0.2.0 filesystem migration, which
8
+ * synthesises a project per legacy `thd_*` folder. The `legacy_` segment is
9
+ * spelled out rather than covered by a permissive character class, because a
10
+ * project id is also a directory name: everything this accepts is joined to a
11
+ * path, so the set it accepts is a containment boundary, not a formatting
12
+ * preference.
13
+ *
14
+ * A host that supplies its own `SessionStore` and mints its own ids should
15
+ * validate with its own schema — `ProjectId` is `prj_${string}` at the type
16
+ * level and this schema is deliberately narrower.
17
+ */
18
+ export const ProjectIdSchema = z
19
+ .string()
20
+ .regex(/^prj_(legacy_)?[a-z0-9]+$/, 'Invalid project ID format')
4
21
  export const RunIdSchema = z.string().regex(/^run_[a-z0-9]+$/, 'Invalid run ID format')
5
22
  export const MessageIdSchema = z.string().regex(/^msg_[a-z0-9]+$/, 'Invalid message ID format')
6
23
 
@@ -36,6 +36,7 @@ import { ZERO_COST } from '../../utils/cost.js'
36
36
  import { toErrorMessage } from '../../utils/error.js'
37
37
  import { generateTaskId } from '../../utils/id.js'
38
38
  import { type Logger, getRootLogger } from '../../utils/logger.js'
39
+ import { requireOpenProject } from '../project/lifecycle.js'
39
40
  import type { ThreadManager } from '../thread/lifecycle.js'
40
41
 
41
42
  /**
@@ -593,12 +594,11 @@ export class AgentManager {
593
594
  )
594
595
  }
595
596
 
596
- const project = await store.getProject(context.projectId, context.tenantId)
597
- if (!project) {
598
- throw new Error(
599
- `Project ${context.projectId} not found for tenant ${context.tenantId} — spawn rejected`,
600
- )
601
- }
597
+ // Same read that loads the limits, now also the gate: an archived
598
+ // workspace accepts no new session. It replaces a bare `getProject` +
599
+ // null check rather than adding a round-trip, because a gate that costs
600
+ // something is a gate someone eventually moves.
601
+ const project = await requireOpenProject(store, context.projectId, context.tenantId, 'spawn')
602
602
 
603
603
  // Capacity: depth + width. Depth uses the parent session's ancestry
604
604
  // chain; width counts existing direct children of the parent.
@@ -20,3 +20,6 @@ export { ThreadManager } from './thread/lifecycle.js'
20
20
  export type { ThreadManagerDeps } from './thread/lifecycle.js'
21
21
 
22
22
  export { AgentManager } from './agent/lifecycle.js'
23
+
24
+ export { ProjectManager, requireOpenProject } from './project/lifecycle.js'
25
+ export type { ProjectManagerDeps } from './project/lifecycle.js'