flint-agent 1.14.0

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 (171) hide show
  1. package/.env.example +108 -0
  2. package/CHANGELOG.md +55 -0
  3. package/FEATURES.md +298 -0
  4. package/LICENSE +21 -0
  5. package/README.md +435 -0
  6. package/bin/flint.js +47 -0
  7. package/config/classifier-prompt.md +218 -0
  8. package/config/models-curated.json +4 -0
  9. package/config/providers.json +74 -0
  10. package/package.json +92 -0
  11. package/patches/ink+6.8.0.patch +78 -0
  12. package/profiles/desktop.md +65 -0
  13. package/profiles/generic.md +20 -0
  14. package/profiles/marketer.md +20 -0
  15. package/profiles/profiles.json +34 -0
  16. package/profiles/ux-reviewer.md +25 -0
  17. package/src/agent/agent.js +1743 -0
  18. package/src/agent/auto.js +346 -0
  19. package/src/agent/backoff.js +143 -0
  20. package/src/agent/compression.js +310 -0
  21. package/src/agent/content-resolver.js +180 -0
  22. package/src/agent/flow-controller.js +309 -0
  23. package/src/agent/intent-manifest.js +231 -0
  24. package/src/agent/intent-timeout.js +46 -0
  25. package/src/agent/intent.js +633 -0
  26. package/src/agent/knowledge.js +114 -0
  27. package/src/agent/learning.js +180 -0
  28. package/src/agent/modes.js +187 -0
  29. package/src/agent/outcome-ask.js +91 -0
  30. package/src/agent/project-context.js +76 -0
  31. package/src/agent/prompt-budget.js +117 -0
  32. package/src/agent/reflection-extractor.js +140 -0
  33. package/src/agent/steering.js +86 -0
  34. package/src/agent/supervisor.js +430 -0
  35. package/src/agent/swap.js +443 -0
  36. package/src/agent/system-prompt.js +446 -0
  37. package/src/agent/time-stamp.js +48 -0
  38. package/src/agent/tool-guard.js +201 -0
  39. package/src/agent/toolcall-text.js +162 -0
  40. package/src/agent/usage.js +297 -0
  41. package/src/agent/vision.js +94 -0
  42. package/src/agent/watchdog.js +139 -0
  43. package/src/agent/workspace-changes.js +177 -0
  44. package/src/api/address.js +14 -0
  45. package/src/api/client.js +280 -0
  46. package/src/api/server.js +535 -0
  47. package/src/api/stream-pipe.js +113 -0
  48. package/src/app-state.js +39 -0
  49. package/src/bootstrap.js +501 -0
  50. package/src/bus/drain-loop.js +497 -0
  51. package/src/bus/index.js +270 -0
  52. package/src/bus/plugins.js +65 -0
  53. package/src/child-idle.js +14 -0
  54. package/src/cli.js +118 -0
  55. package/src/commands/commands.js +1297 -0
  56. package/src/commands/registry.js +132 -0
  57. package/src/components/App.js +491 -0
  58. package/src/components/CarefulMenu.js +145 -0
  59. package/src/components/HistoryWriter.js +86 -0
  60. package/src/components/LineInput.js +69 -0
  61. package/src/components/LiveZone.js +294 -0
  62. package/src/components/OverlayMenu.js +179 -0
  63. package/src/components/SystemPanel.js +156 -0
  64. package/src/components/Table.js +54 -0
  65. package/src/config.js +249 -0
  66. package/src/free-models.js +230 -0
  67. package/src/index.js +1111 -0
  68. package/src/input-handler.js +13 -0
  69. package/src/input-text.js +123 -0
  70. package/src/launcher.js +129 -0
  71. package/src/logging/api-log.js +95 -0
  72. package/src/logging/chat-log-follower.js +113 -0
  73. package/src/logging/chat-log.js +15 -0
  74. package/src/logging/log-collector.js +182 -0
  75. package/src/logging/logger.js +112 -0
  76. package/src/logging/tool-log.js +20 -0
  77. package/src/mcp-client.js +314 -0
  78. package/src/memory/conversation-digest.js +113 -0
  79. package/src/memory/extract-facts.js +98 -0
  80. package/src/memory/facts.js +181 -0
  81. package/src/memory/inbox.js +63 -0
  82. package/src/memory/markdown.js +38 -0
  83. package/src/memory/patterns.js +185 -0
  84. package/src/memory/project.js +66 -0
  85. package/src/memory/reflections.js +74 -0
  86. package/src/memory/retrieval.js +84 -0
  87. package/src/memory/rules.js +105 -0
  88. package/src/memory/session-facts.js +125 -0
  89. package/src/memory/skills.js +191 -0
  90. package/src/memory/sqlite-store.js +653 -0
  91. package/src/memory/store.js +208 -0
  92. package/src/memory/tools.js +196 -0
  93. package/src/memory/user-model.js +86 -0
  94. package/src/message-handler.js +775 -0
  95. package/src/model-check.js +218 -0
  96. package/src/plugins/loader.js +120 -0
  97. package/src/plugins/manager.js +88 -0
  98. package/src/production-env.js +22 -0
  99. package/src/profiles.js +42 -0
  100. package/src/providers/adapters/anthropic.js +270 -0
  101. package/src/providers/adapters/openai.js +120 -0
  102. package/src/providers/keys-dpapi.js +41 -0
  103. package/src/providers/keys-fallback.js +31 -0
  104. package/src/providers/keys.js +132 -0
  105. package/src/providers/models.js +154 -0
  106. package/src/providers/registry.js +56 -0
  107. package/src/providers/state.js +56 -0
  108. package/src/registry.js +96 -0
  109. package/src/restart.js +29 -0
  110. package/src/sandbox/backend.js +130 -0
  111. package/src/security/api-auth.js +132 -0
  112. package/src/security/audit.js +98 -0
  113. package/src/security/child-policy.js +41 -0
  114. package/src/security/command-guard.js +173 -0
  115. package/src/security/content-fence.js +250 -0
  116. package/src/security/content-validator.js +132 -0
  117. package/src/security/index.js +143 -0
  118. package/src/security/network-guard.js +126 -0
  119. package/src/security/pairing.js +180 -0
  120. package/src/security/path-guard.js +140 -0
  121. package/src/security/persona-guard.js +67 -0
  122. package/src/security/policies.js +452 -0
  123. package/src/security/safety-constants.js +34 -0
  124. package/src/security/watchdog.js +107 -0
  125. package/src/sessions.js +130 -0
  126. package/src/spend.js +97 -0
  127. package/src/startup-watchdog.js +59 -0
  128. package/src/stdio/args.js +71 -0
  129. package/src/stdio/guard.js +59 -0
  130. package/src/stdio/protocol.js +167 -0
  131. package/src/stdio/run.js +106 -0
  132. package/src/stdio/session.js +180 -0
  133. package/src/store/agent-slice.js +306 -0
  134. package/src/store/dataset-slice.js +73 -0
  135. package/src/store/index.js +22 -0
  136. package/src/store/process-slice.js +135 -0
  137. package/src/store/session-slice.js +191 -0
  138. package/src/store/ui-slice.js +119 -0
  139. package/src/tasks/db.js +184 -0
  140. package/src/tasks/queries.js +589 -0
  141. package/src/tools/agent-tools.js +473 -0
  142. package/src/tools/checkpoint.js +152 -0
  143. package/src/tools/command-approvals.js +180 -0
  144. package/src/tools/dataset.js +50 -0
  145. package/src/tools/filesystem.js +682 -0
  146. package/src/tools/inbox-tools.js +48 -0
  147. package/src/tools/mesh.js +135 -0
  148. package/src/tools/own-env.js +136 -0
  149. package/src/tools/permissions.js +681 -0
  150. package/src/tools/plugin-tools.js +123 -0
  151. package/src/tools/process-tools.js +595 -0
  152. package/src/tools/registry.js +307 -0
  153. package/src/tools/swap-tools.js +72 -0
  154. package/src/tools/system.js +662 -0
  155. package/src/tools/tasks.js +532 -0
  156. package/src/tools/tool-search.js +171 -0
  157. package/src/ui/header.js +140 -0
  158. package/src/ui/input-cursor.js +23 -0
  159. package/src/ui/last-line.js +25 -0
  160. package/src/ui/line-edit.js +135 -0
  161. package/src/ui/output.js +399 -0
  162. package/src/ui/paste-tokens.js +131 -0
  163. package/src/ui/prompt-attention.js +134 -0
  164. package/src/ui/render-options.js +13 -0
  165. package/src/ui/replay.js +94 -0
  166. package/src/ui/splash.js +49 -0
  167. package/src/ui/status-level.js +36 -0
  168. package/src/ui/tool-ledger.js +203 -0
  169. package/src/ui/window-title.js +150 -0
  170. package/src/update.js +205 -0
  171. package/system.md +63 -0
@@ -0,0 +1,446 @@
1
+ import { swapEnabled } from "./swap.js";
2
+ import { getSpendLevel, spendSettings } from "../spend.js";
3
+ import os from "node:os";
4
+
5
+ const ECONOMY_ADVICE = [
6
+ "## Spending tokens (economy mode)",
7
+ "",
8
+ "Every call sends the whole conversation again, so keep it short:",
9
+ "- Search before reading (`search_in_files`, `glob`), and read the part of a file you need (offset/limit), not all of it.",
10
+ "- Read a page once and note what matters in your reply; do not fetch it again (`swap_read` brings back what was read).",
11
+ "- Make independent tool calls in one response.",
12
+ "- Answer briefly; no recaps of what you already said.",
13
+ ].join("\n");
14
+ import { readFileSync, existsSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { readMemoryMdHead } from "../memory/markdown.js";
17
+ import { getSessionFactsSummary } from "../memory/session-facts.js";
18
+ import { getDigestForPrompt } from "../memory/conversation-digest.js";
19
+ import { loadRecent as loadRecentReflections, formatForPrompt as formatReflections } from "../memory/reflections.js";
20
+ import { compilePreferences, formatForPrompt as formatPatterns } from "../memory/patterns.js";
21
+ import { createBudgetAllocator } from "./prompt-budget.js";
22
+ import { formatForPrompt as formatSkills, syncFromDisk as syncSkills } from "../memory/skills.js";
23
+ import { formatForPrompt as formatFacts } from "../memory/facts.js";
24
+ import { getCurrentProject } from "../memory/project.js";
25
+ import { formatForPrompt as formatUserModel } from "../memory/user-model.js";
26
+ import { compileRules, formatForPrompt as formatRules } from "../memory/rules.js";
27
+ import { getDefinitions } from "../tools/registry.js";
28
+ import { detectInjection } from "../security/content-fence.js";
29
+ import { formatFlintMdBlock } from "./project-context.js";
30
+ import { inboxPromptHint } from "../memory/inbox.js";
31
+ import { getModeForIntent } from "./modes.js";
32
+ import { config } from "../config.js";
33
+
34
+ // --- System context (system.md — ships with Flint) ---
35
+
36
+ function loadSystemMd() {
37
+ // Only Flint's own copy. cwd used to be tried first, so any folder the user
38
+ // started Flint in that held a file called system.md replaced the whole core
39
+ // prompt without a word. A project speaks through FLINT.md instead.
40
+ const p = join(new URL("../..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"), "system.md");
41
+ try {
42
+ if (existsSync(p)) {
43
+ const text = readFileSync(p, "utf-8").trim();
44
+ // With swap off the swap rule describes tools that are not there, and
45
+ // FLINT_SWAP=0 promises the prompt as it was (docs/context-swap.md, A4).
46
+ const base = swapEnabled() ? text : text.split("\n").filter((l) => !l.includes("swap_read")).join("\n");
47
+ // Economy mode only (spend.js): in the other modes the advice would
48
+ // make the agent hold back for no reason.
49
+ return spendSettings(getSpendLevel()).advice ? `${base}\n\n${ECONOMY_ADVICE}` : base;
50
+ }
51
+ } catch {}
52
+ return null;
53
+ }
54
+
55
+ // --- Flint capability summary (from bundled README.md) ---
56
+ // Extracts the "What can it do?" section and the Features headings from
57
+ // Flint's own README. Injected into the system prompt so meta-questions
58
+ // ("what do you support?", "do you work with MCP?") answer from a live source
59
+ // that tracks actual Flint architecture — no hardcoded capability list,
60
+ // no decay, no ambiguity about which README to read.
61
+
62
+ function loadFlintCapabilities() {
63
+ let text = null;
64
+ try {
65
+ const flintRoot = new URL("../..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
66
+ const readmePath = join(flintRoot, "README.md");
67
+ if (existsSync(readmePath)) text = readFileSync(readmePath, "utf-8");
68
+ } catch {}
69
+ if (!text) return null;
70
+
71
+ // "## What can it do?" section up to next top-level heading
72
+ const canDoMatch = text.match(/## What can it do\?([\s\S]*?)(?=\n## |\n# |$)/);
73
+ const canDo = canDoMatch ? canDoMatch[1].trim() : "";
74
+
75
+ // Features headings + first meaningful prose paragraph per feature.
76
+ // Skips markdown tables (| ... |), code fences (```), and list-only
77
+ // blocks so the captured blurb is actually descriptive. This matters
78
+ // because e.g. the MCP feature leads with a big tool-category table
79
+ // and only explains itself in a prose paragraph below it — without
80
+ // skipping the table the agent has no signal about what MCP means
81
+ // and defaults to hallucinating "Minecraft Coder Pack".
82
+ const featuresMatch = text.match(/## Features([\s\S]*?)(?=\n## |\n# |$)/);
83
+ const featureLines = [];
84
+ if (featuresMatch) {
85
+ const lines = featuresMatch[1].split("\n");
86
+ for (let i = 0; i < lines.length; i++) {
87
+ const l = lines[i].trim();
88
+ if (!l.startsWith("### ")) continue;
89
+ const heading = l.replace(/^### /, "");
90
+ let blurb = "";
91
+ let inCodeFence = false;
92
+ // Scan up to 25 lines after the heading or until next heading
93
+ for (let j = i + 1; j < Math.min(i + 25, lines.length); j++) {
94
+ const raw = lines[j];
95
+ const ln = raw.trim();
96
+ if (ln.startsWith("### ") || ln.startsWith("## ") || ln.startsWith("# ")) break;
97
+ if (ln.startsWith("```")) { inCodeFence = !inCodeFence; continue; }
98
+ if (inCodeFence) continue;
99
+ // Skip markdown table rows and separator lines
100
+ if (ln.startsWith("|")) continue;
101
+ if (!ln) { if (blurb) break; else continue; }
102
+ blurb += (blurb ? " " : "") + ln;
103
+ if (blurb.length > 320) { blurb = blurb.slice(0, 320) + "..."; break; }
104
+ }
105
+ featureLines.push(blurb ? `- ${heading} — ${blurb}` : `- ${heading}`);
106
+ }
107
+ }
108
+
109
+ const parts = [];
110
+ if (canDo) parts.push(canDo);
111
+ if (featureLines.length) parts.push("Feature highlights:\n" + featureLines.join("\n"));
112
+ return parts.length ? parts.join("\n\n") : null;
113
+ }
114
+
115
+ // FLINT.md loading moved to ./project-context.js — shared across classifier,
116
+ // agent loop, and any other LLM-call site. See that module for the loader
117
+ // and the trusted-context wrapping.
118
+
119
+ const MAX_CONTEXT_CHARS = config.maxContextChars;
120
+
121
+ // --- OS detection ---
122
+
123
+ function getOsInfo() {
124
+ const platform = process.platform;
125
+ if (platform === "win32") {
126
+ return {
127
+ name: "Windows",
128
+ shell: "bash (via run_command)",
129
+ shellTips: "run_command uses bash even on Windows. Use Unix commands (ls, cat, grep, find). Do NOT use cmd.exe or PowerShell commands.",
130
+ };
131
+ }
132
+ if (platform === "darwin") {
133
+ return {
134
+ name: "macOS",
135
+ shell: "zsh",
136
+ shellTips: "Standard Unix commands. Paths use forward slashes.",
137
+ };
138
+ }
139
+ return {
140
+ name: "Linux",
141
+ shell: "bash",
142
+ shellTips: "Standard Unix commands. Paths use forward slashes.",
143
+ };
144
+ }
145
+
146
+ // --- Dynamic tool list from registry ---
147
+
148
+ function getToolList() {
149
+ const tools = getDefinitions();
150
+ if (!tools.length) return "";
151
+
152
+ // Group tools by category based on name patterns
153
+ const categories = {
154
+ filesystem: { label: "LOCAL FILESYSTEM (this machine)", names: new Set(["read_file", "write_file", "list_directory", "create_directory", "copy_file", "move_file", "delete_file", "search_in_files", "view_image", "edit_file", "glob"]) },
155
+ process: { label: "LOCAL PROCESS (this machine)", names: new Set(["run_command", "run_background_command", "kill_process", "list_processes", "peek_process"]) },
156
+ web: { label: "WEB", names: new Set(["web_fetch", "web_search"]) },
157
+ agent: { label: "MULTI-AGENT", names: new Set(["spawn_agent", "ask_agent", "list_agents"]) },
158
+ memory: { label: "MEMORY", names: new Set(["memory_write", "memory_search", "memory_get", "memory_delete"]) },
159
+ planning: { label: "PLANNING", names: new Set(["create_plan", "update_task", "list_tasks", "add_task", "add_task_note", "link_task_file"]) },
160
+ desktop: { label: "LOCAL DESKTOP (built-in, this machine)", names: new Set(["desktop_screenshot", "desktop_look", "desktop_click", "desktop_type", "desktop_key", "desktop_chrome", "desktop_resume", "desktop_batch", "desktop_shell", "desktop_window", "desktop_manage", "desktop_file", "desktop_wait_stable", "desktop_wait_change"]) },
161
+ mesh: { label: "MESH MEMORY", names: new Set(["mesh_search", "mesh_add", "mesh_recent"]) },
162
+ dataset: { label: "DATASETS", names: new Set(["show_dataset"]) },
163
+ system: { label: "SYSTEM", names: new Set(["restart_agent", "clear_context", "check_balance", "think", "list_models", "switch_model", "switch_provider", "list_providers", "check_inbox"]) },
164
+ };
165
+
166
+ const grouped = {};
167
+ const uncategorized = [];
168
+
169
+ for (const tool of tools) {
170
+ const name = tool.function.name;
171
+ let found = false;
172
+
173
+ // Check exact name match in built-in categories
174
+ for (const [key, cat] of Object.entries(categories)) {
175
+ if (cat.names.has(name)) {
176
+ if (!grouped[key]) grouped[key] = { label: cat.label, tools: [] };
177
+ grouped[key].tools.push(name);
178
+ found = true;
179
+ break;
180
+ }
181
+ }
182
+
183
+ // MCP tools: auto-group by server name (mcp_<server>_<tool>)
184
+ if (!found && name.startsWith("mcp_")) {
185
+ const parts = name.split("_");
186
+ const serverName = parts[1] || "unknown";
187
+ const mcpKey = `mcp_${serverName}`;
188
+ if (!grouped[mcpKey]) grouped[mcpKey] = { label: `MCP: ${serverName} (remote)`, tools: [] };
189
+ grouped[mcpKey].tools.push(name);
190
+ found = true;
191
+ }
192
+
193
+ if (!found) {
194
+ uncategorized.push(name);
195
+ }
196
+ }
197
+
198
+ const sections = [];
199
+ for (const key of Object.keys(categories)) {
200
+ if (grouped[key]) {
201
+ sections.push(`${grouped[key].label}: ${grouped[key].tools.join(", ")}`);
202
+ }
203
+ }
204
+ if (uncategorized.length) {
205
+ sections.push(`OTHER: ${uncategorized.join(", ")}`);
206
+ }
207
+
208
+ return sections.join("\n\n");
209
+ }
210
+
211
+ // --- Build system prompt ---
212
+
213
+ function buildPrompt(opts = {}) {
214
+ const { profile, profileContent } = opts;
215
+ const osInfo = getOsInfo();
216
+
217
+ const parts = [];
218
+
219
+ // Core prompt loaded from system.md (no hardcoded instructions in code)
220
+ const systemMd = loadSystemMd();
221
+ if (systemMd) {
222
+ parts.push(systemMd);
223
+ } else {
224
+ parts.push("You are FLINT, a technical AI agent that executes tasks using available tools.");
225
+ }
226
+
227
+ // Project rules (FLINT.md) — immediately after system.md, before profile.
228
+ // This is the agent's own project notebook: facts about the current
229
+ // project, conventions, server names, workspace identifiers, shortcuts.
230
+ // Trusted because it's maintained by the agent itself (scanned for
231
+ // injection). Placed high in the prompt so it's not lost in context
232
+ // middle. See src/agent/project-context.js for the loader.
233
+ const flintMdBlock = formatFlintMdBlock(process.cwd());
234
+ if (flintMdBlock) {
235
+ parts.push("");
236
+ parts.push(flintMdBlock);
237
+ }
238
+
239
+ // Profile prompt (injected from profile .md file)
240
+ if (profileContent) {
241
+ parts.push("");
242
+ parts.push(`--- ROLE: ${profile || "custom"} ---`);
243
+ parts.push(profileContent);
244
+ parts.push("--- END ROLE ---");
245
+ }
246
+
247
+ // Environment, the part of it that does not move. The clock used to live on
248
+ // this line, in the middle of the section the layout calls FIXED, at minute
249
+ // resolution: it changed the prefix every later token is cached against
250
+ // roughly once a minute. It is now a per-turn section below the boundary.
251
+ parts.push("");
252
+ parts.push(`ENVIRONMENT: ${osInfo.name} (${os.arch()}) | Shell: ${osInfo.shell} | CWD: ${process.cwd()}`);
253
+ if (osInfo.name === "Windows") parts.push("Shell uses bash. Use Unix commands (ls, cat, grep), not PowerShell.");
254
+
255
+ // Task-driven child agent instructions
256
+ if (process.env.AGENT_TASK_ID) {
257
+ parts.push("");
258
+ parts.push([
259
+ `CHILD AGENT MODE: Executing task #${process.env.AGENT_TASK_ID}.`,
260
+ "Focus ONLY on this task. Use update_task to report progress. Auto-completed on exit.",
261
+ ].join("\n"));
262
+ }
263
+
264
+ return parts.join("\n");
265
+ }
266
+
267
+ // --- Agent-memory prefetch ---
268
+ let _agentMemoryContext = null;
269
+
270
+ export async function prefetchAgentMemory() {
271
+ try {
272
+ const { execFileSync } = await import("node:child_process");
273
+ // Try to find agent-memory binary
274
+ const bins = [
275
+ process.env.AGENT_MEMORY_BIN,
276
+ "agent-memory",
277
+ "agent-memory.exe",
278
+ ].filter(Boolean);
279
+ for (const bin of bins) {
280
+ try {
281
+ const result = execFileSync(bin, ["context", "--budget", "3000"], {
282
+ encoding: "utf-8",
283
+ timeout: 5000,
284
+ stdio: ["pipe", "pipe", "ignore"],
285
+ }).trim();
286
+ if (result && result.length > 10) {
287
+ _agentMemoryContext = result;
288
+ return result;
289
+ }
290
+ } catch {}
291
+ }
292
+ } catch {}
293
+ return null;
294
+ }
295
+
296
+ // --- Public API ---
297
+
298
+ export function getSystemMessage(sessionId, opts = {}) {
299
+ const allocator = createBudgetAllocator();
300
+
301
+ // ── FIXED SECTIONS (never truncated) ──
302
+
303
+ // Nothing added here may vary within a session. Everything downstream is
304
+ // cached against this text, so a single moving character in it costs the
305
+ // whole prompt. The mode block used to be appended right here and changes
306
+ // with the intent class, which changes every turn; it is a per-turn section
307
+ // now.
308
+ const coreContent = buildPrompt(opts);
309
+
310
+ allocator.addSection("core", coreContent, { priority: 1, fixed: true });
311
+
312
+ // Flint architectural capabilities (from bundled README) — injected
313
+ // so self-description questions have a live source without the agent
314
+ // needing to guess or read an ambiguous README from the user's cwd.
315
+ const capabilitiesText = loadFlintCapabilities();
316
+ if (capabilitiesText) {
317
+ allocator.addSection(
318
+ "flint-capabilities",
319
+ `<trusted-context name="flint-capabilities" source="README.md">\n${capabilitiesText}\n</trusted-context>`,
320
+ { priority: 2, min: 300, max: 2500 },
321
+ );
322
+ }
323
+
324
+ // ── SESSION-STABLE ZONE (cached across turns, budgeted) ──
325
+
326
+ const memoryContent = readMemoryMdHead(50);
327
+ if (memoryContent) {
328
+ const scan = detectInjection(memoryContent);
329
+ const memText = (scan.detected && scan.score >= 3)
330
+ ? `[BLOCKED: MEMORY.md failed injection scan (score ${scan.score})]`
331
+ : memoryContent;
332
+ allocator.addSection("memory-md", `<untrusted-context name="memory" source="MEMORY.md">\n${memText}\n</untrusted-context>`, { priority: 3, min: 200, max: 2000 });
333
+ }
334
+
335
+ if (_agentMemoryContext) {
336
+ allocator.addSection("agent-memory", `<trusted-context name="agent-memory">\n${_agentMemoryContext}\n</trusted-context>`, { priority: 3, min: 500, max: 3000 });
337
+ }
338
+
339
+ // Memory Layer 1: reflections
340
+ try {
341
+ const recentReflections = loadRecentReflections(5);
342
+ const reflectionsText = formatReflections(recentReflections);
343
+ if (reflectionsText) {
344
+ allocator.addSection("reflections", `<trusted-context name="past-lessons">\n${reflectionsText}\n</trusted-context>`, { priority: 4, min: 200, max: 1500 });
345
+ }
346
+ } catch {}
347
+
348
+ // Memory Layer 2: patterns
349
+ try {
350
+ const prefs = compilePreferences();
351
+ const patternsText = formatPatterns(prefs);
352
+ if (patternsText) {
353
+ allocator.addSection("patterns", `<trusted-context name="tool-patterns">\n${patternsText}\n</trusted-context>`, { priority: 4, min: 200, max: 1500 });
354
+ }
355
+ } catch {}
356
+
357
+ // Memory Layer 3: skills (index only)
358
+ try {
359
+ syncSkills();
360
+ const skillsText = formatSkills();
361
+ if (skillsText) {
362
+ const scan = detectInjection(skillsText);
363
+ const text = (scan.detected && scan.score >= 3)
364
+ ? `<trusted-context name="skills">\n[BLOCKED: skills content failed injection scan (score ${scan.score}).]\n</trusted-context>`
365
+ : `<untrusted-context name="skills" source="~/.flint/memory/skills/">\n${skillsText}\n</untrusted-context>`;
366
+ allocator.addSection("skills", text, { priority: 5, min: 100, max: 1000 });
367
+ }
368
+ } catch {}
369
+
370
+ // Memory Layer 4: facts
371
+ try {
372
+ const currentProject = getCurrentProject();
373
+ const factsText = formatFacts(500, currentProject);
374
+ if (factsText) {
375
+ const scan = detectInjection(factsText);
376
+ const text = (scan.detected && scan.score >= 3)
377
+ ? `<trusted-context name="facts">\n[BLOCKED: facts failed injection scan (score ${scan.score})]\n</trusted-context>`
378
+ : `<untrusted-context name="facts" source="user-extracted">\n${factsText}\n</untrusted-context>`;
379
+ allocator.addSection("facts", text, { priority: 5, min: 100, max: 500 });
380
+ }
381
+ } catch {}
382
+
383
+ // Memory Layer 5: user model
384
+ try {
385
+ const userModelText = formatUserModel(0.5);
386
+ if (userModelText) {
387
+ allocator.addSection("user-profile", `<trusted-context name="user-profile">\n${userModelText}\n</trusted-context>`, { priority: 5, min: 50, max: 300 });
388
+ }
389
+ } catch {}
390
+
391
+ // ── DYNAMIC BOUNDARY ──
392
+ allocator.addSection("boundary", "__DYNAMIC_BOUNDARY__", { priority: 1, fixed: true });
393
+
394
+ // ── PER-TURN ZONE ──
395
+
396
+ // The mode. It moves with the intent class, so it lives below the boundary
397
+ // in the per-turn zone. Fixed, because a truncated half of a mode is worse than a longer
398
+ // prompt.
399
+ //
400
+ // No clock here any more. Even below the boundary a line that moves
401
+ // every minute is still ABOVE the tools on a model whose template renders
402
+ // them after the system message, so every new task paid for the tools and
403
+ // the request again: one benchmark run cached 4096-6144 of ~7500 tokens on Flint's
404
+ // first calls against ~99% for a reference agent, and Flint paid 2.3x. system.md tells
405
+ // the agent to ask the system for the date and time when it needs them.
406
+ let perTurn = "";
407
+ if (opts.intentClass) {
408
+ try {
409
+ const mode = getModeForIntent(opts.intentClass);
410
+ if (mode && mode.promptAddition) {
411
+ perTurn = `<trusted-context name="mode">\n${mode.promptAddition}\n</trusted-context>`;
412
+ }
413
+ } catch {}
414
+ }
415
+ if (perTurn) allocator.addSection("turn-context", perTurn, { priority: 1, fixed: true });
416
+
417
+ if (sessionId) {
418
+ const sessionFacts = getSessionFactsSummary(sessionId, 30);
419
+ if (sessionFacts) {
420
+ allocator.addSection("session-facts", `<trusted-context name="session-facts">\nKey facts extracted from this conversation so far:\n${sessionFacts}\n</trusted-context>`, { priority: 6, min: 100, max: 2000 });
421
+ }
422
+ const digest = getDigestForPrompt(sessionId, 25);
423
+ if (digest) {
424
+ allocator.addSection("digest", `<trusted-context name="conversation-digest">\n${digest}\nUse this to recall what the user asked and what you answered in previous turns.\n</trusted-context>`, { priority: 6, min: 200, max: 3000 });
425
+ }
426
+ }
427
+
428
+ if (opts.datasets) {
429
+ const entries = Object.values(opts.datasets);
430
+ if (entries.length > 0) {
431
+ const lines = entries.map((ds) => {
432
+ const totalPages = Math.ceil(ds.rows.length / ds.pageSize);
433
+ return ` ${ds.id} "${ds.label}" ${ds.rows.length} rows, page ${ds.page}/${totalPages} (source: ${ds.source})`;
434
+ });
435
+ allocator.addSection("datasets", `<trusted-context name="datasets">\n${lines.join("\n")}\nUse show_dataset(id, page) to view pages.\n</trusted-context>`, { priority: 7, min: 50, max: 500 });
436
+ }
437
+ }
438
+
439
+ const inboxHint = inboxPromptHint();
440
+ if (inboxHint) {
441
+ allocator.addSection("inbox", `<trusted-context name="notifications">\n${inboxHint}\n</trusted-context>`, { priority: 8, min: 20, max: 200 });
442
+ }
443
+
444
+ const { prompt } = allocator.build();
445
+ return { role: "system", content: prompt };
446
+ }
@@ -0,0 +1,48 @@
1
+ // The local date and time at the start of each message the operator sends.
2
+ //
3
+ // Owner, 2026-10-02: a research session wrote "Compiled 2026-09-30" into
4
+ // four documents on 2026-10-02. The clock is kept out of the system prompt on
5
+ // purpose (a line that moved every minute at the top of the prompt cost the
6
+ // cache everything after it, 2.3x on a benchmark), and system.md asked the
7
+ // agent to run `date` when it needed the time; it took a date from the
8
+ // articles it had read instead.
9
+ //
10
+ // Here the time goes into the new message, at the end of the conversation,
11
+ // which is never cached anyway; everything before it is unchanged. It is
12
+ // written once, when the message is added to the history, so every later call
13
+ // of the turn and every later turn sends the same bytes. Nothing on screen
14
+ // shows it.
15
+
16
+ const STAMP_RE = /^\[Local time: [^\]\n]*\]\n?/;
17
+
18
+ /** "[Local time: Fri 2026-10-02 10:16 CDT, UTC-05:00]" for the given moment. */
19
+ export function messageTimeStamp(date = new Date()) {
20
+ const p = (n) => String(n).padStart(2, "0");
21
+ const day = date.toLocaleDateString("en-US", { weekday: "short" });
22
+ const ymd = `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`;
23
+ const hm = `${p(date.getHours())}:${p(date.getMinutes())}`;
24
+ let zone = "";
25
+ try {
26
+ zone = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" })
27
+ .formatToParts(date).find((x) => x.type === "timeZoneName")?.value || "";
28
+ } catch {}
29
+ const off = -date.getTimezoneOffset();
30
+ const utc = `UTC${off >= 0 ? "+" : "-"}${p(Math.floor(Math.abs(off) / 60))}:${p(Math.abs(off) % 60)}`;
31
+ return `[Local time: ${day} ${ymd} ${hm}${zone ? ` ${zone}` : ""}, ${utc}]`;
32
+ }
33
+
34
+ /**
35
+ * The message content with the stamp in front: a string gets a first line, a
36
+ * list of parts gets a first text part.
37
+ */
38
+ export function withTimeStamp(content, date = new Date()) {
39
+ const stamp = messageTimeStamp(date);
40
+ if (typeof content === "string") return `${stamp}\n${content}`;
41
+ if (Array.isArray(content)) return [{ type: "text", text: stamp }, ...content];
42
+ return content;
43
+ }
44
+
45
+ /** The text without a leading stamp, for showing a message back to a person. */
46
+ export function stripTimeStamp(text) {
47
+ return typeof text === "string" ? text.replace(STAMP_RE, "") : text;
48
+ }