create-walle 0.9.30 → 0.9.32
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 +1 -1
- package/template/claude-task-manager/api-prompts.js +410 -5
- package/template/claude-task-manager/api-reviews.js +251 -3
- package/template/claude-task-manager/approval-agent.js +322 -24
- package/template/claude-task-manager/db.js +77 -13
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +35 -1
- package/template/claude-task-manager/docs/postgres-concurrency-dgx-spark.html +407 -0
- package/template/claude-task-manager/docs/prompt-manager-redesign-proposal.html +667 -0
- package/template/claude-task-manager/docs/resume-ux-redesign.html +493 -0
- package/template/claude-task-manager/docs/review-redesign-proposal.html +892 -0
- package/template/claude-task-manager/docs/unified-auto-approver-proposal.html +520 -0
- package/template/claude-task-manager/git-utils.js +164 -26
- package/template/claude-task-manager/lib/agent-capabilities.js +26 -5
- package/template/claude-task-manager/lib/agent-cli-cache.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +12 -0
- package/template/claude-task-manager/lib/agent-version-service.js +650 -0
- package/template/claude-task-manager/lib/approval-drift.js +57 -0
- package/template/claude-task-manager/lib/approval-hook.js +13 -0
- package/template/claude-task-manager/lib/auth-rules.js +6 -0
- package/template/claude-task-manager/lib/broadcast-payload-memo.js +109 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +89 -8
- package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +10 -1
- package/template/claude-task-manager/lib/document-review.js +75 -1
- package/template/claude-task-manager/lib/escalation-review.js +33 -3
- package/template/claude-task-manager/lib/headless-term-service.js +54 -9
- package/template/claude-task-manager/lib/mcp-risk.js +128 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +51 -2
- package/template/claude-task-manager/lib/native-agent-model-args.js +149 -0
- package/template/claude-task-manager/lib/opencode-conversation-store.js +269 -0
- package/template/claude-task-manager/lib/permission-match.js +77 -5
- package/template/claude-task-manager/lib/read-pool-client.js +15 -6
- package/template/claude-task-manager/lib/restore-backoff.js +82 -0
- package/template/claude-task-manager/lib/runtime-registry.js +1 -0
- package/template/claude-task-manager/lib/scheduled-wake.js +42 -0
- package/template/claude-task-manager/lib/session-capture.js +33 -6
- package/template/claude-task-manager/lib/session-history.js +60 -3
- package/template/claude-task-manager/lib/session-jobs.js +19 -0
- package/template/claude-task-manager/lib/session-restore.js +5 -4
- package/template/claude-task-manager/lib/session-standup.js +2 -0
- package/template/claude-task-manager/lib/session-stream.js +9 -0
- package/template/claude-task-manager/lib/state-sync/cell-diff.js +31 -4
- package/template/claude-task-manager/lib/state-sync/restore-frame-hold.js +39 -0
- package/template/claude-task-manager/lib/tui-input-modes.js +40 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +103 -36
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +7 -1
- package/template/claude-task-manager/lib/walle-token-chip.js +65 -0
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/providers/claude-code.js +20 -1
- package/template/claude-task-manager/providers/codex-mcp-extract.js +33 -0
- package/template/claude-task-manager/providers/codex-mcp.js +23 -20
- package/template/claude-task-manager/providers/codex.js +65 -1
- package/template/claude-task-manager/public/css/prompts.css +345 -0
- package/template/claude-task-manager/public/css/reviews.css +248 -40
- package/template/claude-task-manager/public/css/walle-session.css +90 -26
- package/template/claude-task-manager/public/index.html +2031 -428
- package/template/claude-task-manager/public/js/document-review-links.js +255 -13
- package/template/claude-task-manager/public/js/file-context-menu.js +185 -0
- package/template/claude-task-manager/public/js/message-renderer.js +46 -1
- package/template/claude-task-manager/public/js/mobile-review-core.js +54 -0
- package/template/claude-task-manager/public/js/prompt-diff.js +55 -0
- package/template/claude-task-manager/public/js/prompt-editor-chrome.js +108 -0
- package/template/claude-task-manager/public/js/prompt-editor-modes.js +87 -0
- package/template/claude-task-manager/public/js/prompt-organize.js +179 -0
- package/template/claude-task-manager/public/js/prompt-reuse.js +77 -0
- package/template/claude-task-manager/public/js/prompts.js +683 -215
- package/template/claude-task-manager/public/js/resume-state.js +186 -0
- package/template/claude-task-manager/public/js/reviews.js +402 -121
- package/template/claude-task-manager/public/js/screenshot-router.js +91 -0
- package/template/claude-task-manager/public/js/session-search-utils.js +81 -0
- package/template/claude-task-manager/public/js/state-sync-client.js +134 -17
- package/template/claude-task-manager/public/js/stream-view.js +93 -20
- package/template/claude-task-manager/public/js/walle-session.js +183 -32
- package/template/claude-task-manager/public/js/walle.js +21 -9
- package/template/claude-task-manager/public/m/app.css +117 -0
- package/template/claude-task-manager/public/m/app.js +309 -3
- package/template/claude-task-manager/public/m/index.html +56 -1
- package/template/claude-task-manager/server.js +724 -305
- package/template/claude-task-manager/workers/db-owner-worker.js +8 -0
- package/template/claude-task-manager/workers/read-pool-worker.js +4 -0
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +10 -1
- package/template/docs/proposals/2026-06-23-local-model-right-path.html +340 -0
- package/template/docs/proposals/2026-06-24-qlora-done-right.md +255 -0
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +21 -3
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +27 -4
- package/template/wall-e/brain.js +50 -5
- package/template/wall-e/chat/force-compact.js +73 -0
- package/template/wall-e/chat.js +137 -9
- package/template/wall-e/coding/action-memory-policy.js +120 -1
- package/template/wall-e/coding/compaction-service.js +10 -3
- package/template/wall-e/coding/model-router.js +116 -0
- package/template/wall-e/coding-orchestrator.js +303 -11
- package/template/wall-e/coding-prompts.js +1 -1
- package/template/wall-e/docs/skill-self-heal-design.html +429 -0
- package/template/wall-e/llm/client.js +115 -6
- package/template/wall-e/llm/codex-cli.js +71 -23
- package/template/wall-e/llm/codex-cli.plugin.json +1 -0
- package/template/wall-e/llm/default-fallback.js +71 -11
- package/template/wall-e/llm/mlx-worker.js +0 -0
- package/template/wall-e/llm/mlx.js +46 -12
- package/template/wall-e/llm/provider-error.js +9 -3
- package/template/wall-e/llm/provider-health-state.js +561 -11
- package/template/wall-e/llm/registry.js +6 -0
- package/template/wall-e/llm/routing-policy.js +255 -25
- package/template/wall-e/llm/tool-call-validator.js +156 -0
- package/template/wall-e/skills/skill-dispatch-decision.js +82 -2
- package/template/wall-e/skills/skill-planner.js +72 -5
- package/template/wall-e/tools/command-registry.js +34 -0
- package/template/wall-e/training/gemma-e4b-qlora.js +197 -1
- package/template/wall-e/training/mlx_lora_launch.py +196 -0
- package/template/wall-e/training/training-coexistence.sh +87 -0
- package/template/wall-e/training/training-mem-guard.sh +76 -0
package/template/wall-e/chat.js
CHANGED
|
@@ -520,7 +520,10 @@ function _getCommandRegistry() {
|
|
|
520
520
|
if (_commandRegistry) return _commandRegistry;
|
|
521
521
|
const { CommandRegistry } = require('./tools/command-registry');
|
|
522
522
|
_commandRegistry = new CommandRegistry();
|
|
523
|
-
|
|
523
|
+
// getProvider lets the /compact builtin summarize via the active default client.
|
|
524
|
+
_commandRegistry.registerBuiltins({
|
|
525
|
+
getProvider: () => { try { return getDefaultClient(); } catch { return null; } },
|
|
526
|
+
});
|
|
524
527
|
try {
|
|
525
528
|
const {
|
|
526
529
|
filterSkillsForAgent,
|
|
@@ -918,7 +921,7 @@ function _providerRowHasRuntimeAccess(row = {}) {
|
|
|
918
921
|
}
|
|
919
922
|
|
|
920
923
|
function _fallbackErrorType(providerError = {}) {
|
|
921
|
-
return new Set(['rate_limited', 'network', 'provider_unavailable', 'timeout']).has(providerError.type);
|
|
924
|
+
return new Set(['rate_limited', 'network', 'provider_unavailable', 'timeout', 'model_unavailable', 'model_circuit_open']).has(providerError.type);
|
|
922
925
|
}
|
|
923
926
|
|
|
924
927
|
function _routingPolicySettings() {
|
|
@@ -929,6 +932,80 @@ function _routingPolicySettings() {
|
|
|
929
932
|
}
|
|
930
933
|
}
|
|
931
934
|
|
|
935
|
+
function _uniqueChatProviders(values = []) {
|
|
936
|
+
const out = [];
|
|
937
|
+
const seen = new Set();
|
|
938
|
+
for (const value of values) {
|
|
939
|
+
const text = String(value || '').trim();
|
|
940
|
+
const key = text.toLowerCase();
|
|
941
|
+
if (!key || seen.has(key)) continue;
|
|
942
|
+
seen.add(key);
|
|
943
|
+
out.push(text);
|
|
944
|
+
}
|
|
945
|
+
return out;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function _chatProviderModelCircuit(providers, model, options = {}) {
|
|
949
|
+
try {
|
|
950
|
+
const healthState = require('./llm/provider-health-state');
|
|
951
|
+
const circuit = healthState.getFirstProviderModelCircuit(_uniqueChatProviders(providers), model, { brain });
|
|
952
|
+
if (!circuit) return null;
|
|
953
|
+
if (options.allowProbe && circuit.probe_due) {
|
|
954
|
+
healthState.reserveProviderModelCircuitProbe(circuit.provider, circuit.model, { brain });
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
return circuit;
|
|
958
|
+
} catch {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function _recordChatCircuitRoutingNotice(circuit, fallback) {
|
|
964
|
+
try {
|
|
965
|
+
const failures = Number(circuit?.failures || 0);
|
|
966
|
+
const total = Number(circuit?.total || 0);
|
|
967
|
+
const rate = Number(circuit?.failure_rate_pct || Math.round(Number(circuit?.failure_rate || 0) * 100));
|
|
968
|
+
const windowMs = Number(circuit?.window_ms || 0);
|
|
969
|
+
const windowLabel = windowMs >= 60 * 60 * 1000
|
|
970
|
+
? `${Math.round(windowMs / (60 * 60 * 1000))}h`
|
|
971
|
+
: 'rolling window';
|
|
972
|
+
const fromLabel = `${circuit?.provider || 'provider'}${circuit?.model ? ' / ' + circuit.model : ''}`;
|
|
973
|
+
const hasFallback = !!fallback;
|
|
974
|
+
const toLabel = hasFallback
|
|
975
|
+
? `${fallback?.providerType || fallback?.provider?.type || 'fallback'}${fallback?.model ? ' / ' + fallback.model : ''}`
|
|
976
|
+
: 'no eligible fallback';
|
|
977
|
+
require('./llm/health').default.recordRoutingNotice({
|
|
978
|
+
kind: 'model_circuit_open',
|
|
979
|
+
severity: 'warning',
|
|
980
|
+
message: hasFallback
|
|
981
|
+
? `Auto-fallback active for ${fromLabel}; answering via ${toLabel} while recovery probes run periodically.`
|
|
982
|
+
: `Auto-fallback active for ${fromLabel}, but this turn has no eligible fallback route. Waiting for recovery probes or Setup changes.`,
|
|
983
|
+
detail: total ? `${rate}% errors over ${windowLabel} (${failures}/${total}).` : '',
|
|
984
|
+
from: { type: circuit?.provider || null, model: circuit?.model || null },
|
|
985
|
+
to: hasFallback ? { type: fallback?.providerType || fallback?.provider?.type || null, model: fallback?.model || null } : null,
|
|
986
|
+
});
|
|
987
|
+
} catch {}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function _chatCircuitOpenError(circuit) {
|
|
991
|
+
const failures = Number(circuit?.failures || 0);
|
|
992
|
+
const total = Number(circuit?.total || 0);
|
|
993
|
+
const rate = Number(circuit?.failure_rate_pct || Math.round(Number(circuit?.failure_rate || 0) * 100));
|
|
994
|
+
const probe = circuit?.probe_due
|
|
995
|
+
? 'a recovery probe is due now'
|
|
996
|
+
: `next recovery probe in ${Math.max(1, Math.ceil(Number(circuit?.next_probe_in_ms || 0) / 60000))}m`;
|
|
997
|
+
const sample = total ? `${rate}% errors (${failures}/${total})` : 'recent failures';
|
|
998
|
+
const err = new Error(`Auto-fallback is active for ${circuit?.provider || 'provider'}${circuit?.model ? ' / ' + circuit.model : ''}: ${sample}; ${probe}.`);
|
|
999
|
+
err.status = 503;
|
|
1000
|
+
const decorated = decorateProviderError(err, {
|
|
1001
|
+
type: 'provider_unavailable_local',
|
|
1002
|
+
provider: circuit?.provider || null,
|
|
1003
|
+
model: circuit?.model || null,
|
|
1004
|
+
});
|
|
1005
|
+
decorated.suppressProviderHealthRecord = true;
|
|
1006
|
+
return decorated;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
932
1009
|
function _allowChatProviderFallback(opts = {}) {
|
|
933
1010
|
if (_clientOverride) return false;
|
|
934
1011
|
if (opts.modelPinned === true || opts.providerPinned === true) return false;
|
|
@@ -1328,7 +1405,7 @@ function _selectFallbackModel(providerId, runtimeType, taskType) {
|
|
|
1328
1405
|
return getDefaultModelForProvider(runtimeType);
|
|
1329
1406
|
}
|
|
1330
1407
|
|
|
1331
|
-
function _listChatFallbackRows({ attemptedProviderTypes, providerAvailability }) {
|
|
1408
|
+
function _listChatFallbackRows({ attemptedProviderTypes, providerAvailability, taskType = 'chat' }) {
|
|
1332
1409
|
let rows = [];
|
|
1333
1410
|
try {
|
|
1334
1411
|
rows = (brain.listModelProviders?.() || [])
|
|
@@ -1352,7 +1429,13 @@ function _listChatFallbackRows({ attemptedProviderTypes, providerAvailability })
|
|
|
1352
1429
|
if (providerAvailability?.isProviderUsable) {
|
|
1353
1430
|
if (!providerAvailability.isProviderUsable(row.id) || !providerAvailability.isProviderUsable(runtimeType)) continue;
|
|
1354
1431
|
}
|
|
1355
|
-
|
|
1432
|
+
const model = resolveCompatibleModel(
|
|
1433
|
+
_selectFallbackModel(row.id, runtimeType, taskType),
|
|
1434
|
+
runtimeType,
|
|
1435
|
+
);
|
|
1436
|
+
const circuit = _chatProviderModelCircuit([runtimeType, row.type, row.id], model, { allowProbe: false });
|
|
1437
|
+
if (circuit) continue;
|
|
1438
|
+
candidates.push({ row, runtimeType, modelCount, model });
|
|
1356
1439
|
}
|
|
1357
1440
|
const rank = {
|
|
1358
1441
|
openai: 10,
|
|
@@ -1377,8 +1460,8 @@ function _listChatProviderFallbacks({ attemptedProviderTypes, providerError, tas
|
|
|
1377
1460
|
if (!_allowChatProviderFallback(opts)) return null;
|
|
1378
1461
|
if (!_fallbackErrorType(providerError)) return null;
|
|
1379
1462
|
const fallbacks = [];
|
|
1380
|
-
for (const { row, runtimeType } of _listChatFallbackRows({ attemptedProviderTypes, providerAvailability })) {
|
|
1381
|
-
const model = resolveCompatibleModel(
|
|
1463
|
+
for (const { row, runtimeType, model: fallbackModel } of _listChatFallbackRows({ attemptedProviderTypes, providerAvailability, taskType })) {
|
|
1464
|
+
const model = fallbackModel || resolveCompatibleModel(
|
|
1382
1465
|
_selectFallbackModel(row.id, runtimeType, taskType),
|
|
1383
1466
|
runtimeType,
|
|
1384
1467
|
);
|
|
@@ -1407,8 +1490,8 @@ function _allowFinalizationProviderFallback(opts = {}) {
|
|
|
1407
1490
|
function _listChatFinalizationFallbacks({ attemptedProviderTypes, taskType, opts, providerAvailability }) {
|
|
1408
1491
|
if (!_allowFinalizationProviderFallback(opts)) return [];
|
|
1409
1492
|
const fallbacks = [];
|
|
1410
|
-
for (const { row, runtimeType } of _listChatFallbackRows({ attemptedProviderTypes, providerAvailability })) {
|
|
1411
|
-
const model = resolveCompatibleModel(
|
|
1493
|
+
for (const { row, runtimeType, model: fallbackModel } of _listChatFallbackRows({ attemptedProviderTypes, providerAvailability, taskType })) {
|
|
1494
|
+
const model = fallbackModel || resolveCompatibleModel(
|
|
1412
1495
|
_selectFallbackModel(row.id, runtimeType, taskType),
|
|
1413
1496
|
runtimeType,
|
|
1414
1497
|
);
|
|
@@ -3690,6 +3773,49 @@ async function chat(message, opts = {}) {
|
|
|
3690
3773
|
|
|
3691
3774
|
const chatStart = Date.now();
|
|
3692
3775
|
const attemptedProviderTypes = new Set([targetProviderType, provider.type].filter(Boolean));
|
|
3776
|
+
const primaryCircuit = !_clientOverride
|
|
3777
|
+
? _chatProviderModelCircuit([provider.type, targetProviderType, selectedRoute.provider], selectedModel, { allowProbe: true })
|
|
3778
|
+
: null;
|
|
3779
|
+
if (primaryCircuit) {
|
|
3780
|
+
let fallback = null;
|
|
3781
|
+
if (_allowCrossProviderFallbackForTurn({
|
|
3782
|
+
opts,
|
|
3783
|
+
wallECodingMode,
|
|
3784
|
+
provider,
|
|
3785
|
+
model: selectedModel,
|
|
3786
|
+
})) {
|
|
3787
|
+
const fallbacks = _listChatProviderFallbacks({
|
|
3788
|
+
attemptedProviderTypes,
|
|
3789
|
+
providerError: { type: 'model_circuit_open' },
|
|
3790
|
+
taskType: opts.taskType || 'chat',
|
|
3791
|
+
opts,
|
|
3792
|
+
providerAvailability,
|
|
3793
|
+
}) || [];
|
|
3794
|
+
fallback = fallbacks[0] || null;
|
|
3795
|
+
}
|
|
3796
|
+
if (fallback) {
|
|
3797
|
+
const fromProvider = primaryCircuit.provider || targetProviderType;
|
|
3798
|
+
const fromModel = primaryCircuit.model || selectedModel;
|
|
3799
|
+
provider = withProviderMessageGuard(fallback.provider);
|
|
3800
|
+
selectedModel = fallback.model;
|
|
3801
|
+
usedProvider = fallback.provider.type || fallback.providerType;
|
|
3802
|
+
attemptedProviderTypes.add(fallback.providerType);
|
|
3803
|
+
attemptedProviderTypes.add(fallback.provider?.type);
|
|
3804
|
+
_recordChatCircuitRoutingNotice(primaryCircuit, fallback);
|
|
3805
|
+
try {
|
|
3806
|
+
_telemetry.track('chat_provider_failover', {
|
|
3807
|
+
from_provider: fromProvider,
|
|
3808
|
+
from_model: fromModel,
|
|
3809
|
+
to_provider: fallback.providerType,
|
|
3810
|
+
to_model: fallback.model,
|
|
3811
|
+
reason: 'model_circuit_open',
|
|
3812
|
+
});
|
|
3813
|
+
} catch {}
|
|
3814
|
+
} else {
|
|
3815
|
+
_recordChatCircuitRoutingNotice(primaryCircuit, null);
|
|
3816
|
+
throw _chatCircuitOpenError(primaryCircuit);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3693
3819
|
for (let turn = 0; turn < allowedTurns; turn++) {
|
|
3694
3820
|
if (opts.abortSignal?.aborted) {
|
|
3695
3821
|
clearTimeout(timeout);
|
|
@@ -3802,7 +3928,7 @@ async function chat(message, opts = {}) {
|
|
|
3802
3928
|
provider,
|
|
3803
3929
|
model: selectedModel,
|
|
3804
3930
|
})
|
|
3805
|
-
&& _listChatFallbackRows({ attemptedProviderTypes, providerAvailability }).length > 0)
|
|
3931
|
+
&& _listChatFallbackRows({ attemptedProviderTypes, providerAvailability, taskType: opts.taskType || 'chat' }).length > 0)
|
|
3806
3932
|
? 0
|
|
3807
3933
|
: 2;
|
|
3808
3934
|
// Unified with coding mode: chat gets the 8k floor so longer
|
|
@@ -3853,6 +3979,7 @@ async function chat(message, opts = {}) {
|
|
|
3853
3979
|
}) || [])
|
|
3854
3980
|
: [];
|
|
3855
3981
|
if (fallbacks.length > 0) {
|
|
3982
|
+
recordProviderFailureAlert(decorated.providerError, brain);
|
|
3856
3983
|
let fallbackSucceeded = false;
|
|
3857
3984
|
let lastFallbackDecorated = null;
|
|
3858
3985
|
for (const fallback of fallbacks) {
|
|
@@ -3872,6 +3999,7 @@ async function chat(message, opts = {}) {
|
|
|
3872
3999
|
});
|
|
3873
4000
|
} catch {}
|
|
3874
4001
|
try {
|
|
4002
|
+
resetTurnTimeout(Math.min(FINALIZATION_TIMEOUT_MS, MESSAGE_TIMEOUT_MS));
|
|
3875
4003
|
const fallbackReasoningOptions = resolveCurrentReasoningOptions(provider, selectedModel);
|
|
3876
4004
|
response = await withRetry(() => provider.chat({
|
|
3877
4005
|
model: selectedModel,
|
|
@@ -3,7 +3,20 @@
|
|
|
3
3
|
const DEFAULT_MIN_SCORE = 120;
|
|
4
4
|
const DEFAULT_MIN_MARGIN = 15;
|
|
5
5
|
const DEFAULT_LIMIT = 8;
|
|
6
|
-
|
|
6
|
+
// Modes, from most honest to most invasive:
|
|
7
|
+
// off — disabled.
|
|
8
|
+
// audit — observe candidates, never touch the call.
|
|
9
|
+
// exemplar — HONEST RAG (recommended): never mutate the call; surface
|
|
10
|
+
// retrieved verified examples for prompt injection so the model
|
|
11
|
+
// writes its OWN call (adapts params → generalizes).
|
|
12
|
+
// assist — fill ONLY missing fields from memory (model still drives).
|
|
13
|
+
// apply — replace the whole input with a remembered action. This is
|
|
14
|
+
// deterministic replay, NOT model capability: passes produced
|
|
15
|
+
// this way are flagged memory-mutated and excluded from
|
|
16
|
+
// capability metrics (see eval-splits.hasSyntheticPass).
|
|
17
|
+
const MODES = new Set(['off', 'audit', 'exemplar', 'assist', 'apply']);
|
|
18
|
+
// Modes where memory substitutes the action rather than the model deciding it.
|
|
19
|
+
const MUTATING_MODES = new Set(['assist', 'apply']);
|
|
7
20
|
const SELECTORS = new Set(['store', 'replay', 'auto']);
|
|
8
21
|
const REPLAY_SELECTOR_TOOLS = new Set(['run_shell', 'apply_patch', 'edit_file', 'write_file']);
|
|
9
22
|
|
|
@@ -71,19 +84,30 @@ class ActionMemoryPolicy {
|
|
|
71
84
|
};
|
|
72
85
|
|
|
73
86
|
let preparedCall = clonedCall;
|
|
87
|
+
decision.mutatedByMemory = false;
|
|
74
88
|
if (this.mode === 'audit') {
|
|
75
89
|
decision.action = 'observe';
|
|
76
90
|
decision.accepted = false;
|
|
77
91
|
decision.reason = selection?.id ? 'audit_only' : 'no_candidate';
|
|
92
|
+
} else if (this.mode === 'exemplar') {
|
|
93
|
+
// Honest RAG: never touch the call. Candidates are returned for the
|
|
94
|
+
// caller to inject into the prompt as worked examples; the model still
|
|
95
|
+
// emits its own tool call.
|
|
96
|
+
decision.action = 'exemplar';
|
|
97
|
+
decision.accepted = false;
|
|
98
|
+
decision.exemplarCount = candidates.length;
|
|
99
|
+
decision.reason = candidates.length ? 'exemplars_available' : 'no_candidate';
|
|
78
100
|
} else if (canUseSelection && this.mode === 'assist') {
|
|
79
101
|
const mergedInput = fillMissingFields(clonedCall.input || {}, selectedInput);
|
|
80
102
|
preparedCall = { ...clonedCall, input: mergedInput };
|
|
81
103
|
decision.action = 'assist';
|
|
82
104
|
decision.accepted = true;
|
|
105
|
+
decision.mutatedByMemory = true;
|
|
83
106
|
} else if (canUseSelection && this.mode === 'apply') {
|
|
84
107
|
preparedCall = { ...clonedCall, input: cloneJson(selectedInput) };
|
|
85
108
|
decision.action = 'apply';
|
|
86
109
|
decision.accepted = true;
|
|
110
|
+
decision.mutatedByMemory = true;
|
|
87
111
|
}
|
|
88
112
|
|
|
89
113
|
this.recordAudit({
|
|
@@ -149,6 +173,38 @@ class ActionMemoryPolicy {
|
|
|
149
173
|
}
|
|
150
174
|
}
|
|
151
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Honest RAG retrieval: return verified past actions as worked exemplars for
|
|
178
|
+
* prompt injection. Does NOT mutate anything. Optionally re-ranks by embedding
|
|
179
|
+
* similarity of the task text when an embedder is provided.
|
|
180
|
+
* @returns {{ exemplars: object[], text: string }}
|
|
181
|
+
*/
|
|
182
|
+
async retrieveExemplars({ context = {}, toolName = '', limit = 3, embedder = null } = {}) {
|
|
183
|
+
if (!this.store?.searchActionMemoryCandidates) return { exemplars: [], text: '' };
|
|
184
|
+
let candidates = [];
|
|
185
|
+
try {
|
|
186
|
+
candidates = this.store.searchActionMemoryCandidates({
|
|
187
|
+
context,
|
|
188
|
+
prediction: toolName ? { name: toolName, input: {} } : null,
|
|
189
|
+
toolName,
|
|
190
|
+
limit: Math.max(limit, this.limit),
|
|
191
|
+
positiveOnly: true,
|
|
192
|
+
}) || [];
|
|
193
|
+
} catch (err) {
|
|
194
|
+
this.logger?.warn?.(`[action-memory] exemplar retrieval failed: ${err.message}`);
|
|
195
|
+
return { exemplars: [], text: '' };
|
|
196
|
+
}
|
|
197
|
+
const ranked = await rerankByEmbedding(candidates, context, embedder, this.logger);
|
|
198
|
+
const exemplars = ranked.slice(0, Math.max(1, limit)).map((candidate) => ({
|
|
199
|
+
tool: candidate.tool || candidate.toolName || toolName,
|
|
200
|
+
input: safeObject(candidate.input),
|
|
201
|
+
outcome: candidate.outcomeStatus || candidate.outcome_status || 'verified',
|
|
202
|
+
score: Number.isFinite(Number(candidate.score)) ? Number(candidate.score) : null,
|
|
203
|
+
source: candidate.sourceKind || candidate.source_kind || 'memory',
|
|
204
|
+
}));
|
|
205
|
+
return { exemplars, text: formatExemplars(exemplars) };
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
recordAudit(row) {
|
|
153
209
|
if (!this.store?.recordSelectionAudit) return null;
|
|
154
210
|
try {
|
|
@@ -175,14 +231,73 @@ async function executeToolCallWithActionMemoryPolicy({
|
|
|
175
231
|
: { call: cloneToolCall(call), decision: { mode: 'off', action: 'skip', accepted: false }, candidates: [] };
|
|
176
232
|
const execution = await execute(evaluated.call, evaluated.decision);
|
|
177
233
|
if (execution && typeof execution === 'object') {
|
|
234
|
+
const action = evaluated.decision?.action || 'skip';
|
|
178
235
|
execution.actionMemory = {
|
|
179
236
|
decision: evaluated.decision,
|
|
180
237
|
candidateCount: evaluated.candidates.length,
|
|
238
|
+
action,
|
|
239
|
+
mutated: Boolean(evaluated.decision?.mutatedByMemory),
|
|
181
240
|
};
|
|
241
|
+
// A pass where memory replaced the model's whole action ('apply') is
|
|
242
|
+
// deterministic replay, not model capability — tag it so honest eval
|
|
243
|
+
// (eval-splits.hasSyntheticPass) excludes it from capability metrics.
|
|
244
|
+
if (action === 'apply') {
|
|
245
|
+
execution.memoryMutatedToolResults = Number(execution.memoryMutatedToolResults || 0) + 1;
|
|
246
|
+
}
|
|
182
247
|
}
|
|
183
248
|
return execution;
|
|
184
249
|
}
|
|
185
250
|
|
|
251
|
+
function formatExemplars(exemplars = []) {
|
|
252
|
+
if (!exemplars.length) return '';
|
|
253
|
+
const lines = ['[MEMORY] Similar verified actions from past work (for reference — adapt to the current task, do not copy blindly):'];
|
|
254
|
+
exemplars.forEach((ex, index) => {
|
|
255
|
+
lines.push(`${index + 1}. ${ex.tool}(${compactJson(ex.input)}) → ${ex.outcome}`);
|
|
256
|
+
});
|
|
257
|
+
return lines.join('\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function compactJson(value) {
|
|
261
|
+
try {
|
|
262
|
+
const text = JSON.stringify(value ?? {});
|
|
263
|
+
return text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
264
|
+
} catch {
|
|
265
|
+
return '{}';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function cosineSimilarity(a = [], b = []) {
|
|
270
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return 0;
|
|
271
|
+
let dot = 0; let na = 0; let nb = 0;
|
|
272
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
273
|
+
dot += a[i] * b[i];
|
|
274
|
+
na += a[i] * a[i];
|
|
275
|
+
nb += b[i] * b[i];
|
|
276
|
+
}
|
|
277
|
+
if (na === 0 || nb === 0) return 0;
|
|
278
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function rerankByEmbedding(candidates = [], context = {}, embedder = null, logger = console) {
|
|
282
|
+
const taskText = String(context.taskText || context.task || context.taskPreview || '').trim();
|
|
283
|
+
if (!embedder || typeof embedder.embed !== 'function' || !taskText || candidates.length <= 1) {
|
|
284
|
+
return candidates;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const texts = [taskText, ...candidates.map((c) => String(c.taskPreview || c.task_preview || '').trim())];
|
|
288
|
+
const vectors = await embedder.embed(texts);
|
|
289
|
+
if (!Array.isArray(vectors) || vectors.length !== texts.length) return candidates;
|
|
290
|
+
const queryVec = vectors[0];
|
|
291
|
+
return candidates
|
|
292
|
+
.map((candidate, index) => ({ candidate, sim: cosineSimilarity(queryVec, vectors[index + 1]) }))
|
|
293
|
+
.sort((left, right) => (right.sim - left.sim) || ((Number(right.candidate.score) || 0) - (Number(left.candidate.score) || 0)))
|
|
294
|
+
.map((entry) => entry.candidate);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
logger?.warn?.(`[action-memory] embedding rerank failed: ${err.message}`);
|
|
297
|
+
return candidates;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
186
301
|
function cloneToolCall(call = {}) {
|
|
187
302
|
return {
|
|
188
303
|
...call,
|
|
@@ -347,7 +462,11 @@ module.exports = {
|
|
|
347
462
|
DEFAULT_LIMIT,
|
|
348
463
|
DEFAULT_MIN_MARGIN,
|
|
349
464
|
DEFAULT_MIN_SCORE,
|
|
465
|
+
MODES,
|
|
466
|
+
MUTATING_MODES,
|
|
350
467
|
executeToolCallWithActionMemoryPolicy,
|
|
351
468
|
fillMissingFields,
|
|
469
|
+
formatExemplars,
|
|
470
|
+
cosineSimilarity,
|
|
352
471
|
replayExampleFromRuntimeContext,
|
|
353
472
|
};
|
|
@@ -56,9 +56,16 @@ class CompactionService {
|
|
|
56
56
|
const actualInput = Number.isFinite(usage?.input) ? usage.input
|
|
57
57
|
: Number.isFinite(usage?.inputTokens) ? usage.inputTokens
|
|
58
58
|
: null;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const estimated = systemTokens + estimateMessagesTokens(messages);
|
|
60
|
+
// Use the LARGER of the provider's real last-turn occupancy and our text
|
|
61
|
+
// estimate. estimateMessagesTokens is a chars/4 heuristic — image-blind and
|
|
62
|
+
// it undercounts code/diff/JSON-heavy history, so on its own it let the live
|
|
63
|
+
// window silently run past 100% (e.g. real 234k while the estimate read
|
|
64
|
+
// <150k). The real occupancy is authoritative but can be slightly stale (it
|
|
65
|
+
// predates messages appended since that turn). max() defeats both blind
|
|
66
|
+
// spots: compaction fires when EITHER signal crosses the threshold.
|
|
67
|
+
if (actualInput == null) return estimated;
|
|
68
|
+
return Math.max(actualInput, estimated);
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
shouldCompact({ messages = [], systemTokens = 0, usage = null, contextWindow = this.contextWindow } = {}) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Difficulty / confidence model routing.
|
|
5
|
+
*
|
|
6
|
+
* A 4B local model is fast and good enough for seen/simple repeated workflows,
|
|
7
|
+
* but has a real capability ceiling on hard/unseen work (the session's own
|
|
8
|
+
* research: Gemma A4B ~77 vs E4B ~52 on LiveCodeBench). Rather than pretend the
|
|
9
|
+
* small model learned everything, route by difficulty:
|
|
10
|
+
* - simple/seen → local model (cheap, fast, retrieval-grounded);
|
|
11
|
+
* - hard/unseen → a stronger model (or cloud).
|
|
12
|
+
*
|
|
13
|
+
* Routing uses only cheap, pre-run signals (retrieval hit, prompt shape, file
|
|
14
|
+
* count, explicit difficulty), and a runtime escalation check that promotes to
|
|
15
|
+
* the strong model when the local model is provably failing (read-loops, no
|
|
16
|
+
* decisive action, invalid tool calls) past a turn budget.
|
|
17
|
+
*
|
|
18
|
+
* Pure + provider-agnostic: callers pass a { local, strong } model map.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DIFFICULTY = Object.freeze({ SIMPLE: 'simple', HARD: 'hard' });
|
|
22
|
+
|
|
23
|
+
const MULTI_STEP_RE = /\b(refactor|migrate|migration|across|end[- ]?to[- ]?end|rewrite|redesign|architecture|all callers|every|multiple files|orchestrat|concurren|race condition|distributed)\b/i;
|
|
24
|
+
const HARD_PROMPT_CHARS = 1200;
|
|
25
|
+
const HARD_FILE_COUNT = 3;
|
|
26
|
+
|
|
27
|
+
function normalizeDifficultyHint(value) {
|
|
28
|
+
const text = String(value || '').trim().toLowerCase();
|
|
29
|
+
if (['easy', 'simple', 'trivial', 'low'].includes(text)) return DIFFICULTY.SIMPLE;
|
|
30
|
+
if (['hard', 'complex', 'difficult', 'high', 'medium'].includes(text)) {
|
|
31
|
+
// 'medium' leans hard for routing — better to use the strong model than to
|
|
32
|
+
// fail on a small one.
|
|
33
|
+
return text === 'medium' ? DIFFICULTY.HARD : DIFFICULTY.HARD;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @returns {{ difficulty: string, score: number, reasons: string[] }}
|
|
40
|
+
*/
|
|
41
|
+
function assessDifficulty({ prompt = '', fileHints = [], retrievalHit = null, explicitDifficulty = null } = {}) {
|
|
42
|
+
const reasons = [];
|
|
43
|
+
|
|
44
|
+
const hint = normalizeDifficultyHint(explicitDifficulty);
|
|
45
|
+
if (hint) {
|
|
46
|
+
return { difficulty: hint, score: hint === DIFFICULTY.HARD ? 1 : 0, reasons: [`explicit difficulty: ${explicitDifficulty}`] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let score = 0;
|
|
50
|
+
const text = String(prompt || '');
|
|
51
|
+
const files = Array.isArray(fileHints) ? fileHints.filter(Boolean) : [];
|
|
52
|
+
|
|
53
|
+
if (retrievalHit === false) { score += 2; reasons.push('unseen: no retrieval/memory match'); }
|
|
54
|
+
else if (retrievalHit === true) { score -= 1; reasons.push('seen: retrieval match available'); }
|
|
55
|
+
|
|
56
|
+
if (text.length >= HARD_PROMPT_CHARS) { score += 1; reasons.push(`long prompt (${text.length} chars)`); }
|
|
57
|
+
if (files.length >= HARD_FILE_COUNT) { score += 1; reasons.push(`multi-file (${files.length} hints)`); }
|
|
58
|
+
if (MULTI_STEP_RE.test(text)) { score += 2; reasons.push('multi-step / cross-cutting keywords'); }
|
|
59
|
+
|
|
60
|
+
const difficulty = score >= 2 ? DIFFICULTY.HARD : DIFFICULTY.SIMPLE;
|
|
61
|
+
return { difficulty, score, reasons };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Pick a model spec for a difficulty; falls back to local when no strong model. */
|
|
65
|
+
function routeModel({ difficulty = DIFFICULTY.SIMPLE, models = {} } = {}) {
|
|
66
|
+
const local = models.local || models.base || null;
|
|
67
|
+
const strong = models.strong || models.large || null;
|
|
68
|
+
if (difficulty === DIFFICULTY.HARD && strong) return strong;
|
|
69
|
+
return local || strong || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One-shot pre-run routing decision.
|
|
74
|
+
* @returns {{ model: object|null, difficulty: string, score: number, reasons: string[] }}
|
|
75
|
+
*/
|
|
76
|
+
function decideRoute({ prompt = '', fileHints = [], retrievalHit = null, explicitDifficulty = null, models = {} } = {}) {
|
|
77
|
+
const assessment = assessDifficulty({ prompt, fileHints, retrievalHit, explicitDifficulty });
|
|
78
|
+
const model = routeModel({ difficulty: assessment.difficulty, models });
|
|
79
|
+
const usedStrong = model && models.strong && model === models.strong;
|
|
80
|
+
return {
|
|
81
|
+
...assessment,
|
|
82
|
+
model,
|
|
83
|
+
routedTo: usedStrong ? 'strong' : 'local',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Runtime confidence escalation: should we abandon the local model mid-run for
|
|
89
|
+
* the strong one? Only past a turn budget, only when behavior is clearly failing,
|
|
90
|
+
* and only when a strong model exists.
|
|
91
|
+
* @returns {{ escalate: boolean, reason: string }}
|
|
92
|
+
*/
|
|
93
|
+
function shouldEscalate({ behavioral = {}, turn = 0, maxLocalTurns = 5, hasStrongModel = true } = {}) {
|
|
94
|
+
if (!hasStrongModel) return { escalate: false, reason: 'no_strong_model' };
|
|
95
|
+
if (turn < maxLocalTurns) return { escalate: false, reason: 'within_local_budget' };
|
|
96
|
+
|
|
97
|
+
const readLoop = Number(behavioral.readLoopRate || 0);
|
|
98
|
+
const decisive = Number(behavioral.decisiveActionRate || 0);
|
|
99
|
+
const valid = behavioral.validToolCallRate == null ? 1 : Number(behavioral.validToolCallRate);
|
|
100
|
+
|
|
101
|
+
const failing = [];
|
|
102
|
+
if (readLoop >= 0.5) failing.push('read-loop');
|
|
103
|
+
if (decisive <= 0.1) failing.push('no decisive action');
|
|
104
|
+
if (valid <= 0.5) failing.push('invalid tool calls');
|
|
105
|
+
|
|
106
|
+
if (failing.length) return { escalate: true, reason: `local model failing past budget: ${failing.join(', ')}` };
|
|
107
|
+
return { escalate: false, reason: 'making_progress' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
DIFFICULTY,
|
|
112
|
+
assessDifficulty,
|
|
113
|
+
routeModel,
|
|
114
|
+
decideRoute,
|
|
115
|
+
shouldEscalate,
|
|
116
|
+
};
|