comisai 1.0.14 → 1.0.16

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 (79) hide show
  1. package/node_modules/@comis/agent/dist/bootstrap/index.d.ts +1 -1
  2. package/node_modules/@comis/agent/dist/bootstrap/index.js +1 -1
  3. package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.d.ts +26 -0
  4. package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.js +86 -54
  5. package/node_modules/@comis/agent/dist/bootstrap/types.d.ts +7 -1
  6. package/node_modules/@comis/agent/dist/bootstrap/workspace-loader.d.ts +12 -0
  7. package/node_modules/@comis/agent/dist/bootstrap/workspace-loader.js +21 -0
  8. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +12 -11
  9. package/node_modules/@comis/agent/dist/context-engine/constants.d.ts +11 -0
  10. package/node_modules/@comis/agent/dist/context-engine/constants.js +11 -0
  11. package/node_modules/@comis/agent/dist/context-engine/index.d.ts +1 -1
  12. package/node_modules/@comis/agent/dist/context-engine/index.js +1 -1
  13. package/node_modules/@comis/agent/dist/context-engine/llm-compaction.js +32 -18
  14. package/node_modules/@comis/agent/dist/executor/cache-break-diff-writer.d.ts +2 -0
  15. package/node_modules/@comis/agent/dist/executor/cache-break-diff-writer.js +36 -0
  16. package/node_modules/@comis/agent/dist/executor/executor-post-execution.d.ts +12 -0
  17. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +117 -27
  18. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +1 -0
  19. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +16 -4
  20. package/node_modules/@comis/agent/dist/executor/executor-stream-setup.js +17 -1
  21. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +29 -1
  22. package/node_modules/@comis/agent/dist/executor/prompt-assembly.d.ts +8 -1
  23. package/node_modules/@comis/agent/dist/executor/prompt-assembly.js +22 -5
  24. package/node_modules/@comis/agent/dist/executor/session-snapshot-cleanup.js +3 -1
  25. package/node_modules/@comis/agent/dist/executor/stream-wrappers/index.d.ts +2 -0
  26. package/node_modules/@comis/agent/dist/executor/stream-wrappers/index.js +2 -0
  27. package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.d.ts +9 -0
  28. package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +65 -3
  29. package/node_modules/@comis/agent/dist/executor/stream-wrappers/stub-filter-injector.d.ts +28 -0
  30. package/node_modules/@comis/agent/dist/executor/stream-wrappers/stub-filter-injector.js +63 -0
  31. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +18 -1
  32. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +275 -133
  33. package/node_modules/@comis/agent/dist/executor/ttl-guard.d.ts +6 -0
  34. package/node_modules/@comis/agent/dist/executor/ttl-guard.js +8 -0
  35. package/node_modules/@comis/agent/dist/session/comis-session-manager.js +7 -2
  36. package/node_modules/@comis/agent/package.json +1 -1
  37. package/node_modules/@comis/channels/dist/shared/typing-controller.d.ts +7 -0
  38. package/node_modules/@comis/channels/dist/shared/typing-controller.js +33 -0
  39. package/node_modules/@comis/channels/package.json +1 -1
  40. package/node_modules/@comis/cli/dist/commands/daemon.js +116 -28
  41. package/node_modules/@comis/cli/dist/doctor/checks/channel-health.js +2 -2
  42. package/node_modules/@comis/cli/dist/wizard/index.d.ts +2 -3
  43. package/node_modules/@comis/cli/dist/wizard/index.js +2 -3
  44. package/node_modules/@comis/cli/dist/wizard/steps/10-write-config.js +1 -1
  45. package/node_modules/@comis/cli/dist/wizard/types.d.ts +1 -2
  46. package/node_modules/@comis/cli/dist/wizard/types.js +1 -2
  47. package/node_modules/@comis/cli/package.json +1 -1
  48. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +64 -10
  49. package/node_modules/@comis/core/dist/config/schema-agent.js +19 -0
  50. package/node_modules/@comis/core/dist/config/schema-skills.d.ts +16 -2
  51. package/node_modules/@comis/core/dist/config/schema-skills.js +9 -2
  52. package/node_modules/@comis/core/dist/config/schema.d.ts +19 -11
  53. package/node_modules/@comis/core/dist/event-bus/events-infra.d.ts +8 -0
  54. package/node_modules/@comis/core/package.json +1 -1
  55. package/node_modules/@comis/daemon/dist/daemon.js +2 -1
  56. package/node_modules/@comis/daemon/dist/wiring/setup-channels.js +19 -2
  57. package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.js +10 -0
  58. package/node_modules/@comis/daemon/dist/wiring/setup-memory.d.ts +2 -0
  59. package/node_modules/@comis/daemon/dist/wiring/setup-memory.js +15 -0
  60. package/node_modules/@comis/daemon/dist/wiring/setup-schedulers.js +1 -0
  61. package/node_modules/@comis/daemon/dist/wiring/setup-shutdown.d.ts +1 -0
  62. package/node_modules/@comis/daemon/dist/wiring/setup-shutdown.js +4 -0
  63. package/node_modules/@comis/daemon/package.json +1 -1
  64. package/node_modules/@comis/gateway/package.json +1 -1
  65. package/node_modules/@comis/infra/package.json +1 -1
  66. package/node_modules/@comis/memory/package.json +1 -1
  67. package/node_modules/@comis/scheduler/dist/cron/cron-types.d.ts +11 -0
  68. package/node_modules/@comis/scheduler/dist/cron/cron-types.js +11 -0
  69. package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.d.ts +26 -1
  70. package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.js +23 -2
  71. package/node_modules/@comis/scheduler/dist/heartbeat/heartbeat-config.d.ts +7 -0
  72. package/node_modules/@comis/scheduler/dist/heartbeat/heartbeat-config.js +2 -0
  73. package/node_modules/@comis/scheduler/package.json +1 -1
  74. package/node_modules/@comis/shared/package.json +1 -1
  75. package/node_modules/@comis/skills/dist/index.d.ts +1 -1
  76. package/node_modules/@comis/skills/dist/index.js +1 -1
  77. package/node_modules/@comis/skills/dist/policy/tool-policy.js +25 -0
  78. package/node_modules/@comis/skills/package.json +1 -1
  79. package/package.json +12 -12
@@ -1,5 +1,5 @@
1
1
  export { type BootstrapFile, type TruncationResult, type PromptMode, type RuntimeInfo, type InboundMetadata, type BootstrapContextFile, SUBAGENT_BOOTSTRAP_ALLOWLIST, BOOTSTRAP_HEAD_RATIO, BOOTSTRAP_TAIL_RATIO, } from "./types.js";
2
- export { loadWorkspaceBootstrapFiles, truncateFileContent, filterBootstrapFilesForSubAgent, filterBootstrapFilesForLightContext, filterBootstrapFilesForGroupChat, buildBootstrapContextFiles, } from "./workspace-loader.js";
2
+ export { loadWorkspaceBootstrapFiles, truncateFileContent, filterBootstrapFilesForSubAgent, filterBootstrapFilesForLightContext, filterBootstrapFilesForCron, filterBootstrapFilesForGroupChat, buildBootstrapContextFiles, } from "./workspace-loader.js";
3
3
  export * from "./sections/index.js";
4
4
  export { extractMarkdownSections, MAX_POST_COMPACTION_CHARS } from "./section-extractor.js";
5
5
  export { assembleRichSystemPrompt, assembleRichSystemPromptBlocks, SECTION_SEPARATOR } from "./system-prompt-assembler.js";
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Bootstrap — workspace file loading & system prompt assembly
3
3
  export { SUBAGENT_BOOTSTRAP_ALLOWLIST, BOOTSTRAP_HEAD_RATIO, BOOTSTRAP_TAIL_RATIO, } from "./types.js";
4
- export { loadWorkspaceBootstrapFiles, truncateFileContent, filterBootstrapFilesForSubAgent, filterBootstrapFilesForLightContext, filterBootstrapFilesForGroupChat, buildBootstrapContextFiles, } from "./workspace-loader.js";
4
+ export { loadWorkspaceBootstrapFiles, truncateFileContent, filterBootstrapFilesForSubAgent, filterBootstrapFilesForLightContext, filterBootstrapFilesForCron, filterBootstrapFilesForGroupChat, buildBootstrapContextFiles, } from "./workspace-loader.js";
5
5
  export * from "./sections/index.js";
6
6
  export { extractMarkdownSections, MAX_POST_COMPACTION_CHARS } from "./section-extractor.js";
7
7
  export { assembleRichSystemPrompt, assembleRichSystemPromptBlocks, SECTION_SEPARATOR } from "./system-prompt-assembler.js";
@@ -82,6 +82,32 @@ export interface AssemblerParams {
82
82
  /** Whether Silent Execution Planner (SEP) is enabled for this agent. */
83
83
  sepEnabled?: boolean;
84
84
  }
85
+ /**
86
+ * Internal descriptor for a system prompt section.
87
+ *
88
+ * Engineering notes:
89
+ * - `id` must be unique across SECTIONS and stable (used by tests).
90
+ * - `includeIn` must be non-empty.
91
+ * - `build` receives `(params, mode)` and is responsible for forwarding
92
+ * `mode === "minimal"` as the existing builders' `isMinimal` parameter.
93
+ *
94
+ * @internal exported for tests only
95
+ */
96
+ export interface SectionDescriptor {
97
+ readonly id: string;
98
+ readonly includeIn: ReadonlySet<PromptMode>;
99
+ readonly build: (params: AssemblerParams, mode: PromptMode) => string[];
100
+ }
101
+ /**
102
+ * Canonical section list in emission order.
103
+ *
104
+ * Order MUST match the previous hand-written `buildAllSections` so the
105
+ * `staticPrefix`/`attribution` index boundaries (2 + 2) continue to enclose
106
+ * identity+persona and safety+language respectively.
107
+ *
108
+ * @internal exported for tests only
109
+ */
110
+ export declare const SECTIONS: ReadonlyArray<SectionDescriptor>;
85
111
  /**
86
112
  * Assemble a rich multi-section system prompt.
87
113
  *
@@ -50,63 +50,92 @@ function joinSections(sections) {
50
50
  const nonEmpty = sections.filter((lines) => lines.length > 0);
51
51
  return nonEmpty.map((lines) => lines.join("\n")).join(SECTION_SEPARATOR);
52
52
  }
53
+ /** Sections present in full, operational, and minimal modes.
54
+ * NB: builders may still self-filter to `[]` when their isMinimal flag is set. */
55
+ const MODES_ALL = new Set(["full", "operational", "minimal"]);
56
+ /** Sections present in full and minimal modes (stripped in operational).
57
+ * Interactive-only guidance that doesn't apply to autonomous cron/heartbeat runs
58
+ * but that minimal sub-agent contexts historically saw before this refactor.
59
+ * Minimal-mode builders typically self-filter to [] via their own isMinimal flag;
60
+ * membership here preserves pre-refactor behavior without changing minimal output. */
61
+ const MODES_FULL_MIN = new Set(["full", "minimal"]);
62
+ /** Sections present only in full mode (stripped in operational AND minimal).
63
+ * Reserved for sections whose builder is cheap but whose output is not desired
64
+ * in either non-interactive context. Currently unused; MODES_FULL_MIN is
65
+ * preferred to preserve pre-refactor minimal output. */
66
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for future use
67
+ const MODES_FULL = new Set(["full"]);
53
68
  /**
54
- * Build all section arrays in the canonical order (1-22).
69
+ * Canonical section list in emission order.
70
+ *
71
+ * Order MUST match the previous hand-written `buildAllSections` so the
72
+ * `staticPrefix`/`attribution` index boundaries (2 + 2) continue to enclose
73
+ * identity+persona and safety+language respectively.
74
+ *
75
+ * @internal exported for tests only
76
+ */
77
+ export const SECTIONS = [
78
+ // --- Static prefix block (indices 0-1 in every mode that includes them) ---
79
+ { id: "identity", includeIn: MODES_ALL, build: (p) => buildIdentitySection(p.agentName ?? "Comis") },
80
+ { id: "persona", includeIn: MODES_ALL, build: (p) => buildPersonaSection(p.bootstrapFiles ?? []) },
81
+ // --- Attribution block (safety self-filters in minimal) ---
82
+ { id: "safety", includeIn: MODES_ALL, build: (p, m) => buildSafetySection(m === "minimal") },
83
+ { id: "language", includeIn: MODES_ALL, build: (p) => buildLanguageSection(p.userLanguage) },
84
+ // --- Semi-stable body: operational-kept sections (MODES_ALL -- builders self-filter for minimal) ---
85
+ { id: "tooling", includeIn: MODES_ALL, build: (p, m) => buildToolingSection(p.toolNames ?? [], m === "minimal" ? "small" : "large", p.toolSummaries) },
86
+ { id: "tool-call-style", includeIn: MODES_ALL, build: (p, m) => buildToolCallStyleSection(m === "minimal", p.toolNames ?? []) },
87
+ // --- Operational-stripped sections (MODES_FULL_MIN -- dropped in "operational") ---
88
+ { id: "self-update", includeIn: MODES_FULL_MIN, build: (p, m) => buildSelfUpdateGatingSection(p.toolNames ?? [], m === "minimal", true) },
89
+ { id: "config-secret", includeIn: MODES_FULL_MIN, build: (p, m) => buildConfigSecretIntegritySection(p.toolNames ?? [], m === "minimal") },
90
+ { id: "privileged", includeIn: MODES_FULL_MIN, build: (p, m) => buildPrivilegedToolsSection(p.toolNames ?? [], m === "minimal", true) },
91
+ { id: "compact-recover", includeIn: MODES_FULL_MIN, build: (p, m) => buildCompactedOutputRecoverySection(m === "minimal") },
92
+ { id: "post-compact", includeIn: MODES_FULL_MIN, build: (p, m) => buildPostCompactionRecoverySection(p.bootstrapFiles ?? [], m === "minimal", p.postCompactionSections) },
93
+ { id: "coding-fallback", includeIn: MODES_FULL_MIN, build: (p, m) => buildCodingFallbackSection(p.toolNames ?? [], m === "minimal", true) },
94
+ { id: "task-delegation", includeIn: MODES_FULL_MIN, build: (p, m) => buildTaskDelegationSection(p.toolNames ?? [], m === "minimal", p.subAgentToolNames, p.mcpToolsInherited, true) },
95
+ // --- Operational-kept body (MODES_ALL) ---
96
+ { id: "skills", includeIn: MODES_ALL, build: (p, m) => buildSkillsSection(p.skillsPrompt, m === "minimal", p.promptSkillsXml, p.activePromptSkillContent) },
97
+ { id: "memory-recall", includeIn: MODES_ALL, build: (p, m) => buildMemoryRecallSection(p.hasMemoryTools ?? false, m === "minimal") },
98
+ { id: "workspace", includeIn: MODES_ALL, build: (p, m) => buildWorkspaceSection(p.workspaceDir, m === "minimal") },
99
+ // --- Operational-stripped body ---
100
+ { id: "documentation", includeIn: MODES_FULL_MIN, build: (p, m) => p.documentationConfig
101
+ ? buildDocumentationSection(p.documentationConfig, p.toolNames ?? [], m === "minimal")
102
+ : [] },
103
+ { id: "messaging", includeIn: MODES_ALL, build: (p, m) => buildMessagingSection(p.toolNames ?? [], m === "minimal", p.channelContext) },
104
+ { id: "background", includeIn: MODES_FULL_MIN, build: (p, m) => buildBackgroundTaskSection(p.toolNames ?? [], m === "minimal", p.channelContext) },
105
+ { id: "silent-replies", includeIn: MODES_FULL_MIN, build: (p, m) => buildSilentRepliesSection(m === "minimal") },
106
+ { id: "heartbeats", includeIn: MODES_FULL_MIN, build: (p, m) => buildHeartbeatsSection(p.heartbeatPrompt, m === "minimal") },
107
+ { id: "reactions", includeIn: MODES_FULL_MIN, build: (p, m) => buildReactionGuidanceSection(p.reactionLevel, p.channelContext?.channelType, m === "minimal") },
108
+ { id: "media-sharing", includeIn: MODES_FULL_MIN, build: (p, m) => buildMediaSharingSection(p.outboundMediaEnabled, m === "minimal") },
109
+ { id: "media-files", includeIn: MODES_FULL_MIN, build: (p, m) => buildMediaFilesSection(p.hasMemoryTools ?? false, (p.toolNames ?? []).includes("message"), p.workspaceDir, p.mediaPersistenceEnabled ?? false, m === "minimal") },
110
+ { id: "autonomous-media", includeIn: MODES_FULL_MIN, build: (p, m) => buildAutonomousMediaSection(p.autonomousMediaEnabled ?? false, m === "minimal") },
111
+ { id: "reasoning", includeIn: MODES_ALL, build: (p, m) => buildReasoningSection(p.reasoningEnabled ?? false, m === "minimal", p.reasoningTagHint ?? false) },
112
+ { id: "sep", includeIn: MODES_FULL_MIN, build: (p, m) => buildTaskPlanningSection(p.sepEnabled ?? false, m === "minimal") },
113
+ { id: "runtime-meta", includeIn: MODES_ALL, build: (p, m) => buildRuntimeMetadataSection(p.runtimeInfo ?? {}, m === "minimal") },
114
+ { id: "sender-trust", includeIn: MODES_FULL_MIN, build: (p, m) => buildSenderTrustSection(p.senderTrustEntries ?? [], p.senderTrustDisplayMode ?? "raw", m === "minimal") },
115
+ { id: "project-context", includeIn: MODES_ALL, build: (p, m) => buildProjectContextSection(p.bootstrapFiles ?? [], m === "minimal", p.excludeBootstrapFromContext ? new Set(["BOOTSTRAP.md"]) : undefined, p.workspaceProfile) },
116
+ ];
117
+ /**
118
+ * Build all section arrays in the canonical order.
119
+ *
120
+ * Filters `SECTIONS` by mode inclusion, then builds each included descriptor.
121
+ * Subagent context is appended unconditionally as the last entry (matches the
122
+ * previous behavior).
55
123
  *
56
124
  * Shared by both `assembleRichSystemPrompt` and `assembleRichSystemPromptBlocks`
57
125
  * to guarantee identity by construction between both assembly functions.
58
126
  *
59
127
  * @returns Array of section line arrays in fixed order
60
128
  */
61
- function buildAllSections(params, isMinimal) {
62
- const agentName = params.agentName ?? "Comis";
63
- const modelTier = isMinimal ? "small" : "large";
64
- return [
65
- // --- Static prefix sections (indices 0-1) ---
66
- buildIdentitySection(agentName), // 1
67
- buildPersonaSection(params.bootstrapFiles ?? []), // 1b: Persona (SOUL.md) before Safety
68
- // --- Attribution sections (indices 2-3) ---
69
- buildSafetySection(isMinimal), // 2
70
- buildLanguageSection(params.userLanguage), // 2b
71
- // --- Semi-stable body sections (indices 4+) ---
72
- buildToolingSection(params.toolNames ?? [], modelTier, params.toolSummaries), // 3
73
- buildToolCallStyleSection(isMinimal, params.toolNames ?? []), // 4
74
- buildSelfUpdateGatingSection(params.toolNames ?? [], isMinimal, true), // 5: deferred to tool result
75
- buildConfigSecretIntegritySection(params.toolNames ?? [], isMinimal), // 5a: always-present
76
- buildPrivilegedToolsSection(params.toolNames ?? [], isMinimal, true), // 5b: deferred to tool result
77
- buildCompactedOutputRecoverySection(isMinimal), // 6
78
- buildPostCompactionRecoverySection(// 6a
79
- params.bootstrapFiles ?? [], isMinimal, params.postCompactionSections),
80
- buildCodingFallbackSection(params.toolNames ?? [], isMinimal, true), // 6b: deferred to tool result
81
- buildTaskDelegationSection(params.toolNames ?? [], isMinimal, params.subAgentToolNames, params.mcpToolsInherited, true), // 6c: deferred to tool result
82
- buildSkillsSection(params.skillsPrompt, isMinimal, params.promptSkillsXml, params.activePromptSkillContent), // 7: Merged filesystem + prompt skills
83
- buildMemoryRecallSection(params.hasMemoryTools ?? false, isMinimal), // 8
84
- buildWorkspaceSection(params.workspaceDir, isMinimal), // 9
85
- params.documentationConfig
86
- ? buildDocumentationSection(params.documentationConfig, params.toolNames ?? [], isMinimal)
87
- : [], // 9b: Documentation
88
- buildMessagingSection(params.toolNames ?? [], isMinimal, params.channelContext), // 10
89
- buildBackgroundTaskSection(params.toolNames ?? [], isMinimal, params.channelContext), // 11
90
- buildSilentRepliesSection(isMinimal), // 14
91
- buildHeartbeatsSection(params.heartbeatPrompt, isMinimal), // 15
92
- buildReactionGuidanceSection(params.reactionLevel, params.channelContext?.channelType, isMinimal), // 16
93
- buildMediaSharingSection(params.outboundMediaEnabled, isMinimal), // 16b
94
- buildMediaFilesSection(// 16c
95
- params.hasMemoryTools ?? false, (params.toolNames ?? []).includes("message"), params.workspaceDir, params.mediaPersistenceEnabled ?? false, isMinimal),
96
- buildAutonomousMediaSection(params.autonomousMediaEnabled ?? false, isMinimal), // 16d
97
- buildReasoningSection(params.reasoningEnabled ?? false, isMinimal, params.reasoningTagHint ?? false), // 17
98
- buildTaskPlanningSection(params.sepEnabled ?? false, isMinimal), // 17b: SEP task planning (static, cache-stable)
99
- // buildDateTimeSection() removed from system prompt (relocated to user-message preamble in prompt-assembly.ts)
100
- buildRuntimeMetadataSection(params.runtimeInfo ?? {}, isMinimal), // 19
101
- // buildInboundMetadataSection() removed from system prompt (relocated to user-message preamble in prompt-assembly.ts)
102
- buildSenderTrustSection(params.senderTrustEntries ?? [], params.senderTrustDisplayMode ?? "raw", isMinimal), // 20b
103
- buildProjectContextSection(// 21
104
- params.bootstrapFiles ?? [], isMinimal, params.excludeBootstrapFromContext ? new Set(["BOOTSTRAP.md"]) : undefined, params.workspaceProfile),
105
- // Subagent: prefer structured params, fall back to raw extraSystemPrompt
106
- ...(params.subagentRole
107
- ? [buildSubagentRoleSection(params.subagentRole)]
108
- : [buildSubagentContextSection(params.extraSystemPrompt)]), // 22
109
- ];
129
+ function buildAllSections(params, mode) {
130
+ const base = SECTIONS
131
+ .filter((s) => s.includeIn.has(mode))
132
+ .map((s) => s.build(params, mode));
133
+ // Subagent section: prefer structured params, fall back to raw extraSystemPrompt.
134
+ // Unconditional; matches the previous hand-written emission.
135
+ const subagent = params.subagentRole
136
+ ? buildSubagentRoleSection(params.subagentRole)
137
+ : buildSubagentContextSection(params.extraSystemPrompt);
138
+ return [...base, subagent];
110
139
  }
111
140
  /**
112
141
  * Append additional sections (backward compat for RAG memory etc.) to a joined string.
@@ -141,8 +170,7 @@ export function assembleRichSystemPrompt(params) {
141
170
  if (mode === "none") {
142
171
  return buildIdentitySection(agentName).join("\n");
143
172
  }
144
- const isMinimal = mode === "minimal";
145
- const allSections = buildAllSections(params, isMinimal);
173
+ const allSections = buildAllSections(params, mode);
146
174
  // Filter out empty arrays, join each section's lines, then join sections
147
175
  const joined = joinSections(allSections);
148
176
  // Append additional sections (backward compat for RAG memory etc.)
@@ -186,9 +214,13 @@ export function assembleRichSystemPromptBlocks(params) {
186
214
  semiStableBody: "",
187
215
  };
188
216
  }
189
- const isMinimal = mode === "minimal";
190
- const allSections = buildAllSections(params, isMinimal);
217
+ const allSections = buildAllSections(params, mode);
191
218
  // Split at boundaries: identity+persona | safety+language | tooling+workspace+...
219
+ // Boundaries are index-based on the filtered section list. In `"full"` and
220
+ // `"operational"` modes the first 2 entries are identity+persona (both are
221
+ // in MODES_FULL_OP) and the next 2 are safety+language (MODES_ALL). In
222
+ // `"minimal"` mode persona is dropped, so the prefix shrinks by one and the
223
+ // byte-identity between full/operational is NOT expected in minimal mode.
192
224
  const staticSections = allSections.slice(0, STATIC_PREFIX_SECTION_COUNT);
193
225
  const attributionSections = allSections.slice(STATIC_PREFIX_SECTION_COUNT, STATIC_PREFIX_SECTION_COUNT + ATTRIBUTION_SECTION_COUNT);
194
226
  const bodySections = allSections.slice(STATIC_PREFIX_SECTION_COUNT + ATTRIBUTION_SECTION_COUNT);
@@ -37,10 +37,16 @@ export interface TruncationResult {
37
37
  * System prompt verbosity mode.
38
38
  *
39
39
  * - `"full"`: All workspace sections included (primary agents)
40
+ * - `"operational"`: All workspace sections relevant to autonomous operations;
41
+ * drops interactive-only guidance (compaction recovery, silent replies,
42
+ * heartbeats, reactions, media, SEP, sender trust). Used for cron and
43
+ * heartbeat executions. `staticPrefix` and `attribution` cache blocks
44
+ * remain byte-identical to `"full"` so the cached identity/attribution
45
+ * prefix is shared across modes.
40
46
  * - `"minimal"`: Only AGENTS.md + TOOLS.md (sub-agents)
41
47
  * - `"none"`: Identity-only prompt (lightweight contexts)
42
48
  */
43
- export type PromptMode = "full" | "minimal" | "none";
49
+ export type PromptMode = "full" | "operational" | "minimal" | "none";
44
50
  /**
45
51
  * Runtime metadata injected into the system prompt header.
46
52
  */
@@ -87,6 +87,18 @@ export declare function filterBootstrapFilesForSubAgent(files: BootstrapFile[]):
87
87
  * @returns Filtered array containing only HEARTBEAT.md
88
88
  */
89
89
  export declare function filterBootstrapFilesForLightContext(files: BootstrapFile[]): BootstrapFile[];
90
+ /**
91
+ * Filter bootstrap files for cron execution.
92
+ *
93
+ * Autonomous cron runs don't need heartbeat cadence rules, user preferences,
94
+ * tooling docs, or workspace bootstrap -- only the agent's persona (SOUL.md)
95
+ * and role behavior (ROLE.md). Mirrors the pattern of
96
+ * filterBootstrapFilesForLightContext.
97
+ *
98
+ * @param files - Full array of loaded bootstrap files
99
+ * @returns Filtered array containing only SOUL.md and ROLE.md
100
+ */
101
+ export declare function filterBootstrapFilesForCron(files: BootstrapFile[]): BootstrapFile[];
90
102
  /**
91
103
  * Filter bootstrap files for group chat context.
92
104
  *
@@ -136,6 +136,27 @@ const HEARTBEAT_LIGHT_ALLOWLIST = new Set(["HEARTBEAT.md"]);
136
136
  export function filterBootstrapFilesForLightContext(files) {
137
137
  return files.filter((f) => HEARTBEAT_LIGHT_ALLOWLIST.has(f.name));
138
138
  }
139
+ /**
140
+ * Allowlist for cron execution context.
141
+ * Retains ONLY SOUL.md and ROLE.md -- the persona and role behavior needed
142
+ * for autonomous cron runs. Strips heartbeat cadence rules, user preferences,
143
+ * tooling docs, and workspace bootstrap.
144
+ */
145
+ const CRON_BOOTSTRAP_ALLOWLIST = new Set(["SOUL.md", "ROLE.md"]);
146
+ /**
147
+ * Filter bootstrap files for cron execution.
148
+ *
149
+ * Autonomous cron runs don't need heartbeat cadence rules, user preferences,
150
+ * tooling docs, or workspace bootstrap -- only the agent's persona (SOUL.md)
151
+ * and role behavior (ROLE.md). Mirrors the pattern of
152
+ * filterBootstrapFilesForLightContext.
153
+ *
154
+ * @param files - Full array of loaded bootstrap files
155
+ * @returns Filtered array containing only SOUL.md and ROLE.md
156
+ */
157
+ export function filterBootstrapFilesForCron(files) {
158
+ return files.filter((f) => CRON_BOOTSTRAP_ALLOWLIST.has(f.name));
159
+ }
139
160
  /**
140
161
  * Excludelist for group chat context mode.
141
162
  * Excludes USER.md (privacy protection: personal preferences
@@ -49,9 +49,14 @@ export function createPiEventBridge(deps) {
49
49
  // -----------------------------------------------------------------
50
50
  case "message_update": {
51
51
  const ame = event.assistantMessageEvent;
52
- if (ame && ame.type === "text_delta") {
53
- m.textEmitted = true; // Track that visible text was produced in some turn
54
- if (deps.onDelta) {
52
+ if (ame && (ame.type === "text_delta" || ame.type === "thinking_delta")) {
53
+ if (ame.type === "text_delta") {
54
+ // Track that visible text was produced in some turn.
55
+ // thinking_delta intentionally excluded — empty-final-turn detection
56
+ // depends on this flag reflecting user-visible text only.
57
+ m.textEmitted = true;
58
+ }
59
+ if (deps.onDelta && typeof ame.delta === "string") {
55
60
  try {
56
61
  deps.onDelta(ame.delta);
57
62
  }
@@ -394,7 +399,7 @@ export function createPiEventBridge(deps) {
394
399
  totalTokens: usage.totalTokens,
395
400
  cost,
396
401
  provider: deps.provider,
397
- model: deps.model,
402
+ model: deps.getCurrentModel?.() ?? deps.model,
398
403
  sessionKey: formatSessionKey(deps.sessionKey),
399
404
  operationType: deps.operationType,
400
405
  });
@@ -457,7 +462,7 @@ export function createPiEventBridge(deps) {
457
462
  channelId: deps.channelId,
458
463
  executionId: deps.executionId,
459
464
  provider: deps.provider,
460
- model: deps.model,
465
+ model: deps.getCurrentModel?.() ?? deps.model,
461
466
  tokens: {
462
467
  prompt: usage.input,
463
468
  completion: usage.output,
@@ -475,7 +480,7 @@ export function createPiEventBridge(deps) {
475
480
  cacheWriteTokens,
476
481
  sessionKey: formatSessionKey(deps.sessionKey),
477
482
  savedVsUncached,
478
- cacheEligible: getCacheProviderInfo(deps.provider, deps.model).cacheEligible,
483
+ cacheEligible: getCacheProviderInfo(deps.provider, deps.getCurrentModel?.() ?? deps.model).cacheEligible,
479
484
  responseId,
480
485
  cacheCreation: effectiveCacheCreation,
481
486
  });
@@ -561,17 +566,13 @@ export function createPiEventBridge(deps) {
561
566
  // SEP: Extract plan from first LLM turn that has tool calls + assistant text.
562
567
  // This runs inside the agentic loop so subsequent turns can track against the plan.
563
568
  if (deps.executionPlan && deps.sepConfig && !deps.executionPlan.current) {
564
- const hasToolCalls = Array.isArray(assistantMsg?.content) && assistantMsg.content.some((c) => {
565
- const block = c;
566
- return block.type === "toolCall" || block.type === "tool_use";
567
- });
568
569
  const assistantTextForPlan = Array.isArray(assistantMsg?.content)
569
570
  ? assistantMsg.content
570
571
  .filter((c) => c?.type === "text")
571
572
  .map((c) => c.text ?? "")
572
573
  .join(" ")
573
574
  : "";
574
- if (hasToolCalls && assistantTextForPlan.length > 0) {
575
+ if (assistantTextForPlan.length > 0) {
575
576
  const steps = extractPlanFromResponse(assistantTextForPlan, deps.sepConfig.maxSteps);
576
577
  if (steps && steps.length >= deps.sepConfig.minSteps) {
577
578
  const plan = {
@@ -96,6 +96,17 @@ export declare const MCP_DEFERRAL_THRESHOLD = 0.1;
96
96
  * this many blocks apart cannot see each other for prefix matching.
97
97
  * Used by: lookback window enforcement in stream-wrappers.ts. */
98
98
  export declare const CACHE_LOOKBACK_WINDOW = 20;
99
+ /** Maximum message blocks before cache-aware compaction trigger.
100
+ * With 4 breakpoints (3 Comis + 1 SDK) and a 20-block lookback window,
101
+ * optimal coverage spans 4 × 20 = 80 blocks. Trigger when the count
102
+ * *exceeds* 60 (i.e. first fires at 61 blocks — 75% of theoretical max)
103
+ * to leave headroom for multi-call turns.
104
+ *
105
+ * APPROXIMATION NOTE: `messages.length` (AgentMessage[]) approximates
106
+ * Anthropic's request-body `messages[]` block count but is not strictly 1:1.
107
+ * Treat 60 as a defensive setpoint, not a calibrated threshold.
108
+ * Used by: llm-compaction layer cache-aware trigger. */
109
+ export declare const CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD = 60;
99
110
  /** Context utilization percentage that triggers LLM compaction. Used by: llm-compaction layer. */
100
111
  export declare const COMPACTION_TRIGGER_PERCENT = 85;
101
112
  /** Default turns to wait before re-triggering compaction. Used by: llm-compaction layer. */
@@ -155,6 +155,17 @@ export const MCP_DEFERRAL_THRESHOLD = 0.10;
155
155
  * this many blocks apart cannot see each other for prefix matching.
156
156
  * Used by: lookback window enforcement in stream-wrappers.ts. */
157
157
  export const CACHE_LOOKBACK_WINDOW = 20;
158
+ /** Maximum message blocks before cache-aware compaction trigger.
159
+ * With 4 breakpoints (3 Comis + 1 SDK) and a 20-block lookback window,
160
+ * optimal coverage spans 4 × 20 = 80 blocks. Trigger when the count
161
+ * *exceeds* 60 (i.e. first fires at 61 blocks — 75% of theoretical max)
162
+ * to leave headroom for multi-call turns.
163
+ *
164
+ * APPROXIMATION NOTE: `messages.length` (AgentMessage[]) approximates
165
+ * Anthropic's request-body `messages[]` block count but is not strictly 1:1.
166
+ * Treat 60 as a defensive setpoint, not a calibrated threshold.
167
+ * Used by: llm-compaction layer cache-aware trigger. */
168
+ export const CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD = 60;
158
169
  // ---------------------------------------------------------------------------
159
170
  // LLM Compaction (Layer 5: )
160
171
  // ---------------------------------------------------------------------------
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export type { TokenBudget, TokenAnchor, ContextLayer, ContextEngine, ContextEngineDeps, ContextEngineMetrics, AssembledContext, LayerCircuitBreaker, MicrocompactionGuard, ObservationMasker, CacheOptimizationMetrics, CacheSessionStats, EvictionStats, } from "./types.js";
10
10
  export type { ToolMaskingTier } from "./constants.js";
11
- export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
11
+ export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
12
12
  export { computeTokenBudget } from "./token-budget.js";
13
13
  export { createContextEngine } from "./context-engine.js";
14
14
  export { createThinkingBlockCleaner } from "./thinking-block-cleaner.js";
@@ -8,7 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  // Constants
11
- export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
11
+ export { SAFETY_MARGIN_PERCENT, MIN_SAFETY_MARGIN_TOKENS, OUTPUT_RESERVE_TOKENS, CONTEXT_ROT_BUFFER_PERCENT, LAYER_CIRCUIT_BREAKER_THRESHOLD, DEFAULT_KEEP_WINDOW_TURNS, MAX_INLINE_TOOL_RESULT_CHARS, MAX_INLINE_MCP_TOOL_RESULT_CHARS, MAX_INLINE_FILE_READ_RESULT_CHARS, TOOL_RESULT_HARD_CAP_CHARS, DEFAULT_OBSERVATION_KEEP_WINDOW, OBSERVATION_MASKING_CHAR_THRESHOLD, TOOL_MASKING_TIERS, resolveToolMaskingTier, EPHEMERAL_TOOL_KEEP_WINDOW, CHARS_PER_TOKEN_RATIO, CHARS_PER_TOKEN_RATIO_STRUCTURED, SYSTEM_PROMPT_HASH_LENGTH, BOOTSTRAP_BUDGET_WARN_PERCENT, MIN_CACHEABLE_TOKENS, DEFAULT_MIN_CACHEABLE_TOKENS, MCP_DEFERRAL_THRESHOLD, CACHE_LOOKBACK_WINDOW, CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD, COMPACTION_TRIGGER_PERCENT, COMPACTION_COOLDOWN_TURNS, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, } from "./constants.js";
12
12
  // Token budget algebra
13
13
  export { computeTokenBudget } from "./token-budget.js";
14
14
  // Context engine factory
@@ -25,7 +25,7 @@
25
25
  * @module
26
26
  */
27
27
  import { generateSummary } from "@mariozechner/pi-coding-agent";
28
- import { COMPACTION_TRIGGER_PERCENT, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, CHARS_PER_TOKEN_RATIO, MIN_MIDDLE_MESSAGES_FOR_COMPACTION, } from "./constants.js";
28
+ import { COMPACTION_TRIGGER_PERCENT, COMPACTION_MAX_RETRIES, OVERSIZED_MESSAGE_CHARS_THRESHOLD, COMPACTION_REQUIRED_SECTIONS, CHARS_PER_TOKEN_RATIO, MIN_MIDDLE_MESSAGES_FOR_COMPACTION, CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD, } from "./constants.js";
29
29
  import { estimateContextCharsWithDualRatio, estimateMessageChars, estimateWithAnchor, } from "../safety/token-estimator.js";
30
30
  // ---------------------------------------------------------------------------
31
31
  // Structured output instructions
@@ -258,27 +258,41 @@ export function createLlmCompactionLayer(config, deps) {
258
258
  if (turnsSinceLastCompaction < config.compactionCooldownTurns) {
259
259
  return messages;
260
260
  }
261
- // Step 3: Threshold check
262
- // Use anchor-based estimation when available for accurate threshold check
263
- /* eslint-disable @typescript-eslint/no-explicit-any */
264
- const contextChars = estimateContextCharsWithDualRatio(messages);
265
- /* eslint-enable @typescript-eslint/no-explicit-any */
266
- const charBasedTokens = Math.ceil(contextChars / CHARS_PER_TOKEN_RATIO);
267
- const anchor = deps.getTokenAnchor?.() ?? null;
268
- const contextTokens = estimateWithAnchor(anchor, messages, charBasedTokens);
269
- const thresholdTokens = Math.floor(budget.windowTokens * COMPACTION_TRIGGER_PERCENT / 100);
270
- if (contextTokens <= thresholdTokens) {
271
- return messages;
261
+ // Step 2b: Cache-aware block count trigger.
262
+ // Fires BEFORE the token-based threshold because lookback overflow
263
+ // causes cache breaks regardless of how few tokens the messages contain.
264
+ const messageCount = messages.length;
265
+ const blockThreshold = CACHE_AWARE_COMPACTION_BLOCK_THRESHOLD;
266
+ const blockCountExceeded = messageCount > blockThreshold;
267
+ // Step 3: Token threshold check (only when block-count trigger didn't fire)
268
+ let contextTokens;
269
+ let thresholdTokens;
270
+ if (!blockCountExceeded) {
271
+ /* eslint-disable @typescript-eslint/no-explicit-any */
272
+ const contextChars = estimateContextCharsWithDualRatio(messages);
273
+ /* eslint-enable @typescript-eslint/no-explicit-any */
274
+ const charBasedTokens = Math.ceil(contextChars / CHARS_PER_TOKEN_RATIO);
275
+ const anchor = deps.getTokenAnchor?.() ?? null;
276
+ contextTokens = estimateWithAnchor(anchor, messages, charBasedTokens);
277
+ thresholdTokens = Math.floor(budget.windowTokens * COMPACTION_TRIGGER_PERCENT / 100);
278
+ if (contextTokens <= thresholdTokens) {
279
+ return messages;
280
+ }
272
281
  }
273
- // Step 4: Pipeline order guarantees observation masker ran before this layer
282
+ // Step 4: Unified log (conditional spread keeps JSON shape clean).
274
283
  deps.logger.warn({
275
- contextTokens,
276
- thresholdTokens,
284
+ messageCount,
285
+ ...(blockCountExceeded
286
+ ? { blockThreshold, trigger: "block_count" }
287
+ : { contextTokens, thresholdTokens, trigger: "token_threshold" }),
277
288
  windowTokens: budget.windowTokens,
278
- messageCount: messages.length,
279
289
  errorKind: "resource",
280
- hint: "Context approaching capacity; LLM compaction will summarize older messages to free space",
281
- }, "LLM compaction triggered: context exceeds 85% threshold");
290
+ hint: blockCountExceeded
291
+ ? "Message count approaching breakpoint lookback limit; compacting to prevent cache fragmentation"
292
+ : "Context approaching capacity; LLM compaction will summarize older messages to free space",
293
+ }, blockCountExceeded
294
+ ? "LLM compaction triggered: message count exceeds cache lookback threshold"
295
+ : "LLM compaction triggered: context exceeds 85% threshold");
282
296
  // Step 5: Resolve model
283
297
  /* eslint-disable @typescript-eslint/no-explicit-any */
284
298
  let model;
@@ -60,6 +60,8 @@ export interface CacheBreakDiffPayload {
60
60
  message: number;
61
61
  sdkAuto: number;
62
62
  };
63
+ /** Model ID for per-model cost attribution. Populated by pi-event-bridge.ts. */
64
+ model?: string;
63
65
  }
64
66
  export interface CacheBreakDiffWriterConfig {
65
67
  /** Directory for diff files (e.g., ~/.comis/cache-breaks) */
@@ -12,6 +12,7 @@
12
12
  import { writeFileSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
13
13
  import { createPatch } from "diff";
14
14
  import { safePath } from "@comis/core";
15
+ import { resolveModelPricing, ZERO_COST } from "../model/model-catalog.js";
15
16
  const MAX_DIFF_FILES = 50;
16
17
  /** DIFF-CONTENT: Maximum chars per category (system, tools) for snapshot content before diffing. */
17
18
  const MAX_SNAPSHOT_CHARS = 50_000;
@@ -40,6 +41,7 @@ export function buildDiffableContent(system, tools, model) {
40
41
  */
41
42
  export function createCacheBreakDiffWriter(config) {
42
43
  let dirEnsured = false;
44
+ const unknownModelWarnLatch = new Set();
43
45
  return (event) => {
44
46
  try {
45
47
  if (!dirEnsured) {
@@ -50,6 +52,24 @@ export function createCacheBreakDiffWriter(config) {
50
52
  const ts = new Date(event.timestamp).toISOString().replace(/[:.]/g, "-");
51
53
  const filename = `${ts}_${event.agentId}_${event.reason}.json`;
52
54
  const filePath = safePath(config.outputDir, filename);
55
+ // Cost attribution: compute estimated USD impact using per-model pricing.
56
+ const modelId = event.model ?? "";
57
+ const pricing = resolveModelPricing(event.provider, modelId);
58
+ const pricingKnown = pricing !== ZERO_COST && pricing.input > 0;
59
+ const retentionDisabled = event.ttlCategory === "none";
60
+ const writeRatePerToken = event.ttlCategory === "long" ? pricing.cacheWrite1h : pricing.cacheWrite;
61
+ const readRatePerToken = pricing.cacheRead;
62
+ const perTokenDelta = Math.max(0, writeRatePerToken - readRatePerToken);
63
+ const estimatedCostUsd = retentionDisabled
64
+ ? 0
65
+ : (pricingKnown ? event.tokenDrop * perTokenDelta : null);
66
+ if (!pricingKnown && !retentionDisabled && modelId) {
67
+ const latchKey = `${event.provider}:${modelId}`;
68
+ if (!unknownModelWarnLatch.has(latchKey)) {
69
+ unknownModelWarnLatch.add(latchKey);
70
+ config.logger.warn({ provider: event.provider, model: modelId, hint: "Cache break cost attribution unavailable for this model", errorKind: "config" }, "Unknown model pricing for cache break cost attribution");
71
+ }
72
+ }
53
73
  const diff = {
54
74
  timestamp: new Date(event.timestamp).toISOString(),
55
75
  agentId: event.agentId,
@@ -85,6 +105,22 @@ export function createCacheBreakDiffWriter(config) {
85
105
  effortValue: event.effortValue,
86
106
  // Breakpoint budget context
87
107
  ...(event.breakpointBudget && { breakpointBudget: event.breakpointBudget }),
108
+ // Cost attribution
109
+ estimatedCostUsd: estimatedCostUsd === null
110
+ ? null
111
+ : Math.round(estimatedCostUsd * 100_000) / 100_000,
112
+ costBreakdown: {
113
+ writeRatePerMTok: pricingKnown
114
+ ? Math.round(writeRatePerToken * 1_000_000 * 100) / 100
115
+ : null,
116
+ readRatePerMTok: pricingKnown
117
+ ? Math.round(readRatePerToken * 1_000_000 * 100) / 100
118
+ : null,
119
+ tokenDrop: event.tokenDrop,
120
+ model: event.model ?? null,
121
+ ttlCategory: event.ttlCategory,
122
+ pricingKnown,
123
+ },
88
124
  };
89
125
  writeFileSync(filePath, JSON.stringify(diff, null, 2) + "\n");
90
126
  // DIFF-CONTENT: Generate unified diff file alongside JSON
@@ -140,6 +140,18 @@ export interface PostExecutionParams {
140
140
  * @returns true if the turn qualifies for memory storage
141
141
  */
142
142
  export declare function shouldStorePairedMemory(userText: string, agentResponse: string): boolean;
143
+ /**
144
+ * Check whether paired memory content was stored recently for this agent.
145
+ *
146
+ * Exact-content hashing (not semantic similarity) -- targets the cron pattern
147
+ * of identical prompt + identical NO_REPLY response. Lazy eviction keeps the
148
+ * cache bounded without a timer.
149
+ *
150
+ * Exported for unit tests.
151
+ */
152
+ export declare function isDuplicatePairedMemory(content: string, agentId: string): boolean;
153
+ /** Reset the paired-memory dedup cache. Exported for unit tests. */
154
+ export declare function resetPairedMemoryDedupForTests(): void;
143
155
  /**
144
156
  * Run post-execution cleanup for a PiExecutor turn.
145
157
  *