@desplega.ai/agent-swarm 1.151.0 → 1.152.1

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 (108) hide show
  1. package/README.md +3 -1
  2. package/dist/{acp-adapter-ex8hzjfc.js → acp-adapter-4s7q25ft.js} +1 -1
  3. package/dist/{actions-2bgpyhww.js → actions-cs6rhmvc.js} +13 -13
  4. package/dist/{app-1706zhr1.js → app-j54aq0dj.js} +3 -3
  5. package/dist/{assistant-bavke5p7.js → assistant-w1pgn7vf.js} +14 -14
  6. package/dist/{boot-reembed-3cmrrmpa.js → boot-reembed-42vgkrrj.js} +4 -4
  7. package/dist/{boot-reembed-nafcmmmc.js → boot-reembed-smy30z32.js} +3 -3
  8. package/dist/{boot-scrub-logs-cd6a2sq0.js → boot-scrub-logs-b6s72fsw.js} +2 -2
  9. package/dist/{claude-adapter-wfp2gzvz.js → claude-adapter-fh7rmcj9.js} +3 -3
  10. package/dist/{claude-sdk-session-bht9jjak.js → claude-sdk-session-xhynatn9.js} +3 -3
  11. package/dist/{cli-f1wky6zp.js → cli-3g61md6v.js} +1 -1
  12. package/dist/{cli-a1z3h107.js → cli-3pft2z6r.js} +4 -4
  13. package/dist/{cli-5tpgq60f.js → cli-72tgtzr1.js} +152 -23
  14. package/dist/{cli-0qb1z6s2.js → cli-7bwaerjt.js} +1 -1
  15. package/dist/{cli-czjtgfys.js → cli-b4dyetqt.js} +2 -2
  16. package/dist/{cli-s14agm5s.js → cli-bdt7w021.js} +12 -3
  17. package/dist/{cli-sjaae6ff.js → cli-dsyg66vt.js} +3 -3
  18. package/dist/{cli-2tbvf1z1.js → cli-ftdjz1mk.js} +8 -8
  19. package/dist/{cli-zkxymas5.js → cli-gfez0qmk.js} +1 -1
  20. package/dist/{cli-q88st9aj.js → cli-gxf2ebyz.js} +2 -2
  21. package/dist/{cli-ppm4975n.js → cli-jtvs5m8q.js} +1 -1
  22. package/dist/{cli-7tgcstn2.js → cli-jytf5mw9.js} +2 -2
  23. package/dist/{cli-g69fdc9g.js → cli-kf8qp2vf.js} +2 -2
  24. package/dist/{cli-5jd2z7av.js → cli-ks8a1cgq.js} +6 -2
  25. package/dist/{cli-gjyvhmmh.js → cli-ng4q6a9t.js} +5 -5
  26. package/dist/{cli-3v9xhjcn.js → cli-sakxd2rg.js} +2 -2
  27. package/dist/{cli-frcta2w4.js → cli-sqmh7jj1.js} +1 -1
  28. package/dist/{cli-vwm416m6.js → cli-v9sk4dzw.js} +1 -1
  29. package/dist/{cli-j9xczhaq.js → cli-vbk025fm.js} +1 -1
  30. package/dist/{cli-r2q6pevn.js → cli-wfve107r.js} +43 -21
  31. package/dist/{cli-9004vfdf.js → cli-y8tayczk.js} +2 -2
  32. package/dist/{cli-gbfwcd3k.js → cli-zfzznn7c.js} +7 -6
  33. package/dist/cli.js +11 -11
  34. package/dist/{codex-adapter-jjjk8w4m.js → codex-adapter-k6yq95sn.js} +2 -2
  35. package/dist/{codex-session-runner-nd6ayxkx.js → codex-session-runner-3rp84pk8.js} +2 -2
  36. package/dist/{commands-9v4psyq1.js → commands-pbr5jzy5.js} +2 -2
  37. package/dist/{db-wnt58tpe.js → db-z0xaa59a.js} +2 -2
  38. package/dist/{dispatcher-05jh1aqk.js → dispatcher-pwpgsj0x.js} +12 -12
  39. package/dist/{handlers-tw90pkh5.js → handlers-5mzk4mbg.js} +15 -15
  40. package/dist/{hook-sxe3r349.js → hook-8bz33zee.js} +1 -1
  41. package/dist/{hook-123e2scb.js → hook-kbh84zb1.js} +2 -2
  42. package/dist/{http-ecsar8ez.js → http-pppxg5vr.js} +100 -36
  43. package/dist/{index-hw26vj40.js → index-68bd9b5f.js} +12 -12
  44. package/dist/{index-16js521z.js → index-k1hq1qt6.js} +8 -8
  45. package/dist/{index-zj518gf9.js → index-mjpxsr6k.js} +13 -13
  46. package/dist/{index-ymn7em0p.js → index-y0te09pn.js} +3 -3
  47. package/dist/{index-nnrbpcjp.js → index-z0zwjtkn.js} +12 -12
  48. package/dist/{keepalive-asqtcm4y.js → keepalive-xvmwnh1d.js} +4 -4
  49. package/dist/{lead-xbfr7fvt.js → lead-pf8nvxt8.js} +16 -16
  50. package/dist/{maintenance-qma55qvw.js → maintenance-jk4q0wgg.js} +4 -4
  51. package/dist/{oauth-refresh-sweep-3s04c6qw.js → oauth-refresh-sweep-6207x9sj.js} +3 -3
  52. package/dist/{onboard-drskd386.js → onboard-b0csgm0q.js} +2 -2
  53. package/dist/{otel-impl-943c96er.js → otel-impl-b6rfehth.js} +1 -1
  54. package/dist/{pricing-refresh-mx51az05.js → pricing-refresh-4e201wtd.js} +4 -4
  55. package/dist/{rbac-roles-wcmrg6sv.js → rbac-roles-mkc5n5qa.js} +4 -4
  56. package/dist/{rbac-roles-78baecqd.js → rbac-roles-zp6e4geh.js} +3 -3
  57. package/dist/{render-v2-8zjntgf5.js → render-v2-4fazz9tf.js} +3 -3
  58. package/dist/{seed-pricing-jqjs6cws.js → seed-pricing-thxpn9vn.js} +3 -3
  59. package/dist/{setup-4pks0vfk.js → setup-wb0z8cat.js} +2 -2
  60. package/dist/{work-objects-wangt792.js → work-objects-h3498kwe.js} +18 -17
  61. package/dist/{worker-p2mr422x.js → worker-2m2tzxp3.js} +16 -16
  62. package/openapi.json +5 -5
  63. package/package.json +1 -1
  64. package/src/agentmail/inbound-archive.ts +71 -0
  65. package/src/agentmail/types.ts +2 -0
  66. package/src/be/db.ts +1 -1
  67. package/src/be/scripts/typecheck.ts +4 -0
  68. package/src/be/swarm-config-guard.ts +7 -1
  69. package/src/extensions/lifecycle.ts +26 -4
  70. package/src/http/extensions.ts +8 -3
  71. package/src/http/webhooks.ts +13 -5
  72. package/src/rbac/legacy-policy.ts +13 -1
  73. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  74. package/src/scripts-runtime/types/stdlib.d.ts +4 -0
  75. package/src/scripts-runtime/types/swarm-sdk.d.ts +4 -0
  76. package/src/server.ts +12 -0
  77. package/src/slack/render-v2.ts +10 -4
  78. package/src/slack/responses.ts +7 -4
  79. package/src/slack/task-output.ts +21 -0
  80. package/src/slack/watcher.ts +3 -2
  81. package/src/slack/work-objects.ts +2 -1
  82. package/src/tests/agentmail-inbound-archive.test.ts +418 -0
  83. package/src/tests/extensions-example-mcp-install.test.ts +2 -2
  84. package/src/tests/extensions-http.test.ts +84 -3
  85. package/src/tests/extensions-lead-equivalence.test.ts +21 -0
  86. package/src/tests/extensions-mcp-tools.test.ts +111 -3
  87. package/src/tests/fixtures/store-progress-malformed-output.json +4 -0
  88. package/src/tests/rbac-admission-e2e.test.ts +35 -4
  89. package/src/tests/rbac-e2e-helpers.ts +18 -0
  90. package/src/tests/rbac-engine.test.ts +2 -2
  91. package/src/tests/rbac-mcp-admission-e2e.test.ts +22 -4
  92. package/src/tests/reload-config.test.ts +17 -0
  93. package/src/tests/scripts-only-gating.test.ts +4 -0
  94. package/src/tests/slack-render-v2.test.ts +24 -2
  95. package/src/tests/slack-task-output.test.ts +36 -0
  96. package/src/tests/store-progress-attachments-handler.test.ts +100 -1
  97. package/src/tests/swarm-config-reserved-keys.test.ts +6 -1
  98. package/src/tests/tool-annotations.test.ts +1 -1
  99. package/src/tools/extension-activate-version.ts +38 -0
  100. package/src/tools/extension-common.ts +2 -1
  101. package/src/tools/extension-delete.ts +29 -0
  102. package/src/tools/extension-disable.ts +36 -0
  103. package/src/tools/extension-enable.ts +36 -0
  104. package/src/tools/extension-install.ts +2 -2
  105. package/src/tools/store-progress.ts +30 -0
  106. package/src/tools/tool-config.ts +5 -1
  107. package/templates/skills/agent-browser/SKILL.md +84 -0
  108. package/templates/skills/agent-browser/content.md +21 -0
@@ -3,6 +3,7 @@ import {
3
3
  getExtensionById,
4
4
  getExtensionFiles,
5
5
  getExtensionVersion,
6
+ insertExtensionRun,
6
7
  listExtensions,
7
8
  pruneExtensionRuns,
8
9
  setExtensionState,
@@ -95,14 +96,33 @@ async function removeRegistered(id: string): Promise<void> {
95
96
  if (previous) await previous.dispose();
96
97
  }
97
98
 
99
+ // Record the request before importing code, including attempts whose load fails.
100
+ async function recordLifecycleRequest(
101
+ record: Extension,
102
+ operation: string,
103
+ agentId: string | undefined,
104
+ version = record.activeVersion,
105
+ ): Promise<void> {
106
+ if (!agentId) return;
107
+ await insertExtensionRun({
108
+ extensionId: record.id,
109
+ version,
110
+ event: `lifecycle.${operation}.requested`,
111
+ action: "continue",
112
+ agentId,
113
+ subject: record.name,
114
+ });
115
+ }
116
+
98
117
  export async function enableExtension(
99
118
  id: string,
100
- opts: { by?: string | null } = {},
119
+ opts: { by?: string | null; agentId?: string } = {},
101
120
  ): Promise<Extension> {
102
121
  initExtensionPostBridge();
103
122
  let record = await getExtensionById(id);
104
123
  if (!record) throw new ExtensionLifecycleError("Extension not found", 404);
105
124
 
125
+ await recordLifecycleRequest(record, "enable", opts.agentId);
106
126
  const agentId = await ensureExtensionAgent(record.name);
107
127
  const identified = await setExtensionState(id, { agentId, updatedBy: opts.by });
108
128
  if (!identified) throw new ExtensionLifecycleError("Extension not found", 404);
@@ -133,10 +153,11 @@ export async function enableExtension(
133
153
 
134
154
  export async function disableExtension(
135
155
  id: string,
136
- opts: { by?: string | null } = {},
156
+ opts: { by?: string | null; agentId?: string } = {},
137
157
  ): Promise<Extension> {
138
158
  const record = await getExtensionById(id);
139
159
  if (!record) throw new ExtensionLifecycleError("Extension not found", 404);
160
+ await recordLifecycleRequest(record, "disable", opts.agentId);
140
161
  await removeRegistered(id);
141
162
  attemptedFingerprints.delete(id);
142
163
  await deactivateExtensionAgent(record.name);
@@ -152,7 +173,7 @@ export async function disableExtension(
152
173
  export async function activateVersion(
153
174
  id: string,
154
175
  version: number,
155
- opts: { by?: string | null } = {},
176
+ opts: { by?: string | null; agentId?: string } = {},
156
177
  ): Promise<Extension> {
157
178
  const current = await getExtensionById(id);
158
179
  if (!current) throw new ExtensionLifecycleError("Extension not found", 404);
@@ -160,6 +181,7 @@ export async function activateVersion(
160
181
  throw new ExtensionLifecycleError(`Extension version ${version} was not found`, 404);
161
182
  }
162
183
 
184
+ await recordLifecycleRequest(current, "activate-version", opts.agentId, version);
163
185
  const wasEnabled = current.enabled;
164
186
  if (wasEnabled) await disableExtension(id, opts);
165
187
  const activated = await activateExtensionVersionSnapshot(id, version, opts.by);
@@ -170,7 +192,7 @@ export async function activateVersion(
170
192
 
171
193
  export async function reloadExtension(
172
194
  id: string,
173
- opts: { by?: string | null } = {},
195
+ opts: { by?: string | null; agentId?: string } = {},
174
196
  ): Promise<Extension> {
175
197
  const record = await getExtensionById(id);
176
198
  if (!record) throw new ExtensionLifecycleError("Extension not found", 404);
@@ -197,7 +197,8 @@ const enableRoute = route({
197
197
  pattern: ["api", "extensions", null, "enable"],
198
198
  operationId: "extensions_enable",
199
199
  summary: "Enable an extension",
200
- description: "Loads and enables an extension. This route is operator-only.",
200
+ description:
201
+ "Loads and enables an extension. Available to leads, operators, and dashboard users.",
201
202
  tags: ["Extensions"],
202
203
  params: idParamsSchema,
203
204
  responses: {
@@ -215,7 +216,8 @@ const disableRoute = route({
215
216
  pattern: ["api", "extensions", null, "disable"],
216
217
  operationId: "extensions_disable",
217
218
  summary: "Disable an extension",
218
- description: "Unloads and disables an extension. This route is operator-only.",
219
+ description:
220
+ "Unloads and disables an extension. Available to leads, operators, and dashboard users.",
219
221
  tags: ["Extensions"],
220
222
  params: idParamsSchema,
221
223
  responses: {
@@ -232,7 +234,7 @@ const activateVersionRoute = route({
232
234
  pattern: ["api", "extensions", null, "activate-version"],
233
235
  operationId: "extensions_activate_version",
234
236
  summary: "Activate an extension version",
235
- description: "Activates a stored version. This route is operator-only.",
237
+ description: "Activates a stored version. Available to leads, operators, and dashboard users.",
236
238
  tags: ["Extensions"],
237
239
  params: idParamsSchema,
238
240
  body: z.object({ version: z.number().int().min(1) }).strict(),
@@ -501,6 +503,7 @@ export async function handleExtensions(
501
503
  try {
502
504
  const extension = await enableExtension(parsed.params.id, {
503
505
  by: await resolveHttpAuditUserId(req, writerAgentId),
506
+ agentId: writerAgentId,
504
507
  });
505
508
  enableRoute.respond(res, 200, { extension: redactExtension(extension) });
506
509
  } catch (error) {
@@ -518,6 +521,7 @@ export async function handleExtensions(
518
521
  try {
519
522
  const extension = await disableExtension(parsed.params.id, {
520
523
  by: await resolveHttpAuditUserId(req, writerAgentId),
524
+ agentId: writerAgentId,
521
525
  });
522
526
  disableRoute.respond(res, 200, { extension: redactExtension(extension) });
523
527
  } catch (error) {
@@ -535,6 +539,7 @@ export async function handleExtensions(
535
539
  try {
536
540
  const extension = await activateVersion(parsed.params.id, parsed.body.version, {
537
541
  by: await resolveHttpAuditUserId(req, writerAgentId),
542
+ agentId: writerAgentId,
538
543
  });
539
544
  activateVersionRoute.respond(res, 200, { extension: redactExtension(extension) });
540
545
  } catch (error) {
@@ -8,6 +8,7 @@ import {
8
8
  isSenderAllowed,
9
9
  verifyAgentMailWebhook,
10
10
  } from "../agentmail";
11
+ import { archiveInboundMessage } from "../agentmail/inbound-archive";
11
12
  import type {
12
13
  CheckRunEvent,
13
14
  CheckSuiteEvent,
@@ -109,7 +110,7 @@ const agentmailWebhook = route({
109
110
  responses: {
110
111
  200: { description: "Event received", schema: z.object({ received: z.literal(true) }) },
111
112
  401: { description: "Invalid signature" },
112
- 503: { description: "AgentMail integration not configured" },
113
+ 503: { description: "AgentMail integration not configured or inbound archive unavailable" },
113
114
  },
114
115
  });
115
116
 
@@ -486,11 +487,18 @@ export async function handleWebhooks(
486
487
  return true;
487
488
  }
488
489
 
489
- // Return 200 immediately — Svix best practice to avoid retries.
490
- // Processing happens asynchronously below; dedup is handled in handlers.ts.
491
- agentmailWebhook.respond(res, 200, { received: true });
492
-
493
490
  const payload = verified as AgentMailWebhookPayload;
491
+ try {
492
+ await archiveInboundMessage(payload);
493
+ } catch (err) {
494
+ console.error("[AgentMail] Inbound archive failed; requesting webhook retry", err);
495
+ res.writeHead(503, { "Content-Type": "application/json" });
496
+ res.end(JSON.stringify({ error: "Inbound archive unavailable" }));
497
+ return true;
498
+ }
499
+ // Only acknowledge received events after durable capture. Task routing below
500
+ // remains independent: rejected senders and routing failures stay discoverable.
501
+ agentmailWebhook.respond(res, 200, { received: true });
494
502
 
495
503
  if (
496
504
  payload.message &&
@@ -11,6 +11,7 @@
11
11
  * `PermissionVerb` (enforced via `satisfies`).
12
12
  */
13
13
 
14
+ import { isEnvFlagEnabled } from "../utils/env-flag";
14
15
  import { hasLeadEquivalence } from "./elevated-agents";
15
16
  import type { PermissionVerb } from "./permissions";
16
17
  import type { RbacPrincipal, RbacResource } from "./types";
@@ -55,6 +56,17 @@ const leadOrOperatorOrUser: LegacyRule = {
55
56
  principal.kind === "operator" || principal.kind === "user" || actsAsLead(principal),
56
57
  };
57
58
 
59
+ // Deployment-only: agents cannot reset this through swarm_config.
60
+ const extensionActivation: LegacyRule = {
61
+ name: "extension-activation",
62
+ denyReason:
63
+ "requires operator/user authentication, or a lead with EXTENSION_ALLOW_LEAD_ACTIVATION enabled",
64
+ evaluate: (principal) =>
65
+ principal.kind === "operator" ||
66
+ principal.kind === "user" ||
67
+ (actsAsLead(principal) && isEnvFlagEnabled("EXTENSION_ALLOW_LEAD_ACTIVATION", true)),
68
+ };
69
+
58
70
  const leadOrTaskCreator: LegacyRule = {
59
71
  name: "lead-or-task-creator",
60
72
  denyReason: "requires lead agent or task creator",
@@ -231,5 +243,5 @@ export const LEGACY_POLICY = {
231
243
  "script.api.rotate": leadOnly,
232
244
  "script.api.delete": leadOnly,
233
245
  "extension.write": leadOrOperatorOrUser,
234
- "extension.activate": operatorOrUser,
246
+ "extension.activate": extensionActivation,
235
247
  } as const satisfies Record<PermissionVerb, LegacyRule>;
@@ -57,6 +57,10 @@ export const SDK_TOOL_NAME_MAP = {
57
57
  script_listRuns: "list-script-runs",
58
58
 
59
59
  // ── extensions ──
60
+ extension_delete: "extension-delete",
61
+ extension_enable: "extension-enable",
62
+ extension_disable: "extension-disable",
63
+ extension_activate_version: "extension-activate-version",
60
64
  extension_install: "extension-install",
61
65
  extension_list: "extension-list",
62
66
 
@@ -525,6 +525,10 @@ declare module "swarm-sdk" {
525
525
  config?: Record<string, unknown>;
526
526
  }): Promise<unknown>;
527
527
  extension_list(args?: { enabledOnly?: boolean }): Promise<unknown>;
528
+ extension_delete(args: { id: string }): Promise<unknown>;
529
+ extension_enable(args: { id: string }): Promise<unknown>;
530
+ extension_disable(args: { id: string }): Promise<unknown>;
531
+ extension_activate_version(args: { id: string; version: number }): Promise<unknown>;
528
532
 
529
533
  // --- write: repos ---
530
534
  repo_update(args: Record<string, unknown>): Promise<unknown>;
@@ -507,6 +507,10 @@ declare module "swarm-sdk" {
507
507
  config?: Record<string, unknown>;
508
508
  }): Promise<unknown>;
509
509
  extension_list(args?: { enabledOnly?: boolean }): Promise<unknown>;
510
+ extension_delete(args: { id: string }): Promise<unknown>;
511
+ extension_enable(args: { id: string }): Promise<unknown>;
512
+ extension_disable(args: { id: string }): Promise<unknown>;
513
+ extension_activate_version(args: { id: string; version: number }): Promise<unknown>;
510
514
 
511
515
  // --- write: repos ---
512
516
  repo_update(args: Record<string, unknown>): Promise<unknown>;
package/src/server.ts CHANGED
@@ -30,6 +30,10 @@ import { registerDbQueryTool } from "./tools/db-query";
30
30
  import { registerDeferTaskTool } from "./tools/defer-task";
31
31
  import { registerDeleteChannelTool } from "./tools/delete-channel";
32
32
  import { registerDeletePageTool } from "./tools/delete-page";
33
+ import { registerExtensionActivateVersionTool } from "./tools/extension-activate-version";
34
+ import { registerExtensionDeleteTool } from "./tools/extension-delete";
35
+ import { registerExtensionDisableTool } from "./tools/extension-disable";
36
+ import { registerExtensionEnableTool } from "./tools/extension-enable";
33
37
  import { registerExtensionInstallTool } from "./tools/extension-install";
34
38
  import { registerExtensionListTool } from "./tools/extension-list";
35
39
  import { registerGetMetricsTool } from "./tools/get-metrics";
@@ -340,6 +344,10 @@ export async function createServer(
340
344
  registerScriptSearchTool(server);
341
345
  registerScriptRunTool(server);
342
346
  registerScriptUpsertTool(server);
347
+ registerExtensionDeleteTool(server);
348
+ registerExtensionEnableTool(server);
349
+ registerExtensionDisableTool(server);
350
+ registerExtensionActivateVersionTool(server);
343
351
  registerExtensionInstallTool(server);
344
352
  registerExtensionListTool(server);
345
353
  registerScriptDeleteTool(server);
@@ -402,6 +410,10 @@ export async function createServer(
402
410
  registerScriptApisTool(server);
403
411
  registerScriptRunTool(server);
404
412
  registerScriptUpsertTool(server);
413
+ registerExtensionDeleteTool(server);
414
+ registerExtensionEnableTool(server);
415
+ registerExtensionDisableTool(server);
416
+ registerExtensionActivateVersionTool(server);
405
417
  registerExtensionInstallTool(server);
406
418
  registerExtensionListTool(server);
407
419
  registerScriptDeleteTool(server);
@@ -44,6 +44,7 @@ import {
44
44
  import { buildAskClosure, type ClosureState, closureState } from "./closure";
45
45
  import { reactionName, type SlackReactionEvent } from "./reaction-shortcode";
46
46
  import { getAgentDisplayName, getAgentEmoji } from "./responses";
47
+ import { isSlackEtaOnlyNotice, slackTaskOutput } from "./task-output";
47
48
 
48
49
  const TREE_UPDATE_DEBOUNCE_MS = 500;
49
50
  const TREE_UPDATE_MIN_INTERVAL_MS = 3_000;
@@ -757,7 +758,7 @@ async function outcomeContent(task: AgentTask, slackReplySent: boolean): Promise
757
758
  : "Agent";
758
759
  return `✅ ${agentName} completed`;
759
760
  }
760
- return `✅\n\n${outcomeText(task.output, "Task completed.")}`;
761
+ return `✅\n\n${outcomeText(slackTaskOutput(task), "Task completed.")}`;
761
762
  }
762
763
 
763
764
  async function agentDisplayNameFor(task: AgentTask): Promise<string> {
@@ -786,7 +787,7 @@ export async function childOutcomeContent(
786
787
  if (slackReplySent && !task.tags?.includes("deferred")) {
787
788
  return `↳ ✅ ${agentName} completed`;
788
789
  }
789
- return `↳ ✅ ${agentName} — result\n\n${outcomeText(task.output, "Task completed.")}`;
790
+ return `↳ ✅ ${agentName} — result\n\n${outcomeText(slackTaskOutput(task), "Task completed.")}`;
790
791
  }
791
792
 
792
793
  // A deferred wake continues a human conversation even though the scheduler
@@ -808,6 +809,7 @@ function isSlackContinuation(task: AgentTask): boolean {
808
809
  */
809
810
  function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): boolean {
810
811
  return (
812
+ !isSlackEtaOnlyNotice(task) &&
811
813
  task.source !== "slack" &&
812
814
  !isSlackContinuation(task) &&
813
815
  !!task.slackChannelId &&
@@ -821,6 +823,7 @@ function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): b
821
823
 
822
824
  function taskNeedsDirectOutcome(task: AgentTask, activatedAt: string): boolean {
823
825
  return (
826
+ !isSlackEtaOnlyNotice(task) &&
824
827
  ((task.source === "slack" && isAskOutcomeStatus(task.status)) ||
825
828
  (isSlackContinuation(task) && isOutcomeStatus(task.status))) &&
826
829
  task.createdAt >= activatedAt
@@ -839,7 +842,7 @@ async function conclusionResultsLines(closure: AgentTask[]): Promise<string[]> {
839
842
  const lines: string[] = [];
840
843
  for (const member of closure) {
841
844
  if (member.taskType === "follow-up" || member.taskType === "reroute-decision") continue;
842
- if (!isOutcomeStatus(member.status)) continue;
845
+ if (!isOutcomeStatus(member.status) || isSlackEtaOnlyNotice(member)) continue;
843
846
  const glyph = await taskStateGlyph(member, new Date());
844
847
  const agentName = await agentDisplayNameFor(member);
845
848
  const card = await getSlackOutcomeMessage(member.id);
@@ -847,7 +850,10 @@ async function conclusionResultsLines(closure: AgentTask[]): Promise<string[]> {
847
850
  lines.push(`↳ ${glyph} ${agentName} — ${card.permalink}`);
848
851
  continue;
849
852
  }
850
- const raw = outcomeText(member.status === "failed" ? member.failureReason : member.output, "");
853
+ const raw = outcomeText(
854
+ member.status === "failed" ? member.failureReason : slackTaskOutput(member),
855
+ "",
856
+ );
851
857
  const digest =
852
858
  raw.length > CONCLUSION_DIGEST_LENGTH
853
859
  ? `${raw.slice(0, CONCLUSION_DIGEST_LENGTH).trimEnd()}…`
@@ -15,6 +15,7 @@ import {
15
15
  markdownToSlack,
16
16
  splitSlackSectionText,
17
17
  } from "./blocks";
18
+ import { isSlackEtaOnlyNotice, slackTaskOutput } from "./task-output";
18
19
 
19
20
  // Re-export for backward compatibility
20
21
  export { markdownToSlack } from "./blocks";
@@ -56,7 +57,7 @@ export function shouldPostInlineCompletionOutput(task: AgentTask): boolean {
56
57
  if (!task.slackChannelId || !task.slackThreadTs) return false;
57
58
  if (task.slackReplySent) return false;
58
59
 
59
- const output = task.output?.trim();
60
+ const output = slackTaskOutput(task)?.trim();
60
61
  return !!output && isTreeOutputTruncated(markdownToSlack(output));
61
62
  }
62
63
 
@@ -85,7 +86,7 @@ export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
85
86
  const chunks = formatInlineCompletionOutputChunks({
86
87
  agentName: agent.name,
87
88
  taskId: task.id,
88
- output: task.output,
89
+ output: slackTaskOutput(task) ?? "",
89
90
  });
90
91
 
91
92
  try {
@@ -115,6 +116,7 @@ export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
115
116
  * Send a task completion message to Slack with the agent's persona.
116
117
  */
117
118
  export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
119
+ if (isSlackEtaOnlyNotice(task)) return true;
118
120
  const app = getSlackApp();
119
121
  if (!app || !task.slackChannelId || !task.slackThreadTs) {
120
122
  return false;
@@ -136,7 +138,7 @@ export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
136
138
 
137
139
  try {
138
140
  if (task.status === "completed") {
139
- const output = task.output || "Task completed.";
141
+ const output = slackTaskOutput(task) || "Task completed.";
140
142
  const slackOutput = markdownToSlack(output);
141
143
  const attachmentsBlock = formatAttachmentsBlockForSlack(await getTaskAttachments(task.id));
142
144
  const body = slackOutput + attachmentsBlock;
@@ -285,6 +287,7 @@ export async function updateToFinal(
285
287
  task: AgentTask,
286
288
  messageTs: string,
287
289
  ): Promise<SlackUpdateResult> {
290
+ if (isSlackEtaOnlyNotice(task)) return "ok";
288
291
  const app = getSlackApp();
289
292
  if (!app || !task.slackChannelId || !task.agentId) return "failed";
290
293
 
@@ -297,7 +300,7 @@ export async function updateToFinal(
297
300
  let completionBlockBatches: unknown[][] | undefined;
298
301
 
299
302
  if (task.status === "completed") {
300
- const output = task.output || "Task completed.";
303
+ const output = slackTaskOutput(task) || "Task completed.";
301
304
  const slackOutput = markdownToSlack(output);
302
305
  const attachmentsBlock = formatAttachmentsBlockForSlack(await getTaskAttachments(task.id));
303
306
  const body = slackOutput + attachmentsBlock;
@@ -0,0 +1,21 @@
1
+ import type { AgentTask } from "../types";
2
+
3
+ /** Keep the stored deferral ETA for task details, but omit it from Slack. */
4
+ export function slackTaskOutput(
5
+ task: Pick<AgentTask, "output" | "tags" | "outputSchema" | "status">,
6
+ ): string | undefined {
7
+ const output = task.output ?? undefined;
8
+ if (task.status !== "completed" || task.outputSchema || !task.tags?.includes("deferred")) {
9
+ return output;
10
+ }
11
+ // Only match the engine-authored line from defer-task, not continuation results.
12
+ const notice = output?.match(/^Deferred until [^\n]+? \([^\n]+?\) -> (.*)$/);
13
+ if (!notice) return output;
14
+ const pending = notice[1]?.trim();
15
+ return pending ? `Pending: ${pending}` : "";
16
+ }
17
+
18
+ /** An ETA-only notice has no useful content left to post. */
19
+ export function isSlackEtaOnlyNotice(task: AgentTask): boolean {
20
+ return !!task.output && slackTaskOutput(task) === "";
21
+ }
@@ -28,6 +28,7 @@ import {
28
28
  updateToFinal,
29
29
  updateTreeMessage,
30
30
  } from "./responses";
31
+ import { slackTaskOutput } from "./task-output";
31
32
 
32
33
  let watcherInterval: ReturnType<typeof setInterval> | null = null;
33
34
  let isProcessing = false;
@@ -169,7 +170,7 @@ export async function buildTreeNodes(tree: TreeMessageState): Promise<TreeNode[]
169
170
  progress: child.progress ?? undefined,
170
171
  duration: childDuration,
171
172
  slackReplySent: child.slackReplySent,
172
- output: child.output ?? undefined,
173
+ output: slackTaskOutput(child),
173
174
  failureReason: child.failureReason ?? undefined,
174
175
  attachments: childAttachments,
175
176
  steered: childSteered,
@@ -192,7 +193,7 @@ export async function buildTreeNodes(tree: TreeMessageState): Promise<TreeNode[]
192
193
  progress: task.progress ?? undefined,
193
194
  duration,
194
195
  slackReplySent: task.slackReplySent,
195
- output: task.output ?? undefined,
196
+ output: slackTaskOutput(task),
196
197
  failureReason: task.failureReason ?? undefined,
197
198
  attachments: rootAttachments,
198
199
  steered: rootSteered,
@@ -4,6 +4,7 @@ import { getAgentById, getTaskById } from "../be/db";
4
4
  import { isEnvFlagEnabled } from "../utils/env-flag";
5
5
  import { getTaskUrl } from "./blocks";
6
6
  import { isUserAllowed } from "./handlers";
7
+ import { slackTaskOutput } from "./task-output";
7
8
 
8
9
  type DetailsEvent = SlackEventMiddlewareArgs<"entity_details_requested">["event"];
9
10
  type Details = Omit<EntityPresentDetailsArguments, "trigger_id" | "token">;
@@ -36,7 +37,7 @@ async function getDetails(event: DetailsEvent, client: WebClient): Promise<Detai
36
37
  if (task.slackChannelId !== event.channel) return { error: { status: "restricted" } };
37
38
 
38
39
  const agent = task.agentId ? await getAgentById(task.agentId) : null;
39
- const result = task.output || task.failureReason || task.progress;
40
+ const result = slackTaskOutput(task) || task.failureReason || task.progress;
40
41
  return {
41
42
  metadata: {
42
43
  entity_type: "slack#/entities/task",