agenr 3.2.0 → 2026.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +104 -3
  2. package/README.md +40 -42
  3. package/dist/adapters/skeln/index.d.ts +629 -51
  4. package/dist/adapters/skeln/index.js +782 -2039
  5. package/dist/build-before-turn-artifact-NPUHVWFE.js +71 -0
  6. package/dist/build-recall-artifact-F3LS3PZX.js +62 -0
  7. package/dist/chunk-5AYIXQRF.js +4452 -0
  8. package/dist/{chunk-FMQTRTWE.js → chunk-5TIP2EPP.js} +1453 -2563
  9. package/dist/{chunk-5LADPJ4C.js → chunk-GAERET5Q.js} +138 -504
  10. package/dist/chunk-GF3PX3VM.js +41 -0
  11. package/dist/chunk-GKZQ5AG5.js +44 -0
  12. package/dist/chunk-LDJN7CVU.js +3231 -0
  13. package/dist/{chunk-ZYADFKX3.js → chunk-MC3C2XM5.js} +34 -1
  14. package/dist/{chunk-KL6X2E3I.js → chunk-NBS62ES5.js} +1168 -816
  15. package/dist/chunk-NSLTJBUC.js +270 -0
  16. package/dist/chunk-OJSIZDZD.js +9 -0
  17. package/dist/chunk-OWGQWQUP.js +45 -0
  18. package/dist/{chunk-ZAX3YSTU.js → chunk-Q5UTJXHZ.js} +2 -140
  19. package/dist/{chunk-TMDNFBBC.js → chunk-SOQW7356.js} +271 -1935
  20. package/dist/chunk-VBPYU7GO.js +597 -0
  21. package/dist/chunk-VTHBPXDQ.js +1750 -0
  22. package/dist/{chunk-UEGURBBW.js → chunk-XFJ4S4G2.js} +844 -39
  23. package/dist/chunk-Y5NB3FTH.js +106 -0
  24. package/dist/{chunk-6HY5F5FE.js → chunk-ZX55JBV2.js} +1704 -314
  25. package/dist/cli.js +1125 -9537
  26. package/dist/core/recall/index.d.ts +64 -6
  27. package/dist/core/recall/index.js +8 -1
  28. package/dist/internal-eval-server.js +9 -6
  29. package/dist/internal-recall-eval-server.js +9 -6
  30. package/dist/lifecycle-checkpoint-IAC5FCQU.js +154 -0
  31. package/dist/{ports-CpzWESmZ.d.ts → ports-C4QkwDBS.d.ts} +114 -69
  32. package/dist/scan-6JKPOQHD.js +6 -0
  33. package/dist/service-EKFACEN6.js +15 -0
  34. package/dist/service-RHNB5AEQ.js +861 -0
  35. package/dist/sink-AUAAWC5O.js +8 -0
  36. package/package.json +7 -4
  37. package/dist/adapters/openclaw/index.d.ts +0 -32
  38. package/dist/adapters/openclaw/index.js +0 -3112
  39. /package/dist/{chunk-GELCEVFA.js → chunk-5AXMFBHR.js} +0 -0
@@ -1,70 +1,95 @@
1
1
  import {
2
- CLOSE_EVENT_HISTORY_LIMIT,
2
+ readGoalGeneration,
3
+ shouldInjectWorkingContext,
4
+ toWorkingContextAuditPointer
5
+ } from "../../chunk-OWGQWQUP.js";
6
+ import {
3
7
  EPISODE_SUMMARY_TIMEOUT_MS,
4
8
  FETCH_TOOL_PARAMETERS,
5
9
  RECALL_TOOL_PARAMETERS,
6
- SESSION_LINEAGE_REASONS,
7
10
  STORE_TOOL_PARAMETERS,
8
11
  UPDATE_TOOL_PARAMETERS,
9
- WORKING_CANDIDATE_PROMOTION_STATUSES,
12
+ buildCompactionSourceRef,
10
13
  buildRecallToolServices,
14
+ buildSessionFileSourceRef,
11
15
  composeHostPluginServices,
12
16
  createClaimExtractionFromAgenrConfig,
17
+ createCompactionPromptTracker,
13
18
  createDeadlineAwareEpisodeSummaryLlm,
19
+ createHostMemoryServices,
20
+ createSessionLifecycleIntakeTracker,
21
+ createSessionMemoryTriggerRouter,
14
22
  createSessionStartTracker,
15
23
  embedEpisodeSummaryWithinBudget,
16
24
  extractRecentTurnsFromMessages,
17
25
  formatAgenrSessionStartRecall,
18
26
  formatUnifiedRecallResults,
19
- isCloseManagedStatus,
20
- isModelVisibleOperationType,
21
- isMutableWorkingSetStatus,
22
- isTrustedHostMutationSource,
23
- isTrustedHostOnlyWorkingOperation,
24
- normalizeEventLimit,
25
- normalizeListLimit,
26
- normalizeOptionalString,
27
+ isPluginEpisodeWriteEnabled,
28
+ logSessionMemoryTriggerResult,
29
+ maybeRunLightDream,
30
+ mergeInjectionContent,
27
31
  normalizePluginInjectionMemoryPolicyConfig,
28
32
  normalizePromptText,
29
33
  parseFetchToolParams,
30
34
  parseRecallToolParams,
31
35
  parseStoreToolParams,
32
36
  parseUpdateToolParams,
37
+ resolveAgenrFeatureFlags,
33
38
  resolveBeforeTurnPolicy,
39
+ resolveCompactionPromptContext,
34
40
  resolveSessionIdentityKey,
35
41
  resolveSessionStartPolicy,
36
42
  resolveWorkingContextGate,
43
+ routeSessionMemoryTriggerSafely,
37
44
  runFetchMemoryTool,
38
45
  runRecallMemoryTool,
39
- runSessionStart,
40
46
  runStoreMemoryTool,
41
47
  runUpdateMemoryTool,
48
+ scheduleGuardedEpisodeWrite,
42
49
  writeBoundedSingleTranscriptEpisode
43
- } from "../../chunk-KL6X2E3I.js";
50
+ } from "../../chunk-NBS62ES5.js";
51
+ import "../../chunk-OJSIZDZD.js";
52
+ import {
53
+ WORKING_CANDIDATE_PROMOTION_STATUSES,
54
+ isModelVisibleOperationType
55
+ } from "../../chunk-NSLTJBUC.js";
44
56
  import {
57
+ MEMORY_DOCTRINE,
58
+ MEMORY_RECALL_SECTION_HEADER,
45
59
  asRecord,
60
+ buildSkelnRecallToolDescription,
61
+ buildSkelnRecallToolGuidelines,
62
+ buildSkelnStoreToolDescription,
63
+ buildStoreToolGuidelines,
64
+ buildUpdateToolDescription,
65
+ buildUpdateToolGuidelines,
46
66
  createSingleTranscriptDiscoveryPort,
47
67
  formatErrorMessage,
48
68
  formatTargetSelector,
49
69
  sanitizeFetchToolParams,
50
70
  sanitizeUpdateToolParams
51
- } from "../../chunk-TMDNFBBC.js";
71
+ } from "../../chunk-LDJN7CVU.js";
52
72
  import {
53
73
  formatAgenrBeforeTurnRecall,
54
- runBeforeTurn
55
- } from "../../chunk-UEGURBBW.js";
74
+ runBeforeTurn,
75
+ runSessionStart
76
+ } from "../../chunk-XFJ4S4G2.js";
56
77
  import {
57
- AGENR_FEATURE_FLAG_KEYS,
58
- DEFAULT_AGENR_FEATURE_FLAGS,
59
78
  buildRecallToolDetails,
60
79
  createLlmClient,
61
- isRecord,
62
- readOptionalFiniteNumber,
63
- readOptionalTrimmedString,
64
80
  resolveLlmApiKey,
65
81
  resolveModel
66
- } from "../../chunk-FMQTRTWE.js";
67
- import "../../chunk-5LADPJ4C.js";
82
+ } from "../../chunk-5TIP2EPP.js";
83
+ import "../../chunk-GAERET5Q.js";
84
+ import {
85
+ withEpisodeWriteGuard
86
+ } from "../../chunk-SOQW7356.js";
87
+ import {
88
+ isRecord,
89
+ readOptionalFiniteNumber,
90
+ readOptionalTrimmedString
91
+ } from "../../chunk-VTHBPXDQ.js";
92
+ import "../../chunk-VBPYU7GO.js";
68
93
 
69
94
  // src/adapters/skeln/config.ts
70
95
  function parseSkelnMemoryPolicyJson(raw) {
@@ -101,14 +126,16 @@ function mergeSkelnMemoryPolicy(fromSettings, fromOptions) {
101
126
  const sessionStart = fromSettings.sessionStart || fromOptions.sessionStart ? { ...fromSettings.sessionStart, ...fromOptions.sessionStart } : void 0;
102
127
  const beforeTurn = fromSettings.beforeTurn || fromOptions.beforeTurn ? { ...fromSettings.beforeTurn, ...fromOptions.beforeTurn } : void 0;
103
128
  const workingContext = fromSettings.workingContext || fromOptions.workingContext ? { ...fromSettings.workingContext, ...fromOptions.workingContext } : void 0;
104
- if (!slotPolicies && !sessionStart && !beforeTurn && !workingContext) {
129
+ const episodes = fromSettings.episodes || fromOptions.episodes ? { ...fromSettings.episodes, ...fromOptions.episodes } : void 0;
130
+ if (!slotPolicies && !sessionStart && !beforeTurn && !workingContext && !episodes) {
105
131
  return void 0;
106
132
  }
107
133
  return {
108
134
  ...slotPolicies ? { slotPolicies } : {},
109
135
  ...sessionStart ? { sessionStart } : {},
110
136
  ...beforeTurn ? { beforeTurn } : {},
111
- ...workingContext ? { workingContext } : {}
137
+ ...workingContext ? { workingContext } : {},
138
+ ...episodes ? { episodes } : {}
112
139
  };
113
140
  }
114
141
  function mergeSlotPolicies(fromSettings, fromOptions) {
@@ -128,183 +155,6 @@ function mergeSlotPolicies(fromSettings, fromOptions) {
128
155
  return { attributeHeads };
129
156
  }
130
157
 
131
- // src/app/working-memory/projection-render.ts
132
- var UTF8_BYTE_LENGTH = new TextEncoder();
133
- function byteLength(content) {
134
- return UTF8_BYTE_LENGTH.encode(content).length;
135
- }
136
- function createWorkingContextStubProjection(input) {
137
- const content = [
138
- "<agenr_work_context>",
139
- "Working memory is unavailable for this turn.",
140
- `Reason: ${input.reason}`,
141
- "Treat this as transient task-state metadata, not durable truth.",
142
- "</agenr_work_context>"
143
- ].join("\n");
144
- return {
145
- kind: "working_set",
146
- renderMode: "stub",
147
- content,
148
- ...input.workingSetId ? { workingSetId: input.workingSetId } : {},
149
- ...input.revision !== void 0 ? { revision: input.revision } : {},
150
- sourceRef: input.sourceRef,
151
- byteLength: byteLength(content)
152
- };
153
- }
154
- function createWorkingContextFullProjection(workingSet, sourceRef) {
155
- const snapshot = workingSet.snapshot;
156
- const lines = [
157
- "<agenr_work_context>",
158
- "This is transient working memory for the current task, not durable truth.",
159
- "It may be stale or hypothetical. Prefer current filesystem, git, tests, tool output, and the user's latest message for current-state claims.",
160
- "",
161
- `Scope: ${escapeText(workingSet.scopeKind)} ${escapeText(workingSet.scopeKey)}`,
162
- `Working set: ${escapeText(workingSet.id)}`,
163
- `Revision: ${workingSet.revision}`,
164
- `Status: ${escapeText(workingSet.status)}`,
165
- ...optionalLine("Objective", snapshot.objective ?? workingSet.title),
166
- ...optionalLine("Summary", snapshot.summary),
167
- ...renderStringList("Current plan", snapshot.currentPlan),
168
- ...renderCheckpoint(snapshot.checkpoint?.summary),
169
- ...renderNextActions(snapshot.nextActions),
170
- ...renderLabeledNotes(
171
- "Touched files",
172
- snapshot.files,
173
- (file) => file.path.trim().length > 0,
174
- (file) => {
175
- const note = file.note ? ` - ${escapeText(file.note)}` : "";
176
- return `${escapeText(file.path)}${note}`;
177
- }
178
- ),
179
- ...renderLabeledNotes(
180
- "Recent commands",
181
- snapshot.commands,
182
- (command) => command.command.trim().length > 0,
183
- (command) => {
184
- const outcome = command.outcome ? ` - ${escapeText(command.outcome)}` : "";
185
- return `${escapeText(command.command)}${outcome}`;
186
- }
187
- ),
188
- ...renderLabeledNotes(
189
- "Decisions",
190
- snapshot.decisions,
191
- (decision) => decision.decision.trim().length > 0,
192
- (decision) => {
193
- const rationale = decision.rationale ? ` - ${escapeText(decision.rationale)}` : "";
194
- return `${escapeText(decision.decision)}${rationale}`;
195
- }
196
- ),
197
- ...renderLabeledNotes(
198
- "Assumptions",
199
- snapshot.assumptions,
200
- (assumption) => assumption.assumption.trim().length > 0,
201
- (assumption) => {
202
- const confidence = assumption.confidence ? ` [${assumption.confidence}]` : "";
203
- const validated = assumption.validated === true ? " (validated)" : assumption.validated === false ? " (unvalidated)" : "";
204
- return `${escapeText(assumption.assumption)}${confidence}${validated}`;
205
- }
206
- ),
207
- ...renderStringList("Blockers", snapshot.blockers),
208
- ...renderStringList("Open questions", snapshot.openQuestions),
209
- ...renderCandidates(snapshot.candidates),
210
- "",
211
- "Rules:",
212
- "- Update this working set when material task state changes.",
213
- "- Leave a checkpoint before pausing, handing off, compacting, forking, or waiting.",
214
- "- Do not close this working set; only the user clears goals with /goal clear.",
215
- "- Do not store transient WIP with agenr_store.",
216
- "- Promote only durable facts, decisions, preferences, or reusable procedures explicitly.",
217
- "</agenr_work_context>"
218
- ];
219
- const content = lines.join("\n");
220
- return {
221
- kind: "working_set",
222
- renderMode: "full",
223
- content,
224
- workingSetId: workingSet.id,
225
- revision: workingSet.revision,
226
- sourceRef,
227
- byteLength: byteLength(content)
228
- };
229
- }
230
- function escapeText(value) {
231
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
232
- }
233
- function optionalLine(label, value) {
234
- const normalized = value?.trim();
235
- return normalized ? [`${label}: ${escapeText(normalized)}`] : [];
236
- }
237
- function renderStringList(title, values) {
238
- const items = (values ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
239
- if (items.length === 0) {
240
- return [];
241
- }
242
- return ["", `${title}:`, ...items.map((item) => `- ${escapeText(item)}`)];
243
- }
244
- function renderCheckpoint(summary) {
245
- const normalized = summary?.trim();
246
- return normalized ? ["", "Last checkpoint:", escapeText(normalized)] : [];
247
- }
248
- function renderLabeledNotes(title, items, isValid, formatLine) {
249
- const filtered = (items ?? []).filter(isValid);
250
- if (filtered.length === 0) {
251
- return [];
252
- }
253
- return ["", `${title}:`, ...filtered.map((item) => `- ${formatLine(item)}`)];
254
- }
255
- function renderNextActions(actions) {
256
- const items = (actions ?? []).filter((action) => action.text.trim().length > 0);
257
- if (items.length === 0) {
258
- return [];
259
- }
260
- return [
261
- "",
262
- "Next actions:",
263
- ...items.map((action) => {
264
- const status = action.status ? ` [${action.status}]` : "";
265
- const ref = action.ref ? ` (${escapeText(action.ref)})` : "";
266
- return `- ${escapeText(action.text)}${status}${ref}`;
267
- })
268
- ];
269
- }
270
- function renderCandidates(candidates) {
271
- const pending = (candidates ?? []).filter((candidate) => candidate.promotionStatus === "pending");
272
- if (pending.length === 0) {
273
- return [];
274
- }
275
- return [
276
- "",
277
- "Pending memory candidates:",
278
- ...pending.map((candidate) => {
279
- if (candidate.kind === "episodic") {
280
- return `- episodic: ${escapeText(candidate.summary)}`;
281
- }
282
- return `- ${candidate.kind}: ${escapeText(candidate.subject)}`;
283
- })
284
- ];
285
- }
286
-
287
- // src/app/working-memory/projection.ts
288
- function createToolSuccessProjection(workingSet, action, timestamp) {
289
- return createWorkingContextFullProjection(workingSet, `agenr_work:${action}:${timestamp}`);
290
- }
291
- function shouldInjectWorkingContext(projection) {
292
- return projection.renderMode === "full" && projection.content.trim().length > 0;
293
- }
294
- function toWorkingContextAuditPointer(projection) {
295
- if (projection.workingSetId === void 0 || projection.revision === void 0) {
296
- return void 0;
297
- }
298
- return {
299
- source: "agenr_work",
300
- workingSetId: projection.workingSetId,
301
- revision: projection.revision,
302
- sourceRef: projection.sourceRef,
303
- bytes: projection.byteLength,
304
- summary: projection.renderMode === "full" ? `Working set ${projection.workingSetId} rev ${projection.revision}` : `Working memory stub (${projection.sourceRef})`
305
- };
306
- }
307
-
308
158
  // src/adapters/skeln/session/scope.ts
309
159
  function resolveSkelnSessionKey(sessionId, cwd) {
310
160
  const normalizedSessionId = sessionId.trim();
@@ -427,13 +277,14 @@ function isSkelnMessageEntry(entry) {
427
277
  // src/adapters/skeln/format/prompt-section.ts
428
278
  function buildAgenrSkelnMemoryPromptSection() {
429
279
  return [
430
- "## Memory Recall",
431
- "Before answering anything about prior work, decisions, preferences, people, dates, unfinished work, or past sessions, call agenr_recall first. Session-start recall is automatic, and conservative before-turn recall may also appear as injected background context; use agenr_recall mid-session when you need context you do not already have.",
432
- "agenr_recall supports exact fact recall plus historical and episodic recall behind one tool: use mode=entries for exact facts, decisions, thresholds, and versions; use mode=auto for prior-state questions like what was the previous approach, what did we use before, or what changed from X to Y; use mode=episodes when you explicitly want session narrative recall.",
433
- "agenr_recall returns truncated entry previews with ids, scores, and preview_truncated flags. Call agenr_fetch with id when preview_truncated=true or exact stored wording is required.",
434
- "When Agenr injects memory automatically, treat it as non-user background context and use it silently when relevant rather than forcing it into the reply.",
435
- "Use agenr_store for durable memory, not for logging. Store only the durable takeaway, standing rule, preference, risk, lesson, or relationship - not progress logs or data already canonical elsewhere.",
436
- "Use agenr_update to correct metadata on an existing entry. Use agenr_store with supersedes for substantive content replacement.",
280
+ MEMORY_RECALL_SECTION_HEADER,
281
+ MEMORY_DOCTRINE.recall.first,
282
+ MEMORY_DOCTRINE.recall.modes,
283
+ MEMORY_DOCTRINE.recall.truncatedPreviewsWithFetch,
284
+ MEMORY_DOCTRINE.recall.injectedContext,
285
+ MEMORY_DOCTRINE.store.skelnNotLogging,
286
+ MEMORY_DOCTRINE.store.claimKeyPromptLine,
287
+ MEMORY_DOCTRINE.update.vsSupersedes,
437
288
  ""
438
289
  ];
439
290
  }
@@ -522,174 +373,276 @@ function traceSystemPromptDoctrineInjected(baseSystemPrompt, updatedSystemPrompt
522
373
  });
523
374
  }
524
375
 
525
- // src/adapters/skeln/hooks/before-agent-start.ts
526
- async function handleAgenrSkelnBeforeAgentStart(event, context, deps) {
527
- const scope = await deps.resolveScope(context);
528
- const doctrine = buildAgenrSkelnMemoryPromptSection().join("\n");
529
- const systemPromptWithDoctrine = appendPromptSection(event.systemPrompt, doctrine);
530
- const trackerState = deps.sessionStartTracker.consume(scope.sessionId, scope.sessionKey);
531
- if (trackerState.isFirst) {
532
- return resolveSessionStartInjection(scope, event.systemPrompt, systemPromptWithDoctrine, deps.servicesPromise);
533
- }
534
- return resolveBeforeTurnInjection(event, scope, event.systemPrompt, systemPromptWithDoctrine, context, deps.servicesPromise);
535
- }
536
- function buildAgenrSkelnInjectionMessage(content) {
376
+ // src/adapters/shared/agenr-episode-summary-llm.ts
377
+ function createAgenrEpisodeSummaryLlm(provider, modelId, apiKey) {
378
+ const client = createLlmClient(provider, modelId, { apiKey });
537
379
  return {
538
- role: "user",
539
- content: [{ type: "text", text: content }],
540
- timestamp: Date.now()
380
+ complete: client.complete,
381
+ completeJson: client.completeJson,
382
+ metadata: {
383
+ modelRef: `${provider}/${modelId}`,
384
+ pricing: {
385
+ input: client.metadata.model.cost?.input ?? 0,
386
+ output: client.metadata.model.cost?.output ?? 0,
387
+ cacheRead: client.metadata.model.cost?.cacheRead ?? 0,
388
+ cacheWrite: client.metadata.model.cost?.cacheWrite ?? 0
389
+ },
390
+ usage: client.metadata.usage
391
+ }
541
392
  };
542
393
  }
543
- function appendPromptSection(systemPrompt, section) {
544
- const trimmedSection = section.trim();
545
- if (trimmedSection.length === 0 || systemPrompt.includes(trimmedSection)) {
546
- return systemPrompt;
547
- }
548
- return `${systemPrompt.trimEnd()}
549
394
 
550
- ${trimmedSection}`;
551
- }
552
- async function resolveSessionStartInjection(scope, baseSystemPrompt, systemPrompt, servicesPromise) {
553
- let workingInjection;
554
- try {
555
- const services = await servicesPromise;
556
- workingInjection = await resolveWorkingContextInjection(services, scope, `skeln:session-start:${scope.sessionKey}`);
557
- if (services.skelnConfig.memoryPolicy?.sessionStart?.enabled === false) {
558
- return composeSkelnBeforeAgentStartResult({
559
- baseSystemPrompt,
560
- systemPrompt,
561
- recallKind: "session_start_recall",
562
- recallSkippedReason: "memoryPolicy.sessionStart.enabled=false",
563
- workingInjection
395
+ // src/adapters/skeln/transcript/parser.ts
396
+ import { createHash } from "crypto";
397
+ import * as fs from "fs/promises";
398
+ import path from "path";
399
+ var SkelnTranscriptParser = class {
400
+ /**
401
+ * Parses one Skeln session JSONL file into normalized transcript messages.
402
+ *
403
+ * @param filePath - Absolute or relative Skeln session file.
404
+ * @returns Parsed transcript payload consumed by shared episode ingest.
405
+ */
406
+ async parseFile(filePath) {
407
+ const raw = await fs.readFile(filePath, "utf8");
408
+ const transcriptHash = createHash("sha256").update(raw).digest("hex");
409
+ const warnings = [];
410
+ const messages = [];
411
+ const records = parseJsonlRecords(raw, filePath, warnings);
412
+ const header = records[0] ? parseHeaderRecord(records[0], filePath, warnings) : {};
413
+ const sessionId = header.sessionId ?? deriveSkelnSessionIdFromFilePath(filePath);
414
+ const headerTimestamp = toIsoTimestamp(header.timestamp);
415
+ for (const record of records.slice(1)) {
416
+ if (record.type !== "message") {
417
+ continue;
418
+ }
419
+ const normalized = normalizeSkelnMessage(record.message);
420
+ if (!normalized) {
421
+ continue;
422
+ }
423
+ messages.push({
424
+ index: messages.length,
425
+ role: normalized.role,
426
+ text: normalized.text,
427
+ ...normalized.timestamp ? { timestamp: normalized.timestamp } : {}
564
428
  });
565
429
  }
566
- const sessionStartPatch = await runSessionStart(
567
- {
568
- sessionKey: scope.sessionKey,
569
- policy: resolveSessionStartPolicy(services.skelnConfig.memoryPolicy)
430
+ const startedAt = messages[0]?.timestamp ?? headerTimestamp;
431
+ const endedAt = messages.at(-1)?.timestamp;
432
+ return {
433
+ messages,
434
+ metadata: {
435
+ ...sessionId ? { sessionId } : {},
436
+ ...header.cwd ? { workingDirectory: header.cwd, project: path.basename(header.cwd) } : {},
437
+ ...startedAt ? { startedAt } : {},
438
+ ...endedAt ? { endedAt } : {},
439
+ messageCount: messages.length,
440
+ transcriptHash,
441
+ reconstructedSurface: "skeln",
442
+ surfaceReconstructionSource: "reconstructed",
443
+ sourceIdentity: path.resolve(filePath),
444
+ sourceIdentityKind: "skeln_session_file"
570
445
  },
571
- services.sessionStart
572
- );
573
- return composeSkelnBeforeAgentStartResult({
574
- baseSystemPrompt,
575
- systemPrompt,
576
- recallKind: "session_start_recall",
577
- recallText: formatAgenrSessionStartRecall(sessionStartPatch),
578
- workingInjection
579
- });
580
- } catch (error) {
581
- logInjectionFailure("session-start", scope, error);
582
- return composeSkelnBeforeAgentStartResult({
583
- baseSystemPrompt,
584
- systemPrompt,
585
- recallKind: "session_start_recall",
586
- recallFailureReason: error instanceof Error ? error.message : String(error),
587
- workingInjection
588
- });
589
- }
590
- }
591
- async function resolveBeforeTurnInjection(event, scope, baseSystemPrompt, systemPrompt, context, servicesPromise) {
592
- const services = await servicesPromise;
593
- const workingInjection = await resolveWorkingContextInjection(services, scope, `skeln:before-turn:${scope.sessionKey}`);
594
- if (services.skelnConfig.memoryPolicy?.beforeTurn?.enabled === false) {
595
- return composeSkelnBeforeAgentStartResult({
596
- baseSystemPrompt,
597
- systemPrompt,
598
- recallKind: "before_turn_recall",
599
- recallSkippedReason: "memoryPolicy.beforeTurn.enabled=false",
600
- workingInjection
601
- });
446
+ warnings
447
+ };
602
448
  }
603
- const currentTurnText = normalizePromptText(event.prompt);
604
- if (!currentTurnText) {
605
- return composeSkelnBeforeAgentStartResult({
606
- baseSystemPrompt,
607
- systemPrompt,
608
- recallKind: "before_turn_recall",
609
- recallSkippedReason: "empty turn prompt",
610
- workingInjection
611
- });
449
+ };
450
+ var skelnTranscriptParser = new SkelnTranscriptParser();
451
+ function parseJsonlRecords(raw, filePath, warnings) {
452
+ const records = [];
453
+ const lines = raw.split(/\r?\n/u);
454
+ for (let index = 0; index < lines.length; index += 1) {
455
+ const line = lines[index]?.trim();
456
+ if (!line) {
457
+ continue;
458
+ }
459
+ try {
460
+ const parsed = JSON.parse(line);
461
+ const record = isRecord(parsed) ? parsed : void 0;
462
+ if (!record) {
463
+ warnings.push(`Skipped non-object Skeln JSONL line ${index + 1} in ${filePath}.`);
464
+ continue;
465
+ }
466
+ records.push(record);
467
+ } catch (error) {
468
+ warnings.push(`Skipped malformed Skeln JSONL line ${index + 1} in ${filePath}: ${formatErrorMessage(error)}`);
469
+ }
612
470
  }
613
- try {
614
- const branchMessages = extractSkelnBeforeTurnBranchMessages(context.sessionManager.getBranch());
615
- const beforeTurnPatch = await runBeforeTurn(
616
- {
617
- sessionKey: scope.sessionKey,
618
- currentTurnText,
619
- recentTurns: extractRecentTurnsFromMessages(branchMessages),
620
- policy: resolveBeforeTurnPolicy(services.skelnConfig.memoryPolicy)
621
- },
622
- services.beforeTurn
623
- );
624
- return composeSkelnBeforeAgentStartResult({
625
- baseSystemPrompt,
626
- systemPrompt,
627
- recallKind: "before_turn_recall",
628
- recallText: formatAgenrBeforeTurnRecall(beforeTurnPatch),
629
- workingInjection
630
- });
631
- } catch (error) {
632
- logInjectionFailure("before-turn", scope, error);
633
- return composeSkelnBeforeAgentStartResult({
634
- baseSystemPrompt,
635
- systemPrompt,
636
- recallKind: "before_turn_recall",
637
- recallFailureReason: error instanceof Error ? error.message : String(error),
638
- workingInjection
639
- });
471
+ if (records.length === 0) {
472
+ warnings.push(`Skipped empty Skeln transcript file: ${filePath}`);
640
473
  }
474
+ return records;
641
475
  }
642
- function composeSkelnBeforeAgentStartResult(input) {
643
- const recallText = input.recallText?.trim();
644
- const memoryTrace = buildBeforeAgentStartMemoryTrace({
645
- baseSystemPrompt: input.baseSystemPrompt,
646
- systemPrompt: input.systemPrompt,
647
- recallKind: input.recallKind,
648
- recallText,
649
- recallSkippedReason: input.recallSkippedReason,
650
- recallFailureReason: input.recallFailureReason,
651
- workingContextTrace: input.workingInjection?.trace
652
- });
476
+ function parseHeaderRecord(record, filePath, warnings) {
477
+ if (!record || record.type !== "session") {
478
+ warnings.push(`Skeln transcript ${filePath} is missing a session header.`);
479
+ return {};
480
+ }
653
481
  return {
654
- systemPrompt: input.systemPrompt,
655
- ...recallText ? { message: buildAgenrSkelnInjectionMessage(recallText) } : {},
656
- ...input.workingInjection?.transientMessages ? { transientMessages: input.workingInjection.transientMessages } : {},
657
- ...input.workingInjection?.workingContextAudit ? { workingContextAudit: input.workingInjection.workingContextAudit } : {},
658
- ...memoryTrace.length > 0 ? { memoryTrace } : {}
482
+ sessionId: readOptionalTrimmedString(record.id),
483
+ timestamp: readOptionalFiniteNumber(record.timestamp),
484
+ cwd: readOptionalTrimmedString(record.cwd)
659
485
  };
660
486
  }
661
- async function resolveWorkingContextInjection(services, scope, sourceRef) {
662
- const gate = resolveWorkingContextGate(services.capabilities.workingMemory, services.skelnConfig.memoryPolicy);
663
- if (!gate.ok) {
664
- return { trace: traceMemorySkipped("working_context", gate.reason) };
487
+ function normalizeSkelnMessage(value) {
488
+ const message = isRecord(value) ? value : void 0;
489
+ if (!message) {
490
+ return void 0;
665
491
  }
666
- return loadWorkingContextProjection(services, scope, sourceRef);
492
+ const role = normalizeRole(readOptionalTrimmedString(message.role));
493
+ if (!role) {
494
+ return void 0;
495
+ }
496
+ const text = normalizeMessageText(message.content);
497
+ if (!text) {
498
+ return void 0;
499
+ }
500
+ const timestamp = toIsoTimestamp(readOptionalFiniteNumber(message.timestamp));
501
+ return {
502
+ role,
503
+ text,
504
+ ...timestamp ? { timestamp } : {}
505
+ };
667
506
  }
668
- async function loadWorkingContextProjection(services, scope, sourceRef) {
669
- try {
670
- const projection = await services.workingMemory.renderProjection({
671
- sourceRef,
672
- scope: toWorkingScopeFromSkelnSession(scope)
673
- });
674
- if (!shouldInjectWorkingContext(projection)) {
675
- const reason = projection.renderMode !== "full" ? "working projection stub" : "empty working projection";
676
- return { trace: traceMemorySkipped("working_context", reason) };
507
+ function normalizeRole(role) {
508
+ if (role === "user") {
509
+ return "user";
510
+ }
511
+ if (role === "assistant") {
512
+ return "assistant";
513
+ }
514
+ return void 0;
515
+ }
516
+ function normalizeMessageText(value) {
517
+ if (typeof value === "string") {
518
+ return value.trim();
519
+ }
520
+ if (!Array.isArray(value)) {
521
+ return "";
522
+ }
523
+ const parts = [];
524
+ for (const part of value) {
525
+ if (typeof part === "string") {
526
+ parts.push(part);
527
+ continue;
528
+ }
529
+ const record = isRecord(part) ? part : void 0;
530
+ const text = readOptionalTrimmedString(record?.text) ?? readOptionalTrimmedString(record?.content);
531
+ if (text) {
532
+ parts.push(text);
677
533
  }
678
- const workingContextAudit = toWorkingContextAuditPointer(projection);
679
- return {
680
- transientMessages: [buildAgenrSkelnInjectionMessage(projection.content)],
681
- ...workingContextAudit ? { workingContextAudit } : {},
682
- trace: traceWorkingContextInjected(workingContextAudit, projection.content)
683
- };
684
- } catch (error) {
685
- const message = error instanceof Error ? error.message : String(error);
686
- console.warn(`[agenr] working-context projection failed for session=${scope.sessionId} key=${scope.sessionKey}: ${message}`);
687
- return { trace: traceMemoryFailed("working_context", message) };
688
534
  }
535
+ return parts.map((part) => part.trim()).filter(Boolean).join("\n").trim();
689
536
  }
690
- function logInjectionFailure(phase, scope, error) {
691
- const message = error instanceof Error ? error.message : String(error);
692
- console.warn(`[agenr] ${phase} recall failed for session=${scope.sessionId} key=${scope.sessionKey}: ${message}`);
537
+ function toIsoTimestamp(timestamp) {
538
+ if (timestamp === void 0) {
539
+ return void 0;
540
+ }
541
+ const date = new Date(timestamp);
542
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
543
+ }
544
+ function deriveSkelnSessionIdFromFilePath(filePath) {
545
+ const basename = path.basename(filePath).replace(/\.jsonl(?:\..*)?$/iu, "").trim();
546
+ return basename.length > 0 ? basename : void 0;
547
+ }
548
+
549
+ // src/adapters/skeln/episode/bounded-session-episode.ts
550
+ function resolveSkelnSessionEpisodeTarget(context) {
551
+ return {
552
+ sessionId: String(context.sessionManager.getSessionId()),
553
+ sessionFile: resolveSessionFile(context)
554
+ };
555
+ }
556
+ async function writeSkelnBoundedSessionEpisode(params) {
557
+ const logger = params.logger ?? console;
558
+ const sessionFile = params.target.sessionFile;
559
+ const sessionId = params.target.sessionId;
560
+ const summaryDeadlineMs = Date.now() + EPISODE_SUMMARY_TIMEOUT_MS;
561
+ if (!sessionFile) {
562
+ logger.info(`[agenr] ${params.actionLabel} skipped for ${params.skipDetails} reason=no_session_file`);
563
+ return;
564
+ }
565
+ const episodeModel = resolveModel(params.services.agenrConfig, "episode");
566
+ const llmApiKey = resolveLlmApiKey(params.services.agenrConfig, episodeModel.provider);
567
+ const summaryLlm = createDeadlineAwareEpisodeSummaryLlm(
568
+ createAgenrEpisodeSummaryLlm(episodeModel.provider, episodeModel.modelId, llmApiKey),
569
+ summaryDeadlineMs
570
+ );
571
+ await writeBoundedSingleTranscriptEpisode({
572
+ filePath: sessionFile,
573
+ context: params.logContext,
574
+ actionLabel: params.actionLabel,
575
+ logger,
576
+ summaryDeadlineMs,
577
+ ports: {
578
+ files: createSingleTranscriptDiscoveryPort(sessionFile),
579
+ transcript: skelnTranscriptParser,
580
+ episodes: params.services.episodes,
581
+ createSummaryLlm: () => summaryLlm,
582
+ embedSummary: (summary) => embedEpisodeSummaryWithinBudget({
583
+ summary,
584
+ embedding: params.services.embedding,
585
+ embeddingAvailable: params.services.embeddingStatus.available,
586
+ deadlineMs: summaryDeadlineMs,
587
+ logger,
588
+ logContext: `[agenr] ${params.actionLabel} embedding skipped for ${params.logContext} file=${sessionFile}`
589
+ })
590
+ },
591
+ ingestOptions: {
592
+ source: "skeln",
593
+ genVersion: params.genVersion,
594
+ skipActiveSessionCheck: true,
595
+ ...params.activityThreshold ? { activityThreshold: params.activityThreshold } : {},
596
+ candidateOverrides: {
597
+ sessionId: params.sourceSessionId ?? sessionId,
598
+ sourceRef: params.buildSourceRef(sessionFile),
599
+ agentId: null,
600
+ surface: "skeln",
601
+ metadataSource: "reconstructed"
602
+ }
603
+ }
604
+ });
605
+ }
606
+ function resolveSessionFile(context) {
607
+ try {
608
+ const sessionFile = context.sessionManager.getSessionFile().trim();
609
+ return sessionFile || void 0;
610
+ } catch {
611
+ return void 0;
612
+ }
613
+ }
614
+
615
+ // src/adapters/skeln/episode/episode-writer.ts
616
+ var SKELN_EPISODE_GENERATOR_VERSION = "skeln-episodic-summary-v1";
617
+ var SKELN_PHASE4_SHUTDOWN_EPISODE_ACTIVITY_THRESHOLD = {
618
+ minMaterialTurns: 8,
619
+ minDurationMs: 20 * 60 * 1e3
620
+ };
621
+ async function writeSkelnShutdownEpisode(params) {
622
+ await writeSkelnBoundedSessionEpisode({
623
+ target: params.target,
624
+ services: params.services,
625
+ logger: params.logger,
626
+ actionLabel: "skeln shutdown episode write",
627
+ genVersion: SKELN_EPISODE_GENERATOR_VERSION,
628
+ activityThreshold: SKELN_PHASE4_SHUTDOWN_EPISODE_ACTIVITY_THRESHOLD,
629
+ buildSourceRef: (sessionFile) => sessionFile,
630
+ logContext: `session=${params.target.sessionId} key=skeln:${params.target.sessionId}`,
631
+ skipDetails: `session=${params.target.sessionId}`
632
+ });
633
+ }
634
+ async function writeSkelnPreCompactionEpisode(params) {
635
+ await writeSkelnBoundedSessionEpisode({
636
+ target: params.target,
637
+ services: params.services,
638
+ logger: params.logger,
639
+ actionLabel: "skeln pre-compaction episode write",
640
+ genVersion: SKELN_EPISODE_GENERATOR_VERSION,
641
+ buildSourceRef: (sessionFile) => sessionFile,
642
+ sourceSessionId: `${params.target.sessionId}:pre-compaction:${params.messageCount}`,
643
+ logContext: `session=${params.target.sessionId}:pre-compaction:${params.messageCount}`,
644
+ skipDetails: `session=${params.target.sessionId}`
645
+ });
693
646
  }
694
647
 
695
648
  // src/adapters/skeln/hooks/session-memory.ts
@@ -722,10 +675,11 @@ function buildSkelnSessionBeforeForkTriggerEvent(scope, event) {
722
675
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
723
676
  };
724
677
  }
725
- function buildSkelnSessionBeforeCompactTriggerEvent(scope) {
678
+ function buildSkelnSessionBeforeCompactTriggerEvent(scope, event = {}) {
726
679
  return {
727
680
  type: "session_before_compact",
728
681
  sessionKey: scope.sessionKey,
682
+ ...Object.keys(event).length > 0 ? { payload: event } : {},
729
683
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
730
684
  };
731
685
  }
@@ -781,12 +735,6 @@ function buildSkelnSessionShutdownTriggerEvent(scope, event) {
781
735
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
782
736
  };
783
737
  }
784
- function logSessionMemoryTriggerResult(result) {
785
- if (result.accepted || result.reason === "feature_disabled") {
786
- return;
787
- }
788
- console.warn(`[agenr] session-memory trigger rejected: ${result.reason} (${result.message})`);
789
- }
790
738
  function resolveSessionStartTransitionReason(reason, previousSessionFile) {
791
739
  if (isLineageSessionStartReason(reason)) {
792
740
  return reason;
@@ -804,7 +752,7 @@ function buildSessionStartPredecessor(transitionReason, previousSessionFile) {
804
752
  if (!sourceRef) {
805
753
  return void 0;
806
754
  }
807
- return { sourceRef };
755
+ return { sourceRef: buildSessionFileSourceRef(sourceRef) };
808
756
  }
809
757
  function buildCompactionCheckpointArtifact(sessionKey, compactionEntry, fromExtension) {
810
758
  return {
@@ -812,7 +760,7 @@ function buildCompactionCheckpointArtifact(sessionKey, compactionEntry, fromExte
812
760
  sessionKey,
813
761
  source: "skeln",
814
762
  sourceId: compactionEntry.id,
815
- sourceRef: `compaction:${compactionEntry.id}`,
763
+ sourceRef: buildCompactionSourceRef(compactionEntry.id),
816
764
  summary: compactionEntry.summary,
817
765
  metadata: {
818
766
  firstKeptEntryId: compactionEntry.firstKeptEntryId,
@@ -821,1694 +769,437 @@ function buildCompactionCheckpointArtifact(sessionKey, compactionEntry, fromExte
821
769
  };
822
770
  }
823
771
 
824
- // src/adapters/skeln/hooks/subagent-findings.ts
825
- var SUBAGENT_OUTCOME_MAX_CHARS = 3e3;
826
- var SUBAGENT_RESULT_LIMIT = 6;
827
- async function recordSkelnSubagentFindings(servicesPromise, resolveScope, context, event) {
828
- const commandNote = buildSkelnSubagentCommandNote(event, (/* @__PURE__ */ new Date()).toISOString());
829
- if (!commandNote) {
772
+ // src/adapters/skeln/hooks/session-memory-routing.ts
773
+ async function routeSkelnSessionMemoryTrigger(servicesPromise, resolveScope, context, buildEvent) {
774
+ await routeSessionMemoryTriggerSafely({
775
+ resolveScope: () => resolveScope(context),
776
+ routeTrigger: createSessionMemoryTriggerRouter(servicesPromise, buildEvent)
777
+ });
778
+ }
779
+
780
+ // src/adapters/skeln/hooks/compaction-handlers.ts
781
+ async function handleSkelnSessionBeforeCompact(event, context, servicesPromise, resolveScope) {
782
+ await routeSkelnSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope) => buildSkelnSessionBeforeCompactTriggerEvent(scope, event));
783
+ await scheduleSkelnPreCompactionEpisodeWrite(event, context, servicesPromise);
784
+ }
785
+ async function resolveSkelnCompactionPromptContext(scope, services, tracker) {
786
+ return resolveCompactionPromptContext({
787
+ sessionId: scope.sessionId,
788
+ sessionKey: scope.sessionKey,
789
+ features: {
790
+ ...services.agenrConfig.features,
791
+ ...services.skelnConfig.featureFlags
792
+ },
793
+ sessionMemoryRepository: services.sessionMemoryRepository,
794
+ tracker
795
+ });
796
+ }
797
+ async function scheduleSkelnPreCompactionEpisodeWrite(event, context, servicesPromise) {
798
+ const target = resolveSkelnSessionEpisodeTarget(context);
799
+ if (!target.sessionFile) {
830
800
  return;
831
801
  }
832
802
  try {
833
- const [services, scope] = await Promise.all([servicesPromise, resolveScope(context)]);
834
- const result = await services.workingMemory.run({
835
- action: "update",
836
- scope: toWorkingScopeFromSkelnSession(scope),
837
- operation: {
838
- type: "add_command_note",
839
- command: commandNote
840
- },
841
- updateReason: "Recorded bounded subagent findings on the parent working set.",
842
- actor: "runtime",
843
- source: "lifecycle_hook"
844
- });
845
- if (!result.ok && !isExpectedSubagentFindingSkip(result.code)) {
846
- console.warn(`[agenr] subagent findings working-memory update failed: ${result.message}`);
803
+ const services = await servicesPromise;
804
+ if (!isPluginEpisodeWriteEnabled(services.skelnConfig.memoryPolicy)) {
805
+ return;
847
806
  }
807
+ const messageCount = resolveSkelnPreCompactionMessageCount(context, event);
808
+ scheduleGuardedEpisodeWrite({
809
+ dreaming: services.dreaming,
810
+ dbPath: services.config.dbPath,
811
+ write: async () => writeSkelnPreCompactionEpisode({
812
+ target,
813
+ messageCount,
814
+ services
815
+ }),
816
+ onFailure: (error) => {
817
+ console.warn(`[agenr] skeln pre-compaction episode write failed for session=${target.sessionId}: ${formatErrorMessage(error)}`);
818
+ }
819
+ });
848
820
  } catch (error) {
849
- console.warn(`[agenr] subagent findings capture failed: ${formatErrorMessage(error)}`);
821
+ console.warn(`[agenr] skeln pre-compaction episode write skipped: ${formatErrorMessage(error)}`);
850
822
  }
851
823
  }
852
- function buildSkelnSubagentCommandNote(event, observedAt) {
853
- if (event.toolName !== "subagent" || event.isError) {
854
- return void 0;
824
+ function resolveSkelnPreCompactionMessageCount(context, event) {
825
+ if (typeof event.messageCount === "number" && Number.isFinite(event.messageCount)) {
826
+ return event.messageCount;
855
827
  }
856
- const details = isRecord(event.details) ? event.details : void 0;
857
- if (!details) {
858
- return void 0;
828
+ try {
829
+ return context.sessionManager.getBranch().length;
830
+ } catch {
831
+ return 0;
859
832
  }
860
- const mode = readOptionalTrimmedString(details.mode) ?? "unknown";
861
- const artifactPath = readOptionalTrimmedString(details.artifactPath);
862
- const results = Array.isArray(details.results) ? details.results.flatMap(normalizeDelegationResult).slice(0, SUBAGENT_RESULT_LIMIT) : [];
863
- if (results.length === 0 && !artifactPath) {
864
- return void 0;
833
+ }
834
+
835
+ // src/adapters/skeln/hooks/before-agent-start.ts
836
+ async function handleAgenrSkelnBeforeAgentStart(event, context, deps) {
837
+ const scope = await deps.resolveScope(context);
838
+ await deps.lifecycleIntakeTracker.wait(scope.sessionId, scope.sessionKey);
839
+ const doctrine = buildAgenrSkelnMemoryPromptSection().join("\n");
840
+ const systemPromptWithDoctrine = appendPromptSection(event.systemPrompt, doctrine);
841
+ const trackerState = deps.sessionStartTracker.consume(scope.sessionId, scope.sessionKey);
842
+ if (trackerState.isFirst) {
843
+ return resolveSessionStartInjection(scope, event.systemPrompt, systemPromptWithDoctrine, deps.servicesPromise);
865
844
  }
866
- const resultLines = results.map(
867
- (result) => `- ${result.agent}${result.name ? `/${result.name}` : ""}: ${result.status}${result.summary ? ` - ${result.summary}` : ""}`
868
- );
869
- const outcome = truncateText(
870
- [
871
- `mode=${mode}`,
872
- ...resultLines,
873
- ...artifactPath ? [`artifact=${artifactPath}`] : [],
874
- ...results.length >= SUBAGENT_RESULT_LIMIT ? ["additional subagent results omitted"] : []
875
- ].join("\n"),
876
- SUBAGENT_OUTCOME_MAX_CHARS
877
- );
878
- return {
879
- command: `subagent ${mode}`,
880
- outcome,
881
- observedAt
882
- };
883
- }
884
- function normalizeDelegationResult(value) {
885
- const record = isRecord(value) ? value : void 0;
886
- if (!record) {
887
- return [];
888
- }
889
- const agent = readOptionalTrimmedString(record.agent);
890
- const status = readOptionalTrimmedString(record.status);
891
- if (!agent || !status) {
892
- return [];
893
- }
894
- const stdout = readOptionalTrimmedString(record.stdout);
895
- const stderr = readOptionalTrimmedString(record.stderr);
896
- const name = readOptionalTrimmedString(record.name);
897
- const summary = firstMeaningfulLine(stdout ?? stderr);
898
- return [
899
- {
900
- agent,
901
- ...name ? { name } : {},
902
- status,
903
- ...summary ? { summary } : {}
904
- }
905
- ];
906
- }
907
- function isExpectedSubagentFindingSkip(code) {
908
- return code === "feature_disabled" || code === "missing_active_set" || code === "missing_scope" || code === "misconfigured";
909
- }
910
- function firstMeaningfulLine(value) {
911
- const line = value?.split(/\r?\n/u).map((part) => part.trim()).find((part) => part.length > 0);
912
- return line ? truncateText(line, 500) : void 0;
913
- }
914
- function truncateText(value, maxChars) {
915
- if (value.length <= maxChars) {
916
- return value;
917
- }
918
- return `${value.slice(0, Math.max(0, maxChars - 15)).trimEnd()} [truncated]`;
919
- }
920
-
921
- // src/adapters/shared/agenr-episode-summary-llm.ts
922
- function createAgenrEpisodeSummaryLlm(provider, modelId, apiKey) {
923
- const client = createLlmClient(provider, modelId, { apiKey });
924
- return {
925
- complete: client.complete,
926
- completeJson: client.completeJson,
927
- metadata: {
928
- modelRef: `${provider}/${modelId}`,
929
- pricing: {
930
- input: client.metadata.model.cost?.input ?? 0,
931
- output: client.metadata.model.cost?.output ?? 0,
932
- cacheRead: client.metadata.model.cost?.cacheRead ?? 0,
933
- cacheWrite: client.metadata.model.cost?.cacheWrite ?? 0
934
- },
935
- usage: client.metadata.usage
936
- }
937
- };
938
- }
939
-
940
- // src/adapters/skeln/transcript/parser.ts
941
- import { createHash } from "crypto";
942
- import * as fs from "fs/promises";
943
- import path from "path";
944
- var SkelnTranscriptParser = class {
945
- /**
946
- * Parses one Skeln session JSONL file into normalized transcript messages.
947
- *
948
- * @param filePath - Absolute or relative Skeln session file.
949
- * @returns Parsed transcript payload consumed by shared episode ingest.
950
- */
951
- async parseFile(filePath) {
952
- const raw = await fs.readFile(filePath, "utf8");
953
- const transcriptHash = createHash("sha256").update(raw).digest("hex");
954
- const warnings = [];
955
- const messages = [];
956
- const records = parseJsonlRecords(raw, filePath, warnings);
957
- const header = records[0] ? parseHeaderRecord(records[0], filePath, warnings) : {};
958
- const sessionId = header.sessionId ?? deriveSkelnSessionIdFromFilePath(filePath);
959
- const headerTimestamp = toIsoTimestamp(header.timestamp);
960
- for (const record of records.slice(1)) {
961
- if (record.type !== "message") {
962
- continue;
963
- }
964
- const normalized = normalizeSkelnMessage(record.message);
965
- if (!normalized) {
966
- continue;
967
- }
968
- messages.push({
969
- index: messages.length,
970
- role: normalized.role,
971
- text: normalized.text,
972
- ...normalized.timestamp ? { timestamp: normalized.timestamp } : {}
973
- });
974
- }
975
- const startedAt = messages[0]?.timestamp ?? headerTimestamp;
976
- const endedAt = messages.at(-1)?.timestamp;
977
- return {
978
- messages,
979
- metadata: {
980
- ...sessionId ? { sessionId } : {},
981
- ...header.cwd ? { workingDirectory: header.cwd, project: path.basename(header.cwd) } : {},
982
- ...startedAt ? { startedAt } : {},
983
- ...endedAt ? { endedAt } : {},
984
- messageCount: messages.length,
985
- transcriptHash,
986
- reconstructedSurface: "skeln",
987
- surfaceReconstructionSource: "reconstructed",
988
- sourceIdentity: path.resolve(filePath),
989
- sourceIdentityKind: "skeln_session_file"
990
- },
991
- warnings
992
- };
993
- }
994
- };
995
- var skelnTranscriptParser = new SkelnTranscriptParser();
996
- function parseJsonlRecords(raw, filePath, warnings) {
997
- const records = [];
998
- const lines = raw.split(/\r?\n/u);
999
- for (let index = 0; index < lines.length; index += 1) {
1000
- const line = lines[index]?.trim();
1001
- if (!line) {
1002
- continue;
1003
- }
1004
- try {
1005
- const parsed = JSON.parse(line);
1006
- const record = isRecord(parsed) ? parsed : void 0;
1007
- if (!record) {
1008
- warnings.push(`Skipped non-object Skeln JSONL line ${index + 1} in ${filePath}.`);
1009
- continue;
1010
- }
1011
- records.push(record);
1012
- } catch (error) {
1013
- warnings.push(`Skipped malformed Skeln JSONL line ${index + 1} in ${filePath}: ${formatErrorMessage(error)}`);
1014
- }
1015
- }
1016
- if (records.length === 0) {
1017
- warnings.push(`Skipped empty Skeln transcript file: ${filePath}`);
1018
- }
1019
- return records;
1020
- }
1021
- function parseHeaderRecord(record, filePath, warnings) {
1022
- if (!record || record.type !== "session") {
1023
- warnings.push(`Skeln transcript ${filePath} is missing a session header.`);
1024
- return {};
1025
- }
1026
- return {
1027
- sessionId: readOptionalTrimmedString(record.id),
1028
- timestamp: readOptionalFiniteNumber(record.timestamp),
1029
- cwd: readOptionalTrimmedString(record.cwd)
1030
- };
1031
- }
1032
- function normalizeSkelnMessage(value) {
1033
- const message = isRecord(value) ? value : void 0;
1034
- if (!message) {
1035
- return void 0;
1036
- }
1037
- const role = normalizeRole(readOptionalTrimmedString(message.role));
1038
- if (!role) {
1039
- return void 0;
1040
- }
1041
- const text = normalizeMessageText(message.content);
1042
- if (!text) {
1043
- return void 0;
1044
- }
1045
- const timestamp = toIsoTimestamp(readOptionalFiniteNumber(message.timestamp));
1046
- return {
1047
- role,
1048
- text,
1049
- ...timestamp ? { timestamp } : {}
1050
- };
1051
- }
1052
- function normalizeRole(role) {
1053
- if (role === "user") {
1054
- return "user";
1055
- }
1056
- if (role === "assistant") {
1057
- return "assistant";
1058
- }
1059
- return void 0;
1060
- }
1061
- function normalizeMessageText(value) {
1062
- if (typeof value === "string") {
1063
- return value.trim();
1064
- }
1065
- if (!Array.isArray(value)) {
1066
- return "";
1067
- }
1068
- const parts = [];
1069
- for (const part of value) {
1070
- if (typeof part === "string") {
1071
- parts.push(part);
1072
- continue;
1073
- }
1074
- const record = isRecord(part) ? part : void 0;
1075
- const text = readOptionalTrimmedString(record?.text) ?? readOptionalTrimmedString(record?.content);
1076
- if (text) {
1077
- parts.push(text);
1078
- }
1079
- }
1080
- return parts.map((part) => part.trim()).filter(Boolean).join("\n").trim();
1081
- }
1082
- function toIsoTimestamp(timestamp) {
1083
- if (timestamp === void 0) {
1084
- return void 0;
1085
- }
1086
- const date = new Date(timestamp);
1087
- return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
1088
- }
1089
- function deriveSkelnSessionIdFromFilePath(filePath) {
1090
- const basename = path.basename(filePath).replace(/\.jsonl(?:\..*)?$/iu, "").trim();
1091
- return basename.length > 0 ? basename : void 0;
1092
- }
1093
-
1094
- // src/adapters/skeln/episode/bounded-session-episode.ts
1095
- function resolveSkelnSessionEpisodeTarget(context) {
1096
- return {
1097
- sessionId: String(context.sessionManager.getSessionId()),
1098
- sessionFile: resolveSessionFile(context)
1099
- };
1100
- }
1101
- async function writeSkelnBoundedSessionEpisode(params) {
1102
- const logger = params.logger ?? console;
1103
- const sessionFile = params.target.sessionFile;
1104
- const sessionId = params.target.sessionId;
1105
- const summaryDeadlineMs = Date.now() + EPISODE_SUMMARY_TIMEOUT_MS;
1106
- if (!sessionFile) {
1107
- logger.info(`[agenr] ${params.actionLabel} skipped for ${params.skipDetails} reason=no_session_file`);
1108
- return;
1109
- }
1110
- const episodeModel = resolveModel(params.services.agenrConfig, "episode");
1111
- const llmApiKey = resolveLlmApiKey(params.services.agenrConfig, episodeModel.provider);
1112
- const summaryLlm = createDeadlineAwareEpisodeSummaryLlm(
1113
- createAgenrEpisodeSummaryLlm(episodeModel.provider, episodeModel.modelId, llmApiKey),
1114
- summaryDeadlineMs
1115
- );
1116
- await writeBoundedSingleTranscriptEpisode({
1117
- filePath: sessionFile,
1118
- context: params.logContext,
1119
- actionLabel: params.actionLabel,
1120
- logger,
1121
- summaryDeadlineMs,
1122
- ports: {
1123
- files: createSingleTranscriptDiscoveryPort(sessionFile),
1124
- transcript: skelnTranscriptParser,
1125
- episodes: params.services.episodes,
1126
- createSummaryLlm: () => summaryLlm,
1127
- embedSummary: (summary) => embedEpisodeSummaryWithinBudget({
1128
- summary,
1129
- embedding: params.services.embedding,
1130
- embeddingAvailable: params.services.embeddingStatus.available,
1131
- deadlineMs: summaryDeadlineMs,
1132
- logger,
1133
- logContext: `[agenr] ${params.actionLabel} embedding skipped for ${params.logContext} file=${sessionFile}`
1134
- })
1135
- },
1136
- ingestOptions: {
1137
- source: "skeln",
1138
- genVersion: params.genVersion,
1139
- skipActiveSessionCheck: true,
1140
- activityThreshold: params.activityThreshold,
1141
- candidateOverrides: {
1142
- sessionId,
1143
- sourceRef: params.buildSourceRef(sessionFile),
1144
- agentId: null,
1145
- surface: "skeln",
1146
- metadataSource: "reconstructed"
1147
- }
1148
- }
1149
- });
1150
- }
1151
- function resolveSessionFile(context) {
1152
- try {
1153
- const sessionFile = context.sessionManager.getSessionFile().trim();
1154
- return sessionFile || void 0;
1155
- } catch {
1156
- return void 0;
1157
- }
1158
- }
1159
-
1160
- // src/adapters/skeln/episode/episode-writer.ts
1161
- var SKELN_EPISODE_GENERATOR_VERSION = "skeln-episodic-summary-v1";
1162
- var SKELN_PHASE4_SHUTDOWN_EPISODE_ACTIVITY_THRESHOLD = {
1163
- minMaterialTurns: 8,
1164
- minDurationMs: 20 * 60 * 1e3
1165
- };
1166
- async function writeSkelnShutdownEpisode(params) {
1167
- await writeSkelnBoundedSessionEpisode({
1168
- target: params.target,
1169
- services: params.services,
1170
- logger: params.logger,
1171
- actionLabel: "skeln shutdown episode write",
1172
- genVersion: SKELN_EPISODE_GENERATOR_VERSION,
1173
- activityThreshold: SKELN_PHASE4_SHUTDOWN_EPISODE_ACTIVITY_THRESHOLD,
1174
- buildSourceRef: (sessionFile) => sessionFile,
1175
- logContext: `session=${params.target.sessionId} key=skeln:${params.target.sessionId}`,
1176
- skipDetails: `session=${params.target.sessionId}`
1177
- });
1178
- }
1179
-
1180
- // src/adapters/skeln/episode/shutdown-episode-write.ts
1181
- function buildSkelnSessionShutdownEpisodeWork(params) {
1182
- const target = resolveSkelnSessionEpisodeTarget(params.context);
1183
- const shutdownWork = params.event.reason === "quit" ? writeScopedSkelnShutdownEpisode(params.servicesPromise, target, params.logger).finally(() => closeSkelnServicesAfterShutdown(params.servicesPromise)) : writeScopedSkelnShutdownEpisode(params.servicesPromise, target, params.logger);
1184
- return shutdownWork.catch((error) => {
1185
- logSkelnShutdownEpisodeFailure(error, params.logger);
1186
- });
1187
- }
1188
- function scheduleSkelnSessionShutdownEpisodeWrite(params) {
1189
- const work = buildSkelnSessionShutdownEpisodeWork(params);
1190
- if (params.event.reason === "quit" && params.event.deferWork) {
1191
- params.event.deferWork(work);
1192
- return Promise.resolve();
1193
- }
1194
- if (params.event.reason === "quit") {
1195
- return work;
1196
- }
1197
- void work;
1198
- return Promise.resolve();
1199
- }
1200
- function logSkelnShutdownEpisodeFailure(error, logger) {
1201
- const log = logger ?? console;
1202
- log.warn(`[agenr] skeln shutdown episode failed: ${formatErrorMessage(error)}`);
1203
- }
1204
- async function closeSkelnServicesAfterShutdown(servicesPromise) {
1205
- try {
1206
- const services = await servicesPromise;
1207
- await services.close();
1208
- } catch {
1209
- }
1210
- }
1211
- async function writeScopedSkelnShutdownEpisode(servicesPromise, target, logger) {
1212
- const services = await servicesPromise;
1213
- if (!services.capabilities.shutdownEpisodes) {
1214
- return;
1215
- }
1216
- await writeSkelnShutdownEpisode({ target, services, logger });
1217
- }
1218
-
1219
- // src/app/features/resolve.ts
1220
- function resolveAgenrFeatureFlags(features) {
1221
- const resolved = { ...DEFAULT_AGENR_FEATURE_FLAGS };
1222
- for (const key of AGENR_FEATURE_FLAG_KEYS) {
1223
- if (features?.[key] !== void 0) {
1224
- resolved[key] = features[key];
1225
- }
1226
- }
1227
- return resolved;
1228
- }
1229
-
1230
- // src/app/features/capabilities.ts
1231
- function resolveRuntimeCapabilities(featureFlags, input = {}) {
1232
- const sessionMemoryFlagEnabled = featureFlags.sessionTreeLineage || featureFlags.sessionTreeCompaction;
1233
- const sessionMemory = resolveRepositoryBackedCapability(sessionMemoryFlagEnabled, input.sessionMemoryRepository);
1234
- return {
1235
- workingMemory: resolveRepositoryBackedCapability(featureFlags.workingMemory, input.workingMemoryRepository),
1236
- sessionMemory,
1237
- shutdownEpisodes: sessionMemory === "enabled",
1238
- goalContinuation: resolveGoalContinuationCapability(featureFlags.goalContinuation, input.goalContinuationHostPort)
1239
- };
1240
- }
1241
- function resolveRepositoryBackedCapability(featureEnabled, repository) {
1242
- if (!featureEnabled) {
1243
- return "disabled";
1244
- }
1245
- return repository ? "enabled" : "misconfigured";
1246
- }
1247
- function resolveGoalContinuationCapability(featureEnabled, hostPort) {
1248
- if (!featureEnabled) {
1249
- return "disabled";
1250
- }
1251
- return hostPort ? "enabled" : "misconfigured";
1252
- }
1253
-
1254
- // src/app/features/runtime-policy.ts
1255
- function resolveRuntimePolicy(featureFlags, input = {}) {
1256
- return {
1257
- featureFlags,
1258
- capabilities: resolveRuntimeCapabilities(featureFlags, {
1259
- workingMemoryRepository: input.workingMemoryRepository,
1260
- sessionMemoryRepository: input.sessionMemoryRepository,
1261
- goalContinuationHostPort: input.goalContinuationHostPort
1262
- }),
1263
- ...input.memoryPolicy ? { memoryPolicy: input.memoryPolicy } : {}
1264
- };
1265
- }
1266
-
1267
- // src/app/goal-continuation/service.ts
1268
- var GOAL_CONTINUATION_FEATURE_DISABLED_MESSAGE = "Goal continuation is disabled by the goalContinuation feature flag.";
1269
- var GOAL_CONTINUATION_HOST_MISSING_MESSAGE = "Goal continuation is host-owned; no host callback was registered for this Agenr runtime.";
1270
- function createGoalContinuationService(featureFlags, hostPort) {
1271
- const featureEnabled = featureFlags.goalContinuation;
1272
- return {
1273
- async runCommand(params) {
1274
- if (!featureEnabled) {
1275
- return {
1276
- ok: false,
1277
- code: "feature_disabled",
1278
- message: GOAL_CONTINUATION_FEATURE_DISABLED_MESSAGE
1279
- };
1280
- }
1281
- if (!hostPort) {
1282
- return {
1283
- ok: false,
1284
- code: "host_callback_missing",
1285
- message: GOAL_CONTINUATION_HOST_MISSING_MESSAGE
1286
- };
1287
- }
1288
- return hostPort.runCommand(params);
1289
- }
1290
- };
1291
- }
1292
-
1293
- // src/app/session-memory/trigger-router.ts
1294
- import { createHash as createHash2 } from "crypto";
1295
-
1296
- // src/app/working-memory/lifecycle-checkpoint.ts
1297
- async function attachWorkingCheckpointRefresh(event, result, workingMemory) {
1298
- const refreshRequest = resolveWorkingCheckpointRefreshRequest(event, result);
1299
- if (!refreshRequest) {
1300
- return result;
1301
- }
1302
- const workingCheckpointRefresh = workingMemory ? await mergeWorkingCheckpoint(refreshRequest, workingMemory) : skippedRefreshWithoutWorkingMemory(refreshRequest.attachLabel);
1303
- return {
1304
- ...result,
1305
- workingCheckpointRefresh
1306
- };
1307
- }
1308
- async function mergeWorkingCheckpoint(request, workingMemory) {
1309
- const scope = resolveWorkingRefreshScope(request.event);
1310
- if (!scope) {
1311
- return {
1312
- ok: false,
1313
- reason: "missing_scope",
1314
- message: `${request.lifecycleLabel} requires working-scope facts.`
1315
- };
1316
- }
1317
- const result = await workingMemory.run({
1318
- action: "update",
1319
- scope,
1320
- operation: {
1321
- type: "merge_checkpoint",
1322
- checkpoint: {
1323
- summary: request.summary,
1324
- recordedAt: request.event.observedAt
1325
- }
1326
- },
1327
- updateReason: request.updateReason,
1328
- actor: "runtime",
1329
- source: "lifecycle_hook"
1330
- });
1331
- if (!result.ok) {
1332
- return mapWorkingMemoryRefreshFailure(result.code, result.message, request.lifecycleLabel);
1333
- }
1334
- if (result.action !== "update") {
1335
- return {
1336
- ok: false,
1337
- reason: "working_memory_unavailable",
1338
- message: `Expected working-memory update result, received ${result.action}.`
1339
- };
1340
- }
1341
- return {
1342
- ok: true,
1343
- action: "working_checkpoint_refreshed",
1344
- workingSetId: result.workingSet.id,
1345
- revision: result.workingSet.revision
1346
- };
1347
- }
1348
- function resolveWorkingCheckpointRefreshRequest(event, result) {
1349
- const compactionEvent = resolveCompactionCheckpointRefreshEvent(event, result);
1350
- if (compactionEvent) {
1351
- const summary = normalizeOptionalString(compactionEvent.artifact.summary);
1352
- if (!summary) {
1353
- return void 0;
1354
- }
1355
- const sourceId = normalizeOptionalString(compactionEvent.artifact.sourceId) ?? "unknown";
1356
- return {
1357
- event: compactionEvent,
1358
- summary,
1359
- updateReason: `Refreshed working checkpoint from session compaction ${sourceId}.`,
1360
- lifecycleLabel: "compaction checkpoint refresh",
1361
- attachLabel: "Compaction checkpoint refresh"
1362
- };
1363
- }
1364
- const shutdownEvent = resolveShutdownCheckpointRefreshEvent(event, result);
1365
- if (!shutdownEvent) {
1366
- return void 0;
1367
- }
1368
- const reason = normalizeShutdownReason(shutdownEvent.shutdownReason);
1369
- return {
1370
- event: shutdownEvent,
1371
- summary: `Session shutdown (${reason}) recorded. Resume from the latest working-set snapshot; no implicit close was performed.`,
1372
- updateReason: `Recorded working checkpoint from session shutdown (${reason}).`,
1373
- lifecycleLabel: "shutdown checkpoint refresh",
1374
- attachLabel: "Shutdown checkpoint refresh"
1375
- };
1376
- }
1377
- function resolveCompactionCheckpointRefreshEvent(event, result) {
1378
- if (event.type !== "session_compact" || result.artifact?.kind !== "compaction_checkpoint") {
1379
- return void 0;
1380
- }
1381
- const artifact = event.artifact;
1382
- if (!isCompactionCheckpointArtifact(artifact)) {
1383
- return void 0;
1384
- }
1385
- return {
1386
- ...event,
1387
- type: "session_compact",
1388
- artifact
1389
- };
1390
- }
1391
- function resolveShutdownCheckpointRefreshEvent(event, result) {
1392
- if (event.type !== "session_shutdown" || result.accepted !== true) {
1393
- return void 0;
1394
- }
1395
- return {
1396
- ...event,
1397
- type: "session_shutdown"
1398
- };
1399
- }
1400
- function isCompactionCheckpointArtifact(artifact) {
1401
- return artifact?.kind === "compaction_checkpoint";
1402
- }
1403
- function resolveWorkingRefreshScope(event) {
1404
- if (event.workingScope && Object.keys(event.workingScope).length > 0) {
1405
- return event.workingScope;
1406
- }
1407
- return void 0;
1408
- }
1409
- function mapWorkingMemoryRefreshFailure(code, message, lifecycleLabel) {
1410
- if (code === "feature_disabled") {
1411
- return {
1412
- ok: false,
1413
- reason: "not_applicable",
1414
- message: `Working memory is disabled; ${lifecycleLabel} was skipped.`
1415
- };
1416
- }
1417
- if (code === "missing_active_set") {
1418
- return {
1419
- ok: false,
1420
- reason: "no_active_working_set",
1421
- code,
1422
- message
1423
- };
1424
- }
1425
- return {
1426
- ok: false,
1427
- reason: "working_memory_unavailable",
1428
- code,
1429
- message
1430
- };
1431
- }
1432
- function normalizeShutdownReason(shutdownReason) {
1433
- const trimmed = shutdownReason?.trim();
1434
- return trimmed && trimmed.length > 0 ? trimmed : "unknown";
1435
- }
1436
- function skippedRefreshWithoutWorkingMemory(label) {
1437
- return {
1438
- ok: false,
1439
- reason: "not_applicable",
1440
- message: `${label} requires a working-memory service, but none was wired into the runtime.`
1441
- };
1442
- }
1443
-
1444
- // src/app/session-memory/trigger-router.ts
1445
- var SESSION_MEMORY_TRIGGER_FLAGS = {
1446
- session_start: "sessionTreeLineage",
1447
- session_before_fork: "sessionTreeLineage",
1448
- session_before_compact: "sessionTreeCompaction",
1449
- session_compact: "sessionTreeCompaction",
1450
- session_before_tree: "sessionTreeCompaction",
1451
- session_tree: "sessionTreeLineage",
1452
- session_shutdown: "sessionTreeLineage"
1453
- };
1454
- var LINEAGE_REASONS = new Set(SESSION_LINEAGE_REASONS);
1455
- async function routeSessionMemoryTrigger(event, featureFlags, deps = {}) {
1456
- const flagKey = SESSION_MEMORY_TRIGGER_FLAGS[event.type];
1457
- if (!featureFlags[flagKey]) {
1458
- return {
1459
- accepted: false,
1460
- reason: "feature_disabled",
1461
- message: `Session-memory trigger ${event.type} is disabled by feature flags.`
1462
- };
1463
- }
1464
- if (!deps.repository) {
1465
- return {
1466
- accepted: false,
1467
- reason: "misconfigured",
1468
- message: `Session-memory trigger ${event.type} is enabled, but no session-memory repository was wired into the runtime.`
1469
- };
1470
- }
1471
- const artifactInput = normalizeArtifactInput(event);
1472
- if (artifactInput.kind === "invalid") {
1473
- return invalidEvent(artifactInput.message);
1474
- }
1475
- const lineageInput = normalizeLineageInput(event);
1476
- if (lineageInput.kind === "invalid") {
1477
- return invalidEvent(lineageInput.message);
1478
- }
1479
- if (artifactInput.kind === "none" && lineageInput.kind === "none") {
1480
- return attachWorkingCheckpointRefresh(
1481
- event,
1482
- {
1483
- accepted: true,
1484
- action: isCheckpointRelevantTrigger(event.type) ? "checkpoint_relevant" : "no_lineage",
1485
- message: isCheckpointRelevantTrigger(event.type) ? `Session-memory trigger ${event.type} was accepted for checkpoint-relevant lifecycle handling.` : `Session-memory trigger ${event.type} did not include lineage or artifact facts.`
1486
- },
1487
- deps.workingMemory
1488
- );
1489
- }
1490
- const intakeInput = {
1491
- ...artifactInput.kind === "artifact" ? { artifact: artifactInput.input } : {},
1492
- ...lineageInput.kind === "lineage" ? { lineage: lineageInput.input } : {}
1493
- };
1494
- const intake = await deps.repository.recordTriggerIntake(intakeInput);
1495
- const artifact = intake.artifact;
1496
- const lineageEdge = intake.lineageEdge;
1497
- const action = resolveTriggerAction(artifact, lineageEdge);
1498
- return attachWorkingCheckpointRefresh(
1499
- event,
1500
- {
1501
- accepted: true,
1502
- action,
1503
- message: buildAcceptedMessage(event.type, action, artifact, lineageEdge),
1504
- ...lineageEdge ? { lineageEdge } : {},
1505
- ...artifact ? { artifact } : {}
1506
- },
1507
- deps.workingMemory
1508
- );
1509
- }
1510
- function normalizeLineageInput(event) {
1511
- if (!event.transitionReason || event.transitionReason === "new" || event.transitionReason === "unknown") {
1512
- return { kind: "none" };
1513
- }
1514
- if (!LINEAGE_REASONS.has(event.transitionReason)) {
1515
- return { kind: "invalid", message: `Unsupported session lineage reason "${event.transitionReason}".` };
1516
- }
1517
- const childSessionKey = normalizeOptionalString(event.childSessionKey) ?? normalizeOptionalString(event.sessionKey);
1518
- if (!childSessionKey) {
1519
- return { kind: "invalid", message: "Session lineage requires a child session key." };
1520
- }
1521
- const parentSessionKey = normalizeOptionalString(event.predecessor?.sessionKey);
1522
- const parentSourceRef = normalizeOptionalString(event.predecessor?.sourceRef);
1523
- if (!parentSessionKey && !parentSourceRef) {
1524
- return { kind: "none" };
1525
- }
1526
- const forkEntryId = normalizeOptionalString(event.predecessor?.forkEntryId);
1527
- const forkPosition = normalizeOptionalString(event.predecessor?.forkPosition);
1528
- return {
1529
- kind: "lineage",
1530
- input: {
1531
- childSessionKey,
1532
- ...parentSessionKey ? { parentSessionKey } : {},
1533
- ...parentSourceRef ? { parentSourceRef } : {},
1534
- reason: event.transitionReason,
1535
- ...forkEntryId ? { forkEntryId } : {},
1536
- ...forkPosition ? { forkPosition } : {},
1537
- observedAt: event.observedAt
1538
- }
1539
- };
1540
- }
1541
- function normalizeArtifactInput(event) {
1542
- if (!event.artifact) {
1543
- return { kind: "none" };
1544
- }
1545
- const sessionKey = normalizeOptionalString(event.artifact.sessionKey) ?? normalizeOptionalString(event.sessionKey);
1546
- if (!sessionKey) {
1547
- return { kind: "invalid", message: "Session artifact intake requires a session key." };
1548
- }
1549
- const source = normalizeOptionalString(event.artifact.source);
1550
- const sourceId = normalizeOptionalString(event.artifact.sourceId);
1551
- const summary = normalizeOptionalString(event.artifact.summary);
1552
- if (!source || !sourceId || !summary) {
1553
- return { kind: "invalid", message: "Session artifact intake requires source, sourceId, and summary." };
1554
- }
1555
- const contentHash = normalizeOptionalString(event.artifact.contentHash) ?? hashArtifactContent(event.artifact);
1556
- return {
1557
- kind: "artifact",
1558
- input: {
1559
- ...event.artifact,
1560
- sessionKey,
1561
- source,
1562
- sourceId,
1563
- summary,
1564
- contentHash
1565
- }
1566
- };
1567
- }
1568
- function hashArtifactContent(artifact) {
1569
- const payload = JSON.stringify({
1570
- kind: artifact.kind,
1571
- source: artifact.source,
1572
- sourceId: artifact.sourceId,
1573
- sourceRef: artifact.sourceRef,
1574
- summary: artifact.summary,
1575
- metadata: artifact.metadata
1576
- });
1577
- return createHash2("sha256").update(payload).digest("hex");
1578
- }
1579
- function resolveTriggerAction(artifact, lineageEdge) {
1580
- if (artifact && lineageEdge) {
1581
- return "recorded";
1582
- }
1583
- if (artifact) {
1584
- return "artifact_recorded";
1585
- }
1586
- return "lineage_recorded";
1587
- }
1588
- function buildAcceptedMessage(triggerType, action, artifact, lineageEdge) {
1589
- if (action === "recorded" && artifact && lineageEdge) {
1590
- return `Session-memory trigger ${triggerType} recorded ${lineageEdge.reason} lineage edge ${lineageEdge.id} and ${artifact.kind} artifact ${artifact.id}.`;
1591
- }
1592
- if (action === "artifact_recorded" && artifact) {
1593
- return `Session-memory trigger ${triggerType} recorded ${artifact.kind} artifact ${artifact.id}.`;
1594
- }
1595
- if (lineageEdge) {
1596
- return `Session-memory trigger ${triggerType} recorded ${lineageEdge.reason} lineage edge ${lineageEdge.id}.`;
1597
- }
1598
- return `Session-memory trigger ${triggerType} was accepted.`;
845
+ return resolveBeforeTurnInjection(event, scope, event.systemPrompt, systemPromptWithDoctrine, context, deps);
1599
846
  }
1600
- function invalidEvent(message) {
847
+ function buildAgenrSkelnInjectionMessage(content) {
1601
848
  return {
1602
- accepted: false,
1603
- reason: "invalid_event",
1604
- message
849
+ role: "user",
850
+ content: [{ type: "text", text: content }],
851
+ timestamp: Date.now()
1605
852
  };
1606
853
  }
1607
- function isCheckpointRelevantTrigger(type) {
1608
- return type === "session_before_fork" || type === "session_before_compact" || type === "session_before_tree" || type === "session_shutdown";
1609
- }
1610
-
1611
- // src/app/working-memory/close-service.ts
1612
- function resolveCloseTerminalStatus(closeMode) {
1613
- return closeMode === "abandon" ? "abandoned" : "closed";
1614
- }
1615
- function buildWorkingCloseSnapshot(input) {
1616
- const finalCheckpoint = buildFinalCheckpoint(input);
1617
- const candidates = [...input.snapshot.candidates ?? []];
1618
- if (input.createEpisode && !candidates.some((candidate) => candidate.kind === "episodic")) {
1619
- candidates.push({
1620
- kind: "episodic",
1621
- summary: finalCheckpoint.summary,
1622
- provenance: {
1623
- evidenceEventSequences: input.eventSequences,
1624
- sourceRef: `working_set:${input.workingSetId}#rev:${input.currentRevision}`,
1625
- note: input.closeReason
1626
- },
1627
- promotionStatus: "pending"
1628
- });
854
+ function appendPromptSection(systemPrompt, section) {
855
+ const trimmedSection = section.trim();
856
+ if (trimmedSection.length === 0 || systemPrompt.includes(trimmedSection)) {
857
+ return systemPrompt;
1629
858
  }
1630
- return {
1631
- snapshot: {
1632
- ...input.snapshot,
1633
- checkpoint: finalCheckpoint,
1634
- candidates: candidates.length > 0 ? candidates : void 0,
1635
- lastMaterialChange: input.closeReason
1636
- },
1637
- candidates
1638
- };
1639
- }
1640
- function buildFinalCheckpoint(input) {
1641
- const summary = normalizeSummary(input.closeReason) ?? input.snapshot.summary ?? input.snapshot.objective ?? "Working set closed.";
1642
- return {
1643
- summary,
1644
- recordedAt: input.now,
1645
- ...input.snapshot.nextActions && input.snapshot.nextActions.length > 0 ? { nextActions: input.snapshot.nextActions.map((action) => action.text).filter((text) => text.trim().length > 0) } : {},
1646
- ...input.snapshot.blockers && input.snapshot.blockers.length > 0 ? { blockers: input.snapshot.blockers } : {}
1647
- };
1648
- }
1649
- function normalizeSummary(value) {
1650
- const trimmed = value.trim();
1651
- return trimmed.length > 0 ? trimmed : void 0;
1652
- }
1653
-
1654
- // src/app/working-memory/repository.ts
1655
- function isWorkingSetCreateFailure(result) {
1656
- return "kind" in result;
1657
- }
1658
- function isWorkingSetWriteFailure(result) {
1659
- return "kind" in result;
1660
- }
859
+ return `${systemPrompt.trimEnd()}
1661
860
 
1662
- // src/app/working-memory/results.ts
1663
- function createFailure(code, message, details) {
1664
- return {
1665
- ok: false,
1666
- code,
1667
- message,
1668
- ...details ? { details } : {}
1669
- };
861
+ ${trimmedSection}`;
1670
862
  }
1671
- function writeFailureToResult(workingSetId, failure) {
1672
- switch (failure.kind) {
1673
- case "not_found":
1674
- return createFailure("not_found", "Working set was not found.", workingSetId ? { workingSetId } : void 0);
1675
- case "revision_conflict":
1676
- return createFailure("revision_conflict", "Working-set revision conflict.", {
1677
- actualRevision: failure.actualRevision,
1678
- ...workingSetId ? { workingSetId } : {}
1679
- });
1680
- case "terminal_status":
1681
- return createFailure("terminal_status", `Working set is already ${failure.status}.`, {
1682
- status: failure.status,
1683
- ...workingSetId ? { workingSetId } : {}
863
+ async function resolveSessionStartInjection(scope, baseSystemPrompt, systemPrompt, servicesPromise) {
864
+ let workingInjection;
865
+ try {
866
+ const services = await servicesPromise;
867
+ workingInjection = await resolveWorkingContextInjection(services, scope, `skeln:session-start:${scope.sessionKey}`);
868
+ if (services.skelnConfig.memoryPolicy?.sessionStart?.enabled === false) {
869
+ return composeSkelnBeforeAgentStartResult({
870
+ baseSystemPrompt,
871
+ systemPrompt,
872
+ recallKind: "session_start_recall",
873
+ recallSkippedReason: "memoryPolicy.sessionStart.enabled=false",
874
+ workingInjection
1684
875
  });
1685
- }
1686
- }
1687
-
1688
- // src/app/working-memory/scope-resolver.ts
1689
- function resolveWorkingScope(input) {
1690
- const scope = normalizeWorkingScope(input);
1691
- if (scope.taskId) {
1692
- return {
1693
- ok: true,
1694
- scope: {
1695
- ...scope,
1696
- scopeKind: "task",
1697
- scopeKey: `task:${scope.taskId}`
1698
- }
1699
- };
1700
- }
1701
- if (scope.conversationKey) {
1702
- return {
1703
- ok: true,
1704
- scope: {
1705
- ...scope,
1706
- scopeKind: "conversation",
1707
- scopeKey: `conversation:${scope.conversationKey}`
1708
- }
1709
- };
1710
- }
1711
- if (scope.gitRoot && scope.gitBranch) {
1712
- return {
1713
- ok: true,
1714
- scope: {
1715
- ...scope,
1716
- scopeKind: "git_branch",
1717
- scopeKey: buildScopeKey("git_branch", [scope.project, scope.gitRoot, scope.gitBranch])
1718
- }
1719
- };
1720
- }
1721
- if (scope.gitRoot && scope.cwd) {
1722
- return {
1723
- ok: true,
1724
- scope: {
1725
- ...scope,
1726
- scopeKind: "git_cwd",
1727
- scopeKey: buildScopeKey("git_cwd", [scope.project, scope.gitRoot, scope.cwd])
1728
- }
1729
- };
1730
- }
1731
- return {
1732
- ok: false,
1733
- code: "missing_scope",
1734
- message: "Working memory needs a task, conversation, or git scope."
1735
- };
1736
- }
1737
- function normalizeWorkingScope(input) {
1738
- const scope = input ?? {};
1739
- return {
1740
- ...normalizeField("sessionId", scope.sessionId),
1741
- ...normalizeField("gitRoot", scope.gitRoot),
1742
- ...normalizeField("gitBranch", scope.gitBranch),
1743
- ...normalizeField("cwd", scope.cwd),
1744
- ...normalizeField("project", scope.project),
1745
- ...normalizeField("taskId", scope.taskId),
1746
- ...normalizeField("conversationKey", scope.conversationKey)
1747
- };
1748
- }
1749
- function buildScopeKey(prefix, parts) {
1750
- return [prefix, ...parts.filter((part) => part !== void 0)].join(":");
1751
- }
1752
- function normalizeField(key, value) {
1753
- if (typeof value !== "string") {
1754
- return {};
1755
- }
1756
- const trimmed = value.trim();
1757
- return trimmed.length > 0 ? { [key]: trimmed } : {};
1758
- }
1759
-
1760
- // src/app/working-memory/find-current-set.ts
1761
- async function lookupCurrentWorkingSets(scope, repository) {
1762
- const scopeResolution = resolveWorkingScope(scope);
1763
- if (!scopeResolution.ok) {
1764
- return createFailure("missing_scope", scopeResolution.message);
1765
- }
1766
- const matches = await repository.findCurrentWorkingSets(scopeResolution.scope);
1767
- if (matches.length > 1) {
1768
- return createFailure("ambiguous_scope", "Multiple current working sets matched the resolved scope.", {
1769
- scopeKey: scopeResolution.scope.scopeKey,
1770
- workingSetIds: matches.map((match) => match.id)
876
+ }
877
+ const sessionStartPatch = await runSessionStart(
878
+ {
879
+ sessionKey: scope.sessionKey,
880
+ policy: resolveSessionStartPolicy(services.skelnConfig.memoryPolicy)
881
+ },
882
+ services.sessionStart
883
+ );
884
+ return composeSkelnBeforeAgentStartResult({
885
+ baseSystemPrompt,
886
+ systemPrompt,
887
+ recallKind: "session_start_recall",
888
+ recallText: formatAgenrSessionStartRecall(sessionStartPatch),
889
+ workingInjection
1771
890
  });
1772
- }
1773
- return {
1774
- ok: true,
1775
- scope: scopeResolution.scope,
1776
- matches
1777
- };
1778
- }
1779
- async function findUniqueCurrentWorkingSet(scope, repository) {
1780
- const lookup = await lookupCurrentWorkingSets(scope, repository);
1781
- if (!lookup.ok) {
1782
- return lookup;
1783
- }
1784
- if (lookup.matches.length === 0) {
1785
- return createFailure("missing_active_set", "No current working set matched the resolved scope.", {
1786
- scopeKey: lookup.scope.scopeKey
891
+ } catch (error) {
892
+ logInjectionFailure("session-start", scope, error);
893
+ return composeSkelnBeforeAgentStartResult({
894
+ baseSystemPrompt,
895
+ systemPrompt,
896
+ recallKind: "session_start_recall",
897
+ recallFailureReason: error instanceof Error ? error.message : String(error),
898
+ workingInjection
1787
899
  });
1788
900
  }
1789
- return {
1790
- ok: true,
1791
- workingSet: lookup.matches[0],
1792
- scope: lookup.scope
1793
- };
1794
- }
1795
-
1796
- // src/app/working-memory/select-working-set.ts
1797
- async function selectWorkingSet(params, repository) {
1798
- const workingSetId = params.workingSetId?.trim();
1799
- if (workingSetId) {
1800
- const workingSet = await repository.getWorkingSet(workingSetId);
1801
- if (!workingSet) {
1802
- return createFailure("not_found", `Working set ${workingSetId} was not found.`, { workingSetId });
1803
- }
1804
- return { ok: true, workingSet };
1805
- }
1806
- const current = await findUniqueCurrentWorkingSet(params.scope, repository);
1807
- if (!current.ok) {
1808
- return current;
1809
- }
1810
- return {
1811
- ok: true,
1812
- workingSet: current.workingSet,
1813
- scope: current.scope
1814
- };
1815
- }
1816
-
1817
- // src/app/working-memory/validation.ts
1818
- function normalizeRequiredString(value, message) {
1819
- const trimmed = value?.trim();
1820
- if (!trimmed) {
1821
- return createFailure("invalid_request", message);
1822
- }
1823
- return { ok: true, value: trimmed };
1824
- }
1825
- function normalizeExpectedRevision(value) {
1826
- if (value === void 0 || !Number.isInteger(value) || value < 0) {
1827
- return createFailure("invalid_request", "expectedRevision must be a non-negative integer.");
1828
- }
1829
- return { ok: true, value };
1830
- }
1831
- function canDefaultExpectedRevision(source) {
1832
- return isTrustedHostMutationSource(source);
1833
- }
1834
- function resolveExpectedRevision(selectedRevision, providedRevision, source) {
1835
- if (providedRevision === void 0) {
1836
- if (!canDefaultExpectedRevision(source)) {
1837
- return createFailure("invalid_request", "expectedRevision must be a non-negative integer.");
1838
- }
1839
- return { ok: true, value: selectedRevision };
1840
- }
1841
- return normalizeExpectedRevision(providedRevision);
1842
- }
1843
- function validateWorkingBudgetState(budget) {
1844
- const entries = [
1845
- ["tokenBudget", budget.tokenBudget],
1846
- ["tokenUsed", budget.tokenUsed],
1847
- ["wallClockBudgetSeconds", budget.wallClockBudgetSeconds],
1848
- ["wallClockUsedSeconds", budget.wallClockUsedSeconds],
1849
- ["turnBudget", budget.turnBudget],
1850
- ["turnsUsed", budget.turnsUsed],
1851
- ["requireReviewAfterSeconds", budget.requireReviewAfterSeconds]
1852
- ];
1853
- for (const [key, value] of entries) {
1854
- if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
1855
- return createFailure("invalid_request", `${key} must be a non-negative finite number.`);
1856
- }
1857
- }
1858
- return { ok: true };
1859
901
  }
1860
- function validateWorkingUsageDelta(usage) {
1861
- const entries = [
1862
- ["tokenDelta", usage.tokenDelta],
1863
- ["wallClockSecondsDelta", usage.wallClockSecondsDelta],
1864
- ["turnDelta", usage.turnDelta]
1865
- ];
1866
- const hasDelta = entries.some(([, value]) => value !== void 0);
1867
- if (!hasDelta) {
1868
- return createFailure("invalid_request", "account_usage requires at least one usage delta.");
902
+ async function resolveBeforeTurnInjection(event, scope, baseSystemPrompt, systemPrompt, context, deps) {
903
+ const services = await deps.servicesPromise;
904
+ const compactionContext = await resolveSkelnCompactionPromptContext(scope, services, deps.compactionPromptTracker);
905
+ const workingInjection = await resolveWorkingContextInjection(services, scope, `skeln:before-turn:${scope.sessionKey}`);
906
+ if (services.skelnConfig.memoryPolicy?.beforeTurn?.enabled === false) {
907
+ return composeSkelnBeforeAgentStartResult({
908
+ baseSystemPrompt,
909
+ systemPrompt,
910
+ recallKind: "before_turn_recall",
911
+ recallSkippedReason: "memoryPolicy.beforeTurn.enabled=false",
912
+ recallText: compactionContext,
913
+ workingInjection
914
+ });
1869
915
  }
1870
- for (const [key, value] of entries) {
1871
- if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
1872
- return createFailure("invalid_request", `${key} must be a non-negative finite number.`);
1873
- }
916
+ const currentTurnText = normalizePromptText(event.prompt);
917
+ if (!currentTurnText) {
918
+ return composeSkelnBeforeAgentStartResult({
919
+ baseSystemPrompt,
920
+ systemPrompt,
921
+ recallKind: "before_turn_recall",
922
+ recallSkippedReason: "empty turn prompt",
923
+ recallText: compactionContext,
924
+ workingInjection
925
+ });
1874
926
  }
1875
- return { ok: true };
1876
- }
1877
-
1878
- // src/app/working-memory/handlers/close.ts
1879
- async function handleClose(params, repository, timestamp) {
1880
- if (!isTrustedHostMutationSource(params.source)) {
1881
- return createFailure(
1882
- "close_not_allowed",
1883
- "agenr_work close is reserved for /goal clear. Record progress with merge_checkpoint and leave the working set open."
927
+ try {
928
+ const branchMessages = extractSkelnBeforeTurnBranchMessages(context.sessionManager.getBranch());
929
+ const beforeTurnPatch = await runBeforeTurn(
930
+ {
931
+ sessionKey: scope.sessionKey,
932
+ currentTurnText,
933
+ recentTurns: extractRecentTurnsFromMessages(branchMessages),
934
+ policy: resolveBeforeTurnPolicy(services.skelnConfig.memoryPolicy)
935
+ },
936
+ services.beforeTurn
1884
937
  );
1885
- }
1886
- const closeReason = normalizeRequiredString(params.closeReason, "agenr_work close requires closeReason.");
1887
- if (!closeReason.ok) {
1888
- return closeReason;
1889
- }
1890
- const selection = await selectWorkingSet(params, repository);
1891
- if (!selection.ok) {
1892
- return selection;
1893
- }
1894
- const expectedRevision = resolveExpectedRevision(selection.workingSet.revision, params.expectedRevision, params.source);
1895
- if (!expectedRevision.ok) {
1896
- return expectedRevision;
1897
- }
1898
- if (isCloseManagedStatus(selection.workingSet.status)) {
1899
- return createFailure("terminal_status", `Working set ${selection.workingSet.id} is already ${selection.workingSet.status}.`, {
1900
- workingSetId: selection.workingSet.id,
1901
- status: selection.workingSet.status
938
+ return composeSkelnBeforeAgentStartResult({
939
+ baseSystemPrompt,
940
+ systemPrompt,
941
+ recallKind: "before_turn_recall",
942
+ recallText: mergeInjectionContent(compactionContext, formatAgenrBeforeTurnRecall(beforeTurnPatch)),
943
+ workingInjection
944
+ });
945
+ } catch (error) {
946
+ logInjectionFailure("before-turn", scope, error);
947
+ return composeSkelnBeforeAgentStartResult({
948
+ baseSystemPrompt,
949
+ systemPrompt,
950
+ recallKind: "before_turn_recall",
951
+ recallFailureReason: error instanceof Error ? error.message : String(error),
952
+ recallText: compactionContext,
953
+ workingInjection
1902
954
  });
1903
955
  }
1904
- const events = await repository.listWorkingEvents(selection.workingSet.id, CLOSE_EVENT_HISTORY_LIMIT);
1905
- const terminalStatus = resolveCloseTerminalStatus(params.closeMode);
1906
- const closePayload = buildWorkingCloseSnapshot({
1907
- workingSetId: selection.workingSet.id,
1908
- snapshot: selection.workingSet.snapshot,
1909
- currentRevision: selection.workingSet.revision,
1910
- closeReason: closeReason.value,
1911
- createEpisode: params.createEpisode,
1912
- eventSequences: events.map((event) => event.sequence),
1913
- now: timestamp
1914
- });
1915
- const writeResult = await repository.updateWorkingSet({
1916
- workingSetId: selection.workingSet.id,
1917
- expectedRevision: expectedRevision.value,
1918
- eventType: terminalStatus,
1919
- payload: {
1920
- closeReason: closeReason.value,
1921
- closeMode: params.closeMode ?? "close",
1922
- candidates: closePayload.candidates,
1923
- sourceRef: `working_set:${selection.workingSet.id}#rev:${selection.workingSet.revision}`
1924
- },
1925
- status: terminalStatus,
1926
- snapshot: closePayload.snapshot,
1927
- title: selection.workingSet.title,
1928
- objective: selection.workingSet.snapshot.objective,
1929
- closedAt: timestamp,
1930
- closeReason: closeReason.value,
1931
- actor: params.actor,
1932
- source: params.source,
1933
- now: timestamp
1934
- });
1935
- return toCloseResult(selection.workingSet.id, writeResult, closePayload.candidates);
1936
956
  }
1937
- function toCloseResult(workingSetId, writeResult, candidates) {
1938
- if (isWorkingSetWriteFailure(writeResult)) {
1939
- return writeFailureToResult(workingSetId, writeResult);
1940
- }
957
+ function composeSkelnBeforeAgentStartResult(input) {
958
+ const recallText = input.recallText?.trim();
959
+ const memoryTrace = buildBeforeAgentStartMemoryTrace({
960
+ baseSystemPrompt: input.baseSystemPrompt,
961
+ systemPrompt: input.systemPrompt,
962
+ recallKind: input.recallKind,
963
+ recallText,
964
+ recallSkippedReason: input.recallSkippedReason,
965
+ recallFailureReason: input.recallFailureReason,
966
+ workingContextTrace: input.workingInjection?.trace
967
+ });
1941
968
  return {
1942
- ok: true,
1943
- action: "close",
1944
- workingSet: writeResult.workingSet,
1945
- event: writeResult.event,
1946
- candidates
969
+ systemPrompt: input.systemPrompt,
970
+ ...recallText ? { message: buildAgenrSkelnInjectionMessage(recallText) } : {},
971
+ ...input.workingInjection?.transientMessages ? { transientMessages: input.workingInjection.transientMessages } : {},
972
+ ...input.workingInjection?.workingContextAudit ? { workingContextAudit: input.workingInjection.workingContextAudit } : {},
973
+ ...memoryTrace.length > 0 ? { memoryTrace } : {}
1947
974
  };
1948
975
  }
1949
-
1950
- // src/app/working-memory/goal-generation.ts
1951
- var INITIAL_GOAL_GENERATION = 1;
1952
- function readGoalGeneration(snapshot) {
1953
- return snapshot?.goalGeneration ?? INITIAL_GOAL_GENERATION;
1954
- }
1955
- function nextGoalGenerationAfterObjectiveChange(snapshot, nextObjective) {
1956
- if (snapshot.objective === nextObjective) {
1957
- return readGoalGeneration(snapshot);
976
+ async function resolveWorkingContextInjection(services, scope, sourceRef) {
977
+ const gate = resolveWorkingContextGate(services.capabilities.workingMemory, services.skelnConfig.memoryPolicy);
978
+ if (!gate.ok) {
979
+ return { trace: traceMemorySkipped("working_context", gate.reason) };
1958
980
  }
1959
- return readGoalGeneration(snapshot) + 1;
981
+ return loadWorkingContextProjection(services, scope, sourceRef);
1960
982
  }
1961
-
1962
- // src/app/working-memory/resolve-create-scope.ts
1963
- async function resolveCreateScope(params, repository) {
1964
- const workingSetId = params.workingSetId?.trim();
1965
- if (workingSetId) {
1966
- const workingSet = await repository.getWorkingSet(workingSetId);
1967
- if (workingSet) {
1968
- return createFailure("active_set_exists", "A working set is already active for this scope.", {
1969
- workingSetId: workingSet.id,
1970
- scopeKey: workingSet.scopeKey
1971
- });
1972
- }
1973
- return createFailure("not_found", `Working set ${workingSetId} was not found.`, { workingSetId });
1974
- }
1975
- const lookup = await lookupCurrentWorkingSets(params.scope, repository);
1976
- if (!lookup.ok) {
1977
- return lookup;
1978
- }
1979
- if (lookup.matches.length === 1) {
1980
- const existing = lookup.matches[0];
1981
- return createFailure("active_set_exists", "A working set already exists for this scope.", {
1982
- workingSetId: existing.id,
1983
- scopeKey: lookup.scope.scopeKey
983
+ async function loadWorkingContextProjection(services, scope, sourceRef) {
984
+ try {
985
+ const projection = await services.workingMemory.renderProjection({
986
+ sourceRef,
987
+ scope: toWorkingScopeFromSkelnSession(scope)
1984
988
  });
989
+ if (!shouldInjectWorkingContext(projection)) {
990
+ const reason = projection.renderMode !== "full" ? "working projection stub" : "empty working projection";
991
+ return { trace: traceMemorySkipped("working_context", reason) };
992
+ }
993
+ const workingContextAudit = toWorkingContextAuditPointer(projection);
994
+ return {
995
+ transientMessages: [buildAgenrSkelnInjectionMessage(projection.content)],
996
+ ...workingContextAudit ? { workingContextAudit } : {},
997
+ trace: traceWorkingContextInjected(workingContextAudit, projection.content)
998
+ };
999
+ } catch (error) {
1000
+ const message = error instanceof Error ? error.message : String(error);
1001
+ console.warn(`[agenr] working-context projection failed for session=${scope.sessionId} key=${scope.sessionKey}: ${message}`);
1002
+ return { trace: traceMemoryFailed("working_context", message) };
1985
1003
  }
1986
- return {
1987
- ok: true,
1988
- scope: lookup.scope
1989
- };
1004
+ }
1005
+ function logInjectionFailure(phase, scope, error) {
1006
+ const message = error instanceof Error ? error.message : String(error);
1007
+ console.warn(`[agenr] ${phase} recall failed for session=${scope.sessionId} key=${scope.sessionKey}: ${message}`);
1990
1008
  }
1991
1009
 
1992
- // src/app/working-memory/handlers/create.ts
1993
- async function handleCreate(params, repository, timestamp, sourceLabel) {
1994
- const operation = params.operation;
1995
- if (!operation || operation.type !== "set_objective") {
1996
- return createFailure("invalid_request", "agenr_work create requires a set_objective operation.");
1997
- }
1998
- const updateReason = normalizeRequiredString(params.updateReason, "agenr_work create requires updateReason.");
1999
- if (!updateReason.ok) {
2000
- return updateReason;
2001
- }
2002
- const scopeResolution = await resolveCreateScope(params, repository);
2003
- if (!scopeResolution.ok) {
2004
- return scopeResolution;
2005
- }
2006
- const { scope } = scopeResolution;
2007
- const initialBudget = params.initialBudget ? validateWorkingBudgetState(params.initialBudget) : { ok: true };
2008
- if (!initialBudget.ok) {
2009
- return initialBudget;
2010
- }
2011
- const created = await repository.createWorkingSet({
2012
- scope,
2013
- title: operation.title,
2014
- objective: operation.objective,
2015
- status: "active",
2016
- snapshot: {
2017
- goalGeneration: INITIAL_GOAL_GENERATION,
2018
- objective: operation.objective,
2019
- continuation: { policy: params.continuationPolicy ?? "manual" },
2020
- ...params.initialBudget ? { budgets: params.initialBudget } : {},
2021
- lastMaterialChange: updateReason.value
2022
- },
2023
- actor: params.actor,
2024
- source: params.source,
2025
- sourceLabel,
2026
- sessionId: scope.sessionId,
2027
- now: timestamp
2028
- });
2029
- if (isWorkingSetCreateFailure(created)) {
2030
- return createFailure("active_set_exists", "A working set already exists for this scope.", {
2031
- scopeKey: created.scopeKey
1010
+ // src/adapters/skeln/hooks/subagent-findings.ts
1011
+ var SUBAGENT_OUTCOME_MAX_CHARS = 3e3;
1012
+ var SUBAGENT_RESULT_LIMIT = 6;
1013
+ async function recordSkelnSubagentFindings(servicesPromise, resolveScope, context, event) {
1014
+ const commandNote = buildSkelnSubagentCommandNote(event, (/* @__PURE__ */ new Date()).toISOString());
1015
+ if (!commandNote) {
1016
+ return;
1017
+ }
1018
+ try {
1019
+ const [services, scope] = await Promise.all([servicesPromise, resolveScope(context)]);
1020
+ const result = await services.workingMemory.run({
1021
+ action: "update",
1022
+ scope: toWorkingScopeFromSkelnSession(scope),
1023
+ operation: {
1024
+ type: "add_command_note",
1025
+ command: commandNote
1026
+ },
1027
+ updateReason: "Recorded bounded subagent findings on the parent working set.",
1028
+ actor: "runtime",
1029
+ source: "lifecycle_hook"
2032
1030
  });
2033
- }
2034
- return {
2035
- ok: true,
2036
- action: "create",
2037
- workingSet: created.workingSet,
2038
- event: created.event,
2039
- projection: createToolSuccessProjection(created.workingSet, "create", timestamp)
2040
- };
2041
- }
2042
-
2043
- // src/app/working-memory/handlers/get.ts
2044
- async function handleGet(params, repository, timestamp) {
2045
- const selection = await selectWorkingSet(params, repository);
2046
- if (!selection.ok) {
2047
- return selection;
2048
- }
2049
- const events = params.includeEvents ? await repository.listWorkingEvents(selection.workingSet.id, normalizeEventLimit(params.eventLimit)) : void 0;
2050
- return {
2051
- ok: true,
2052
- action: "get",
2053
- workingSet: selection.workingSet,
2054
- ...events ? { events } : {},
2055
- projection: createToolSuccessProjection(selection.workingSet, "get", timestamp)
2056
- };
2057
- }
2058
-
2059
- // src/app/working-memory/handlers/list.ts
2060
- async function handleList(params, repository) {
2061
- const scopeResolution = params.scope ? resolveWorkingScope(params.scope) : void 0;
2062
- if (scopeResolution && !scopeResolution.ok) {
2063
- return createFailure("missing_scope", scopeResolution.message);
2064
- }
2065
- const workingSets = await repository.listWorkingSets({
2066
- ...scopeResolution?.ok ? { scope: scopeResolution.scope } : {},
2067
- limit: normalizeListLimit(params.listLimit)
2068
- });
2069
- return {
2070
- ok: true,
2071
- action: "list",
2072
- workingSets
2073
- };
2074
- }
2075
-
2076
- // src/app/working-memory/apply-operation.ts
2077
- function applyOperation(record, operation, updateReason) {
2078
- const snapshot = { ...record.snapshot, lastMaterialChange: updateReason };
2079
- let status = record.status;
2080
- let title = record.title;
2081
- let objective = record.snapshot.objective;
2082
- switch (operation.type) {
2083
- case "set_objective":
2084
- snapshot.goalGeneration = nextGoalGenerationAfterObjectiveChange(snapshot, operation.objective);
2085
- snapshot.objective = operation.objective;
2086
- objective = operation.objective;
2087
- title = operation.title ?? title;
2088
- break;
2089
- case "replace_plan":
2090
- snapshot.currentPlan = operation.currentPlan;
2091
- snapshot.nextActions = operation.nextActions;
2092
- break;
2093
- case "merge_checkpoint":
2094
- snapshot.checkpoint = operation.checkpoint;
2095
- if (operation.checkpoint.nextActions) {
2096
- snapshot.nextActions = operation.checkpoint.nextActions.map((text) => ({ text, status: "pending" }));
2097
- }
2098
- if (operation.checkpoint.blockers) {
2099
- snapshot.blockers = operation.checkpoint.blockers;
2100
- }
2101
- break;
2102
- case "add_file_note":
2103
- snapshot.files = [...snapshot.files ?? [], operation.file];
2104
- break;
2105
- case "add_command_note":
2106
- snapshot.commands = [...snapshot.commands ?? [], operation.command];
2107
- break;
2108
- case "record_decision":
2109
- snapshot.decisions = [...snapshot.decisions ?? [], operation.decision];
2110
- break;
2111
- case "record_assumption":
2112
- snapshot.assumptions = [...snapshot.assumptions ?? [], operation.assumption];
2113
- break;
2114
- case "set_next_actions":
2115
- snapshot.nextActions = operation.nextActions;
2116
- break;
2117
- case "set_status":
2118
- if (isCloseManagedStatus(operation.status)) {
2119
- return createFailure("invalid_request", "Use agenr_work close for closed or abandoned terminal states.");
2120
- }
2121
- status = operation.status;
2122
- break;
2123
- case "add_candidate":
2124
- snapshot.candidates = [...snapshot.candidates ?? [], operation.candidate];
2125
- break;
2126
- case "configure_budget": {
2127
- const budget = mergeBudgetState(snapshot.budgets, operation.budget);
2128
- if (!budget.ok) {
2129
- return budget;
2130
- }
2131
- const limited = applyBudgetLimitedStatus(status, budget.value, updateReason);
2132
- snapshot.budgets = limited.budgets;
2133
- status = limited.status;
2134
- break;
2135
- }
2136
- case "account_usage": {
2137
- const budget = applyUsageDelta(snapshot.budgets, operation.usage, updateReason);
2138
- if (!budget.ok) {
2139
- return budget;
2140
- }
2141
- const limited = applyBudgetLimitedStatus(status, budget.value, operation.usage.recordedAt ?? updateReason);
2142
- snapshot.budgets = limited.budgets;
2143
- status = limited.status;
2144
- break;
1031
+ if (!result.ok && !isExpectedSubagentFindingSkip(result.code)) {
1032
+ console.warn(`[agenr] subagent findings working-memory update failed: ${result.message}`);
2145
1033
  }
2146
- case "set_continuation_policy":
2147
- snapshot.continuation = pruneContinuation({
2148
- ...snapshot.continuation,
2149
- policy: operation.policy,
2150
- ...operation.resumeAfter !== void 0 ? { resumeAfter: operation.resumeAfter } : {},
2151
- ...operation.staleAfter !== void 0 ? { staleAfter: operation.staleAfter } : {},
2152
- ...operation.stopReason !== void 0 ? { stopReason: operation.stopReason } : {}
2153
- });
2154
- break;
1034
+ } catch (error) {
1035
+ console.warn(`[agenr] subagent findings capture failed: ${formatErrorMessage(error)}`);
2155
1036
  }
2156
- return {
2157
- ok: true,
2158
- snapshot,
2159
- status,
2160
- title,
2161
- objective
2162
- };
2163
1037
  }
2164
- function mergeBudgetState(current, update) {
2165
- const validation = validateWorkingBudgetState(update);
2166
- if (!validation.ok) {
2167
- return validation;
1038
+ function buildSkelnSubagentCommandNote(event, observedAt) {
1039
+ if (event.toolName !== "subagent" || event.isError) {
1040
+ return void 0;
2168
1041
  }
2169
- return {
2170
- ok: true,
2171
- value: pruneBudget({
2172
- ...current ?? {},
2173
- ...update
2174
- })
2175
- };
2176
- }
2177
- function applyUsageDelta(current, usage, limitedAt) {
2178
- const validation = validateWorkingUsageDelta(usage);
2179
- if (!validation.ok) {
2180
- return validation;
1042
+ const details = isRecord(event.details) ? event.details : void 0;
1043
+ if (!details) {
1044
+ return void 0;
2181
1045
  }
2182
- const next = {
2183
- ...current ?? {},
2184
- tokenUsed: addDelta(current?.tokenUsed, usage.tokenDelta),
2185
- wallClockUsedSeconds: addDelta(current?.wallClockUsedSeconds, usage.wallClockSecondsDelta),
2186
- turnsUsed: addDelta(current?.turnsUsed, usage.turnDelta)
2187
- };
2188
- const limitReason = resolveBudgetLimitReason(next);
2189
- return {
2190
- ok: true,
2191
- value: pruneBudget({
2192
- ...next,
2193
- ...limitReason ? { limitReason, limitedAt: usage.recordedAt ?? limitedAt } : {}
2194
- })
2195
- };
2196
- }
2197
- function applyBudgetLimitedStatus(currentStatus, budget, limitedAt) {
2198
- const limitReason = resolveBudgetLimitReason(budget);
2199
- if (!limitReason || !budget) {
2200
- return { status: currentStatus, budgets: budget };
1046
+ const mode = readOptionalTrimmedString(details.mode) ?? "unknown";
1047
+ const artifactPath = readOptionalTrimmedString(details.artifactPath);
1048
+ const results = Array.isArray(details.results) ? details.results.flatMap(normalizeDelegationResult).slice(0, SUBAGENT_RESULT_LIMIT) : [];
1049
+ if (results.length === 0 && !artifactPath) {
1050
+ return void 0;
2201
1051
  }
1052
+ const resultLines = results.map(
1053
+ (result) => `- ${result.agent}${result.name ? `/${result.name}` : ""}: ${result.status}${result.summary ? ` - ${result.summary}` : ""}`
1054
+ );
1055
+ const outcome = truncateText(
1056
+ [
1057
+ `mode=${mode}`,
1058
+ ...resultLines,
1059
+ ...artifactPath ? [`artifact=${artifactPath}`] : [],
1060
+ ...results.length >= SUBAGENT_RESULT_LIMIT ? ["additional subagent results omitted"] : []
1061
+ ].join("\n"),
1062
+ SUBAGENT_OUTCOME_MAX_CHARS
1063
+ );
2202
1064
  return {
2203
- status: "budget_limited",
2204
- budgets: pruneBudget({
2205
- ...budget,
2206
- limitReason: budget.limitReason ?? limitReason,
2207
- limitedAt: budget.limitedAt ?? limitedAt
2208
- })
1065
+ command: `subagent ${mode}`,
1066
+ outcome,
1067
+ observedAt
2209
1068
  };
2210
1069
  }
2211
- function resolveBudgetLimitReason(budget) {
2212
- if (!budget) {
2213
- return void 0;
2214
- }
2215
- if (budget.tokenBudget !== void 0 && (budget.tokenUsed ?? 0) >= budget.tokenBudget) {
2216
- return "token";
2217
- }
2218
- if (budget.wallClockBudgetSeconds !== void 0 && (budget.wallClockUsedSeconds ?? 0) >= budget.wallClockBudgetSeconds) {
2219
- return "wall_clock";
1070
+ function normalizeDelegationResult(value) {
1071
+ const record = isRecord(value) ? value : void 0;
1072
+ if (!record) {
1073
+ return [];
2220
1074
  }
2221
- if (budget.turnBudget !== void 0 && (budget.turnsUsed ?? 0) >= budget.turnBudget) {
2222
- return "turn";
1075
+ const agent = readOptionalTrimmedString(record.agent);
1076
+ const status = readOptionalTrimmedString(record.status);
1077
+ if (!agent || !status) {
1078
+ return [];
2223
1079
  }
2224
- return void 0;
1080
+ const stdout = readOptionalTrimmedString(record.stdout);
1081
+ const stderr = readOptionalTrimmedString(record.stderr);
1082
+ const name = readOptionalTrimmedString(record.name);
1083
+ const summary = firstMeaningfulLine(stdout ?? stderr);
1084
+ return [
1085
+ {
1086
+ agent,
1087
+ ...name ? { name } : {},
1088
+ status,
1089
+ ...summary ? { summary } : {}
1090
+ }
1091
+ ];
2225
1092
  }
2226
- function addDelta(current, delta) {
2227
- if (delta === void 0) {
2228
- return current;
2229
- }
2230
- return (current ?? 0) + delta;
1093
+ function isExpectedSubagentFindingSkip(code) {
1094
+ return code === "feature_disabled" || code === "missing_active_set" || code === "missing_scope" || code === "misconfigured";
2231
1095
  }
2232
- function pruneBudget(budget) {
2233
- return Object.fromEntries(Object.entries(budget).filter(([, value]) => value !== void 0));
1096
+ function firstMeaningfulLine(value) {
1097
+ const line = value?.split(/\r?\n/u).map((part) => part.trim()).find((part) => part.length > 0);
1098
+ return line ? truncateText(line, 500) : void 0;
2234
1099
  }
2235
- function pruneContinuation(continuation) {
2236
- if (!continuation) {
2237
- return void 0;
1100
+ function truncateText(value, maxChars) {
1101
+ if (value.length <= maxChars) {
1102
+ return value;
2238
1103
  }
2239
- const pruned = Object.fromEntries(Object.entries(continuation).filter(([, value]) => value !== void 0));
2240
- return Object.keys(pruned).length > 0 ? pruned : void 0;
1104
+ return `${value.slice(0, Math.max(0, maxChars - 15)).trimEnd()} [truncated]`;
2241
1105
  }
2242
1106
 
2243
- // src/app/working-memory/handlers/commit-applied-change.ts
2244
- function isAppliedWorkingSetCommitFailure(result) {
2245
- return "kind" in result;
2246
- }
2247
- async function commitAppliedWorkingSetChange(repository, input) {
2248
- if (input.operation.type === "account_usage") {
2249
- const writeResult2 = await repository.patchWorkingSetUsage({
2250
- workingSetId: input.workingSetId,
2251
- expectedRevision: input.expectedRevision,
2252
- status: input.applied.status,
2253
- snapshot: input.applied.snapshot,
2254
- title: input.applied.title,
2255
- objective: input.applied.objective,
2256
- now: input.now
2257
- });
2258
- if (isWorkingSetWriteFailure(writeResult2)) {
2259
- return writeResult2;
2260
- }
2261
- return {
2262
- type: "usage_patch",
2263
- workingSet: writeResult2.workingSet
2264
- };
2265
- }
2266
- const writeResult = await repository.updateWorkingSet({
2267
- workingSetId: input.workingSetId,
2268
- expectedRevision: input.expectedRevision,
2269
- eventType: input.operation.type,
2270
- payload: {
2271
- operation: input.operation,
2272
- updateReason: input.updateReason
2273
- },
2274
- status: input.applied.status,
2275
- snapshot: input.applied.snapshot,
2276
- title: input.applied.title,
2277
- objective: input.applied.objective,
2278
- actor: input.actor,
2279
- source: input.source,
2280
- now: input.now
1107
+ // src/adapters/skeln/episode/shutdown-episode-write.ts
1108
+ function buildSkelnSessionShutdownEpisodeWork(params) {
1109
+ const target = resolveSkelnSessionEpisodeTarget(params.context);
1110
+ const shutdownWork = params.event.reason === "quit" ? writeScopedSkelnShutdownEpisode(params.servicesPromise, target, params.logger).finally(() => closeSkelnServicesAfterShutdown(params.servicesPromise)) : writeScopedSkelnShutdownEpisode(params.servicesPromise, target, params.logger);
1111
+ return shutdownWork.catch((error) => {
1112
+ logSkelnShutdownEpisodeFailure(error, params.logger);
2281
1113
  });
2282
- if (isWorkingSetWriteFailure(writeResult)) {
2283
- return writeResult;
2284
- }
2285
- return {
2286
- type: "semantic",
2287
- workingSet: writeResult.workingSet,
2288
- event: writeResult.event
2289
- };
2290
1114
  }
2291
-
2292
- // src/app/working-memory/handlers/prepare-external-mutation.ts
2293
- async function handlePrepareExternalGoalMutation(params, repository, timestamp) {
2294
- if (!isTrustedHostMutationSource(params.source)) {
2295
- return createFailure("invalid_request", "prepare_external_goal_mutation is reserved for trusted host runtime paths.");
2296
- }
2297
- const selection = await selectWorkingSet(params, repository);
2298
- if (!selection.ok) {
2299
- if (selection.code === "missing_active_set") {
2300
- return {
2301
- ok: true,
2302
- action: "prepare_external_goal_mutation",
2303
- prepared: false,
2304
- events: []
2305
- };
2306
- }
2307
- return selection;
2308
- }
2309
- if (params.requireCheckpoint && !params.checkpoint && !selection.workingSet.snapshot.checkpoint) {
2310
- return createFailure("invalid_request", `Active goal requires a checkpoint before ${params.mutationKind}.`, {
2311
- workingSetId: selection.workingSet.id,
2312
- revision: selection.workingSet.revision,
2313
- mutationKind: params.mutationKind
2314
- });
1115
+ function scheduleSkelnSessionShutdownEpisodeWrite(params) {
1116
+ const work = buildSkelnSessionShutdownEpisodeWork(params);
1117
+ if (params.event.reason === "quit" && params.event.deferWork) {
1118
+ params.event.deferWork(work);
1119
+ return Promise.resolve();
2315
1120
  }
2316
- if (!isMutableWorkingSetStatus(selection.workingSet.status)) {
2317
- return {
2318
- ok: true,
2319
- action: "prepare_external_goal_mutation",
2320
- prepared: true,
2321
- workingSet: selection.workingSet,
2322
- events: []
2323
- };
1121
+ if (params.event.reason === "quit") {
1122
+ return work;
2324
1123
  }
2325
- const events = [];
2326
- let workingSet = selection.workingSet;
2327
- for (const { operation, updateReason } of resolvePrepareOperations(params)) {
2328
- const applied = applyOperation(workingSet, operation, updateReason);
2329
- if (!applied.ok) {
2330
- return applied;
2331
- }
2332
- const writeResult = await commitAppliedWorkingSetChange(repository, {
2333
- workingSetId: workingSet.id,
2334
- expectedRevision: workingSet.revision,
2335
- operation,
2336
- updateReason,
2337
- applied,
2338
- actor: params.actor,
2339
- source: params.source,
2340
- now: timestamp
2341
- });
2342
- if (isAppliedWorkingSetCommitFailure(writeResult)) {
2343
- return writeFailureToResult(workingSet.id, writeResult);
2344
- }
2345
- workingSet = writeResult.workingSet;
2346
- if (writeResult.type === "semantic") {
2347
- events.push(writeResult.event);
2348
- }
1124
+ void work;
1125
+ return Promise.resolve();
1126
+ }
1127
+ function logSkelnShutdownEpisodeFailure(error, logger) {
1128
+ const log = logger ?? console;
1129
+ log.warn(`[agenr] skeln shutdown episode failed: ${formatErrorMessage(error)}`);
1130
+ }
1131
+ async function closeSkelnServicesAfterShutdown(servicesPromise) {
1132
+ try {
1133
+ const services = await servicesPromise;
1134
+ await services.close();
1135
+ } catch {
2349
1136
  }
2350
- return {
2351
- ok: true,
2352
- action: "prepare_external_goal_mutation",
2353
- prepared: true,
2354
- workingSet,
2355
- events
2356
- };
2357
1137
  }
2358
- function resolvePrepareOperations(params) {
2359
- const operations = [];
2360
- if (params.usage) {
2361
- operations.push({
2362
- operation: { type: "account_usage", usage: params.usage },
2363
- updateReason: params.updateReason ?? `Accounted progress before external goal mutation (${params.mutationKind}).`
2364
- });
1138
+ async function writeScopedSkelnShutdownEpisode(servicesPromise, target, logger) {
1139
+ const services = await servicesPromise;
1140
+ if (!isPluginEpisodeWriteEnabled(services.skelnConfig.memoryPolicy)) {
1141
+ return;
2365
1142
  }
2366
- if (params.checkpoint) {
2367
- operations.push({
2368
- operation: { type: "merge_checkpoint", checkpoint: params.checkpoint },
2369
- updateReason: params.updateReason ?? `Recorded checkpoint before external goal mutation (${params.mutationKind}).`
2370
- });
1143
+ await withEpisodeWriteGuard({ port: services.dreaming, dbPath: services.config.dbPath }, async () => writeSkelnShutdownEpisode({ target, services, logger }));
1144
+ await runSkelnPostSessionLightDream(services, logger);
1145
+ }
1146
+ async function runSkelnPostSessionLightDream(services, logger) {
1147
+ const log = logger ?? console;
1148
+ try {
1149
+ const result = await maybeRunLightDream(
1150
+ { trigger: "post_session" },
1151
+ {
1152
+ port: services.dreaming,
1153
+ dbPath: services.config.dbPath,
1154
+ config: services.agenrConfig,
1155
+ embedding: services.embedding,
1156
+ ...services.claimExtraction ? { createClaimExtractionLlm: () => services.claimExtraction.llm } : {}
1157
+ }
1158
+ );
1159
+ if (result.status === "ran") {
1160
+ log.info(`[agenr] skeln shutdown light dream completed run=${result.result.runId}`);
1161
+ } else if (result.reason === "run_in_progress" || result.reason === "episode_write_in_progress") {
1162
+ log.info(`[agenr] skeln shutdown light dream skipped reason=${result.reason}`);
1163
+ }
1164
+ } catch (error) {
1165
+ log.warn(`[agenr] skeln shutdown light dream failed: ${formatErrorMessage(error)}`);
2371
1166
  }
2372
- return operations;
2373
1167
  }
2374
1168
 
2375
- // src/app/working-memory/handlers/update.ts
2376
- async function handleUpdate(params, repository, timestamp) {
2377
- const operation = params.operation;
2378
- if (!operation) {
2379
- return createFailure("invalid_request", "agenr_work update requires a typed operation.");
2380
- }
2381
- if (isTrustedHostOnlyWorkingOperation(operation.type) && !isTrustedHostMutationSource(params.source)) {
2382
- return createFailure("invalid_request", `${operation.type} is reserved for trusted host runtime paths.`);
2383
- }
2384
- const updateReason = normalizeRequiredString(params.updateReason, "agenr_work update requires updateReason.");
2385
- if (!updateReason.ok) {
2386
- return updateReason;
2387
- }
2388
- const selection = await selectWorkingSet(params, repository);
2389
- if (!selection.ok) {
2390
- return selection;
2391
- }
2392
- const expectedRevision = resolveExpectedRevision(selection.workingSet.revision, params.expectedRevision, params.source);
2393
- if (!expectedRevision.ok) {
2394
- return expectedRevision;
2395
- }
2396
- if (!isMutableWorkingSetStatus(selection.workingSet.status)) {
2397
- return createFailure("terminal_status", `Working set ${selection.workingSet.id} is already ${selection.workingSet.status}.`, {
2398
- workingSetId: selection.workingSet.id,
2399
- status: selection.workingSet.status
2400
- });
2401
- }
2402
- const applied = applyOperation(selection.workingSet, operation, updateReason.value);
2403
- if (!applied.ok) {
2404
- return applied;
2405
- }
2406
- const writeResult = await commitAppliedWorkingSetChange(repository, {
2407
- workingSetId: selection.workingSet.id,
2408
- expectedRevision: expectedRevision.value,
2409
- operation,
2410
- updateReason: updateReason.value,
2411
- applied,
2412
- actor: params.actor,
2413
- source: params.source,
2414
- now: timestamp
2415
- });
2416
- if (isAppliedWorkingSetCommitFailure(writeResult)) {
2417
- return writeFailureToResult(selection.workingSet.id, writeResult);
2418
- }
1169
+ // src/app/features/capabilities.ts
1170
+ function resolveRuntimeCapabilities(featureFlags, input = {}) {
1171
+ const sessionMemoryFlagEnabled = featureFlags.sessionTreeLineage || featureFlags.sessionTreeCompaction;
1172
+ const sessionMemory = resolveRepositoryBackedCapability(sessionMemoryFlagEnabled, input.sessionMemoryRepository);
2419
1173
  return {
2420
- ok: true,
2421
- action: "update",
2422
- workingSet: writeResult.workingSet,
2423
- ...writeResult.type === "semantic" ? { event: writeResult.event } : {},
2424
- projection: createToolSuccessProjection(writeResult.workingSet, "update", timestamp)
1174
+ workingMemory: resolveRepositoryBackedCapability(featureFlags.workingMemory, input.workingMemoryRepository),
1175
+ sessionMemory,
1176
+ shutdownEpisodes: sessionMemory === "enabled",
1177
+ goalContinuation: resolveGoalContinuationCapability(featureFlags.goalContinuation, input.goalContinuationHostPort)
2425
1178
  };
2426
1179
  }
2427
-
2428
- // src/app/working-memory/ready.ts
2429
- var WORKING_MEMORY_DISABLED_MESSAGE = "Working memory is disabled by the workingMemory feature flag.";
2430
- var WORKING_MEMORY_MISCONFIGURED_MESSAGE = "Working memory is enabled, but no working-memory repository was wired into the runtime.";
2431
- function workingMemoryNotReadyFailure(input) {
2432
- if (!input.featureEnabled) {
2433
- return createFailure("feature_disabled", WORKING_MEMORY_DISABLED_MESSAGE);
2434
- }
2435
- if (!input.repository) {
2436
- return createFailure("misconfigured", WORKING_MEMORY_MISCONFIGURED_MESSAGE);
1180
+ function resolveRepositoryBackedCapability(featureEnabled, repository) {
1181
+ if (!featureEnabled) {
1182
+ return "disabled";
2437
1183
  }
2438
- return null;
1184
+ return repository ? "enabled" : "misconfigured";
2439
1185
  }
2440
-
2441
- // src/app/working-memory/service.ts
2442
- function createWorkingMemoryService(featureFlags, deps = {}) {
2443
- const featureEnabled = featureFlags.workingMemory;
2444
- const repository = deps.repository;
2445
- const now = () => deps.now ? deps.now().toISOString() : (/* @__PURE__ */ new Date()).toISOString();
2446
- const readiness = () => workingMemoryNotReadyFailure({ featureEnabled, repository });
2447
- return {
2448
- async run(params) {
2449
- const notReady = readiness();
2450
- if (notReady) {
2451
- return notReady;
2452
- }
2453
- switch (params.action) {
2454
- case "get":
2455
- return handleGet(params, repository, now());
2456
- case "list":
2457
- return handleList(params, repository);
2458
- case "create":
2459
- return handleCreate(params, repository, now(), deps.sourceLabel);
2460
- case "update":
2461
- return handleUpdate(params, repository, now());
2462
- case "close":
2463
- return handleClose(params, repository, now());
2464
- }
2465
- },
2466
- async prepareExternalGoalMutation(params) {
2467
- const notReady = readiness();
2468
- if (notReady) {
2469
- return notReady;
2470
- }
2471
- return handlePrepareExternalGoalMutation(params, repository, now());
2472
- },
2473
- async renderProjection(input) {
2474
- const request = typeof input === "string" ? { sourceRef: input } : input;
2475
- if (!featureEnabled) {
2476
- return createWorkingContextStubProjection({
2477
- reason: "feature_disabled",
2478
- sourceRef: request.sourceRef
2479
- });
2480
- }
2481
- if (!repository) {
2482
- return createWorkingContextStubProjection({
2483
- reason: "misconfigured",
2484
- sourceRef: request.sourceRef
2485
- });
2486
- }
2487
- const selection = await selectWorkingSet({ workingSetId: request.workingSetId, scope: request.scope }, repository);
2488
- if (!selection.ok) {
2489
- return createWorkingContextStubProjection({
2490
- reason: selection.code === "ambiguous_scope" ? "ambiguous_scope" : "missing_active_set",
2491
- sourceRef: request.sourceRef
2492
- });
2493
- }
2494
- return createWorkingContextFullProjection(selection.workingSet, request.sourceRef);
2495
- }
2496
- };
1186
+ function resolveGoalContinuationCapability(featureEnabled, hostPort) {
1187
+ if (!featureEnabled) {
1188
+ return "disabled";
1189
+ }
1190
+ return hostPort ? "enabled" : "misconfigured";
2497
1191
  }
2498
1192
 
2499
- // src/app/host-memory/create-host-memory-services.ts
2500
- function createHostMemoryServices(featureFlags, options = {}) {
2501
- const workingMemory = createWorkingMemoryService(featureFlags, {
2502
- repository: options.workingMemoryRepository,
2503
- sourceLabel: options.workingMemorySourceLabel
2504
- });
1193
+ // src/app/features/runtime-policy.ts
1194
+ function resolveRuntimePolicy(featureFlags, input = {}) {
2505
1195
  return {
2506
- workingMemory,
2507
- goalContinuation: createGoalContinuationService(featureFlags, options.goalContinuationHostPort),
2508
- routeSessionMemoryTrigger: (event) => routeSessionMemoryTrigger(event, featureFlags, {
2509
- repository: options.sessionMemoryRepository,
2510
- workingMemory
2511
- })
1196
+ featureFlags,
1197
+ capabilities: resolveRuntimeCapabilities(featureFlags, {
1198
+ workingMemoryRepository: input.workingMemoryRepository,
1199
+ sessionMemoryRepository: input.sessionMemoryRepository,
1200
+ goalContinuationHostPort: input.goalContinuationHostPort
1201
+ }),
1202
+ ...input.memoryPolicy ? { memoryPolicy: input.memoryPolicy } : {}
2512
1203
  };
2513
1204
  }
2514
1205
 
@@ -2518,13 +1209,13 @@ async function createAgenrSkelnServices(config = {}) {
2518
1209
  config,
2519
1210
  readSlotPolicies: (hostConfig) => hostConfig.memoryPolicy?.slotPolicies,
2520
1211
  resolveClaimExtraction: ({ agenrConfig }) => createClaimExtractionFromAgenrConfig(agenrConfig),
2521
- extend: ({ config: hostConfig, resolvedConfig, agenrConfig, runtimeServices }) => {
1212
+ extend: async ({ config: hostConfig, resolvedConfig, agenrConfig, runtimeServices }) => {
2522
1213
  const featureFlags = resolveAgenrFeatureFlags({
2523
1214
  ...agenrConfig.features,
2524
1215
  ...hostConfig.featureFlags
2525
1216
  });
2526
1217
  const goalContinuationHostPort = void 0;
2527
- const hostMemory = createHostMemoryServices(featureFlags, {
1218
+ const hostMemory = await createHostMemoryServices(featureFlags, {
2528
1219
  workingMemoryRepository: runtimeServices.workingMemoryRepository,
2529
1220
  sessionMemoryRepository: runtimeServices.sessionMemoryRepository,
2530
1221
  workingMemorySourceLabel: "skeln",
@@ -2650,10 +1341,7 @@ function registerAgenrSkelnFetchTool(skeln, servicesPromise, resolveScope) {
2650
1341
  label: "Agenr Fetch",
2651
1342
  description: "Fetch the full body and metadata for one durable memory entry by id or subject.",
2652
1343
  promptSnippet: "Use agenr_fetch after agenr_recall when you need the full entry body for an id shown in recall results.",
2653
- promptGuidelines: [
2654
- "Provide exactly one target selector: id or subject.",
2655
- "Prefer id from agenr_recall when preview_truncated=true or when exact wording matters."
2656
- ],
1344
+ promptGuidelines: [MEMORY_DOCTRINE.update.targetSelector, MEMORY_DOCTRINE.fetch.preferId],
2657
1345
  parameters: toolSchema(FETCH_TOOL_PARAMETERS),
2658
1346
  execute: async (_toolCallId, rawParams, _signal, _onUpdate, context) => {
2659
1347
  try {
@@ -2904,13 +1592,9 @@ function registerAgenrSkelnRecallTool(skeln, servicesPromise, resolveScope) {
2904
1592
  skeln.registerTool({
2905
1593
  name: "agenr_recall",
2906
1594
  label: "Agenr Recall",
2907
- description: "Retrieve knowledge from agenr long-term memory. Use mode=auto for normal use, including exact facts, historical-state questions, time-bounded episode questions, and procedural questions.",
1595
+ description: buildSkelnRecallToolDescription(),
2908
1596
  promptSnippet: "Use agenr_recall to retrieve durable memory, prior episode summaries, or canonical procedures from agenr.",
2909
- promptGuidelines: [
2910
- "Use focused natural-language queries instead of broad 'everything' searches.",
2911
- "Use mode=procedures for how-to or checklist questions, and mode=episodes for what-happened questions tied to time or sessions.",
2912
- "Use asOf when the user asks what was true at an earlier point in time."
2913
- ],
1597
+ promptGuidelines: buildSkelnRecallToolGuidelines(),
2914
1598
  parameters: toolSchema(RECALL_TOOL_PARAMETERS),
2915
1599
  execute: async (_toolCallId, rawParams, _signal, _onUpdate, context) => {
2916
1600
  try {
@@ -2933,42 +1617,61 @@ function registerAgenrSkelnStoreTool(skeln, servicesPromise, resolveScope) {
2933
1617
  skeln.registerTool({
2934
1618
  name: "agenr_store",
2935
1619
  label: "Agenr Store",
2936
- description: "Store a new durable memory entry in agenr. Store only durable facts, decisions, preferences, lessons, milestones, and relationships that help a future Skeln session make a better decision.",
1620
+ description: buildSkelnStoreToolDescription(),
2937
1621
  promptSnippet: "Use agenr_store to persist durable memory that should survive across Skeln sessions.",
2938
- promptGuidelines: [
2939
- "Do not store progress logs, plans, or data already canonical in git, tickets, calendars, signed docs, chat/email, or databases.",
2940
- "Store the durable takeaway, standing rule, preference, risk, lesson, or relationship instead of raw activity.",
2941
- "Use claimKey for slot-like facts that may be superseded later, such as versions, strategies, owners, or limits."
2942
- ],
1622
+ promptGuidelines: buildStoreToolGuidelines(),
2943
1623
  parameters: toolSchema(STORE_TOOL_PARAMETERS),
2944
1624
  execute: async (_toolCallId, rawParams, _signal, _onUpdate, context) => {
2945
1625
  try {
2946
1626
  const params = parseStoreToolParams(rawParams, SKELN_PARAM_READER);
2947
1627
  const [services, scope] = await Promise.all([servicesPromise, resolveScope(context)]);
2948
- return toSkelnToolResult(
2949
- await runStoreMemoryTool(params, services, {
2950
- session: scope,
2951
- sourcePrefix: "skeln-session",
2952
- defaultSourceContext: "Stored via agenr_store from Skeln.",
2953
- extraDetails: { sessionKey: scope.sessionKey },
2954
- onWarning: (warning) => console.warn(`[agenr] tool=agenr_store session=${scope.sessionId} warning: ${warning}`)
2955
- })
2956
- );
1628
+ const outcome = await runStoreMemoryTool(params, services, {
1629
+ session: scope,
1630
+ sourcePrefix: "skeln-session",
1631
+ defaultSourceContext: "Stored via agenr_store from Skeln.",
1632
+ extraDetails: { sessionKey: scope.sessionKey },
1633
+ onWarning: (warning) => console.warn(`[agenr] tool=agenr_store session=${scope.sessionId} warning: ${warning}`)
1634
+ });
1635
+ triggerSkelnImportanceLightDream(services, outcome.details.status);
1636
+ return toSkelnToolResult(outcome);
2957
1637
  } catch (error) {
2958
1638
  return toolFailureResult(error);
2959
1639
  }
2960
1640
  }
2961
1641
  });
2962
1642
  }
1643
+ function triggerSkelnImportanceLightDream(services, status) {
1644
+ if (status !== "stored") {
1645
+ return;
1646
+ }
1647
+ void maybeRunLightDream(
1648
+ { trigger: "importance" },
1649
+ {
1650
+ port: services.dreaming,
1651
+ dbPath: services.config.dbPath,
1652
+ config: services.agenrConfig,
1653
+ embedding: services.embedding,
1654
+ ...services.claimExtraction ? { createClaimExtractionLlm: () => services.claimExtraction.llm } : {}
1655
+ }
1656
+ ).then((result) => {
1657
+ if (result.status === "ran") {
1658
+ console.info(`[agenr] skeln importance light dream completed run=${result.result.runId}`);
1659
+ } else if (result.reason === "run_in_progress" || result.reason === "episode_write_in_progress") {
1660
+ console.info(`[agenr] skeln importance light dream skipped reason=${result.reason}`);
1661
+ }
1662
+ }).catch((error) => {
1663
+ console.warn(`[agenr] skeln importance light dream failed: ${formatErrorMessage(error)}`);
1664
+ });
1665
+ }
2963
1666
 
2964
1667
  // src/adapters/skeln/tools/update.ts
2965
1668
  function registerAgenrSkelnUpdateTool(skeln, servicesPromise, resolveScope) {
2966
1669
  skeln.registerTool({
2967
1670
  name: "agenr_update",
2968
1671
  label: "Agenr Update",
2969
- description: "Update an existing memory entry in place. Supports importance, expiry, claimKey, validFrom, and validTo.",
1672
+ description: buildUpdateToolDescription(),
2970
1673
  promptSnippet: "Use agenr_update to correct metadata on an existing durable memory entry.",
2971
- promptGuidelines: ["Provide exactly one target selector: id or subject.", "Use agenr_store with supersedes for substantive content replacement."],
1674
+ promptGuidelines: buildUpdateToolGuidelines(),
2972
1675
  parameters: toolSchema(UPDATE_TOOL_PARAMETERS),
2973
1676
  execute: async (_toolCallId, rawParams, _signal, _onUpdate, context) => {
2974
1677
  try {
@@ -2992,7 +1695,8 @@ function registerAgenrSkelnUpdateTool(skeln, servicesPromise, resolveScope) {
2992
1695
  expiry: params.expiry,
2993
1696
  claimKey: params.claimKeyInput,
2994
1697
  validFrom: params.validFrom,
2995
- validTo: params.validTo
1698
+ validTo: params.validTo,
1699
+ project: params.project
2996
1700
  })
2997
1701
  }
2998
1702
  });
@@ -3830,6 +2534,8 @@ function registerAgenrSkelnMemory(skeln, options = {}) {
3830
2534
  const servicesPromise = createAgenrSkelnServices(config);
3831
2535
  const scopeTracker = createSkelnSessionScopeTracker();
3832
2536
  const sessionStartTracker = createSessionStartTracker();
2537
+ const compactionPromptTracker = createCompactionPromptTracker();
2538
+ const lifecycleIntakeTracker = createSessionLifecycleIntakeTracker();
3833
2539
  const lifecycle = skeln;
3834
2540
  const resolveScope = async (context) => resolveCurrentSkelnSessionScope(context, scopeTracker, options);
3835
2541
  void servicesPromise.catch((error) => {
@@ -3837,8 +2543,16 @@ function registerAgenrSkelnMemory(skeln, options = {}) {
3837
2543
  });
3838
2544
  registerAgenrSkelnTools(skeln, servicesPromise, resolveScope);
3839
2545
  registerAgenrSkelnFailureBoundary(lifecycle);
3840
- registerAgenrSkelnInjectionHooks(lifecycle, scopeTracker, servicesPromise, sessionStartTracker, resolveScope);
3841
- registerAgenrSkelnSessionMemoryHooks(lifecycle, scopeTracker, servicesPromise, resolveScope);
2546
+ registerAgenrSkelnInjectionHooks(
2547
+ lifecycle,
2548
+ scopeTracker,
2549
+ servicesPromise,
2550
+ sessionStartTracker,
2551
+ compactionPromptTracker,
2552
+ lifecycleIntakeTracker,
2553
+ resolveScope
2554
+ );
2555
+ registerAgenrSkelnSessionMemoryHooks(lifecycle, scopeTracker, servicesPromise, resolveScope, compactionPromptTracker, lifecycleIntakeTracker);
3842
2556
  registerAgenrSkelnSubagentFindingHooks(lifecycle, servicesPromise, resolveScope);
3843
2557
  return {
3844
2558
  executeWorkCommand: (context, params) => executeAgenrSkelnWorkCommand(servicesPromise, resolveScope, context, params)
@@ -3847,13 +2561,19 @@ function registerAgenrSkelnMemory(skeln, options = {}) {
3847
2561
  function extension(skeln) {
3848
2562
  registerAgenrSkelnMemory(skeln);
3849
2563
  }
3850
- function registerAgenrSkelnInjectionHooks(skeln, scopeTracker, servicesPromise, sessionStartTracker, resolveScope) {
2564
+ function registerAgenrSkelnInjectionHooks(skeln, scopeTracker, servicesPromise, sessionStartTracker, compactionPromptTracker, lifecycleIntakeTracker, resolveScope) {
3851
2565
  skeln.on("session_start", async (event, context) => {
3852
2566
  try {
3853
2567
  const scope = await resolveScope(context);
3854
- rememberSkelnSessionStart(scopeTracker, sessionStartTracker, scope, event.previousSessionFile);
3855
- const services = await servicesPromise;
3856
- logSessionMemoryTriggerResult(await services.routeSessionMemoryTrigger(buildSkelnSessionStartTriggerEvent(scope, event)));
2568
+ rememberSkelnSessionStart(scopeTracker, scope);
2569
+ await lifecycleIntakeTracker.track(
2570
+ scope.sessionId,
2571
+ scope.sessionKey,
2572
+ (async () => {
2573
+ const services = await servicesPromise;
2574
+ logSessionMemoryTriggerResult(await services.routeSessionMemoryTrigger(buildSkelnSessionStartTriggerEvent(scope, event)));
2575
+ })()
2576
+ );
3857
2577
  } catch (error) {
3858
2578
  console.warn(`[agenr] session_start scope failed: ${formatErrorMessage(error)}`);
3859
2579
  }
@@ -3863,32 +2583,65 @@ function registerAgenrSkelnInjectionHooks(skeln, scopeTracker, servicesPromise,
3863
2583
  async (event, context) => handleAgenrSkelnBeforeAgentStart(event, context, {
3864
2584
  servicesPromise,
3865
2585
  sessionStartTracker,
2586
+ compactionPromptTracker,
2587
+ lifecycleIntakeTracker,
3866
2588
  resolveScope
3867
2589
  })
3868
2590
  );
3869
2591
  }
3870
- function registerAgenrSkelnSessionMemoryHooks(skeln, scopeTracker, servicesPromise, resolveScope) {
2592
+ function registerAgenrSkelnSessionMemoryHooks(skeln, scopeTracker, servicesPromise, resolveScope, compactionPromptTracker, lifecycleIntakeTracker) {
3871
2593
  skeln.on("session_before_fork", async (event, context) => {
3872
- await routeScopedSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope) => buildSkelnSessionBeforeForkTriggerEvent(scope, event));
2594
+ const scope = await resolveScope(context);
2595
+ await lifecycleIntakeTracker.track(
2596
+ scope.sessionId,
2597
+ scope.sessionKey,
2598
+ routeSkelnSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope2) => buildSkelnSessionBeforeForkTriggerEvent(scope2, event))
2599
+ );
3873
2600
  });
3874
- skeln.on("session_before_compact", async (_event, context) => {
3875
- await routeScopedSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope) => buildSkelnSessionBeforeCompactTriggerEvent(scope));
2601
+ skeln.on("session_before_compact", async (event, context) => {
2602
+ const scope = await resolveScope(context);
2603
+ await lifecycleIntakeTracker.track(scope.sessionId, scope.sessionKey, handleSkelnSessionBeforeCompact(event, context, servicesPromise, resolveScope));
3876
2604
  });
3877
2605
  skeln.on("session_compact", async (event, context) => {
3878
- await routeScopedSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope) => buildSkelnSessionCompactTriggerEvent(scope, event));
2606
+ const scope = await resolveScope(context);
2607
+ await lifecycleIntakeTracker.track(
2608
+ scope.sessionId,
2609
+ scope.sessionKey,
2610
+ routeSkelnSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope2) => buildSkelnSessionCompactTriggerEvent(scope2, event))
2611
+ );
3879
2612
  });
3880
2613
  skeln.on("session_before_tree", async (event, context) => {
3881
- await routeScopedSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope) => buildSkelnSessionBeforeTreeTriggerEvent(scope, event));
2614
+ const scope = await resolveScope(context);
2615
+ await lifecycleIntakeTracker.track(
2616
+ scope.sessionId,
2617
+ scope.sessionKey,
2618
+ routeSkelnSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope2) => buildSkelnSessionBeforeTreeTriggerEvent(scope2, event))
2619
+ );
3882
2620
  });
3883
2621
  skeln.on("session_tree", async (event, context) => {
3884
- await routeScopedSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope) => buildSkelnSessionTreeTriggerEvent(scope, event));
2622
+ const scope = await resolveScope(context);
2623
+ await lifecycleIntakeTracker.track(
2624
+ scope.sessionId,
2625
+ scope.sessionKey,
2626
+ routeSkelnSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope2) => buildSkelnSessionTreeTriggerEvent(scope2, event))
2627
+ );
3885
2628
  });
3886
2629
  skeln.on("session_shutdown", async (event, context) => {
3887
- await handleSkelnSessionShutdown(servicesPromise, scopeTracker, resolveScope, event, context);
2630
+ await handleSkelnSessionShutdown(servicesPromise, scopeTracker, resolveScope, event, context, compactionPromptTracker, lifecycleIntakeTracker);
3888
2631
  });
3889
2632
  }
3890
- async function handleSkelnSessionShutdown(servicesPromise, scopeTracker, resolveScope, event, context) {
3891
- await routeScopedSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope) => buildSkelnSessionShutdownTriggerEvent(scope, event));
2633
+ async function handleSkelnSessionShutdown(servicesPromise, scopeTracker, resolveScope, event, context, compactionPromptTracker, lifecycleIntakeTracker) {
2634
+ try {
2635
+ const scope = await resolveScope(context);
2636
+ await lifecycleIntakeTracker.track(
2637
+ scope.sessionId,
2638
+ scope.sessionKey,
2639
+ routeSkelnSessionMemoryTrigger(servicesPromise, resolveScope, context, (scope2) => buildSkelnSessionShutdownTriggerEvent(scope2, event))
2640
+ );
2641
+ compactionPromptTracker.clear(scope.sessionId, scope.sessionKey);
2642
+ await lifecycleIntakeTracker.clear(scope.sessionId, scope.sessionKey);
2643
+ } catch {
2644
+ }
3892
2645
  clearTrackedSkelnScope(scopeTracker, context);
3893
2646
  await scheduleSkelnSessionShutdownEpisodeWrite({ event, context, servicesPromise });
3894
2647
  }
@@ -3898,15 +2651,6 @@ function registerAgenrSkelnSubagentFindingHooks(skeln, servicesPromise, resolveS
3898
2651
  return void 0;
3899
2652
  });
3900
2653
  }
3901
- async function routeScopedSessionMemoryTrigger(servicesPromise, resolveScope, context, buildEvent) {
3902
- try {
3903
- const scope = await resolveScope(context);
3904
- const services = await servicesPromise;
3905
- logSessionMemoryTriggerResult(await services.routeSessionMemoryTrigger(buildEvent(scope)));
3906
- } catch (error) {
3907
- console.warn(`[agenr] session-memory trigger failed: ${formatErrorMessage(error)}`);
3908
- }
3909
- }
3910
2654
  async function resolveCurrentSkelnSessionScope(context, scopeTracker, options) {
3911
2655
  const sessionId = String(context.sessionManager.getSessionId());
3912
2656
  const tracked = scopeTracker.getSessionScope(sessionId);
@@ -3924,9 +2668,8 @@ async function resolveCurrentSkelnSessionScope(context, scopeTracker, options) {
3924
2668
  const override = options.getHostContext ? await options.getHostContext(context) : void 0;
3925
2669
  return toSkelnSessionScope(mergeSkelnHostContext(defaults, override), sessionId);
3926
2670
  }
3927
- function rememberSkelnSessionStart(scopeTracker, sessionStartTracker, scope, previousSessionFile) {
2671
+ function rememberSkelnSessionStart(scopeTracker, scope) {
3928
2672
  scopeTracker.rememberSessionStart(scope);
3929
- sessionStartTracker.rememberSessionStart(scope.sessionId, scope.sessionKey, previousSessionFile);
3930
2673
  }
3931
2674
  function resolveContextCwd(context) {
3932
2675
  return context.cwd || context.sessionManager.getCwd();