@agent-native/core 0.94.2 → 0.95.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 (85) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/pr-visual-recap.mdx +20 -12
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/artifact-response.ts +154 -1
  6. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +3 -0
  7. package/corpus/core/src/agent/engine/registry.ts +6 -1
  8. package/corpus/core/src/agent/engine/types.ts +2 -0
  9. package/corpus/core/src/cli/code-agent-executor.ts +61 -1
  10. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  11. package/corpus/core/src/cli/recap.ts +262 -25
  12. package/corpus/core/src/client/settings/UsageSection.tsx +26 -5
  13. package/corpus/core/src/index.ts +2 -0
  14. package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
  15. package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
  16. package/corpus/core/src/integrations/adapters/slack.ts +77 -7
  17. package/corpus/core/src/integrations/types.ts +30 -0
  18. package/corpus/core/src/integrations/webhook-handler.ts +54 -18
  19. package/corpus/core/src/scripts/call-agent.ts +1 -0
  20. package/corpus/core/src/server/request-context.ts +2 -0
  21. package/corpus/core/src/usage/store.ts +82 -20
  22. package/corpus/templates/plan/actions/record-recap-usage-cost.ts +49 -0
  23. package/corpus/templates/plan/actions/record-recap-usage.ts +15 -22
  24. package/dist/a2a/artifact-response.d.ts.map +1 -1
  25. package/dist/a2a/artifact-response.js +127 -1
  26. package/dist/a2a/artifact-response.js.map +1 -1
  27. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  28. package/dist/agent/engine/ai-sdk-engine.js +3 -0
  29. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  30. package/dist/agent/engine/registry.d.ts +1 -1
  31. package/dist/agent/engine/registry.d.ts.map +1 -1
  32. package/dist/agent/engine/registry.js +2 -0
  33. package/dist/agent/engine/registry.js.map +1 -1
  34. package/dist/agent/engine/types.d.ts +2 -0
  35. package/dist/agent/engine/types.d.ts.map +1 -1
  36. package/dist/agent/engine/types.js.map +1 -1
  37. package/dist/cli/code-agent-executor.d.ts +2 -0
  38. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  39. package/dist/cli/code-agent-executor.js +52 -3
  40. package/dist/cli/code-agent-executor.js.map +1 -1
  41. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  42. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  43. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  44. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  45. package/dist/cli/recap.d.ts +22 -2
  46. package/dist/cli/recap.d.ts.map +1 -1
  47. package/dist/cli/recap.js +199 -22
  48. package/dist/cli/recap.js.map +1 -1
  49. package/dist/client/settings/UsageSection.d.ts.map +1 -1
  50. package/dist/client/settings/UsageSection.js +12 -3
  51. package/dist/client/settings/UsageSection.js.map +1 -1
  52. package/dist/collab/struct-routes.d.ts +1 -1
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  57. package/dist/integrations/a2a-continuation-processor.js +76 -21
  58. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  59. package/dist/integrations/a2a-continuations-store.d.ts +4 -1
  60. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  61. package/dist/integrations/a2a-continuations-store.js +130 -6
  62. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  63. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  64. package/dist/integrations/adapters/slack.js +56 -7
  65. package/dist/integrations/adapters/slack.js.map +1 -1
  66. package/dist/integrations/types.d.ts +25 -0
  67. package/dist/integrations/types.d.ts.map +1 -1
  68. package/dist/integrations/types.js.map +1 -1
  69. package/dist/integrations/webhook-handler.js +54 -16
  70. package/dist/integrations/webhook-handler.js.map +1 -1
  71. package/dist/resources/handlers.d.ts +2 -2
  72. package/dist/scripts/call-agent.d.ts.map +1 -1
  73. package/dist/scripts/call-agent.js +1 -0
  74. package/dist/scripts/call-agent.js.map +1 -1
  75. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  76. package/dist/server/request-context.d.ts +2 -0
  77. package/dist/server/request-context.d.ts.map +1 -1
  78. package/dist/server/request-context.js.map +1 -1
  79. package/dist/server/transcribe-voice.d.ts +1 -1
  80. package/dist/usage/store.d.ts +21 -0
  81. package/dist/usage/store.d.ts.map +1 -1
  82. package/dist/usage/store.js +54 -19
  83. package/dist/usage/store.js.map +1 -1
  84. package/docs/content/pr-visual-recap.mdx +20 -12
  85. package/package.json +1 -1
@@ -18,7 +18,11 @@ import {
18
18
  type A2AContinuation,
19
19
  } from "./a2a-continuations-store.js";
20
20
  import { signInternalToken } from "./internal-token.js";
21
- import type { PlatformAdapter } from "./types.js";
21
+ import type {
22
+ OutgoingMessage,
23
+ PlatformAdapter,
24
+ PlatformRunProgress,
25
+ } from "./types.js";
22
26
 
23
27
  const PROCESSOR_PATH = `${FRAMEWORK_ROUTE_PREFIX}/integrations/process-a2a-continuation`;
24
28
  const TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
@@ -150,6 +154,8 @@ async function processClaimedContinuation(
150
154
  return;
151
155
  }
152
156
 
157
+ const progress = await resumeA2AContinuationProgress(continuation, adapter);
158
+
153
159
  const auth = await signContinuationToken(continuation);
154
160
  const client = new A2AClient(continuation.agentUrl, auth.apiKey, {
155
161
  requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS,
@@ -162,6 +168,7 @@ async function processClaimedContinuation(
162
168
  while (Date.now() < deadline) {
163
169
  task = await client.getTask(continuation.a2aTaskId);
164
170
  if (TERMINAL_STATES.has(task.status.state)) break;
171
+ await reportA2AContinuationProgress(continuation, progress, task);
165
172
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
166
173
  }
167
174
  } catch (err) {
@@ -171,6 +178,7 @@ async function processClaimedContinuation(
171
178
  continuation,
172
179
  adapter,
173
180
  remotePollFailureReason(continuation),
181
+ progress,
174
182
  );
175
183
  return;
176
184
  }
@@ -182,6 +190,7 @@ async function processClaimedContinuation(
182
190
  continuation,
183
191
  adapter,
184
192
  err instanceof Error ? err.message : String(err),
193
+ progress,
185
194
  );
186
195
  return;
187
196
  }
@@ -190,24 +199,12 @@ async function processClaimedContinuation(
190
199
  }
191
200
 
192
201
  if (!task || !TERMINAL_STATES.has(task.status.state)) {
193
- const recoverableArtifactText = extractRecoverableArtifactText(task);
194
- if (recoverableArtifactText) {
195
- await deliverAndCompleteA2AContinuation(
196
- continuation,
197
- adapter,
198
- formatContinuationArtifactText(
199
- recoverableArtifactText,
200
- continuation.agentUrl,
201
- ),
202
- );
203
- return;
204
- }
205
-
206
202
  if (shouldStopPollingRemoteTask(continuation)) {
207
203
  await notifyAndFailA2AContinuation(
208
204
  continuation,
209
205
  adapter,
210
206
  remotePollFailureReason(continuation),
207
+ progress,
211
208
  );
212
209
  return;
213
210
  }
@@ -219,7 +216,7 @@ async function processClaimedContinuation(
219
216
  const reason =
220
217
  extractTaskText(task) ||
221
218
  `Remote A2A task ${continuation.a2aTaskId} ended with state ${task.status.state}`;
222
- await notifyAndFailA2AContinuation(continuation, adapter, reason);
219
+ await notifyAndFailA2AContinuation(continuation, adapter, reason, progress);
223
220
  return;
224
221
  }
225
222
 
@@ -232,11 +229,64 @@ async function processClaimedContinuation(
232
229
  continuation,
233
230
  adapter,
234
231
  `Remote A2A task ${continuation.a2aTaskId} completed without text`,
232
+ progress,
235
233
  );
236
234
  return;
237
235
  }
238
236
 
239
- await deliverAndCompleteA2AContinuation(continuation, adapter, text);
237
+ await deliverAndCompleteA2AContinuation(
238
+ continuation,
239
+ adapter,
240
+ text,
241
+ progress,
242
+ );
243
+ }
244
+
245
+ async function resumeA2AContinuationProgress(
246
+ continuation: A2AContinuation,
247
+ adapter: PlatformAdapter,
248
+ ): Promise<PlatformRunProgress | null> {
249
+ if (!continuation.progressRef || !adapter.resumeRunProgress) return null;
250
+ try {
251
+ const progress = await adapter.resumeRunProgress(
252
+ continuation.incoming,
253
+ continuation.progressRef,
254
+ );
255
+ if (!progress) return null;
256
+ await progress.onEvent({
257
+ type: "agent_call_progress",
258
+ agent: continuation.agentName,
259
+ state: "working",
260
+ elapsedSeconds: Math.max(
261
+ 0,
262
+ Math.round((Date.now() - continuation.createdAt) / 1_000),
263
+ ),
264
+ detail: "Continuing in the background",
265
+ });
266
+ return progress;
267
+ } catch {
268
+ // A continuation still has a normal reply fallback. Do not log the
269
+ // opaque provider reference or the inbound message payload.
270
+ return null;
271
+ }
272
+ }
273
+
274
+ async function reportA2AContinuationProgress(
275
+ continuation: A2AContinuation,
276
+ progress: PlatformRunProgress | null,
277
+ task: Task,
278
+ ): Promise<void> {
279
+ if (!progress) return;
280
+ await progress.onEvent({
281
+ type: "agent_call_progress",
282
+ agent: continuation.agentName,
283
+ state: task.status.state,
284
+ elapsedSeconds: Math.max(
285
+ 0,
286
+ Math.round((Date.now() - continuation.createdAt) / 1_000),
287
+ ),
288
+ detail: "Still working on the delegated request",
289
+ });
240
290
  }
241
291
 
242
292
  async function waitForContinuationDue(
@@ -262,6 +312,7 @@ async function notifyAndFailA2AContinuation(
262
312
  continuation: A2AContinuation,
263
313
  adapter: PlatformAdapter,
264
314
  reason: string,
315
+ progress: PlatformRunProgress | null = null,
265
316
  ): Promise<void> {
266
317
  const deliveryContinuation = await claimA2AContinuationDelivery(
267
318
  continuation.id,
@@ -273,11 +324,14 @@ async function notifyAndFailA2AContinuation(
273
324
  reason,
274
325
  );
275
326
  try {
327
+ const outgoing = adapter.formatAgentResponse(message);
276
328
  await withTimeout(
277
- adapter.sendResponse(
278
- adapter.formatAgentResponse(message),
279
- deliveryContinuation.incoming,
280
- { placeholderRef: deliveryContinuation.placeholderRef ?? undefined },
329
+ deliverA2AContinuationResponse(
330
+ adapter,
331
+ deliveryContinuation,
332
+ outgoing,
333
+ progress,
334
+ "error",
281
335
  ),
282
336
  PLATFORM_SEND_TIMEOUT_MS,
283
337
  `${deliveryContinuation.platform} failure notification timed out`,
@@ -296,6 +350,7 @@ async function deliverAndCompleteA2AContinuation(
296
350
  continuation: A2AContinuation,
297
351
  adapter: PlatformAdapter,
298
352
  text: string,
353
+ progress: PlatformRunProgress | null = null,
299
354
  ): Promise<void> {
300
355
  const deliveryContinuation = await claimA2AContinuationDelivery(
301
356
  continuation.id,
@@ -303,11 +358,14 @@ async function deliverAndCompleteA2AContinuation(
303
358
  if (!deliveryContinuation) return;
304
359
 
305
360
  try {
361
+ const outgoing = adapter.formatAgentResponse(text);
306
362
  await withTimeout(
307
- adapter.sendResponse(
308
- adapter.formatAgentResponse(text),
309
- deliveryContinuation.incoming,
310
- { placeholderRef: deliveryContinuation.placeholderRef ?? undefined },
363
+ deliverA2AContinuationResponse(
364
+ adapter,
365
+ deliveryContinuation,
366
+ outgoing,
367
+ progress,
368
+ "done",
311
369
  ),
312
370
  PLATFORM_SEND_TIMEOUT_MS,
313
371
  `${deliveryContinuation.platform} response delivery timed out`,
@@ -327,6 +385,42 @@ async function deliverAndCompleteA2AContinuation(
327
385
  await completeAfterSuccessfulDelivery(deliveryContinuation);
328
386
  }
329
387
 
388
+ async function deliverA2AContinuationResponse(
389
+ adapter: PlatformAdapter,
390
+ continuation: A2AContinuation,
391
+ message: OutgoingMessage,
392
+ progress: PlatformRunProgress | null,
393
+ status: "done" | "error",
394
+ ): Promise<void> {
395
+ if (progress) {
396
+ try {
397
+ await progress.onEvent({
398
+ type: "agent_call",
399
+ agent: continuation.agentName,
400
+ status,
401
+ });
402
+ await progress.complete(message);
403
+ return;
404
+ } catch {
405
+ // A resumed Slack stream can no longer be finalized (for example when
406
+ // chat.stopStream rejects). Preserve the final answer with the same
407
+ // thread reply fallback used by the initial webhook run. Also ask the
408
+ // adapter to terminate the native stream: otherwise Slack can keep the
409
+ // task card in its working state after the thread fallback succeeds.
410
+ try {
411
+ await progress.fail?.(
412
+ "I couldn't update the live response, but I posted the final result in this thread.",
413
+ );
414
+ } catch {
415
+ // The thread reply below is still the authoritative final answer.
416
+ }
417
+ }
418
+ }
419
+ await adapter.sendResponse(message, continuation.incoming, {
420
+ placeholderRef: continuation.placeholderRef ?? undefined,
421
+ });
422
+ }
423
+
330
424
  async function rescheduleAndRedispatchA2AContinuation(
331
425
  continuationId: string,
332
426
  ): Promise<void> {
@@ -524,13 +618,6 @@ function extractTaskText(task: Task): string {
524
618
  .join("\n");
525
619
  }
526
620
 
527
- function extractRecoverableArtifactText(task: Task | null): string {
528
- if (!task?.status.message?.metadata?.agentNativeRecoverableArtifacts) {
529
- return "";
530
- }
531
- return extractTaskText(task);
532
- }
533
-
534
621
  function formatContinuationArtifactText(
535
622
  text: string,
536
623
  agentUrl: string,
@@ -10,7 +10,7 @@ import {
10
10
  ensureIndexExists,
11
11
  } from "../db/ddl-guard.js";
12
12
  import { isDuplicateColumnError } from "../db/migrations.js";
13
- import type { IncomingMessage } from "./types.js";
13
+ import type { IncomingMessage, PlatformRunProgressRef } from "./types.js";
14
14
 
15
15
  let _initPromise: Promise<void> | undefined;
16
16
  const PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;
@@ -28,6 +28,8 @@ function buildCreateSql(): string {
28
28
  external_thread_id TEXT NOT NULL,
29
29
  incoming_payload TEXT NOT NULL,
30
30
  placeholder_ref TEXT,
31
+ progress_ref TEXT,
32
+ progress_ref_claimed ${intType()} NOT NULL DEFAULT 0,
31
33
  owner_email TEXT NOT NULL,
32
34
  org_id TEXT,
33
35
  agent_name TEXT NOT NULL,
@@ -76,10 +78,25 @@ async function ensureTable(): Promise<void> {
76
78
  "dedupe_key",
77
79
  `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS dedupe_key TEXT`,
78
80
  );
81
+ await ensureColumnExists(
82
+ "integration_a2a_continuations",
83
+ "progress_ref",
84
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS progress_ref TEXT`,
85
+ );
86
+ await ensureColumnExists(
87
+ "integration_a2a_continuations",
88
+ "progress_ref_claimed",
89
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS progress_ref_claimed ${intType()} NOT NULL DEFAULT 0`,
90
+ );
91
+ await backfillProgressRefOwners(client);
79
92
  await ensureIndexExists(
80
93
  "idx_a2a_continuations_dedupe_key",
81
94
  `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_dedupe_key ON integration_a2a_continuations(integration_task_id, agent_url, dedupe_key)`,
82
95
  );
96
+ await ensureIndexExists(
97
+ "idx_a2a_continuations_one_progress_owner",
98
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_a2a_continuations_one_progress_owner ON integration_a2a_continuations(integration_task_id) WHERE progress_ref_claimed = 1`,
99
+ );
83
100
  return;
84
101
  }
85
102
  // SQLite (local dev): keep existing behavior
@@ -101,11 +118,22 @@ async function ensureTable(): Promise<void> {
101
118
  );
102
119
  await addColumnIfMissing("a2a_auth_token", "TEXT");
103
120
  await addColumnIfMissing("dedupe_key", "TEXT");
121
+ await addColumnIfMissing("progress_ref", "TEXT");
122
+ await addColumnIfMissing(
123
+ "progress_ref_claimed",
124
+ `${intType()} NOT NULL DEFAULT 0`,
125
+ );
126
+ await backfillProgressRefOwners(client);
104
127
  await retryOnDdlRace(() =>
105
128
  client.execute(
106
129
  `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_dedupe_key ON integration_a2a_continuations(integration_task_id, agent_url, dedupe_key)`,
107
130
  ),
108
131
  );
132
+ await retryOnDdlRace(() =>
133
+ client.execute(
134
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_a2a_continuations_one_progress_owner ON integration_a2a_continuations(integration_task_id) WHERE progress_ref_claimed = 1`,
135
+ ),
136
+ );
109
137
  })().catch((err) => {
110
138
  // Retry init on the next call after a failed startup.
111
139
  _initPromise = undefined;
@@ -128,6 +156,33 @@ async function addColumnIfMissing(name: string, definition: string) {
128
156
  }
129
157
  }
130
158
 
159
+ async function backfillProgressRefOwners(
160
+ client: ReturnType<typeof getDbExec>,
161
+ ): Promise<void> {
162
+ await client.execute(`
163
+ UPDATE integration_a2a_continuations AS candidate
164
+ SET progress_ref_claimed = 1
165
+ WHERE candidate.progress_ref IS NOT NULL
166
+ AND candidate.status NOT IN ('completed', 'failed')
167
+ AND candidate.progress_ref_claimed = 0
168
+ AND NOT EXISTS (
169
+ SELECT 1
170
+ FROM integration_a2a_continuations AS owner
171
+ WHERE owner.integration_task_id = candidate.integration_task_id
172
+ AND owner.progress_ref_claimed = 1
173
+ )
174
+ AND candidate.id = (
175
+ SELECT selected.id
176
+ FROM integration_a2a_continuations AS selected
177
+ WHERE selected.integration_task_id = candidate.integration_task_id
178
+ AND selected.progress_ref IS NOT NULL
179
+ AND selected.status NOT IN ('completed', 'failed')
180
+ ORDER BY selected.created_at ASC, selected.id ASC
181
+ LIMIT 1
182
+ )
183
+ `);
184
+ }
185
+
131
186
  export type A2AContinuationStatus =
132
187
  | "pending"
133
188
  | "processing"
@@ -142,6 +197,8 @@ export interface A2AContinuation {
142
197
  externalThreadId: string;
143
198
  incoming: IncomingMessage;
144
199
  placeholderRef: string | null;
200
+ progressRef: PlatformRunProgressRef | null;
201
+ progressRefClaimed: boolean;
145
202
  ownerEmail: string;
146
203
  orgId: string | null;
147
204
  agentName: string;
@@ -158,6 +215,44 @@ export interface A2AContinuation {
158
215
  completedAt: number | null;
159
216
  }
160
217
 
218
+ const MAX_PROGRESS_REF_KIND_CHARS = 128;
219
+ const MAX_PROGRESS_REF_STREAM_TS_CHARS = 256;
220
+
221
+ /**
222
+ * Keep only the tiny, adapter-owned continuation reference. Invalid rows are
223
+ * treated as unavailable rather than throwing during a retry sweep.
224
+ */
225
+ function parseProgressRef(value: unknown): PlatformRunProgressRef | null {
226
+ if (typeof value !== "string" || value.length === 0) return null;
227
+ try {
228
+ const parsed = JSON.parse(value) as unknown;
229
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
230
+ return null;
231
+ }
232
+ const { kind, streamTs } = parsed as Record<string, unknown>;
233
+ if (
234
+ typeof kind !== "string" ||
235
+ typeof streamTs !== "string" ||
236
+ kind.length === 0 ||
237
+ streamTs.length === 0 ||
238
+ kind.length > MAX_PROGRESS_REF_KIND_CHARS ||
239
+ streamTs.length > MAX_PROGRESS_REF_STREAM_TS_CHARS
240
+ ) {
241
+ return null;
242
+ }
243
+ return { kind, streamTs };
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ function serializeProgressRef(value: unknown): string | null {
250
+ const parsed = parseProgressRef(
251
+ typeof value === "string" ? value : JSON.stringify(value),
252
+ );
253
+ return parsed ? JSON.stringify(parsed) : null;
254
+ }
255
+
161
256
  function rowToContinuation(row: Record<string, unknown>): A2AContinuation {
162
257
  return {
163
258
  id: row.id as string,
@@ -166,6 +261,8 @@ function rowToContinuation(row: Record<string, unknown>): A2AContinuation {
166
261
  externalThreadId: row.external_thread_id as string,
167
262
  incoming: JSON.parse(row.incoming_payload as string) as IncomingMessage,
168
263
  placeholderRef: (row.placeholder_ref as string | null) ?? null,
264
+ progressRef: parseProgressRef(row.progress_ref),
265
+ progressRefClaimed: Number(row.progress_ref_claimed ?? 0) === 1,
169
266
  ownerEmail: row.owner_email as string,
170
267
  orgId: (row.org_id as string | null) ?? null,
171
268
  agentName: row.agent_name as string,
@@ -190,6 +287,7 @@ export async function insertA2AContinuation(input: {
190
287
  externalThreadId: string;
191
288
  incoming: IncomingMessage;
192
289
  placeholderRef?: string | null;
290
+ progressRef?: PlatformRunProgressRef | null;
193
291
  ownerEmail: string;
194
292
  orgId?: string | null;
195
293
  agentName: string;
@@ -203,14 +301,15 @@ export async function insertA2AContinuation(input: {
203
301
  const now = Date.now();
204
302
  const id = `a2a-cont-${now}-${Math.random().toString(36).slice(2, 8)}`;
205
303
  const payload = JSON.stringify(input.incoming);
304
+ const progressRef = serializeProgressRef(input.progressRef);
206
305
 
207
306
  try {
208
307
  await client.execute({
209
308
  sql: `INSERT INTO integration_a2a_continuations
210
309
  (id, integration_task_id, platform, external_thread_id, incoming_payload,
211
- placeholder_ref, owner_email, org_id, agent_name, agent_url, dedupe_key, a2a_task_id, a2a_auth_token,
310
+ placeholder_ref, progress_ref, progress_ref_claimed, owner_email, org_id, agent_name, agent_url, dedupe_key, a2a_task_id, a2a_auth_token,
212
311
  status, attempts, next_check_at, created_at, updated_at)
213
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
214
313
  args: [
215
314
  id,
216
315
  input.integrationTaskId,
@@ -218,6 +317,8 @@ export async function insertA2AContinuation(input: {
218
317
  input.externalThreadId,
219
318
  payload,
220
319
  input.placeholderRef ?? null,
320
+ null,
321
+ 0,
221
322
  input.ownerEmail,
222
323
  input.orgId ?? null,
223
324
  input.agentName,
@@ -232,7 +333,6 @@ export async function insertA2AContinuation(input: {
232
333
  now,
233
334
  ],
234
335
  });
235
- return (await getA2AContinuation(id))!;
236
336
  } catch (err: any) {
237
337
  if (!isDuplicateContinuationError(err)) throw err;
238
338
  const existing = await findA2AContinuation(
@@ -240,7 +340,65 @@ export async function insertA2AContinuation(input: {
240
340
  input.agentUrl,
241
341
  input.a2aTaskId,
242
342
  );
243
- if (existing) return existing;
343
+ if (existing) {
344
+ // A retry can reach this row after the original invocation created it
345
+ // without a resumable progress surface (or with one that has gone
346
+ // stale). Keep the most recent valid adapter reference for active work,
347
+ // but never resurrect short-lived delivery state after a terminal row
348
+ // has deliberately scrubbed it.
349
+ if (
350
+ progressRef &&
351
+ existing.status !== "completed" &&
352
+ existing.status !== "failed"
353
+ ) {
354
+ if (existing.progressRefClaimed) {
355
+ if (JSON.stringify(existing.progressRef) !== progressRef) {
356
+ await client.execute({
357
+ sql: `UPDATE integration_a2a_continuations
358
+ SET progress_ref = ?, updated_at = ?
359
+ WHERE id = ? AND status NOT IN ('completed', 'failed')
360
+ AND progress_ref_claimed = 1
361
+ AND (progress_ref IS NULL OR progress_ref <> ?)`,
362
+ args: [progressRef, now, existing.id, progressRef],
363
+ });
364
+ }
365
+ } else {
366
+ await claimA2AContinuationProgressRef(existing.id, progressRef);
367
+ }
368
+ return (await getA2AContinuation(existing.id)) ?? existing;
369
+ }
370
+ return existing;
371
+ }
372
+ throw err;
373
+ }
374
+
375
+ if (progressRef) {
376
+ await claimA2AContinuationProgressRef(id, progressRef);
377
+ }
378
+ return (await getA2AContinuation(id))!;
379
+ }
380
+
381
+ /**
382
+ * A native platform stream has one terminal completion. Claim it for a single
383
+ * downstream continuation, and retain the ownership marker after terminal
384
+ * cleanup scrubs the short-lived stream reference. The partial unique index
385
+ * makes concurrent downstream inserts safe across processes.
386
+ */
387
+ async function claimA2AContinuationProgressRef(
388
+ id: string,
389
+ progressRef: string,
390
+ ): Promise<void> {
391
+ try {
392
+ await getDbExec().execute({
393
+ sql: `UPDATE integration_a2a_continuations
394
+ SET progress_ref = ?, progress_ref_claimed = 1
395
+ WHERE id = ? AND progress_ref_claimed = 0`,
396
+ args: [progressRef, id],
397
+ });
398
+ } catch (err) {
399
+ // A sibling continuation already owns this stream and will finalize it.
400
+ // This continuation still delivers through the normal response path.
401
+ if (isDuplicateContinuationError(err)) return;
244
402
  throw err;
245
403
  }
246
404
  }
@@ -462,7 +620,7 @@ export async function completeA2AContinuation(id: string): Promise<void> {
462
620
  await client.execute({
463
621
  sql: `UPDATE integration_a2a_continuations
464
622
  SET status = ?, updated_at = ?, completed_at = ?,
465
- incoming_payload = ?, a2a_auth_token = NULL
623
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
466
624
  WHERE id = ? AND status IN ('processing', 'delivering', 'completed')`,
467
625
  args: ["completed", now, now, "{}", id],
468
626
  });
@@ -478,7 +636,7 @@ export async function failA2AContinuation(
478
636
  await client.execute({
479
637
  sql: `UPDATE integration_a2a_continuations
480
638
  SET status = ?, updated_at = ?, error_message = ?,
481
- incoming_payload = ?, a2a_auth_token = NULL
639
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
482
640
  WHERE id = ? AND status <> 'completed'`,
483
641
  args: ["failed", now, errorMessage.slice(0, 2000), "{}", id],
484
642
  });
@@ -22,6 +22,7 @@ import type {
22
22
  IntegrationStatus,
23
23
  OutboundTarget,
24
24
  PlatformRunProgress,
25
+ PlatformRunProgressRef,
25
26
  IntegrationContextMessage,
26
27
  IntegrationFileReference,
27
28
  } from "../types.js";
@@ -359,6 +360,16 @@ export function slackAdapter(
359
360
  return startSlackRunProgress(token, incoming);
360
361
  },
361
362
 
363
+ async resumeRunProgress(
364
+ incoming: IncomingMessage,
365
+ ref: PlatformRunProgressRef,
366
+ ): Promise<PlatformRunProgress | null> {
367
+ if (!isSlackStreamProgressRef(ref)) return null;
368
+ const token = await resolveBotToken(incoming);
369
+ if (!token) return null;
370
+ return resumeSlackRunProgress(token, incoming, ref.streamTs);
371
+ },
372
+
362
373
  async sendResponse(
363
374
  message: OutgoingMessage,
364
375
  context: IncomingMessage,
@@ -1273,6 +1284,21 @@ async function postSlackJson(
1273
1284
  return data;
1274
1285
  }
1275
1286
 
1287
+ function streamFailureCode(error: unknown): string {
1288
+ const message = error instanceof Error ? error.message.trim() : "";
1289
+ return /^[a-z0-9_:-]{1,80}$/i.test(message) ? message : "unknown";
1290
+ }
1291
+
1292
+ function streamChunkType(chunk: Record<string, unknown>): string {
1293
+ const type = chunk.type;
1294
+ return type === "task_update" ||
1295
+ type === "plan_update" ||
1296
+ type === "markdown_text" ||
1297
+ type === "blocks"
1298
+ ? type
1299
+ : "unknown";
1300
+ }
1301
+
1276
1302
  async function startSlackRunProgress(
1277
1303
  token: string,
1278
1304
  incoming: IncomingMessage,
@@ -1304,12 +1330,45 @@ async function startSlackRunProgress(
1304
1330
  },
1305
1331
  ],
1306
1332
  });
1307
- } catch {
1333
+ } catch (error) {
1334
+ console.warn("[slack] chat.startStream failed; using standard reply", {
1335
+ errorCode: streamFailureCode(error),
1336
+ isDirectMessage: incoming.conversationType === "dm",
1337
+ hasRecipientTeam: Boolean(incoming.tenantId),
1338
+ hasRecipientUser: Boolean(incoming.senderId),
1339
+ });
1308
1340
  return null;
1309
1341
  }
1310
1342
 
1311
1343
  const streamTs = started.ts;
1312
1344
  if (typeof streamTs !== "string") return null;
1345
+ return createSlackRunProgress(token, incoming, channel, threadTs, streamTs);
1346
+ }
1347
+
1348
+ function isSlackStreamProgressRef(ref: PlatformRunProgressRef): boolean {
1349
+ return (
1350
+ ref.kind === "slack-stream" && /^\d{1,20}\.\d{1,9}$/.test(ref.streamTs)
1351
+ );
1352
+ }
1353
+
1354
+ async function resumeSlackRunProgress(
1355
+ token: string,
1356
+ incoming: IncomingMessage,
1357
+ streamTs: string,
1358
+ ): Promise<PlatformRunProgress | null> {
1359
+ const channel = incoming.platformContext.channelId;
1360
+ const threadTs = incoming.platformContext.threadTs;
1361
+ if (typeof channel !== "string" || typeof threadTs !== "string") return null;
1362
+ return createSlackRunProgress(token, incoming, channel, threadTs, streamTs);
1363
+ }
1364
+
1365
+ function createSlackRunProgress(
1366
+ token: string,
1367
+ incoming: IncomingMessage,
1368
+ channel: string,
1369
+ threadTs: string,
1370
+ streamTs: string,
1371
+ ): PlatformRunProgress {
1313
1372
  const tasks = new Map<string, { title: string; status: string }>();
1314
1373
  const toolTaskIds = new Map<string, string>();
1315
1374
  const agentTaskIds = new Map<string, string>();
@@ -1328,12 +1387,22 @@ async function startSlackRunProgress(
1328
1387
  const now = Date.now();
1329
1388
  const write = async (value: Record<string, unknown>) => {
1330
1389
  lastWriteAt = Date.now();
1331
- await postSlackJson(token, "chat.appendStream", {
1332
- channel,
1333
- ts: streamTs,
1334
- markdown_text: "Progress updated.",
1335
- chunks: [value],
1336
- }).catch(() => {});
1390
+ try {
1391
+ await postSlackJson(token, "chat.appendStream", {
1392
+ channel,
1393
+ ts: streamTs,
1394
+ markdown_text: "Progress updated.",
1395
+ chunks: [value],
1396
+ });
1397
+ } catch (error) {
1398
+ console.warn(
1399
+ "[slack] chat.appendStream failed; progress may be stale",
1400
+ {
1401
+ chunkType: streamChunkType(value),
1402
+ errorCode: streamFailureCode(error),
1403
+ },
1404
+ );
1405
+ }
1337
1406
  };
1338
1407
  if (now - lastWriteAt >= 900) {
1339
1408
  await write(chunk);
@@ -1357,6 +1426,7 @@ async function startSlackRunProgress(
1357
1426
  `${prefix}:${explicit || ++sequence}`.slice(0, 240);
1358
1427
 
1359
1428
  return {
1429
+ ref: { kind: "slack-stream", streamTs },
1360
1430
  async onEvent(event) {
1361
1431
  if (!cancelControl) {
1362
1432
  const context = getIntegrationRequestContext();