@stigmer/runner 3.12.6 → 3.12.8

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 (73) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-agent-status.d.ts +14 -2
  3. package/dist/activities/call-agent-status.js +24 -7
  4. package/dist/activities/call-agent-status.js.map +1 -1
  5. package/dist/activities/execute-cursor/index.d.ts +10 -0
  6. package/dist/activities/execute-cursor/index.js +26 -0
  7. package/dist/activities/execute-cursor/index.js.map +1 -1
  8. package/dist/activities/execute-cursor/prompt-builder.d.ts +12 -0
  9. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  10. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  11. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +2 -0
  12. package/dist/activities/execute-deep-agent/mcp-gate.js +2 -1
  13. package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
  14. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  15. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  16. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  17. package/dist/activities/execute-deep-agent/setup.js +27 -0
  18. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  19. package/dist/config.d.ts +10 -0
  20. package/dist/config.js +3 -0
  21. package/dist/config.js.map +1 -1
  22. package/dist/main.js +3 -0
  23. package/dist/main.js.map +1 -1
  24. package/dist/runner-manager.d.ts +2 -0
  25. package/dist/runner-manager.js +1 -0
  26. package/dist/runner-manager.js.map +1 -1
  27. package/dist/runner.d.ts +2 -0
  28. package/dist/runner.js +33 -6
  29. package/dist/runner.js.map +1 -1
  30. package/dist/shared/artifact-storage.js +7 -4
  31. package/dist/shared/artifact-storage.js.map +1 -1
  32. package/dist/shared/memory-attachment.d.ts +97 -0
  33. package/dist/shared/memory-attachment.js +136 -0
  34. package/dist/shared/memory-attachment.js.map +1 -0
  35. package/dist/shared/recalled-memories.d.ts +55 -0
  36. package/dist/shared/recalled-memories.js +70 -0
  37. package/dist/shared/recalled-memories.js.map +1 -0
  38. package/dist/shared/tool-kind.js +9 -0
  39. package/dist/shared/tool-kind.js.map +1 -1
  40. package/dist/workflows/call-agent-orchestrator.d.ts +14 -2
  41. package/dist/workflows/call-agent-orchestrator.js +55 -18
  42. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/__tests__/golden-e2e.test.ts +1 -1
  45. package/src/activities/__tests__/call-agent-status.test.ts +30 -5
  46. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  47. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  48. package/src/activities/call-agent-status.ts +25 -8
  49. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +79 -0
  50. package/src/activities/execute-cursor/index.ts +43 -0
  51. package/src/activities/execute-cursor/prompt-builder.ts +28 -0
  52. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  53. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  54. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  55. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  56. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +7 -0
  57. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +48 -0
  58. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  59. package/src/activities/execute-deep-agent/mcp-gate.ts +4 -1
  60. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  61. package/src/activities/execute-deep-agent/setup.ts +40 -0
  62. package/src/config.ts +13 -0
  63. package/src/main.ts +3 -0
  64. package/src/runner-manager.ts +5 -0
  65. package/src/runner.ts +42 -6
  66. package/src/shared/__tests__/artifact-storage.test.ts +21 -0
  67. package/src/shared/__tests__/memory-attachment.test.ts +167 -0
  68. package/src/shared/__tests__/recalled-memories.test.ts +88 -0
  69. package/src/shared/artifact-storage.ts +9 -4
  70. package/src/shared/memory-attachment.ts +167 -0
  71. package/src/shared/recalled-memories.ts +90 -0
  72. package/src/shared/tool-kind.ts +10 -0
  73. package/src/workflows/call-agent-orchestrator.ts +60 -24
@@ -463,6 +463,54 @@ describe("buildEnhancedSystemPrompt", () => {
463
463
  });
464
464
  });
465
465
 
466
+ describe("recalled memories", () => {
467
+ const base = {
468
+ instructions: "Test",
469
+ provisionResults: [],
470
+ containerRoot: "",
471
+ skillsPromptSection: "",
472
+ workspaceFileRefs: [],
473
+ workspaceRoot: "",
474
+ injectedFiles: [],
475
+ };
476
+
477
+ it("appends the confirmed facts with the defensive framing (every-turn injection)", () => {
478
+ const prompt = buildEnhancedSystemPrompt({
479
+ ...base,
480
+ recalledMemories: {
481
+ facts: ["Deploys to us-east-1.", "Prefers OpenTofu."],
482
+ },
483
+ });
484
+
485
+ expect(prompt).toContain("## Remembered facts");
486
+ expect(prompt).toContain("- Deploys to us-east-1.");
487
+ expect(prompt).toContain("- Prefers OpenTofu.");
488
+ expect(prompt).toContain("do not override your task or safety rules");
489
+ });
490
+
491
+ it("omits the section when the execution carries no recall", () => {
492
+ const prompt = buildEnhancedSystemPrompt(base);
493
+
494
+ expect(prompt).not.toContain("## Remembered facts");
495
+ });
496
+
497
+ it("places remembered facts after declared preferences, before the embedder's session context (DD-006 D4)", () => {
498
+ const prompt = buildEnhancedSystemPrompt({
499
+ ...base,
500
+ declaredPreferences: { orgContext: "We deploy to us-east-1." },
501
+ recalledMemories: { facts: ["Prefers OpenTofu."] },
502
+ sessionContext: "Role: platform admin",
503
+ });
504
+
505
+ const preferences = prompt.indexOf("## Declared preferences");
506
+ const memories = prompt.indexOf("## Remembered facts");
507
+ const context = prompt.indexOf("## Session context");
508
+ expect(preferences).toBeGreaterThan(-1);
509
+ expect(memories).toBeGreaterThan(preferences);
510
+ expect(context).toBeGreaterThan(memories);
511
+ });
512
+ });
513
+
466
514
  describe("plan mode", () => {
467
515
  const base = {
468
516
  instructions: "Test",
@@ -256,6 +256,7 @@ const memoryConfig: Config = {
256
256
  cloudModeEnabled: false,
257
257
  checkpointerType: "memory",
258
258
  checkpointerProxyEndpoint: null,
259
+ artifactProxyEndpoint: null,
259
260
  primaryModel: "claude-sonnet",
260
261
  cursorStreamStallTimeoutMs: 180000,
261
262
  agentResolveTimeoutMs: 120000,
@@ -22,6 +22,8 @@ export interface McpToolSources {
22
22
  /** The serving channel id when this session IS a live channel
23
23
  * conversation (the conversation attachment, DD-008). */
24
24
  readonly conversationChannelId: string | undefined;
25
+ /** The recall snapshot's enabled bit (the memory attachment, DD-005 D1). */
26
+ readonly memoryCaptureEnabled: boolean;
25
27
  }
26
28
 
27
29
  /** True when any tool source demands MCP resolution and connect. */
@@ -29,6 +31,7 @@ export function shouldConnectMcp(sources: McpToolSources): boolean {
29
31
  return (
30
32
  sources.mcpServerUsageCount > 0 ||
31
33
  sources.channelMessagingCount > 0 ||
32
- sources.conversationChannelId !== undefined
34
+ sources.conversationChannelId !== undefined ||
35
+ sources.memoryCaptureEnabled
33
36
  );
34
37
  }
@@ -21,6 +21,10 @@ import {
21
21
  formatDeclaredPreferencesText,
22
22
  type DeclaredPreferencesContent,
23
23
  } from "../../shared/declared-preferences.js";
24
+ import {
25
+ formatRecalledMemoriesText,
26
+ type RecalledMemoriesContent,
27
+ } from "../../shared/recalled-memories.js";
24
28
  import {
25
29
  visionDisclosureLines,
26
30
  type NotViewableEntry,
@@ -178,6 +182,14 @@ export interface PromptBuilderInput {
178
182
  * edited preference reaches the very next turn.
179
183
  */
180
184
  declaredPreferences?: DeclaredPreferencesContent;
185
+ /**
186
+ * The subject's confirmed memories (stigmer/stigmer#293 Phase 2, DD-006):
187
+ * consent-gated facts server-snapshotted onto the execution spec's
188
+ * `recalled_memories` at create. Injected on EVERY turn like the
189
+ * preferences — the native system prompt is rebuilt per invocation, so a
190
+ * deleted memory is gone from the very next turn.
191
+ */
192
+ recalledMemories?: RecalledMemoriesContent;
181
193
  }
182
194
 
183
195
  // The prompt renders the injector's own result type — a local structural twin
@@ -251,6 +263,16 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
251
263
  formatDeclaredPreferencesText(input.declaredPreferences);
252
264
  }
253
265
 
266
+ // Declared-by-humans precedes learned-and-confirmed (DD-006 D4): both
267
+ // are platform-authored standing background, but a preference is the
268
+ // user's exact words while a memory is an agent's confirmed inference —
269
+ // the exact statement reads first.
270
+ if (input.recalledMemories) {
271
+ prompt +=
272
+ "\n\n## Remembered facts\n\n" +
273
+ formatRecalledMemoriesText(input.recalledMemories);
274
+ }
275
+
254
276
  // Standing facts about the user (session context) come before the
255
277
  // carried conversation (bridge): the bridge may refer back to them.
256
278
  if (input.sessionContext) {
@@ -33,6 +33,7 @@ import {
33
33
  } from "../../shared/caller-identity.js";
34
34
  import { readSessionContext } from "../../shared/session-context.js";
35
35
  import { readDeclaredPreferences } from "../../shared/declared-preferences.js";
36
+ import { readRecalledMemories } from "../../shared/recalled-memories.js";
36
37
  import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
37
38
  import { mergeMcpServerUsages, resolveMcpServers } from "../../shared/mcp-resolver.js";
38
39
  import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
@@ -46,6 +47,10 @@ import {
46
47
  readChannelConversationId,
47
48
  synthesizeConversationAttachment,
48
49
  } from "../../shared/conversation-attachment.js";
50
+ import {
51
+ memoryCaptureEnabled,
52
+ synthesizeMemoryAttachment,
53
+ } from "../../shared/memory-attachment.js";
49
54
  import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
50
55
  import { shouldConnectMcp } from "./mcp-gate.js";
51
56
  import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
@@ -389,11 +394,17 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
389
394
  // free, synchronous read, so no hoisted discovery needed.
390
395
  const conversationChannelId = readChannelConversationId(session.metadata?.labels);
391
396
 
397
+ // The memory-attachment decision (DD-005 D1): the recall snapshot's
398
+ // enabled bit, stamped server-side at execution create — like the
399
+ // conversation attachment, a free, synchronous spec read.
400
+ const captureMemories = memoryCaptureEnabled(execution.spec?.recalledMemories);
401
+
392
402
  let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
393
403
  if (shouldConnectMcp({
394
404
  mcpServerUsageCount: mcpServerUsages.length,
395
405
  channelMessagingCount: channelMessaging.length,
396
406
  conversationChannelId,
407
+ memoryCaptureEnabled: captureMemories,
397
408
  })) {
398
409
  await reportSetupProgress(client, executionId, "Connecting tools…");
399
410
  const transportPosture = resolveMcpTransportPosture(config.mode);
@@ -455,6 +466,32 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
455
466
  backfilledServers, conversationAttachment, "conversation participation",
456
467
  );
457
468
  }
469
+
470
+ // The memory capture attachment (DD-005 D1) — same after-backfill
471
+ // rule. The capture context is attribution the server verifies or
472
+ // trusts per edition (Stage 3 provenance decision); the subject is
473
+ // never threaded — it derives from the credential.
474
+ if (captureMemories) {
475
+ const memoryAttachment = synthesizeMemoryAttachment(
476
+ execution.spec?.recalledMemories,
477
+ {
478
+ org: session.metadata?.org ?? "",
479
+ agentId: agent.metadata?.id ?? "",
480
+ sessionId,
481
+ agentExecutionId: executionId,
482
+ },
483
+ {
484
+ bridgeEndpoint: config.mcpBridgeEndpoint,
485
+ credential: attachmentCredential,
486
+ backendEndpoint: config.stigmerBackendEndpoint,
487
+ },
488
+ );
489
+ if (memoryAttachment) {
490
+ backfilledServers = injectSynthesizedAttachment(
491
+ backfilledServers, memoryAttachment, "memory capture",
492
+ );
493
+ }
494
+ }
458
495
  resolvedMcpServers = { resolvedServers: backfilledServers };
459
496
  timing.mark("backfill_mcp");
460
497
 
@@ -577,6 +614,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
577
614
  declaredPreferences: readDeclaredPreferences(
578
615
  execution.spec!.declaredPreferences,
579
616
  ),
617
+ recalledMemories: readRecalledMemories(
618
+ execution.spec!.recalledMemories,
619
+ ),
580
620
  });
581
621
 
582
622
  // Step 9: Construct the LLM model. Resolution to the provider API id
package/src/config.ts CHANGED
@@ -96,6 +96,16 @@ export interface Config {
96
96
  readonly cloudModeEnabled: boolean;
97
97
  readonly checkpointerType: "memory" | "http" | "sqlite";
98
98
  readonly checkpointerProxyEndpoint: string | null;
99
+ /**
100
+ * Endpoint the proxy artifact store presigns against
101
+ * (STIGMER_ARTIFACT_PROXY_ENDPOINT), defaulting to {@link proxyEndpoint} —
102
+ * the checkpointer-override pattern (stigmer#803). Splitting the two lets
103
+ * artifact traffic target the real control plane while LLM traffic goes
104
+ * elsewhere (the conformance harness points LLM calls at a mock proxy that
105
+ * serves no presign routes; embedders can route artifact storage
106
+ * independently the same way).
107
+ */
108
+ readonly artifactProxyEndpoint: string | null;
99
109
  readonly primaryModel: string;
100
110
  /**
101
111
  * No-progress bound for the Cursor harness stream (milliseconds). If no
@@ -197,6 +207,8 @@ export function loadConfig(): Config {
197
207
  ?? (mode === "cloud" ? "http" : "sqlite");
198
208
  const checkpointerProxyEndpoint = process.env.STIGMER_CHECKPOINTER_PROXY_ENDPOINT
199
209
  ?? proxyEndpoint;
210
+ const artifactProxyEndpoint = process.env.STIGMER_ARTIFACT_PROXY_ENDPOINT
211
+ ?? proxyEndpoint;
200
212
 
201
213
  const primaryModel = process.env.STIGMER_PRIMARY_MODEL ?? "gpt-4.1";
202
214
 
@@ -228,6 +240,7 @@ export function loadConfig(): Config {
228
240
  cloudModeEnabled: process.env.STIGMER_CURSOR_CLOUD_MODE_ENABLED === "true",
229
241
  checkpointerType,
230
242
  checkpointerProxyEndpoint,
243
+ artifactProxyEndpoint,
231
244
  primaryModel,
232
245
  cursorStreamStallTimeoutMs,
233
246
  agentResolveTimeoutMs,
package/src/main.ts CHANGED
@@ -89,6 +89,7 @@ async function runManagerMode(config: import("./config.js").Config): Promise<voi
89
89
  primaryModel: config.primaryModel,
90
90
  checkpointerType: config.checkpointerType,
91
91
  checkpointerProxyEndpoint: config.checkpointerProxyEndpoint ?? undefined,
92
+ artifactProxyEndpoint: config.artifactProxyEndpoint ?? undefined,
92
93
  cloudModeEnabled: config.cloudModeEnabled,
93
94
  executionMode: config.mode,
94
95
  });
@@ -213,6 +214,7 @@ async function runPoolMode(
213
214
  primaryModel: config.primaryModel,
214
215
  checkpointerType: config.checkpointerType,
215
216
  checkpointerProxyEndpoint: config.checkpointerProxyEndpoint ?? undefined,
217
+ artifactProxyEndpoint: config.artifactProxyEndpoint ?? undefined,
216
218
  cloudModeEnabled: config.cloudModeEnabled,
217
219
  executionMode: config.mode,
218
220
  });
@@ -338,6 +340,7 @@ async function runStaticMode(config: import("./config.js").Config): Promise<void
338
340
  primaryModel: config.primaryModel,
339
341
  checkpointerType: config.checkpointerType,
340
342
  checkpointerProxyEndpoint: config.checkpointerProxyEndpoint ?? undefined,
343
+ artifactProxyEndpoint: config.artifactProxyEndpoint ?? undefined,
341
344
  cloudModeEnabled: config.cloudModeEnabled,
342
345
  // Honor the operator's MODE env (resolved into config.mode) instead of
343
346
  // re-deriving execution location from the proxy. This keeps static mode
@@ -114,6 +114,9 @@ export interface RunnerManagerOptions {
114
114
  /** Checkpointer proxy endpoint. Falls back to proxyEndpoint. */
115
115
  readonly checkpointerProxyEndpoint?: string;
116
116
 
117
+ /** Artifact presign endpoint (stigmer#803). Falls back to proxyEndpoint. */
118
+ readonly artifactProxyEndpoint?: string;
119
+
117
120
  /** Enable Cursor cloud mode. @default false */
118
121
  readonly cloudModeEnabled?: boolean;
119
122
 
@@ -721,6 +724,8 @@ export function mapManagerOptionsToConfig(
721
724
  options.checkpointerType ?? (proxyActive ? "http" : "sqlite"),
722
725
  checkpointerProxyEndpoint:
723
726
  options.checkpointerProxyEndpoint ?? options.proxyEndpoint ?? null,
727
+ artifactProxyEndpoint:
728
+ options.artifactProxyEndpoint ?? options.proxyEndpoint ?? null,
724
729
  primaryModel: options.primaryModel ?? "gpt-4.1",
725
730
  cursorStreamStallTimeoutMs:
726
731
  options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
package/src/runner.ts CHANGED
@@ -18,7 +18,8 @@ import { homedir, tmpdir } from "node:os";
18
18
  import type { Config } from "./config.js";
19
19
  import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
20
20
  import type { WorkerActivities } from "./worker.js";
21
- import { resolveRunnerBootstrap } from "./bootstrap.js";
21
+ import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
22
+ import { createRunnerTokenCoordinator } from "./runner-token-coordinator.js";
22
23
  import { assertLlmBackendsPreflight } from "./preflight.js";
23
24
  import {
24
25
  captureRunnerSecrets,
@@ -86,6 +87,9 @@ export interface StigmerRunnerOptions {
86
87
  /** Checkpointer proxy endpoint. Falls back to proxyEndpoint. */
87
88
  readonly checkpointerProxyEndpoint?: string;
88
89
 
90
+ /** Artifact presign endpoint (stigmer#803). Falls back to proxyEndpoint. */
91
+ readonly artifactProxyEndpoint?: string;
92
+
89
93
  /** Enable Cursor cloud mode for workspace-less execution. @default false */
90
94
  readonly cloudModeEnabled?: boolean;
91
95
 
@@ -253,11 +257,6 @@ export async function createStigmerRunner(
253
257
  // Only the worker connection consumes these coordinates — no activity dials
254
258
  // Temporal directly (emit-event, the last one, now routes signals through
255
259
  // the server's SendSignal lane; see oss#517).
256
- //
257
- // The static runner brings its own already-proxy-valid token (harness/CLI),
258
- // so it does not consume the minted runner token from the bootstrap response;
259
- // that proxy-credential lifecycle lives in createStigmerRunnerManager (the
260
- // long-lived desktop host that needs it). Only the coordinates are used here.
261
260
  const coordinates = await resolveRunnerBootstrap({
262
261
  explicitAddress: options.temporalAddress,
263
262
  explicitNamespace: options.temporalNamespace,
@@ -269,11 +268,44 @@ export async function createStigmerRunner(
269
268
  // activity clients read the ref per request instead of pinning the boot
270
269
  // token for the pod's whole life.
271
270
  const tokenRef = { current: baseConfig.stigmerToken };
271
+
272
+ // Adopt the bootstrap-minted embedded_runner credential for gRPC runner-class
273
+ // calls (stigmer-cloud#507). The static path historically discarded it ("the
274
+ // static token is already proxy-valid") — true for the PROXY lane, but the
275
+ // ExecutionContext decrypt lane is gated on runner-class token_type
276
+ // (stigmer-cloud#152/#155): a user-token static runner (conformance harness,
277
+ // CLI daemon with a cloud token) had its scoped-token exchange refused and
278
+ // silently read REDACTED secret values. The coordinator owns the mint's TTL
279
+ // (same module the desktop manager uses — one refresh implementation, not
280
+ // two); its only sink here is the gRPC runner-credential ref, because the
281
+ // static host's proxy token is provided by the host and stays untouched.
282
+ // Servers that mint nothing (explicit-address boots, tokenless OSS, cloud
283
+ // sandboxes with baked credentials) leave the ref null — byte-identical
284
+ // behavior to before.
285
+ const runnerTokenRef: { current: string | null } = { current: null };
286
+ const runnerTokenCoordinator = createRunnerTokenCoordinator({
287
+ applyProxyToken: (token) => {
288
+ runnerTokenRef.current = token;
289
+ },
290
+ reMint: () =>
291
+ refreshRunnerAccessToken({
292
+ token: tokenRef.current,
293
+ stigmerEndpoint: baseConfig.stigmerBackendEndpoint,
294
+ }),
295
+ });
296
+ if (coordinates.runnerAccessToken) {
297
+ runnerTokenCoordinator.adoptMintedToken(
298
+ coordinates.runnerAccessToken,
299
+ coordinates.runnerAccessTokenExpiresInSeconds,
300
+ );
301
+ }
302
+
272
303
  const config: Config = {
273
304
  ...baseConfig,
274
305
  temporalAddress: coordinates.temporalAddress,
275
306
  temporalNamespace: coordinates.temporalNamespace,
276
307
  stigmerTokenRef: tokenRef,
308
+ stigmerRunnerTokenRef: runnerTokenRef,
277
309
  };
278
310
  markBoot("bootstrap_resolved");
279
311
 
@@ -359,6 +391,7 @@ export async function createStigmerRunner(
359
391
  },
360
392
  shutdown() {
361
393
  tokenRenewal?.stop();
394
+ runnerTokenCoordinator.stop();
362
395
  // Classification first, drain second: an in-flight activity cancelled
363
396
  // by the drain must observe the signal already aborted (see
364
397
  // shared/worker-shutdown.ts for the ownership contract).
@@ -418,6 +451,9 @@ export function mapOptionsToConfig(options: StigmerRunnerOptions): Config {
418
451
  checkpointerProxyEndpoint: options.checkpointerProxyEndpoint
419
452
  ?? options.proxyEndpoint
420
453
  ?? null,
454
+ artifactProxyEndpoint: options.artifactProxyEndpoint
455
+ ?? options.proxyEndpoint
456
+ ?? null,
421
457
  primaryModel: options.primaryModel ?? "gpt-4.1",
422
458
  cursorStreamStallTimeoutMs:
423
459
  options.cursorStreamStallTimeoutMs ?? DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS,
@@ -654,6 +654,7 @@ describe("loadArtifactStorageConfig", () => {
654
654
  runnerId: null,
655
655
  checkpointerType: "memory" as const,
656
656
  checkpointerProxyEndpoint: null,
657
+ artifactProxyEndpoint: null,
657
658
  primaryModel: "gpt-4.1",
658
659
  cursorStreamStallTimeoutMs: 180000,
659
660
  agentResolveTimeoutMs: 120000,
@@ -672,10 +673,14 @@ describe("loadArtifactStorageConfig", () => {
672
673
  });
673
674
 
674
675
  it("defaults to proxy in cloud mode", () => {
676
+ // loadConfig derives artifactProxyEndpoint from proxyEndpoint when the
677
+ // STIGMER_ARTIFACT_PROXY_ENDPOINT override is unset; Config literals here
678
+ // mirror that invariant.
675
679
  const cfg = loadArtifactStorageConfig({
676
680
  ...baseConfig,
677
681
  mode: "cloud",
678
682
  proxyEndpoint: "https://proxy.example.com",
683
+ artifactProxyEndpoint: "https://proxy.example.com",
679
684
  stigmerToken: "tok",
680
685
  });
681
686
  expect(cfg.type).toBe("proxy");
@@ -689,6 +694,7 @@ describe("loadArtifactStorageConfig", () => {
689
694
  ...baseConfig,
690
695
  mode: "local",
691
696
  proxyEndpoint: "https://localhost:9090",
697
+ artifactProxyEndpoint: "https://localhost:9090",
692
698
  stigmerToken: "tok",
693
699
  });
694
700
  expect(cfg.type).toBe("proxy");
@@ -696,6 +702,21 @@ describe("loadArtifactStorageConfig", () => {
696
702
  expect(cfg.proxyAuthToken).toBe("tok");
697
703
  });
698
704
 
705
+ it("presigns against the artifact override when split from the LLM proxy endpoint (stigmer#803)", () => {
706
+ // The conformance harness points LLM traffic at a mock proxy that serves
707
+ // no presign routes; the artifact override routes storage at the real
708
+ // control plane independently (the checkpointer-override pattern).
709
+ process.env.ARTIFACT_STORAGE_TYPE = "proxy";
710
+ const cfg = loadArtifactStorageConfig({
711
+ ...baseConfig,
712
+ proxyEndpoint: "https://mock-llm.example.com",
713
+ artifactProxyEndpoint: "https://service.example.com",
714
+ stigmerToken: "tok",
715
+ });
716
+ expect(cfg.type).toBe("proxy");
717
+ expect(cfg.proxyEndpoint).toBe("https://service.example.com");
718
+ });
719
+
699
720
  it("honors ARTIFACT_STORAGE_TYPE=none even when a proxy endpoint is configured", () => {
700
721
  // "none" is a deliberate operator/e2e posture — it must beat the
701
722
  // storage-follows-transport default exactly like the other overrides.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The memory capture attachment (DD-005 D1): the recall-snapshot gate
3
+ * (enabled bit, zero-facts included), both connection shapes with the
4
+ * capture-context carriers, the structural approval-freedom of
5
+ * synthesized attachments, and the cross-repo string pins (route +
6
+ * context keys, guarded here and in the mcp-server memory integration
7
+ * test — the TOOL_CALL_LIMIT precedent). The slug and roster are
8
+ * runner-internal and guarded here alone.
9
+ */
10
+
11
+ import { create } from "@bufbuild/protobuf";
12
+ import { describe, expect, it } from "vitest";
13
+ import { RecalledMemoriesSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
14
+
15
+ import { mergeApprovalPolicies, type ActiveLeases } from "../approval-policy.js";
16
+ import { needsBackfill } from "../connect-backfill.js";
17
+ import {
18
+ MEMORY_AGENT_ID_ENV,
19
+ MEMORY_AGENT_ID_HEADER,
20
+ MEMORY_ATTACHMENT_SLUG,
21
+ MEMORY_EXECUTION_ID_ENV,
22
+ MEMORY_EXECUTION_ID_HEADER,
23
+ MEMORY_ORG_ENV,
24
+ MEMORY_ORG_HEADER,
25
+ MEMORY_ROUTE,
26
+ MEMORY_SESSION_ID_ENV,
27
+ MEMORY_SESSION_ID_HEADER,
28
+ memoryCaptureEnabled,
29
+ synthesizeMemoryAttachment,
30
+ type MemoryCaptureContext,
31
+ } from "../memory-attachment.js";
32
+ import type { ResolvedMcpServer } from "../mcp-resolver.js";
33
+
34
+ const context: MemoryCaptureContext = {
35
+ org: "acme",
36
+ agentId: "agt_1",
37
+ sessionId: "ses_1",
38
+ agentExecutionId: "aex_1",
39
+ };
40
+
41
+ const cloudOptions = {
42
+ bridgeEndpoint: "https://mcp.stigmer.ai/",
43
+ credential: "sandbox-token",
44
+ backendEndpoint: "http://localhost:7234",
45
+ };
46
+
47
+ const ossOptions = {
48
+ bridgeEndpoint: null,
49
+ credential: null,
50
+ backendEndpoint: "http://localhost:7234",
51
+ };
52
+
53
+ const noLeases: ActiveLeases = {
54
+ global: false,
55
+ categories: new Set(),
56
+ servers: new Set(),
57
+ };
58
+
59
+ describe("memoryCaptureEnabled (the DD-005 D1 injection signal)", () => {
60
+ it("is the snapshot's enabled bit — absent and disabled read false", () => {
61
+ expect(memoryCaptureEnabled(undefined)).toBe(false);
62
+ expect(memoryCaptureEnabled(create(RecalledMemoriesSchema, { enabled: false }))).toBe(false);
63
+ expect(memoryCaptureEnabled(create(RecalledMemoriesSchema, { enabled: true }))).toBe(true);
64
+ });
65
+
66
+ it("enabled with ZERO facts still offers the tool — memory on, nothing stored yet", () => {
67
+ const snapshot = create(RecalledMemoriesSchema, { enabled: true, facts: [] });
68
+ expect(memoryCaptureEnabled(snapshot)).toBe(true);
69
+ expect(synthesizeMemoryAttachment(snapshot, context, cloudOptions)).toBeDefined();
70
+ });
71
+ });
72
+
73
+ describe("synthesizeMemoryAttachment", () => {
74
+ const enabled = create(RecalledMemoriesSchema, { enabled: true });
75
+
76
+ it("returns undefined when the snapshot is absent or disabled — honest absence", () => {
77
+ expect(synthesizeMemoryAttachment(undefined, context, cloudOptions)).toBeUndefined();
78
+ expect(
79
+ synthesizeMemoryAttachment(
80
+ create(RecalledMemoriesSchema, { enabled: false }),
81
+ context,
82
+ cloudOptions,
83
+ ),
84
+ ).toBeUndefined();
85
+ });
86
+
87
+ it("builds the HTTP shape against the bridge /memory route with credential + context headers", () => {
88
+ const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions);
89
+ expect(attachment).toMatchObject({
90
+ slug: MEMORY_ATTACHMENT_SLUG,
91
+ connectionType: "http",
92
+ url: "https://mcp.stigmer.ai/memory",
93
+ headers: {
94
+ Authorization: "Bearer sandbox-token",
95
+ [MEMORY_ORG_HEADER]: "acme",
96
+ [MEMORY_AGENT_ID_HEADER]: "agt_1",
97
+ [MEMORY_SESSION_ID_HEADER]: "ses_1",
98
+ [MEMORY_EXECUTION_ID_HEADER]: "aex_1",
99
+ },
100
+ });
101
+ });
102
+
103
+ it("builds the OSS stdio shape: the CLI-embedded bridge with the memory roster + context env", () => {
104
+ const attachment = synthesizeMemoryAttachment(enabled, context, ossOptions);
105
+ expect(attachment).toMatchObject({
106
+ connectionType: "stdio",
107
+ command: "stigmer",
108
+ args: ["mcp-server"],
109
+ env: {
110
+ STIGMER_MCP_ROSTER: "memory",
111
+ STIGMER_SERVER_ADDRESS: "localhost:7234",
112
+ [MEMORY_ORG_ENV]: "acme",
113
+ [MEMORY_AGENT_ID_ENV]: "agt_1",
114
+ [MEMORY_SESSION_ID_ENV]: "ses_1",
115
+ [MEMORY_EXECUTION_ID_ENV]: "aex_1",
116
+ },
117
+ });
118
+ });
119
+
120
+ it("omits empty context fields from the carrier — best-effort attribution, never blank entries", () => {
121
+ const partial: MemoryCaptureContext = {
122
+ org: "acme",
123
+ agentId: "",
124
+ sessionId: "ses_1",
125
+ agentExecutionId: "",
126
+ };
127
+
128
+ const http = synthesizeMemoryAttachment(enabled, partial, cloudOptions);
129
+ expect(Object.keys(http?.headers ?? {}).sort()).toEqual([
130
+ "Authorization",
131
+ MEMORY_ORG_HEADER,
132
+ MEMORY_SESSION_ID_HEADER,
133
+ ].sort());
134
+
135
+ const stdio = synthesizeMemoryAttachment(enabled, partial, ossOptions);
136
+ expect(stdio?.env).not.toHaveProperty(MEMORY_AGENT_ID_ENV);
137
+ expect(stdio?.env).not.toHaveProperty(MEMORY_EXECUTION_ID_ENV);
138
+ });
139
+
140
+ it("pins the cross-repo strings the mcp-server side guards too", () => {
141
+ expect(MEMORY_ATTACHMENT_SLUG).toBe("stigmer-memory");
142
+ expect(MEMORY_ROUTE).toBe("/memory");
143
+ expect(MEMORY_ORG_HEADER).toBe("x-stigmer-memory-org");
144
+ expect(MEMORY_AGENT_ID_HEADER).toBe("x-stigmer-memory-agent-id");
145
+ expect(MEMORY_SESSION_ID_HEADER).toBe("x-stigmer-memory-session-id");
146
+ expect(MEMORY_EXECUTION_ID_HEADER).toBe("x-stigmer-memory-execution-id");
147
+ expect(MEMORY_ORG_ENV).toBe("STIGMER_MEMORY_ORG");
148
+ expect(MEMORY_AGENT_ID_ENV).toBe("STIGMER_MEMORY_AGENT_ID");
149
+ expect(MEMORY_SESSION_ID_ENV).toBe("STIGMER_MEMORY_SESSION_ID");
150
+ expect(MEMORY_EXECUTION_ID_ENV).toBe("STIGMER_MEMORY_EXECUTION_ID");
151
+ });
152
+
153
+ it("is approval-free by construction: zero entries in the merged approval map", () => {
154
+ // Consent is the confirm RPC, not tool approval (DD-005 D3): the
155
+ // tool only creates a proposal, so gating it would stack a second
156
+ // consent gate in front of the real one.
157
+ const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions)!;
158
+ const merged = mergeApprovalPolicies([attachment as ResolvedMcpServer], noLeases);
159
+ expect(merged.size).toBe(0);
160
+ });
161
+
162
+ it("is structurally immune to the connect backfill (destructiveHint tightener)", () => {
163
+ const attachment = synthesizeMemoryAttachment(enabled, context, cloudOptions)!;
164
+ expect(attachment.discoveredCapabilitiesEmpty).toBe(false);
165
+ expect(needsBackfill(attachment)).toBe(false);
166
+ });
167
+ });