@cursor/july 0.1.97 → 0.1.99

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 (179) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/bin/agent-serve.js +6 -7
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +16 -0
  5. package/dist/channels/origin/origin-channel.d.ts +2 -0
  6. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  7. package/dist/channels/origin/origin-channel.js +37 -4
  8. package/dist/channels/slack/defaults.d.ts.map +1 -1
  9. package/dist/channels/slack/defaults.js +76 -55
  10. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  11. package/dist/channels/slack/dispatch.js +1 -1
  12. package/dist/channels/slack/live-delivery.d.ts +1 -1
  13. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  14. package/dist/channels/slack/live-delivery.js +4 -2
  15. package/dist/channels/slack/progress-delivery.d.ts +28 -0
  16. package/dist/channels/slack/progress-delivery.d.ts.map +1 -0
  17. package/dist/channels/slack/progress-delivery.js +115 -0
  18. package/dist/channels/slack/types.d.ts +9 -1
  19. package/dist/channels/slack/types.d.ts.map +1 -1
  20. package/dist/docs/404.html +2 -2
  21. package/dist/docs/ab.html +4 -4
  22. package/dist/docs/assets/{app.B3rWNYE1.js → app.BplEAZ3S.js} +1 -1
  23. package/dist/docs/assets/chunks/@localSearchIndexroot.DpuI4i7Z.js +1 -0
  24. package/dist/docs/assets/chunks/{VPLocalSearchBox.C1lhqJJR.js → VPLocalSearchBox.CHXyv4wP.js} +1 -1
  25. package/dist/docs/assets/chunks/{theme.B40_SXuv.js → theme.BTcsZmjQ.js} +2 -2
  26. package/dist/docs/assets/{reference_cli.md.RyZf5OTE.js → reference_cli.md.DfoeyvL0.js} +4 -4
  27. package/dist/docs/assets/{reference_cli.md.RyZf5OTE.lean.js → reference_cli.md.DfoeyvL0.lean.js} +1 -1
  28. package/dist/docs/assets/{reference_http-api.md.5zOAbV86.js → reference_http-api.md.BEJx9XVj.js} +1 -1
  29. package/dist/docs/assets/{reference_tools.md.B84gw9Ii.js → reference_tools.md.CvAHsdSp.js} +10 -2
  30. package/dist/docs/assets/{reference_tools.md.B84gw9Ii.lean.js → reference_tools.md.CvAHsdSp.lean.js} +1 -1
  31. package/dist/docs/building-with-agents.html +4 -4
  32. package/dist/docs/concepts.html +4 -4
  33. package/dist/docs/deployment.html +4 -4
  34. package/dist/docs/design/runtime-abstraction.md +1757 -0
  35. package/dist/docs/evals.html +4 -4
  36. package/dist/docs/guides/agent-to-agent.html +4 -4
  37. package/dist/docs/guides/cloud-runtime.html +4 -4
  38. package/dist/docs/guides/convert-automation.html +4 -4
  39. package/dist/docs/guides/github.html +4 -4
  40. package/dist/docs/guides/human-in-the-loop.html +4 -4
  41. package/dist/docs/guides/mcp-oauth.html +4 -4
  42. package/dist/docs/guides/opentelemetry.html +4 -4
  43. package/dist/docs/guides/slack.html +4 -4
  44. package/dist/docs/guides/webhooks.html +4 -4
  45. package/dist/docs/hashmap.json +1 -1
  46. package/dist/docs/hillclimbing.html +4 -4
  47. package/dist/docs/index.html +4 -4
  48. package/dist/docs/llms-full.txt +1789 -7
  49. package/dist/docs/llms.txt +1 -0
  50. package/dist/docs/quickstart.html +4 -4
  51. package/dist/docs/reference/agent-config.html +4 -4
  52. package/dist/docs/reference/artifacts.html +4 -4
  53. package/dist/docs/reference/channels.html +4 -4
  54. package/dist/docs/reference/cli.html +7 -7
  55. package/dist/docs/reference/cli.md +1 -7
  56. package/dist/docs/reference/connections.html +4 -4
  57. package/dist/docs/reference/hooks.html +4 -4
  58. package/dist/docs/reference/http-api.html +6 -6
  59. package/dist/docs/reference/http-api.md +1 -0
  60. package/dist/docs/reference/instructions.html +4 -4
  61. package/dist/docs/reference/playground.html +4 -4
  62. package/dist/docs/reference/project-layout.html +4 -4
  63. package/dist/docs/reference/prompt.html +4 -4
  64. package/dist/docs/reference/schedules.html +4 -4
  65. package/dist/docs/reference/sessions.html +4 -4
  66. package/dist/docs/reference/skills.html +4 -4
  67. package/dist/docs/reference/subagents.html +4 -4
  68. package/dist/docs/reference/tools.html +14 -6
  69. package/dist/docs/reference/tools.md +25 -0
  70. package/dist/docs/scaffolding-agents.html +4 -4
  71. package/dist/docs/storage.html +4 -4
  72. package/dist/docs/templates/agentic-owners.html +4 -4
  73. package/dist/docs/templates/agents-md.html +4 -4
  74. package/dist/docs/templates/code-wiki.html +4 -4
  75. package/dist/docs/templates/demo.html +4 -4
  76. package/dist/docs/templates/pr-autofixer.html +4 -4
  77. package/dist/docs/templates/security-help.html +4 -4
  78. package/dist/docs/templates/security-reviewer.html +4 -4
  79. package/dist/docs/templates/triage.html +4 -4
  80. package/dist/docs/troubleshooting.html +4 -4
  81. package/dist/files-backends/cursor-hosted.d.ts +3 -2
  82. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  83. package/dist/files-backends/cursor-hosted.js +3 -2
  84. package/dist/index.d.ts +1 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +1 -0
  87. package/dist/internal/as-of.d.ts +19 -0
  88. package/dist/internal/as-of.d.ts.map +1 -0
  89. package/dist/internal/as-of.js +82 -0
  90. package/dist/internal/cli-deploy.d.ts +2 -1
  91. package/dist/internal/cli-deploy.d.ts.map +1 -1
  92. package/dist/internal/cli-deploy.js +100 -5
  93. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  94. package/dist/internal/cursor-event-relay.js +5 -6
  95. package/dist/internal/framework-file-storage.d.ts +10 -16
  96. package/dist/internal/framework-file-storage.d.ts.map +1 -1
  97. package/dist/internal/framework-file-storage.js +11 -40
  98. package/dist/internal/framework-storage-selection.d.ts +13 -11
  99. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  100. package/dist/internal/framework-storage-selection.js +37 -26
  101. package/dist/internal/hosted-admission-adapter.d.ts +28 -0
  102. package/dist/internal/hosted-admission-adapter.d.ts.map +1 -0
  103. package/dist/internal/hosted-admission-adapter.js +7 -0
  104. package/dist/internal/hosted-delivery-protocol.d.ts +6 -1
  105. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  106. package/dist/internal/hosted-delivery-protocol.js +1 -1
  107. package/dist/internal/hosted-delivery.d.ts +15 -1
  108. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  109. package/dist/internal/hosted-delivery.js +47 -21
  110. package/dist/internal/hosted-execution-diag.d.ts +11 -0
  111. package/dist/internal/hosted-execution-diag.d.ts.map +1 -1
  112. package/dist/internal/hosted-execution-diag.js +39 -2
  113. package/dist/internal/http-channel.d.ts.map +1 -1
  114. package/dist/internal/http-channel.js +4 -1
  115. package/dist/internal/null-kv-storage.d.ts +48 -0
  116. package/dist/internal/null-kv-storage.d.ts.map +1 -0
  117. package/dist/internal/null-kv-storage.js +58 -0
  118. package/dist/internal/server.d.ts +3 -0
  119. package/dist/internal/server.d.ts.map +1 -1
  120. package/dist/internal/server.js +82 -19
  121. package/dist/internal/session-engine.d.ts.map +1 -1
  122. package/dist/internal/session-engine.js +24 -4
  123. package/dist/internal/session-run-log.d.ts +120 -0
  124. package/dist/internal/session-run-log.d.ts.map +1 -0
  125. package/dist/internal/session-run-log.js +359 -0
  126. package/dist/internal/storage-coordinator.d.ts +6 -0
  127. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  128. package/dist/internal/storage-coordinator.js +9 -1
  129. package/dist/internal/tool-policy.d.ts +20 -8
  130. package/dist/internal/tool-policy.d.ts.map +1 -1
  131. package/dist/internal/tool-policy.js +11 -0
  132. package/dist/playground/assets/index-D7rGZNjy.css +1 -0
  133. package/dist/playground/assets/index-jzNbJENa.js +69 -0
  134. package/dist/playground/index.html +2 -2
  135. package/dist/storage-protocol.d.ts +8 -0
  136. package/dist/storage-protocol.d.ts.map +1 -1
  137. package/dist/storage-protocol.js +8 -0
  138. package/dist/tools.d.ts +12 -2
  139. package/dist/tools.d.ts.map +1 -1
  140. package/dist/types.d.ts +25 -0
  141. package/dist/types.d.ts.map +1 -1
  142. package/docs/design/runtime-abstraction.md +1757 -0
  143. package/docs/reference/cli.md +1 -7
  144. package/docs/reference/http-api.md +1 -0
  145. package/docs/reference/tools.md +25 -0
  146. package/package.json +1 -1
  147. package/src/bin/agent-serve.ts +6 -7
  148. package/src/channels/github/github-channel.ts +17 -0
  149. package/src/channels/origin/origin-channel.ts +74 -10
  150. package/src/channels/slack/defaults.ts +91 -63
  151. package/src/channels/slack/dispatch.ts +2 -0
  152. package/src/channels/slack/live-delivery.ts +5 -3
  153. package/src/channels/slack/progress-delivery.ts +133 -0
  154. package/src/channels/slack/types.ts +9 -1
  155. package/src/files-backends/cursor-hosted.ts +3 -2
  156. package/src/index.ts +4 -0
  157. package/src/internal/as-of.ts +107 -0
  158. package/src/internal/cli-deploy.ts +128 -8
  159. package/src/internal/cursor-event-relay.ts +5 -6
  160. package/src/internal/framework-file-storage.ts +11 -45
  161. package/src/internal/framework-storage-selection.ts +41 -25
  162. package/src/internal/hosted-admission-adapter.ts +42 -0
  163. package/src/internal/hosted-delivery-protocol.ts +7 -2
  164. package/src/internal/hosted-delivery.ts +88 -36
  165. package/src/internal/hosted-execution-diag.ts +45 -0
  166. package/src/internal/http-channel.ts +8 -0
  167. package/src/internal/null-kv-storage.ts +74 -0
  168. package/src/internal/server.ts +100 -2
  169. package/src/internal/session-engine.ts +32 -1
  170. package/src/internal/session-run-log.ts +584 -0
  171. package/src/internal/storage-coordinator.ts +11 -1
  172. package/src/internal/tool-policy.ts +31 -9
  173. package/src/storage-protocol.ts +10 -0
  174. package/src/tools.ts +12 -2
  175. package/src/types.ts +25 -0
  176. package/dist/docs/assets/chunks/@localSearchIndexroot.BU9nHdnQ.js +0 -1
  177. package/dist/playground/assets/index-DrkI6y5O.js +0 -88
  178. package/dist/playground/assets/index-DzNGwm7q.css +0 -1
  179. /package/dist/docs/assets/{reference_http-api.md.5zOAbV86.lean.js → reference_http-api.md.BEJx9XVj.lean.js} +0 -0
@@ -594,7 +594,7 @@ commands with an explicit `--json` flag skip this automatic check.
594
594
  agent-sdk deploy [--dir <path>] [--slug <slug> | --all] [--team <id>]
595
595
  [--repo <https-url>] [--ref <git-ref>] [--path <agent-path>]
596
596
  [--cursor-events-repo owner/name]...
597
- [--allow-domain <domain>]... [--multi-tenant]
597
+ [--allow-domain <domain>]...
598
598
  [--no-wait] [--json]
599
599
  ```
600
600
 
@@ -622,12 +622,6 @@ be a lowercase hostname with at least two labels and an alphabetic
622
622
  top-level domain. One leading `*.` wildcard is allowed. A deployment
623
623
  can declare at most 20 domains.
624
624
 
625
- `--multi-tenant` adds a release to an existing Cursor-managed
626
- (multi-tenant) product such as `security-reviewer`. It is valid only
627
- for `architecture: "v2"` and cannot create a new multi-tenant
628
- application — those stay on the operator seed path. Without the flag,
629
- HTTP deploy stays single-tenant and a managed slug 409s.
630
-
631
625
  By default, the command polls every three seconds for up to ten minutes
632
626
  and succeeds only when the deployment reaches `running`. `--no-wait`
633
627
  returns after the deployment request is accepted. Multi-agent deploys
@@ -53,6 +53,7 @@ and `playgroundUrl` deep-links the session in the playground.
53
53
  | `message` | Required user message |
54
54
  | `title` | Session title |
55
55
  | `dryRun` | Run read tools and stub write tools |
56
+ | `asOf` | ISO-8601 instant with a timezone, frozen at create; the prompt states it, `ctx.now()` returns it, and tools declaring `timeArgs` refuse calls not bounded at or before it. `400` when unusable |
56
57
  | `workspaceFiles` | UTF-8 files written into the session workspace |
57
58
  | `cloud` | Per-session cloud options merged over the agent defaults |
58
59
 
@@ -147,6 +147,31 @@ export default defineTool({
147
147
  `dryRunResult` keeps the result shape stable. Without it, a stubbed write
148
148
  returns `"Operation acknowledged."`. Traces mark the result as stubbed.
149
149
 
150
+ ### Bound time arguments under `asOf`
151
+
152
+ Declare `timeArgs` with the input properties that carry time bounds. In a
153
+ session created with `asOf`, every named argument must be an absolute
154
+ ISO-8601 instant with a timezone, no later than the session's frozen
155
+ instant. A call that omits one, or supplies a relative or later value, is
156
+ refused with an error telling the model to retry with absolute bounds.
157
+ Sessions without `asOf` ignore the declaration.
158
+
159
+ ```ts
160
+ export default defineTool({
161
+ description: "Query telemetry between two instants.",
162
+ inputSchema: z.object({ since: z.string(), until: z.string() }),
163
+ effect: "read",
164
+ timeArgs: ["since", "until"],
165
+ async execute({ since, until }) {
166
+ return queryTelemetry(since, until);
167
+ },
168
+ });
169
+ ```
170
+
171
+ Every name in `timeArgs` must exist in the input schema; a session with
172
+ `asOf` refuses to start otherwise, because a misspelled name would silently
173
+ check nothing.
174
+
150
175
  ## Define an agent tool
151
176
 
152
177
  Set `execution: "agent"` and the tool materializes as a shell script
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cursor/july",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "repository": {
@@ -71,6 +71,7 @@ import {
71
71
  isServeRestartWorker,
72
72
  runEnterRestartSupervisor,
73
73
  } from "../internal/serve-restart.js";
74
+ import { flushSessionRunFatal } from "../internal/session-run-log.js";
74
75
  import { clearTerminal, stdoutPalette } from "../internal/terminal-style.js";
75
76
  import { cmdUpdate, warnIfOutdated } from "../internal/update-check.js";
76
77
  import type { AgentProject, AgentServeHandle } from "../types.js";
@@ -140,7 +141,7 @@ Usage:
140
141
  ${CLI} deploy [--dir <path>] [--slug <slug> | --all] [--team <id>] [--json]
141
142
  [--no-wait] [--repo <url>] [--ref <ref>]
142
143
  [--path <agentPath>] [--cursor-events-repo <owner/name>]...
143
- [--allow-domain <domain>]... [--multi-tenant]
144
+ [--allow-domain <domain>]...
144
145
  ${CLI} manifest [--dir <path>] [--json]
145
146
  ${CLI} deployments [--team <id>] [--json]
146
147
  ${CLI} deployment <slug> [--team <id>] [--json]
@@ -268,8 +269,6 @@ Commands:
268
269
  (flags always win). When --dir is a multi-agent parent,
269
270
  pick one interactively, or pass --slug / --all. First deploy
270
271
  of a slug prints the alias token exactly once — save it.
271
- --multi-tenant adds a release to an existing Cursor-managed
272
- product (v2 only); it cannot create a new multi-tenant app.
273
272
  deployments List the team's deployments (slug, status, generation, kind).
274
273
  deployment Show one deployment in full (status, lastError, engine info).
275
274
  stop Stop a deployment and wait until it reports stopped.
@@ -432,9 +431,6 @@ Options:
432
431
  --allow-domain deploy: egress domain the engine pod may reach
433
432
  (repeatable; unioned with hosting.egressDomains from the
434
433
  agent manifest). Repo-backed deploys only.
435
- --multi-tenant deploy: add a release to an existing Cursor-managed
436
- (multi-tenant) product. architecture v2 only. Cannot
437
- create a new multi-tenant application.
438
434
  --from-argv secrets set: allow NAME=VALUE on argv (leaks into shell
439
435
  history and agent-captured terminals). Default is names
440
436
  only with a hidden prompt or piped stdin.
@@ -578,6 +574,7 @@ async function main(argv: string[]): Promise<number> {
578
574
  all: { type: "boolean", default: false },
579
575
  "cursor-events-repo": { type: "string", multiple: true },
580
576
  "allow-domain": { type: "string", multiple: true },
577
+ // Hidden Anysphere-only flag; omitted from help.
581
578
  "multi-tenant": { type: "boolean", default: false },
582
579
  "from-argv": { type: "boolean", default: false },
583
580
  strict: { type: "boolean", default: false },
@@ -1729,7 +1726,9 @@ function installTransportErrorGuards(): void {
1729
1726
  process.stderr.write(
1730
1727
  `[agent-sdk] uncaughtException: ${error.stack ?? error.message}\n`
1731
1728
  );
1732
- process.exit(1);
1729
+ void flushSessionRunFatal(error).finally(() => {
1730
+ process.exit(1);
1731
+ });
1733
1732
  });
1734
1733
  }
1735
1734
 
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../internal/continuation-identity.js";
14
14
  import { isDeferredChannelSession } from "../../internal/deferred-channel-session.js";
15
15
  import { isDevMode } from "../../internal/dev-mode.js";
16
+ import { attachHostedAdmissionAdapter } from "../../internal/hosted-admission-adapter.js";
16
17
  import { bindLocalIngressSend } from "../../internal/local-ingress.js";
17
18
  import type {
18
19
  AgentCloudOptions,
@@ -804,6 +805,22 @@ export function githubChannel(
804
805
  ],
805
806
  });
806
807
  githubChannelOptionsByDefinition.set(definition, channelOptions);
808
+ attachHostedAdmissionAdapter(definition, {
809
+ kind: GITHUB_CHANNEL_KIND,
810
+ prepareArgs: withPropagatingGitHubBackgroundErrors,
811
+ dispatch: ({ eventType, payload, deliveryId, args }) =>
812
+ dispatchGitHubWebhook({
813
+ options: channelOptions,
814
+ eventName: eventType,
815
+ payload,
816
+ deliveryId,
817
+ args,
818
+ }),
819
+ getOutcome: (response) => {
820
+ const outcome = getGitHubDispatchOutcome(response);
821
+ return outcome === "skip" || outcome === "task" ? outcome : undefined;
822
+ },
823
+ });
807
824
  if (cursorCredentials !== undefined) {
808
825
  registerGithubCursorAccountRuntime(definition, {
809
826
  options,
@@ -10,7 +10,9 @@ import {
10
10
  continuationSource,
11
11
  normalizeIdentity,
12
12
  } from "../../internal/continuation-identity.js";
13
+ import { isDeferredChannelSession } from "../../internal/deferred-channel-session.js";
13
14
  import { isDevMode } from "../../internal/dev-mode.js";
15
+ import { attachHostedAdmissionAdapter } from "../../internal/hosted-admission-adapter.js";
14
16
  import { bindLocalIngressSend } from "../../internal/local-ingress.js";
15
17
  import type {
16
18
  AgentCloudOptions,
@@ -441,11 +443,35 @@ function enrichEventContext(input: {
441
443
  };
442
444
  }
443
445
 
446
+ export type OriginDispatchOutcome = "skip" | "send" | "task";
447
+
448
+ const dispatchOutcomeByResponse = new WeakMap<
449
+ Response,
450
+ OriginDispatchOutcome
451
+ >();
452
+
453
+ function withDispatchOutcome(
454
+ response: Response,
455
+ outcome: OriginDispatchOutcome
456
+ ): Response {
457
+ dispatchOutcomeByResponse.set(response, outcome);
458
+ return response;
459
+ }
460
+
461
+ export function getOriginDispatchOutcome(
462
+ response: Response
463
+ ): OriginDispatchOutcome | undefined {
464
+ return dispatchOutcomeByResponse.get(response);
465
+ }
466
+
444
467
  function skipped(
445
468
  reason: string,
446
469
  extra: Record<string, unknown> = {}
447
470
  ): Response {
448
- return Response.json({ ok: true, skipped: true, reason, ...extra });
471
+ return withDispatchOutcome(
472
+ Response.json({ ok: true, skipped: true, reason, ...extra }),
473
+ "skip"
474
+ );
449
475
  }
450
476
 
451
477
  function acknowledgeTask(
@@ -462,9 +488,12 @@ function acknowledgeTask(
462
488
  });
463
489
  })
464
490
  );
465
- return Response.json(
466
- { ok: true, dispatched: true, mode: "task", ...extra },
467
- { status: 202 }
491
+ return withDispatchOutcome(
492
+ Response.json(
493
+ { ok: true, dispatched: true, mode: "task", ...extra },
494
+ { status: 202 }
495
+ ),
496
+ "task"
468
497
  );
469
498
  }
470
499
 
@@ -485,12 +514,26 @@ async function startTurn(input: {
485
514
  title: input.dispatch.title ?? defaultOriginTitle(input.ctx),
486
515
  ...(input.cloud === undefined ? {} : { cloud: input.cloud }),
487
516
  });
488
- return Response.json({
489
- ok: true,
490
- sessionId: session.id,
491
- isNew: session.isNew,
492
- continuationToken: session.continuationToken,
493
- });
517
+ if (isDeferredChannelSession(session)) {
518
+ // Hosted admission captured the send; the control plane allocates the
519
+ // session after acknowledgement, so there is nothing to report yet.
520
+ return withDispatchOutcome(
521
+ Response.json(
522
+ { ok: true, dispatched: true, mode: "turn" },
523
+ { status: 202 }
524
+ ),
525
+ "send"
526
+ );
527
+ }
528
+ return withDispatchOutcome(
529
+ Response.json({
530
+ ok: true,
531
+ sessionId: session.id,
532
+ isNew: session.isNew,
533
+ continuationToken: session.continuationToken,
534
+ }),
535
+ "send"
536
+ );
494
537
  }
495
538
 
496
539
  async function resolveCloudOptions(
@@ -812,6 +855,27 @@ export function originChannel(
812
855
  ],
813
856
  });
814
857
  originChannelOptionsByDefinition.set(definition, channelOptions);
858
+ attachHostedAdmissionAdapter(definition, {
859
+ kind: ORIGIN_CHANNEL_KIND,
860
+ dispatch: async ({ payload, args }) => {
861
+ const envelope = parseOriginWebhookEnvelope(payload);
862
+ if (envelope === null) {
863
+ return Response.json(
864
+ { ok: false, error: "invalid_origin_payload" },
865
+ { status: 400 }
866
+ );
867
+ }
868
+ return dispatchOriginWebhook({
869
+ options: channelOptions,
870
+ envelope,
871
+ args,
872
+ });
873
+ },
874
+ getOutcome: (response) => {
875
+ const outcome = getOriginDispatchOutcome(response);
876
+ return outcome === "skip" || outcome === "task" ? outcome : undefined;
877
+ },
878
+ });
815
879
  if (usesCursorAccount) {
816
880
  registerOriginCursorAccountRuntime(definition, {
817
881
  options: channelOptions,
@@ -18,6 +18,7 @@ import {
18
18
  } from "./constants.js";
19
19
  import { createStreamDeliveryStrategy } from "./live-delivery.js";
20
20
  import { defaultSlackLog, type SlackLog } from "./log.js";
21
+ import { createProgressDelivery } from "./progress-delivery.js";
21
22
  import { buildTaskChunkForResult, buildTaskChunksForCalls } from "./stream.js";
22
23
  import type {
23
24
  SlackAccessPolicy,
@@ -154,6 +155,10 @@ export function buildDefaultEvents(
154
155
  const botToken = credentials?.botToken;
155
156
  const getLog = options.getLog ?? (() => defaultSlackLog);
156
157
  const strategy = createStreamDeliveryStrategy({ botToken, getLog });
158
+ const progress = createProgressDelivery({ botToken, getLog });
159
+
160
+ const streamLive = (state: SlackChannelState): boolean =>
161
+ streaming && !state.streamingUnavailable;
157
162
 
158
163
  const clearWorkingStatus = async (
159
164
  channelId: string,
@@ -174,6 +179,62 @@ export function buildDefaultEvents(
174
179
  }
175
180
  };
176
181
 
182
+ const finishProgress = async (args: {
183
+ channel: ChannelEventContext<SlackChannelState>;
184
+ text: string;
185
+ sessionId: string;
186
+ }): Promise<boolean> => {
187
+ const log = getLog();
188
+ const state = args.channel.state;
189
+ if (
190
+ state.channelId === null ||
191
+ state.threadTs === null ||
192
+ args.text === ""
193
+ ) {
194
+ return false;
195
+ }
196
+ if (await progress.replace({ channel: args.channel, text: args.text })) {
197
+ log.info("reply delivered via progress update", {
198
+ channel: state.channelId,
199
+ thread_ts: state.threadTs,
200
+ session: args.sessionId,
201
+ chars: args.text.length,
202
+ });
203
+ return true;
204
+ }
205
+ await progress.clear({ channel: args.channel });
206
+ try {
207
+ const posted = await postMessage({
208
+ botToken,
209
+ channel: state.channelId,
210
+ text: args.text,
211
+ threadTs: state.threadTs,
212
+ });
213
+ state.lastPostedText = args.text;
214
+ state.lastBotMessageTs = posted.ts;
215
+ log.info("reply delivered via postMessage", {
216
+ channel: state.channelId,
217
+ thread_ts: state.threadTs,
218
+ message_ts: posted.ts,
219
+ session: args.sessionId,
220
+ chars: args.text.length,
221
+ });
222
+ return true;
223
+ } catch (error) {
224
+ log.error(
225
+ `postMessage failed: ${
226
+ error instanceof Error ? error.message : String(error)
227
+ }`,
228
+ {
229
+ channel: state.channelId,
230
+ thread_ts: state.threadTs,
231
+ session: args.sessionId,
232
+ }
233
+ );
234
+ return false;
235
+ }
236
+ };
237
+
177
238
  /**
178
239
  * Deliver assistant text to the bound Slack thread. Used by a stop-reason
179
240
  * `message.completed` and by `turn.completed` when the last assistant
@@ -185,7 +246,6 @@ export function buildDefaultEvents(
185
246
  text: string;
186
247
  sessionId: string;
187
248
  }): Promise<boolean> => {
188
- const log = getLog();
189
249
  const state = args.channel.state;
190
250
  if (
191
251
  state.channelId === null ||
@@ -199,8 +259,7 @@ export function buildDefaultEvents(
199
259
  // pending. Treat a match as "already on Slack", not "already finalized".
200
260
  const streamTs = state.streamTs;
201
261
  if (streamTs !== null && streamTs !== "") {
202
- const canStream = streaming && state.streamingUnavailable !== true;
203
- if (canStream) {
262
+ if (streamLive(state)) {
204
263
  await strategy.finalize({
205
264
  channel: args.channel,
206
265
  channelId: state.streamChannelId ?? state.channelId,
@@ -214,8 +273,7 @@ export function buildDefaultEvents(
214
273
  return true;
215
274
  }
216
275
 
217
- const canStream = streaming && state.streamingUnavailable !== true;
218
- if (canStream) {
276
+ if (streamLive(state)) {
219
277
  const { delivered } = await strategy.finalize({
220
278
  channel: args.channel,
221
279
  channelId: state.streamChannelId ?? state.channelId,
@@ -227,47 +285,20 @@ export function buildDefaultEvents(
227
285
  }
228
286
  }
229
287
 
230
- try {
231
- const posted = await postMessage({
232
- botToken,
233
- channel: state.channelId,
234
- text: args.text,
235
- threadTs: state.threadTs,
236
- });
237
- state.lastPostedText = args.text;
238
- state.lastBotMessageTs = posted.ts;
239
- log.info("reply delivered via postMessage", {
240
- channel: state.channelId,
241
- thread_ts: state.threadTs,
242
- message_ts: posted.ts,
243
- session: args.sessionId,
244
- chars: args.text.length,
245
- });
246
- return true;
247
- } catch (error) {
248
- log.error(
249
- `postMessage failed: ${
250
- error instanceof Error ? error.message : String(error)
251
- }`,
252
- {
253
- channel: state.channelId,
254
- thread_ts: state.threadTs,
255
- session: args.sessionId,
256
- }
257
- );
258
- return false;
259
- }
288
+ return finishProgress(args);
260
289
  };
261
290
 
262
291
  return {
263
292
  async "turn.started"(event, channel) {
264
293
  const log = getLog();
294
+ await progress.clear({ channel });
265
295
  channel.state.pendingToolCallMessage = null;
266
296
  channel.state.lastReasoningTypingAtMs = null;
267
297
  channel.state.lastReasoningTypingStatus = null;
268
298
  channel.state.streamTs = null;
269
299
  channel.state.streamChannelId = null;
270
300
  channel.state.lastPostedText = null;
301
+ channel.state.streamBlockText = null;
271
302
  // Keep lastBotMessageTs across turns — warm delta watermark.
272
303
  channel.state.approvalMessageByCallId = {};
273
304
 
@@ -293,7 +324,7 @@ export function buildDefaultEvents(
293
324
  },
294
325
 
295
326
  async "message.appended"(event, channel) {
296
- if (!streaming || channel.state.streamingUnavailable === true) {
327
+ if (!streamLive(channel.state)) {
297
328
  return;
298
329
  }
299
330
  const channelId =
@@ -362,8 +393,7 @@ export function buildDefaultEvents(
362
393
  if (
363
394
  strategy.inlineToolSteps &&
364
395
  thinkingSteps &&
365
- streaming &&
366
- channel.state.streamingUnavailable !== true
396
+ streamLive(channel.state)
367
397
  ) {
368
398
  const channelId =
369
399
  channel.state.streamChannelId ?? channel.state.channelId;
@@ -403,17 +433,24 @@ export function buildDefaultEvents(
403
433
  threadTs: channel.state.threadTs,
404
434
  status: statusText,
405
435
  });
436
+ if (
437
+ thinkingSteps &&
438
+ channel.state.streamingUnavailable === true &&
439
+ (channel.state.streamTs === null || channel.state.streamTs === "")
440
+ ) {
441
+ await progress.upsert({ channel, text: statusText });
442
+ }
406
443
  }
407
444
  },
408
445
 
409
446
  async "action.result"(event, channel) {
447
+ const streamTs = channel.state.streamTs;
410
448
  if (
411
449
  !strategy.inlineToolSteps ||
412
450
  !thinkingSteps ||
413
- !streaming ||
414
- channel.state.streamingUnavailable === true ||
415
- channel.state.streamTs === null ||
416
- channel.state.streamTs === ""
451
+ !streamLive(channel.state) ||
452
+ streamTs === null ||
453
+ streamTs === ""
417
454
  ) {
418
455
  return;
419
456
  }
@@ -430,7 +467,7 @@ export function buildDefaultEvents(
430
467
  await appendChatStream({
431
468
  botToken,
432
469
  channel: channelId,
433
- ts: channel.state.streamTs,
470
+ ts: streamTs,
434
471
  chunks: [chunk],
435
472
  });
436
473
  },
@@ -439,6 +476,9 @@ export function buildDefaultEvents(
439
476
  if (event.data.finishReason === "tool_call") {
440
477
  channel.state.pendingToolCallMessage =
441
478
  firstNonEmptyLine(event.data.text) ?? null;
479
+ // The harness restarts its cumulative text after a tool call, so the
480
+ // next block appends in full instead of diffing against this one.
481
+ channel.state.streamBlockText = null;
442
482
  return;
443
483
  }
444
484
  channel.state.pendingToolCallMessage = null;
@@ -452,6 +492,7 @@ export function buildDefaultEvents(
452
492
  // and clear typing.
453
493
  if (event.data.text === "") {
454
494
  await strategy.finalizeEmpty({ channel });
495
+ await progress.clear({ channel });
455
496
  await clearWorkingStatus(state.channelId, state.threadTs);
456
497
  return;
457
498
  }
@@ -492,6 +533,7 @@ export function buildDefaultEvents(
492
533
  // Always close leftovers. deliverReplyText may fall through to
493
534
  // postMessage when streaming is unavailable and leave streamTs open.
494
535
  await strategy.cleanupOrphan({ channel });
536
+ await progress.clear({ channel });
495
537
 
496
538
  // Always drop the assistant chip. message.completed already clears
497
539
  // after a normal reply, but a tool-call-only finish (or a missed
@@ -539,7 +581,7 @@ export function buildDefaultEvents(
539
581
  const errorText = formatSlackTurnFailureMessage(event.data.message);
540
582
 
541
583
  let delivered = false;
542
- if (streaming && state.streamingUnavailable !== true) {
584
+ if (streamLive(state)) {
543
585
  ({ delivered } = await strategy.fail({
544
586
  channel,
545
587
  channelId: state.streamChannelId ?? state.channelId,
@@ -548,25 +590,11 @@ export function buildDefaultEvents(
548
590
  }));
549
591
  }
550
592
  if (!delivered) {
551
- try {
552
- await postMessage({
553
- botToken,
554
- channel: state.channelId,
555
- text: errorText,
556
- threadTs: state.threadTs,
557
- });
558
- } catch (error) {
559
- log.error(
560
- `turn.failed postMessage failed: ${
561
- error instanceof Error ? error.message : String(error)
562
- }`,
563
- {
564
- channel: state.channelId,
565
- thread_ts: state.threadTs,
566
- session: event.sessionId,
567
- }
568
- );
569
- }
593
+ await finishProgress({
594
+ channel,
595
+ text: errorText,
596
+ sessionId: event.sessionId,
597
+ });
570
598
  }
571
599
  await clearWorkingStatus(state.channelId, state.threadTs);
572
600
  },
@@ -126,8 +126,10 @@ export function emptySlackChannelState(
126
126
  streamTs: null,
127
127
  streamChannelId: null,
128
128
  lastPostedText: null,
129
+ streamBlockText: null,
129
130
  lastBotMessageTs: null,
130
131
  streamingUnavailable: false,
132
+ progressTs: null,
131
133
  approvalMessageByCallId: {},
132
134
  ...patch,
133
135
  };
@@ -41,7 +41,7 @@ type StreamArgs = {
41
41
  /**
42
42
  * How the default handlers deliver an in-progress reply. Methods write live
43
43
  * delivery state (`streamTs`, `streamChannelId`, `lastPostedText`,
44
- * `streamingUnavailable`) on `channel.state` directly.
44
+ * `streamBlockText`, `streamingUnavailable`) on `channel.state` directly.
45
45
  */
46
46
  export interface LiveDeliveryStrategy {
47
47
  readonly inlineToolSteps: boolean;
@@ -127,6 +127,7 @@ export function createStreamDeliveryStrategy(
127
127
  channel.state.streamTs = started.ts;
128
128
  channel.state.streamChannelId = started.channel;
129
129
  channel.state.lastPostedText = hasMarkdown ? (markdown_text ?? "") : "";
130
+ channel.state.streamBlockText = channel.state.lastPostedText;
130
131
  log.info("chat.startStream ok", {
131
132
  channel: started.channel,
132
133
  stream_ts: started.ts,
@@ -167,7 +168,7 @@ export function createStreamDeliveryStrategy(
167
168
  text: string;
168
169
  }): Promise<{ ok: boolean; error?: string }> => {
169
170
  const appendText = computeStreamAppend(
170
- args.channel.state.lastPostedText,
171
+ args.channel.state.streamBlockText,
171
172
  args.text
172
173
  );
173
174
  const stopped = await stopChatStream({
@@ -213,7 +214,7 @@ export function createStreamDeliveryStrategy(
213
214
  return;
214
215
  }
215
216
  const appendText = computeStreamAppend(
216
- channel.state.lastPostedText,
217
+ channel.state.streamBlockText,
217
218
  text
218
219
  );
219
220
  if (appendText === "") {
@@ -238,6 +239,7 @@ export function createStreamDeliveryStrategy(
238
239
  return;
239
240
  }
240
241
  channel.state.lastPostedText = text;
242
+ channel.state.streamBlockText = text;
241
243
  },
242
244
 
243
245
  async appendChunks({ channel, channelId, chunks, sessionId }) {