@namzu/sdk 48.3.0 → 49.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 +45 -0
- package/dist/config/registry.d.ts +2 -0
- package/dist/config/registry.d.ts.map +1 -1
- package/dist/config/registry.js +23 -3
- package/dist/config/registry.js.map +1 -1
- package/dist/connector/mcp/discovery.d.ts +9 -5
- package/dist/connector/mcp/discovery.d.ts.map +1 -1
- package/dist/connector/mcp/discovery.js +16 -8
- package/dist/connector/mcp/discovery.js.map +1 -1
- package/dist/connector/mcp/mcp-toolset.d.ts +3 -0
- package/dist/connector/mcp/mcp-toolset.d.ts.map +1 -1
- package/dist/connector/mcp/mcp-toolset.js +20 -2
- package/dist/connector/mcp/mcp-toolset.js.map +1 -1
- package/dist/connector/mcp/reconnect.d.ts +2 -0
- package/dist/connector/mcp/reconnect.d.ts.map +1 -1
- package/dist/connector/mcp/reconnect.js +36 -5
- package/dist/connector/mcp/reconnect.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +14 -10
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +178 -41
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/prompt/resident-learning.js +2 -2
- package/dist/prompt/resident-learning.js.map +1 -1
- package/dist/prompt/resident-step.d.ts +2 -2
- package/dist/prompt/resident-step.js +4 -4
- package/dist/prompt/resident-step.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/screen.d.ts.map +1 -1
- package/dist/registry/tool/screen.js +33 -2
- package/dist/registry/tool/screen.js.map +1 -1
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +41 -12
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/index.d.ts +6 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +20 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +32 -8
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +2 -0
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +4 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +7 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.d.ts +2 -0
- package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.js +5 -1
- package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/memory/disk.d.ts +7 -2
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +58 -5
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/index.d.ts +9 -0
- package/dist/store/memory/index.d.ts.map +1 -1
- package/dist/store/memory/index.js +43 -5
- package/dist/store/memory/index.js.map +1 -1
- package/dist/store/memory/markdown.d.ts +7 -2
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +33 -0
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/memory.d.ts +7 -2
- package/dist/store/memory/memory.d.ts.map +1 -1
- package/dist/store/memory/memory.js +34 -0
- package/dist/store/memory/memory.js.map +1 -1
- package/dist/store/memory/revision.d.ts +16 -0
- package/dist/store/memory/revision.d.ts.map +1 -0
- package/dist/store/memory/revision.js +144 -0
- package/dist/store/memory/revision.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +6 -4
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +46 -8
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -0
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +24 -10
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/coordinator/wait-with-idle-bound.d.ts.map +1 -1
- package/dist/tools/coordinator/wait-with-idle-bound.js +4 -0
- package/dist/tools/coordinator/wait-with-idle-bound.js.map +1 -1
- package/dist/tools/memory/delete.d.ts.map +1 -1
- package/dist/tools/memory/delete.js +38 -3
- package/dist/tools/memory/delete.js.map +1 -1
- package/dist/tools/memory/read.d.ts.map +1 -1
- package/dist/tools/memory/read.js +38 -2
- package/dist/tools/memory/read.js.map +1 -1
- package/dist/tools/memory/search.d.ts.map +1 -1
- package/dist/tools/memory/search.js +38 -7
- package/dist/tools/memory/search.js.map +1 -1
- package/dist/tools/memory/update.d.ts.map +1 -1
- package/dist/tools/memory/update.js +30 -3
- package/dist/tools/memory/update.js.map +1 -1
- package/dist/toolsets/manager.d.ts +13 -0
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +60 -1
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/turn/memory-recall.d.ts +2 -0
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +3 -1
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/types/memory/index.d.ts +29 -0
- package/dist/types/memory/index.d.ts.map +1 -1
- package/dist/types/memory/index.js +6 -0
- package/dist/types/memory/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +12 -2
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +5 -2
- package/dist/types/plugin/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config/registry.ts +24 -3
- package/src/connector/mcp/discovery.ts +29 -8
- package/src/connector/mcp/mcp-toolset.ts +25 -4
- package/src/connector/mcp/reconnect.ts +34 -4
- package/src/plugin/lifecycle.ts +217 -46
- package/src/prompt/resident-learning.ts +2 -2
- package/src/prompt/resident-step.ts +4 -4
- package/src/public-runtime.ts +2 -0
- package/src/registry/tool/screen.ts +34 -2
- package/src/runtime/jobs/registry.ts +39 -15
- package/src/runtime/query/index.ts +24 -1
- package/src/runtime/query/iteration/index.ts +35 -10
- package/src/runtime/query/iteration/outstanding-work.ts +2 -0
- package/src/runtime/query/iteration/phases/context.ts +4 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -1
- package/src/runtime/query/iteration/step-shaping.ts +9 -1
- package/src/store/index.ts +1 -0
- package/src/store/memory/disk.ts +82 -7
- package/src/store/memory/index.ts +66 -7
- package/src/store/memory/markdown.ts +54 -2
- package/src/store/memory/memory.ts +52 -2
- package/src/store/memory/revision.ts +158 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +45 -7
- package/src/tools/builtins/wait-for-job.ts +25 -11
- package/src/tools/coordinator/wait-with-idle-bound.ts +3 -0
- package/src/tools/memory/delete.ts +33 -3
- package/src/tools/memory/read.ts +35 -2
- package/src/tools/memory/search.ts +38 -7
- package/src/tools/memory/update.ts +28 -3
- package/src/toolsets/manager.ts +74 -1
- package/src/turn/memory-recall.ts +5 -1
- package/src/types/memory/index.ts +43 -0
- package/src/types/plugin/index.ts +11 -6
|
@@ -55,10 +55,10 @@ export class MCPToolDiscovery {
|
|
|
55
55
|
private log: Logger
|
|
56
56
|
private options: MCPToolDiscoveryOptions
|
|
57
57
|
/**
|
|
58
|
-
* Last admitted tool set per
|
|
58
|
+
* Last admitted tool set per owner identity, for drift detection.
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Plain discovery uses the server name; plugin discovery supplies its
|
|
61
|
+
* qualified source id. A client id is minted per connection, so on the path a
|
|
62
62
|
* real MCP server actually takes — a plugin enabling, connecting, and
|
|
63
63
|
* being disabled again — every discovery was the first one that id had
|
|
64
64
|
* ever seen, and drift could not fire however many times the server
|
|
@@ -119,9 +119,15 @@ export class MCPToolDiscovery {
|
|
|
119
119
|
return results
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
async discoverFrom(
|
|
122
|
+
async discoverFrom(
|
|
123
|
+
client: MCPClient,
|
|
124
|
+
options?: { readonly isCurrent?: () => boolean },
|
|
125
|
+
): Promise<MCPDiscoveredTool[]> {
|
|
123
126
|
const state = client.getState()
|
|
124
127
|
const advertised = await client.listTools()
|
|
128
|
+
// A newer listing can finish before this one. Do not let the older
|
|
129
|
+
// generation replace the baseline or emit a drift event after that.
|
|
130
|
+
if (options?.isCurrent?.() === false) return []
|
|
125
131
|
|
|
126
132
|
// The boundary. Without it the REMOTE side decides what enters the
|
|
127
133
|
// agent's registry, which inverts least privilege: a server could
|
|
@@ -144,7 +150,7 @@ export class MCPToolDiscovery {
|
|
|
144
150
|
})
|
|
145
151
|
}
|
|
146
152
|
|
|
147
|
-
this.detectDrift(client.id, state.serverName, admitted)
|
|
153
|
+
this.detectDrift(state.serverName, client.id, state.serverName, admitted)
|
|
148
154
|
|
|
149
155
|
return admitted.map((tool) => ({
|
|
150
156
|
tool,
|
|
@@ -153,6 +159,16 @@ export class MCPToolDiscovery {
|
|
|
153
159
|
}))
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
/** Record the exact already-admitted snapshot another owner mounted. */
|
|
163
|
+
recordAdmittedTools(
|
|
164
|
+
identity: string,
|
|
165
|
+
clientId: string,
|
|
166
|
+
serverName: string,
|
|
167
|
+
admitted: readonly MCPToolDefinition[],
|
|
168
|
+
): void {
|
|
169
|
+
this.detectDrift(identity, clientId, serverName, [...admitted])
|
|
170
|
+
}
|
|
171
|
+
|
|
156
172
|
/**
|
|
157
173
|
* The prompts a server publishes, through the same admission gate its
|
|
158
174
|
* tools go through.
|
|
@@ -251,9 +267,14 @@ export class MCPToolDiscovery {
|
|
|
251
267
|
})
|
|
252
268
|
}
|
|
253
269
|
|
|
254
|
-
private detectDrift(
|
|
255
|
-
|
|
256
|
-
|
|
270
|
+
private detectDrift(
|
|
271
|
+
identity: string,
|
|
272
|
+
clientId: string,
|
|
273
|
+
serverName: string,
|
|
274
|
+
admitted: MCPToolDefinition[],
|
|
275
|
+
): void {
|
|
276
|
+
const previous = this.lastSeen.get(identity)
|
|
277
|
+
this.lastSeen.set(identity, admitted)
|
|
257
278
|
if (!previous) return
|
|
258
279
|
|
|
259
280
|
const drift = diffTools(previous, admitted)
|
|
@@ -5,7 +5,11 @@ import { NAMZU } from '../../constants/telemetry/index.js'
|
|
|
5
5
|
import { ToolsetConflictError } from '../../toolsets/combine.js'
|
|
6
6
|
import type { Toolset, ToolsetAvailability } from '../../toolsets/types.js'
|
|
7
7
|
import { deferred } from '../../toolsets/wrappers.js'
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
MCPResource,
|
|
10
|
+
MCPServerCapabilities,
|
|
11
|
+
MCPToolDefinition,
|
|
12
|
+
} from '../../types/connector/index.js'
|
|
9
13
|
import type { ToolContext, ToolDefinition, ToolResult } from '../../types/tool/index.js'
|
|
10
14
|
import type { ToolSource } from '../../types/toolset/index.js'
|
|
11
15
|
import { toErrorMessage } from '../../utils/error.js'
|
|
@@ -103,6 +107,8 @@ export interface MCPToolsetOptions {
|
|
|
103
107
|
* added and removed names apply at the next refresh boundary.
|
|
104
108
|
*/
|
|
105
109
|
readonly onDrift?: (event: { serverName: string; clientId: string; drift: MCPToolDrift }) => void
|
|
110
|
+
/** The exact admitted server listing processed at construction and each committed refresh. */
|
|
111
|
+
readonly onToolsAdmitted?: (tools: readonly MCPToolDefinition[]) => void
|
|
106
112
|
/** Current tool, prompt and resource names refused by this server's policy. */
|
|
107
113
|
readonly onRefused?: (event: {
|
|
108
114
|
serverName: string
|
|
@@ -320,14 +326,20 @@ export async function mcpToolset(
|
|
|
320
326
|
let promptDefs: ToolDefinition[] = []
|
|
321
327
|
let mainTools: ToolDefinition[] = []
|
|
322
328
|
let closed = false
|
|
329
|
+
let toolRefreshGeneration = 0
|
|
330
|
+
let admittedToolSnapshot: readonly MCPToolDefinition[] = []
|
|
323
331
|
const listeners = new Set<() => void>()
|
|
324
332
|
|
|
325
333
|
function rebuildMain(): void {
|
|
326
334
|
mainTools = [...toolDefs, ...promptDefs]
|
|
327
335
|
}
|
|
328
336
|
|
|
329
|
-
async function refreshTools(): Promise<
|
|
330
|
-
const
|
|
337
|
+
async function refreshTools(): Promise<boolean> {
|
|
338
|
+
const generation = ++toolRefreshGeneration
|
|
339
|
+
const discovered = await discovery.discoverFrom(client, {
|
|
340
|
+
isCurrent: () => !closed && generation === toolRefreshGeneration,
|
|
341
|
+
})
|
|
342
|
+
if (closed || generation !== toolRefreshGeneration) return false
|
|
331
343
|
const previous = new Map(toolDefs.map((definition) => [definition.name, definition]))
|
|
332
344
|
toolDefs = discovered.map((d) => {
|
|
333
345
|
const name = mcpToolsetName(serverName, d.tool.name)
|
|
@@ -346,6 +358,8 @@ export async function mcpToolset(
|
|
|
346
358
|
if (!discovered.some((entry) => entry.tool.name === name)) heldToolNames.delete(name)
|
|
347
359
|
}
|
|
348
360
|
rebuildMain()
|
|
361
|
+
admittedToolSnapshot = discovered.map((entry) => entry.tool)
|
|
362
|
+
return true
|
|
349
363
|
}
|
|
350
364
|
|
|
351
365
|
async function refreshPrompts(): Promise<void> {
|
|
@@ -414,9 +428,12 @@ export async function mcpToolset(
|
|
|
414
428
|
}
|
|
415
429
|
}
|
|
416
430
|
assertUnique()
|
|
431
|
+
options.onToolsAdmitted?.(admittedToolSnapshot)
|
|
417
432
|
|
|
418
433
|
const notify = (): void => {
|
|
434
|
+
if (closed) return
|
|
419
435
|
assertUnique()
|
|
436
|
+
options.onToolsAdmitted?.(admittedToolSnapshot)
|
|
420
437
|
for (const listener of listeners) listener()
|
|
421
438
|
}
|
|
422
439
|
const onChange = (listener: () => void): (() => void) => {
|
|
@@ -449,7 +466,11 @@ export async function mcpToolset(
|
|
|
449
466
|
if (closed) return
|
|
450
467
|
if (method === 'notifications/tools/list_changed') {
|
|
451
468
|
if (!currentCapabilities()?.tools?.listChanged) return
|
|
452
|
-
void refreshTools()
|
|
469
|
+
void refreshTools()
|
|
470
|
+
.then((updated) => {
|
|
471
|
+
if (updated) notify()
|
|
472
|
+
})
|
|
473
|
+
.catch(onRefreshFailure('tools'))
|
|
453
474
|
} else if (method === 'notifications/prompts/list_changed') {
|
|
454
475
|
if (!currentCapabilities()?.prompts?.listChanged) return
|
|
455
476
|
void refreshPrompts().then(notify).catch(onRefreshFailure('prompts'))
|
|
@@ -83,6 +83,7 @@ export class MCPReconnectSupervisor {
|
|
|
83
83
|
private stopped = false
|
|
84
84
|
private inFlight = false
|
|
85
85
|
private timer?: ReturnType<typeof setTimeout>
|
|
86
|
+
private finishWait?: () => void
|
|
86
87
|
|
|
87
88
|
constructor(
|
|
88
89
|
private readonly client: MCPClient,
|
|
@@ -106,7 +107,7 @@ export class MCPReconnectSupervisor {
|
|
|
106
107
|
|
|
107
108
|
/** Begin watching. Idempotent. */
|
|
108
109
|
start(): void {
|
|
109
|
-
if (
|
|
110
|
+
if (this.stopped || this.unsubscribe || !this.policy().enabled) return
|
|
110
111
|
this.unsubscribe = this.client.onLifecycle((event) => {
|
|
111
112
|
if (event.type !== 'mcp_client_disconnected' && event.type !== 'mcp_client_error') return
|
|
112
113
|
void this.recover()
|
|
@@ -124,8 +125,9 @@ export class MCPReconnectSupervisor {
|
|
|
124
125
|
this.stopped = true
|
|
125
126
|
this.unsubscribe?.()
|
|
126
127
|
this.unsubscribe = undefined
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
// Clearing the timer alone strands recover() inside an unresolved
|
|
129
|
+
// promise. Settle it so the stopped check can retire the loop.
|
|
130
|
+
this.finishWait?.()
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
private async recover(): Promise<void> {
|
|
@@ -154,9 +156,22 @@ export class MCPReconnectSupervisor {
|
|
|
154
156
|
|
|
155
157
|
try {
|
|
156
158
|
await this.client.connect()
|
|
159
|
+
// A host may stop the supervisor and dispose its live policy
|
|
160
|
+
// while connect() is still settling. Do not read that policy or
|
|
161
|
+
// publish a reconnect callback after teardown. A successful
|
|
162
|
+
// late connect also needs a second close: the host may have
|
|
163
|
+
// finished its own disconnect before this connect settled.
|
|
164
|
+
if (this.stopped) {
|
|
165
|
+
this.closeLateConnection()
|
|
166
|
+
return
|
|
167
|
+
}
|
|
157
168
|
await this.policy().onReconnected?.()
|
|
158
169
|
return
|
|
159
170
|
} catch {
|
|
171
|
+
if (this.stopped) {
|
|
172
|
+
this.closeLateConnection()
|
|
173
|
+
return
|
|
174
|
+
}
|
|
160
175
|
// Deliberately swallowed: a failed attempt is the normal
|
|
161
176
|
// case here and the client has already logged and emitted
|
|
162
177
|
// its own error. Re-raising would surface a routine retry
|
|
@@ -164,15 +179,30 @@ export class MCPReconnectSupervisor {
|
|
|
164
179
|
delay = Math.min(delay * 2, this.policy().maxDelayMs)
|
|
165
180
|
}
|
|
166
181
|
}
|
|
182
|
+
if (this.stopped) return
|
|
167
183
|
this.policy().onGaveUp?.(this.policy().maxAttempts)
|
|
168
184
|
} finally {
|
|
169
185
|
this.inFlight = false
|
|
170
186
|
}
|
|
171
187
|
}
|
|
172
188
|
|
|
189
|
+
private closeLateConnection(): void {
|
|
190
|
+
// A failed handshake can leave an opened transport, too. Stop is
|
|
191
|
+
// synchronous and the host's disable path has its own bounded close;
|
|
192
|
+
// keep this late cleanup best-effort without delaying recovery exit.
|
|
193
|
+
void this.client.disconnect().catch(() => {})
|
|
194
|
+
}
|
|
195
|
+
|
|
173
196
|
private wait(ms: number): Promise<void> {
|
|
174
197
|
return new Promise((resolve) => {
|
|
175
|
-
|
|
198
|
+
const finish = () => {
|
|
199
|
+
if (this.timer) clearTimeout(this.timer)
|
|
200
|
+
this.timer = undefined
|
|
201
|
+
if (this.finishWait === finish) this.finishWait = undefined
|
|
202
|
+
resolve()
|
|
203
|
+
}
|
|
204
|
+
this.finishWait = finish
|
|
205
|
+
this.timer = setTimeout(finish, ms)
|
|
176
206
|
})
|
|
177
207
|
}
|
|
178
208
|
}
|
package/src/plugin/lifecycle.ts
CHANGED
|
@@ -49,6 +49,7 @@ interface PluginContributionRecord {
|
|
|
49
49
|
mcpToolsets: Toolset[]
|
|
50
50
|
mcpUnsubscribers: Array<() => void>
|
|
51
51
|
mcpNamesBySource: Map<string, Set<string>>
|
|
52
|
+
configReleases: Array<() => void>
|
|
52
53
|
/** Namespaced skill names, so rollback and disable can take them back. */
|
|
53
54
|
skillNames: string[]
|
|
54
55
|
}
|
|
@@ -62,6 +63,68 @@ interface PluginAdmission {
|
|
|
62
63
|
readonly inCode?: DefinedPlugin
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
const DEFAULT_PLUGIN_MCP_CONNECT_TIMEOUT_MS = 10_000
|
|
67
|
+
const PLUGIN_MCP_CLOSE_TIMEOUT_MS = 5_000
|
|
68
|
+
|
|
69
|
+
/** One deadline across handshake and all initial discovery requests. */
|
|
70
|
+
async function beforePluginMcpDeadline<T>(
|
|
71
|
+
work: () => Promise<T>,
|
|
72
|
+
deadlineAt: number,
|
|
73
|
+
timeoutMs: number,
|
|
74
|
+
serverName: string,
|
|
75
|
+
onLate?: (value: T) => void,
|
|
76
|
+
onTimeout?: () => void,
|
|
77
|
+
): Promise<T> {
|
|
78
|
+
const expired = (): Error =>
|
|
79
|
+
new Error(`Plugin MCP server "${serverName}" did not answer within ${timeoutMs}ms`)
|
|
80
|
+
const remaining = deadlineAt - Date.now()
|
|
81
|
+
if (remaining <= 0) throw expired()
|
|
82
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
83
|
+
let timedOut = false
|
|
84
|
+
try {
|
|
85
|
+
const pending = work()
|
|
86
|
+
// A timed-out discovery can still finish after rollback. In particular,
|
|
87
|
+
// mcpToolset() starts a reconnect supervisor just before it returns.
|
|
88
|
+
// Close that late result without publishing any of its tools.
|
|
89
|
+
void pending.then(
|
|
90
|
+
(value) => {
|
|
91
|
+
if (timedOut) onLate?.(value)
|
|
92
|
+
},
|
|
93
|
+
() => {},
|
|
94
|
+
)
|
|
95
|
+
return await Promise.race([
|
|
96
|
+
pending,
|
|
97
|
+
new Promise<never>((_, reject) => {
|
|
98
|
+
timer = setTimeout(() => {
|
|
99
|
+
timedOut = true
|
|
100
|
+
onTimeout?.()
|
|
101
|
+
reject(expired())
|
|
102
|
+
}, remaining)
|
|
103
|
+
}),
|
|
104
|
+
])
|
|
105
|
+
} finally {
|
|
106
|
+
if (timer) clearTimeout(timer)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Teardown is best-effort, but one stalled close must not block rollback or disable. */
|
|
111
|
+
async function closePluginMcpWithin(work: () => Promise<void>, label: string): Promise<void> {
|
|
112
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
113
|
+
try {
|
|
114
|
+
await Promise.race([
|
|
115
|
+
work(),
|
|
116
|
+
new Promise<never>((_, reject) => {
|
|
117
|
+
timer = setTimeout(
|
|
118
|
+
() => reject(new Error(`${label} did not close within ${PLUGIN_MCP_CLOSE_TIMEOUT_MS}ms`)),
|
|
119
|
+
PLUGIN_MCP_CLOSE_TIMEOUT_MS,
|
|
120
|
+
)
|
|
121
|
+
}),
|
|
122
|
+
])
|
|
123
|
+
} finally {
|
|
124
|
+
if (timer) clearTimeout(timer)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
65
128
|
/** Dynamic imports bypass ToolDefinition's TypeScript contract. */
|
|
66
129
|
function assertPluginTool(
|
|
67
130
|
value: unknown,
|
|
@@ -100,6 +163,9 @@ function immutableManifest(manifest: PluginDefinition['manifest']): PluginDefini
|
|
|
100
163
|
command: server.command,
|
|
101
164
|
...(server.args ? { args: Object.freeze([...server.args]) } : {}),
|
|
102
165
|
...(server.env ? { env: Object.freeze({ ...server.env }) } : {}),
|
|
166
|
+
...(server.connectTimeoutMs !== undefined
|
|
167
|
+
? { connectTimeoutMs: server.connectTimeoutMs }
|
|
168
|
+
: {}),
|
|
103
169
|
}),
|
|
104
170
|
)
|
|
105
171
|
return Object.freeze({
|
|
@@ -199,6 +265,8 @@ export class PluginLifecycleManager {
|
|
|
199
265
|
}>
|
|
200
266
|
> = new Map()
|
|
201
267
|
private pluginContributions: Map<PluginId, PluginContributionRecord> = new Map()
|
|
268
|
+
/** Reserve each id before async admission so lifecycle calls cannot overlap. */
|
|
269
|
+
private readonly lifecycleTails = new Map<PluginId, Promise<void>>()
|
|
202
270
|
/** Immutable executable admissions owned by this manager, never by the mutable registry. */
|
|
203
271
|
private pluginAdmissions: Map<PluginId, PluginAdmission> = new Map()
|
|
204
272
|
private hookTimeoutMs: number
|
|
@@ -206,18 +274,16 @@ export class PluginLifecycleManager {
|
|
|
206
274
|
private readonly scopeRoots: Readonly<Record<PluginScope, string>>
|
|
207
275
|
private log: Logger
|
|
208
276
|
/**
|
|
209
|
-
* The
|
|
277
|
+
* The cross-enable drift history for each plugin-owned MCP source.
|
|
210
278
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* the window a rug pull uses.
|
|
279
|
+
* The toolset applies policy and performs the only listing. It passes the
|
|
280
|
+
* admitted snapshot here after it commits, keyed by its full plugin source.
|
|
281
|
+
* Keeping this recorder for the manager lifetime detects a server that
|
|
282
|
+
* changes between disable and the next enable without conflating unrelated
|
|
283
|
+
* plugins that use the same local server label.
|
|
217
284
|
*
|
|
218
|
-
* Its client list
|
|
219
|
-
*
|
|
220
|
-
* reads.
|
|
285
|
+
* Its client list stays empty because this instance records snapshots;
|
|
286
|
+
* it never makes a second request to the server.
|
|
221
287
|
*/
|
|
222
288
|
private mcpDiscovery: MCPToolDiscovery
|
|
223
289
|
private readonly mcpToolPolicies?: Readonly<Record<string, MCPToolPolicy>>
|
|
@@ -235,8 +301,20 @@ export class PluginLifecycleManager {
|
|
|
235
301
|
this.onMCPToolDrift = config.onMCPToolDrift
|
|
236
302
|
this.log = config.log.child({ [SCOPE_ATTRIBUTE]: 'plugin/lifecycle' })
|
|
237
303
|
this.mcpDiscovery = new MCPToolDiscovery([], {
|
|
238
|
-
...(config.
|
|
239
|
-
|
|
304
|
+
...(config.onMCPToolDrift
|
|
305
|
+
? {
|
|
306
|
+
onDrift: (event) => {
|
|
307
|
+
try {
|
|
308
|
+
this.onMCPToolDrift?.(event)
|
|
309
|
+
} catch (error) {
|
|
310
|
+
this.log.warn('Plugin MCP drift listener failed', {
|
|
311
|
+
'namzu.plugin.mcp_server': event.serverName,
|
|
312
|
+
'exception.message': toErrorMessage(error),
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
: {}),
|
|
240
318
|
logger: this.log,
|
|
241
319
|
})
|
|
242
320
|
}
|
|
@@ -453,6 +531,21 @@ export class PluginLifecycleManager {
|
|
|
453
531
|
})
|
|
454
532
|
}
|
|
455
533
|
|
|
534
|
+
private serializeLifecycle<T>(pluginId: PluginId, work: () => Promise<T>): Promise<T> {
|
|
535
|
+
const previous = this.lifecycleTails.get(pluginId) ?? Promise.resolve()
|
|
536
|
+
const running = previous.then(work)
|
|
537
|
+
// A failed enable must not poison later disable, retry or uninstall.
|
|
538
|
+
const tail = running.then(
|
|
539
|
+
() => undefined,
|
|
540
|
+
() => undefined,
|
|
541
|
+
)
|
|
542
|
+
this.lifecycleTails.set(pluginId, tail)
|
|
543
|
+
void tail.then(() => {
|
|
544
|
+
if (this.lifecycleTails.get(pluginId) === tail) this.lifecycleTails.delete(pluginId)
|
|
545
|
+
})
|
|
546
|
+
return running
|
|
547
|
+
}
|
|
548
|
+
|
|
456
549
|
private async readLegacyAdmission(plugin: PluginDefinition): Promise<PluginAdmission> {
|
|
457
550
|
const rootDir = await resolveWithinReal(this.scopeRoots[plugin.scope], plugin.rootDir)
|
|
458
551
|
const manifest = immutableManifest(
|
|
@@ -482,7 +575,11 @@ export class PluginLifecycleManager {
|
|
|
482
575
|
})
|
|
483
576
|
}
|
|
484
577
|
|
|
485
|
-
|
|
578
|
+
enable(pluginId: PluginId): Promise<void> {
|
|
579
|
+
return this.serializeLifecycle(pluginId, () => this.enableOwned(pluginId))
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private async enableOwned(pluginId: PluginId): Promise<void> {
|
|
486
583
|
const plugin = this.pluginRegistry.getOrThrow(pluginId)
|
|
487
584
|
let admission = this.pluginAdmissions.get(pluginId)
|
|
488
585
|
|
|
@@ -543,6 +640,7 @@ export class PluginLifecycleManager {
|
|
|
543
640
|
mcpToolsets: [],
|
|
544
641
|
mcpUnsubscribers: [],
|
|
545
642
|
mcpNamesBySource: new Map(),
|
|
643
|
+
configReleases: [],
|
|
546
644
|
skillNames: [],
|
|
547
645
|
}
|
|
548
646
|
|
|
@@ -703,30 +801,75 @@ export class PluginLifecycleManager {
|
|
|
703
801
|
},
|
|
704
802
|
logger: this.log,
|
|
705
803
|
})
|
|
706
|
-
|
|
707
|
-
|
|
804
|
+
// A failed handshake still owns a transport (and possibly a child
|
|
805
|
+
// process). Put it under rollback ownership before the first await.
|
|
708
806
|
contributions.mcpClients.push(client)
|
|
807
|
+
const timeoutMs = config.connectTimeoutMs ?? DEFAULT_PLUGIN_MCP_CONNECT_TIMEOUT_MS
|
|
808
|
+
const deadlineAt = Date.now() + timeoutMs
|
|
809
|
+
const closeLateClient = () => {
|
|
810
|
+
void closePluginMcpWithin(
|
|
811
|
+
() => client.disconnect(),
|
|
812
|
+
`Plugin MCP server "${config.name}" after late connection`,
|
|
813
|
+
).catch((error) => {
|
|
814
|
+
this.log.warn('Late MCP disconnect failed', {
|
|
815
|
+
'namzu.mcp.client_id': client.id,
|
|
816
|
+
'exception.message': toErrorMessage(error),
|
|
817
|
+
})
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
await beforePluginMcpDeadline(
|
|
821
|
+
() => client.connect(),
|
|
822
|
+
deadlineAt,
|
|
823
|
+
timeoutMs,
|
|
824
|
+
config.name,
|
|
825
|
+
closeLateClient,
|
|
826
|
+
)
|
|
709
827
|
|
|
710
828
|
// The toolset owns reconnection and reads this policy on each attempt.
|
|
711
829
|
const policyScope = this.configRegistry?.register(
|
|
712
|
-
`mcp.${config.name}`,
|
|
830
|
+
`mcp.plugin.${pluginName}.${config.name}`,
|
|
713
831
|
MCPReconnectOptionsSchema,
|
|
714
832
|
)
|
|
715
|
-
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
await this.mcpDiscovery.discoverFrom(client)
|
|
833
|
+
if (policyScope) contributions.configReleases.push(() => policyScope.dispose())
|
|
834
|
+
// The toolset performs the only listing. Record precisely the admitted
|
|
835
|
+
// snapshot it mounts, including later refreshes, under the plugin source.
|
|
719
836
|
const sourceId = `plugin:${pluginName}/mcp:${config.name}`
|
|
720
837
|
const policy = this.mcpToolPolicies?.[config.name] ?? this.mcpToolPolicies?.['*']
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
838
|
+
let acceptingSnapshot = true
|
|
839
|
+
const entries = await beforePluginMcpDeadline(
|
|
840
|
+
() =>
|
|
841
|
+
mcpToolset(client, {
|
|
842
|
+
id: sourceId,
|
|
843
|
+
availability: 'deferred',
|
|
844
|
+
allow: policy?.allow,
|
|
845
|
+
deny: policy?.deny,
|
|
846
|
+
reconnect: policyScope ? () => policyScope.get() : {},
|
|
847
|
+
onToolsAdmitted: (tools) => {
|
|
848
|
+
if (acceptingSnapshot) {
|
|
849
|
+
this.mcpDiscovery.recordAdmittedTools(sourceId, client.id, config.name, tools)
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
logger: this.log,
|
|
853
|
+
}),
|
|
854
|
+
deadlineAt,
|
|
855
|
+
timeoutMs,
|
|
856
|
+
config.name,
|
|
857
|
+
(lateEntries) => {
|
|
858
|
+
for (const entry of lateEntries) {
|
|
859
|
+
void closePluginMcpWithin(async () => {
|
|
860
|
+
await entry.close?.()
|
|
861
|
+
}, `Late MCP toolset ${entry.source.id}`).catch((error) => {
|
|
862
|
+
this.log.warn('Late MCP toolset close failed', {
|
|
863
|
+
'namzu.toolset.source_id': entry.source.id,
|
|
864
|
+
'exception.message': toErrorMessage(error),
|
|
865
|
+
})
|
|
866
|
+
})
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
() => {
|
|
870
|
+
acceptingSnapshot = false
|
|
871
|
+
},
|
|
872
|
+
)
|
|
730
873
|
contributions.mcpToolsets.push(...entries)
|
|
731
874
|
const wrapped = entries.map((entry) =>
|
|
732
875
|
prefixed(entry, `${pluginName}${PLUGIN_NAMESPACE_SEPARATOR}`),
|
|
@@ -779,25 +922,49 @@ export class PluginLifecycleManager {
|
|
|
779
922
|
contributions: PluginContributionRecord,
|
|
780
923
|
operation: 'rollback' | 'disable',
|
|
781
924
|
): Promise<void> {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
925
|
+
try {
|
|
926
|
+
for (const unsubscribe of contributions.mcpUnsubscribers) unsubscribe()
|
|
927
|
+
await Promise.all(
|
|
928
|
+
contributions.mcpToolsets.map(async (entry) => {
|
|
929
|
+
try {
|
|
930
|
+
await closePluginMcpWithin(async () => {
|
|
931
|
+
await entry.close?.()
|
|
932
|
+
}, `MCP toolset ${entry.source.id}`)
|
|
933
|
+
} catch (error) {
|
|
934
|
+
this.log.warn('MCP toolset close failed', {
|
|
935
|
+
'namzu.plugin.operation': operation,
|
|
936
|
+
'namzu.toolset.source_id': entry.source.id,
|
|
937
|
+
'exception.message': toErrorMessage(error),
|
|
938
|
+
})
|
|
939
|
+
}
|
|
940
|
+
}),
|
|
941
|
+
)
|
|
942
|
+
await Promise.all(
|
|
943
|
+
contributions.mcpClients.map(async (client) => {
|
|
944
|
+
try {
|
|
945
|
+
await closePluginMcpWithin(() => client.disconnect(), `MCP client ${client.id}`)
|
|
946
|
+
} catch (error) {
|
|
947
|
+
this.log.warn('MCP disconnect failed', {
|
|
948
|
+
'namzu.plugin.operation': operation,
|
|
949
|
+
'namzu.mcp.client_id': client.id,
|
|
950
|
+
'exception.message': toErrorMessage(error),
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
}),
|
|
954
|
+
)
|
|
955
|
+
for (const names of contributions.mcpNamesBySource.values()) {
|
|
956
|
+
for (const name of names) this.removeContributedTool(name)
|
|
793
957
|
}
|
|
958
|
+
} finally {
|
|
959
|
+
for (const release of contributions.configReleases) release()
|
|
794
960
|
}
|
|
795
|
-
for (const names of contributions.mcpNamesBySource.values()) {
|
|
796
|
-
for (const name of names) this.removeContributedTool(name)
|
|
797
|
-
}
|
|
798
961
|
}
|
|
799
962
|
|
|
800
|
-
|
|
963
|
+
disable(pluginId: PluginId): Promise<void> {
|
|
964
|
+
return this.serializeLifecycle(pluginId, () => this.disableOwned(pluginId))
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private async disableOwned(pluginId: PluginId): Promise<void> {
|
|
801
968
|
const plugin = this.pluginRegistry.getOrThrow(pluginId)
|
|
802
969
|
const admission = this.pluginAdmissions.get(pluginId)
|
|
803
970
|
const contributions = this.pluginContributions.get(pluginId)
|
|
@@ -856,12 +1023,16 @@ export class PluginLifecycleManager {
|
|
|
856
1023
|
})
|
|
857
1024
|
}
|
|
858
1025
|
|
|
859
|
-
|
|
1026
|
+
uninstall(pluginId: PluginId): Promise<void> {
|
|
1027
|
+
return this.serializeLifecycle(pluginId, () => this.uninstallOwned(pluginId))
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
private async uninstallOwned(pluginId: PluginId): Promise<void> {
|
|
860
1031
|
const plugin = this.pluginRegistry.getOrThrow(pluginId)
|
|
861
1032
|
const admission = this.pluginAdmissions.get(pluginId)
|
|
862
1033
|
|
|
863
1034
|
if (this.pluginContributions.has(pluginId)) {
|
|
864
|
-
await this.
|
|
1035
|
+
await this.disableOwned(pluginId)
|
|
865
1036
|
}
|
|
866
1037
|
|
|
867
1038
|
this.pluginRegistry.unregister(pluginId)
|
|
@@ -103,7 +103,7 @@ export function createResidentStepContext(
|
|
|
103
103
|
...contributions,
|
|
104
104
|
Object.freeze({
|
|
105
105
|
id: 'namzu.resident-step.learning-catalogue',
|
|
106
|
-
placement: '
|
|
106
|
+
placement: 'context' as const,
|
|
107
107
|
render: () =>
|
|
108
108
|
[
|
|
109
109
|
'## Available evaluated resident skills',
|
|
@@ -113,7 +113,7 @@ export function createResidentStepContext(
|
|
|
113
113
|
}),
|
|
114
114
|
Object.freeze({
|
|
115
115
|
id: 'namzu.resident-step.selected-learning',
|
|
116
|
-
placement: '
|
|
116
|
+
placement: 'context' as const,
|
|
117
117
|
render: () => {
|
|
118
118
|
if (!selected.size) return null
|
|
119
119
|
try {
|
|
@@ -58,8 +58,8 @@ export interface ResidentStepPromptOptions {
|
|
|
58
58
|
* contributions. Register them on the registry supplied to `query`.
|
|
59
59
|
*
|
|
60
60
|
* The static contribution carries stable work guidance and the host's output
|
|
61
|
-
* contract. The
|
|
62
|
-
* approved learning
|
|
61
|
+
* contract. The context contribution captures this invocation's state and
|
|
62
|
+
* approved learning as request-only user-role context. Both remain present on every
|
|
63
63
|
* iteration; build fresh contributions for the next admitted step.
|
|
64
64
|
*
|
|
65
65
|
* Project instructions and authorization remain on their existing host/runtime
|
|
@@ -128,14 +128,14 @@ export function createResidentStepContributions(
|
|
|
128
128
|
}),
|
|
129
129
|
Object.freeze({
|
|
130
130
|
id: 'namzu.resident-step.continuation',
|
|
131
|
-
placement: '
|
|
131
|
+
placement: 'context' as const,
|
|
132
132
|
render: () => snapshot,
|
|
133
133
|
}),
|
|
134
134
|
...(boundNames.length
|
|
135
135
|
? [
|
|
136
136
|
Object.freeze({
|
|
137
137
|
id: 'namzu.resident-step.source-bound-learning',
|
|
138
|
-
placement: '
|
|
138
|
+
placement: 'context' as const,
|
|
139
139
|
render: () => {
|
|
140
140
|
let sources: readonly ResidentLearningSource[] = []
|
|
141
141
|
try {
|
package/src/public-runtime.ts
CHANGED
|
@@ -506,6 +506,7 @@ export {
|
|
|
506
506
|
MEMORY_VERIFY_NOTICE,
|
|
507
507
|
MemoryContentRejectedError,
|
|
508
508
|
MemoryNameConflictError,
|
|
509
|
+
MemoryRevisionConflictError,
|
|
509
510
|
describeMemoryAge,
|
|
510
511
|
isMemoryName,
|
|
511
512
|
memoryIndexLine,
|
|
@@ -1143,6 +1144,7 @@ export {
|
|
|
1143
1144
|
MEMORY_TYPES,
|
|
1144
1145
|
assertMemoryStatus,
|
|
1145
1146
|
assertMemoryType,
|
|
1147
|
+
hasConditionalMemoryWrites,
|
|
1146
1148
|
isMemoryType,
|
|
1147
1149
|
} from './types/memory/index.js'
|
|
1148
1150
|
export {
|