@namzu/sdk 12.2.0 → 13.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 +113 -0
- package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.d.ts +2 -0
- package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.d.ts.map +1 -0
- package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.js +55 -0
- package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.js.map +1 -0
- package/dist/manager/agent/lifecycle.d.ts +15 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +70 -26
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- 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/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.d.ts +21 -0
- package/dist/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.d.ts.map +1 -0
- package/dist/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.js +205 -0
- package/dist/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.js.map +1 -0
- package/dist/session/__tests__/integration/handoff-single-e2e.test.js +14 -5
- package/dist/session/__tests__/integration/handoff-single-e2e.test.js.map +1 -1
- package/dist/session/errors.d.ts +25 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +21 -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 +10 -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 +27 -4
- package/dist/session/handoff/single.js.map +1 -1
- package/dist/store/session/__tests__/a-session-has-one-writer.test.d.ts +2 -0
- package/dist/store/session/__tests__/a-session-has-one-writer.test.d.ts.map +1 -0
- package/dist/store/session/__tests__/a-session-has-one-writer.test.js +79 -0
- package/dist/store/session/__tests__/a-session-has-one-writer.test.js.map +1 -0
- package/dist/store/session/disk.d.ts +1 -1
- package/dist/store/session/disk.d.ts.map +1 -1
- package/dist/store/session/disk.js +15 -2
- package/dist/store/session/disk.js.map +1 -1
- package/dist/store/session/memory.d.ts +1 -1
- package/dist/store/session/memory.d.ts.map +1 -1
- package/dist/store/session/memory.js +14 -2
- package/dist/store/session/memory.js.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/bridge/a2a/__tests__/project-is-the-a2a-context.test.ts +66 -0
- package/src/manager/agent/lifecycle.ts +71 -26
- package/src/public-runtime.ts +6 -0
- package/src/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.ts +270 -0
- package/src/session/__tests__/integration/handoff-single-e2e.test.ts +14 -5
- package/src/session/errors.ts +29 -0
- package/src/session/handoff/broadcast.ts +10 -5
- package/src/session/handoff/single.ts +27 -4
- package/src/store/session/__tests__/a-session-has-one-writer.test.ts +116 -0
- package/src/store/session/disk.ts +21 -2
- package/src/store/session/memory.ts +20 -2
- package/src/types/session/store.ts +28 -1
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { appendFile, mkdir, readFile, readdir, rm } from 'node:fs/promises'
|
|
25
25
|
import { join } from 'node:path'
|
|
26
|
-
import { TenantIsolationError } from '../../session/errors.js'
|
|
26
|
+
import { StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
27
27
|
import { SessionAlreadySummarizedError } from '../../session/summary/errors.js'
|
|
28
28
|
import type { MessageId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
29
29
|
import type { Message } from '../../types/message/index.js'
|
|
@@ -308,7 +308,11 @@ export class DiskSessionStore implements SessionStore {
|
|
|
308
308
|
return results
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
async updateSession(
|
|
311
|
+
async updateSession(
|
|
312
|
+
session: Session,
|
|
313
|
+
tenantId: TenantId,
|
|
314
|
+
expectedOwnerVersion?: number,
|
|
315
|
+
): Promise<void> {
|
|
312
316
|
const located = await this.locateSession(session.id)
|
|
313
317
|
if (!located) {
|
|
314
318
|
throw new Error(`Session ${session.id} not found`)
|
|
@@ -323,6 +327,21 @@ export class DiskSessionStore implements SessionStore {
|
|
|
323
327
|
if (existing) {
|
|
324
328
|
this.assertTenant(existing.tenantId, tenantId, `session(${session.id})`)
|
|
325
329
|
}
|
|
330
|
+
// Against what is on disk, not against the payload. The write itself is
|
|
331
|
+
// atomic; this read-compare-write is NOT a critical section, so two
|
|
332
|
+
// processes can still both pass — see the contract note on
|
|
333
|
+
// `SessionStore.updateSession`. In one process it is the real lock.
|
|
334
|
+
if (
|
|
335
|
+
expectedOwnerVersion !== undefined &&
|
|
336
|
+
existing !== null &&
|
|
337
|
+
existing.ownerVersion !== expectedOwnerVersion
|
|
338
|
+
) {
|
|
339
|
+
throw new StaleSessionError({
|
|
340
|
+
sessionId: session.id,
|
|
341
|
+
expectedVersion: expectedOwnerVersion,
|
|
342
|
+
actualVersion: existing.ownerVersion,
|
|
343
|
+
})
|
|
344
|
+
}
|
|
326
345
|
const updated: Session = { ...session, updatedAt: new Date() }
|
|
327
346
|
await atomicWriteJson(join(located.path, 'session.json'), serializeSession(updated))
|
|
328
347
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (Convention #5 deny-by-default, session-hierarchy.md §12.2).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { TenantIsolationError } from '../../session/errors.js'
|
|
11
|
+
import { StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
12
12
|
import { SessionAlreadySummarizedError } from '../../session/summary/errors.js'
|
|
13
13
|
import type { MessageId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
14
14
|
import type { Message } from '../../types/message/index.js'
|
|
@@ -151,7 +151,11 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
151
151
|
return matches
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
async updateSession(
|
|
154
|
+
async updateSession(
|
|
155
|
+
session: Session,
|
|
156
|
+
tenantId: TenantId,
|
|
157
|
+
expectedOwnerVersion?: number,
|
|
158
|
+
): Promise<void> {
|
|
155
159
|
const record = this.sessions.get(session.id)
|
|
156
160
|
if (!record) {
|
|
157
161
|
throw new Error(`Session ${session.id} not found`)
|
|
@@ -163,6 +167,20 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
163
167
|
resource: `session(${session.id}) payload`,
|
|
164
168
|
})
|
|
165
169
|
}
|
|
170
|
+
// Compared against the STORED version, not against `session.ownerVersion`
|
|
171
|
+
// — the payload is the caller's copy, and comparing it to itself is
|
|
172
|
+
// precisely the check the handoff path was already making and getting
|
|
173
|
+
// nothing from.
|
|
174
|
+
if (
|
|
175
|
+
expectedOwnerVersion !== undefined &&
|
|
176
|
+
record.session.ownerVersion !== expectedOwnerVersion
|
|
177
|
+
) {
|
|
178
|
+
throw new StaleSessionError({
|
|
179
|
+
sessionId: session.id,
|
|
180
|
+
expectedVersion: expectedOwnerVersion,
|
|
181
|
+
actualVersion: record.session.ownerVersion,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
166
184
|
this.sessions.set(session.id, { tenantId, session: { ...session, updatedAt: new Date() } })
|
|
167
185
|
}
|
|
168
186
|
|
|
@@ -105,7 +105,34 @@ export interface SessionStore {
|
|
|
105
105
|
|
|
106
106
|
getSession(sessionId: SessionId, tenantId: TenantId): Promise<Session | null>
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Write a Session back, optionally only if nobody else wrote it first.
|
|
110
|
+
*
|
|
111
|
+
* **`expectedOwnerVersion` is the single-writer lock this level is supposed
|
|
112
|
+
* to own, and it did not exist.** `Session.ownerVersion` is documented as
|
|
113
|
+
* the CAS counter for handoff, but nothing enforced it: both stores
|
|
114
|
+
* overwrote unconditionally, and the handoff's own check compared a
|
|
115
|
+
* snapshot it had read several awaits earlier against itself. Two
|
|
116
|
+
* concurrent handoffs on one idle session both passed, both provisioned a
|
|
117
|
+
* worktree, and one silently erased the other.
|
|
118
|
+
*
|
|
119
|
+
* Supply it and the store compares against the version it HAS STORED —
|
|
120
|
+
* not against the payload, which is the caller's stale copy — and throws
|
|
121
|
+
* {@link StaleSessionError} rather than writing. Omit it and the behaviour
|
|
122
|
+
* is exactly what it always was, which is the compatibility promise: this
|
|
123
|
+
* parameter is optional so that widening the interface stays invisible to
|
|
124
|
+
* callers and harmless to hosts implementing their own store. A required
|
|
125
|
+
* parameter would break every implementor for a guarantee they can opt
|
|
126
|
+
* into.
|
|
127
|
+
*
|
|
128
|
+
* **In-process only, stated rather than implied.** `DiskSessionStore`
|
|
129
|
+
* writes atomically, but its read-compare-write is not a critical section,
|
|
130
|
+
* so two PROCESSES can still both pass the check. Closing that needs a
|
|
131
|
+
* lease with an expiry — not a PID registry, because a Session is durable
|
|
132
|
+
* and written from hosts where a PID is not a checkable fact. The same
|
|
133
|
+
* honesty the spawn lock already carries.
|
|
134
|
+
*/
|
|
135
|
+
updateSession(session: Session, tenantId: TenantId, expectedOwnerVersion?: number): Promise<void>
|
|
109
136
|
|
|
110
137
|
/**
|
|
111
138
|
* List every Session that belongs to the given Thread for the caller's
|