mixdog 0.9.45 → 0.9.47
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.
- package/package.json +5 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +23 -10
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +77 -65
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -410,55 +410,6 @@ function capToolOutput(result, options = {}) {
|
|
|
410
410
|
return `${head}\n... [tool-output truncated: ${Math.round(bytes / 1024)} KB -> ${Math.round(cap / 1024)} KB cap (tool_output_token_limit)] ...\n${tail}`;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
const _GREP_SWEEP_NUDGE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_ENABLE_GREP_SWEEP_NUDGE || ''));
|
|
414
|
-
const _GREP_SWEEP_MIN_CALLS = 20;
|
|
415
|
-
const _GREP_SWEEP_MIN_UNIQUE = 16;
|
|
416
|
-
const _grepSweepState = new Map();
|
|
417
|
-
|
|
418
|
-
function _grepSweepTerms(pattern) {
|
|
419
|
-
const raw = Array.isArray(pattern) ? pattern.join('|') : String(pattern || '');
|
|
420
|
-
return [...new Set(raw
|
|
421
|
-
.split(/[|,\s()"'`[\]{}.*+?^$\\:-]+/g)
|
|
422
|
-
.map((s) => s.trim().toLowerCase())
|
|
423
|
-
.filter((s) => s.length >= 4))];
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function _normalizeSweepPath(value) {
|
|
427
|
-
return String(value || '.')
|
|
428
|
-
.replace(/\\/g, '/')
|
|
429
|
-
.replace(/^([A-Za-z]):/, (_, d) => d.toLowerCase() + ':')
|
|
430
|
-
.replace(/\/+/g, '/')
|
|
431
|
-
.replace(/\/$/, '') || '.';
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function _grepSweepKey(args = {}) {
|
|
435
|
-
const path = _normalizeSweepPath(args.path || '.');
|
|
436
|
-
const glob = Array.isArray(args.glob) ? args.glob.join(',') : String(args.glob || '');
|
|
437
|
-
const terms = _grepSweepTerms(args.pattern).slice(0, 3).join('|');
|
|
438
|
-
return `${path}::${glob}::${terms}`;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function _maybeAppendGrepSweepNudge(toolName, args, result, scope) {
|
|
442
|
-
if (!_GREP_SWEEP_NUDGE_ENABLED || toolName !== 'grep' || typeof result !== 'string') return result;
|
|
443
|
-
const key = scope || 'global';
|
|
444
|
-
const now = Date.now();
|
|
445
|
-
let state = _grepSweepState.get(key);
|
|
446
|
-
if (!state || now - state.lastTs > 10 * 60_000) {
|
|
447
|
-
state = { count: 0, unique: new Set(), lastTs: 0, nudged: false };
|
|
448
|
-
_grepSweepState.set(key, state);
|
|
449
|
-
}
|
|
450
|
-
state.count += 1;
|
|
451
|
-
state.unique.add(_grepSweepKey(args));
|
|
452
|
-
state.lastTs = now;
|
|
453
|
-
if (_grepSweepState.size > 256) {
|
|
454
|
-
const oldest = [..._grepSweepState.entries()].sort((a, b) => (a[1].lastTs || 0) - (b[1].lastTs || 0)).slice(0, 32);
|
|
455
|
-
for (const [oldKey] of oldest) _grepSweepState.delete(oldKey);
|
|
456
|
-
}
|
|
457
|
-
if (state.nudged || state.count < _GREP_SWEEP_MIN_CALLS || state.unique.size < _GREP_SWEEP_MIN_UNIQUE) return result;
|
|
458
|
-
state.nudged = true;
|
|
459
|
-
return `${result}\n\n[grep_sweep_hint] Many distinct grep searches were used in this session. Stop rewording search terms; synthesize from existing anchors or switch tools once (code_graph/find/read known regions).`;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
413
|
export async function executeBuiltinTool(name, args, cwd, options = {}) {
|
|
463
414
|
if (options.abortSignal && !options.signal) {
|
|
464
415
|
options = { ...options, signal: options.abortSignal };
|
|
@@ -521,8 +472,7 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
|
|
|
521
472
|
return formatUnknownBuiltinToolMessage(name, args);
|
|
522
473
|
}
|
|
523
474
|
})();
|
|
524
|
-
|
|
525
|
-
return capToolOutput(_appendClampNotices(args, _withNudge), options);
|
|
475
|
+
return capToolOutput(_appendClampNotices(args, _toolResult), options);
|
|
526
476
|
}
|
|
527
477
|
|
|
528
478
|
// Surface arg-guard clamp notices (args._clampNotices, see pushClampNotice in
|
|
@@ -3,13 +3,13 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
3
3
|
name: 'code_graph',
|
|
4
4
|
title: 'Code Graph',
|
|
5
5
|
annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
6
|
-
description: 'Repo
|
|
6
|
+
description: 'Repo code structure/flow over verified source files only. IDs→graph; literal/zero→grep. Exact identifiers: find_symbol/references/callers/callees; keywords: symbol_search/search. Unsupported target arrays are omitted, never silently mixed. Batch symbols[]/files[] by mode.',
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
|
-
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: '
|
|
11
|
-
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: '
|
|
12
|
-
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers
|
|
10
|
+
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'File modes={overview,imports,dependents,related,impact}; symbols with files→files[] file outline; symbol modes={find_symbol,symbol_search,search,references,callers,callees}; fileless symbols→symbol_search keywords.' },
|
|
11
|
+
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file path(s); supported targets only.' },
|
|
12
|
+
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Exact identifiers (find_symbol/references/callers/callees) or keywords (symbol_search/search); multiple exact symbols use one symbols[] call.' },
|
|
13
13
|
body: { type: 'boolean', description: 'Include body.' },
|
|
14
14
|
limit: { type: 'number', minimum: 1, description: 'Max results.' },
|
|
15
15
|
depth: { type: 'number', minimum: 1, maximum: 5, description: 'Caller depth.' },
|
|
@@ -363,7 +363,7 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
363
363
|
throw new Error('apply_patch: "patch" is required (unified diff or V4A patch string)');
|
|
364
364
|
}
|
|
365
365
|
if (isCompactedPlaceholderPatch(patchStr)) {
|
|
366
|
-
throw new Error('patch body is a compacted-history placeholder ([mixdog compacted …]), not real patch content. Re-read the target file
|
|
366
|
+
throw new Error('patch body is a compacted-history placeholder ([mixdog compacted …]), not real patch content and cannot be executed. Re-read the current target file contents now, then construct and submit a fresh full patch from those contents. Do not reuse the marker, omit the patch argument, or reconstruct the old patch from history.');
|
|
367
367
|
}
|
|
368
368
|
const patchByteLen = Buffer.byteLength(patchStr, 'utf8');
|
|
369
369
|
if (patchByteLen > APPLY_PATCH_MAX_BYTES) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.7.19",
|
|
3
|
+
"_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
|
|
4
|
+
"assets": {
|
|
5
|
+
"darwin-arm64": {
|
|
6
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-darwin-arm64",
|
|
7
|
+
"sha256": "c808fcfb69d2817fd7678dd364d2e1dd68f6ef2d9807f618bdf110c03ca00ccb"
|
|
8
|
+
},
|
|
9
|
+
"darwin-x64": {
|
|
10
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-darwin-x64",
|
|
11
|
+
"sha256": "7ed0eeff84796559ca865fc93e7a2a94f2c3fa00126f919064bb8273656fcd85"
|
|
12
|
+
},
|
|
13
|
+
"linux-arm64": {
|
|
14
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-linux-arm64",
|
|
15
|
+
"sha256": "3cad9c9542f2d97732953a102ef72daf4c79be5361560698d5fd2c5f0c18eda7"
|
|
16
|
+
},
|
|
17
|
+
"linux-x64": {
|
|
18
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-linux-x64",
|
|
19
|
+
"sha256": "cb0e30346f437811dff19238e1854bd37d97a78f35b13e0b704ebeaa9499ae87"
|
|
20
|
+
},
|
|
21
|
+
"win32-x64": {
|
|
22
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.19/mixdog-patch-win32-x64.exe",
|
|
23
|
+
"sha256": "4bf61a99f63e446570d9d2a9c9ef38475d0d70c0a2b632a5f44d4fe9b684b968"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -39,6 +39,29 @@ function trimOrNull(v) {
|
|
|
39
39
|
return s === '' ? null : s
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export function normalizeCoreInput(input = {}, options = {}) {
|
|
43
|
+
const summary = trimOrNull(input.summary ?? input.content)
|
|
44
|
+
const element = trimOrNull(input.element) ?? (summary ? summary.slice(0, CORE_ELEMENT_MAX) : null)
|
|
45
|
+
const suppliedCategory = trimOrNull(input.category)
|
|
46
|
+
const category = (suppliedCategory ?? 'fact').toLowerCase()
|
|
47
|
+
const errors = []
|
|
48
|
+
|
|
49
|
+
if (options.requireElement && !element) errors.push('element required')
|
|
50
|
+
if (options.requireSummary && !summary) errors.push('summary required')
|
|
51
|
+
if (options.requireCategory && !suppliedCategory) errors.push('category required')
|
|
52
|
+
if (element && element.length > CORE_ELEMENT_MAX) {
|
|
53
|
+
errors.push(`element too long (${element.length}/${CORE_ELEMENT_MAX} chars, remove ${element.length - CORE_ELEMENT_MAX})`)
|
|
54
|
+
}
|
|
55
|
+
if (summary && summary.length > CORE_SUMMARY_MAX) {
|
|
56
|
+
errors.push(`summary too long (${summary.length}/${CORE_SUMMARY_MAX} chars, remove ${summary.length - CORE_SUMMARY_MAX})`)
|
|
57
|
+
}
|
|
58
|
+
if (suppliedCategory && !VALID_CAT.has(category)) {
|
|
59
|
+
errors.push(`invalid category "${category}". Valid: ${[...VALID_CAT].join(', ')}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { element, summary, category, suppliedCategory, errors }
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
function _getDb(dataDir) {
|
|
43
66
|
if (!dataDir) throw new Error('core-memory: dataDir required')
|
|
44
67
|
const db = getDatabase(dataDir)
|
|
@@ -206,21 +229,14 @@ export async function listCore(dataDir, projectId = null) {
|
|
|
206
229
|
return r.rows
|
|
207
230
|
}
|
|
208
231
|
|
|
209
|
-
export async function addCore(dataDir,
|
|
232
|
+
export async function addCore(dataDir, input, projectId) {
|
|
210
233
|
if (projectId === undefined) throw new Error('addCore: projectId required — pass null for COMMON pool, or slug string for scoped pool')
|
|
211
|
-
const el =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (sm.length > CORE_SUMMARY_MAX) {
|
|
218
|
-
throw new Error(`core summary too long (${sm.length}/${CORE_SUMMARY_MAX} chars, remove ${sm.length - CORE_SUMMARY_MAX}) — 1 fact in 1-2 sentences; move procedures/multi-step/code to recap or docs.`)
|
|
219
|
-
}
|
|
220
|
-
const cat = (trimOrNull(category) ?? 'fact').toLowerCase()
|
|
221
|
-
if (!VALID_CAT.has(cat)) {
|
|
222
|
-
throw new Error(`invalid category "${cat}". Valid: ${[...VALID_CAT].join(', ')}`)
|
|
223
|
-
}
|
|
234
|
+
const { element: el, summary: sm, category: cat, errors } = normalizeCoreInput(input, {
|
|
235
|
+
requireElement: true,
|
|
236
|
+
requireSummary: true,
|
|
237
|
+
requireCategory: true,
|
|
238
|
+
})
|
|
239
|
+
if (errors.length) throw new Error(errors.join('; '))
|
|
224
240
|
const db = _getDb(dataDir)
|
|
225
241
|
const now = Date.now()
|
|
226
242
|
await _backfillNullEmbeddings(db)
|
|
@@ -299,19 +315,23 @@ export async function editCore(dataDir, id, patch) {
|
|
|
299
315
|
const db = _getDb(dataDir)
|
|
300
316
|
const cur = (await db.query(`SELECT * FROM core_entries WHERE id = $1`, [numId])).rows[0]
|
|
301
317
|
if (!cur) throw new Error(`no entry with id=${numId}`)
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const
|
|
305
|
-
const newCategory =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
318
|
+
const incoming = normalizeCoreInput(patch)
|
|
319
|
+
const newElement = incoming.element ?? cur.element
|
|
320
|
+
const newSummary = incoming.summary ?? cur.summary
|
|
321
|
+
const newCategory = incoming.suppliedCategory ? incoming.category : cur.category
|
|
322
|
+
const { errors } = normalizeCoreInput({
|
|
323
|
+
element: newElement,
|
|
324
|
+
summary: newSummary,
|
|
325
|
+
category: newCategory,
|
|
326
|
+
}, {
|
|
327
|
+
requireElement: true,
|
|
328
|
+
requireSummary: true,
|
|
329
|
+
requireCategory: true,
|
|
330
|
+
})
|
|
331
|
+
if (errors.length) throw new Error(errors.join('; '))
|
|
309
332
|
if (newElement === cur.element && newSummary === cur.summary && newCategory === cur.category) {
|
|
310
333
|
throw new Error('no change')
|
|
311
334
|
}
|
|
312
|
-
if (newSummary && newSummary.length > CORE_SUMMARY_MAX) {
|
|
313
|
-
throw new Error(`core summary too long (${newSummary.length}/${CORE_SUMMARY_MAX} chars, remove ${newSummary.length - CORE_SUMMARY_MAX}) — 1 fact in 1-2 sentences; move procedures/multi-step/code to recap or docs.`)
|
|
314
|
-
}
|
|
315
335
|
const now = Date.now()
|
|
316
336
|
const textChanged = newElement !== cur.element || newSummary !== cur.summary
|
|
317
337
|
if (!textChanged) {
|
|
@@ -45,7 +45,6 @@ export function createHttpRouter({
|
|
|
45
45
|
registerClient,
|
|
46
46
|
deregisterClient,
|
|
47
47
|
getDraining,
|
|
48
|
-
getCycle1CallLlm,
|
|
49
48
|
getTraceDb,
|
|
50
49
|
setTraceDb,
|
|
51
50
|
ingestTranscriptFile,
|
|
@@ -425,198 +424,6 @@ export function createHttpRouter({
|
|
|
425
424
|
return
|
|
426
425
|
}
|
|
427
426
|
|
|
428
|
-
// DEV-ONLY cycle1 chunking bench. Gated by env MIXDOG_DEV_BENCH=1 so
|
|
429
|
-
// production is untouched (route returns 404 when unset). Mirrors cycle1's
|
|
430
|
-
// exact fetch query + per-session windowing, then runs each window through
|
|
431
|
-
// buildCycle1ChunkPrompt + callAgentDispatch + parseCycle1LineFormat. STRICT
|
|
432
|
-
// read-only — no UPDATE, no transaction, no commit.
|
|
433
|
-
if (req.method === 'POST' && req.url === '/dev/cycle1-bench') {
|
|
434
|
-
const db = getDb()
|
|
435
|
-
// Gate: env MIXDOG_DEV_BENCH=1 OR a runtime flag file, so it can be
|
|
436
|
-
// toggled without restarting the host agent (env only reaches the worker
|
|
437
|
-
// on a full CC restart, not via dev-sync full-restart).
|
|
438
|
-
const _devBenchOn = process.env.MIXDOG_DEV_BENCH === '1'
|
|
439
|
-
|| (DATA_DIR && fs.existsSync(path.join(DATA_DIR, '.dev-bench-enabled')))
|
|
440
|
-
if (!_devBenchOn) {
|
|
441
|
-
sendJson(res, { error: 'not found' }, 404)
|
|
442
|
-
return
|
|
443
|
-
}
|
|
444
|
-
if (!isLocalOrigin(req)) {
|
|
445
|
-
sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
|
|
446
|
-
return
|
|
447
|
-
}
|
|
448
|
-
try {
|
|
449
|
-
const body = await readBody(req)
|
|
450
|
-
const sets = Math.max(1, Number(body?.sets ?? 5))
|
|
451
|
-
const repeat = Math.max(1, Number(body?.repeat ?? 1))
|
|
452
|
-
// Optional variant matrix. Each variant: {name, rules}. rules=null → default prompt.
|
|
453
|
-
const rawVariants = Array.isArray(body?.variants) ? body.variants : null
|
|
454
|
-
const variants = rawVariants && rawVariants.length > 0
|
|
455
|
-
? rawVariants.map((v, i) => ({
|
|
456
|
-
name: typeof v?.name === 'string' && v.name ? v.name : `variant-${i + 1}`,
|
|
457
|
-
rules: Array.isArray(v?.rules) ? v.rules : null,
|
|
458
|
-
}))
|
|
459
|
-
: null
|
|
460
|
-
|
|
461
|
-
// Lazy-load LLM + chunking helpers so production boot pays nothing.
|
|
462
|
-
// Use the same in-process agent dispatch adapter as real cycle1 — the legacy
|
|
463
|
-
// agent-ipc callAgentDispatch() path is dead in the detached standalone
|
|
464
|
-
// memory daemon (no connected IPC), so the dev bench must mirror prod.
|
|
465
|
-
const [{ buildCycle1ChunkPrompt, parseCycle1LineFormat }, { resolveMaintenancePreset }] = await Promise.all([
|
|
466
|
-
import('./memory-cycle1.mjs'),
|
|
467
|
-
import('../../shared/llm/index.mjs'),
|
|
468
|
-
])
|
|
469
|
-
const benchCallLlm = getCycle1CallLlm()
|
|
470
|
-
|
|
471
|
-
const CYCLE1_MIN_BATCH = 3
|
|
472
|
-
const CYCLE1_SESSION_CAP = 10
|
|
473
|
-
const BATCH_SIZE = 100
|
|
474
|
-
const TIMEOUT_MS = 180_000
|
|
475
|
-
const fetchLimit = CYCLE1_SESSION_CAP * BATCH_SIZE
|
|
476
|
-
|
|
477
|
-
const fetchResult = await db.query(
|
|
478
|
-
`SELECT id, ts, role, content, session_id, source_ref, project_id
|
|
479
|
-
FROM entries
|
|
480
|
-
WHERE chunk_root IS NULL AND session_id IS NOT NULL
|
|
481
|
-
ORDER BY ts DESC, id DESC
|
|
482
|
-
LIMIT $1`,
|
|
483
|
-
[fetchLimit],
|
|
484
|
-
)
|
|
485
|
-
const rowsDesc = fetchResult.rows
|
|
486
|
-
|
|
487
|
-
if (rowsDesc.length < CYCLE1_MIN_BATCH) {
|
|
488
|
-
sendJson(res, {
|
|
489
|
-
ok: true,
|
|
490
|
-
sets, repeat,
|
|
491
|
-
windowsAvailable: 0,
|
|
492
|
-
note: `not enough pending rows (need >= ${CYCLE1_MIN_BATCH}, got ${rowsDesc.length})`,
|
|
493
|
-
results: [],
|
|
494
|
-
})
|
|
495
|
-
return
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Partition by session_id — same as memory-cycle1.mjs _runCycle1Impl L207-233.
|
|
499
|
-
const sessionMap = new Map()
|
|
500
|
-
for (const row of rowsDesc.slice().reverse()) {
|
|
501
|
-
const sid = row.session_id
|
|
502
|
-
if (!sessionMap.has(sid)) sessionMap.set(sid, [])
|
|
503
|
-
sessionMap.get(sid).push(row)
|
|
504
|
-
}
|
|
505
|
-
const windows = []
|
|
506
|
-
for (const [sid, sessionRows] of sessionMap) {
|
|
507
|
-
if (sessionRows.length < CYCLE1_MIN_BATCH) continue
|
|
508
|
-
const windowCount = Math.max(1, Math.ceil(sessionRows.length / BATCH_SIZE))
|
|
509
|
-
const baseSize = Math.floor(sessionRows.length / windowCount)
|
|
510
|
-
const remainder = sessionRows.length % windowCount
|
|
511
|
-
let _offset = 0
|
|
512
|
-
for (let i = 0; i < windowCount; i++) {
|
|
513
|
-
const size = baseSize + (i < remainder ? 1 : 0)
|
|
514
|
-
windows.push({ sid, rows: sessionRows.slice(_offset, _offset + size) })
|
|
515
|
-
_offset += size
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
const chosen = windows.slice(0, sets)
|
|
519
|
-
|
|
520
|
-
const preset = resolveMaintenancePreset('memory')
|
|
521
|
-
|
|
522
|
-
function summariseChunks(chunks, totalEntries) {
|
|
523
|
-
const usedIdx = new Set()
|
|
524
|
-
for (const c of chunks) for (const i of (c._idxList || [])) usedIdx.add(i)
|
|
525
|
-
const omitted = []
|
|
526
|
-
for (let i = 1; i <= totalEntries; i++) if (!usedIdx.has(i)) omitted.push(i)
|
|
527
|
-
return { covered: usedIdx.size, omitted }
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// When variants are absent, fall back to a single implicit baseline so the
|
|
531
|
-
// pre-variant call shape (single rows × repeat) keeps producing the same
|
|
532
|
-
// {runs:[…]} payload the trigger already knows how to print.
|
|
533
|
-
const variantList = variants ?? [{ name: 'baseline', rules: null }]
|
|
534
|
-
|
|
535
|
-
async function runOnce(rows, customRules) {
|
|
536
|
-
const userMessage = buildCycle1ChunkPrompt(rows, customRules)
|
|
537
|
-
const t0 = Date.now()
|
|
538
|
-
let raw, error
|
|
539
|
-
try {
|
|
540
|
-
raw = await benchCallLlm({
|
|
541
|
-
preset,
|
|
542
|
-
timeout: TIMEOUT_MS,
|
|
543
|
-
}, userMessage)
|
|
544
|
-
} catch (e) {
|
|
545
|
-
error = e?.message ?? String(e)
|
|
546
|
-
}
|
|
547
|
-
const llmMs = Date.now() - t0
|
|
548
|
-
if (error) return { ok: false, llmMs, error }
|
|
549
|
-
const parsed = parseCycle1LineFormat(raw)
|
|
550
|
-
const chunks = Array.isArray(parsed?.chunks) ? parsed.chunks : []
|
|
551
|
-
const { covered, omitted } = summariseChunks(chunks, rows.length)
|
|
552
|
-
const ratio = chunks.length > 0
|
|
553
|
-
? parseFloat((rows.length / chunks.length).toFixed(2))
|
|
554
|
-
: null
|
|
555
|
-
return {
|
|
556
|
-
ok: true,
|
|
557
|
-
llmMs,
|
|
558
|
-
entries: rows.length,
|
|
559
|
-
chunks: chunks.length,
|
|
560
|
-
ratio,
|
|
561
|
-
covered,
|
|
562
|
-
omitted,
|
|
563
|
-
chunkList: chunks.map(c => ({
|
|
564
|
-
idx: c._idxList,
|
|
565
|
-
element: c.element,
|
|
566
|
-
category: c.category,
|
|
567
|
-
summary: c.summary,
|
|
568
|
-
})),
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const results = []
|
|
573
|
-
for (let s = 0; s < chosen.length; s++) {
|
|
574
|
-
const { sid, rows } = chosen[s]
|
|
575
|
-
const sidShort = String(sid).slice(0, 8)
|
|
576
|
-
if (variants) {
|
|
577
|
-
// Variant mode: same rows, one run per variant per repeat.
|
|
578
|
-
const variantResults = []
|
|
579
|
-
for (const v of variantList) {
|
|
580
|
-
const runs = []
|
|
581
|
-
for (let r = 0; r < repeat; r++) {
|
|
582
|
-
const run = await runOnce(rows, v.rules)
|
|
583
|
-
runs.push({ repIdx: r + 1, ...run })
|
|
584
|
-
}
|
|
585
|
-
variantResults.push({ name: v.name, runs })
|
|
586
|
-
}
|
|
587
|
-
results.push({
|
|
588
|
-
setIdx: s + 1,
|
|
589
|
-
sessionIdShort: sidShort,
|
|
590
|
-
entries: rows.length,
|
|
591
|
-
variants: variantResults,
|
|
592
|
-
})
|
|
593
|
-
} else {
|
|
594
|
-
// Legacy single-baseline payload shape.
|
|
595
|
-
const runs = []
|
|
596
|
-
for (let r = 0; r < repeat; r++) {
|
|
597
|
-
const run = await runOnce(rows, null)
|
|
598
|
-
runs.push({ repIdx: r + 1, ...run })
|
|
599
|
-
}
|
|
600
|
-
results.push({
|
|
601
|
-
setIdx: s + 1,
|
|
602
|
-
sessionIdShort: sidShort,
|
|
603
|
-
entries: rows.length,
|
|
604
|
-
runs,
|
|
605
|
-
})
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
sendJson(res, {
|
|
609
|
-
ok: true,
|
|
610
|
-
sets, repeat,
|
|
611
|
-
windowsAvailable: windows.length,
|
|
612
|
-
variants: variants ? variantList.map(v => v.name) : null,
|
|
613
|
-
results,
|
|
614
|
-
})
|
|
615
|
-
} catch (e) {
|
|
616
|
-
sendError(res, e?.message || String(e))
|
|
617
|
-
}
|
|
618
|
-
return
|
|
619
|
-
}
|
|
620
427
|
|
|
621
428
|
if (req.method === 'POST' && req.url === '/api/tool') {
|
|
622
429
|
if (!isLocalOrigin(req)) {
|
|
@@ -33,8 +33,9 @@ import {
|
|
|
33
33
|
promoteCoreCandidate,
|
|
34
34
|
dismissCoreCandidate,
|
|
35
35
|
CORE_SUMMARY_MAX,
|
|
36
|
+
normalizeCoreInput,
|
|
36
37
|
} from './core-memory-store.mjs'
|
|
37
|
-
import { resolveProjectScope } from './project-id-resolver.mjs'
|
|
38
|
+
import { resolveProjectId, resolveProjectScope } from './project-id-resolver.mjs'
|
|
38
39
|
import { resolvePluginData } from '../../shared/plugin-paths.mjs'
|
|
39
40
|
import { getMetaValue, isBootstrapComplete } from './memory.mjs'
|
|
40
41
|
|
|
@@ -57,6 +58,7 @@ export function createMemoryActionHandlers({
|
|
|
57
58
|
getCycle3CallLlm,
|
|
58
59
|
ingestTranscriptFile,
|
|
59
60
|
cwdFromTranscriptPath,
|
|
61
|
+
editCoreImpl = editCore,
|
|
60
62
|
}) {
|
|
61
63
|
const DATA_DIR = dataDir
|
|
62
64
|
|
|
@@ -643,16 +645,43 @@ export function createMemoryActionHandlers({
|
|
|
643
645
|
}
|
|
644
646
|
// Local trim helper — the manage-block trimOrNull at :1807 is scoped to
|
|
645
647
|
// that branch and unreachable from here.
|
|
646
|
-
//
|
|
648
|
+
// Only the explicit string "common" selects the COMMON pool. Core add
|
|
649
|
+
// validates missing, null, and blank project_id below; edits use the
|
|
650
|
+
// target entry's stored pool.
|
|
647
651
|
const hasProjectIdKey = Object.prototype.hasOwnProperty.call(args, 'project_id')
|
|
652
|
+
const projectIdText = typeof args.project_id === 'string' ? args.project_id.trim() : ''
|
|
648
653
|
const projectId = (() => {
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
if (s === '' || s.toLowerCase() === 'common') return null
|
|
652
|
-
if (s === '*') return '*'
|
|
653
|
-
return s
|
|
654
|
+
if (projectIdText.toLowerCase() === 'common') return null
|
|
655
|
+
return projectIdText || null
|
|
654
656
|
})()
|
|
655
657
|
try {
|
|
658
|
+
if (op === 'add' || op === 'edit') {
|
|
659
|
+
const normalized = normalizeCoreInput(args, {
|
|
660
|
+
requireElement: true,
|
|
661
|
+
requireSummary: true,
|
|
662
|
+
requireCategory: true,
|
|
663
|
+
})
|
|
664
|
+
const errors = [...normalized.errors]
|
|
665
|
+
if (op === 'add') {
|
|
666
|
+
if (!hasProjectIdKey || !projectIdText) {
|
|
667
|
+
const inferredProjectId = typeof args.cwd === 'string' && args.cwd
|
|
668
|
+
? resolveProjectId(args.cwd)
|
|
669
|
+
: null
|
|
670
|
+
errors.unshift(
|
|
671
|
+
`project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool` +
|
|
672
|
+
(typeof inferredProjectId === 'string' && /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)?$/.test(inferredProjectId)
|
|
673
|
+
? ` (cwd suggests "${inferredProjectId}")`
|
|
674
|
+
: ''),
|
|
675
|
+
)
|
|
676
|
+
} else if (projectId === '*') {
|
|
677
|
+
errors.unshift('project_id "*" only valid for op="list"')
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (errors.length) {
|
|
681
|
+
return { text: `core ${op}: ${errors.join('; ')}`, isError: true }
|
|
682
|
+
}
|
|
683
|
+
args = { ...args, element: normalized.element, summary: normalized.summary, category: normalized.category }
|
|
684
|
+
}
|
|
656
685
|
if (projectId === '*' && op !== 'list') {
|
|
657
686
|
return { text: `core ${op}: project_id "*" only valid for op="list"`, isError: true }
|
|
658
687
|
}
|
|
@@ -681,14 +710,11 @@ export function createMemoryActionHandlers({
|
|
|
681
710
|
return { text: lines.join('\n') }
|
|
682
711
|
}
|
|
683
712
|
if (op === 'add') {
|
|
684
|
-
if (!hasProjectIdKey) {
|
|
685
|
-
return { text: 'core add: project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool', isError: true }
|
|
686
|
-
}
|
|
687
713
|
const entry = await addCore(coreDataDir, args, projectId)
|
|
688
714
|
return { text: `core added (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
|
|
689
715
|
}
|
|
690
716
|
if (op === 'edit') {
|
|
691
|
-
const entry = await
|
|
717
|
+
const entry = await editCoreImpl(coreDataDir, args.id, args)
|
|
692
718
|
return { text: `core edited (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
|
|
693
719
|
}
|
|
694
720
|
if (op === 'delete') {
|
|
@@ -11,11 +11,10 @@ import { __mixdogMemoryLog, throwIfAborted, resourceDir } from './memory-cycle2-
|
|
|
11
11
|
|
|
12
12
|
export const CYCLE2_ACTIVE_TARGET_CAP = 100
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
export const CYCLE2_ACTIVE_MIN_FLOOR = 20
|
|
14
|
+
// Default active-row floor for cycle2. It is zero so rolling active rechecks
|
|
15
|
+
// always run; protection from archive verdicts is opt-in via
|
|
16
|
+
// config.active_floor.
|
|
17
|
+
export const CYCLE2_ACTIVE_MIN_FLOOR = 0
|
|
19
18
|
|
|
20
19
|
// Status-based verb whitelist. 3-tier policy: pending → active/archived,
|
|
21
20
|
// active → active/archived/update/merge.
|
|
@@ -224,6 +223,15 @@ export function loadCurrentRulesDigest() {
|
|
|
224
223
|
}
|
|
225
224
|
} catch {}
|
|
226
225
|
}
|
|
226
|
+
const workflows = join(resourceDir(), 'workflows')
|
|
227
|
+
try {
|
|
228
|
+
if (existsSync(workflows)) {
|
|
229
|
+
for (const dir of readdirSync(workflows).sort()) {
|
|
230
|
+
const workflow = join(workflows, dir, 'WORKFLOW.md')
|
|
231
|
+
if (existsSync(workflow)) sources.push(workflow)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
227
235
|
const parts = []
|
|
228
236
|
for (const p of sources) {
|
|
229
237
|
try {
|
|
@@ -259,16 +259,19 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
259
259
|
// over-cap path was historically the ONLY one that re-examined active.
|
|
260
260
|
// Reserve a bounded slice of batch slots for the stalest active rows so
|
|
261
261
|
// rule-duplicate / drifted promotions are archived continuously instead of
|
|
262
|
-
// sitting forever un-rechecked. Bounded count + reviewed_at rotation
|
|
263
|
-
//
|
|
262
|
+
// sitting forever un-rechecked. Bounded count + reviewed_at rotation keep
|
|
263
|
+
// each cycle's exposure small (with the default floor of 0 the pool CAN
|
|
264
|
+
// erode to zero over repeated cycles if the gate keeps archiving; set
|
|
265
|
+
// config.active_floor to retain a hard minimum):
|
|
264
266
|
// only the oldest few are re-judged per cycle, and the gate — shown
|
|
265
267
|
// {{CURRENT_RULES}} — keeps genuine A/B entries and archives only
|
|
266
268
|
// restatements. Embedding dedup is skipped on purpose: rule restatements
|
|
267
269
|
// are often cross-language paraphrases whose cosine never clears the merge
|
|
268
270
|
// threshold, but the LLM gate catches the semantic overlap.
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
271
|
+
// Rolling active recheck always runs outside the over-cap broad review,
|
|
272
|
+
// including when the active pool is small. Floor protection is opt-in via
|
|
273
|
+
// config.active_floor and is enforced by the shared floor guard below.
|
|
274
|
+
const activeRecheckQuota = reviewActiveRows
|
|
272
275
|
? 0
|
|
273
276
|
: Math.max(0, Math.min(Number(config.active_recheck_quota ?? 8), batchSize - 1))
|
|
274
277
|
const pendingLimit = batchSize - activeRecheckQuota
|
|
@@ -112,7 +112,30 @@ function coreText(core) {
|
|
|
112
112
|
return `${core?.element || ''}\n${core?.summary || ''}`
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function
|
|
115
|
+
function hasSubstantialNonLatinScript(value) {
|
|
116
|
+
const text = String(value ?? '')
|
|
117
|
+
const letters = text.match(/\p{L}/gu) || []
|
|
118
|
+
const latinLetters = letters.filter((letter) => /\p{Script=Latin}/u.test(letter))
|
|
119
|
+
const nonLatinLetters = letters.length - latinLetters.length
|
|
120
|
+
return nonLatinLetters >= 3 && nonLatinLetters >= letters.length * 0.3
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function cosineSimilarity(a, b) {
|
|
124
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return null
|
|
125
|
+
let dot = 0
|
|
126
|
+
let aNorm = 0
|
|
127
|
+
let bNorm = 0
|
|
128
|
+
for (let i = 0; i < a.length; i++) {
|
|
129
|
+
if (!Number.isFinite(a[i]) || !Number.isFinite(b[i])) return null
|
|
130
|
+
dot += a[i] * b[i]
|
|
131
|
+
aNorm += a[i] ** 2
|
|
132
|
+
bNorm += b[i] ** 2
|
|
133
|
+
}
|
|
134
|
+
if (aNorm === 0 || bNorm === 0) return null
|
|
135
|
+
return dot / Math.sqrt(aNorm * bNorm)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function isSafeConservativeUpdate(current, action) {
|
|
116
139
|
if (!current || !action?.element || !action?.summary) return { ok: false, reason: 'missing text' }
|
|
117
140
|
const newElement = normalizeComparable(action.element)
|
|
118
141
|
const newSummary = normalizeComparable(action.summary)
|
|
@@ -123,11 +146,23 @@ function isSafeConservativeUpdate(current, action) {
|
|
|
123
146
|
const newText = `${action.element}\n${action.summary}`
|
|
124
147
|
const oldLen = normalizeComparable(oldText).length
|
|
125
148
|
const newLen = normalizeComparable(newText).length
|
|
126
|
-
if (oldLen > 0 && newLen > oldLen + 20) return { ok: false, reason: 'rewrite expands entry' }
|
|
127
|
-
|
|
128
149
|
const sim = charDice(oldText, newText)
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
const crossLanguageRewrite = sim < 0.28 && hasSubstantialNonLatinScript(oldText)
|
|
151
|
+
if (!crossLanguageRewrite) {
|
|
152
|
+
if (oldLen > 0 && newLen > oldLen + 20) return { ok: false, reason: 'rewrite expands entry' }
|
|
153
|
+
if (sim < 0.28) return { ok: false, reason: `rewrite drift sim=${sim.toFixed(2)}` }
|
|
154
|
+
return { ok: true, reason: 'safe compression' }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const [oldEmbedding, newEmbedding] = await Promise.all([embedText(oldText), embedText(newText)])
|
|
159
|
+
const cosine = cosineSimilarity(oldEmbedding, newEmbedding)
|
|
160
|
+
if (cosine == null) return { ok: false, reason: 'cross-language embedding invalid' }
|
|
161
|
+
if (cosine < 0.6) return { ok: false, reason: `cross-language semantic drift cosine=${cosine.toFixed(2)}` }
|
|
162
|
+
return { ok: true, reason: `safe cross-language rewrite cosine=${cosine.toFixed(2)}` }
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return { ok: false, reason: `cross-language embedding failed: ${err?.message || 'unknown error'}` }
|
|
165
|
+
}
|
|
131
166
|
}
|
|
132
167
|
|
|
133
168
|
function findElementConflict(coreById, currentId, element, projectId) {
|
|
@@ -659,7 +694,7 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
|
|
|
659
694
|
}
|
|
660
695
|
if (a.verb === 'update') {
|
|
661
696
|
proposed.updated++
|
|
662
|
-
const safety = conservative ? isSafeConservativeUpdate(coreById.get(a.id), a) : { ok: true, reason: 'confirmed' }
|
|
697
|
+
const safety = conservative ? await isSafeConservativeUpdate(coreById.get(a.id), a) : { ok: true, reason: 'confirmed' }
|
|
663
698
|
const conflictId = conservative
|
|
664
699
|
? findElementConflict(coreById, a.id, a.element, coreById.get(a.id)?.project_id ?? null)
|
|
665
700
|
: null
|