nemoris 0.1.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 (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. package/vendor/smol-toml-1.5.2.tgz +0 -0
@@ -0,0 +1,108 @@
1
+ function isProviderBackedRun(run) {
2
+ if (!run) return false;
3
+ return run.mode === "provider" || (run.mode === "live" && Boolean(run.providerId));
4
+ }
5
+
6
+ function isHealthyProviderRun(run) {
7
+ if (!isProviderBackedRun(run)) return false;
8
+ if (run.deliveryUncertain || run.fallbackAttempted) return false;
9
+
10
+ const yielded = run.yielded === true;
11
+ const followUpHealthy = !yielded || run.followUpConsumed === true;
12
+ const handoffRequired = run.interaction?.handoff?.required === true;
13
+ const handoffHealthy = !handoffRequired || run.handoffDelivered === true;
14
+ const deliveryEvidenceRequired = run.deliveryEvidenceRequired === true;
15
+ const deliveryEvidenceHealthy = !deliveryEvidenceRequired || run.deliveryEvidenceHealthy === true;
16
+ const completionHealthy = run.interaction?.completion?.required !== true || run.completionQueued === true;
17
+ const ackHealthy = run.interaction?.ack?.required !== true || run.ackQueued === true;
18
+
19
+ return followUpHealthy && handoffHealthy && deliveryEvidenceHealthy && completionHealthy && ackHealthy;
20
+ }
21
+
22
+ function countRecentHealthyProviders(runs) {
23
+ return runs.filter((run) => isHealthyProviderRun(run)).length;
24
+ }
25
+
26
+ function buildRecommendation({ readinessTier, blockers, healthyProviderRuns }) {
27
+ if (readinessTier === "ready_for_pilot" && healthyProviderRuns >= 2) {
28
+ return "candidate_for_daily_driver";
29
+ }
30
+ if (readinessTier === "ready_for_pilot" || readinessTier === "close") {
31
+ return "continue_provider_pilot";
32
+ }
33
+ if (blockers.includes("provider_unreachable")) {
34
+ return "fix_provider_first";
35
+ }
36
+ if (blockers.includes("retrieval_is_degraded_or_falling_back")) {
37
+ return "fix_retrieval_health_first";
38
+ }
39
+ if (blockers.includes("output_contract_not_consistently_satisfied")) {
40
+ return "improve_report_quality_first";
41
+ }
42
+ if (blockers.includes("interaction_lifecycle_has_blocking_issues")) {
43
+ return "fix_interaction_contract_first";
44
+ }
45
+ return "not_ready_for_pilot";
46
+ }
47
+
48
+ export function buildPilotStatus({ jobId, readiness, evaluation, runs = [], dependencyHealth = null, reportFallback = null } = {}) {
49
+ const laneRuns = runs.filter((run) => run.jobId === jobId);
50
+ const providerRuns = laneRuns.filter((run) => isProviderBackedRun(run));
51
+ const latestRun = laneRuns[0] || null;
52
+ const latestProviderRun = providerRuns[0] || null;
53
+ const healthyProviderRuns = countRecentHealthyProviders(providerRuns);
54
+ const readinessTier = readiness?.readinessTier || "not_ready";
55
+ const blockers = readiness?.blockers || [];
56
+ const recommendation = buildRecommendation({
57
+ readinessTier,
58
+ blockers,
59
+ healthyProviderRuns
60
+ });
61
+
62
+ return {
63
+ jobId,
64
+ readinessTier,
65
+ recommendation,
66
+ blockers,
67
+ providerRunsObserved: providerRuns.length,
68
+ healthyProviderRuns,
69
+ latestRun: latestRun
70
+ ? {
71
+ filePath: latestRun.filePath,
72
+ timestamp: latestRun.timestamp,
73
+ mode: latestRun.mode,
74
+ providerId: latestRun.providerId,
75
+ modelId: latestRun.modelId,
76
+ followUpConsumed: latestRun.followUpConsumed,
77
+ handoffDelivered: latestRun.handoffDelivered,
78
+ deliveryEvidenceHealthy: latestRun.deliveryEvidenceHealthy,
79
+ deliveryUncertain: latestRun.deliveryUncertain,
80
+ fallbackAttempted: latestRun.fallbackAttempted
81
+ }
82
+ : null,
83
+ latestProviderRun: latestProviderRun
84
+ ? {
85
+ filePath: latestProviderRun.filePath,
86
+ timestamp: latestProviderRun.timestamp,
87
+ providerId: latestProviderRun.providerId,
88
+ modelId: latestProviderRun.modelId,
89
+ followUpConsumed: latestProviderRun.followUpConsumed,
90
+ handoffDelivered: latestProviderRun.handoffDelivered,
91
+ deliveryEvidenceHealthy: latestProviderRun.deliveryEvidenceHealthy,
92
+ deliveryUncertain: latestProviderRun.deliveryUncertain,
93
+ fallbackAttempted: latestProviderRun.fallbackAttempted
94
+ }
95
+ : null,
96
+ evaluation: evaluation
97
+ ? {
98
+ filePath: evaluation.filePath,
99
+ overallScore: evaluation.rubric?.overallScore ?? null,
100
+ contractSatisfiedRatio: evaluation.contractCheck?.satisfiedRatio ?? null,
101
+ interaction: evaluation.interaction || null,
102
+ retrieval: evaluation.retrieval || null
103
+ }
104
+ : null,
105
+ dependencyHealth,
106
+ reportFallback
107
+ };
108
+ }
@@ -0,0 +1,261 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { canonicalJson } from "../utils/canonical-json.js";
3
+
4
+ /**
5
+ * PromptBuilder — deterministic, token-budgeted context assembler (Phase 3).
6
+ */
7
+
8
+ export function reduceEvent(prevState, event) {
9
+ const state = prevState ? { ...prevState } : {
10
+ goal: null,
11
+ last_actions: [],
12
+ failures: [],
13
+ open_questions: []
14
+ };
15
+
16
+ const payload = typeof event.payload_json === "string"
17
+ ? JSON.parse(event.payload_json)
18
+ : event.payload_json;
19
+
20
+ switch (event.kind) {
21
+ case "message_in":
22
+ if (!state.goal && payload.text) {
23
+ state.goal = payload.text.slice(0, 120);
24
+ }
25
+ state.last_actions.push({
26
+ ts: event.ts,
27
+ text: `User: ${payload.text || ""}`,
28
+ event_id: event.id
29
+ });
30
+ break;
31
+
32
+ case "message_out":
33
+ state.last_actions.push({
34
+ ts: event.ts,
35
+ text: `Assistant: ${payload.text || ""}`,
36
+ event_id: event.id
37
+ });
38
+ break;
39
+
40
+ case "tool_call":
41
+ state.last_actions.push({
42
+ ts: event.ts,
43
+ text: `Tool call: ${payload.name || payload.tool_name || "unknown"}`,
44
+ event_id: event.id
45
+ });
46
+ break;
47
+
48
+ case "tool_result": {
49
+ const toolName = payload.name || payload.tool_name || "unknown";
50
+ const ok = payload.ok !== false;
51
+ if (!ok) {
52
+ state.failures.push({
53
+ ts: event.ts,
54
+ summary: `Tool failed: ${toolName}`,
55
+ event_id: event.id
56
+ });
57
+ }
58
+ state.last_actions.push({
59
+ ts: event.ts,
60
+ text: `Tool result: ${toolName} ${ok ? "ok" : "failed"}`,
61
+ event_id: event.id
62
+ });
63
+ break;
64
+ }
65
+
66
+ case "state_patch":
67
+ if (payload.patch) {
68
+ Object.assign(state, payload.patch);
69
+ }
70
+ break;
71
+ }
72
+
73
+ // Cap arrays
74
+ if (state.last_actions.length > 20) state.last_actions = state.last_actions.slice(-20);
75
+ if (state.failures.length > 10) state.failures = state.failures.slice(-10);
76
+ if (state.open_questions.length > 5) state.open_questions = state.open_questions.slice(-5);
77
+
78
+ return state;
79
+ }
80
+
81
+ export class PromptBuilder {
82
+ constructor({ contextLedger } = {}) {
83
+ this.contextLedger = contextLedger;
84
+ }
85
+
86
+ buildContextItems(events, workingState) {
87
+ const items = [];
88
+
89
+ // 1. Working state item
90
+ const wsText = [
91
+ `Goal: ${workingState.goal || "None"}`,
92
+ `Recent Actions:\n${workingState.last_actions.slice(-5).map(a => `- ${a.text}`).join("\n")}`,
93
+ workingState.failures.length > 0 ? `Failures:\n${workingState.failures.map(f => `- ${f.summary}`).join("\n")}` : null
94
+ ].filter(Boolean).join("\n\n");
95
+
96
+ items.push({
97
+ kind: "working_state",
98
+ text: wsText,
99
+ estTokens: Math.ceil(wsText.length / 4),
100
+ utilityScore: 1.0
101
+ });
102
+
103
+ // 2. Recent turns (last 10 message_in/message_out)
104
+ const turnEvents = events
105
+ .filter(e => e.kind === "message_in" || e.kind === "message_out")
106
+ .slice(-10);
107
+
108
+ turnEvents.forEach((e, idx) => {
109
+ const payload = typeof e.payload_json === "string" ? JSON.parse(e.payload_json) : e.payload_json;
110
+ const text = payload.text || "";
111
+ items.push({
112
+ kind: "recent_turn",
113
+ id: e.id,
114
+ role: e.kind === "message_in" ? "user" : "assistant",
115
+ text,
116
+ estTokens: Math.ceil(text.length / 4),
117
+ utilityScore: 0.5 + (idx / turnEvents.length) * 0.4, // Newer = higher score
118
+ ts: e.ts
119
+ });
120
+ });
121
+
122
+ // 3. Tool briefs (last 5 tool_result)
123
+ const toolEvents = events
124
+ .filter(e => e.kind === "tool_result")
125
+ .slice(-5);
126
+
127
+ toolEvents.forEach((e, idx) => {
128
+ const payload = typeof e.payload_json === "string" ? JSON.parse(e.payload_json) : e.payload_json;
129
+ let brief = payload.brief || (typeof payload.result === "string" ? payload.result : JSON.stringify(payload.result || payload));
130
+ if (brief.length > 300) brief = brief.slice(0, 300) + "...";
131
+
132
+ items.push({
133
+ kind: "tool_brief",
134
+ id: e.id,
135
+ text: `Tool result (${payload.name || "unknown"}): ${brief}`,
136
+ estTokens: Math.ceil(brief.length / 4),
137
+ utilityScore: 0.3 + (idx / toolEvents.length) * 0.2,
138
+ ts: e.ts
139
+ });
140
+ });
141
+
142
+ return items;
143
+ }
144
+
145
+ async buildPrompt({ sessionId, events, snapshot, systemText, tools, tokenBudgetTotal }) {
146
+ // 1. Reduce working state
147
+ let workingState = snapshot ? JSON.parse(snapshot.state_json) : null;
148
+ for (const event of events) {
149
+ workingState = reduceEvent(workingState, event);
150
+ }
151
+ if (!workingState) workingState = reduceEvent(null, {});
152
+
153
+ // 2. Build context items
154
+ const allItems = this.buildContextItems(events, workingState);
155
+
156
+ // 3. Token allocations
157
+ const systemTokens = Math.ceil(systemText.length / 4);
158
+ const wsItem = allItems.find(i => i.kind === "working_state");
159
+ const wsTokens = wsItem ? wsItem.estTokens : 0;
160
+
161
+ let remainingBudget = tokenBudgetTotal - systemTokens - wsTokens;
162
+ if (remainingBudget < 0) remainingBudget = 0;
163
+
164
+ const recentTurnsBudget = Math.floor(remainingBudget * 0.4);
165
+ const toolBriefsBudget = Math.floor(remainingBudget * 0.2);
166
+
167
+ // 4 & 5. Fill budgets
168
+ const chosenItems = [];
169
+ if (wsItem) chosenItems.push(wsItem);
170
+
171
+ // Fill recent turns (descending recency)
172
+ const recentTurns = allItems.filter(i => i.kind === "recent_turn").sort((a, b) => b.ts - a.ts);
173
+ let turnsUsed = 0;
174
+ for (const turn of recentTurns) {
175
+ if (turnsUsed + turn.estTokens <= recentTurnsBudget) {
176
+ chosenItems.push(turn);
177
+ turnsUsed += turn.estTokens;
178
+ }
179
+ }
180
+
181
+ // Fill tool briefs (descending recency)
182
+ const toolBriefs = allItems.filter(i => i.kind === "tool_brief").sort((a, b) => b.ts - a.ts);
183
+ let toolsUsed = 0;
184
+ for (const brief of toolBriefs) {
185
+ if (toolsUsed + brief.estTokens <= toolBriefsBudget) {
186
+ chosenItems.push(brief);
187
+ toolsUsed += brief.estTokens;
188
+ }
189
+ }
190
+
191
+ // 6. Assemble final messages array
192
+ const messages = [];
193
+
194
+ // System message
195
+ messages.push({
196
+ role: "system",
197
+ content: `${systemText}\n\n${wsItem ? wsItem.text : ""}`.trim()
198
+ });
199
+
200
+ // Recent turns (must be in original order)
201
+ const sortedTurns = chosenItems
202
+ .filter(i => i.kind === "recent_turn")
203
+ .sort((a, b) => a.ts - b.ts);
204
+
205
+ for (const turn of sortedTurns) {
206
+ messages.push({ role: turn.role, content: turn.text });
207
+ }
208
+
209
+ // Tool briefs (append to last user message or as new user message)
210
+ const briefsText = chosenItems
211
+ .filter(i => i.kind === "tool_brief")
212
+ .sort((a, b) => a.ts - b.ts)
213
+ .map(i => i.text)
214
+ .join("\n\n");
215
+
216
+ if (briefsText) {
217
+ const lastUserMsg = [...messages].reverse().find(m => m.role === "user");
218
+ if (lastUserMsg) {
219
+ lastUserMsg.content += `\n\nRecent tool results:\n${briefsText}`;
220
+ } else {
221
+ messages.push({ role: "user", content: `Recent tool results:\n${briefsText}` });
222
+ }
223
+ }
224
+
225
+ // Prefix hash
226
+ const toolsJson = canonicalJson((tools || []).map(t => t.name).sort());
227
+ const prefixSource = systemText + toolsJson;
228
+ const prefixHash = createHash("sha256").update(prefixSource).digest("hex");
229
+
230
+ const totalTokensEst = systemTokens + wsTokens + turnsUsed + toolsUsed;
231
+
232
+ // 7. Log to prompt_builds
233
+ if (this.contextLedger && sessionId) {
234
+ try {
235
+ const buildId = randomUUID();
236
+ const chosenKinds = chosenItems.map(i => i.kind);
237
+ this.contextLedger.db.prepare(`
238
+ insert into prompt_builds (id, session_id, ts, token_budget, chosen_item_ids_json, total_tokens_est, prefix_hash)
239
+ values (?, ?, ?, ?, ?, ?, ?)
240
+ `).run(
241
+ buildId,
242
+ sessionId,
243
+ Date.now(),
244
+ tokenBudgetTotal,
245
+ JSON.stringify(chosenKinds),
246
+ totalTokensEst,
247
+ prefixHash
248
+ );
249
+ } catch (err) {
250
+ // Logging failure must never break a turn
251
+ }
252
+ }
253
+
254
+ return {
255
+ messages,
256
+ workingState,
257
+ tokenEstimate: totalTokensEst,
258
+ prefixHash
259
+ };
260
+ }
261
+ }