@namzu/sdk 0.1.5-rc.2 → 0.1.5
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 +17 -0
- package/dist/bridge/tools/connector/adapter.d.ts +2 -2
- package/dist/bridge/tools/connector/adapter.d.ts.map +1 -1
- package/dist/bridge/tools/connector/adapter.js +3 -1
- package/dist/bridge/tools/connector/adapter.js.map +1 -1
- package/dist/connector/BaseConnector.d.ts +2 -1
- package/dist/connector/BaseConnector.d.ts.map +1 -1
- package/dist/connector/BaseConnector.js.map +1 -1
- package/dist/connector/builtins/http.d.ts +1 -1
- package/dist/connector/builtins/http.d.ts.map +1 -1
- package/dist/connector/builtins/http.js +1 -1
- package/dist/connector/builtins/http.js.map +1 -1
- package/dist/connector/builtins/webhook.d.ts +1 -1
- package/dist/connector/builtins/webhook.d.ts.map +1 -1
- package/dist/connector/builtins/webhook.js +1 -1
- package/dist/connector/builtins/webhook.js.map +1 -1
- package/dist/index.d.ts +5 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -22
- package/dist/index.js.map +1 -1
- package/dist/manager/connector/environment.d.ts +4 -4
- package/dist/manager/connector/environment.d.ts.map +1 -1
- package/dist/manager/connector/environment.js.map +1 -1
- package/dist/manager/connector/lifecycle.d.ts +2 -2
- package/dist/manager/connector/lifecycle.d.ts.map +1 -1
- package/dist/manager/connector/lifecycle.js.map +1 -1
- package/dist/manager/connector/tenant.d.ts +3 -3
- package/dist/manager/connector/tenant.d.ts.map +1 -1
- package/dist/manager/connector/tenant.js.map +1 -1
- package/dist/manager/index.d.ts +1 -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/run/emergency.d.ts.map +1 -1
- package/dist/manager/run/emergency.js +44 -12
- package/dist/manager/run/emergency.js.map +1 -1
- package/dist/rag/vector-store.d.ts +2 -2
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/registry/connector/scoped.d.ts +5 -4
- package/dist/registry/connector/scoped.d.ts.map +1 -1
- package/dist/registry/connector/scoped.js.map +1 -1
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -0
- package/dist/registry/index.js.map +1 -1
- package/dist/store/index.d.ts +4 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +3 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/task/__tests__/disk-concurrency.test.d.ts +2 -0
- package/dist/store/task/__tests__/disk-concurrency.test.d.ts.map +1 -0
- package/dist/store/task/__tests__/disk-concurrency.test.js +91 -0
- package/dist/store/task/__tests__/disk-concurrency.test.js.map +1 -0
- package/dist/store/task/disk.d.ts +6 -0
- package/dist/store/task/disk.d.ts.map +1 -1
- package/dist/store/task/disk.js +150 -36
- package/dist/store/task/disk.js.map +1 -1
- package/dist/types/connector/core.d.ts +2 -2
- package/dist/types/connector/core.d.ts.map +1 -1
- package/dist/types/connector/definition.d.ts +7 -7
- package/dist/types/connector/definition.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +4 -4
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/connector/scope.d.ts +3 -2
- package/dist/types/connector/scope.d.ts.map +1 -1
- package/dist/types/connector/scope.js.map +1 -1
- package/dist/types/connector/tenant.d.ts +4 -4
- package/dist/types/connector/tenant.d.ts.map +1 -1
- package/dist/types/rag/knowledge-base.d.ts +2 -2
- package/dist/types/rag/knowledge-base.d.ts.map +1 -1
- package/dist/types/rag/scope.d.ts +2 -1
- package/dist/types/rag/scope.d.ts.map +1 -1
- package/dist/types/rag/storage.d.ts +3 -3
- package/dist/types/rag/storage.d.ts.map +1 -1
- package/dist/types/rag/vector.d.ts +3 -3
- package/dist/types/rag/vector.d.ts.map +1 -1
- package/dist/vault/InMemoryCredentialVault.d.ts +3 -3
- package/dist/vault/InMemoryCredentialVault.d.ts.map +1 -1
- package/dist/vault/InMemoryCredentialVault.js.map +1 -1
- package/package.json +1 -1
- package/src/bridge/tools/connector/adapter.ts +5 -3
- package/src/connector/BaseConnector.ts +2 -1
- package/src/connector/builtins/http.ts +1 -1
- package/src/connector/builtins/webhook.ts +1 -1
- package/src/index.ts +46 -40
- package/src/manager/connector/environment.ts +5 -5
- package/src/manager/connector/lifecycle.ts +2 -2
- package/src/manager/connector/tenant.ts +8 -3
- package/src/manager/index.ts +1 -0
- package/src/manager/run/emergency.ts +45 -16
- package/src/rag/vector-store.ts +2 -2
- package/src/registry/connector/scoped.ts +7 -6
- package/src/registry/index.ts +1 -0
- package/src/store/index.ts +5 -0
- package/src/store/task/__tests__/disk-concurrency.test.ts +118 -0
- package/src/store/task/disk.ts +150 -37
- package/src/types/connector/core.ts +2 -2
- package/src/types/connector/definition.ts +7 -7
- package/src/types/connector/mcp.ts +4 -4
- package/src/types/connector/scope.ts +3 -2
- package/src/types/connector/tenant.ts +4 -4
- package/src/types/rag/knowledge-base.ts +2 -2
- package/src/types/rag/scope.ts +3 -1
- package/src/types/rag/storage.ts +3 -3
- package/src/types/rag/vector.ts +3 -3
- package/src/vault/InMemoryCredentialVault.ts +3 -3
- package/dist/manager/agent/index.d.ts +0 -2
- package/dist/manager/agent/index.d.ts.map +0 -1
- package/dist/manager/agent/index.js +0 -2
- package/dist/manager/agent/index.js.map +0 -1
- package/dist/registry/agent/index.d.ts +0 -2
- package/dist/registry/agent/index.d.ts.map +0 -1
- package/dist/registry/agent/index.js +0 -2
- package/dist/registry/agent/index.js.map +0 -1
- package/dist/router/index.d.ts +0 -2
- package/dist/router/index.d.ts.map +0 -1
- package/dist/router/index.js +0 -2
- package/dist/router/index.js.map +0 -1
- package/src/manager/agent/index.ts +0 -1
- package/src/registry/agent/index.ts +0 -1
- package/src/router/index.ts +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import type { RunId } from '../../../types/ids/index.js'
|
|
6
|
+
import { generateRunId } from '../../../utils/id.js'
|
|
7
|
+
import { DiskTaskStore } from '../disk.js'
|
|
8
|
+
|
|
9
|
+
describe('DiskTaskStore — concurrency regressions', () => {
|
|
10
|
+
let baseDir: string
|
|
11
|
+
let runId: RunId
|
|
12
|
+
let store: DiskTaskStore
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
baseDir = mkdtempSync(join(tmpdir(), 'namzu-task-concurrency-'))
|
|
16
|
+
runId = generateRunId()
|
|
17
|
+
store = new DiskTaskStore({ baseDir, defaultRunId: runId })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(baseDir, { recursive: true, force: true })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('does not deadlock when two deletes race on mutually-referencing tasks', async () => {
|
|
25
|
+
const a = await store.create({ runId, subject: 'A' })
|
|
26
|
+
const b = await store.create({ runId, subject: 'B' })
|
|
27
|
+
|
|
28
|
+
// Establish bidirectional edge: A blocks B AND B blocks A.
|
|
29
|
+
// (Nonsensical semantically, but the store allows it and the lock logic
|
|
30
|
+
// must not deadlock.)
|
|
31
|
+
await store.block(a.id, b.id)
|
|
32
|
+
await store.block(b.id, a.id)
|
|
33
|
+
|
|
34
|
+
// Race the two deletes. The pre-fix implementation (lock this → iterate
|
|
35
|
+
// related → lock each) could acquire A→B from one call and B→A from the
|
|
36
|
+
// other, deadlocking. withLocks() sorts IDs canonically so both calls
|
|
37
|
+
// acquire [A, B] in the same order.
|
|
38
|
+
const withTimeout = <T>(p: Promise<T>, ms: number): Promise<T> =>
|
|
39
|
+
Promise.race([
|
|
40
|
+
p,
|
|
41
|
+
new Promise<never>((_resolve, reject) =>
|
|
42
|
+
setTimeout(() => reject(new Error('timeout — likely deadlocked')), ms),
|
|
43
|
+
),
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const [resA, resB] = await withTimeout(
|
|
47
|
+
Promise.all([store.delete(a.id), store.delete(b.id)]),
|
|
48
|
+
2000,
|
|
49
|
+
)
|
|
50
|
+
expect(resA).toBe(true)
|
|
51
|
+
expect(resB).toBe(true)
|
|
52
|
+
expect(await store.get(a.id)).toBeUndefined()
|
|
53
|
+
expect(await store.get(b.id)).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('serializes concurrent same-ID updates (withLock race regression)', async () => {
|
|
57
|
+
const task = await store.create({ runId, subject: 'shared' })
|
|
58
|
+
|
|
59
|
+
// update()'s metadata merge does `{ ...task.metadata, ...updates.metadata }`
|
|
60
|
+
// inside withLock. If withLock serializes correctly, every update's key
|
|
61
|
+
// survives into the final metadata (each sees the latest merged state).
|
|
62
|
+
// If withLock had the race bug, two concurrent updates would both read
|
|
63
|
+
// the SAME snapshot, each add their one key, and one update's key would
|
|
64
|
+
// be overwritten when the second committed — yielding < N keys in the
|
|
65
|
+
// final metadata.
|
|
66
|
+
const n = 20
|
|
67
|
+
await Promise.all(
|
|
68
|
+
Array.from({ length: n }, (_v, i) =>
|
|
69
|
+
store.update(task.id, { metadata: { [`k${i}`]: true } }),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const final = await store.get(task.id)
|
|
74
|
+
const keys = Object.keys(final?.metadata ?? {})
|
|
75
|
+
expect(keys).toHaveLength(n)
|
|
76
|
+
for (let i = 0; i < n; i++) {
|
|
77
|
+
expect(final?.metadata?.[`k${i}`]).toBe(true)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('establishes bidirectional edge atomically under create()', async () => {
|
|
82
|
+
const blocker = await store.create({ runId, subject: 'blocker' })
|
|
83
|
+
|
|
84
|
+
// Race: create a child with blockedBy=[blocker] while concurrently
|
|
85
|
+
// deleting the blocker. Either outcome is acceptable (child created
|
|
86
|
+
// and blocker still present in its blocks list, OR blocker gone and
|
|
87
|
+
// child created with dangling reference), but we must NEVER see
|
|
88
|
+
// blocker still present WITHOUT having child in its blocks list.
|
|
89
|
+
const tasks = await Promise.all([
|
|
90
|
+
store.create({ runId, subject: 'child', blockedBy: [blocker.id] }),
|
|
91
|
+
// No delete here — keep the create edge test focused. The point is
|
|
92
|
+
// that after create() resolves, the blocker's blocks list contains
|
|
93
|
+
// the new task ID atomically.
|
|
94
|
+
])
|
|
95
|
+
const child = tasks[0]
|
|
96
|
+
|
|
97
|
+
const blockerAfter = await store.get(blocker.id)
|
|
98
|
+
expect(blockerAfter).toBeDefined()
|
|
99
|
+
expect(blockerAfter?.blocks).toContain(child.id)
|
|
100
|
+
expect(child.blockedBy).toContain(blocker.id)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('block() skips gracefully when one task disappeared before lock acquired', async () => {
|
|
104
|
+
const a = await store.create({ runId, subject: 'A' })
|
|
105
|
+
const b = await store.create({ runId, subject: 'B' })
|
|
106
|
+
|
|
107
|
+
// Delete B, then try to block a → b. The findTask pre-check passes for a
|
|
108
|
+
// but fails for b, so block() returns early (silent no-op per existing
|
|
109
|
+
// contract).
|
|
110
|
+
await store.delete(b.id)
|
|
111
|
+
|
|
112
|
+
// block() should not throw and A's blocks list should remain empty.
|
|
113
|
+
await store.block(a.id, b.id)
|
|
114
|
+
|
|
115
|
+
const aAfter = await store.get(a.id)
|
|
116
|
+
expect(aAfter?.blocks).not.toContain(b.id)
|
|
117
|
+
})
|
|
118
|
+
})
|
package/src/store/task/disk.ts
CHANGED
|
@@ -61,8 +61,13 @@ export class DiskTaskStore implements TaskStore {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
private async withLock<T>(taskId: TaskId, fn: () => Promise<T>): Promise<T> {
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// Loop instead of single await: after awaiting a lock, the map may
|
|
65
|
+
// already hold a NEW lock acquired by another coroutine that woke
|
|
66
|
+
// up before us. Re-check on each iteration until we observe an empty
|
|
67
|
+
// slot, at which point the synchronous set() below claims it.
|
|
68
|
+
while (true) {
|
|
69
|
+
const existing = this.locks.get(taskId)
|
|
70
|
+
if (!existing) break
|
|
66
71
|
await existing.catch(() => undefined)
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -82,6 +87,22 @@ export class DiskTaskStore implements TaskStore {
|
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Acquires locks on multiple task IDs in a canonical (lexicographic) order
|
|
92
|
+
* to prevent deadlocks when operations touch several related tasks.
|
|
93
|
+
* Duplicates are removed; each ID is locked exactly once.
|
|
94
|
+
*/
|
|
95
|
+
private async withLocks<T>(taskIds: readonly TaskId[], fn: () => Promise<T>): Promise<T> {
|
|
96
|
+
const unique = [...new Set(taskIds)].sort() as TaskId[]
|
|
97
|
+
const acquire = async (i: number): Promise<T> => {
|
|
98
|
+
if (i >= unique.length) return fn()
|
|
99
|
+
const nextId = unique[i]
|
|
100
|
+
if (nextId === undefined) return fn()
|
|
101
|
+
return this.withLock(nextId, () => acquire(i + 1))
|
|
102
|
+
}
|
|
103
|
+
return acquire(0)
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
on(listener: TaskEventListener): () => void {
|
|
86
107
|
this.listeners.push(listener)
|
|
87
108
|
return () => {
|
|
@@ -93,7 +114,12 @@ export class DiskTaskStore implements TaskStore {
|
|
|
93
114
|
for (const listener of this.listeners) {
|
|
94
115
|
try {
|
|
95
116
|
listener(event)
|
|
96
|
-
} catch {
|
|
117
|
+
} catch (err) {
|
|
118
|
+
this.log.warn('Task event listener threw', {
|
|
119
|
+
error: err instanceof Error ? err.message : String(err),
|
|
120
|
+
eventType: event.type,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
97
123
|
}
|
|
98
124
|
}
|
|
99
125
|
|
|
@@ -118,18 +144,27 @@ export class DiskTaskStore implements TaskStore {
|
|
|
118
144
|
|
|
119
145
|
const dir = this.taskDir(runId)
|
|
120
146
|
await mkdir(dir, { recursive: true })
|
|
121
|
-
await atomicWriteJson(this.taskPath(runId, taskId), task)
|
|
122
147
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
148
|
+
const blockers = params.blockedBy ?? []
|
|
149
|
+
if (blockers.length === 0) {
|
|
150
|
+
await atomicWriteJson(this.taskPath(runId, taskId), task)
|
|
151
|
+
} else {
|
|
152
|
+
// Hold locks on all blockers while establishing the bidirectional edge:
|
|
153
|
+
// update each blocker's `blocks` list AND write the new task together,
|
|
154
|
+
// so concurrent delete(blockerId) sees a consistent pair.
|
|
155
|
+
await this.withLocks(blockers, async () => {
|
|
156
|
+
for (const blockerId of blockers) {
|
|
126
157
|
const blocker = await this.readTask(runId, blockerId)
|
|
127
158
|
if (blocker && !blocker.blocks.includes(taskId)) {
|
|
128
159
|
blocker.blocks.push(taskId)
|
|
129
160
|
await atomicWriteJson(this.taskPath(runId, blockerId), blocker)
|
|
130
161
|
}
|
|
131
|
-
|
|
132
|
-
|
|
162
|
+
// If blocker is missing, we still write the new task with its
|
|
163
|
+
// blockedBy reference; the dangling reference is visible to
|
|
164
|
+
// subsequent readers rather than silently pruned.
|
|
165
|
+
}
|
|
166
|
+
await atomicWriteJson(this.taskPath(runId, taskId), task)
|
|
167
|
+
})
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
this.log.info('Task created', { taskId, subject: params.subject, runId })
|
|
@@ -185,30 +220,59 @@ export class DiskTaskStore implements TaskStore {
|
|
|
185
220
|
const found = await this.findTask(id)
|
|
186
221
|
if (!found) return false
|
|
187
222
|
|
|
188
|
-
|
|
223
|
+
// Read the task once (unlocked) to discover its related IDs, then acquire
|
|
224
|
+
// locks on the entire set (self + blockers + blocked) in canonical order.
|
|
225
|
+
// Locking the full set up-front in sorted order prevents deadlock when two
|
|
226
|
+
// deletes race on tasks that mutually reference each other.
|
|
227
|
+
//
|
|
228
|
+
// Known trade-off: the lock set is computed from the unlocked preview. If
|
|
229
|
+
// create()/block() adds a NEW relation between preview and lock acquisition,
|
|
230
|
+
// we will mutate that neighbor without holding its lock. The alternative
|
|
231
|
+
// (retry loop with expanding lock set) adds substantial complexity for a
|
|
232
|
+
// rare interleaving in a single-tenant single-writer store; revisit if the
|
|
233
|
+
// store grows concurrent writers.
|
|
234
|
+
const preview = await this.readTask(found.runId, id)
|
|
235
|
+
if (!preview) return false
|
|
236
|
+
|
|
237
|
+
const relatedIds: TaskId[] = [id, ...preview.blockedBy, ...preview.blocks]
|
|
238
|
+
|
|
239
|
+
return this.withLocks(relatedIds, async () => {
|
|
240
|
+
// Re-read under lock: the task's block graph may have changed between
|
|
241
|
+
// the unlocked preview and lock acquisition.
|
|
189
242
|
const task = await this.readTask(found.runId, id)
|
|
190
243
|
if (!task) return false
|
|
191
244
|
|
|
192
245
|
for (const blockerId of task.blockedBy) {
|
|
193
|
-
await this.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
})
|
|
246
|
+
const blocker = await this.readTask(task.runId, blockerId)
|
|
247
|
+
if (blocker) {
|
|
248
|
+
blocker.blocks = blocker.blocks.filter((bid) => bid !== id)
|
|
249
|
+
await atomicWriteJson(this.taskPath(task.runId, blockerId), blocker)
|
|
250
|
+
}
|
|
200
251
|
}
|
|
201
252
|
for (const blockedId of task.blocks) {
|
|
202
|
-
await this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
})
|
|
253
|
+
const blocked = await this.readTask(task.runId, blockedId)
|
|
254
|
+
if (blocked) {
|
|
255
|
+
blocked.blockedBy = blocked.blockedBy.filter((bid) => bid !== id)
|
|
256
|
+
await atomicWriteJson(this.taskPath(task.runId, blockedId), blocked)
|
|
257
|
+
}
|
|
209
258
|
}
|
|
210
259
|
|
|
211
|
-
|
|
260
|
+
try {
|
|
261
|
+
await unlink(this.taskPath(task.runId, id))
|
|
262
|
+
} catch (err) {
|
|
263
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
264
|
+
if (code !== 'ENOENT') {
|
|
265
|
+
this.log.error(
|
|
266
|
+
'Failed to delete task file; relations may be in a partially-updated state',
|
|
267
|
+
{
|
|
268
|
+
taskId: id,
|
|
269
|
+
error: err instanceof Error ? err.message : String(err),
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
throw err
|
|
273
|
+
}
|
|
274
|
+
// ENOENT: already gone, treat as success.
|
|
275
|
+
}
|
|
212
276
|
this.log.info('Task deleted', { taskId: id })
|
|
213
277
|
this.emit({ type: 'task.deleted', taskId: id, task, timestamp: Date.now() })
|
|
214
278
|
return true
|
|
@@ -222,7 +286,13 @@ export class DiskTaskStore implements TaskStore {
|
|
|
222
286
|
let files: string[]
|
|
223
287
|
try {
|
|
224
288
|
files = await readdir(dir)
|
|
225
|
-
} catch {
|
|
289
|
+
} catch (err) {
|
|
290
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
291
|
+
if (code === 'ENOENT') return []
|
|
292
|
+
this.log.warn('Failed to list task directory', {
|
|
293
|
+
dir,
|
|
294
|
+
error: err instanceof Error ? err.message : String(err),
|
|
295
|
+
})
|
|
226
296
|
return []
|
|
227
297
|
}
|
|
228
298
|
|
|
@@ -233,8 +303,11 @@ export class DiskTaskStore implements TaskStore {
|
|
|
233
303
|
const raw = await readFile(join(dir, file), 'utf-8')
|
|
234
304
|
const task = JSON.parse(raw) as Task
|
|
235
305
|
tasks.push(task)
|
|
236
|
-
} catch {
|
|
237
|
-
this.log.warn('Failed to read task file', {
|
|
306
|
+
} catch (err) {
|
|
307
|
+
this.log.warn('Failed to read task file', {
|
|
308
|
+
file,
|
|
309
|
+
error: err instanceof Error ? err.message : String(err),
|
|
310
|
+
})
|
|
238
311
|
}
|
|
239
312
|
}
|
|
240
313
|
|
|
@@ -274,19 +347,40 @@ export class DiskTaskStore implements TaskStore {
|
|
|
274
347
|
const blockedFound = await this.findTask(blockedId)
|
|
275
348
|
if (!blockerFound || !blockedFound) return
|
|
276
349
|
|
|
277
|
-
|
|
350
|
+
// Acquire BOTH locks before mutating either side of the edge. Sequential
|
|
351
|
+
// single-locks allow a concurrent operation to interleave and observe
|
|
352
|
+
// a half-established relationship.
|
|
353
|
+
await this.withLocks([blockerId, blockedId], async () => {
|
|
278
354
|
const blocker = await this.readTask(blockerFound.runId, blockerId)
|
|
279
|
-
|
|
355
|
+
const blocked = await this.readTask(blockedFound.runId, blockedId)
|
|
356
|
+
|
|
357
|
+
// Re-validate under lock: either side may have been deleted between
|
|
358
|
+
// the pre-check (findTask) and lock acquisition. Establishing only
|
|
359
|
+
// one side of the edge would leave a dangling reference; skip the
|
|
360
|
+
// whole operation instead.
|
|
361
|
+
if (!blocker || !blocked) {
|
|
362
|
+
this.log.warn('block(): task disappeared before lock acquired; skipping', {
|
|
363
|
+
blockerId,
|
|
364
|
+
blockedId,
|
|
365
|
+
blockerExists: !!blocker,
|
|
366
|
+
blockedExists: !!blocked,
|
|
367
|
+
})
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let mutated = false
|
|
372
|
+
if (!blocker.blocks.includes(blockedId)) {
|
|
280
373
|
blocker.blocks.push(blockedId)
|
|
281
374
|
await atomicWriteJson(this.taskPath(blocker.runId, blockerId), blocker)
|
|
375
|
+
mutated = true
|
|
282
376
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await this.withLock(blockedId, async () => {
|
|
286
|
-
const blocked = await this.readTask(blockedFound.runId, blockedId)
|
|
287
|
-
if (blocked && !blocked.blockedBy.includes(blockerId)) {
|
|
377
|
+
if (!blocked.blockedBy.includes(blockerId)) {
|
|
288
378
|
blocked.blockedBy.push(blockerId)
|
|
289
379
|
await atomicWriteJson(this.taskPath(blocked.runId, blockedId), blocked)
|
|
380
|
+
mutated = true
|
|
381
|
+
}
|
|
382
|
+
if (!mutated) {
|
|
383
|
+
this.log.debug('block(): edge already exists', { blockerId, blockedId })
|
|
290
384
|
}
|
|
291
385
|
})
|
|
292
386
|
}
|
|
@@ -307,10 +401,29 @@ export class DiskTaskStore implements TaskStore {
|
|
|
307
401
|
}
|
|
308
402
|
|
|
309
403
|
private async readTask(runId: RunId, taskId: TaskId): Promise<Task | null> {
|
|
404
|
+
const path = this.taskPath(runId, taskId)
|
|
405
|
+
let raw: string
|
|
406
|
+
try {
|
|
407
|
+
raw = await readFile(path, 'utf-8')
|
|
408
|
+
} catch (err) {
|
|
409
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
410
|
+
if (code === 'ENOENT') return null
|
|
411
|
+
this.log.warn('Failed to read task', {
|
|
412
|
+
taskId,
|
|
413
|
+
path,
|
|
414
|
+
error: err instanceof Error ? err.message : String(err),
|
|
415
|
+
})
|
|
416
|
+
return null
|
|
417
|
+
}
|
|
418
|
+
|
|
310
419
|
try {
|
|
311
|
-
const raw = await readFile(this.taskPath(runId, taskId), 'utf-8')
|
|
312
420
|
return JSON.parse(raw) as Task
|
|
313
|
-
} catch {
|
|
421
|
+
} catch (err) {
|
|
422
|
+
this.log.error('Corrupt task JSON on disk', {
|
|
423
|
+
taskId,
|
|
424
|
+
path,
|
|
425
|
+
error: err instanceof Error ? err.message : String(err),
|
|
426
|
+
})
|
|
314
427
|
return null
|
|
315
428
|
}
|
|
316
429
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
|
-
import type { ConnectorInstanceId } from '../ids/index.js'
|
|
2
|
+
import type { ConnectorId, ConnectorInstanceId } from '../ids/index.js'
|
|
3
3
|
|
|
4
4
|
export type ConnectionType = 'http' | 'webhook' | 'custom'
|
|
5
5
|
|
|
@@ -39,7 +39,7 @@ export interface ConnectorTrigger {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface ConnectorEvent {
|
|
42
|
-
connectorId:
|
|
42
|
+
connectorId: ConnectorId
|
|
43
43
|
instanceId: ConnectorInstanceId
|
|
44
44
|
trigger: string
|
|
45
45
|
payload: unknown
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
|
-
import type { ConnectorInstanceId } from '../ids/index.js'
|
|
2
|
+
import type { ConnectorId, ConnectorInstanceId } from '../ids/index.js'
|
|
3
3
|
import type {
|
|
4
4
|
AuthConfig,
|
|
5
5
|
AuthType,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from './core.js'
|
|
12
12
|
|
|
13
13
|
export interface ConnectorDefinition<TConfig = unknown> {
|
|
14
|
-
id:
|
|
14
|
+
id: ConnectorId
|
|
15
15
|
name: string
|
|
16
16
|
description: string
|
|
17
17
|
version?: string
|
|
@@ -24,7 +24,7 @@ export interface ConnectorDefinition<TConfig = unknown> {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface ConnectorConfig {
|
|
27
|
-
connectorId:
|
|
27
|
+
connectorId: ConnectorId
|
|
28
28
|
name: string
|
|
29
29
|
auth?: AuthConfig
|
|
30
30
|
options?: Record<string, unknown>
|
|
@@ -32,7 +32,7 @@ export interface ConnectorConfig {
|
|
|
32
32
|
|
|
33
33
|
export interface ConnectorInstance {
|
|
34
34
|
id: ConnectorInstanceId
|
|
35
|
-
connectorId:
|
|
35
|
+
connectorId: ConnectorId
|
|
36
36
|
config: ConnectorConfig
|
|
37
37
|
status: ConnectorStatus
|
|
38
38
|
createdAt: number
|
|
@@ -63,9 +63,9 @@ export interface ConnectorLifecycle<TConfig = unknown> {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export type ConnectorLifecycleEvent =
|
|
66
|
-
| { type: 'connector_registered'; connectorId:
|
|
67
|
-
| { type: 'connector_unregistered'; connectorId:
|
|
68
|
-
| { type: 'instance_created'; instanceId: ConnectorInstanceId; connectorId:
|
|
66
|
+
| { type: 'connector_registered'; connectorId: ConnectorId }
|
|
67
|
+
| { type: 'connector_unregistered'; connectorId: ConnectorId }
|
|
68
|
+
| { type: 'instance_created'; instanceId: ConnectorInstanceId; connectorId: ConnectorId }
|
|
69
69
|
| { type: 'instance_connecting'; instanceId: ConnectorInstanceId }
|
|
70
70
|
| { type: 'instance_connected'; instanceId: ConnectorInstanceId }
|
|
71
71
|
| { type: 'instance_disconnected'; instanceId: ConnectorInstanceId }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConnectorInstanceId, MCPClientId, MCPServerId } from '../ids/index.js'
|
|
1
|
+
import type { ConnectorId, ConnectorInstanceId, MCPClientId, MCPServerId } from '../ids/index.js'
|
|
2
2
|
import type {
|
|
3
3
|
ConnectorDefinition,
|
|
4
4
|
ConnectorExecuteParams,
|
|
@@ -180,7 +180,7 @@ export interface MCPConnectorBridgeConfig {
|
|
|
180
180
|
|
|
181
181
|
export interface MCPConnectorBridgeToolMapping {
|
|
182
182
|
mcpToolName: string
|
|
183
|
-
connectorId:
|
|
183
|
+
connectorId: ConnectorId
|
|
184
184
|
instanceId: ConnectorInstanceId
|
|
185
185
|
methodName: string
|
|
186
186
|
}
|
|
@@ -205,8 +205,8 @@ export type MCPEventListener = (event: MCPLifecycleEvent) => void
|
|
|
205
205
|
type ConnectorManager = {
|
|
206
206
|
getInstance(instanceId: ConnectorInstanceId): ConnectorInstance | undefined
|
|
207
207
|
getRegistry(): {
|
|
208
|
-
get(connectorId:
|
|
209
|
-
getOrThrow(connectorId:
|
|
208
|
+
get(connectorId: ConnectorId): ConnectorDefinition | undefined
|
|
209
|
+
getOrThrow(connectorId: ConnectorId): ConnectorDefinition
|
|
210
210
|
}
|
|
211
211
|
listConnectedInstances(): ConnectorInstance[]
|
|
212
212
|
execute(params: ConnectorExecuteParams): Promise<ConnectorExecuteResult>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConnectorId } from '../ids/index.js'
|
|
1
2
|
import type { AuthConfig } from './core.js'
|
|
2
3
|
import type { ConnectorConfig } from './definition.js'
|
|
3
4
|
|
|
@@ -18,7 +19,7 @@ export interface ScopeRef {
|
|
|
18
19
|
|
|
19
20
|
export interface ScopedConnectorConfig {
|
|
20
21
|
scope: ScopeRef
|
|
21
|
-
connectorId:
|
|
22
|
+
connectorId: ConnectorId
|
|
22
23
|
|
|
23
24
|
config?: Partial<ConnectorConfig>
|
|
24
25
|
|
|
@@ -30,7 +31,7 @@ export interface ScopedConnectorConfig {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface ResolvedConnectorConfig {
|
|
33
|
-
connectorId:
|
|
34
|
+
connectorId: ConnectorId
|
|
34
35
|
config: ConnectorConfig
|
|
35
36
|
auth?: AuthConfig
|
|
36
37
|
enabled: boolean
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CredentialId, EnvironmentId, TenantId } from '../ids/index.js'
|
|
1
|
+
import type { ConnectorId, CredentialId, EnvironmentId, TenantId } from '../ids/index.js'
|
|
2
2
|
import type { AuthConfig, AuthType } from './core.js'
|
|
3
3
|
|
|
4
4
|
export type EnvironmentTier = 'production' | 'staging' | 'development' | 'testing'
|
|
@@ -25,7 +25,7 @@ export interface TenantRateLimitConfig {
|
|
|
25
25
|
|
|
26
26
|
export interface CredentialRef {
|
|
27
27
|
id: CredentialId
|
|
28
|
-
connectorId:
|
|
28
|
+
connectorId: ConnectorId
|
|
29
29
|
tenantId: TenantId
|
|
30
30
|
label: string
|
|
31
31
|
authType: AuthType
|
|
@@ -36,11 +36,11 @@ export interface CredentialRef {
|
|
|
36
36
|
export interface CredentialVault {
|
|
37
37
|
store(
|
|
38
38
|
tenantId: TenantId,
|
|
39
|
-
connectorId:
|
|
39
|
+
connectorId: ConnectorId,
|
|
40
40
|
label: string,
|
|
41
41
|
auth: AuthConfig,
|
|
42
42
|
): Promise<CredentialRef>
|
|
43
43
|
retrieve(credentialId: CredentialId): Promise<AuthConfig | undefined>
|
|
44
44
|
revoke(credentialId: CredentialId): Promise<boolean>
|
|
45
|
-
list(tenantId: TenantId, connectorId?:
|
|
45
|
+
list(tenantId: TenantId, connectorId?: ConnectorId): Promise<CredentialRef[]>
|
|
46
46
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DocumentId, KnowledgeBaseId } from '../ids/index.js'
|
|
1
|
+
import type { DocumentId, KnowledgeBaseId, TenantId } from '../ids/index.js'
|
|
2
2
|
import type { ChunkingConfig } from './chunking.js'
|
|
3
3
|
import type { EmbeddingConfig } from './embedding.js'
|
|
4
4
|
import type { IngestionResult } from './ingestion.js'
|
|
@@ -9,7 +9,7 @@ export interface KnowledgeBaseConfig {
|
|
|
9
9
|
id?: KnowledgeBaseId
|
|
10
10
|
name: string
|
|
11
11
|
description?: string
|
|
12
|
-
tenantId:
|
|
12
|
+
tenantId: TenantId
|
|
13
13
|
namespace?: string
|
|
14
14
|
chunking?: Partial<ChunkingConfig>
|
|
15
15
|
retrieval?: Partial<RetrievalConfig>
|
package/src/types/rag/scope.ts
CHANGED
package/src/types/rag/storage.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ChunkId, DocumentId, KnowledgeBaseId } from '../ids/index.js'
|
|
1
|
+
import type { ChunkId, DocumentId, KnowledgeBaseId, TenantId } from '../ids/index.js'
|
|
2
2
|
import type { DocumentMetadata } from './scope.js'
|
|
3
3
|
|
|
4
4
|
export interface Document {
|
|
5
5
|
id: DocumentId
|
|
6
6
|
knowledgeBaseId: KnowledgeBaseId
|
|
7
|
-
tenantId:
|
|
7
|
+
tenantId: TenantId
|
|
8
8
|
content: string
|
|
9
9
|
metadata: DocumentMetadata
|
|
10
10
|
createdAt: number
|
|
@@ -15,7 +15,7 @@ export interface Chunk {
|
|
|
15
15
|
id: ChunkId
|
|
16
16
|
documentId: DocumentId
|
|
17
17
|
knowledgeBaseId: KnowledgeBaseId
|
|
18
|
-
tenantId:
|
|
18
|
+
tenantId: TenantId
|
|
19
19
|
content: string
|
|
20
20
|
index: number
|
|
21
21
|
tokenCount: number
|
package/src/types/rag/vector.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChunkId, DocumentId, KnowledgeBaseId } from '../ids/index.js'
|
|
1
|
+
import type { ChunkId, DocumentId, KnowledgeBaseId, TenantId } from '../ids/index.js'
|
|
2
2
|
import type { Chunk } from './storage.js'
|
|
3
3
|
|
|
4
4
|
export interface VectorSearchResult {
|
|
@@ -9,7 +9,7 @@ export interface VectorSearchResult {
|
|
|
9
9
|
export interface VectorStoreQuery {
|
|
10
10
|
embedding: number[]
|
|
11
11
|
topK: number
|
|
12
|
-
tenantId:
|
|
12
|
+
tenantId: TenantId
|
|
13
13
|
knowledgeBaseId?: KnowledgeBaseId
|
|
14
14
|
filter?: Record<string, unknown>
|
|
15
15
|
minScore?: number
|
|
@@ -20,5 +20,5 @@ export interface VectorStore {
|
|
|
20
20
|
search(query: VectorStoreQuery): Promise<VectorSearchResult[]>
|
|
21
21
|
delete(chunkIds: ChunkId[]): Promise<void>
|
|
22
22
|
deleteByDocument(documentId: DocumentId): Promise<void>
|
|
23
|
-
deleteByKnowledgeBase(knowledgeBaseId: KnowledgeBaseId, tenantId:
|
|
23
|
+
deleteByKnowledgeBase(knowledgeBaseId: KnowledgeBaseId, tenantId: TenantId): Promise<void>
|
|
24
24
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthConfig, CredentialRef, CredentialVault } from '../types/connector/index.js'
|
|
2
|
-
import type { CredentialId, TenantId } from '../types/ids/index.js'
|
|
2
|
+
import type { ConnectorId, CredentialId, TenantId } from '../types/ids/index.js'
|
|
3
3
|
import { generateCredentialId } from '../utils/id.js'
|
|
4
4
|
import { type Logger, getRootLogger } from '../utils/logger.js'
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ export class InMemoryCredentialVault implements CredentialVault {
|
|
|
14
14
|
|
|
15
15
|
async store(
|
|
16
16
|
tenantId: TenantId,
|
|
17
|
-
connectorId:
|
|
17
|
+
connectorId: ConnectorId,
|
|
18
18
|
label: string,
|
|
19
19
|
auth: AuthConfig,
|
|
20
20
|
): Promise<CredentialRef> {
|
|
@@ -48,7 +48,7 @@ export class InMemoryCredentialVault implements CredentialVault {
|
|
|
48
48
|
return existed
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
async list(tenantId: TenantId, connectorId?:
|
|
51
|
+
async list(tenantId: TenantId, connectorId?: ConnectorId): Promise<CredentialRef[]> {
|
|
52
52
|
const results: CredentialRef[] = []
|
|
53
53
|
for (const ref of this.refs.values()) {
|
|
54
54
|
if (ref.tenantId !== tenantId) continue
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/manager/agent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/manager/agent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/registry/agent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/registry/agent/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/router/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/router/index.js
DELETED