@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.d.ts +2 -0
  3. package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.d.ts.map +1 -0
  4. package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.js +55 -0
  5. package/dist/bridge/a2a/__tests__/project-is-the-a2a-context.test.js.map +1 -0
  6. package/dist/manager/agent/lifecycle.d.ts +15 -0
  7. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  8. package/dist/manager/agent/lifecycle.js +70 -26
  9. package/dist/manager/agent/lifecycle.js.map +1 -1
  10. package/dist/public-runtime.d.ts +1 -1
  11. package/dist/public-runtime.d.ts.map +1 -1
  12. package/dist/public-runtime.js +7 -1
  13. package/dist/public-runtime.js.map +1 -1
  14. package/dist/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.d.ts +21 -0
  15. package/dist/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.d.ts.map +1 -0
  16. package/dist/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.js +205 -0
  17. package/dist/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.js.map +1 -0
  18. package/dist/session/__tests__/integration/handoff-single-e2e.test.js +14 -5
  19. package/dist/session/__tests__/integration/handoff-single-e2e.test.js.map +1 -1
  20. package/dist/session/errors.d.ts +25 -0
  21. package/dist/session/errors.d.ts.map +1 -1
  22. package/dist/session/errors.js +21 -0
  23. package/dist/session/errors.js.map +1 -1
  24. package/dist/session/handoff/broadcast.d.ts.map +1 -1
  25. package/dist/session/handoff/broadcast.js +10 -5
  26. package/dist/session/handoff/broadcast.js.map +1 -1
  27. package/dist/session/handoff/single.d.ts.map +1 -1
  28. package/dist/session/handoff/single.js +27 -4
  29. package/dist/session/handoff/single.js.map +1 -1
  30. package/dist/store/session/__tests__/a-session-has-one-writer.test.d.ts +2 -0
  31. package/dist/store/session/__tests__/a-session-has-one-writer.test.d.ts.map +1 -0
  32. package/dist/store/session/__tests__/a-session-has-one-writer.test.js +79 -0
  33. package/dist/store/session/__tests__/a-session-has-one-writer.test.js.map +1 -0
  34. package/dist/store/session/disk.d.ts +1 -1
  35. package/dist/store/session/disk.d.ts.map +1 -1
  36. package/dist/store/session/disk.js +15 -2
  37. package/dist/store/session/disk.js.map +1 -1
  38. package/dist/store/session/memory.d.ts +1 -1
  39. package/dist/store/session/memory.d.ts.map +1 -1
  40. package/dist/store/session/memory.js +14 -2
  41. package/dist/store/session/memory.js.map +1 -1
  42. package/dist/types/session/store.d.ts +28 -1
  43. package/dist/types/session/store.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/bridge/a2a/__tests__/project-is-the-a2a-context.test.ts +66 -0
  46. package/src/manager/agent/lifecycle.ts +71 -26
  47. package/src/public-runtime.ts +6 -0
  48. package/src/session/__tests__/integration/a-successful-delegation-disposes-its-workspace.test.ts +270 -0
  49. package/src/session/__tests__/integration/handoff-single-e2e.test.ts +14 -5
  50. package/src/session/errors.ts +29 -0
  51. package/src/session/handoff/broadcast.ts +10 -5
  52. package/src/session/handoff/single.ts +27 -4
  53. package/src/store/session/__tests__/a-session-has-one-writer.test.ts +116 -0
  54. package/src/store/session/disk.ts +21 -2
  55. package/src/store/session/memory.ts +20 -2
  56. 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(session: Session, tenantId: TenantId): Promise<void> {
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(session: Session, tenantId: TenantId): Promise<void> {
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
- updateSession(session: Session, tenantId: TenantId): Promise<void>
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