dsh-memu-memory 1.1.2 → 1.2.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 (199) hide show
  1. package/AGENTS.md +8 -6
  2. package/README.md +50 -5
  3. package/README.zh.md +49 -5
  4. package/docs/active-memory-ab.md +385 -0
  5. package/docs/binding-lifecycle.md +99 -0
  6. package/docs/contributing.md +6 -4
  7. package/docs/emotion-model-spec.md +281 -0
  8. package/docs/env-and-commands.md +13 -0
  9. package/docs/host-api.md +35 -3
  10. package/docs/memory-semantics.md +40 -11
  11. package/docs/recall-benchmark.md +238 -0
  12. package/docs/troubleshooting.md +395 -366
  13. package/lib/active-memory.js +165 -0
  14. package/lib/activity-log.js +190 -0
  15. package/lib/backup.js +1103 -0
  16. package/lib/binding-diagnostics.js +635 -0
  17. package/lib/client.js +728 -58
  18. package/lib/consolidation-ledger.js +347 -0
  19. package/lib/consolidation-receipt.js +284 -0
  20. package/lib/consolidation-snapshot.js +274 -0
  21. package/lib/consolidation-worker.js +115 -0
  22. package/lib/curator.js +81 -37
  23. package/lib/cycle.js +43 -6
  24. package/lib/embedding-catalog.js +763 -0
  25. package/lib/embedding-download.js +407 -0
  26. package/lib/embedding-runtime.js +710 -0
  27. package/lib/engine.js +2 -2
  28. package/lib/index.js +36 -0
  29. package/lib/inject.js +232 -103
  30. package/lib/local-embedding-entry.js +145 -0
  31. package/lib/local-embedding-manager.js +267 -0
  32. package/lib/local-embedding-protocol.js +173 -0
  33. package/lib/local-embedding-worker.js +652 -0
  34. package/lib/local-vector-service.js +283 -0
  35. package/lib/memory.js +599 -57
  36. package/lib/memu-config.js +10 -22
  37. package/lib/memu.js +53 -19
  38. package/lib/mood.js +268 -39
  39. package/lib/paths.js +24 -0
  40. package/lib/preset-scope.js +1 -1
  41. package/lib/private.js +450 -29
  42. package/lib/prompts/private-consolidation.coding.md +1 -0
  43. package/lib/prompts/private-consolidation.md +1 -0
  44. package/lib/prompts/private-consolidation.mixed.md +1 -0
  45. package/lib/prompts/private-consolidation.rp.md +1 -0
  46. package/lib/prompts.js +4 -4
  47. package/lib/remote-summary.js +51 -25
  48. package/lib/rpc.js +54 -5
  49. package/lib/state.js +137 -172
  50. package/lib/tools.js +58 -7
  51. package/lib/vector-migration.js +878 -0
  52. package/lib/vector-profile.js +968 -0
  53. package/package.json +4 -2
  54. package/scripts/acceptance-local.mjs +7 -5
  55. package/scripts/active-memory-ab-live.mjs +655 -0
  56. package/scripts/active-memory-ab.mjs +1302 -0
  57. package/scripts/chain-matrix.mjs +537 -0
  58. package/scripts/check-all.mjs +29 -1
  59. package/scripts/check-doc-drift.mjs +353 -0
  60. package/scripts/diagnose-bindings.mjs +176 -0
  61. package/scripts/local-runtime-install.mjs +142 -0
  62. package/scripts/local-runtime-verify.mjs +56 -0
  63. package/scripts/local-vector-verify.mjs +93 -0
  64. package/scripts/recall-bench.mjs +813 -0
  65. package/scripts/repetition-ab-scenarios.mjs +93 -0
  66. package/scripts/repetition-detector-snapshot.mjs +314 -0
  67. package/scripts/repetition-eval-live.mjs +360 -0
  68. package/scripts/repetition-eval.mjs +702 -0
  69. package/tests/activation.test.mjs +89 -0
  70. package/tests/active-memory-ab.test.mjs +745 -0
  71. package/tests/active-memory.test.mjs +129 -0
  72. package/tests/activity-log.test.mjs +126 -0
  73. package/tests/activity-wiring.test.mjs +103 -0
  74. package/tests/appendix-b.test.mjs +212 -0
  75. package/tests/appendix-b2.test.mjs +166 -0
  76. package/tests/audit-regression.test.mjs +272 -0
  77. package/tests/audit-safety.test.mjs +227 -0
  78. package/tests/backend-compensation.test.mjs +272 -0
  79. package/tests/backup.test.mjs +793 -0
  80. package/tests/binding-diagnostics.test.mjs +720 -0
  81. package/tests/browser.test.mjs +366 -0
  82. package/tests/client-render.test.mjs +892 -0
  83. package/tests/coalesce.test.mjs +113 -0
  84. package/tests/config.test.mjs +159 -0
  85. package/tests/consolidation-credential.test.mjs +320 -0
  86. package/tests/consolidation-fork.test.mjs +357 -0
  87. package/tests/consolidation-ledger.test.mjs +298 -0
  88. package/tests/consolidation-snapshot.test.mjs +321 -0
  89. package/tests/consolidation-value.test.mjs +89 -0
  90. package/tests/context-scope.test.mjs +40 -0
  91. package/tests/cue.test.mjs +44 -0
  92. package/tests/curator-probe.test.mjs +433 -0
  93. package/tests/curator.test.mjs +150 -0
  94. package/tests/cycle.test.mjs +411 -0
  95. package/tests/disclosure.test.mjs +231 -0
  96. package/tests/doc-drift.test.mjs +222 -0
  97. package/tests/embedding-catalog.test.mjs +266 -0
  98. package/tests/embedding-download.test.mjs +391 -0
  99. package/tests/embedding-runtime.test.mjs +389 -0
  100. package/tests/emotion-chain.test.mjs +84 -0
  101. package/tests/emotion-contract.test.mjs +445 -0
  102. package/tests/engine.test.mjs +35 -0
  103. package/tests/fixtures/active-memory-ab/README.md +56 -0
  104. package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
  105. package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
  106. package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
  107. package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
  108. package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
  109. package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
  110. package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
  111. package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
  112. package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
  113. package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
  114. package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
  115. package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
  116. package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
  117. package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
  118. package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
  119. package/tests/fixtures/active-memory-ab/manifest.json +199 -0
  120. package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
  121. package/tests/fixtures/recall-bench/02-exact.json +153 -0
  122. package/tests/fixtures/recall-bench/03-negation.json +173 -0
  123. package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
  124. package/tests/fixtures/recall-bench/05-conflict.json +194 -0
  125. package/tests/fixtures/recall-bench/06-boundary.json +273 -0
  126. package/tests/fixtures/recall-bench/07-secret.json +191 -0
  127. package/tests/fixtures/recall-bench/08-coding.json +164 -0
  128. package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
  129. package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
  130. package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
  131. package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
  132. package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
  133. package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
  134. package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
  135. package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
  136. package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
  137. package/tests/fixtures/repetition-eval/02-en.json +97 -0
  138. package/tests/fixtures/repetition-eval/03-code.json +91 -0
  139. package/tests/fixtures/repetition-eval/04-names.json +116 -0
  140. package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
  141. package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
  142. package/tests/graph-view.test.mjs +187 -0
  143. package/tests/graph.test.mjs +77 -0
  144. package/tests/hook-order.test.mjs +72 -0
  145. package/tests/humanlike.test.mjs +232 -0
  146. package/tests/hybrid-recall.test.mjs +92 -0
  147. package/tests/inbound.test.mjs +393 -0
  148. package/tests/inject-guide.test.mjs +364 -0
  149. package/tests/inject.test.mjs +222 -0
  150. package/tests/injection-budget-settings.test.mjs +35 -0
  151. package/tests/injection-budget.test.mjs +64 -0
  152. package/tests/injection-chain.test.mjs +191 -0
  153. package/tests/loader-register.mjs +3 -0
  154. package/tests/loader.mjs +12 -0
  155. package/tests/local-embedding-entry.test.mjs +95 -0
  156. package/tests/local-embedding-manager.test.mjs +476 -0
  157. package/tests/local-embedding-protocol.test.mjs +95 -0
  158. package/tests/local-embedding-worker.test.mjs +346 -0
  159. package/tests/managed-memory.test.mjs +536 -0
  160. package/tests/managed-runtime.test.mjs +40 -0
  161. package/tests/media.test.mjs +132 -0
  162. package/tests/migration-payload.test.mjs +42 -0
  163. package/tests/mood.test.mjs +109 -0
  164. package/tests/multiprocess.test.mjs +29 -0
  165. package/tests/new-session-gate.test.mjs +361 -0
  166. package/tests/package-shipping.test.mjs +84 -0
  167. package/tests/preset-scope.test.mjs +212 -0
  168. package/tests/prompts.test.mjs +250 -0
  169. package/tests/react-snapshot.test.mjs +23 -0
  170. package/tests/recall-bench.test.mjs +306 -0
  171. package/tests/remote.test.mjs +140 -0
  172. package/tests/rename.test.mjs +30 -0
  173. package/tests/repetition-detector-snapshot.test.mjs +231 -0
  174. package/tests/repetition-eval-live.test.mjs +177 -0
  175. package/tests/repetition-eval.test.mjs +373 -0
  176. package/tests/salient-date.test.mjs +142 -0
  177. package/tests/settings-beat.test.mjs +221 -0
  178. package/tests/short-term.test.mjs +166 -0
  179. package/tests/sidebar-beat.test.mjs +208 -0
  180. package/tests/sidebar-refresh.test.mjs +123 -0
  181. package/tests/sidebar.test.mjs +70 -0
  182. package/tests/snippet.test.mjs +47 -0
  183. package/tests/source-kind.test.mjs +52 -0
  184. package/tests/state.test.mjs +150 -0
  185. package/tests/stats.test.mjs +112 -0
  186. package/tests/storage.test.mjs +125 -0
  187. package/tests/stubs/dsh-llm.js +5 -0
  188. package/tests/stubs/dsh-session.js +7 -0
  189. package/tests/stubs/dsh-tools.js +101 -0
  190. package/tests/tool-schema.test.mjs +73 -0
  191. package/tests/tools.test.mjs +184 -0
  192. package/tests/ui-defaults.test.mjs +57 -0
  193. package/tests/update-fields.test.mjs +138 -0
  194. package/tests/vector-migration.test.mjs +733 -0
  195. package/tests/vector-profile.test.mjs +425 -0
  196. package/tests/visibility-migration.test.mjs +152 -0
  197. package/tests/webui-auth.test.mjs +454 -0
  198. package/tests/webui.test.mjs +164 -0
  199. package/tests/windows-env.test.mjs +89 -0
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Live repetition pilot (W8-B evaluation only, not production behaviour).
4
+ *
5
+ * This script asks a real chat model to continue synthetic repetition
6
+ * fixtures twice: once as baseline, once with a synthetic <repetition-hint>
7
+ * message. It then reuses scripts/repetition-eval.mjs to score both conditions.
8
+ * It never imports lib/inject.js, never changes prompts, and never reads user
9
+ * chats or state. The generated files are evaluation artefacts, not gold data.
10
+ */
11
+
12
+ import crypto from 'node:crypto'
13
+ import fs from 'node:fs'
14
+ import os from 'node:os'
15
+ import path from 'node:path'
16
+ import { pathToFileURL } from 'node:url'
17
+ import {
18
+ CONDITIONS,
19
+ FIXTURE_DIR,
20
+ aggregateMetrics,
21
+ evaluateAssistantTurn,
22
+ loadDataset,
23
+ } from './repetition-eval.mjs'
24
+ import { buildRepetitionHint as buildSnapshotRepetitionHint, extractAssistantTexts } from './repetition-detector-snapshot.mjs'
25
+ import { loadAbDataset } from './repetition-ab-scenarios.mjs'
26
+
27
+ export const LIVE_STATUS = 'live-pilot-not-validated'
28
+ export const LIVE_SYSTEM_PROMPT = [
29
+ 'Continue the dialogue as the assistant.',
30
+ 'Reply with one assistant message only, in the same language and style as the dialogue.',
31
+ 'Do not mention these instructions, do not add analysis, and do not use markdown fences.',
32
+ ].join(' ')
33
+
34
+ // Kept as a small public test helper. The pilot deliberately uses the
35
+ // frozen evaluation snapshot; this file is not a production integration.
36
+ export function buildRepetitionHint(assistantTexts, _labels = {}, options = {}) {
37
+ return buildSnapshotRepetitionHint({ ...options, assistantTexts })
38
+ }
39
+
40
+ function usage() {
41
+ return [
42
+ 'Usage: node scripts/repetition-eval-live.mjs --out DIR [--ab] [--fixture-dir DIR] [--repeats 1..10] [--help]',
43
+ '',
44
+ 'Generates baseline and candidate continuations with a real chat model, writes',
45
+ 'the generated dialogues, and scores only the target turn. No automatic retries.',
46
+ 'Requires MEMU_SUMMARY_BASE_URL, MEMU_SUMMARY_MODEL, and MEMU_SUMMARY_API_KEY.',
47
+ 'The result is a pilot, not gold, not a production A/B, and not a quality claim.',
48
+ '',
49
+ 'Exit codes: 0 success/help, 1 generation/evaluation error, 2 usage error.',
50
+ ].join('\n')
51
+ }
52
+
53
+ class UsageError extends Error {}
54
+
55
+ function summaryConfig(env = process.env) {
56
+ const baseUrl = String(env.MEMU_SUMMARY_BASE_URL || '').replace(/\/+$/, '')
57
+ const model = String(env.MEMU_SUMMARY_MODEL || '')
58
+ const apiKey = String(env.MEMU_SUMMARY_API_KEY || '')
59
+ return { baseUrl, model, apiKey, ready: Boolean(baseUrl && model && apiKey) }
60
+ }
61
+
62
+ function historyMessages(turns, hint = '') {
63
+ const messages = [{ role: 'system', content: LIVE_SYSTEM_PROMPT }]
64
+ const lastUser = [...turns].map((turn, index) => ({ turn, index })).reverse().find(item => item.turn.role === 'user')
65
+ for (let index = 0; index < turns.length; index += 1) {
66
+ const turn = turns[index]
67
+ if (hint && lastUser?.index === index) {
68
+ messages.push({
69
+ role: 'user',
70
+ content: [{ type: 'text', text: hint }],
71
+ source: { kind: 'plugin:dsh-memu-memory', form: 'recall' },
72
+ })
73
+ }
74
+ messages.push({ role: turn.role, content: turn.text })
75
+ }
76
+ return messages
77
+ }
78
+
79
+ async function requestChat({ messages, config, fetchImpl, timeoutMs, maxTokens }) {
80
+ const response = await fetchImpl(config.baseUrl + '/chat/completions', {
81
+ method: 'POST',
82
+ headers: { 'content-type': 'application/json', authorization: 'Bearer ' + config.apiKey },
83
+ body: JSON.stringify({
84
+ model: config.model,
85
+ temperature: 0.6,
86
+ max_tokens: maxTokens,
87
+ messages: messages.map(({ role, content }) => ({
88
+ role,
89
+ content: Array.isArray(content)
90
+ ? content.map(block => typeof block?.text === 'string' ? block.text : '').join('')
91
+ : content,
92
+ })),
93
+ }),
94
+ signal: AbortSignal.timeout(timeoutMs),
95
+ })
96
+ if (!response.ok) throw new Error('live repetition pilot: provider returned HTTP ' + response.status)
97
+ return response.json()
98
+ }
99
+
100
+ async function defaultChat({ messages, config, fetchImpl = globalThis.fetch, timeoutMs = 60_000 }) {
101
+ if (!config.ready) throw new Error('live repetition pilot: summary endpoint is not configured')
102
+ const payload = await requestChat({ messages, config, fetchImpl, timeoutMs, maxTokens: 4000 })
103
+ const choice = payload && payload.choices && payload.choices[0]
104
+ const content = choice && choice.message && choice.message.content
105
+ if (typeof content !== 'string' || content.trim().length === 0) {
106
+ const reason = choice && choice.finish_reason ? choice.finish_reason : 'unknown'
107
+ const reasoning = choice && choice.message && typeof choice.message.reasoning_content === 'string' ? choice.message.reasoning_content.length : 0
108
+ throw new Error('live repetition pilot: provider returned an empty assistant message (finish_reason=' + reason + ', reasoning_chars=' + reasoning + ')')
109
+ }
110
+ return { text: content.trim(), usage: payload.usage || null, finishReason: choice.finish_reason || null, responseModel: payload.model || null }
111
+ }
112
+
113
+ export function targetAssistantIndex(fixture) {
114
+ for (let index = fixture.turns.length - 1; index >= 0; index -= 1) {
115
+ if (fixture.turns[index].role === 'assistant') return index
116
+ }
117
+ throw new Error('live repetition pilot: fixture has no assistant turn')
118
+ }
119
+
120
+ export async function generateSample({ fixture, condition, targetIndex = targetAssistantIndex(fixture), chat, config, fetchImpl, timeoutMs }) {
121
+ if (!CONDITIONS.includes(condition)) throw new Error('unknown repetition condition: ' + condition)
122
+ const target = fixture.turns[targetIndex]
123
+ if (!target || target.role !== 'assistant') throw new Error('live repetition pilot: targetIndex must point at an assistant turn')
124
+ const prefix = fixture.turns.slice(0, targetIndex)
125
+ const prefixMessages = prefix.map(turn => ({ role: turn.role, content: turn.text }))
126
+ const priorAssistantTexts = extractAssistantTexts(prefixMessages)
127
+ const hint = condition === 'candidate'
128
+ ? buildSnapshotRepetitionHint({ assistantTexts: priorAssistantTexts })
129
+ : ''
130
+ const messages = historyMessages(prefix, hint)
131
+ const generated = await chat({ messages, config, fetchImpl, timeoutMs })
132
+ const text = typeof generated === 'string' ? generated : generated.text
133
+ if (typeof text !== 'string' || !text.trim()) throw new Error('live repetition pilot: empty output')
134
+ const targetLabels = {
135
+ necessaryPhrases: (fixture.labels?.necessaryPhrases || []).filter(label => label.turn === targetIndex),
136
+ facts: (fixture.labels?.facts || []).filter(fact => fact.turn === targetIndex),
137
+ }
138
+ const evaluation = evaluateAssistantTurn({
139
+ id: fixture.id + '-live-' + condition,
140
+ category: fixture.category,
141
+ condition,
142
+ text,
143
+ priorAssistantTexts,
144
+ labels: targetLabels,
145
+ window: fixture.window || 4,
146
+ })
147
+ return {
148
+ schemaVersion: 1,
149
+ kind: 'repetition-eval-live-sample',
150
+ draft: true,
151
+ notGold: true,
152
+ live: true,
153
+ id: fixture.id + '-live-' + condition,
154
+ pairId: fixture.pairId,
155
+ category: fixture.category,
156
+ condition,
157
+ targetIndex,
158
+ prefixTurns: prefix,
159
+ generatedText: text,
160
+ hint,
161
+ hintApplied: Boolean(hint),
162
+ labels: targetLabels,
163
+ usage: typeof generated === 'string' ? null : generated.usage,
164
+ finishReason: typeof generated === 'string' ? null : generated.finishReason,
165
+ responseModel: typeof generated === 'string' ? null : generated.responseModel,
166
+ evaluation,
167
+ }
168
+ }
169
+
170
+ function sha256(text) {
171
+ return crypto.createHash('sha256').update(text).digest('hex')
172
+ }
173
+
174
+ function group(evaluations, key) {
175
+ const out = {}
176
+ for (const evaluation of evaluations) {
177
+ const value = evaluation[key]
178
+ if (!out[value]) out[value] = []
179
+ out[value].push(evaluation)
180
+ }
181
+ return Object.fromEntries(Object.entries(out).map(([value, list]) => [value, aggregateMetrics(list)]))
182
+ }
183
+
184
+ export async function runLivePilot(options = {}) {
185
+ const source = options.ab ? loadAbDataset() : loadDataset(options.fixtureDir || FIXTURE_DIR)
186
+ const config = options.config || summaryConfig()
187
+ if (!config.ready) throw new Error('live repetition pilot: MEMU_SUMMARY_BASE_URL/MODEL/API_KEY are required')
188
+ const chat = options.chat || defaultChat
189
+ const outDir = path.resolve(options.outDir || path.join(os.tmpdir(), 'dsh-memu-repetition-live'))
190
+ fs.mkdirSync(outDir, { recursive: true })
191
+ const samples = []
192
+ const files = []
193
+ const repeats = options.repeats ?? 1
194
+ if (!Number.isInteger(repeats) || repeats < 1 || repeats > 10) throw new UsageError('repeats must be an integer from 1 to 10')
195
+ const tasks = []
196
+ for (let repeat = 1; repeat <= repeats; repeat += 1) {
197
+ for (const fixture of source.dialogues) tasks.push({ fixture, repeat })
198
+ }
199
+ let seed = 20261029
200
+ const random = () => { seed = (1664525 * seed + 1013904223) >>> 0; return seed / 4294967296 }
201
+ for (let i = tasks.length - 1; i > 0; i -= 1) {
202
+ const j = Math.floor(random() * (i + 1))
203
+ ;[tasks[i], tasks[j]] = [tasks[j], tasks[i]]
204
+ }
205
+ const failures = []
206
+ const callLimit = 120
207
+ let attempts = 0
208
+ const planned = tasks.length * 2
209
+ if (planned > callLimit) throw new UsageError('planned requests exceed the 120-attempt limit')
210
+ const jobs = tasks.flatMap(({ fixture, repeat }) => {
211
+ const conditions = random() < 0.5 ? ['baseline', 'candidate'] : ['candidate', 'baseline']
212
+ return conditions.map(condition => ({ fixture, repeat, condition }))
213
+ })
214
+ let nextJob = 0
215
+ let consecutiveFailures = 0
216
+ let stoppedEarly = false
217
+ const work = async () => {
218
+ while (nextJob < jobs.length && !stoppedEarly) {
219
+ const { fixture, repeat, condition } = jobs[nextJob++]
220
+ if (attempts >= callLimit) throw new Error('live repetition pilot: 120-attempt limit reached')
221
+ attempts += 1
222
+ const start = Date.now()
223
+ let sample
224
+ try {
225
+ sample = await generateSample({ fixture, condition, chat, config, fetchImpl: options.fetchImpl, timeoutMs: options.timeoutMs })
226
+ consecutiveFailures = 0
227
+ } catch {
228
+ sample = { id: fixture.id + '-live-' + condition, pairId: fixture.pairId, condition, category: fixture.category,
229
+ status: 'failed', error: 'generation failed; provider details omitted', evaluation: null }
230
+ failures.push({ id: sample.id, repeat, condition })
231
+ consecutiveFailures += 1
232
+ if (consecutiveFailures >= 4) stoppedEarly = true
233
+ }
234
+ sample.repeat = repeat
235
+ sample.durationMs = Date.now() - start
236
+ const fileName = `${repeat}-${String(sample.id).replace(/[^a-z0-9-]/g, '-')}.json`
237
+ const text = JSON.stringify(sample, null, 2).replace(/\r?\n/g, '\r\n')
238
+ fs.writeFileSync(path.join(outDir, fileName), text, 'utf8')
239
+ files.push({ name: fileName, sha256: sha256(text) })
240
+ samples.push(sample)
241
+ options.onProgress?.({ attempts, completed: samples.length, planned, sample: sample.id, repeat, ok: Boolean(sample.evaluation) })
242
+ }
243
+ }
244
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(4, Number(options.concurrency) || 1)) }, work))
245
+ samples.sort((a, b) => a.repeat - b.repeat || a.pairId.localeCompare(b.pairId) || a.condition.localeCompare(b.condition))
246
+ const evaluations = samples.map(sample => sample.evaluation).filter(Boolean)
247
+ const overall = aggregateMetrics(evaluations)
248
+ const report = {
249
+ schemaVersion: 1,
250
+ kind: 'repetition-eval-live',
251
+ ok: failures.length === 0,
252
+ status: LIVE_STATUS,
253
+ draft: true,
254
+ notGold: true,
255
+ live: true,
256
+ dataset: {
257
+ name: 'repetition-eval-live-pilot',
258
+ source: source.name,
259
+ sourceSha256: source.sha256,
260
+ sourceFiles: source.files,
261
+ dialogueCount: samples.length,
262
+ fileCount: files.length,
263
+ repeats,
264
+ files,
265
+ },
266
+ config: {
267
+ model: config.model,
268
+ baseUrlHost: (() => { try { return new URL(config.baseUrl).host } catch { return '' } })(),
269
+ temperature: 0.6,
270
+ maxTokens: 4000,
271
+ automaticRetries: 0,
272
+ seed: 20261029,
273
+ attempts,
274
+ callLimit,
275
+ stoppedEarly,
276
+ window: 4,
277
+ target: 'last assistant turn; original prefix supplied as context',
278
+ systemPrompt: LIVE_SYSTEM_PROMPT,
279
+ candidateHint: 'frozen evaluation snapshot <repetition-hint> as a synthetic plugin message before the last user turn',
280
+ },
281
+ metrics: {
282
+ ...overall,
283
+ byCategory: group(evaluations, 'category'),
284
+ byCondition: group(evaluations, 'condition'),
285
+ humanPreference: {
286
+ status: 'not-collected',
287
+ preferenceRate: null,
288
+ denominator: 0,
289
+ emptySet: true,
290
+ note: 'Generated output is not a human preference rating.',
291
+ },
292
+ },
293
+ dialogues: evaluations,
294
+ failures,
295
+ caveats: [
296
+ 'Live pilot from synthetic fixtures; not gold, not a production A/B, and not a quality claim.',
297
+ 'The candidate condition uses a frozen evaluation snapshot and synthetic plugin-message placement; this is still an offline pilot.',
298
+ 'Model output is nondeterministic; one run cannot establish statistical significance.',
299
+ 'Only the final assistant turn is generated; earlier user and assistant turns are fixture context.',
300
+ 'Only target-turn labels are scored; exact-match failure can reflect paraphrase or omission, not only forgetting.',
301
+ 'No user chat, user state, or production prompt file was read or modified.',
302
+ ],
303
+ }
304
+ fs.writeFileSync(path.join(outDir, 'report.json'), JSON.stringify(report, null, 2).replace(/\r?\n/g, '\r\n'), 'utf8')
305
+ return report
306
+ }
307
+
308
+ function parseArgs(argv) {
309
+ const options = {}
310
+ for (let index = 0; index < argv.length; index += 1) {
311
+ const arg = argv[index]
312
+ if (arg === '--out') {
313
+ const value = argv[++index]
314
+ if (!value || value.startsWith('--')) throw new UsageError('--out requires a directory')
315
+ options.outDir = value
316
+ } else if (arg === '--fixture-dir') {
317
+ const value = argv[++index]
318
+ if (!value || value.startsWith('--')) throw new UsageError('--fixture-dir requires a directory')
319
+ options.fixtureDir = value
320
+ } else if (arg === '--ab') {
321
+ options.ab = true
322
+ } else if (arg === '--repeats') {
323
+ const value = argv[++index]
324
+ if (!value || value.startsWith('--')) throw new UsageError('--repeats requires a number from 1 to 10')
325
+ options.repeats = Number(value)
326
+ if (!Number.isInteger(options.repeats) || options.repeats < 1 || options.repeats > 10) throw new UsageError('--repeats must be an integer from 1 to 10')
327
+ } else if (arg === '--help' || arg === '-h') {
328
+ options.help = true
329
+ } else {
330
+ throw new UsageError('unknown argument ' + arg)
331
+ }
332
+ }
333
+ return options
334
+ }
335
+
336
+ async function main() {
337
+ const options = parseArgs(process.argv.slice(2))
338
+ if (options.help) {
339
+ console.log(usage())
340
+ return
341
+ }
342
+ if (!options.outDir) throw new UsageError('--out is required')
343
+ const report = await runLivePilot(options)
344
+ console.log(JSON.stringify({
345
+ ok: report.ok,
346
+ status: report.status,
347
+ outDir: path.resolve(options.outDir),
348
+ metrics: report.metrics,
349
+ failures: report.failures,
350
+ }, null, 2))
351
+ if (!report.ok) process.exitCode = 1
352
+ }
353
+
354
+ const invoked = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url : false
355
+ if (invoked) {
356
+ main().catch(error => {
357
+ console.error(JSON.stringify({ ok: false, kind: 'repetition-eval-live', error: error && error.message ? error.message : String(error) }, null, 2))
358
+ process.exitCode = error instanceof UsageError ? 2 : 1
359
+ })
360
+ }