@stigmer/runner 3.12.8 → 3.12.9

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 (66) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/fetch-interceptor.js +29 -16
  3. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  4. package/dist/activities/execute-cursor/index.d.ts +14 -0
  5. package/dist/activities/execute-cursor/index.js +55 -5
  6. package/dist/activities/execute-cursor/index.js.map +1 -1
  7. package/dist/activities/execute-cursor/message-translator.d.ts +15 -9
  8. package/dist/activities/execute-cursor/message-translator.js +15 -9
  9. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  10. package/dist/activities/execute-deep-agent/index.js +9 -0
  11. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  12. package/dist/activities/execute-deep-agent/setup.d.ts +10 -1
  13. package/dist/activities/execute-deep-agent/setup.js +20 -3
  14. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  15. package/dist/payload-codecs.d.ts +1 -1
  16. package/dist/payload-codecs.js +6 -4
  17. package/dist/payload-codecs.js.map +1 -1
  18. package/dist/shared/memory-retrieval.d.ts +126 -0
  19. package/dist/shared/memory-retrieval.js +293 -0
  20. package/dist/shared/memory-retrieval.js.map +1 -0
  21. package/dist/shared/runner-credential-keys.js +2 -1
  22. package/dist/shared/runner-credential-keys.js.map +1 -1
  23. package/package.json +3 -2
  24. package/src/__tests__/history-encryption-e2e.test.ts +1 -1
  25. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -1
  26. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +49 -15
  27. package/src/activities/execute-cursor/fetch-interceptor.ts +37 -16
  28. package/src/activities/execute-cursor/index.ts +58 -5
  29. package/src/activities/execute-cursor/message-translator.ts +15 -9
  30. package/src/activities/execute-deep-agent/index.ts +10 -0
  31. package/src/activities/execute-deep-agent/setup.ts +34 -6
  32. package/src/payload-codecs.ts +12 -9
  33. package/src/shared/__tests__/memory-retrieval.test.ts +310 -0
  34. package/src/shared/memory-retrieval.ts +383 -0
  35. package/src/shared/runner-credential-keys.ts +2 -1
  36. package/dist/claimcheck/compressor.d.ts +0 -2
  37. package/dist/claimcheck/compressor.js +0 -8
  38. package/dist/claimcheck/compressor.js.map +0 -1
  39. package/dist/claimcheck/config.d.ts +0 -7
  40. package/dist/claimcheck/config.js +0 -10
  41. package/dist/claimcheck/config.js.map +0 -1
  42. package/dist/claimcheck/index.d.ts +0 -3
  43. package/dist/claimcheck/index.js +0 -4
  44. package/dist/claimcheck/index.js.map +0 -1
  45. package/dist/claimcheck/payload-codec.d.ts +0 -23
  46. package/dist/claimcheck/payload-codec.js +0 -105
  47. package/dist/claimcheck/payload-codec.js.map +0 -1
  48. package/dist/encryption/config.d.ts +0 -64
  49. package/dist/encryption/config.js +0 -113
  50. package/dist/encryption/config.js.map +0 -1
  51. package/dist/encryption/index.d.ts +0 -3
  52. package/dist/encryption/index.js +0 -3
  53. package/dist/encryption/index.js.map +0 -1
  54. package/dist/encryption/payload-codec.d.ts +0 -41
  55. package/dist/encryption/payload-codec.js +0 -130
  56. package/dist/encryption/payload-codec.js.map +0 -1
  57. package/src/__tests__/claimcheck-codec.test.ts +0 -256
  58. package/src/__tests__/encryption-codec.test.ts +0 -287
  59. package/src/__tests__/fixtures/encrypted-payload-fixture.json +0 -15
  60. package/src/claimcheck/compressor.ts +0 -9
  61. package/src/claimcheck/config.ts +0 -20
  62. package/src/claimcheck/index.ts +0 -3
  63. package/src/claimcheck/payload-codec.ts +0 -139
  64. package/src/encryption/config.ts +0 -161
  65. package/src/encryption/index.ts +0 -3
  66. package/src/encryption/payload-codec.ts +0 -152
@@ -171,23 +171,26 @@ describe("fetch-interceptor", () => {
171
171
  });
172
172
  });
173
173
 
174
- describe("cursor_models_fetch timing", () => {
174
+ describe("timed REST path timing", () => {
175
175
  beforeEach(() => {
176
176
  installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, stigmerToken: STIGMER_TOKEN });
177
177
  });
178
178
 
179
179
  /**
180
- * Collect emitted `cursor_models_fetch` timing lines from a console.log
181
- * spy, ignoring every other log line (install banner, warnings, other
182
- * timelines).
180
+ * Collect emitted timing lines for one timeline event from a
181
+ * console.log spy, ignoring every other log line (install banner,
182
+ * warnings, other timelines).
183
183
  */
184
- function timingLines(spy: ReturnType<typeof vi.spyOn>): Array<Record<string, unknown>> {
184
+ function timingLines(
185
+ spy: ReturnType<typeof vi.spyOn>,
186
+ event: string,
187
+ ): Array<Record<string, unknown>> {
185
188
  const lines: Array<Record<string, unknown>> = [];
186
189
  for (const call of spy.mock.calls) {
187
190
  if (typeof call[0] !== "string") continue;
188
191
  try {
189
192
  const parsed = JSON.parse(call[0]) as Record<string, unknown>;
190
- if (parsed.stigmer_timing === "cursor_models_fetch") lines.push(parsed);
193
+ if (parsed.stigmer_timing === event) lines.push(parsed);
191
194
  } catch {
192
195
  // Not a JSON log line — ignore.
193
196
  }
@@ -195,7 +198,7 @@ describe("fetch-interceptor", () => {
195
198
  return lines;
196
199
  }
197
200
 
198
- it("emits one timing line for a Cursor-domain /v1/models call, carrying execution_id", async () => {
201
+ it("emits one cursor_models_fetch line for a Cursor-domain /v1/models call, carrying execution_id", async () => {
199
202
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
200
203
  const executionContext = getExecutionContext();
201
204
 
@@ -203,7 +206,7 @@ describe("fetch-interceptor", () => {
203
206
  await globalThis.fetch("https://api.cursor.com/v1/models", { method: "GET" });
204
207
  });
205
208
 
206
- const lines = timingLines(spy);
209
+ const lines = timingLines(spy, "cursor_models_fetch");
207
210
  expect(lines).toHaveLength(1);
208
211
  expect(lines[0]!.execution_id).toBe("exec-models-1");
209
212
  expect(lines[0]!.http_status).toBe(200);
@@ -212,16 +215,46 @@ describe("fetch-interceptor", () => {
212
215
  expect(segments.map((s) => s.name)).toEqual(["models_fetch"]);
213
216
  });
214
217
 
215
- it("emits for the proxy-endpoint-targeted /v1/models form too", async () => {
218
+ it("emits one cursor_token_exchange line for the SDK's token exchange, carrying execution_id (cloud#484)", async () => {
219
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
220
+ const executionContext = getExecutionContext();
221
+
222
+ await executionContext.run({ executionId: "exec-exchange-1" }, async () => {
223
+ await globalThis.fetch(
224
+ "https://api2.cursor.sh/auth/exchange_user_api_key",
225
+ { method: "POST" },
226
+ );
227
+ });
228
+
229
+ const lines = timingLines(spy, "cursor_token_exchange");
230
+ expect(lines).toHaveLength(1);
231
+ expect(lines[0]!.execution_id).toBe("exec-exchange-1");
232
+ expect(lines[0]!.http_status).toBe(200);
233
+ expect(lines[0]!.total_ms).toBeTypeOf("number");
234
+ const segments = lines[0]!.segments as Array<{ name: string }>;
235
+ expect(segments.map((s) => s.name)).toEqual(["token_exchange"]);
236
+ // The exchange emits ONLY its own timeline, never the models one.
237
+ expect(timingLines(spy, "cursor_models_fetch")).toHaveLength(0);
238
+ });
239
+
240
+ it("emits for the proxy-endpoint-targeted forms too", async () => {
216
241
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
217
242
 
218
243
  await globalThis.fetch(`${PROXY_ENDPOINT}/v1/models`, { method: "GET" });
244
+ await globalThis.fetch(
245
+ `${PROXY_ENDPOINT}/auth/exchange_user_api_key`,
246
+ { method: "POST" },
247
+ );
219
248
 
220
- // The fetch itself was rewritten through the proxy path AND timed.
249
+ // Both fetches were rewritten through the proxy path AND timed.
221
250
  expect(calls[0]!.url).toBe(
222
251
  `${PROXY_ENDPOINT}/v1/proxy/cursor/api.cursor.com/v1/models`,
223
252
  );
224
- expect(timingLines(spy)).toHaveLength(1);
253
+ expect(calls[1]!.url).toBe(
254
+ `${PROXY_ENDPOINT}/v1/proxy/cursor/api2.cursor.sh/auth/exchange_user_api_key`,
255
+ );
256
+ expect(timingLines(spy, "cursor_models_fetch")).toHaveLength(1);
257
+ expect(timingLines(spy, "cursor_token_exchange")).toHaveLength(1);
225
258
  });
226
259
 
227
260
  it("omits execution_id (rather than fabricating one) outside an execution context", async () => {
@@ -229,22 +262,23 @@ describe("fetch-interceptor", () => {
229
262
 
230
263
  await globalThis.fetch("https://api.cursor.com/v1/models", { method: "GET" });
231
264
 
232
- const lines = timingLines(spy);
265
+ const lines = timingLines(spy, "cursor_models_fetch");
233
266
  expect(lines).toHaveLength(1);
234
267
  // undefined context values are dropped by JSON.stringify.
235
268
  expect("execution_id" in lines[0]!).toBe(false);
236
269
  });
237
270
 
238
- it("does NOT emit for other rewritten REST paths", async () => {
271
+ it("does NOT emit for rewritten REST paths outside the timed set", async () => {
239
272
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
240
273
 
241
274
  await globalThis.fetch(
242
- "https://api2.cursor.sh/auth/exchange_user_api_key",
275
+ "https://api.cursor.com/v1/agents",
243
276
  { method: "POST" },
244
277
  );
245
278
 
246
279
  expect(calls).toHaveLength(1);
247
- expect(timingLines(spy)).toHaveLength(0);
280
+ expect(timingLines(spy, "cursor_models_fetch")).toHaveLength(0);
281
+ expect(timingLines(spy, "cursor_token_exchange")).toHaveLength(0);
248
282
  });
249
283
  });
250
284
 
@@ -19,15 +19,34 @@ import { AsyncLocalStorage } from "node:async_hooks";
19
19
 
20
20
  import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
21
21
 
22
+ /** One REST path's timing identity: the emitted timeline event and its
23
+ * single segment name. */
24
+ interface TimedRestPath {
25
+ readonly event: string;
26
+ readonly segment: string;
27
+ }
28
+
22
29
  /**
23
- * The one REST path worth timing individually: the Cursor SDK calls
24
- * GET /v1/models inside Agent.create/Agent.resume purely to validate the
25
- * model id, and in proxy mode that is a runner → Stigmer → Cursor double
26
- * hop sitting inside the resolve_agent setup segment (issue #209). The
27
- * emitted `cursor_models_fetch` timeline splits that network cost out of
28
- * the segment total without touching the SDK.
30
+ * REST paths worth timing individually both sit inside Agent.create/
31
+ * Agent.resume on the user-visible resolve_agent setup path, and in proxy
32
+ * mode each is a runner → Stigmer → Cursor double hop. The emitted
33
+ * timelines split that network cost out of the segment total without
34
+ * touching the SDK:
35
+ *
36
+ * - GET /v1/models (`cursor_models_fetch`): the SDK's model-id validation
37
+ * read, ~0.95s of the pre-cache resolve_agent segment (issue #209).
38
+ * - POST /auth/exchange_user_api_key (`cursor_token_exchange`): the SDK's
39
+ * API-key → access-token exchange, the strongest suspect for the
40
+ * remaining unexplained 0.6–1.3s inside Agent.create
41
+ * (stigmer-cloud#484 — this timeline is that issue's Step 1, measure).
29
42
  */
30
- const MODELS_PATH = "/v1/models";
43
+ const TIMED_REST_PATHS: ReadonlyMap<string, TimedRestPath> = new Map([
44
+ ["/v1/models", { event: "cursor_models_fetch", segment: "models_fetch" }],
45
+ [
46
+ "/auth/exchange_user_api_key",
47
+ { event: "cursor_token_exchange", segment: "token_exchange" },
48
+ ],
49
+ ]);
31
50
 
32
51
  const CURSOR_DOMAINS = [
33
52
  "api2.cursor.sh",
@@ -258,13 +277,14 @@ async function fetchWithUrlRewrite(
258
277
  const rewrittenUrl = rewriteUrl(url, config.proxyEndpoint);
259
278
  const rewrittenInit = replaceAuth(init, config);
260
279
  const path = extractPath(url);
261
- const modelsTiming = path === MODELS_PATH ? new TimingRecorder() : undefined;
280
+ const timedPath = TIMED_REST_PATHS.get(path);
281
+ const timing = timedPath ? new TimingRecorder() : undefined;
262
282
 
263
283
  try {
264
284
  const response = await originalFetch(rewrittenUrl, rewrittenInit);
265
285
 
266
- if (modelsTiming) {
267
- emitModelsFetchTiming(modelsTiming, config, response.status);
286
+ if (timedPath && timing) {
287
+ emitRestTiming(timedPath, timing, config, response.status);
268
288
  }
269
289
 
270
290
  if (!response.ok) {
@@ -290,22 +310,23 @@ async function fetchWithUrlRewrite(
290
310
  }
291
311
 
292
312
  /**
293
- * Emit the `cursor_models_fetch` timeline for one proxied GET /v1/models.
313
+ * Emit the timeline for one proxied REST call on a timed path.
294
314
  *
295
315
  * `execution_id` comes from the AsyncLocalStorage execution context — the
296
316
  * whole ExecuteCursor activity runs inside runWithExecutionContext, so the
297
317
  * value is correct even with concurrent activities on one runner process.
298
- * `recordTimingMetric` deliberately ignores this event (no mapped OTel
299
- * instrument): it is a forensic stdout line only, joined to the
318
+ * `recordTimingMetric` deliberately ignores these events (no mapped OTel
319
+ * instruments): they are forensic stdout lines only, joined to the
300
320
  * execution_setup timeline by execution_id in cold-start-baseline analysis.
301
321
  */
302
- function emitModelsFetchTiming(
322
+ function emitRestTiming(
323
+ timedPath: TimedRestPath,
303
324
  timing: TimingRecorder,
304
325
  config: ProxyConfig,
305
326
  httpStatus: number,
306
327
  ): void {
307
- timing.mark("models_fetch");
308
- emitTimingLog("cursor_models_fetch", {
328
+ timing.mark(timedPath.segment);
329
+ emitTimingLog(timedPath.event, {
309
330
  execution_id: executionContext.getStore()?.executionId ?? config.executionId,
310
331
  http_status: httpStatus,
311
332
  }, timing);
@@ -57,7 +57,8 @@ import {
57
57
  } from "../../shared/caller-identity.js";
58
58
  import { readSessionContext } from "../../shared/session-context.js";
59
59
  import { readDeclaredPreferences } from "../../shared/declared-preferences.js";
60
- import { readRecalledMemories } from "../../shared/recalled-memories.js";
60
+ import type { RecalledMemoriesContent } from "../../shared/recalled-memories.js";
61
+ import { selectRecalledFacts } from "../../shared/memory-retrieval.js";
61
62
  import { withholdSecretContentFromMessages } from "../../shared/tool-row.js";
62
63
  import { StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
63
64
  import { resolveUsableArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
@@ -1182,6 +1183,38 @@ async function executeCursorInner(
1182
1183
  const structuredOutputSchema = spec.executionConfig?.structuredOutputSchema as
1183
1184
  Record<string, unknown> | undefined;
1184
1185
 
1186
+ // Phase 9c: Semantic memory selection (DD-008), memoized to at most one
1187
+ // run per invocation. Deliberately NOT decided by the Phase-10
1188
+ // resolution alone: a resumed-agent primary send carries no memories,
1189
+ // but a mid-send poisoned-handle failure rebuilds on a FRESH agent
1190
+ // whose recovery prompt does — the buildFromPlan drop-hazard class —
1191
+ // so every memory-carrying build site awaits this lazily instead.
1192
+ // Selection runs against the frozen first message's semantics: above
1193
+ // the activation threshold it picks top-k for spec.message, otherwise
1194
+ // (and on any failure) it injects the full candidate set — Phase 2
1195
+ // behavior. The outcome report is stamped on the turn's status ONCE,
1196
+ // picked up by the next persist; a re-invocation replays the report
1197
+ // already persisted on the execution rather than re-selecting (the
1198
+ // written-once rule).
1199
+ let memorySelection: Promise<RecalledMemoriesContent | undefined> | undefined;
1200
+ const selectMemoriesOnce = (): Promise<RecalledMemoriesContent | undefined> => {
1201
+ memorySelection ??= selectRecalledFacts(spec.recalledMemories, spec.message, {
1202
+ proxyEndpoint: config.proxyEndpoint,
1203
+ stigmerToken: config.stigmerToken,
1204
+ executionId,
1205
+ priorReport: execution.status?.recalledMemoriesReport,
1206
+ }).then((selection) => {
1207
+ if (selection.report !== undefined) {
1208
+ status.recalledMemoriesReport = selection.report;
1209
+ }
1210
+ return selection.content;
1211
+ });
1212
+ return memorySelection;
1213
+ };
1214
+ const recalledMemories = promptCarriesStandingContext(resolution.reason)
1215
+ ? await selectMemoriesOnce()
1216
+ : undefined;
1217
+
1185
1218
  // Phase 10: Build the prompt
1186
1219
  const interactionMode = spec.executionConfig?.interactionMode
1187
1220
  ?? InteractionMode.UNSPECIFIED;
@@ -1208,7 +1241,7 @@ async function executeCursorInner(
1208
1241
  senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
1209
1242
  sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
1210
1243
  declaredPreferences: readDeclaredPreferences(spec.declaredPreferences),
1211
- recalledMemories: readRecalledMemories(spec.recalledMemories),
1244
+ recalledMemories,
1212
1245
  conversationCatchup: readConversationCatchup(spec.conversationCatchup),
1213
1246
  // The turn's recorded transcript, seeded from the persisted execution
1214
1247
  // on a reinvocation (Phase 3). Consumed only by the HITL-recovery
@@ -1916,7 +1949,10 @@ async function executeCursorInner(
1916
1949
  senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
1917
1950
  sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
1918
1951
  declaredPreferences: readDeclaredPreferences(spec.declaredPreferences),
1919
- recalledMemories: readRecalledMemories(spec.recalledMemories),
1952
+ // Lazily selected: the primary send may have been a resumed-agent
1953
+ // shape that carried no memories, but this fresh agent's prompt
1954
+ // must (see the Phase 9c memoized selection).
1955
+ recalledMemories: await selectMemoriesOnce(),
1920
1956
  conversationCatchup: readConversationCatchup(spec.conversationCatchup),
1921
1957
  // Composed fresh (not reused from Phase 10): the failed primary
1922
1958
  // stream may have appended partial work onto status.messages,
@@ -2606,6 +2642,23 @@ export function primarySendCarriesImages(
2606
2642
  return !(isHitlReinvocation(approvalDecisions) && reason === "resumed_successfully");
2607
2643
  }
2608
2644
 
2645
+ /**
2646
+ * Whether a prompt built for this resolution carries the STANDING context —
2647
+ * instructions, skills, declared preferences, recalled memories, session
2648
+ * context. Exactly one resolution shape does not: a successfully RESUMED
2649
+ * agent, whose native conversation already holds the first message's
2650
+ * context (both its prompt shapes — the raw follow-up and the
2651
+ * decisions-only HITL reinvocation — send no standing sections).
2652
+ *
2653
+ * The named authority for that routing property (the
2654
+ * primarySendCarriesImages idiom): buildPrompt's internal routing and the
2655
+ * activity's standing-context preparation (e.g. the memory selection gate)
2656
+ * both consult THIS predicate, so the two can never drift.
2657
+ */
2658
+ export function promptCarriesStandingContext(reason: AgentResolutionReason): boolean {
2659
+ return reason !== "resumed_successfully";
2660
+ }
2661
+
2609
2662
  /**
2610
2663
  * Append the structured-output contract to a prompt when the execution
2611
2664
  * requests one. A per-turn directive (the buildFromPlan rule): it must ride
@@ -2647,7 +2700,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2647
2700
  // an empty conversation strand the agent with instructions and no story,
2648
2701
  // and the session inherits that amnesia permanently (issue #366).
2649
2702
  if (isHitlReinvocation(approvalDecisions)) {
2650
- if (resolution.reason !== "resumed_successfully") {
2703
+ if (promptCarriesStandingContext(resolution.reason)) {
2651
2704
  return buildHitlRecoveryPrompt(
2652
2705
  {
2653
2706
  instructions,
@@ -2698,7 +2751,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2698
2751
  // cloud DD-006). Catchup last: it is context, and context sits closest to
2699
2752
  // the task (the enhanced prompt's own ordering doctrine); the input files
2700
2753
  // precede it because they are this turn's payload, not background.
2701
- if (resolution.reason === "resumed_successfully") {
2754
+ if (!promptCarriesStandingContext(resolution.reason)) {
2702
2755
  const prefixes = [
2703
2756
  formatInteractionModePrefix(interactionMode),
2704
2757
  formatImplementPlanSection(buildFromPlan, attachments),
@@ -501,15 +501,21 @@ function blockText(b: Record<string, unknown>): string | undefined {
501
501
  *
502
502
  * The Cursor SDK returns sub-agent work as a blob in the task tool's
503
503
  * completed event (not as streaming events with a distinct agent_id).
504
- * Re-verified 2026-07-02 with live recordings on both the pinned SDK (1.0.13)
505
- * and the latest (1.0.22): zero events reach the parent's run.stream() between
506
- * the task tool's "running" and "completed" events, every event carries the
507
- * parent's agent_id, and the child agentId visible in the task args at spawn
508
- * is NOT queryable mid-run through any public read surface (Agent.listRuns /
509
- * Agent.messages.list / Agent.getRun all return not-found for it; the SDK's
510
- * on-disk sub-agent transcript is written only at completion). Live nested
511
- * visibility is therefore an upstream SDK limitation — do not try to fake it
512
- * here; the UI shows an elapsed-time affordance instead (SubAgentSection).
504
+ * Re-verified 2026-08-23 on the pinned SDK (1.0.13) AND the latest (1.0.28),
505
+ * this time including the write side (stigmer/stigmer#839): an injected
506
+ * platform.eventStore/eventNotifier receives parent events only on 1.0.13 and
507
+ * is bypassed entirely on 1.0.28 (persistence moved into the executor daemon's
508
+ * own state dir); zero events reach the parent's run.stream() mid-task; the
509
+ * agentId in the task args is not a platform-store agent id at all, so it can
510
+ * never be addressable via Agent.listRuns / Agent.getRun (this is WHY the
511
+ * 2026-07-02 read-side polls all returned not-found). On 1.0.28 the child
512
+ * transcript lands at a deterministic path knowable at spawn
513
+ * (~/.cursor/projects/<slug>/agent-transcripts/<parentId>/subagents/<argsAgentId>.jsonl)
514
+ * but is flushed only at completion — verified with 200ms sampling across a
515
+ * full sub-agent run. Live nested visibility is therefore still an upstream
516
+ * SDK limitation — do not try to fake it here; the UI shows an elapsed-time
517
+ * affordance instead (SubAgentSection). Probe scripts and recordings:
518
+ * stigmer-cloud _projects/2026-08/20260823.03.cursor-subagent-live-progress.
513
519
  * The result shape is:
514
520
  *
515
521
  * { status: "success", value: { conversationSteps: ConversationStep[] } }
@@ -180,6 +180,16 @@ export function createDeepAgentActivities(config: Config) {
180
180
  ? seedStatusFromExecution(setup.execution)
181
181
  : create(AgentExecutionStatusSchema, {});
182
182
 
183
+ // The semantic retriever's injection outcome (DD-008 D5), computed at
184
+ // prompt build in setup Step 8. Stamped here — before the first
185
+ // persist — because setup predates this status object; the server's
186
+ // presence-guarded merge preserves it across report-less writes. On a
187
+ // re-invocation the seeded status already carries the (replayed)
188
+ // report, so this stamp is idempotent by construction.
189
+ if (setup.recalledMemoriesReport !== undefined) {
190
+ initialStatus.recalledMemoriesReport = setup.recalledMemoriesReport;
191
+ }
192
+
183
193
  const statusBuilder = new StatusBuilder(executionId, initialStatus);
184
194
 
185
195
  statusBuilder.setApprovalProvider({
@@ -15,7 +15,7 @@ import { z } from "zod";
15
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
16
  type AgentGraph = any;
17
17
 
18
- import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
18
+ import type { AgentExecution, RecalledMemoriesReport } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
19
19
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
20
20
  import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
21
21
  import type { DynamicStructuredTool } from "@langchain/core/tools";
@@ -33,7 +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
+ import { selectRecalledFacts } from "../../shared/memory-retrieval.js";
37
37
  import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-manager.js";
38
38
  import { mergeMcpServerUsages, resolveMcpServers } from "../../shared/mcp-resolver.js";
39
39
  import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
@@ -201,6 +201,15 @@ export interface SetupResult {
201
201
  * baseline/candidate/reconcile seam (`capture.ts`) and the CAS capture-class.
202
202
  */
203
203
  readonly gitWorkspace: boolean;
204
+ /**
205
+ * The semantic retriever's injection outcome (DD-008 D5), produced at
206
+ * prompt build (Step 8) — the one place selection runs. The activity
207
+ * stamps it onto the turn's status proto before the first persist; the
208
+ * server's presence-guarded merge preserves it across report-less
209
+ * writes. Undefined when nothing was injected (recall absent, disabled,
210
+ * or empty) — absent report = wholesale, true by construction.
211
+ */
212
+ readonly recalledMemoriesReport: RecalledMemoriesReport | undefined;
204
213
  }
205
214
 
206
215
  export interface SetupDependencies {
@@ -590,7 +599,27 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
590
599
  }
591
600
  timing.mark("inject_attachments");
592
601
 
593
- // Step 8: Build enhanced system prompt
602
+ // Step 8: Build enhanced system prompt.
603
+ //
604
+ // Recalled memories go through the semantic retriever (DD-008), not the
605
+ // raw snapshot read: above the activation threshold it selects the
606
+ // top-k facts for THIS turn's message (one batched embeddings call);
607
+ // otherwise — and on any failure — it injects the full candidate set,
608
+ // exactly the Phase 2 behavior. A re-invocation of this execution
609
+ // (approval resume) replays the outcome recorded on the execution's
610
+ // status instead of re-selecting, so the prompt never shifts
611
+ // mid-execution. The report returned here is stamped onto the turn's
612
+ // status by the activity (index.ts) — setup has no status object yet.
613
+ const memorySelection = await selectRecalledFacts(
614
+ execution.spec!.recalledMemories,
615
+ execution.spec!.message,
616
+ {
617
+ proxyEndpoint: config.proxyEndpoint,
618
+ stigmerToken: config.stigmerToken,
619
+ executionId,
620
+ priorReport: execution.status?.recalledMemoriesReport,
621
+ },
622
+ );
594
623
  const systemPrompt = buildEnhancedSystemPrompt({
595
624
  instructions,
596
625
  provisionResults,
@@ -614,9 +643,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
614
643
  declaredPreferences: readDeclaredPreferences(
615
644
  execution.spec!.declaredPreferences,
616
645
  ),
617
- recalledMemories: readRecalledMemories(
618
- execution.spec!.recalledMemories,
619
- ),
646
+ recalledMemories: memorySelection.content,
620
647
  });
621
648
 
622
649
  // Step 9: Construct the LLM model. Resolution to the provider API id
@@ -964,6 +991,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
964
991
  casObserver,
965
992
  captureMode,
966
993
  gitWorkspace,
994
+ recalledMemoriesReport: memorySelection.report,
967
995
  };
968
996
  } catch (err) {
969
997
  if (mcpConnection) {
@@ -12,9 +12,9 @@
12
12
  * decode: claim-check → decrypt (restore the blob, then decrypt)
13
13
  */
14
14
 
15
+ import type { BootstrapKeyMaterial } from "@stigmer/temporal-codecs";
15
16
  import type { PayloadCodec } from "@temporalio/common";
16
17
  import type { Config } from "./config.js";
17
- import type { BootstrapKeyMaterial } from "./encryption/config.js";
18
18
  import { getRunnerSecret } from "./shared/runner-credential-store.js";
19
19
 
20
20
  export async function createPayloadCodecs(
@@ -23,12 +23,18 @@ export async function createPayloadCodecs(
23
23
  ): Promise<PayloadCodec[] | undefined> {
24
24
  const codecs: PayloadCodec[] = [];
25
25
 
26
- const { loadPayloadEncryptionConfig, EncryptionPayloadCodec } = await import(
27
- "./encryption/index.js"
28
- );
26
+ const {
27
+ loadPayloadEncryptionConfig,
28
+ EncryptionPayloadCodec,
29
+ loadClaimcheckConfig,
30
+ ClaimcheckPayloadCodec,
31
+ } = await import("@stigmer/temporal-codecs");
29
32
  // Env-configured keys win outright; server-managed (bootstrap) keys apply
30
- // only when the env is silent — see encryption/config.ts for the rationale.
31
- const encryptionConfig = loadPayloadEncryptionConfig(bootstrapKeys);
33
+ // only when the env is silent — see the lib's encryption/config.ts for the
34
+ // rationale. Key VALUES resolve through getRunnerSecret (the #508 boot
35
+ // capture moves them out of process.env — agent shells must not read
36
+ // them), which is why the loader takes the reader as an argument.
37
+ const encryptionConfig = loadPayloadEncryptionConfig(getRunnerSecret, bootstrapKeys);
32
38
  if (encryptionConfig) {
33
39
  codecs.push(new EncryptionPayloadCodec(encryptionConfig));
34
40
  const source = getRunnerSecret("STIGMER_PAYLOAD_ENCRYPTION_KEY") ? "env" : "bootstrap";
@@ -41,9 +47,6 @@ export async function createPayloadCodecs(
41
47
  );
42
48
  }
43
49
 
44
- const { loadClaimcheckConfig, ClaimcheckPayloadCodec } = await import(
45
- "./claimcheck/index.js"
46
- );
47
50
  const claimcheckConfig = loadClaimcheckConfig();
48
51
  if (claimcheckConfig.enabled) {
49
52
  const { loadArtifactStorageConfig, createArtifactStorage } = await import(