@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.
- package/CHANGELOG.md +122 -0
- package/dist/agents/runAgent.d.ts +9 -0
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js +101 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts +10 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +16 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/stdio.d.ts +12 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +33 -1
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts +2 -0
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +13 -1
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts +2 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts.map +1 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js +73 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js.map +1 -0
- package/dist/contracts/schemas.d.ts +15 -0
- package/dist/contracts/schemas.d.ts.map +1 -1
- package/dist/contracts/schemas.js +18 -1
- package/dist/contracts/schemas.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +6 -4
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/index.d.ts +2 -0
- package/dist/manager/index.d.ts.map +1 -1
- package/dist/manager/index.js +1 -0
- package/dist/manager/index.js.map +1 -1
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts +2 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts.map +1 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js +208 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js.map +1 -0
- package/dist/manager/project/lifecycle.d.ts +66 -0
- package/dist/manager/project/lifecycle.d.ts.map +1 -0
- package/dist/manager/project/lifecycle.js +125 -0
- package/dist/manager/project/lifecycle.js.map +1 -0
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +7 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/session/errors.d.ts +71 -1
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +50 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/handoff/broadcast.d.ts.map +1 -1
- package/dist/session/handoff/broadcast.js +4 -5
- package/dist/session/handoff/broadcast.js.map +1 -1
- package/dist/session/handoff/single.d.ts.map +1 -1
- package/dist/session/handoff/single.js +5 -4
- package/dist/session/handoff/single.js.map +1 -1
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts +2 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts.map +1 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js +75 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js.map +1 -0
- package/dist/session/migration/filesystem.d.ts.map +1 -1
- package/dist/session/migration/filesystem.js +24 -0
- package/dist/session/migration/filesystem.js.map +1 -1
- package/dist/store/session/disk.d.ts +3 -1
- package/dist/store/session/disk.d.ts.map +1 -1
- package/dist/store/session/disk.js +64 -1
- package/dist/store/session/disk.js.map +1 -1
- package/dist/store/session/memory.d.ts +3 -1
- package/dist/store/session/memory.d.ts.map +1 -1
- package/dist/store/session/memory.js +39 -1
- package/dist/store/session/memory.js.map +1 -1
- package/dist/types/project/entity.d.ts +17 -0
- package/dist/types/project/entity.d.ts.map +1 -1
- package/dist/types/session/store.d.ts +28 -1
- package/dist/types/session/store.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/runAgent.ts +9 -0
- package/src/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.ts +127 -0
- package/src/connector/mcp/http-sse.ts +17 -0
- package/src/connector/mcp/stdio.ts +34 -1
- package/src/connector/mcp/streamable-http.ts +15 -1
- package/src/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.ts +84 -0
- package/src/contracts/schemas.ts +18 -1
- package/src/manager/agent/lifecycle.ts +6 -6
- package/src/manager/index.ts +3 -0
- package/src/manager/project/__tests__/a-closed-workspace-takes-no-work.test.ts +268 -0
- package/src/manager/project/lifecycle.ts +160 -0
- package/src/public-runtime.ts +9 -0
- package/src/session/errors.ts +85 -1
- package/src/session/handoff/broadcast.ts +4 -5
- package/src/session/handoff/single.ts +5 -4
- package/src/session/migration/__tests__/a-migrated-project-id-is-an-id.test.ts +92 -0
- package/src/session/migration/filesystem.ts +28 -0
- package/src/store/session/disk.ts +79 -2
- package/src/store/session/memory.ts +49 -2
- package/src/types/project/entity.ts +18 -0
- 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
|
|
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":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/types/session/store.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/agents/runAgent.ts
CHANGED
|
@@ -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)
|
|
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)
|
|
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
|
+
})
|
package/src/contracts/schemas.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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.
|
package/src/manager/index.ts
CHANGED
|
@@ -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'
|