experimental-ash 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +18 -2
  2. package/dist/docs/public/channels/README.md +75 -9
  3. package/dist/docs/public/schedules.md +13 -4
  4. package/dist/src/channel/adapter-context.d.ts +4 -0
  5. package/dist/src/channel/adapter-context.js +6 -0
  6. package/dist/src/channel/adapter.d.ts +10 -10
  7. package/dist/src/channel/cross-channel-receive.d.ts +1 -1
  8. package/dist/src/channel/cross-channel-receive.js +40 -0
  9. package/dist/src/channel/routes.d.ts +7 -0
  10. package/dist/src/channel/send.js +1 -1
  11. package/dist/src/channel/session.d.ts +47 -1
  12. package/dist/src/channel/session.js +46 -0
  13. package/dist/src/channel/types.d.ts +6 -5
  14. package/dist/src/chunks/{dev-authored-source-watcher-DtLxnrXI.js → dev-authored-source-watcher-j7YWh2Gx.js} +1 -1
  15. package/dist/src/chunks/{host-Dor4C8jo.js → host-DkTSR6YJ.js} +2 -2
  16. package/dist/src/chunks/{paths-AVYgVLR3.js → paths-Dwv0Eash.js} +21 -21
  17. package/dist/src/chunks/{prewarm-DsMkM8wg.js → prewarm-CQYfka30.js} +1 -1
  18. package/dist/src/cli/commands/info.js +1 -1
  19. package/dist/src/cli/run.js +1 -1
  20. package/dist/src/evals/cli/eval.js +1 -1
  21. package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
  22. package/dist/src/execution/sandbox/bindings/vercel.js +1 -34
  23. package/dist/src/execution/workflow-entry.js +35 -31
  24. package/dist/src/execution/workflow-steps.d.ts +16 -0
  25. package/dist/src/execution/workflow-steps.js +32 -4
  26. package/dist/src/harness/attachment-staging.js +2 -1
  27. package/dist/src/internal/application/package.js +1 -1
  28. package/dist/src/public/channels/slack/api.d.ts +13 -8
  29. package/dist/src/public/channels/slack/api.js +31 -17
  30. package/dist/src/public/channels/slack/index.d.ts +2 -2
  31. package/dist/src/public/channels/slack/index.js +1 -0
  32. package/dist/src/public/channels/slack/interactions.js +3 -3
  33. package/dist/src/public/channels/slack/slackChannel.d.ts +5 -3
  34. package/dist/src/public/channels/slack/slackChannel.js +26 -15
  35. package/dist/src/public/channels/twilio/api.d.ts +9 -0
  36. package/dist/src/public/channels/twilio/api.js +11 -0
  37. package/dist/src/public/channels/twilio/index.d.ts +1 -1
  38. package/dist/src/public/channels/twilio/index.js +1 -1
  39. package/dist/src/public/channels/twilio/twilioChannel.d.ts +2 -0
  40. package/dist/src/public/channels/twilio/twilioChannel.js +8 -11
  41. package/dist/src/public/definitions/defineChannel.d.ts +9 -1
  42. package/dist/src/public/definitions/defineChannel.js +7 -11
  43. package/dist/src/public/definitions/sandbox.d.ts +2 -3
  44. package/dist/src/public/sandbox/backends/vercel.d.ts +4 -4
  45. package/dist/src/public/sandbox/backends/vercel.js +2 -2
  46. package/dist/src/public/sandbox/index.d.ts +1 -1
  47. package/dist/src/public/sandbox/vercel-sandbox.d.ts +3 -28
  48. package/dist/src/runtime/types.d.ts +1 -2
  49. package/dist/src/shared/sandbox-backend.d.ts +4 -4
  50. package/dist/src/shared/sandbox-definition.d.ts +6 -6
  51. package/package.json +1 -1
@@ -1 +1 @@
1
- import{n as e}from"../../chunks/paths-AVYgVLR3.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-Dor4C8jo.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
1
+ import{n as e}from"../../chunks/paths-Dwv0Eash.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-DkTSR6YJ.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
@@ -1,7 +1,7 @@
1
1
  import type * as VercelSandboxSdk from "#compiled/@vercel/sandbox/index.js";
2
2
  import type { Sandbox as SdkSandbox } from "#compiled/@vercel/sandbox/index.js";
3
3
  import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
4
- import type { VercelSandbox, VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions } from "#public/sandbox/vercel-sandbox.js";
4
+ import type { VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions } from "#public/sandbox/vercel-sandbox.js";
5
5
  type VercelSandboxModule = typeof VercelSandboxSdk;
6
6
  /**
7
7
  * User-controllable subset of `Sandbox.create` parameters.
@@ -18,5 +18,5 @@ export interface CreateVercelSandboxBackendInput {
18
18
  /**
19
19
  * Creates the Vercel-backed sandbox backend.
20
20
  */
21
- export declare function createVercelSandboxBackend(input?: CreateVercelSandboxBackendInput): SandboxBackend<VercelSandbox, VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions>;
21
+ export declare function createVercelSandboxBackend(input?: CreateVercelSandboxBackendInput): SandboxBackend<VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions>;
22
22
  export {};
@@ -168,7 +168,7 @@ function createHandle(sandbox, sessionKey) {
168
168
  if (options !== undefined) {
169
169
  await sandbox.update(options);
170
170
  }
171
- return buildVercelSandbox(sandbox, sessionKey);
171
+ return buildSandboxSession(createVercelInternalSandboxSession(sandbox, sessionKey));
172
172
  },
173
173
  async captureState() {
174
174
  return {
@@ -180,39 +180,6 @@ function createHandle(sandbox, sessionKey) {
180
180
  async dispose() { },
181
181
  };
182
182
  }
183
- function buildVercelSandbox(sandbox, id) {
184
- const session = buildSandboxSession(createVercelInternalSandboxSession(sandbox, id));
185
- return {
186
- ...session,
187
- get name() {
188
- return sandbox.name;
189
- },
190
- get persistent() {
191
- return sandbox.persistent;
192
- },
193
- get status() {
194
- return sandbox.status;
195
- },
196
- get networkPolicy() {
197
- return sandbox.networkPolicy;
198
- },
199
- get tags() {
200
- return sandbox.tags;
201
- },
202
- async update(params) {
203
- await sandbox.update(params);
204
- },
205
- async stop(opts) {
206
- await sandbox.stop(opts);
207
- },
208
- async snapshot(opts) {
209
- return await sandbox.snapshot(opts);
210
- },
211
- domain(port) {
212
- return sandbox.domain(port);
213
- },
214
- };
215
- }
216
183
  function createVercelInternalSandboxSession(sandbox, id) {
217
184
  return {
218
185
  id,
@@ -108,16 +108,21 @@ async function runWorkflowLoop(input) {
108
108
  });
109
109
  return { output: currentResult.output ?? "" };
110
110
  }
111
- const parkToken = currentSession.continuationToken || deriveAdapterContinuationToken(currentSerializedContext);
112
- if (!parkToken) {
113
- throw new Error("Cannot park: no continuation token available. The adapter must " +
114
- "provide a token via getContinuationToken() (e.g., by posting the " +
115
- "first message to the platform during the initial turn).");
111
+ // The first turn may have re-keyed the session via
112
+ // `ctx.session.setContinuationToken(...)` (e.g. Slack auto-anchor on
113
+ // first post). `currentSession.continuationToken` already reflects
114
+ // the latest token thanks to `reconcileSessionContinuationToken`
115
+ // inside `durableRunStep`, so creating the park hook here lands at
116
+ // the right token from the start.
117
+ if (!currentSession.continuationToken) {
118
+ throw new Error("Cannot park: no continuation token available. The channel must " +
119
+ "post the first message during the initial turn (anchoring the " +
120
+ "session) or `send()` must be called with an explicit " +
121
+ "continuationToken.");
116
122
  }
117
- const hook = createHook({
118
- token: parkToken,
119
- });
120
- const iterator = hook[Symbol.asyncIterator]();
123
+ let parkToken = currentSession.continuationToken;
124
+ let hook = createHook({ token: parkToken });
125
+ let iterator = hook[Symbol.asyncIterator]();
121
126
  let pendingNext = null;
122
127
  const getNextPromise = () => {
123
128
  pendingNext ??= iterator.next();
@@ -126,6 +131,24 @@ async function runWorkflowLoop(input) {
126
131
  const consumeNext = () => {
127
132
  pendingNext = null;
128
133
  };
134
+ /**
135
+ * Disposes the current park hook and creates a new one at
136
+ * {@link nextToken}. Called after a turn whose handlers
137
+ * re-keyed the session via `setContinuationToken(...)`. In-flight
138
+ * deliveries to the old token after this returns are silently
139
+ * dropped — channels that re-key mid-session must coordinate with
140
+ * their senders so follow-up traffic uses the new token.
141
+ */
142
+ const rekeyHook = async (nextToken) => {
143
+ if (nextToken === parkToken || !nextToken)
144
+ return;
145
+ await closeHookIterator(iterator);
146
+ await disposeHook(hook);
147
+ parkToken = nextToken;
148
+ hook = createHook({ token: parkToken });
149
+ iterator = hook[Symbol.asyncIterator]();
150
+ pendingNext = null;
151
+ };
129
152
  try {
130
153
  while (true) {
131
154
  if (hasPendingRuntimeActionBatch(currentSession)) {
@@ -137,6 +160,7 @@ async function runWorkflowLoop(input) {
137
160
  bufferedDeliveries,
138
161
  getNextPromise,
139
162
  consumeNext,
163
+ rekeyHook,
140
164
  serializedContext: currentSerializedContext,
141
165
  session: currentSession,
142
166
  });
@@ -184,6 +208,7 @@ async function runWorkflowLoop(input) {
184
208
  });
185
209
  return { output: currentResult.output ?? "" };
186
210
  }
211
+ await rekeyHook(currentSession.continuationToken);
187
212
  }
188
213
  }
189
214
  finally {
@@ -292,6 +317,7 @@ async function waitForPendingRuntimeActionResults(input) {
292
317
  });
293
318
  currentSession = proxyResult.session;
294
319
  currentSerializedContext = proxyResult.serializedContext;
320
+ await input.rekeyHook(currentSession.continuationToken);
295
321
  }
296
322
  },
297
323
  session: currentSession,
@@ -456,25 +482,3 @@ async function disposeHook(hook) {
456
482
  }
457
483
  await symbolDispose.call(hook);
458
484
  }
459
- /**
460
- * Derives the continuation token from the serialized adapter state.
461
- *
462
- * Used when no explicit continuation token was provided on RunInput
463
- * (schedule-initiated sessions). The token is derived from the
464
- * adapter's serialized resume identity — for Slack, this is the
465
- * continuation token `slack:{channelId}:{threadTs}` when threadTs is populated.
466
- */
467
- function deriveAdapterContinuationToken(serializedContext) {
468
- const channel = serializedContext["ash.channel"];
469
- if (!channel?.state)
470
- return undefined;
471
- const serializedThread = channel.state.serializedThread;
472
- if (!serializedThread?.id)
473
- return undefined;
474
- const id = serializedThread.id;
475
- const lastColon = id.lastIndexOf(":");
476
- if (lastColon === -1)
477
- return undefined;
478
- const threadTs = id.slice(lastColon + 1);
479
- return threadTs.length > 0 ? id : undefined;
480
- }
@@ -1,4 +1,5 @@
1
1
  import type { DeliverPayload, HookPayload, SessionAuthContext, SubagentInputRequestHookPayload } from "#channel/types.js";
2
+ import { deserializeContext } from "#context/serialize.js";
2
3
  import type { HarnessSession } from "#harness/types.js";
3
4
  import type { RuntimeCompiledArtifactsSource } from "#runtime/compiled-artifacts-source.js";
4
5
  import type { DurableStepResult } from "#execution/types.js";
@@ -6,6 +7,21 @@ import type { DurableStepResult } from "#execution/types.js";
6
7
  * Runs one atomic harness step inside a durable `"use step"` boundary.
7
8
  */
8
9
  export declare function durableRunStep(serializedContext: Record<string, unknown>, session: HarnessSession, input: HookPayload | undefined): Promise<DurableStepResult>;
10
+ /**
11
+ * Reconciles `session.continuationToken` with the live
12
+ * `ContinuationTokenKey` value in context.
13
+ *
14
+ * Channels mutate the token by calling
15
+ * `ctx.session.setContinuationToken(...)`, which writes through to
16
+ * `ContinuationTokenKey`. The serialized context picks the new value
17
+ * up automatically, but the in-memory {@link HarnessSession} the
18
+ * workflow body carries needs to be re-stamped so its `parkToken`
19
+ * read after the step reflects the re-key.
20
+ *
21
+ * Exported for unit testing; the workflow runtime is the only
22
+ * production caller.
23
+ */
24
+ export declare function reconcileSessionContinuationToken(ctx: Awaited<ReturnType<typeof deserializeContext>>, session: HarnessSession): HarnessSession;
9
25
  /**
10
26
  * Starts every pending runtime action for the parked parent session.
11
27
  *
@@ -3,7 +3,7 @@ import { buildAdapterContext } from "#channel/adapter-context.js";
3
3
  import { callAdapterEventHandler, defaultDeliverResult } from "#channel/adapter.js";
4
4
  import { toContextAccessor } from "#context/container.js";
5
5
  import { dispatchStreamEventHooks, runHookLifecycleStep } from "#context/hook-lifecycle.js";
6
- import { AuthKey, BundleKey, CapabilitiesKey, ChannelKey, InitiatorAuthKey, ModeKey, } from "#context/keys.js";
6
+ import { AuthKey, BundleKey, CapabilitiesKey, ChannelKey, ContinuationTokenKey, InitiatorAuthKey, ModeKey, } from "#context/keys.js";
7
7
  import { runStep } from "#context/run-step.js";
8
8
  import { deserializeContext, serializeContext } from "#context/serialize.js";
9
9
  import { getHarnessEmissionState, isHarnessBetweenTurns } from "#harness/emission.js";
@@ -65,6 +65,7 @@ export async function durableRunStep(serializedContext, session, input) {
65
65
  // only edits a message). Persist any context mutations and re-park
66
66
  // without running a model turn.
67
67
  if (input?.kind === "deliver" && resolved === undefined) {
68
+ const rekeyed = reconcileSessionContinuationToken(ctx, session);
68
69
  const nextSerializedContext = serializeContext(ctx);
69
70
  const writable = getWritable();
70
71
  const writer = writable.getWriter();
@@ -72,7 +73,7 @@ export async function durableRunStep(serializedContext, session, input) {
72
73
  return {
73
74
  action: "park",
74
75
  serializedContext: nextSerializedContext,
75
- session,
76
+ session: rekeyed,
76
77
  };
77
78
  }
78
79
  const writable = getWritable();
@@ -84,7 +85,7 @@ export async function durableRunStep(serializedContext, session, input) {
84
85
  await writer.write(encodeMessageStreamEvent(timestampHandleMessageStreamEvent(toEmit)));
85
86
  await dispatchStreamEventHooks({ ctx, registry: hookRegistry, event: toEmit });
86
87
  };
87
- const stepResult = await runStep(ctx, session, async (enrichedSession) => {
88
+ let stepResult = await runStep(ctx, session, async (enrichedSession) => {
88
89
  const bundle = ctx.require(BundleKey);
89
90
  const capabilities = ctx.get(CapabilitiesKey);
90
91
  const mode = ctx.require(ModeKey);
@@ -129,7 +130,13 @@ export async function durableRunStep(serializedContext, session, input) {
129
130
  }
130
131
  return runHarnessStep(enrichedSession, resolved);
131
132
  });
133
+ // Reconcile the session's continuation token with `ContinuationTokenKey`
134
+ // in case a handler called `session.setContinuationToken(...)` during
135
+ // the turn (e.g. Slack's auto-anchor on first post). Idempotent when
136
+ // the token is unchanged.
137
+ const rekeyed = reconcileSessionContinuationToken(ctx, stepResult.session);
132
138
  const nextSerializedContext = serializeContext(ctx);
139
+ stepResult = { ...stepResult, session: rekeyed };
133
140
  // Interactive OAuth. When the step left one or more pending
134
141
  // connection tool calls (placeholders written to history) and at
135
142
  // least one matching pending authorization, the workflow body owns
@@ -177,6 +184,26 @@ export async function durableRunStep(serializedContext, session, input) {
177
184
  session: stepResult.session,
178
185
  };
179
186
  }
187
+ /**
188
+ * Reconciles `session.continuationToken` with the live
189
+ * `ContinuationTokenKey` value in context.
190
+ *
191
+ * Channels mutate the token by calling
192
+ * `ctx.session.setContinuationToken(...)`, which writes through to
193
+ * `ContinuationTokenKey`. The serialized context picks the new value
194
+ * up automatically, but the in-memory {@link HarnessSession} the
195
+ * workflow body carries needs to be re-stamped so its `parkToken`
196
+ * read after the step reflects the re-key.
197
+ *
198
+ * Exported for unit testing; the workflow runtime is the only
199
+ * production caller.
200
+ */
201
+ export function reconcileSessionContinuationToken(ctx, session) {
202
+ const next = ctx.get(ContinuationTokenKey);
203
+ if (next === undefined || next === session.continuationToken)
204
+ return session;
205
+ return { ...session, continuationToken: next };
206
+ }
180
207
  /**
181
208
  * Starts every pending runtime action for the parked parent session.
182
209
  *
@@ -333,11 +360,12 @@ export async function runProxyInputRequestStep(input) {
333
360
  // adapter and later text-reply deliveries miss the cached batch.
334
361
  ctx.set(ChannelKey, { ...adapter, state: { ...adapterCtx.state } });
335
362
  const nextSerializedContext = serializeContext(ctx);
336
- const nextSession = upsertProxyInputRequests({
363
+ const sessionWithProxyEntries = upsertProxyInputRequests({
337
364
  entries: proxyResult.entries,
338
365
  forChildContinuationToken: input.hookPayload.childContinuationToken,
339
366
  session: proxyResult.session,
340
367
  });
368
+ const nextSession = reconcileSessionContinuationToken(ctx, sessionWithProxyEntries);
341
369
  return {
342
370
  serializedContext: nextSerializedContext,
343
371
  session: nextSession,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { basename } from "node:path";
3
3
  import { buildAdapterContext } from "#channel/adapter-context.js";
4
4
  import { getAdapterKind } from "#channel/adapter.js";
5
+ import { buildSessionHandle } from "#channel/session.js";
5
6
  import { loadContext } from "#context/container.js";
6
7
  import { ChannelKey, SandboxKey } from "#context/keys.js";
7
8
  import { fileDataToBytes } from "#internal/attachments/data.js";
@@ -83,7 +84,7 @@ export async function stageAttachmentsToSandbox(message) {
83
84
  const adapter = container.get(ChannelKey);
84
85
  const adapterCtx = adapter
85
86
  ? buildAdapterContext(adapter, container)
86
- : { ctx: container, state: {} };
87
+ : { ctx: container, state: {}, session: buildSessionHandle(container) };
87
88
  return stageAttachmentsForAdapter(message, sandbox, adapterCtx);
88
89
  }
89
90
  /**
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
6
6
  let cachedPackageInfo;
7
7
  // The package build stamps the published version into `dist` so bundled
8
8
  // deployments can still report package metadata without resolving package.json.
9
- const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.18.1";
9
+ const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.18.2";
10
10
  const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
11
11
  const WORKFLOW_MODULE_ALIASES = {
12
12
  "workflow/api": "src/compiled/@workflow/core/runtime.js",
@@ -22,12 +22,13 @@ import { type CardElement, type FileUpload } from "#compiled/chat/index.js";
22
22
  */
23
23
  export type SlackBotToken = string | (() => string | Promise<string>);
24
24
  /**
25
- * Builds the Slack channel-local continuation token (`<channelId>:<threadTs>`).
26
- * Route `send()` namespaces this with the channel name before handing it to the
27
- * runtime (`slack:<channelId>:<threadTs>`), so Slack routes should pass the raw
28
- * channel-local form here.
25
+ * Builds the Slack channel-local continuation token
26
+ * (`<channelId>:<threadTs>`). Route `send()` namespaces this with the
27
+ * channel name before handing it to the runtime
28
+ * (`slack:<channelId>:<threadTs>`). `threadTs` may be empty for
29
+ * threadless sessions; the channel auto-anchors on its first post.
29
30
  */
30
- export declare function encodeSlackContinuationToken(channelId: string, threadTs: string): string;
31
+ export declare function slackContinuationToken(channelId: string, threadTs: string): string;
31
32
  /**
32
33
  * Materializes a {@link SlackBotToken} to a concrete string. Falls
33
34
  * back to `process.env.SLACK_BOT_TOKEN` when no value was passed.
@@ -267,15 +268,19 @@ export interface SlackBinding {
267
268
  readonly slack: SlackHandle;
268
269
  }
269
270
  /**
270
- * Internal-only constructor for the `{ thread, slack }` pair. Returns
271
- * two objects that share one bot-token resolver / `request()` closure
272
- * and one `recentMessages` array (mutated by `refresh`).
271
+ * Internal constructor for the `{ thread, slack }` pair.
272
+ *
273
+ * Auto-anchor: when the binding starts without a `threadTs`, the first
274
+ * `chat.postMessage` adopts its own `ts` as the thread root; the live
275
+ * `threadTs` is updated and `onAnchor` fires so the caller can persist
276
+ * the anchor. Ephemerals and files-only posts do not anchor.
273
277
  */
274
278
  export declare function buildSlackBinding(input: {
275
279
  readonly botToken: SlackBotToken | undefined;
276
280
  readonly channelId: string;
277
281
  readonly threadTs: string;
278
282
  readonly teamId: string | undefined;
283
+ readonly onAnchor?: (ts: string) => void;
279
284
  }): SlackBinding;
280
285
  /**
281
286
  * Best-effort GFM → Slack mrkdwn converter used only in contexts that
@@ -20,12 +20,13 @@ import { encodeSlackApiBody } from "#public/channels/slack/api-encoding.js";
20
20
  import { cardToBlocks, cardToFallbackText } from "#public/channels/slack/blocks.js";
21
21
  const log = createLogger("slack.api");
22
22
  /**
23
- * Builds the Slack channel-local continuation token (`<channelId>:<threadTs>`).
24
- * Route `send()` namespaces this with the channel name before handing it to the
25
- * runtime (`slack:<channelId>:<threadTs>`), so Slack routes should pass the raw
26
- * channel-local form here.
23
+ * Builds the Slack channel-local continuation token
24
+ * (`<channelId>:<threadTs>`). Route `send()` namespaces this with the
25
+ * channel name before handing it to the runtime
26
+ * (`slack:<channelId>:<threadTs>`). `threadTs` may be empty for
27
+ * threadless sessions; the channel auto-anchors on its first post.
27
28
  */
28
- export function encodeSlackContinuationToken(channelId, threadTs) {
29
+ export function slackContinuationToken(channelId, threadTs) {
29
30
  return `${channelId}:${threadTs}`;
30
31
  }
31
32
  /**
@@ -67,19 +68,29 @@ export function createSlackRequester(botToken) {
67
68
  return (operation, body) => callSlackApi({ botToken, operation, body });
68
69
  }
69
70
  /**
70
- * Internal-only constructor for the `{ thread, slack }` pair. Returns
71
- * two objects that share one bot-token resolver / `request()` closure
72
- * and one `recentMessages` array (mutated by `refresh`).
71
+ * Internal constructor for the `{ thread, slack }` pair.
72
+ *
73
+ * Auto-anchor: when the binding starts without a `threadTs`, the first
74
+ * `chat.postMessage` adopts its own `ts` as the thread root; the live
75
+ * `threadTs` is updated and `onAnchor` fires so the caller can persist
76
+ * the anchor. Ephemerals and files-only posts do not anchor.
73
77
  */
74
78
  export function buildSlackBinding(input) {
75
79
  const request = createSlackRequester(input.botToken);
76
80
  const messages = [];
81
+ let currentThreadTs = input.threadTs;
82
+ function anchorIfUnset(ts) {
83
+ if (currentThreadTs.length > 0 || ts.length === 0)
84
+ return;
85
+ currentThreadTs = ts;
86
+ input.onAnchor?.(ts);
87
+ }
77
88
  async function uploadFiles(files, options) {
78
89
  if (files.length === 0) {
79
90
  return { fileIds: [], raw: { ok: true } };
80
91
  }
81
92
  const channelId = options?.channelId ?? input.channelId;
82
- const threadTs = options?.threadTs ?? input.threadTs;
93
+ const threadTs = options?.threadTs ?? currentThreadTs;
83
94
  const token = await resolveSlackBotToken(input.botToken);
84
95
  const fileIds = [];
85
96
  for (const file of files) {
@@ -151,12 +162,13 @@ export function buildSlackBinding(input) {
151
162
  : "";
152
163
  return { id, raw: result.raw };
153
164
  }
154
- const body = buildPostMessageBody(message, input.channelId, input.threadTs);
165
+ const body = buildPostMessageBody(message, input.channelId, currentThreadTs);
155
166
  const response = await request("chat.postMessage", body);
156
167
  if (response.ok !== true) {
157
168
  throw new Error(`Slack chat.postMessage failed: ${response.error ?? "unknown_error"}`);
158
169
  }
159
170
  const id = typeof response.ts === "string" ? response.ts : "";
171
+ anchorIfUnset(id);
160
172
  // blocks / card + files: structured message lands first, then upload
161
173
  // files as a follow-up post in the same thread.
162
174
  if (files.length > 0 && hasStructured) {
@@ -171,7 +183,7 @@ export function buildSlackBinding(input) {
171
183
  },
172
184
  async postEphemeral(userId, rawMessage) {
173
185
  const message = normalizePostInput(rawMessage);
174
- const body = buildPostMessageBody(message, input.channelId, input.threadTs);
186
+ const body = buildPostMessageBody(message, input.channelId, currentThreadTs);
175
187
  body.user = userId;
176
188
  const response = await request("chat.postEphemeral", body);
177
189
  if (response.ok !== true) {
@@ -181,12 +193,12 @@ export function buildSlackBinding(input) {
181
193
  return { id, raw: response };
182
194
  },
183
195
  async startTyping(status) {
184
- if (!input.channelId || !input.threadTs)
196
+ if (!input.channelId || !currentThreadTs)
185
197
  return;
186
198
  try {
187
199
  const body = {
188
200
  channel_id: input.channelId,
189
- thread_ts: input.threadTs,
201
+ thread_ts: currentThreadTs,
190
202
  status: status ?? "",
191
203
  };
192
204
  if (status !== undefined && status.length > 0) {
@@ -205,12 +217,12 @@ export function buildSlackBinding(input) {
205
217
  },
206
218
  async refresh() {
207
219
  messages.length = 0;
208
- if (!input.channelId || !input.threadTs)
220
+ if (!input.channelId || !currentThreadTs)
209
221
  return;
210
222
  try {
211
223
  const response = await request("conversations.replies", {
212
224
  channel: input.channelId,
213
- ts: input.threadTs,
225
+ ts: currentThreadTs,
214
226
  limit: 50,
215
227
  });
216
228
  if (response.ok !== true || !Array.isArray(response.messages)) {
@@ -218,7 +230,7 @@ export function buildSlackBinding(input) {
218
230
  return;
219
231
  }
220
232
  for (const raw of response.messages) {
221
- messages.push(parseThreadMessage(raw, input.threadTs));
233
+ messages.push(parseThreadMessage(raw, currentThreadTs));
222
234
  }
223
235
  }
224
236
  catch (error) {
@@ -231,7 +243,9 @@ export function buildSlackBinding(input) {
231
243
  };
232
244
  const slack = {
233
245
  channelId: input.channelId,
234
- threadTs: input.threadTs,
246
+ get threadTs() {
247
+ return currentThreadTs;
248
+ },
235
249
  teamId: input.teamId,
236
250
  request,
237
251
  uploadFiles,
@@ -1,6 +1,6 @@
1
- export { slackChannel, type SlackApiResponse, type SlackBotToken, type SlackChannel, type SlackChannelConfig, type SlackChannelCredentials, type SlackChannelEvents, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackHandle, type SlackInboundResult, type SlackInboundResultOrPromise, type SlackInitialPost, type SlackInteractionAction, type SlackMentionResult, type SlackMentionResultOrPromise, type SlackReceiveArgs, type SlackThread, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
1
+ export { slackChannel, type SlackApiResponse, type SlackBotToken, type SlackChannel, type SlackChannelConfig, type SlackChannelCredentials, type SlackChannelEvents, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackHandle, type SlackInboundResult, type SlackInboundResultOrPromise, type SlackInitialMessage, type SlackInteractionAction, type SlackMentionResult, type SlackMentionResultOrPromise, type SlackReceiveArgs, type SlackThread, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
2
2
  export type { SlackAttachment, SlackAuthor, SlackInboundContext, SlackMessage, } from "#public/channels/slack/inbound.js";
3
- export type { SlackPostInput, SlackPostedMessage, SlackThreadMessage, SlackUploadFilesOptions, SlackUploadFilesResult, } from "#public/channels/slack/api.js";
3
+ export { slackContinuationToken, type SlackPostInput, type SlackPostedMessage, type SlackThreadMessage, type SlackUploadFilesOptions, type SlackUploadFilesResult, } from "#public/channels/slack/api.js";
4
4
  export { cardToBlocks, cardToFallbackText, type BlockKitBlock, } from "#public/channels/slack/blocks.js";
5
5
  /**
6
6
  * Card builders and element types re-exported from the vendored chat
@@ -1,4 +1,5 @@
1
1
  export { slackChannel, } from "#public/channels/slack/slackChannel.js";
2
+ export { slackContinuationToken, } from "#public/channels/slack/api.js";
2
3
  export { cardToBlocks, cardToFallbackText, } from "#public/channels/slack/blocks.js";
3
4
  /**
4
5
  * Card builders and element types re-exported from the vendored chat
@@ -17,7 +17,7 @@
17
17
  * work runs under `waitUntil` so the webhook ACK is immediate.
18
18
  */
19
19
  import { createLogger } from "#internal/logging.js";
20
- import { buildSlackBinding, encodeSlackContinuationToken, resolveSlackBotToken, } from "#public/channels/slack/api.js";
20
+ import { buildSlackBinding, resolveSlackBotToken, slackContinuationToken, } from "#public/channels/slack/api.js";
21
21
  import { buildAnsweredBlocks, buildFreeformModalView, deriveHitlResponse, freeformRequestIdFromActionId, HITL_FREEFORM_MODAL_ACTION_ID, HITL_FREEFORM_MODAL_BLOCK_ID, HITL_FREEFORM_MODAL_CALLBACK_ID, isFreeformAction, isHitlAction, } from "#public/channels/slack/hitl.js";
22
22
  const log = createLogger("slack.interactions");
23
23
  /**
@@ -124,7 +124,7 @@ export async function handleInteractionPost(rawBody, ctx, deps) {
124
124
  await openFreeformModal({ payload, interaction, freeformAction, deps });
125
125
  return ack;
126
126
  }
127
- const continuationToken = encodeSlackContinuationToken(interaction.channelId, interaction.threadTs);
127
+ const continuationToken = slackContinuationToken(interaction.channelId, interaction.threadTs);
128
128
  const inputResponses = interaction.actions
129
129
  .map(deriveHitlResponse)
130
130
  .filter((r) => r !== null);
@@ -184,7 +184,7 @@ async function openFreeformModal(input) {
184
184
  return;
185
185
  }
186
186
  const metadata = {
187
- continuationToken: encodeSlackContinuationToken(input.interaction.channelId, input.interaction.threadTs),
187
+ continuationToken: slackContinuationToken(input.interaction.channelId, input.interaction.threadTs),
188
188
  channelId: input.interaction.channelId,
189
189
  threadTs: input.interaction.threadTs,
190
190
  messageTs,
@@ -1,4 +1,5 @@
1
1
  import type { TypedReceiveRoute } from "#channel/receive-args.js";
2
+ import type { SessionHandle } from "#channel/session.js";
2
3
  import type { SessionAuthContext } from "#channel/types.js";
3
4
  import type { CardElement } from "#compiled/chat/index.js";
4
5
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
@@ -35,6 +36,7 @@ export interface SlackContext {
35
36
  * boundaries.
36
37
  */
37
38
  export interface SlackEventContext extends SlackContext {
39
+ readonly session: SessionHandle;
38
40
  state: SlackChannelState;
39
41
  }
40
42
  export type { SlackApiResponse, SlackBotToken, SlackHandle, SlackThread, } from "#public/channels/slack/api.js";
@@ -103,14 +105,14 @@ export interface SlackReceiveArgs {
103
105
  * the caller wants a visible "this is why we're here" anchor before
104
106
  * the model speaks. Mutually exclusive with {@link threadTs}.
105
107
  */
106
- readonly initialPost?: SlackInitialPost;
108
+ readonly initialMessage?: SlackInitialMessage;
107
109
  }
108
110
  /**
109
111
  * Pre-agent post issued by `slackChannel().receive` when the caller
110
- * provides `args.initialPost`. The shape mirrors `ctx.thread.post`'s
112
+ * provides `args.initialMessage`. The shape mirrors `ctx.thread.post`'s
111
113
  * card variant so the same `Card({...})` construction can be reused.
112
114
  */
113
- export interface SlackInitialPost {
115
+ export interface SlackInitialMessage {
114
116
  readonly card: CardElement;
115
117
  readonly fallbackText?: string;
116
118
  }