agenthooksprotocol 0.1.1 → 0.2.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.
@@ -1,16 +1,42 @@
1
1
  import { type ContentSelection, type ContentUpload } from "../draft/generated.js";
2
- /** An owned native content stream, distinct from a receiver content reference.
3
- * Construction does not read, lock, or upload the stream. Passing this source to
4
- * an awaited SDK operation transfers cleanup responsibility to that operation;
5
- * callers must not consume or reuse the underlying stream afterward. Native
6
- * streams can prefetch independently; use highWaterMark: 0 to disable that.
7
- */
2
+ /** Compatibility adapter for a native lazy stream. The stream is consumed by
3
+ * its attachment owner, not staged in an invocation store. */
8
4
  export declare class ContentSource {
9
5
  #private;
10
6
  constructor(stream: ReadableStream<Uint8Array>);
11
- /** Native stream identity is preserved for snapshot sharing and cleanup. */
12
7
  get stream(): ReadableStream<Uint8Array>;
13
8
  }
9
+ type ContentMetadata = {
10
+ size?: number | undefined;
11
+ sha256?: string | undefined;
12
+ };
13
+ /** Sole backing owner for immutable bytes or an unread lazy source. Metadata
14
+ * stays on content items. A handle may be shared within, but not across, calls. */
15
+ export declare class Attachment extends ContentSource {
16
+ #private;
17
+ private constructor();
18
+ /** One defensive input copy, retained directly by this owner. */
19
+ static bytes(bytes: Uint8Array): Attachment;
20
+ /** Evaluated once, on demand. The returned mutable buffer is copied once. */
21
+ static lazy(open: (signal: AbortSignal) => Uint8Array | Promise<Uint8Array>, dispose?: () => void | Promise<void>): Attachment;
22
+ /** Adapt a native source without a second snapshot cache. */
23
+ static fromStream(stream: ReadableStream<Uint8Array>): Attachment;
24
+ /** Defensive caller copy; uploads use the same private materialization. */
25
+ read(signal?: AbortSignal, metadata?: ContentMetadata): Promise<Uint8Array>;
26
+ close(): Promise<void>;
27
+ }
28
+ /** Effective slot index. Owners, not an invocation manager, outlive Hooks. */
29
+ export declare class AttachmentContent {
30
+ private readonly bodies;
31
+ private readonly owners;
32
+ private closed;
33
+ constructor(event: unknown);
34
+ get ids(): readonly string[];
35
+ /** @internal Transfer cleanup responsibility without copying backing bytes. */
36
+ detachFrom(pending: Set<Attachment>): void;
37
+ read(id: string, signal?: AbortSignal): Promise<Uint8Array>;
38
+ close(): Promise<void>;
39
+ }
14
40
  /** Producer content is metadata with an owned source or raw stream in `body`. */
15
41
  export interface StreamContentItem {
16
42
  id: string;
@@ -34,19 +60,23 @@ export interface ContentManagerOptions {
34
60
  maxSnapshotBytes?: number;
35
61
  allowLoopback?: boolean;
36
62
  }
37
- /** One manager per producer/session. Selection is NOT authorization: callers must
38
- * authorize/project opaque native/input/output data before prepare. References
39
- * are deliberately never cached: each prepare uploads in its receiver's scope.
63
+ /** Per-invocation delivery and cleanup coordinator, not a content store.
64
+ * Byte materialization lives only in Attachment. This class retains owner
65
+ * identities and non-owning aggregate budget counters, never byte buffers.
66
+ * Selection is not authorization; project opaque host data before prepare.
40
67
  */
41
68
  export declare class ContentManager {
42
- private snapshots;
43
- private readers;
44
- private seenStreams;
69
+ private hasAttachments;
70
+ private readonly ownership;
71
+ /** @internal Build a slot index retaining the existing owners. */
72
+ resultContent(event: unknown): AttachmentContent | undefined;
73
+ private readonly attachments;
45
74
  private lifetime;
46
- private retainedBytes;
47
75
  private closePromise;
48
- private readonly limit;
76
+ private readonly budget;
49
77
  private readonly allowLoopback;
78
+ /** @internal Detach returned owners; dispose everything else in close(). */
79
+ transfer(content: AttachmentContent): void;
50
80
  constructor(options?: ContentManagerOptions);
51
81
  /** Take cleanup ownership without selecting or reading any body. Call before
52
82
  * routing/projection so unmatched and unauthorized sources are also closed.
@@ -57,19 +87,25 @@ export declare class ContentManager {
57
87
  * Reuses the same bounded snapshot as prepare; returns an independent copy.
58
88
  * Aborting the initial read cancels the stream and leaves that snapshot failed.
59
89
  */
60
- readBody(body: ContentSource | ReadableStream<Uint8Array>, signal?: AbortSignal): Promise<Uint8Array>;
90
+ readBody(body: ContentSource | ReadableStream<Uint8Array>, signal?: AbortSignal, metadata?: ContentMetadata): Promise<Uint8Array>;
61
91
  /** Returns a fresh wire tree; never mutates the producer event or reads an
62
92
  * omitted/metadata stream. All selected uploads finish before this resolves.
63
93
  */
64
94
  prepare(event: any, selection: ContentSelection, upload: ContentUpload | undefined, send: ContentUploadRequest, signal?: AbortSignal): Promise<any>;
65
- /** Copy an already selected snapshot without reading unselected sources. */
66
- copySnapshot(body: ReadableStream<Uint8Array>): Promise<Uint8Array | undefined>;
67
- /** Initiates cancellation and releases snapshots; repeated calls return the
68
- * same promise. Never waits for producer-controlled cancellation promises.
69
- */
95
+ /** @internal A protocol correlation lease shares the same owner, never bytes. */
96
+ retainBody(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<{
97
+ owner: Attachment;
98
+ size: number;
99
+ release: () => Promise<void>;
100
+ }>;
101
+ /** @internal Project compatibility results, or preserve the actual owner. */
102
+ resultBody(body: ReadableStream<Uint8Array>, synthesized: boolean): Promise<Attachment | ReadableStream<Uint8Array> | undefined>;
103
+ /** @internal Invocation-scoped borrow of an active protocol exchange. */
104
+ lease(owner: Attachment): () => Promise<void>;
105
+ /** Cancel and dispose remaining owners. No backing bytes live here. */
70
106
  close(): Promise<void>;
71
107
  private validateUpload;
72
108
  private snapshot;
73
- private readSnapshot;
74
109
  }
110
+ export {};
75
111
  //# sourceMappingURL=content.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../src/client/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,uBAAuB,CAAC;AAE/B;;;;;GAKG;AACH,qBAAa,aAAa;;gBAGZ,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC;IAM9C,4EAA4E;IAC5E,IAAI,MAAM,IAAI,cAAc,CAAC,UAAU,CAAC,CAEvC;CACF;AAOD,iFAAiF;AACjF,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,aAAa,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CACjC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,aAAa,KAClB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAID;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,SAAS,CAGb;IACJ,OAAO,CAAC,OAAO,CAAsD;IACrE,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;gBAE5B,OAAO,GAAE,qBAA0B;IAO/C;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAiBzB;;;OAGG;IACG,QAAQ,CACZ,IAAI,EAAE,aAAa,GAAG,cAAc,CAAC,UAAU,CAAC,EAChD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IA4BtB;;OAEG;IACG,OAAO,CACX,KAAK,EAAE,GAAG,EACV,SAAS,EAAE,gBAAgB,EAC3B,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,IAAI,EAAE,oBAAoB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,GAAG,CAAC;IA+If,4EAA4E;IACtE,YAAY,CAChB,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAWlC;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAetB,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,QAAQ;YAWF,YAAY;CAuD3B"}
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../src/client/content.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,6 @@
1
1
  import { parseContentUploadReceipt, } from "../draft/generated.js";
2
- /** An owned native content stream, distinct from a receiver content reference.
3
- * Construction does not read, lock, or upload the stream. Passing this source to
4
- * an awaited SDK operation transfers cleanup responsibility to that operation;
5
- * callers must not consume or reuse the underlying stream afterward. Native
6
- * streams can prefetch independently; use highWaterMark: 0 to disable that.
7
- */
2
+ /** Compatibility adapter for a native lazy stream. The stream is consumed by
3
+ * its attachment owner, not staged in an invocation store. */
8
4
  export class ContentSource {
9
5
  #stream;
10
6
  constructor(stream) {
@@ -12,9 +8,268 @@ export class ContentSource {
12
8
  throw new Error("Content source requires a native ReadableStream");
13
9
  this.#stream = stream;
14
10
  }
15
- /** Native stream identity is preserved for snapshot sharing and cleanup. */
16
- get stream() {
17
- return this.#stream;
11
+ get stream() { return this.#stream; }
12
+ }
13
+ // This is only an identity index for legacy native streams, never a byte store.
14
+ const streamOwners = new WeakMap();
15
+ const retainedOwners = new WeakSet();
16
+ let ownerAccess;
17
+ /** Sole backing owner for immutable bytes or an unread lazy source. Metadata
18
+ * stays on content items. A handle may be shared within, but not across, calls. */
19
+ export class Attachment extends ContentSource {
20
+ #load;
21
+ #snapshot;
22
+ #lifetime = new AbortController();
23
+ #invocation;
24
+ #budget = { used: 0, limit: 64 * 1024 * 1024 };
25
+ #reserved = 0;
26
+ #dispose;
27
+ #disposal;
28
+ #closed;
29
+ #primaryRelease;
30
+ #leases = 1;
31
+ constructor(load, dispose, retained = true) {
32
+ let owner;
33
+ super(new ReadableStream({
34
+ async pull(controller) {
35
+ try {
36
+ controller.enqueue(await owner.read());
37
+ controller.close();
38
+ }
39
+ catch (error) {
40
+ controller.error(error);
41
+ }
42
+ },
43
+ cancel() { return owner.close(); },
44
+ }, { highWaterMark: 0 }));
45
+ owner = this;
46
+ this.#load = load;
47
+ this.#dispose = dispose;
48
+ streamOwners.set(this.stream, this);
49
+ if (retained)
50
+ retainedOwners.add(this);
51
+ }
52
+ static {
53
+ ownerAccess = {
54
+ claim(owner, invocation, budget) {
55
+ if (owner.#invocation && owner.#invocation !== invocation)
56
+ throw new Error("Attachment already belongs to another invocation");
57
+ if (owner.#closed)
58
+ throw new Error("Attachment closed");
59
+ if (!owner.#invocation) {
60
+ if (owner.#snapshot !== undefined)
61
+ throw new Error("Read attachments cannot be transferred to an invocation");
62
+ owner.#invocation = invocation;
63
+ owner.#budget = budget;
64
+ }
65
+ },
66
+ snapshot: (owner, signal) => owner.#materialize(signal),
67
+ peek: (owner) => owner.#snapshot,
68
+ adapt: (stream) => Attachment.#fromStream(stream),
69
+ retain(owner) {
70
+ owner.#lifetime.signal.throwIfAborted();
71
+ owner.#leases++;
72
+ let released;
73
+ return () => released ??= owner.#release();
74
+ },
75
+ };
76
+ }
77
+ /** One defensive input copy, retained directly by this owner. */
78
+ static bytes(bytes) {
79
+ let initial = new Uint8Array(bytes);
80
+ return new Attachment(async (_signal, reserve) => {
81
+ const bytes = initial;
82
+ reserve(bytes.byteLength);
83
+ initial = undefined;
84
+ return bytes;
85
+ }, () => { initial = undefined; });
86
+ }
87
+ /** Evaluated once, on demand. The returned mutable buffer is copied once. */
88
+ static lazy(open, dispose = () => { }) {
89
+ return new Attachment(async (signal, reserve) => {
90
+ const bytes = await open(signal);
91
+ signal.throwIfAborted();
92
+ if (!(bytes instanceof Uint8Array))
93
+ throw new TypeError("Attachment source must return Uint8Array");
94
+ reserve(bytes.byteLength);
95
+ return new Uint8Array(bytes);
96
+ }, dispose);
97
+ }
98
+ /** Adapt a native source without a second snapshot cache. */
99
+ static fromStream(stream) {
100
+ if (!(stream instanceof ReadableStream))
101
+ throw new TypeError("Attachment source requires a native ReadableStream");
102
+ const owner = Attachment.#fromStream(stream);
103
+ retainedOwners.add(owner);
104
+ return owner;
105
+ }
106
+ static #fromStream(stream) {
107
+ const existing = streamOwners.get(stream);
108
+ if (existing)
109
+ return existing;
110
+ let reader;
111
+ const owner = new Attachment(async (signal, reserve) => {
112
+ reader = stream.getReader();
113
+ const chunks = [];
114
+ let size = 0;
115
+ const cancel = () => { void reader?.cancel(signal.reason).catch(() => { }); };
116
+ signal.addEventListener("abort", cancel, { once: true });
117
+ try {
118
+ for (;;) {
119
+ const next = await abortable(reader.read(), signal);
120
+ signal.throwIfAborted();
121
+ if (next.done)
122
+ break;
123
+ if (!(next.value instanceof Uint8Array))
124
+ throw new Error("Content stream must yield Uint8Array chunks");
125
+ reserve(next.value.byteLength);
126
+ chunks.push(new Uint8Array(next.value));
127
+ size += next.value.byteLength;
128
+ }
129
+ const bytes = new Uint8Array(size);
130
+ let offset = 0;
131
+ for (const chunk of chunks) {
132
+ bytes.set(chunk, offset);
133
+ offset += chunk.length;
134
+ }
135
+ return bytes;
136
+ }
137
+ finally {
138
+ signal.removeEventListener("abort", cancel);
139
+ reader.releaseLock();
140
+ reader = undefined;
141
+ }
142
+ }, () => {
143
+ // Request cancellation without awaiting producer cleanup, which may never settle.
144
+ if (reader)
145
+ void reader.cancel().catch(() => { });
146
+ else if (!stream.locked)
147
+ void stream.cancel().catch(() => { });
148
+ }, false);
149
+ streamOwners.set(stream, owner);
150
+ return owner;
151
+ }
152
+ #cleanup() {
153
+ if (!this.#disposal) {
154
+ const dispose = this.#dispose;
155
+ this.#dispose = () => { };
156
+ this.#disposal = Promise.resolve().then(dispose);
157
+ }
158
+ return this.#disposal;
159
+ }
160
+ #materialize(signal) {
161
+ this.#lifetime.signal.throwIfAborted();
162
+ signal?.throwIfAborted();
163
+ if (!this.#snapshot) {
164
+ const load = this.#load;
165
+ this.#load = undefined;
166
+ const abort = () => this.#lifetime.abort(signal?.reason);
167
+ signal?.addEventListener("abort", abort, { once: true });
168
+ this.#snapshot = (async () => {
169
+ try {
170
+ const bytes = await abortable(load(this.#lifetime.signal, (size) => {
171
+ this.#lifetime.signal.throwIfAborted();
172
+ if (this.#budget.used + size > this.#budget.limit)
173
+ throw new Error("Content snapshot memory limit exceeded");
174
+ this.#budget.used += size;
175
+ this.#reserved += size;
176
+ }), this.#lifetime.signal);
177
+ this.#lifetime.signal.throwIfAborted();
178
+ // All loaders allocate ordinary ArrayBuffers before returning bytes.
179
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
180
+ this.#lifetime.signal.throwIfAborted();
181
+ return { bytes, size: bytes.length,
182
+ sha256: Array.from(digest, byte => byte.toString(16).padStart(2, "0")).join("") };
183
+ }
184
+ catch (error) {
185
+ this.#budget.used -= this.#reserved;
186
+ this.#reserved = 0;
187
+ throw error;
188
+ }
189
+ finally {
190
+ signal?.removeEventListener("abort", abort);
191
+ await this.#cleanup();
192
+ }
193
+ })();
194
+ }
195
+ return signal ? abortable(this.#snapshot, signal) : this.#snapshot;
196
+ }
197
+ /** Defensive caller copy; uploads use the same private materialization. */
198
+ async read(signal, metadata = {}) {
199
+ const snapshot = await this.#materialize(signal);
200
+ validateSnapshotMetadata(snapshot, metadata);
201
+ return snapshot.bytes.slice();
202
+ }
203
+ close() {
204
+ return this.#primaryRelease ??= this.#release();
205
+ }
206
+ #release() {
207
+ if (--this.#leases !== 0)
208
+ return Promise.resolve();
209
+ if (this.#closed)
210
+ return this.#closed;
211
+ this.#lifetime.abort(new Error("Attachment closed"));
212
+ this.#load = undefined;
213
+ this.#snapshot = undefined;
214
+ this.#budget.used -= this.#reserved;
215
+ this.#reserved = 0;
216
+ return this.#closed = this.#cleanup();
217
+ }
218
+ }
219
+ /** Effective slot index. Owners, not an invocation manager, outlive Hooks. */
220
+ export class AttachmentContent {
221
+ bodies = new Map();
222
+ owners = new Set();
223
+ closed;
224
+ constructor(event) {
225
+ const seen = new WeakSet();
226
+ const visit = (value, path) => {
227
+ if (!value || typeof value !== "object" || seen.has(value))
228
+ return;
229
+ const stream = sourceStream(value);
230
+ if (stream) {
231
+ const owner = ownerAccess.adapt(stream);
232
+ this.bodies.set(path, { owner });
233
+ this.owners.add(owner);
234
+ return;
235
+ }
236
+ seen.add(value);
237
+ const body = sourceStream(value.body);
238
+ if (typeof value.id === "string" && body) {
239
+ const owner = ownerAccess.adapt(body);
240
+ const previous = this.bodies.get(value.id);
241
+ if (previous && (previous.owner !== owner || previous.size !== value.size || previous.sha256 !== value.sha256))
242
+ throw new Error("Ambiguous content item id");
243
+ this.bodies.set(value.id, { owner, size: value.size, sha256: value.sha256 });
244
+ this.owners.add(owner);
245
+ }
246
+ for (const [key, child] of Object.entries(value)) {
247
+ if (key === "body" && body && typeof value.id === "string")
248
+ continue;
249
+ visit(child, path + "/" + key.replaceAll("~", "~0").replaceAll("/", "~1"));
250
+ }
251
+ };
252
+ visit(event, "");
253
+ }
254
+ get ids() { return [...this.bodies.keys()]; }
255
+ /** @internal Transfer cleanup responsibility without copying backing bytes. */
256
+ detachFrom(pending) {
257
+ for (const owner of this.owners)
258
+ pending.delete(owner);
259
+ }
260
+ async read(id, signal) {
261
+ const body = this.bodies.get(id);
262
+ if (!body)
263
+ throw new Error("Unknown local content item id");
264
+ return body.owner.read(signal, body);
265
+ }
266
+ close() {
267
+ if (this.closed)
268
+ return this.closed;
269
+ this.bodies.clear();
270
+ this.closed = Promise.all([...this.owners].map(owner => owner.close())).then(() => { });
271
+ this.owners.clear();
272
+ return this.closed;
18
273
  }
19
274
  }
20
275
  function sourceStream(value) {
@@ -22,22 +277,35 @@ function sourceStream(value) {
22
277
  return value.stream;
23
278
  return value instanceof ReadableStream ? value : undefined;
24
279
  }
25
- /** One manager per producer/session. Selection is NOT authorization: callers must
26
- * authorize/project opaque native/input/output data before prepare. References
27
- * are deliberately never cached: each prepare uploads in its receiver's scope.
280
+ function validateSnapshotMetadata(snapshot, metadata) {
281
+ if (metadata.size !== undefined && metadata.size !== snapshot.size)
282
+ throw new Error("Content size mismatch");
283
+ if (metadata.sha256 !== undefined && metadata.sha256 !== snapshot.sha256)
284
+ throw new Error("Content SHA-256 mismatch");
285
+ }
286
+ /** Per-invocation delivery and cleanup coordinator, not a content store.
287
+ * Byte materialization lives only in Attachment. This class retains owner
288
+ * identities and non-owning aggregate budget counters, never byte buffers.
289
+ * Selection is not authorization; project opaque host data before prepare.
28
290
  */
29
291
  export class ContentManager {
30
- snapshots = new WeakMap();
31
- readers = new Set();
32
- seenStreams = new Set();
292
+ hasAttachments = false;
293
+ ownership = {};
294
+ /** @internal Build a slot index retaining the existing owners. */
295
+ resultContent(event) {
296
+ return this.hasAttachments ? new AttachmentContent(event) : undefined;
297
+ }
298
+ attachments = new Set();
33
299
  lifetime = new AbortController();
34
- retainedBytes = 0;
35
300
  closePromise;
36
- limit;
301
+ budget;
37
302
  allowLoopback;
303
+ /** @internal Detach returned owners; dispose everything else in close(). */
304
+ transfer(content) { content.detachFrom(this.attachments); }
38
305
  constructor(options = {}) {
39
- this.limit = options.maxSnapshotBytes ?? 64 * 1024 * 1024;
40
- if (!Number.isSafeInteger(this.limit) || this.limit < 0)
306
+ const limit = options.maxSnapshotBytes ?? 64 * 1024 * 1024;
307
+ this.budget = { used: 0, limit };
308
+ if (!Number.isSafeInteger(limit) || limit < 0)
41
309
  throw new Error("Invalid snapshot byte limit");
42
310
  this.allowLoopback = options.allowLoopback ?? false;
43
311
  }
@@ -48,25 +316,40 @@ export class ContentManager {
48
316
  own(event) {
49
317
  this.lifetime.signal.throwIfAborted();
50
318
  const visited = new WeakSet();
319
+ let conflict = false;
51
320
  const visit = (value) => {
52
321
  if (value === null || typeof value !== "object" || visited.has(value))
53
322
  return;
54
323
  visited.add(value);
55
324
  const stream = sourceStream(value);
56
325
  if (stream) {
57
- this.seenStreams.add(stream);
326
+ const existing = streamOwners.get(stream);
327
+ const owner = ownerAccess.adapt(stream);
328
+ try {
329
+ ownerAccess.claim(owner, this.ownership, this.budget);
330
+ }
331
+ catch {
332
+ conflict = true;
333
+ return;
334
+ }
335
+ // Explicit Attachment sources opt into retained result ownership.
336
+ if (existing && retainedOwners.has(existing))
337
+ this.hasAttachments = true;
338
+ this.attachments.add(owner);
58
339
  return;
59
340
  }
60
341
  for (const child of Object.values(value))
61
342
  visit(child);
62
343
  };
63
344
  visit(event);
345
+ if (conflict)
346
+ throw new Error("Attachment already belongs to another invocation");
64
347
  }
65
348
  /** Reads local producer bytes for composition, never remote references.
66
349
  * Reuses the same bounded snapshot as prepare; returns an independent copy.
67
350
  * Aborting the initial read cancels the stream and leaves that snapshot failed.
68
351
  */
69
- async readBody(body, signal) {
352
+ async readBody(body, signal, metadata = {}) {
70
353
  this.lifetime.signal.throwIfAborted();
71
354
  this.own(body);
72
355
  signal?.throwIfAborted();
@@ -82,6 +365,7 @@ export class ContentManager {
82
365
  try {
83
366
  const snapshot = await abortable(this.snapshot(stream, controller.signal), controller.signal);
84
367
  controller.signal.throwIfAborted();
368
+ validateSnapshotMetadata(snapshot, metadata);
85
369
  return snapshot.bytes.slice();
86
370
  }
87
371
  finally {
@@ -155,10 +439,7 @@ export class ContentManager {
155
439
  const timeout = setTimeout(() => controller.abort(new DOMException("Content upload timed out", "TimeoutError")), upload.timeoutMs);
156
440
  try {
157
441
  const snapshot = await abortable(this.snapshot(stream, controller.signal), controller.signal);
158
- if (value.size !== undefined && value.size !== snapshot.size)
159
- throw new Error("Content size mismatch");
160
- if (value.sha256 !== undefined && value.sha256 !== snapshot.sha256)
161
- throw new Error("Content SHA-256 mismatch");
442
+ validateSnapshotMetadata(snapshot, value);
162
443
  if (snapshot.size > upload.maxBytes)
163
444
  throw new Error("Content exceeds upload maxBytes");
164
445
  const response = await abortable(send(upload.endpoint, {
@@ -206,37 +487,56 @@ export class ContentManager {
206
487
  };
207
488
  return visit(event);
208
489
  }
209
- /** Copy an already selected snapshot without reading unselected sources. */
210
- async copySnapshot(body) {
211
- const snapshot = this.snapshots.get(body);
212
- if (!snapshot)
490
+ /** @internal A protocol correlation lease shares the same owner, never bytes. */
491
+ async retainBody(body, signal) {
492
+ const owner = ownerAccess.adapt(body);
493
+ const snapshot = await ownerAccess.snapshot(owner, signal);
494
+ return { owner, size: snapshot.size, release: ownerAccess.retain(owner) };
495
+ }
496
+ /** @internal Project compatibility results, or preserve the actual owner. */
497
+ async resultBody(body, synthesized) {
498
+ const owner = ownerAccess.adapt(body);
499
+ if (this.hasAttachments)
500
+ return owner;
501
+ const pending = ownerAccess.peek(owner);
502
+ if (!synthesized && !pending)
213
503
  return undefined;
214
504
  try {
215
- return (await snapshot).bytes.slice();
505
+ if (synthesized)
506
+ await ownerAccess.snapshot(owner);
507
+ else
508
+ await pending;
216
509
  }
217
510
  catch {
218
- // Failed reads have no reusable result payload; delivery reports the error.
219
511
  return undefined;
220
512
  }
513
+ // Legacy callers receive a lazy view of this same owner, not a queued copy.
514
+ const release = ownerAccess.retain(owner);
515
+ return new ReadableStream({
516
+ async pull(controller) {
517
+ try {
518
+ controller.enqueue(await owner.read());
519
+ controller.close();
520
+ }
521
+ catch (error) {
522
+ controller.error(error);
523
+ }
524
+ finally {
525
+ await release();
526
+ }
527
+ },
528
+ cancel() { return release(); },
529
+ }, { highWaterMark: 0 });
221
530
  }
222
- /** Initiates cancellation and releases snapshots; repeated calls return the
223
- * same promise. Never waits for producer-controlled cancellation promises.
224
- */
531
+ /** @internal Invocation-scoped borrow of an active protocol exchange. */
532
+ lease(owner) { return ownerAccess.retain(owner); }
533
+ /** Cancel and dispose remaining owners. No backing bytes live here. */
225
534
  close() {
226
535
  if (this.closePromise)
227
536
  return this.closePromise;
228
- this.closePromise = Promise.resolve();
229
537
  this.lifetime.abort(new Error("Content manager closed"));
230
- const readers = [...this.readers];
231
- this.snapshots = new WeakMap();
232
- const unread = [...this.seenStreams].filter((stream) => !stream.locked);
233
- this.seenStreams.clear();
234
- this.readers.clear();
235
- for (const reader of readers)
236
- void reader.cancel().catch(() => undefined);
237
- for (const stream of unread)
238
- void stream.cancel().catch(() => undefined);
239
- this.retainedBytes = 0;
538
+ this.closePromise = Promise.all([...this.attachments].map(owner => owner.close())).then(() => { });
539
+ this.attachments.clear();
240
540
  return this.closePromise;
241
541
  }
242
542
  validateUpload(upload) {
@@ -256,65 +556,7 @@ export class ContentManager {
256
556
  throw new Error("Invalid upload budget");
257
557
  }
258
558
  snapshot(stream, signal) {
259
- const existing = this.snapshots.get(stream);
260
- if (existing)
261
- return existing;
262
- const pending = this.readSnapshot(stream, signal);
263
- this.snapshots.set(stream, pending);
264
- return pending;
265
- }
266
- async readSnapshot(stream, signal) {
267
- signal.throwIfAborted();
268
- const reader = stream.getReader();
269
- this.readers.add(reader);
270
- const chunks = [];
271
- let size = 0;
272
- const cancel = () => {
273
- void reader.cancel(signal.reason).catch(() => undefined);
274
- };
275
- signal.addEventListener("abort", cancel, { once: true });
276
- try {
277
- for (;;) {
278
- const next = await abortable(reader.read(), signal);
279
- signal.throwIfAborted();
280
- if (next.done)
281
- break;
282
- if (!(next.value instanceof Uint8Array))
283
- throw new Error("Content stream must yield Uint8Array chunks");
284
- if (this.retainedBytes + next.value.byteLength > this.limit)
285
- throw new Error("Content snapshot memory limit exceeded");
286
- if (next.value.byteLength === 0)
287
- continue;
288
- chunks.push(next.value.slice());
289
- size += next.value.byteLength;
290
- this.retainedBytes += next.value.byteLength;
291
- }
292
- const bytes = new Uint8Array(size);
293
- let offset = 0;
294
- for (const chunk of chunks) {
295
- bytes.set(chunk, offset);
296
- offset += chunk.byteLength;
297
- }
298
- chunks.length = 0;
299
- const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
300
- signal.throwIfAborted();
301
- return {
302
- bytes,
303
- size,
304
- sha256: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
305
- };
306
- }
307
- catch (error) {
308
- if (!this.lifetime.signal.aborted)
309
- this.retainedBytes -= size;
310
- void reader.cancel(error).catch(() => undefined);
311
- throw error;
312
- }
313
- finally {
314
- signal.removeEventListener("abort", cancel);
315
- this.readers.delete(reader);
316
- reader.releaseLock();
317
- }
559
+ return ownerAccess.snapshot(ownerAccess.adapt(stream), signal);
318
560
  }
319
561
  }
320
562
  function isFileReferencePath(path) {