@gea-ai/agent-sdk 0.1.260910-alpha.0 → 0.1.260910-alpha.2

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 (51) hide show
  1. package/README.md +3 -1253
  2. package/dist/agent-channel-author-runtime.d.ts +79 -0
  3. package/dist/agent-channel-author-runtime.d.ts.map +1 -0
  4. package/dist/agent-channel-author-runtime.js +511 -0
  5. package/dist/agent-channel-author-runtime.js.map +1 -0
  6. package/dist/agent-channel-receiver.d.ts +46 -0
  7. package/dist/agent-channel-receiver.d.ts.map +1 -0
  8. package/dist/agent-channel-receiver.js +356 -0
  9. package/dist/agent-channel-receiver.js.map +1 -0
  10. package/dist/agent-channel-runtime.d.ts +6 -0
  11. package/dist/agent-channel-runtime.d.ts.map +1 -0
  12. package/dist/agent-channel-runtime.js +623 -0
  13. package/dist/agent-channel-runtime.js.map +1 -0
  14. package/dist/agent-channel-worker.d.ts +22 -0
  15. package/dist/agent-channel-worker.d.ts.map +1 -0
  16. package/dist/agent-channel-worker.js +335 -0
  17. package/dist/agent-channel-worker.js.map +1 -0
  18. package/dist/agent-session.d.ts.map +1 -1
  19. package/dist/agent-session.js +177 -4
  20. package/dist/agent-session.js.map +1 -1
  21. package/dist/agent-worker.d.ts +27 -1
  22. package/dist/agent-worker.d.ts.map +1 -1
  23. package/dist/agent-worker.js +867 -27
  24. package/dist/agent-worker.js.map +1 -1
  25. package/dist/channels/feishu-codec.d.ts +18 -0
  26. package/dist/channels/feishu-codec.d.ts.map +1 -0
  27. package/dist/channels/feishu-codec.js +109 -0
  28. package/dist/channels/feishu-codec.js.map +1 -0
  29. package/dist/channels/feishu-inbound.d.ts +35 -0
  30. package/dist/channels/feishu-inbound.d.ts.map +1 -0
  31. package/dist/channels/feishu-inbound.js +136 -0
  32. package/dist/channels/feishu-inbound.js.map +1 -0
  33. package/dist/channels/feishu-websocket.d.ts +5 -0
  34. package/dist/channels/feishu-websocket.d.ts.map +1 -0
  35. package/dist/channels/feishu-websocket.js +424 -0
  36. package/dist/channels/feishu-websocket.js.map +1 -0
  37. package/dist/channels/feishu.d.ts +32 -0
  38. package/dist/channels/feishu.d.ts.map +1 -0
  39. package/dist/channels/feishu.js +233 -0
  40. package/dist/channels/feishu.js.map +1 -0
  41. package/dist/channels/index.d.ts +180 -0
  42. package/dist/channels/index.d.ts.map +1 -0
  43. package/dist/channels/index.js +93 -0
  44. package/dist/channels/index.js.map +1 -0
  45. package/dist/index.d.ts +18 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +38 -8
  48. package/dist/index.js.map +1 -1
  49. package/package.json +16 -3
  50. package/src/channels/README.md +298 -0
  51. package/src/channels/feishu.ts +295 -0
@@ -0,0 +1,623 @@
1
+ import { z } from "zod";
2
+ import { AGENT_CHANNEL_EXECUTION_TIMEOUT_MS, AGENT_CHANNEL_EXECUTION_RECOVERY_GRACE_MS, canonicalAgentChannelJson as canonical, createAgentChannelStableId, agentChannelErrorSchema, agentChannelRunTerminalSchema, agentChannelOperationStatusSchema, } from "@gea-ai/contract/agent-channels";
3
+ import { MAX_CHANNEL_QUEUE_SIZE, agentChannelRuntimeIngressSchema, agentChannelRuntimeResolveSchema, agentChannelRuntimeStartReceiptSchema, agentChannelRuntimeProjectionSchema, agentChannelJournalPageSchema, agentChannelOutboundResultSchema, agentChannelDeliveryEventSchema, } from "@gea-ai/contract/agent-channel-runtime";
4
+ const MAX_PENDING_DELIVERIES = 256;
5
+ const RETRY_DELAYS_MS = [1000, 5000, 30000, 120000];
6
+ const opKey = (id) => `v2:operation:${id}`;
7
+ const json = (body, status = 200) => Response.json(body, { status, headers: { "cache-control": "no-store" } });
8
+ const same = (a, b) => canonical(a) === canonical(b);
9
+ const hostEnvelope = z.discriminatedUnion("ok", [
10
+ z.object({ ok: z.literal(true), result: z.unknown() }),
11
+ z.object({ ok: z.literal(false), error: agentChannelErrorSchema }),
12
+ ]);
13
+ async function commandBase(config, executionTarget, kind, operationId = "journal") {
14
+ const addressId = await createAgentChannelStableId("address", config.address);
15
+ return {
16
+ protocolVersion: "gea.channel-command/v2",
17
+ address: config.address,
18
+ addressId,
19
+ executionTarget,
20
+ channelPath: config.channelPath,
21
+ operationId,
22
+ commandId: await createAgentChannelStableId(kind, addressId, operationId),
23
+ };
24
+ }
25
+ async function host(environment, command) {
26
+ const response = await environment.CHANNEL.fetch(`/v2/commands/${command.kind}`, {
27
+ method: "POST",
28
+ headers: { "content-type": "application/json" },
29
+ body: JSON.stringify(command),
30
+ });
31
+ return hostEnvelope.parse(await response.json());
32
+ }
33
+ /** An accepted operation owns its code target; the address owns only stable session state. */
34
+ export async function handleCustomChannelAddressRequest(request, storage, environment) {
35
+ const url = new URL(request.url);
36
+ if (url.pathname === "/v2/capabilities" && request.method === "GET")
37
+ return json({
38
+ controlledCallVersion: 1,
39
+ receiverAuthorizationContextVersion: 1,
40
+ });
41
+ if (url.pathname === "/v2/ingress" && request.method === "POST") {
42
+ const input = agentChannelRuntimeIngressSchema.parse(await request.json());
43
+ if (input.input.principal.principalId !== input.address.principalId)
44
+ return json({ error: { code: "channel_principal_mismatch" } }, 403);
45
+ const fingerprint = await createAgentChannelStableId("input", input.input);
46
+ const result = await storage.transaction(async (tx) => {
47
+ request.signal.throwIfAborted();
48
+ // Native controlled dispatch holds admission authority through this durable transaction.
49
+ if (input.receiver &&
50
+ input.receiver.installationId !== input.address.installationId)
51
+ return { error: "channel_receiver_authority_required" };
52
+ const existingConfig = await tx.get("v2:configuration");
53
+ if (existingConfig && !same(existingConfig.address, input.address))
54
+ return { error: "channel_address_conflict" };
55
+ const config = existingConfig ?? {
56
+ address: input.address,
57
+ sessionId: input.executionTarget.sessionId,
58
+ stateVersion: input.executionTarget.stateVersion,
59
+ readTarget: input.executionTarget,
60
+ channelPath: input.channelPath,
61
+ ...(input.input.initialState === undefined
62
+ ? {}
63
+ : { initialState: input.input.initialState }),
64
+ };
65
+ if (input.expectedSessionId &&
66
+ input.expectedSessionId !== config.sessionId)
67
+ return { error: "channel_execution_target_mismatch" };
68
+ const existing = await tx.get(opKey(input.input.operationId));
69
+ if (existing && existing.fingerprint !== fingerprint)
70
+ return { error: "operation_payload_conflict" };
71
+ if (!existing) {
72
+ if (config.stateVersion !== input.executionTarget.stateVersion)
73
+ return { error: "channel_state_version_incompatible" };
74
+ if (config.sessionId !== input.executionTarget.sessionId ||
75
+ config.readTarget.workerId !== input.executionTarget.workerId ||
76
+ config.channelPath !== input.channelPath)
77
+ return { error: "channel_address_conflict" };
78
+ const queue = (await tx.get("v2:queue")) ?? [];
79
+ if (queue.length >= MAX_CHANNEL_QUEUE_SIZE)
80
+ return { error: "channel_queue_full" };
81
+ request.signal.throwIfAborted();
82
+ await tx.put("v2:configuration", {
83
+ ...config,
84
+ readTarget: input.executionTarget,
85
+ });
86
+ await tx.put(opKey(input.input.operationId), {
87
+ executionTarget: input.executionTarget,
88
+ input: input.input,
89
+ fingerprint,
90
+ status: "accepted",
91
+ });
92
+ await tx.put("v2:queue", [...queue, input.input.operationId]);
93
+ await tx.setAlarm(Date.now());
94
+ }
95
+ return {
96
+ conversationId: input.address.providerLocalKey,
97
+ duplicate: Boolean(existing),
98
+ executionTarget: existing?.executionTarget ?? input.executionTarget,
99
+ operationId: input.input.operationId,
100
+ status: "accepted",
101
+ };
102
+ });
103
+ return "error" in result
104
+ ? json({ error: { code: result.error } }, 409)
105
+ : json(result, 202);
106
+ }
107
+ if (url.pathname === "/v2/session" && request.method === "GET") {
108
+ const config = await storage.get("v2:configuration");
109
+ return json(config ? { address: config.address, sessionId: config.sessionId } : null);
110
+ }
111
+ if (url.pathname === "/v2/wake" && request.method === "POST") {
112
+ await storage.setAlarm(Date.now());
113
+ return json({ accepted: true }, 202);
114
+ }
115
+ if (url.pathname === "/v2/deliveries" && request.method === "GET") {
116
+ const page = z
117
+ .object({
118
+ after: z.string().max(128).optional(),
119
+ limit: z.coerce.number().int().min(1).max(100).default(100),
120
+ })
121
+ .parse(Object.fromEntries(url.searchParams));
122
+ const entries = [
123
+ ...(await storage.list({ prefix: "v2:outbox:" })),
124
+ ].filter(([key]) => !page.after || key > page.after);
125
+ const selected = [];
126
+ let size = 0;
127
+ for (const entry of entries) {
128
+ const bytes = new TextEncoder().encode(JSON.stringify(entry[1])).byteLength;
129
+ if (selected.length &&
130
+ (selected.length >= page.limit || size + bytes > 6 * 1024 * 1024))
131
+ break;
132
+ selected.push(entry);
133
+ size += bytes;
134
+ }
135
+ return json({
136
+ deliveries: selected.map(([, row]) => row),
137
+ nextCursor: selected.length < entries.length ? selected.at(-1)[0] : null,
138
+ pausedDeliveryId: (await storage.get("v2:delivery-paused")) ?? null,
139
+ lastError: (await storage.get("v2:last-error")) ?? null,
140
+ });
141
+ }
142
+ const statusMatch = /^\/v2\/status\/([^/]+)$/u.exec(url.pathname);
143
+ if (statusMatch && request.method === "GET") {
144
+ const operationId = decodeURIComponent(statusMatch[1]);
145
+ const operation = await storage.get(opKey(operationId));
146
+ const config = await storage.get("v2:configuration");
147
+ if (!operation || !config)
148
+ return json({ error: { code: "operation_not_found" } }, 404);
149
+ return json(agentChannelOperationStatusSchema.parse({
150
+ executionTarget: operation.executionTarget,
151
+ operationId,
152
+ status: operation.status,
153
+ ...(operation.command && operation.status !== "accepted"
154
+ ? { runId: operation.command.runId }
155
+ : {}),
156
+ ...(operation.terminal?.status === "succeeded"
157
+ ? { reply: operation.terminal.reply }
158
+ : {}),
159
+ ...(operation.terminal?.status === "failed"
160
+ ? { error: operation.terminal.error }
161
+ : operation.error
162
+ ? { error: operation.error }
163
+ : {}),
164
+ }));
165
+ }
166
+ const operationMatch = /^\/v2\/operations\/([^/]+)$/u.exec(url.pathname);
167
+ if (operationMatch && request.method === "GET") {
168
+ const operation = await storage.get(opKey(decodeURIComponent(operationMatch[1])));
169
+ const deliveries = await storage.list({ prefix: "v2:outbox:" });
170
+ return operation
171
+ ? json({
172
+ ...operation,
173
+ deliveries: [...deliveries.values()].filter((row) => row.task.operationId === operation.input.operationId),
174
+ })
175
+ : json({ error: { code: "operation_not_found" } }, 404);
176
+ }
177
+ if (url.pathname === "/v2/deliveries/resolve" && request.method === "POST") {
178
+ const input = agentChannelRuntimeResolveSchema.parse(await request.json());
179
+ const result = await storage.transaction(async (tx) => {
180
+ const entry = [
181
+ ...(await tx.list({ prefix: "v2:outbox:" })),
182
+ ].find(([, row]) => row.task.id === input.deliveryId);
183
+ if (!entry)
184
+ return { error: "delivery_not_found" };
185
+ const [key, row] = entry;
186
+ if (row.status !== "unknown" && row.status !== "failed")
187
+ return { error: "delivery_not_resolvable" };
188
+ const status = input.action === "sent"
189
+ ? "sent"
190
+ : input.action === "abandon"
191
+ ? "failed"
192
+ : "pending";
193
+ const updated = {
194
+ ...row,
195
+ status,
196
+ nextAttemptAt: Date.now(),
197
+ resolution: {
198
+ actor: input.actor,
199
+ reason: input.reason,
200
+ action: input.action,
201
+ at: Date.now(),
202
+ },
203
+ };
204
+ if (input.action === "retry")
205
+ await tx.put(key, updated);
206
+ else {
207
+ const result = input.action === "sent"
208
+ ? {
209
+ status: "sent",
210
+ receipt: { confirmedBy: input.actor, reason: input.reason },
211
+ }
212
+ : {
213
+ status: "failed",
214
+ error: {
215
+ code: "delivery_abandoned",
216
+ message: input.reason,
217
+ retryable: false,
218
+ },
219
+ };
220
+ await storeDeliveryResult(tx, key, { ...updated, result }, result);
221
+ }
222
+ if ((await tx.get("v2:delivery-paused")) === input.deliveryId)
223
+ await tx.delete("v2:delivery-paused");
224
+ await tx.setAlarm(Date.now());
225
+ return { resolved: true };
226
+ });
227
+ return "error" in result
228
+ ? json({ error: { code: result.error } }, 409)
229
+ : json(result);
230
+ }
231
+ return json({ error: { code: "not_found" } }, 404);
232
+ }
233
+ async function startNext(storage, environment, config) {
234
+ const queue = (await storage.get("v2:queue")) ?? [];
235
+ const operationId = queue[0];
236
+ if (!operationId)
237
+ return;
238
+ const operation = await storage.get(opKey(operationId));
239
+ if (!operation)
240
+ throw new Error("Channel queue refers to a missing operation.");
241
+ if (operation.command &&
242
+ Date.now() <
243
+ operation.command.executionDeadline +
244
+ AGENT_CHANNEL_EXECUTION_RECOVERY_GRACE_MS)
245
+ return;
246
+ const base = await commandBase(config, operation.executionTarget, "start", operationId);
247
+ const command = {
248
+ ...base,
249
+ kind: "start_run",
250
+ input: operation.input,
251
+ executionEpoch: crypto.randomUUID(),
252
+ executionDeadline: Date.now() + AGENT_CHANNEL_EXECUTION_TIMEOUT_MS,
253
+ runId: await createAgentChannelStableId("run", base.addressId, operationId),
254
+ responseMessageId: await createAgentChannelStableId("assistant", base.addressId, operationId),
255
+ userMessageId: await createAgentChannelStableId("user", base.addressId, operationId),
256
+ };
257
+ // The command and its lease exist before dispatch. A process crash is recovered by the next alarm.
258
+ await storage.put(opKey(operationId), { ...operation, command });
259
+ const response = await host(environment, command);
260
+ if (!response.ok) {
261
+ if (!response.error.retryable)
262
+ await storage.transaction(async (tx) => {
263
+ await tx.put(opKey(operationId), {
264
+ ...operation,
265
+ command,
266
+ status: "failed",
267
+ error: response.error,
268
+ });
269
+ await tx.put("v2:queue", ((await tx.get("v2:queue")) ?? []).filter((id) => id !== operationId));
270
+ });
271
+ else
272
+ await storage.put(opKey(operationId), {
273
+ ...operation,
274
+ command: undefined,
275
+ });
276
+ return;
277
+ }
278
+ const accepted = agentChannelRuntimeStartReceiptSchema.parse(response.result);
279
+ if (accepted.commandId !== command.commandId ||
280
+ accepted.executionEpoch !== command.executionEpoch ||
281
+ accepted.runId !== command.runId)
282
+ throw new Error("Channel execution receipt does not match its durable command.");
283
+ await storage.put(opKey(operationId), {
284
+ ...operation,
285
+ command,
286
+ status: "running",
287
+ });
288
+ }
289
+ async function projectEvents(storage, environment, config) {
290
+ const after = (await storage.get("v2:checkpoint")) ?? 0;
291
+ const base = await commandBase(config, config.readTarget, "projection");
292
+ const read = await host(environment, {
293
+ ...base,
294
+ kind: "read_events",
295
+ after,
296
+ limit: 64,
297
+ });
298
+ if (!read.ok)
299
+ throw new Error(read.error.message);
300
+ const page = agentChannelJournalPageSchema.parse(read.result);
301
+ if (!page.events.length)
302
+ return;
303
+ const candidates = await Promise.all(page.events.map(async (event) => {
304
+ const operation = await storage.get(opKey(event.operationId));
305
+ if (!operation)
306
+ throw new Error("Channel event refers to a missing operation.");
307
+ return {
308
+ event,
309
+ input: operation.input,
310
+ executionTarget: operation.executionTarget,
311
+ };
312
+ }));
313
+ // Preserve journal order and never run another operation's events through this deployment.
314
+ const executionTarget = candidates[0].executionTarget;
315
+ const boundary = candidates.findIndex((entry) => !same(entry.executionTarget, executionTarget));
316
+ const events = candidates
317
+ .slice(0, boundary < 0 ? candidates.length : boundary)
318
+ .map(({ event, input }) => ({ event, input }));
319
+ const state = await storage.get("v2:state");
320
+ const projected = await host(environment, {
321
+ ...base,
322
+ executionTarget,
323
+ kind: "project_events",
324
+ events,
325
+ ...(state === undefined ? {} : { state }),
326
+ ...(config.initialState === undefined
327
+ ? {}
328
+ : { initialState: config.initialState }),
329
+ });
330
+ if (!projected.ok)
331
+ throw new Error(projected.error.message);
332
+ const result = agentChannelRuntimeProjectionSchema.parse(projected.result);
333
+ if (result.cursor !== events.at(-1).event.sequence)
334
+ throw new Error("Channel projection cursor mismatch.");
335
+ await storage.transaction(async (tx) => {
336
+ if (((await tx.get("v2:checkpoint")) ?? 0) !== after)
337
+ return;
338
+ const staged = new Map();
339
+ for (const intent of result.intents) {
340
+ if (!events.some(({ event }) => event.id === intent.eventId &&
341
+ event.operationId === intent.operationId))
342
+ throw new Error("Channel output refers to an event outside its projection batch.");
343
+ const identity = await createAgentChannelStableId("outbox", intent.eventId, intent.key);
344
+ const old = staged.get(identity) ??
345
+ (await tx.get(`v2:intent:${identity}`));
346
+ if (old && !same(old.payload, intent.payload))
347
+ throw new Error("Channel output key has conflicting payloads.");
348
+ if (!old)
349
+ staged.set(identity, intent);
350
+ }
351
+ const pending = [
352
+ ...(await tx.list({ prefix: "v2:outbox:" })).values(),
353
+ ].filter((row) => row.status !== "sent" && row.status !== "failed").length;
354
+ if (pending + staged.size > MAX_PENDING_DELIVERIES)
355
+ throw new Error("Channel pending output limit reached (256).");
356
+ let sequence = (await tx.get("v2:outbox-sequence")) ?? 0;
357
+ for (const [identity, intent] of staged) {
358
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(identity));
359
+ const bytes = new Uint8Array(digest).slice(0, 16);
360
+ bytes[6] = (bytes[6] & 15) | 64;
361
+ bytes[8] = (bytes[8] & 63) | 128;
362
+ const hex = [...bytes]
363
+ .map((byte) => byte.toString(16).padStart(2, "0"))
364
+ .join("");
365
+ const id = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
366
+ const row = {
367
+ executionTarget,
368
+ task: { id, ...intent },
369
+ attempt: 0,
370
+ status: "pending",
371
+ ...(result.idempotencyWindowMs
372
+ ? { idempotencyWindowMs: result.idempotencyWindowMs }
373
+ : {}),
374
+ };
375
+ await tx.put(`v2:outbox:${String(++sequence).padStart(16, "0")}`, row);
376
+ await tx.put(`v2:intent:${identity}`, { payload: intent.payload });
377
+ }
378
+ await tx.put("v2:outbox-sequence", sequence);
379
+ await tx.put("v2:state", result.state);
380
+ await tx.put("v2:checkpoint", result.cursor);
381
+ if (result.cursor >= ((await tx.get("v2:journal-unread")) ?? 0))
382
+ await tx.delete("v2:journal-unread");
383
+ const completed = new Set();
384
+ for (const { event, input } of events) {
385
+ if (event.type !== "run.succeeded" && event.type !== "run.failed")
386
+ continue;
387
+ const operation = await tx.get(opKey(input.operationId));
388
+ if (!operation)
389
+ throw new Error("Channel operation disappeared during projection.");
390
+ const terminal = event.type === "run.succeeded"
391
+ ? agentChannelRunTerminalSchema.options[0].parse({
392
+ status: "succeeded",
393
+ ...agentChannelRunTerminalSchema.options[0]
394
+ .omit({ status: true })
395
+ .parse(event.data),
396
+ })
397
+ : agentChannelRunTerminalSchema.options[1].parse({
398
+ status: "failed",
399
+ ...agentChannelRunTerminalSchema.options[1]
400
+ .omit({ status: true })
401
+ .parse(event.data),
402
+ });
403
+ await tx.put(opKey(input.operationId), {
404
+ ...operation,
405
+ status: terminal.status,
406
+ terminal,
407
+ attempt: event.attempt,
408
+ });
409
+ completed.add(input.operationId);
410
+ }
411
+ if (completed.size)
412
+ await tx.put("v2:queue", ((await tx.get("v2:queue")) ?? []).filter((id) => !completed.has(id)));
413
+ });
414
+ }
415
+ async function deliverNext(storage, environment, config) {
416
+ if (await storage.get("v2:delivery-paused"))
417
+ return;
418
+ const entry = [
419
+ ...(await storage.list({ prefix: "v2:outbox:" })),
420
+ ].find(([, row]) => row.status !== "sent" && row.status !== "failed");
421
+ if (!entry)
422
+ return;
423
+ const [key, previous] = entry;
424
+ const now = Date.now();
425
+ let row = previous;
426
+ if (row.nextAttemptAt && row.nextAttemptAt > now)
427
+ return;
428
+ if (row.status === "sending") {
429
+ if (row.deadline && row.deadline > now)
430
+ return;
431
+ row = { ...row, status: "unknown" };
432
+ }
433
+ if (row.status === "unknown" &&
434
+ (!row.idempotencyWindowMs ||
435
+ !row.firstAttemptAt ||
436
+ now >= row.firstAttemptAt + row.idempotencyWindowMs ||
437
+ row.attempt >= 5)) {
438
+ const result = previous.status === "unknown" && previous.result?.status === "unknown"
439
+ ? previous.result
440
+ : {
441
+ status: "unknown",
442
+ error: {
443
+ code: "delivery_result_unknown",
444
+ message: "The provider outcome requires operator resolution.",
445
+ retryable: false,
446
+ },
447
+ };
448
+ await storage.transaction((tx) => storeDeliveryResult(tx, key, { ...row, result }, result));
449
+ await storage.put("v2:delivery-paused", row.task.id);
450
+ return;
451
+ }
452
+ const attempt = row.attempt + 1;
453
+ const firstAttemptAt = row.firstAttemptAt ?? now;
454
+ await storage.put(key, {
455
+ ...row,
456
+ status: "sending",
457
+ attempt,
458
+ firstAttemptAt,
459
+ deadline: now + 30000,
460
+ });
461
+ let result;
462
+ try {
463
+ const response = await host(environment, {
464
+ ...(await commandBase(config, row.executionTarget, "outbound", row.task.operationId)),
465
+ kind: "outbound",
466
+ delivery: { ...row.task, attempt, firstAttemptAt },
467
+ });
468
+ result = response.ok
469
+ ? agentChannelOutboundResultSchema.parse(response.result)
470
+ : {
471
+ status: response.error.retryable ? "unknown" : "failed",
472
+ error: response.error,
473
+ };
474
+ }
475
+ catch {
476
+ result = {
477
+ status: "unknown",
478
+ error: {
479
+ code: "delivery_result_unknown",
480
+ message: "The outbound invocation ended without a durable receipt.",
481
+ retryable: false,
482
+ },
483
+ };
484
+ }
485
+ // A later rejected request cannot establish whether an earlier ambiguous request sent.
486
+ if (row.status === "unknown" && result.status !== "sent")
487
+ result = {
488
+ status: "unknown",
489
+ error: { ...result.error, retryable: false },
490
+ };
491
+ const finalStatus = result.status === "retryable" && attempt >= 5 ? "failed" : result.status;
492
+ await storage.transaction((tx) => storeDeliveryResult(tx, key, {
493
+ ...row,
494
+ status: finalStatus,
495
+ attempt,
496
+ firstAttemptAt,
497
+ result,
498
+ nextAttemptAt: Date.now() +
499
+ (result.status === "retryable"
500
+ ? (result.retryAfterMs ??
501
+ RETRY_DELAYS_MS[Math.min(attempt - 1, 3)])
502
+ : 1000),
503
+ }, result));
504
+ if (result.status === "unknown" &&
505
+ (!row.idempotencyWindowMs ||
506
+ Date.now() >= firstAttemptAt + row.idempotencyWindowMs ||
507
+ attempt >= 5))
508
+ await storage.put("v2:delivery-paused", row.task.id);
509
+ }
510
+ async function storeDeliveryResult(storage, key, row, result) {
511
+ const operation = await storage.get(opKey(row.task.operationId));
512
+ if (!operation?.command)
513
+ throw new Error("Channel delivery has no originating run.");
514
+ const event = agentChannelDeliveryEventSchema.parse({
515
+ id: await createAgentChannelStableId("delivery-event", row.task.id, row.attempt, result.status, row.resolution ?? null),
516
+ operationId: row.task.operationId,
517
+ runId: operation.command.runId,
518
+ attempt: operation.attempt ?? 1,
519
+ type: `delivery.${row.status === "failed" ? "failed" : result.status}`,
520
+ data: {
521
+ deliveryId: row.task.id,
522
+ attempt: row.attempt,
523
+ result,
524
+ ...(row.resolution ? { resolution: row.resolution } : {}),
525
+ },
526
+ });
527
+ await storage.put(key, row);
528
+ await storage.put(`v2:delivery-event:${event.id}`, {
529
+ executionTarget: row.executionTarget,
530
+ deliveryId: row.task.id,
531
+ event,
532
+ });
533
+ }
534
+ async function flushDeliveryEvents(storage, environment, config) {
535
+ for (const [key, pending] of await storage.list({
536
+ prefix: "v2:delivery-event:",
537
+ })) {
538
+ const response = await host(environment, {
539
+ ...(await commandBase(config, pending.executionTarget, "delivery-event", pending.event.operationId)),
540
+ kind: "record_delivery",
541
+ ...pending,
542
+ });
543
+ if (!response.ok)
544
+ throw new Error(response.error.message);
545
+ const { sequence } = z
546
+ .object({ sequence: z.number().int().positive() })
547
+ .parse(response.result);
548
+ await storage.transaction(async (tx) => {
549
+ await tx.put("v2:journal-unread", Math.max(sequence, (await tx.get("v2:journal-unread")) ?? 0));
550
+ await tx.delete(key);
551
+ });
552
+ }
553
+ }
554
+ /** Input execution, event projection, and delivery advance independently on durable alarms. */
555
+ export async function handleCustomChannelAddressAlarm(storage, environment) {
556
+ const config = await storage.get("v2:configuration");
557
+ if (!config)
558
+ return;
559
+ const queue = (await storage.get("v2:queue")) ?? [];
560
+ const pendingEvents = await storage.list({ prefix: "v2:delivery-event:" });
561
+ const paused = await storage.get("v2:delivery-paused");
562
+ const pendingDeliveries = [
563
+ ...(await storage.list({ prefix: "v2:outbox:" })).values(),
564
+ ].some((row) => row.status !== "sent" && row.status !== "failed");
565
+ if (!queue.length &&
566
+ !pendingEvents.size &&
567
+ !(await storage.get("v2:journal-unread")) &&
568
+ (!pendingDeliveries || paused)) {
569
+ await storage.deleteAlarm?.();
570
+ return;
571
+ }
572
+ // Persist recovery before calling the host; clear it after all queues are idle.
573
+ await storage.setAlarm(Date.now() + 1000);
574
+ let projectionSucceeded = false;
575
+ let failed = false;
576
+ try {
577
+ await projectEvents(storage, environment, config);
578
+ projectionSucceeded = true;
579
+ }
580
+ catch (error) {
581
+ failed = true;
582
+ await storage.put("v2:last-error", {
583
+ message: error instanceof Error ? error.message : String(error),
584
+ at: Date.now(),
585
+ });
586
+ }
587
+ const steps = [
588
+ ...(projectionSucceeded
589
+ ? [() => startNext(storage, environment, config)]
590
+ : []),
591
+ () => deliverNext(storage, environment, config),
592
+ () => flushDeliveryEvents(storage, environment, config),
593
+ ];
594
+ for (const step of steps) {
595
+ try {
596
+ await step();
597
+ }
598
+ catch (error) {
599
+ // Alarm is the protocol retry boundary; retain diagnostics without replacing valid state.
600
+ failed = true;
601
+ await storage.put("v2:last-error", {
602
+ message: error instanceof Error ? error.message : String(error),
603
+ at: Date.now(),
604
+ });
605
+ }
606
+ }
607
+ if (!failed)
608
+ await storage.delete("v2:last-error");
609
+ const activeQueue = (await storage.get("v2:queue")) ?? [];
610
+ const unsentEvents = await storage.list({ prefix: "v2:delivery-event:" });
611
+ const unread = await storage.get("v2:journal-unread");
612
+ const blocked = await storage.get("v2:delivery-paused");
613
+ const outstanding = [
614
+ ...(await storage.list({ prefix: "v2:outbox:" })).values(),
615
+ ].filter((row) => row.status !== "sent" && row.status !== "failed");
616
+ if (!failed &&
617
+ !activeQueue.length &&
618
+ !unsentEvents.size &&
619
+ !unread &&
620
+ (!outstanding.length || blocked))
621
+ await storage.deleteAlarm?.();
622
+ }
623
+ //# sourceMappingURL=agent-channel-runtime.js.map