@stigmer/runner 3.12.5 → 3.12.7

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 (102) 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/call-agent.js +19 -6
  6. package/dist/activities/call-agent.js.map +1 -1
  7. package/dist/activities/execute-cursor/error-classifier.d.ts +9 -0
  8. package/dist/activities/execute-cursor/error-classifier.js +30 -1
  9. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  10. package/dist/activities/execute-cursor/index.d.ts +10 -0
  11. package/dist/activities/execute-cursor/index.js +20 -7
  12. package/dist/activities/execute-cursor/index.js.map +1 -1
  13. package/dist/activities/execute-cursor/prompt-builder.d.ts +12 -0
  14. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  15. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  16. package/dist/activities/execute-cursor/service-tier.d.ts +38 -29
  17. package/dist/activities/execute-cursor/service-tier.js +92 -63
  18. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  19. package/dist/activities/execute-cursor/usage-accumulator.d.ts +16 -2
  20. package/dist/activities/execute-cursor/usage-accumulator.js +12 -2
  21. package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
  22. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  23. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  24. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  25. package/dist/activities/execute-deep-agent/setup.js +2 -0
  26. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.js +3 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/main.js +3 -0
  31. package/dist/main.js.map +1 -1
  32. package/dist/runner-manager.d.ts +2 -0
  33. package/dist/runner-manager.js +1 -0
  34. package/dist/runner-manager.js.map +1 -1
  35. package/dist/runner.d.ts +2 -0
  36. package/dist/runner.js +33 -6
  37. package/dist/runner.js.map +1 -1
  38. package/dist/shared/artifact-storage.js +7 -4
  39. package/dist/shared/artifact-storage.js.map +1 -1
  40. package/dist/shared/caller-identity.d.ts +10 -7
  41. package/dist/shared/caller-identity.js +10 -7
  42. package/dist/shared/caller-identity.js.map +1 -1
  43. package/dist/shared/recalled-memories.d.ts +55 -0
  44. package/dist/shared/recalled-memories.js +70 -0
  45. package/dist/shared/recalled-memories.js.map +1 -0
  46. package/dist/shared/thinking-mode.d.ts +35 -0
  47. package/dist/shared/thinking-mode.js +43 -0
  48. package/dist/shared/thinking-mode.js.map +1 -0
  49. package/dist/workflow-engine/loader.js +52 -13
  50. package/dist/workflow-engine/loader.js.map +1 -1
  51. package/dist/workflow-engine/tasks/human-input.d.ts +2 -1
  52. package/dist/workflow-engine/tasks/human-input.js +8 -1
  53. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  54. package/dist/workflow-engine/types.d.ts +17 -3
  55. package/dist/workflow-engine/types.js.map +1 -1
  56. package/dist/workflows/call-agent-orchestrator.d.ts +14 -2
  57. package/dist/workflows/call-agent-orchestrator.js +55 -18
  58. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  59. package/dist/workflows/human-input-orchestrator.d.ts +4 -0
  60. package/dist/workflows/human-input-orchestrator.js +13 -0
  61. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  62. package/package.json +2 -2
  63. package/src/__tests__/golden-e2e.test.ts +1 -1
  64. package/src/activities/__tests__/call-agent-status.test.ts +30 -5
  65. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  66. package/src/activities/__tests__/discover-mcp-server.test.ts +1 -0
  67. package/src/activities/call-agent-status.ts +25 -8
  68. package/src/activities/call-agent.ts +20 -6
  69. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +79 -0
  70. package/src/activities/execute-cursor/__tests__/error-classifier-billing.test.ts +67 -0
  71. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +67 -7
  72. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +33 -1
  73. package/src/activities/execute-cursor/error-classifier.ts +42 -1
  74. package/src/activities/execute-cursor/index.ts +30 -6
  75. package/src/activities/execute-cursor/prompt-builder.ts +28 -0
  76. package/src/activities/execute-cursor/service-tier.ts +94 -63
  77. package/src/activities/execute-cursor/usage-accumulator.ts +11 -1
  78. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  79. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  80. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  81. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  82. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +48 -0
  83. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  84. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  85. package/src/activities/execute-deep-agent/setup.ts +4 -0
  86. package/src/config.ts +13 -0
  87. package/src/main.ts +3 -0
  88. package/src/runner-manager.ts +5 -0
  89. package/src/runner.ts +42 -6
  90. package/src/shared/__tests__/artifact-storage.test.ts +21 -0
  91. package/src/shared/__tests__/recalled-memories.test.ts +88 -0
  92. package/src/shared/artifact-storage.ts +9 -4
  93. package/src/shared/caller-identity.ts +10 -7
  94. package/src/shared/recalled-memories.ts +90 -0
  95. package/src/shared/thinking-mode.ts +52 -0
  96. package/src/workflow-engine/__tests__/loader.test.ts +47 -3
  97. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +39 -0
  98. package/src/workflow-engine/loader.ts +63 -17
  99. package/src/workflow-engine/tasks/human-input.ts +8 -1
  100. package/src/workflow-engine/types.ts +18 -3
  101. package/src/workflows/call-agent-orchestrator.ts +60 -24
  102. package/src/workflows/human-input-orchestrator.ts +20 -2
@@ -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";
@@ -577,6 +578,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
577
578
  declaredPreferences: readDeclaredPreferences(
578
579
  execution.spec!.declaredPreferences,
579
580
  ),
581
+ recalledMemories: readRecalledMemories(
582
+ execution.spec!.recalledMemories,
583
+ ),
580
584
  });
581
585
 
582
586
  // 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,88 @@
1
+ /**
2
+ * Unit tests for the recalled-memories module (stigmer/stigmer#293 Phase 2,
3
+ * DD-006). Like declared-preferences there is no string key to mirror-guard —
4
+ * the value rides the typed `AgentExecutionSpec.recalled_memories` proto
5
+ * field, so codegen enforces the cross-repo contract. What IS pinned here:
6
+ * the render-only-when-something-to-say read semantics (disabled OR empty
7
+ * renders nothing — the enabled bit with zero facts is Stage 3's remember-
8
+ * tool signal, not this module's concern), the server-composed fact order,
9
+ * the content-only rendering (memory_id never reaches the prompt), and the
10
+ * framing's behavioral contract.
11
+ */
12
+
13
+ import { describe, it, expect } from "vitest";
14
+ import { create } from "@bufbuild/protobuf";
15
+ import { RecalledMemoriesSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
16
+
17
+ import {
18
+ formatRecalledMemoriesText,
19
+ readRecalledMemories,
20
+ } from "../recalled-memories.js";
21
+
22
+ const FACT_OLDER = "Deploys to us-east-1.";
23
+ const FACT_NEWER = "Prefers OpenTofu over Terraform.";
24
+
25
+ describe("readRecalledMemories", () => {
26
+ it("reads the facts in server-composed order (oldest-first in both editions)", () => {
27
+ const recalled = create(RecalledMemoriesSchema, {
28
+ enabled: true,
29
+ facts: [
30
+ { memoryId: "mem_older", content: FACT_OLDER },
31
+ { memoryId: "mem_newer", content: FACT_NEWER },
32
+ ],
33
+ });
34
+ expect(readRecalledMemories(recalled)).toEqual({
35
+ facts: [FACT_OLDER, FACT_NEWER],
36
+ });
37
+ });
38
+
39
+ it("answers undefined when the field is absent (pre-Phase-2 executions)", () => {
40
+ expect(readRecalledMemories(undefined)).toBeUndefined();
41
+ });
42
+
43
+ it("answers undefined when recall is disabled — facts on a disabled snapshot are never rendered", () => {
44
+ const recalled = create(RecalledMemoriesSchema, {
45
+ enabled: false,
46
+ facts: [{ memoryId: "mem_1", content: FACT_OLDER }],
47
+ });
48
+ expect(readRecalledMemories(recalled)).toBeUndefined();
49
+ });
50
+
51
+ it("answers undefined for enabled-with-zero-facts — a meaningful snapshot state (the remember-tool signal, DD-005 D1) that renders nothing", () => {
52
+ const recalled = create(RecalledMemoriesSchema, { enabled: true });
53
+ expect(readRecalledMemories(recalled)).toBeUndefined();
54
+ });
55
+
56
+ it("drops blank facts defensively and trims the rest — the server never stamps them (write-time min_len)", () => {
57
+ const recalled = create(RecalledMemoriesSchema, {
58
+ enabled: true,
59
+ facts: [
60
+ { memoryId: "mem_1", content: ` ${FACT_OLDER} ` },
61
+ { memoryId: "mem_2", content: " " },
62
+ ],
63
+ });
64
+ expect(readRecalledMemories(recalled)).toEqual({ facts: [FACT_OLDER] });
65
+ });
66
+ });
67
+
68
+ describe("formatRecalledMemoriesText", () => {
69
+ it("frames the facts as user-confirmed, user-controlled background — never authority", () => {
70
+ const framed = formatRecalledMemoriesText({ facts: [FACT_OLDER] });
71
+
72
+ expect(framed).toContain("this user previously confirmed");
73
+ expect(framed).toContain("not instructions");
74
+ expect(framed).toContain("do not override your task or safety rules");
75
+ expect(framed).toContain("review and delete them at any time");
76
+ });
77
+
78
+ it("renders one list item per fact, preserving the snapshot's order", () => {
79
+ const framed = formatRecalledMemoriesText({
80
+ facts: [FACT_OLDER, FACT_NEWER],
81
+ });
82
+
83
+ expect(framed).toContain(`- ${FACT_OLDER}`);
84
+ expect(framed).toContain(`- ${FACT_NEWER}`);
85
+ expect(framed.indexOf(FACT_OLDER)).toBeLessThan(framed.indexOf(FACT_NEWER));
86
+ expect(framed.endsWith(FACT_NEWER)).toBe(true);
87
+ });
88
+ });
@@ -376,19 +376,22 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
376
376
  // configured, push artifacts through it (the proxy brokers R2). This holds for
377
377
  // both cloud runners and the local desktop runner — the latter executes
378
378
  // locally (mode === "local") yet still uploads via the proxy. An explicit
379
- // ARTIFACT_STORAGE_TYPE always wins.
379
+ // ARTIFACT_STORAGE_TYPE always wins. Presigns target artifactProxyEndpoint —
380
+ // STIGMER_ARTIFACT_PROXY_ENDPOINT when split from the LLM proxy endpoint
381
+ // (stigmer#803, the checkpointer-override pattern), the plain proxy
382
+ // endpoint otherwise.
380
383
  const envType = process.env.ARTIFACT_STORAGE_TYPE;
381
384
  const type: ArtifactStorageType =
382
385
  envType === "proxy" ? "proxy" :
383
386
  envType === "local" ? "local" :
384
387
  envType === "none" ? "none" :
385
- config.proxyEndpoint ? "proxy" : "local";
388
+ config.artifactProxyEndpoint ? "proxy" : "local";
386
389
 
387
390
  return {
388
391
  type,
389
392
  localPath: process.env.LOCAL_ARTIFACT_PATH ?? defaultLocalArtifactPath(),
390
393
  localServeUrl: process.env.LOCAL_ARTIFACT_SERVE_URL ?? "http://localhost:7235",
391
- proxyEndpoint: type === "proxy" ? (config.proxyEndpoint ?? null) : null,
394
+ proxyEndpoint: type === "proxy" ? (config.artifactProxyEndpoint ?? null) : null,
392
395
  // Prefer the live ref: renewal rotates the token in place and uploads
393
396
  // must present the current credential, not the boot one.
394
397
  proxyAuthToken: type === "proxy"
@@ -408,7 +411,9 @@ export function createArtifactStorage(cfg: ArtifactStorageConfig): ArtifactStora
408
411
  }
409
412
  if (cfg.type === "proxy") {
410
413
  if (!cfg.proxyEndpoint) {
411
- throw new Error("Proxy artifact storage requires STIGMER_PROXY_ENDPOINT");
414
+ throw new Error(
415
+ "Proxy artifact storage requires STIGMER_ARTIFACT_PROXY_ENDPOINT or STIGMER_PROXY_ENDPOINT",
416
+ );
412
417
  }
413
418
  const tokenAtBoot = typeof cfg.proxyAuthToken === "string"
414
419
  ? cfg.proxyAuthToken
@@ -58,15 +58,18 @@ export const ANONYMOUS_KIND = "anonymous";
58
58
 
59
59
  /**
60
60
  * Audit-actor id that backends stamp when NO caller identity exists —
61
- * the OSS server writes it on every create (no local auth), and the
62
- * cloud's AuditActorBuilder falls back to it for caller-less internal
63
- * writes. It names "nobody in particular": unrelated sessions from
64
- * unrelated people all carry it, so presenting it as a caller identity
65
- * would make the one string a grantable value that silently covers ALL
66
- * such traffic in an MCP server's binding sheet. A creator matching this
61
+ * the OSS server writes it on every create unless the deployment
62
+ * configured an operator identity (STIGMER_OPERATOR_EMAIL,
63
+ * stigmer/stigmer#400; a configured install stamps a real actor whose
64
+ * email resolves below like any other), and the cloud's
65
+ * AuditActorBuilder falls back to it for caller-less internal writes.
66
+ * It names "nobody in particular": unrelated sessions from unrelated
67
+ * people all carry it, so presenting it as a caller identity would make
68
+ * the one string a grantable value that silently covers ALL such
69
+ * traffic in an MCP server's binding sheet. A creator matching this
67
70
  * sentinel (and carrying no email) is therefore unresolvable and falls
68
71
  * to anonymous — the deny-by-default the docs guide already promises
69
- * for self-hosted backends.
72
+ * for unconfigured self-hosted backends.
70
73
  */
71
74
  export const SYSTEM_CREATOR_SENTINEL = "system";
72
75
 
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Recalled memories (stigmer/stigmer#293 Phase 2, DD-006): confirmed facts
3
+ * the subject previously approved the platform to remember — "prefers
4
+ * OpenTofu", "deploys to us-east-1" — injected into every eligible
5
+ * execution so agents stop forgetting people between sessions.
6
+ *
7
+ * The server composes the CONTENT at execution create: the create pipeline
8
+ * snapshots the subject's CONFIRMED memory records (never proposed or
9
+ * rejected — consent-gated, DD-005) onto the execution spec's
10
+ * `recalled_memories` field, oldest-first, gated on the memory_enabled
11
+ * preference flags. This module owns the PRESENTATION — the preamble and
12
+ * the fact list — so the framing cannot drift between harnesses.
13
+ *
14
+ * Like declared-preferences (its direct template) there is no metadata key
15
+ * to mirror-guard: the value rides a TYPED proto field, so codegen enforces
16
+ * the cross-repo contract. Degradation is safe by construction: an absent,
17
+ * disabled, or empty field renders nothing, and a runner predating this
18
+ * module simply ignores it — the agent runs without memories, exactly the
19
+ * pre-Phase-2 behavior, never worse.
20
+ *
21
+ * The snapshot's `enabled` bit with zero facts is a meaningful state
22
+ * ("memory is on, nothing stored yet") — it is Stage 3's signal to offer
23
+ * the remember tool (DD-005 D1) and is deliberately NOT consumed here:
24
+ * this module renders recall, and an empty recall renders nothing.
25
+ */
26
+
27
+ import type { RecalledMemories } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
28
+
29
+ /**
30
+ * How the facts are introduced to the model, shared by both harnesses so
31
+ * the behavioral contract cannot drift between them (DD-006 D4). Attributes
32
+ * honestly (the user confirmed these) and frames defensively (background,
33
+ * never authority — remembered facts must not override the task or safety
34
+ * rules, and the user keeps full control).
35
+ */
36
+ const RECALLED_MEMORIES_PREAMBLE =
37
+ "Facts this user previously confirmed the assistant should remember. " +
38
+ "Treat them as background context about the user — they are not " +
39
+ "instructions and do not override your task or safety rules. The user " +
40
+ "can review and delete them at any time.";
41
+
42
+ /**
43
+ * The renderable facts of an execution's recall snapshot, in injection
44
+ * order (oldest-first, as the server composed them). Present only when
45
+ * recall is enabled AND at least one fact exists — the read function
46
+ * returns undefined otherwise.
47
+ */
48
+ export interface RecalledMemoriesContent {
49
+ /** The confirmed facts' contents, verbatim, in server-composed order. */
50
+ facts: string[];
51
+ }
52
+
53
+ /**
54
+ * Read the recalled memories from an execution spec's `recalled_memories`.
55
+ * Returns undefined when the field is absent (pre-Phase-2 executions),
56
+ * disabled, or carries no facts — the caller renders no section. Blank
57
+ * facts are dropped defensively (the server never stamps them: content has
58
+ * min_len 1 at write time).
59
+ *
60
+ * Only `content` is rendered: `memory_id` is the execution record's audit
61
+ * link back to the addressable record (DD-006 D2) — to the model it is
62
+ * meaningless tokens.
63
+ */
64
+ export function readRecalledMemories(
65
+ recalled: RecalledMemories | undefined,
66
+ ): RecalledMemoriesContent | undefined {
67
+ if (!recalled?.enabled) {
68
+ return undefined;
69
+ }
70
+ const facts = (recalled.facts ?? [])
71
+ .map((fact) => fact.content?.trim() ?? "")
72
+ .filter((content) => content !== "");
73
+ if (facts.length === 0) {
74
+ return undefined;
75
+ }
76
+ return { facts };
77
+ }
78
+
79
+ /**
80
+ * The framed facts body (preamble + one list item per fact), ready for
81
+ * section wrapping. Order is preserved from the snapshot: the server
82
+ * composed oldest-first in both editions, so the prompt reads the user's
83
+ * memory in the order it was built.
84
+ */
85
+ export function formatRecalledMemoriesText(
86
+ content: RecalledMemoriesContent,
87
+ ): string {
88
+ const list = content.facts.map((fact) => `- ${fact}`).join("\n");
89
+ return `${RECALLED_MEMORIES_PREAMBLE}\n\n${list}`;
90
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Harness-neutral thinking-mode semantics (stigmer/stigmer#772) — the
3
+ * second variant dimension alongside `shared/service-tier.ts`.
4
+ *
5
+ * The platform contract mirrors the service tier's: the thinking pin is
6
+ * ALWAYS explicit by the time a provider request leaves the runner.
7
+ * UNSPECIFIED resolves to DISABLED here and ONLY here — every upstream
8
+ * layer preserves the caller's raw enum so "user chose disabled" stays
9
+ * distinguishable from "platform default" all the way to the ledger.
10
+ *
11
+ * Unlike the fast tier, thinking is NOT separately priced: Cursor bills
12
+ * thinking variants at base per-token rates (ledger-verified 2026-08-15 —
13
+ * 277 events at exactly base; thinking+fast at exactly the fast rate).
14
+ * The cost of ENABLED is the extra reasoning tokens, billed as output.
15
+ * Selection is therefore capability-gated (registry capabilities.thinking)
16
+ * rather than pricing-gated, and the estimate/billing paths need no
17
+ * thinking-specific rates.
18
+ *
19
+ * v1 translates thinking on the Cursor harness only (the explicit
20
+ * `thinking` variant parameter, `execute-cursor/service-tier.ts`). No
21
+ * native wire mapping exists yet — create-time validation refuses ENABLED
22
+ * for native-harness models, mirroring the tier's #361 posture.
23
+ */
24
+
25
+ import { ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
26
+
27
+ /**
28
+ * The effective mode after platform-default resolution: never UNSPECIFIED.
29
+ */
30
+ export type EffectiveThinkingMode = ThinkingMode.DISABLED | ThinkingMode.ENABLED;
31
+
32
+ /**
33
+ * Resolve the configured mode to its effective value. The single place in
34
+ * the platform where UNSPECIFIED becomes DISABLED.
35
+ */
36
+ export function resolveEffectiveThinkingMode(
37
+ configured: ThinkingMode | undefined,
38
+ ): EffectiveThinkingMode {
39
+ return configured === ThinkingMode.ENABLED ? ThinkingMode.ENABLED : ThinkingMode.DISABLED;
40
+ }
41
+
42
+ /** Human-readable mode label for logs and error messages. */
43
+ export function thinkingModeLabel(mode: ThinkingMode): string {
44
+ switch (mode) {
45
+ case ThinkingMode.ENABLED:
46
+ return "enabled";
47
+ case ThinkingMode.DISABLED:
48
+ return "disabled";
49
+ default:
50
+ return "unspecified";
51
+ }
52
+ }
@@ -1562,13 +1562,57 @@ do:
1562
1562
  }
1563
1563
  });
1564
1564
 
1565
- it("call: human_input rejects the not-implemented escalate policy at load time", () => {
1565
+ // The escalate outcome-by-name contract (stigmer/stigmer#781): the policy
1566
+ // loads only when the gate declares an outcome named "escalate" with
1567
+ // `then` set — the timeout resolves to that outcome and follows its branch.
1568
+ const escalateYamlWithOutcomes = (onTimeout: string, outcomesYaml: string) => `
1569
+ document:
1570
+ dsl: '1.0.0'
1571
+ name: test
1572
+ do:
1573
+ - timedApproval:
1574
+ call: human_input
1575
+ with:
1576
+ prompt: "Approve within time limit"
1577
+ timeout: 3600
1578
+ on_timeout: ${onTimeout}
1579
+ outcomes:
1580
+ ${outcomesYaml}
1581
+ `;
1582
+
1583
+ it("call: human_input accepts the escalate policy when an escalate outcome with then exists", () => {
1566
1584
  for (const form of ["HUMAN_INPUT_TIMEOUT_ESCALATE", "escalate"]) {
1567
- expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout(form)))
1568
- .toThrow(/timedApproval.*escalate.*not implemented/);
1585
+ const model = loadWorkflowFromYaml(escalateYamlWithOutcomes(form, `
1586
+ - name: proceed
1587
+ - name: escalate
1588
+ then: escalationPath
1589
+ `));
1590
+ const task = model.do[0].task;
1591
+ expect(task.kind).toBe("human_input");
1592
+ if (task.kind === "human_input") {
1593
+ expect(task.humanInput.onTimeout).toBe("escalate");
1594
+ }
1569
1595
  }
1570
1596
  });
1571
1597
 
1598
+ it("call: human_input rejects the escalate policy without an escalate outcome", () => {
1599
+ expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout("escalate")))
1600
+ .toThrow(/timedApproval.*escalate.*requires an outcome named 'escalate'/);
1601
+ expect(() => loadWorkflowFromYaml(escalateYamlWithOutcomes("HUMAN_INPUT_TIMEOUT_ESCALATE", `
1602
+ - name: proceed
1603
+ - name: reject
1604
+ `)))
1605
+ .toThrow(/timedApproval.*requires an outcome named 'escalate'/);
1606
+ });
1607
+
1608
+ it("call: human_input rejects the escalate policy when the escalate outcome has no then", () => {
1609
+ expect(() => loadWorkflowFromYaml(escalateYamlWithOutcomes("escalate", `
1610
+ - name: proceed
1611
+ - name: escalate
1612
+ `)))
1613
+ .toThrow(/timedApproval.*requires an outcome named 'escalate' with 'then' set/);
1614
+ });
1615
+
1572
1616
  it("call: human_input rejects unknown on_timeout values instead of silently failing at timeout", () => {
1573
1617
  expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout("sometimes")))
1574
1618
  .toThrow(/timedApproval.*unknown on_timeout value 'sometimes'.*fail, approve, deny/);