@cursor/july 0.1.25 → 0.1.26

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cursor/july",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
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": {
@@ -10,6 +10,7 @@ import {
10
10
  import { isDevMode } from "../../internal/dev-mode.js";
11
11
  import type {
12
12
  AgentCloudOptions,
13
+ ArtifactsApi,
13
14
  AuthContext,
14
15
  ChannelDefinition,
15
16
  ChannelEventHandlers,
@@ -68,7 +69,7 @@ import type {
68
69
  */
69
70
  type GitHubParsedContext = Omit<
70
71
  GitHubEventContext,
71
- "github" | "thread" | "host"
72
+ "github" | "thread" | "host" | "artifacts"
72
73
  >;
73
74
 
74
75
  const DEFAULT_PULL_REQUEST_ACTIONS: ReadonlySet<string> = new Set([
@@ -737,6 +738,7 @@ async function handlePullRequest(input: {
737
738
  bindingState: binding,
738
739
  installationId,
739
740
  host: input.args.host,
741
+ artifacts: input.args.artifacts,
740
742
  });
741
743
  const dispatch =
742
744
  input.options.onPullRequest === undefined
@@ -828,6 +830,7 @@ async function handleIssueComment(input: {
828
830
  bindingState: binding,
829
831
  installationId,
830
832
  host: input.args.host,
833
+ artifacts: input.args.artifacts,
831
834
  });
832
835
 
833
836
  const dispatch =
@@ -918,6 +921,7 @@ async function handleReviewComment(input: {
918
921
  bindingState: binding,
919
922
  installationId,
920
923
  host: input.args.host,
924
+ artifacts: input.args.artifacts,
921
925
  });
922
926
 
923
927
  const dispatch =
@@ -994,6 +998,7 @@ async function handleIssue(input: {
994
998
  bindingState: binding,
995
999
  installationId,
996
1000
  host: input.args.host,
1001
+ artifacts: input.args.artifacts,
997
1002
  });
998
1003
  const dispatch = await input.options.onIssue(ctx, parsed.issue);
999
1004
  if (dispatch === null) {
@@ -1053,6 +1058,7 @@ async function handleCheckSuite(input: {
1053
1058
  ctx: parsed.unanchoredCtx,
1054
1059
  installationId,
1055
1060
  host: input.args.host,
1061
+ artifacts: input.args.artifacts,
1056
1062
  }),
1057
1063
  parsed.checkSuite
1058
1064
  );
@@ -1079,6 +1085,7 @@ async function handleCheckSuite(input: {
1079
1085
  bindingState: binding,
1080
1086
  installationId,
1081
1087
  host: input.args.host,
1088
+ artifacts: input.args.artifacts,
1082
1089
  });
1083
1090
  const dispatch = await input.options.onCheckSuite(ctx, parsed.checkSuite);
1084
1091
  if (dispatch === null) {
@@ -1143,6 +1150,7 @@ async function handleCheckRun(input: {
1143
1150
  ctx: parsed.unanchoredCtx,
1144
1151
  installationId,
1145
1152
  host: input.args.host,
1153
+ artifacts: input.args.artifacts,
1146
1154
  }),
1147
1155
  parsed.checkRun
1148
1156
  );
@@ -1169,6 +1177,7 @@ async function handleCheckRun(input: {
1169
1177
  bindingState: binding,
1170
1178
  installationId,
1171
1179
  host: input.args.host,
1180
+ artifacts: input.args.artifacts,
1172
1181
  });
1173
1182
  const dispatch = await input.options.onCheckRun(ctx, parsed.checkRun);
1174
1183
  if (dispatch === null) {
@@ -1229,6 +1238,7 @@ async function handleWorkflowRun(input: {
1229
1238
  ctx: parsed.unanchoredCtx,
1230
1239
  installationId,
1231
1240
  host: input.args.host,
1241
+ artifacts: input.args.artifacts,
1232
1242
  }),
1233
1243
  parsed.workflowRun
1234
1244
  );
@@ -1255,6 +1265,7 @@ async function handleWorkflowRun(input: {
1255
1265
  bindingState: binding,
1256
1266
  installationId,
1257
1267
  host: input.args.host,
1268
+ artifacts: input.args.artifacts,
1258
1269
  });
1259
1270
  const dispatch = await input.options.onWorkflowRun(ctx, parsed.workflowRun);
1260
1271
  if (dispatch === null) {
@@ -1326,6 +1337,7 @@ async function handleStatus(input: {
1326
1337
  bindingState: binding,
1327
1338
  installationId,
1328
1339
  host: input.args.host,
1340
+ artifacts: input.args.artifacts,
1329
1341
  });
1330
1342
  const dispatch = await input.options.onStatus(ctx, parsed.status);
1331
1343
  if (dispatch === null) {
@@ -1399,6 +1411,7 @@ async function handleGenericEvent(input: {
1399
1411
  bindingState: binding,
1400
1412
  installationId,
1401
1413
  host: input.args.host,
1414
+ artifacts: input.args.artifacts,
1402
1415
  });
1403
1416
  const dispatch = await input.options.onEvent(ctx, parsed.event);
1404
1417
  if (dispatch === null) {
@@ -2224,11 +2237,12 @@ function escapeRegExp(value: string): string {
2224
2237
  */
2225
2238
  function enrichEventContext(input: {
2226
2239
  bindingConfig: GitHubBindingConfig;
2227
- ctx: Omit<GitHubEventContext, "github" | "thread" | "host"> &
2240
+ ctx: Omit<GitHubEventContext, "github" | "thread" | "host" | "artifacts"> &
2228
2241
  Partial<Pick<GitHubEventContext, "github" | "thread">>;
2229
2242
  bindingState: GitHubBindingState;
2230
2243
  installationId?: number;
2231
2244
  host: HostContext;
2245
+ artifacts: ArtifactsApi;
2232
2246
  }): GitHubEventContext {
2233
2247
  const { github, thread } = buildGitHubBinding({
2234
2248
  config: input.bindingConfig,
@@ -2242,6 +2256,7 @@ function enrichEventContext(input: {
2242
2256
  github,
2243
2257
  thread,
2244
2258
  host: input.host,
2259
+ artifacts: input.artifacts,
2245
2260
  };
2246
2261
  }
2247
2262
 
@@ -2254,6 +2269,7 @@ function enrichCiContext(input: {
2254
2269
  ctx: GitHubParsedContext;
2255
2270
  installationId?: number;
2256
2271
  host: HostContext;
2272
+ artifacts: ArtifactsApi;
2257
2273
  }): GitHubEventContext {
2258
2274
  const binding = githubBindingStateForEvent({
2259
2275
  kind: input.ctx.conversation.kind,
@@ -2269,6 +2285,7 @@ function enrichCiContext(input: {
2269
2285
  bindingState: binding,
2270
2286
  installationId: input.installationId,
2271
2287
  host: input.host,
2288
+ artifacts: input.artifacts,
2272
2289
  });
2273
2290
  }
2274
2291
 
@@ -2,6 +2,7 @@ import { createHmac } from "node:crypto";
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { scmMetadataToGitHubPayload } from "../../internal/cursor-event-relay.js";
4
4
  import type {
5
+ ArtifactsApi,
5
6
  ChannelHandlerArgs,
6
7
  ChannelRouteHandler,
7
8
  ChannelSession,
@@ -334,6 +335,47 @@ describe("githubChannel", () => {
334
335
  );
335
336
  });
336
337
 
338
+ it("hands the artifacts facade to inbound hooks (ctx.artifacts)", async () => {
339
+ const tags: Array<Parameters<ArtifactsApi["tag"]>[0]> = [];
340
+ const artifacts: ArtifactsApi = {
341
+ tag: async (input) => {
342
+ tags.push(input);
343
+ return {
344
+ id: `art_${tags.length}`,
345
+ kind: input.kind ?? "artifact",
346
+ key: input.key,
347
+ data: input.data,
348
+ source: "host",
349
+ createdAt: new Date().toISOString(),
350
+ updatedAt: new Date().toISOString(),
351
+ };
352
+ },
353
+ list: async () => [],
354
+ };
355
+ const send = vi.fn<SendMessageFn>(async () => fakeSession());
356
+ const response = await routeHandler(
357
+ githubChannel({
358
+ auth: [],
359
+ onPullRequest: async (ctx, pr) => {
360
+ await ctx.artifacts.tag({
361
+ key: `pr:${pr.number}`,
362
+ data: { headSha: pr.headSha },
363
+ });
364
+ return null;
365
+ },
366
+ })
367
+ )(
368
+ new Request("http://127.0.0.1:3000/v1/channels/github", {
369
+ method: "POST",
370
+ headers: { "x-github-event": "pull_request" },
371
+ body: JSON.stringify(pullRequestPayload()),
372
+ }),
373
+ { ...handlerArgs(send), artifacts }
374
+ );
375
+ await expect(response.json()).resolves.toMatchObject({ ok: true });
376
+ expect(tags).toEqual([{ key: "pr:42", data: { headSha: "abc123" } }]);
377
+ });
378
+
337
379
  it("dispatches @mention issue comments via the default onComment gate", async () => {
338
380
  const send = vi.fn<SendMessageFn>(async () => fakeSession());
339
381
  const response = await routeHandler(
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AgentCloudOptions,
3
+ ArtifactsApi,
3
4
  AuthContext,
4
5
  AuthPolicy,
5
6
  ChannelEventHandlers,
@@ -306,6 +307,11 @@ export interface GitHubEventContext {
306
307
  thread: GitHubThread;
307
308
  /** Shared host services (MCP / host GitHub / Slack). Same as tool `ctx.host`. */
308
309
  host: HostContext;
310
+ /**
311
+ * Durable artifacts (unbound — the hook runs before any session is
312
+ * admitted; pass `sessionId` in `tag` input to attribute one).
313
+ */
314
+ artifacts: ArtifactsApi;
309
315
  }
310
316
 
311
317
  /**
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { ErrorCode, type WebAPIPlatformError, WebClient } from "@slack/web-api";
6
- import type { HostContext } from "../../types.js";
6
+ import type { ArtifactsApi, HostContext } from "../../types.js";
7
7
  import {
8
8
  SLACK_DEFAULT_LOADING_MESSAGES,
9
9
  SLACK_LOADING_MESSAGE_MAX_LEN,
@@ -540,6 +540,7 @@ export function buildSlackBinding(args: {
540
540
  threadTs: string;
541
541
  teamId?: string;
542
542
  host: HostContext;
543
+ artifacts: ArtifactsApi;
543
544
  onThreadTsChanged?: (threadTs: string) => void;
544
545
  log?: SlackLog;
545
546
  }): SlackBinding {
@@ -643,5 +644,6 @@ export function buildSlackBinding(args: {
643
644
  ...(args.teamId === undefined ? {} : { teamId: args.teamId }),
644
645
  },
645
646
  host: args.host,
647
+ artifacts: args.artifacts,
646
648
  };
647
649
  }
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { ChannelSession, SendMessageFn } from "../../types.js";
2
+ import type {
3
+ ArtifactsApi,
4
+ ChannelSession,
5
+ SendMessageFn,
6
+ } from "../../types.js";
3
7
  import { blockActionAuth } from "./auth.js";
4
8
  import {
5
9
  mrkdwnSections,
@@ -57,6 +61,15 @@ function createSendRecorder(): {
57
61
  return { send, calls };
58
62
  }
59
63
 
64
+ function testArtifacts(): ArtifactsApi {
65
+ return {
66
+ tag: async () => {
67
+ throw new Error("artifacts not configured in test");
68
+ },
69
+ list: async () => [],
70
+ };
71
+ }
72
+
60
73
  function testHost() {
61
74
  return {
62
75
  mcp: {
@@ -171,6 +184,7 @@ describe("dispatchBlockAction", () => {
171
184
  },
172
185
  send,
173
186
  credentials: undefined,
187
+ artifacts: testArtifacts(),
174
188
  host: testHost(),
175
189
  log: createSlackLogger(() => undefined),
176
190
  });
@@ -204,6 +218,7 @@ describe("dispatchBlockAction", () => {
204
218
  handler: () => ({ auth: testAuth }),
205
219
  send,
206
220
  credentials: undefined,
221
+ artifacts: testArtifacts(),
207
222
  host: testHost(),
208
223
  log: createSlackLogger(() => undefined),
209
224
  });
@@ -217,6 +232,7 @@ describe("dispatchBlockAction", () => {
217
232
  handler: () => null,
218
233
  send,
219
234
  credentials: undefined,
235
+ artifacts: testArtifacts(),
220
236
  host: testHost(),
221
237
  log: createSlackLogger(() => undefined),
222
238
  });
@@ -232,6 +248,7 @@ describe("dispatchBlockAction", () => {
232
248
  handler: () => ({ auth: testAuth }),
233
249
  send,
234
250
  credentials: undefined,
251
+ artifacts: testArtifacts(),
235
252
  host: testHost(),
236
253
  log: createSlackLogger(() => undefined),
237
254
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import type { HostContext, SendMessageFn } from "../../types.js";
2
+ import type { ArtifactsApi, HostContext, SendMessageFn } from "../../types.js";
3
3
  import {
4
4
  type CursorSlackEnvelope,
5
5
  createPendingSlackApiTransport,
@@ -33,6 +33,15 @@ function fakeHost(): HostContext {
33
33
  return {} as HostContext;
34
34
  }
35
35
 
36
+ function testArtifacts(): ArtifactsApi {
37
+ return {
38
+ tag: async () => {
39
+ throw new Error("artifacts not configured in test");
40
+ },
41
+ list: async () => [],
42
+ };
43
+ }
44
+
36
45
  describe("slackChannel({ cursorAccount: true })", () => {
37
46
  it("registers a runtime with a pending transport and no Socket Mode routes", () => {
38
47
  const definition = slackChannel({ cursorAccount: true });
@@ -167,6 +176,7 @@ describe("dispatchCursorSlackEnvelope", () => {
167
176
  envelope: envelope(),
168
177
  send,
169
178
  host: fakeHost(),
179
+ artifacts: testArtifacts(),
170
180
  });
171
181
 
172
182
  expect(sent).toHaveLength(1);
@@ -184,6 +194,39 @@ describe("dispatchCursorSlackEnvelope", () => {
184
194
  expect(methods).toContain("assistant.threads.setStatus");
185
195
  });
186
196
 
197
+ it("hands the artifacts facade to the mention handler binding", async () => {
198
+ const tags: Array<Parameters<ArtifactsApi["tag"]>[0]> = [];
199
+ const artifacts: ArtifactsApi = {
200
+ tag: async (input) => {
201
+ tags.push(input);
202
+ return {
203
+ id: `art_${tags.length}`,
204
+ kind: input.kind ?? "artifact",
205
+ key: input.key,
206
+ data: input.data,
207
+ source: "host",
208
+ createdAt: new Date().toISOString(),
209
+ updatedAt: new Date().toISOString(),
210
+ };
211
+ },
212
+ list: async () => [],
213
+ };
214
+ const { runtime, send } = setup({
215
+ onAppMention: async (ctx) => {
216
+ await ctx.artifacts.tag({ key: "from-relay", data: "r" });
217
+ return null;
218
+ },
219
+ });
220
+ await dispatchCursorSlackEnvelope({
221
+ runtime,
222
+ envelope: envelope(),
223
+ send,
224
+ host: fakeHost(),
225
+ artifacts,
226
+ });
227
+ expect(tags).toEqual([{ key: "from-relay", data: "r" }]);
228
+ });
229
+
187
230
  it("routes DMs to the direct-message handler", async () => {
188
231
  const kinds: string[] = [];
189
232
  const { runtime, send } = setup({
@@ -205,12 +248,14 @@ describe("dispatchCursorSlackEnvelope", () => {
205
248
  }),
206
249
  send,
207
250
  host: fakeHost(),
251
+ artifacts: testArtifacts(),
208
252
  });
209
253
  await dispatchCursorSlackEnvelope({
210
254
  runtime,
211
255
  envelope: envelope({ eventId: "Ev3" }),
212
256
  send,
213
257
  host: fakeHost(),
258
+ artifacts: testArtifacts(),
214
259
  });
215
260
  expect(kinds).toEqual(["dm", "mention"]);
216
261
  });
@@ -222,12 +267,14 @@ describe("dispatchCursorSlackEnvelope", () => {
222
267
  envelope: envelope({ eventId: "EvDup" }),
223
268
  send,
224
269
  host: fakeHost(),
270
+ artifacts: testArtifacts(),
225
271
  });
226
272
  await dispatchCursorSlackEnvelope({
227
273
  runtime,
228
274
  envelope: envelope({ eventId: "EvDup" }),
229
275
  send,
230
276
  host: fakeHost(),
277
+ artifacts: testArtifacts(),
231
278
  });
232
279
  expect(sent).toHaveLength(1);
233
280
  });
@@ -245,6 +292,7 @@ describe("dispatchCursorSlackEnvelope", () => {
245
292
  }),
246
293
  send,
247
294
  host: fakeHost(),
295
+ artifacts: testArtifacts(),
248
296
  });
249
297
  expect(sent).toHaveLength(1);
250
298
  expect(sent[0].options.continuationToken).toBe("C0100:111.222");
@@ -12,7 +12,7 @@
12
12
  * dispatches relayed envelopes through the pack's normal inbound path.
13
13
  */
14
14
 
15
- import type { HostContext, SendMessageFn } from "../../types.js";
15
+ import type { ArtifactsApi, HostContext, SendMessageFn } from "../../types.js";
16
16
  import { dispatchInboundMessage } from "./dispatch.js";
17
17
  import type { EventIdDedupe } from "./event-id-dedupe.js";
18
18
  import { slackTextToMarkdown } from "./inbound.js";
@@ -114,6 +114,7 @@ export async function dispatchCursorSlackEnvelope(args: {
114
114
  envelope: CursorSlackEnvelope;
115
115
  send: SendMessageFn;
116
116
  host: HostContext;
117
+ artifacts: ArtifactsApi;
117
118
  hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
118
119
  isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
119
120
  getContinuationLastBotMessageTs?: (
@@ -167,6 +168,7 @@ export async function dispatchCursorSlackEnvelope(args: {
167
168
  send: args.send,
168
169
  credentials: runtime.credentials,
169
170
  host: args.host,
171
+ artifacts: args.artifacts,
170
172
  log,
171
173
  evalCommandsEnabled: runtime.options.evalCommands !== false,
172
174
  cursorAgentName: envelope.agentName,
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import type {
6
+ ArtifactsApi,
6
7
  ChannelSession,
7
8
  HostContext,
8
9
  SendMessageFn,
@@ -67,6 +68,15 @@ function acceptHandler(): SlackMessageHandler {
67
68
  return async () => ({ auth: testAuth });
68
69
  }
69
70
 
71
+ function testArtifacts(): ArtifactsApi {
72
+ return {
73
+ tag: async () => {
74
+ throw new Error("artifacts not configured in test");
75
+ },
76
+ list: async () => [],
77
+ };
78
+ }
79
+
70
80
  function testHost(options?: {
71
81
  nudgeRoot?: string;
72
82
  askRoot?: string;
@@ -170,6 +180,7 @@ describe("dispatchInboundFromEventBody", () => {
170
180
  onAppMention: acceptHandler(),
171
181
  onDirectMessage: async () => null,
172
182
  credentials: undefined,
183
+ artifacts: testArtifacts(),
173
184
  eventIds: createEventIdDedupe(),
174
185
  host: testHost(),
175
186
  log: createSlackLogger((line) => lines.push(line)),
@@ -196,6 +207,7 @@ describe("dispatchInboundFromEventBody", () => {
196
207
  onAppMention: acceptHandler(),
197
208
  onDirectMessage: async () => null,
198
209
  credentials: undefined as undefined,
210
+ artifacts: testArtifacts(),
199
211
  host: testHost(),
200
212
  eventIds,
201
213
  log: createSlackLogger(() => undefined),
@@ -219,6 +231,7 @@ describe("dispatchInboundFromEventBody", () => {
219
231
  onAppMention: async () => null,
220
232
  onDirectMessage: async () => null,
221
233
  credentials: undefined,
234
+ artifacts: testArtifacts(),
222
235
  eventIds: createEventIdDedupe(),
223
236
  host: testHost(),
224
237
  });
@@ -239,6 +252,7 @@ describe("dispatchInboundFromEventBody", () => {
239
252
  },
240
253
  onDirectMessage: async () => null,
241
254
  credentials: undefined,
255
+ artifacts: testArtifacts(),
242
256
  eventIds: createEventIdDedupe(),
243
257
  host: testHost(),
244
258
  log: createSlackLogger(() => undefined),
@@ -273,6 +287,7 @@ describe("dispatchInboundFromEventBody", () => {
273
287
  onAppMention: async () => null,
274
288
  onDirectMessage: acceptHandler(),
275
289
  credentials: undefined,
290
+ artifacts: testArtifacts(),
276
291
  eventIds: createEventIdDedupe(),
277
292
  host: testHost(),
278
293
  }
@@ -292,6 +307,7 @@ describe("dispatchInboundFromEventBody", () => {
292
307
  onAppMention: async () => null,
293
308
  onDirectMessage: async () => null,
294
309
  credentials: undefined,
310
+ artifacts: testArtifacts(),
295
311
  eventIds: createEventIdDedupe(),
296
312
  host: testHost(),
297
313
  }
@@ -324,6 +340,7 @@ describe("dispatchInboundFromEventBody", () => {
324
340
  }),
325
341
  onDirectMessage: async () => null,
326
342
  credentials: undefined,
343
+ artifacts: testArtifacts(),
327
344
  eventIds: createEventIdDedupe(),
328
345
  host: testHost(),
329
346
  });
@@ -357,6 +374,7 @@ describe("engagement routing", () => {
357
374
  onAppMention: acceptHandler(),
358
375
  onDirectMessage: async () => null,
359
376
  credentials: undefined,
377
+ artifacts: testArtifacts(),
360
378
  eventIds: createEventIdDedupe(),
361
379
  host: testHost(),
362
380
  log: createSlackLogger(() => undefined),
@@ -379,6 +397,7 @@ describe("engagement routing", () => {
379
397
  onAppMention: acceptHandler(),
380
398
  onDirectMessage: async () => null,
381
399
  credentials: undefined as undefined,
400
+ artifacts: testArtifacts(),
382
401
  eventIds: createEventIdDedupe(),
383
402
  host: testHost(),
384
403
  log: createSlackLogger(() => undefined),
@@ -433,6 +452,7 @@ describe("engagement routing", () => {
433
452
  onAppMention: acceptHandler(),
434
453
  onDirectMessage: async () => null,
435
454
  credentials: undefined,
455
+ artifacts: testArtifacts(),
436
456
  eventIds: createEventIdDedupe(),
437
457
  host: testHost(),
438
458
  log: createSlackLogger(() => undefined),
@@ -443,6 +463,56 @@ describe("engagement routing", () => {
443
463
  });
444
464
 
445
465
  describe("dispatchInboundMessage", () => {
466
+ it("hands the artifacts facade to the handler binding (ctx.artifacts)", async () => {
467
+ const tags: Array<Parameters<ArtifactsApi["tag"]>[0]> = [];
468
+ const artifacts: ArtifactsApi = {
469
+ tag: async (input) => {
470
+ tags.push(input);
471
+ return {
472
+ id: `art_${tags.length}`,
473
+ kind: input.kind ?? "artifact",
474
+ key: input.key,
475
+ data: input.data,
476
+ source: "host",
477
+ createdAt: new Date().toISOString(),
478
+ updatedAt: new Date().toISOString(),
479
+ };
480
+ },
481
+ list: async () => [],
482
+ };
483
+ const { send, calls } = createSendRecorder();
484
+ await dispatchInboundMessage({
485
+ kind: "channel_post",
486
+ message: {
487
+ text: "alert fired",
488
+ body: "alert fired",
489
+ markdown: "alert fired",
490
+ ts: "2.0",
491
+ threadTs: "1.0",
492
+ channelId: "C1",
493
+ teamId: "T1",
494
+ author: { userId: "U1", isBot: false },
495
+ },
496
+ handler: async (ctx) => {
497
+ await ctx.artifacts.tag({
498
+ kind: "alert_watch",
499
+ key: `${ctx.slack.channelId}:${ctx.slack.threadTs}`,
500
+ data: { status: "watching" },
501
+ });
502
+ return { auth: testAuth };
503
+ },
504
+ send,
505
+ credentials: undefined,
506
+ artifacts,
507
+ host: testHost(),
508
+ log: createSlackLogger(() => undefined),
509
+ });
510
+ expect(tags).toEqual([
511
+ { kind: "alert_watch", key: "C1:1.0", data: { status: "watching" } },
512
+ ]);
513
+ expect(calls).toHaveLength(1);
514
+ });
515
+
446
516
  it("logs delivery failure when send throws", async () => {
447
517
  const lines: string[] = [];
448
518
  await dispatchInboundMessage({
@@ -462,6 +532,7 @@ describe("dispatchInboundMessage", () => {
462
532
  throw new Error("engine down");
463
533
  },
464
534
  credentials: undefined,
535
+ artifacts: testArtifacts(),
465
536
  host: testHost(),
466
537
  log: createSlackLogger((line) => lines.push(line)),
467
538
  });
@@ -500,6 +571,7 @@ describe("dispatchInboundMessage", () => {
500
571
  handler: acceptHandler(),
501
572
  send,
502
573
  credentials: { botToken: "xoxb-test" },
574
+ artifacts: testArtifacts(),
503
575
  host,
504
576
  hasContinuationSession: async () => true,
505
577
  log: createSlackLogger((line) => lines.push(line)),
@@ -538,6 +610,7 @@ describe("dispatchInboundMessage", () => {
538
610
  handler: acceptHandler(),
539
611
  send,
540
612
  credentials: { botToken: "xoxb-test" },
613
+ artifacts: testArtifacts(),
541
614
  host: testHost({
542
615
  nudgeRoot: join(root, "nudges"),
543
616
  askRoot: join(root, "asks"),
@@ -572,6 +645,7 @@ describe("dispatchInboundMessage", () => {
572
645
  handler: acceptHandler(),
573
646
  send,
574
647
  credentials: { botToken: "xoxb-test" },
648
+ artifacts: testArtifacts(),
575
649
  host: testHost({
576
650
  evals: {
577
651
  list: async () => {
@@ -613,6 +687,7 @@ describe("dispatchInboundMessage", () => {
613
687
  handler: acceptHandler(),
614
688
  send,
615
689
  credentials: undefined,
690
+ artifacts: testArtifacts(),
616
691
  host: testHost({
617
692
  evals: {
618
693
  list: async () => {
@@ -657,6 +732,7 @@ describe("dispatchInboundMessage", () => {
657
732
  handler: acceptHandler(),
658
733
  send,
659
734
  credentials: undefined,
735
+ artifacts: testArtifacts(),
660
736
  host,
661
737
  });
662
738
  await dispatchInboundMessage({
@@ -665,6 +741,7 @@ describe("dispatchInboundMessage", () => {
665
741
  handler: acceptHandler(),
666
742
  send,
667
743
  credentials: undefined,
744
+ artifacts: testArtifacts(),
668
745
  host,
669
746
  });
670
747
  expect(calls).toHaveLength(1);
@@ -705,6 +782,7 @@ describe("dispatchInboundMessage", () => {
705
782
  handler: acceptHandler(),
706
783
  send,
707
784
  credentials: { botToken: "xoxb-test" },
785
+ artifacts: testArtifacts(),
708
786
  host: testHost(),
709
787
  log: createSlackLogger((line) => lines.push(line)),
710
788
  });
@@ -749,6 +827,7 @@ describe("dispatchInboundMessage", () => {
749
827
  handler: acceptHandler(),
750
828
  send,
751
829
  credentials: { botToken: "xoxb-test" },
830
+ artifacts: testArtifacts(),
752
831
  host: testHost(),
753
832
  hasContinuationSession: async () => true,
754
833
  // Agent watermark (not Datadog's parent) — human reply at 2.0 is in delta.
@@ -787,6 +866,7 @@ describe("dispatchInboundMessage", () => {
787
866
  handler: acceptHandler(),
788
867
  send,
789
868
  credentials: undefined,
869
+ artifacts: testArtifacts(),
790
870
  host,
791
871
  log: createSlackLogger(() => undefined),
792
872
  });
@@ -2,7 +2,12 @@
2
2
  * Shared inbound dispatch for Slack Events API bodies and Socket Mode.
3
3
  */
4
4
 
5
- import type { HostContext, JsonValue, SendMessageFn } from "../../types.js";
5
+ import type {
6
+ ArtifactsApi,
7
+ HostContext,
8
+ JsonValue,
9
+ SendMessageFn,
10
+ } from "../../types.js";
6
11
  import { buildSlackBinding, slackContinuationKey } from "./api.js";
7
12
  import { firstLineTitle, maybeSetThreadTitle } from "./assistant.js";
8
13
  import type { SlackBotMentionGate } from "./bot-mentions.js";
@@ -65,6 +70,8 @@ export interface SlackInboundDispatchDeps extends SlackContinuationProbes {
65
70
  credentials: SlackCredentials | undefined;
66
71
  eventIds: EventIdDedupe;
67
72
  host: HostContext;
73
+ /** Unbound artifacts facade handed to handler bindings (`ctx.artifacts`). */
74
+ artifacts: ArtifactsApi;
68
75
  log?: SlackLog;
69
76
  /** Dispatch on `app_mention`. Defaults to `true`. */
70
77
  mentionsEnabled?: boolean;
@@ -110,6 +117,7 @@ export async function dispatchInboundMessage(args: {
110
117
  send: SendMessageFn;
111
118
  credentials: SlackCredentials | undefined;
112
119
  host: HostContext;
120
+ artifacts: ArtifactsApi;
113
121
  hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
114
122
  isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
115
123
  getContinuationLastBotMessageTs?: (
@@ -132,6 +140,7 @@ export async function dispatchInboundMessage(args: {
132
140
  channelId: message.channelId,
133
141
  threadTs: message.threadTs,
134
142
  host: args.host,
143
+ artifacts: args.artifacts,
135
144
  log,
136
145
  ...(message.teamId === undefined ? {} : { teamId: message.teamId }),
137
146
  });
@@ -450,6 +459,7 @@ export function dispatchInboundFromEventBody(
450
459
  send: deps.send,
451
460
  credentials: deps.credentials,
452
461
  host: deps.host,
462
+ artifacts: deps.artifacts,
453
463
  log,
454
464
  evalCommandsEnabled: deps.evalCommandsEnabled,
455
465
  cursorAgentName: deps.cursorAgentName,
@@ -490,6 +500,7 @@ export function dispatchInboundFromEventBody(
490
500
  send: deps.send,
491
501
  credentials: deps.credentials,
492
502
  host: deps.host,
503
+ artifacts: deps.artifacts,
493
504
  log,
494
505
  evalCommandsEnabled: deps.evalCommandsEnabled,
495
506
  cursorAgentName: deps.cursorAgentName,
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type {
11
11
  ApprovalDecision,
12
+ ArtifactsApi,
12
13
  AuthContext,
13
14
  ChannelSession,
14
15
  HostContext,
@@ -275,6 +276,7 @@ export interface SlackBlockActionDispatchDeps extends SlackContinuationProbes {
275
276
  send: SendMessageFn;
276
277
  credentials: SlackCredentials | undefined;
277
278
  host: HostContext;
279
+ artifacts: ArtifactsApi;
278
280
  log?: SlackLog;
279
281
  }
280
282
 
@@ -347,6 +349,7 @@ export async function dispatchBlockAction(
347
349
  send: deps.send,
348
350
  credentials: deps.credentials,
349
351
  host: deps.host,
352
+ artifacts: deps.artifacts,
350
353
  log,
351
354
  // Click text is synthetic — never treat it as a stop/eval directive.
352
355
  evalCommandsEnabled: false,
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { allowAll, defineChannel } from "../../channels.js";
11
11
  import type {
12
+ ArtifactsApi,
12
13
  ChannelDefinition,
13
14
  ChannelEventHandlers,
14
15
  HostContext,
@@ -215,6 +216,7 @@ export function slackChannel<const Name extends string = string>(
215
216
  send: SendMessageFn;
216
217
  waitUntil: (promise: Promise<unknown>) => void;
217
218
  host: HostContext;
219
+ artifacts: ArtifactsApi;
218
220
  hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
219
221
  isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
220
222
  getContinuationLastBotMessageTs?: (
@@ -230,6 +232,7 @@ export function slackChannel<const Name extends string = string>(
230
232
  credentials,
231
233
  eventIds,
232
234
  host: args.host,
235
+ artifacts: args.artifacts,
233
236
  log: getLog(),
234
237
  mentionsEnabled,
235
238
  directMessagesEnabled,
@@ -410,6 +413,7 @@ export function slackChannel<const Name extends string = string>(
410
413
  send: args.send,
411
414
  credentials,
412
415
  host: args.host,
416
+ artifacts: args.artifacts,
413
417
  log: getLog(),
414
418
  evalCommandsEnabled,
415
419
  ...(args.hasContinuationSession === undefined
@@ -454,6 +458,7 @@ export function slackChannel<const Name extends string = string>(
454
458
  send: args.send,
455
459
  waitUntil: args.waitUntil,
456
460
  host: args.host,
461
+ artifacts: args.artifacts,
457
462
  ...(args.hasContinuationSession === undefined
458
463
  ? {}
459
464
  : { hasContinuationSession: args.hasContinuationSession }),
@@ -496,6 +501,7 @@ export function slackChannel<const Name extends string = string>(
496
501
  send: args.send,
497
502
  credentials,
498
503
  host: args.host,
504
+ artifacts: args.artifacts,
499
505
  log: getLog(),
500
506
  hasContinuationSession: args.hasContinuationSession,
501
507
  isContinuationBusy: args.isContinuationBusy,
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type {
6
6
  AgentCloudOptions,
7
+ ArtifactsApi,
7
8
  AuthContext,
8
9
  ChannelEventHandlers,
9
10
  HostContext,
@@ -189,6 +190,11 @@ export interface SlackBinding {
189
190
  };
190
191
  /** Shared host services (MCP / GitHub / Slack). */
191
192
  host: HostContext;
193
+ /**
194
+ * Durable artifacts (unbound — the handler runs before any session is
195
+ * admitted; pass `sessionId` in `tag` input to attribute one).
196
+ */
197
+ artifacts: ArtifactsApi;
192
198
  }
193
199
 
194
200
  export type SlackMessageHandler = (
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { defineStorage } from "../storage.js";
6
- import type { JsonValue } from "../types.js";
6
+ import type { ArtifactRecord, ArtifactsApi, JsonValue } from "../types.js";
7
7
  import { ReminderRunner, ReminderValidationError } from "./reminder-runner.js";
8
8
  import type { SessionEngine } from "./session-engine.js";
9
9
  import { StorageCoordinator } from "./storage-coordinator.js";
@@ -25,9 +25,28 @@ function mockEngine(
25
25
  getEventStream: () => ({}) as never,
26
26
  waitForCompletion: async () => ({ status: "completed" as const }),
27
27
  }));
28
+ const artifactRows: ArtifactRecord[] = [];
29
+ const artifacts: ArtifactsApi = {
30
+ tag: async (input) => {
31
+ const record: ArtifactRecord = {
32
+ id: `art_${artifactRows.length + 1}`,
33
+ kind: input.kind ?? "artifact",
34
+ key: input.key,
35
+ data: input.data,
36
+ sessionId: input.sessionId,
37
+ source: "host",
38
+ createdAt: new Date().toISOString(),
39
+ updatedAt: new Date().toISOString(),
40
+ };
41
+ artifactRows.push(record);
42
+ return record;
43
+ },
44
+ list: async () => [...artifactRows],
45
+ };
28
46
  return {
29
47
  stateRoot,
30
48
  storage,
49
+ artifacts,
31
50
  host: {
32
51
  mcp: {} as never,
33
52
  github: {} as never,
@@ -145,6 +164,29 @@ describe("ReminderRunner", () => {
145
164
  expect(message).toContain("check ci");
146
165
  });
147
166
 
167
+ it("run handlers get the unbound artifacts facade", async () => {
168
+ dir = await mkdtemp(join(tmpdir(), "rem-runner-"));
169
+ const engine = mockEngine(dir);
170
+ const runner = new ReminderRunner(engine);
171
+ await runner.start();
172
+
173
+ const created = await runner.create({
174
+ purpose: "alert_watch",
175
+ channelId: "drive",
176
+ continuationToken: "tok",
177
+ every: "1m",
178
+ run: async ({ artifacts }) => {
179
+ await artifacts.tag({ key: "from-reminder", data: "r" });
180
+ return { action: "skip" };
181
+ },
182
+ });
183
+ await runner.dispatch(created.id);
184
+
185
+ const rows = await engine.artifacts.list();
186
+ expect(rows.map((row) => row.key)).toEqual(["from-reminder"]);
187
+ expect(rows[0]?.sessionId).toBeUndefined();
188
+ });
189
+
148
190
  it("disarms prompt reminders when the bound session is gone", async () => {
149
191
  dir = await mkdtemp(join(tmpdir(), "rem-runner-"));
150
192
  const engine = mockEngine(dir);
@@ -664,6 +664,7 @@ export class ReminderRunner {
664
664
  host: this.engine.host,
665
665
  mcp: this.engine.host.mcp,
666
666
  appAuth: APP_AUTH,
667
+ artifacts: this.engine.artifacts,
667
668
  async followup(args: { message: string }) {
668
669
  const auth = await self.resolveFollowupAuth(
669
670
  record.channelId,
@@ -151,6 +151,7 @@ export class ScheduleRunner {
151
151
  },
152
152
  appAuth: APP_AUTH,
153
153
  host: this.engine.host,
154
+ artifacts: this.engine.artifacts,
154
155
  });
155
156
  // Collect ids from receive() calls the handler chose not to await
156
157
  // (commonly wrapped in waitUntil); receive resolves at message accept,
@@ -4,12 +4,13 @@ import { join } from "node:path";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
5
  import { z } from "zod";
6
6
  import { defineArtifacts } from "../artifacts.js";
7
- import { httpChannel } from "../channels.js";
7
+ import { defineChannel, httpChannel } from "../channels.js";
8
8
  import { defineTool } from "../tools.js";
9
9
  import type {
10
10
  AgentProject,
11
11
  ArtifactRecord,
12
12
  AuthContext,
13
+ DiscoveredChannel,
13
14
  DiscoveredTool,
14
15
  ServeOptions,
15
16
  } from "../types.js";
@@ -35,7 +36,10 @@ const seedTool = defineTool({
35
36
  },
36
37
  });
37
38
 
38
- function testProject(rootDir: string): AgentProject {
39
+ function testProject(
40
+ rootDir: string,
41
+ options?: { channels?: DiscoveredChannel[] }
42
+ ): AgentProject {
39
43
  const discovered: DiscoveredTool = {
40
44
  name: "seed",
41
45
  description: seedTool.description,
@@ -59,7 +63,7 @@ function testProject(rootDir: string): AgentProject {
59
63
  seedFiles: [],
60
64
  },
61
65
  httpChannel: httpChannel(),
62
- channels: [],
66
+ channels: options?.channels ?? [],
63
67
  schedules: [],
64
68
  hooks: [],
65
69
  abs: [],
@@ -87,10 +91,15 @@ afterEach(async () => {
87
91
  }
88
92
  });
89
93
 
90
- async function startTestServer(options?: Partial<ServeOptions>) {
94
+ async function startTestServer(
95
+ options?: Partial<ServeOptions>,
96
+ projectOptions?: { channels?: DiscoveredChannel[] }
97
+ ) {
91
98
  const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-artroute-"));
92
99
  cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
93
- const mounts: AgentMount[] = [{ slug: "", project: testProject(stateRoot) }];
100
+ const mounts: AgentMount[] = [
101
+ { slug: "", project: testProject(stateRoot, projectOptions) },
102
+ ];
94
103
  const handle = await startServer(mounts, {
95
104
  port: 0,
96
105
  stateRoot,
@@ -181,6 +190,28 @@ describe("GET /v1/artifacts", () => {
181
190
  expect(listed.artifacts).toEqual([]);
182
191
  });
183
192
 
193
+ it("channel onStart gets the unbound artifacts facade", async () => {
194
+ const channel: DiscoveredChannel = {
195
+ id: "starter",
196
+ definition: defineChannel({
197
+ async onStart({ artifacts }) {
198
+ await artifacts.tag({
199
+ kind: "report",
200
+ key: "from-onstart",
201
+ data: 1,
202
+ });
203
+ },
204
+ }),
205
+ };
206
+ const handle = await startTestServer(
207
+ { dev: true },
208
+ { channels: [channel] }
209
+ );
210
+ const listed = await listArtifacts(handle.url);
211
+ expect(listed.artifacts.map((a) => a.key)).toEqual(["from-onstart"]);
212
+ expect(listed.artifacts[0]?.sessionId).toBeUndefined();
213
+ });
214
+
184
215
  it("requires the bearer token when one is configured", async () => {
185
216
  const handle = await startTestServer({ authToken: "sekrit" });
186
217
  const denied = await fetch(`${handle.url}/v1/artifacts`);
@@ -494,6 +494,7 @@ export async function startServer(
494
494
  interruptContinuation: (continuationToken) =>
495
495
  engine.interruptContinuation(channel.id, continuationToken),
496
496
  logger,
497
+ artifacts: engine.artifacts,
497
498
  };
498
499
  try {
499
500
  await definition.onStart(startArgs);
@@ -1778,6 +1779,7 @@ async function startCursorSlackRelay(input: {
1778
1779
  envelope,
1779
1780
  send: args.send,
1780
1781
  host: args.host,
1782
+ artifacts: args.artifacts,
1781
1783
  hasContinuationSession: (continuationToken) =>
1782
1784
  target.runtime.engine.hasContinuationSession(
1783
1785
  target.channelId,
@@ -3,17 +3,21 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { defineArtifacts } from "../artifacts.js";
6
- import { httpChannel } from "../channels.js";
6
+ import { defineChannel, httpChannel } from "../channels.js";
7
7
  import { defineHook } from "../hooks.js";
8
+ import { defineSchedule } from "../schedules.js";
8
9
  import { defineTool } from "../tools.js";
9
10
  import type {
10
11
  AgentProject,
11
12
  ArtifactsDefinition,
13
+ DiscoveredChannel,
12
14
  DiscoveredHook,
15
+ DiscoveredSchedule,
13
16
  DiscoveredTool,
14
17
  JsonObject,
15
18
  ToolContext,
16
19
  } from "../types.js";
20
+ import { ScheduleRunner } from "./schedule-runner.js";
17
21
  import type { AgentRunner } from "./sdk-runner.js";
18
22
  import { SessionEngine } from "./session-engine.js";
19
23
 
@@ -46,6 +50,8 @@ function testProject(options?: {
46
50
  artifacts?: ArtifactsDefinition;
47
51
  tools?: DiscoveredTool[];
48
52
  hooks?: DiscoveredHook[];
53
+ schedules?: DiscoveredSchedule[];
54
+ channels?: DiscoveredChannel[];
49
55
  }): AgentProject {
50
56
  return {
51
57
  rootDir: "/tmp/artifacts-fixture",
@@ -62,8 +68,8 @@ function testProject(options?: {
62
68
  seedFiles: [],
63
69
  },
64
70
  httpChannel: httpChannel(),
65
- channels: [],
66
- schedules: [],
71
+ channels: options?.channels ?? [],
72
+ schedules: options?.schedules ?? [],
67
73
  hooks: options?.hooks ?? [],
68
74
  abs: [],
69
75
  artifacts: options?.artifacts,
@@ -198,6 +204,50 @@ describe("session engine artifacts wiring", () => {
198
204
  expect(await engine.artifacts.list()).toHaveLength(0);
199
205
  });
200
206
 
207
+ it("schedule handlers get the unbound artifacts facade", async () => {
208
+ const cron = "* * * * *";
209
+ const schedule: DiscoveredSchedule = {
210
+ name: "tagger",
211
+ cron,
212
+ kind: "run",
213
+ definition: defineSchedule({
214
+ cron,
215
+ async run({ artifacts }) {
216
+ await artifacts.tag({ key: "from-schedule", data: "s" });
217
+ },
218
+ }),
219
+ };
220
+ const project = testProject({ schedules: [schedule] });
221
+ const engine = await makeEngine(project, echoRunner());
222
+ const runner = new ScheduleRunner(engine, project);
223
+ await runner.dispatch("tagger");
224
+
225
+ const rows = await engine.artifacts.list();
226
+ expect(rows.map((row) => row.key)).toEqual(["from-schedule"]);
227
+ expect(rows[0]?.sessionId).toBeUndefined();
228
+ });
229
+
230
+ it("channel receive hooks get the unbound artifacts facade", async () => {
231
+ const channel: DiscoveredChannel = {
232
+ id: "handoff",
233
+ definition: defineChannel({
234
+ async receive(input, { send, artifacts }) {
235
+ await artifacts.tag({ key: "from-receive", data: "r" });
236
+ return send(input.message, { auth: input.auth });
237
+ },
238
+ }),
239
+ };
240
+ const engine = await makeEngine(
241
+ testProject({ channels: [channel] }),
242
+ echoRunner()
243
+ );
244
+ await engine.receive("handoff", { message: "hand-off", auth: null });
245
+
246
+ const rows = await engine.artifacts.list();
247
+ expect(rows.map((row) => row.key)).toEqual(["from-receive"]);
248
+ expect(rows[0]?.sessionId).toBeUndefined();
249
+ });
250
+
201
251
  it("hook ctx.artifacts is bound to the hook's session", async () => {
202
252
  const hook: DiscoveredHook = {
203
253
  name: "tagger",
@@ -716,7 +716,10 @@ export class SessionEngine {
716
716
  lastCreated = session;
717
717
  return session;
718
718
  };
719
- const returned = await definition.receive(input, { send });
719
+ const returned = await definition.receive(input, {
720
+ send,
721
+ artifacts: this.artifactsApi(),
722
+ });
720
723
  const session = returned ?? lastCreated;
721
724
  if (session === undefined) {
722
725
  throw new Error(
package/src/types.ts CHANGED
@@ -1505,6 +1505,12 @@ export interface ChannelStartArgs {
1505
1505
  * should prefer this over `console.*` so CLI/eval hosts can redirect.
1506
1506
  */
1507
1507
  logger: (line: string) => void;
1508
+ /**
1509
+ * Durable artifacts (unbound, same facade as
1510
+ * {@link ChannelHandlerArgs.artifacts}) — for host pipelines the channel
1511
+ * drives outside any route handler.
1512
+ */
1513
+ artifacts: ArtifactsApi;
1508
1514
  }
1509
1515
 
1510
1516
  /**
@@ -1551,7 +1557,7 @@ export interface ChannelConfig<TState = JsonValue> {
1551
1557
  */
1552
1558
  receive?: (
1553
1559
  input: ReceiveInput,
1554
- args: { send: SendMessageFn }
1560
+ args: { send: SendMessageFn; artifacts: ArtifactsApi }
1555
1561
  // biome-ignore lint/suspicious/noConfusingVoidType: hooks may return a session, nothing, or an async nothing
1556
1562
  ) => Promise<ChannelSession | undefined | void> | ChannelSession | void;
1557
1563
  /** Stream-event handlers for sessions owned by this channel. */
@@ -1742,6 +1748,11 @@ export interface ScheduleHandlerArgs {
1742
1748
  appAuth: AuthContext;
1743
1749
  /** Shared host services (MCP / GitHub / Slack). Same as tool `ctx.host`. */
1744
1750
  host: HostContext;
1751
+ /**
1752
+ * Durable artifacts (unbound — schedule handlers have no ambient session;
1753
+ * pass `sessionId` explicitly to attribute).
1754
+ */
1755
+ artifacts: ArtifactsApi;
1745
1756
  }
1746
1757
 
1747
1758
  export interface ScheduleConfig {
@@ -1795,6 +1806,12 @@ export interface ReminderFireContext extends ReminderUntilContext {
1795
1806
  /** Wake the reminder's already-bound session (target is implicit). */
1796
1807
  followup(args: { message: string }): Promise<ChannelSession>;
1797
1808
  cancel(reason?: string): Promise<void>;
1809
+ /**
1810
+ * Durable artifacts (unbound — the reminder is bound to a continuation
1811
+ * token, not a resolved session; pass `sessionId` in `tag` input to
1812
+ * attribute one).
1813
+ */
1814
+ artifacts: ArtifactsApi;
1798
1815
  }
1799
1816
 
1800
1817
  /**