agent-nuvira 3.5.6 → 3.5.9
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/.agents/skills/index.json +16 -0
- package/.agents/skills/motion/SKILL.md +27 -0
- package/README.md +56 -1
- package/dist/agents/agents/package-agent.d.ts +22 -0
- package/dist/agents/agents/package-agent.d.ts.map +1 -1
- package/dist/agents/agents/package-agent.js +92 -4
- package/dist/agents/agents/package-agent.js.map +1 -1
- package/dist/agents/release-preflight.d.ts +42 -5
- package/dist/agents/release-preflight.d.ts.map +1 -1
- package/dist/agents/release-preflight.js +82 -5
- package/dist/agents/release-preflight.js.map +1 -1
- package/dist/cli/chat.d.ts +76 -17
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +357 -127
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/code-map.d.ts +51 -0
- package/dist/cli/code-map.d.ts.map +1 -1
- package/dist/cli/code-map.js +174 -2
- package/dist/cli/code-map.js.map +1 -1
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +4 -1
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/doctor.d.ts +31 -2
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +143 -7
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/execute.js +5 -5
- package/dist/cli/execute.js.map +1 -1
- package/dist/cli/loop-executor.d.ts.map +1 -1
- package/dist/cli/loop-executor.js +80 -14
- package/dist/cli/loop-executor.js.map +1 -1
- package/dist/cli/parity.d.ts +12 -0
- package/dist/cli/parity.d.ts.map +1 -1
- package/dist/cli/parity.js +46 -1
- package/dist/cli/parity.js.map +1 -1
- package/dist/cli/tool-install-prompt.d.ts +2 -7
- package/dist/cli/tool-install-prompt.d.ts.map +1 -1
- package/dist/cli/tool-install-prompt.js +11 -28
- package/dist/cli/tool-install-prompt.js.map +1 -1
- package/dist/config/process-env.d.ts.map +1 -1
- package/dist/config/process-env.js +2 -2
- package/dist/config/process-env.js.map +1 -1
- package/dist/config/scratch-workspace.d.ts +134 -0
- package/dist/config/scratch-workspace.d.ts.map +1 -0
- package/dist/config/scratch-workspace.js +213 -0
- package/dist/config/scratch-workspace.js.map +1 -0
- package/dist/editing/types.d.ts +23 -1
- package/dist/editing/types.d.ts.map +1 -1
- package/dist/editing/types.js +51 -7
- package/dist/editing/types.js.map +1 -1
- package/dist/gateway/adapters.d.ts +24 -0
- package/dist/gateway/adapters.d.ts.map +1 -1
- package/dist/gateway/adapters.js +12 -0
- package/dist/gateway/adapters.js.map +1 -1
- package/dist/gateway/oversized-reply.d.ts +39 -0
- package/dist/gateway/oversized-reply.d.ts.map +1 -0
- package/dist/gateway/oversized-reply.js +54 -0
- package/dist/gateway/oversized-reply.js.map +1 -0
- package/dist/gateway/registry.d.ts +0 -10
- package/dist/gateway/registry.d.ts.map +1 -1
- package/dist/gateway/registry.js +148 -4
- package/dist/gateway/registry.js.map +1 -1
- package/dist/inference/route-resolver.d.ts +39 -0
- package/dist/inference/route-resolver.d.ts.map +1 -1
- package/dist/inference/route-resolver.js +51 -2
- package/dist/inference/route-resolver.js.map +1 -1
- package/dist/inference/thinking-replay.d.ts +86 -0
- package/dist/inference/thinking-replay.d.ts.map +1 -0
- package/dist/inference/thinking-replay.js +110 -0
- package/dist/inference/thinking-replay.js.map +1 -0
- package/dist/inference/tools.d.ts +9 -0
- package/dist/inference/tools.d.ts.map +1 -1
- package/dist/inference/tools.js +12 -1
- package/dist/inference/tools.js.map +1 -1
- package/dist/learning/assessment-protocol.d.ts +120 -0
- package/dist/learning/assessment-protocol.d.ts.map +1 -0
- package/dist/learning/assessment-protocol.js +236 -0
- package/dist/learning/assessment-protocol.js.map +1 -0
- package/dist/learning/assessment-turn-context.d.ts +54 -0
- package/dist/learning/assessment-turn-context.d.ts.map +1 -0
- package/dist/learning/assessment-turn-context.js +71 -0
- package/dist/learning/assessment-turn-context.js.map +1 -0
- package/dist/learning/exhaustion-report.d.ts +24 -1
- package/dist/learning/exhaustion-report.d.ts.map +1 -1
- package/dist/learning/exhaustion-report.js +18 -6
- package/dist/learning/exhaustion-report.js.map +1 -1
- package/dist/learning/perf-probe.d.ts +140 -0
- package/dist/learning/perf-probe.d.ts.map +1 -0
- package/dist/learning/perf-probe.js +316 -0
- package/dist/learning/perf-probe.js.map +1 -0
- package/dist/learning/quota-ledger.d.ts +24 -0
- package/dist/learning/quota-ledger.d.ts.map +1 -1
- package/dist/learning/quota-ledger.js +40 -0
- package/dist/learning/quota-ledger.js.map +1 -1
- package/dist/learning/runtime-dependencies.d.ts +183 -0
- package/dist/learning/runtime-dependencies.d.ts.map +1 -0
- package/dist/learning/runtime-dependencies.js +349 -0
- package/dist/learning/runtime-dependencies.js.map +1 -0
- package/dist/learning/self-check-discovery.d.ts +117 -0
- package/dist/learning/self-check-discovery.d.ts.map +1 -0
- package/dist/learning/self-check-discovery.js +334 -0
- package/dist/learning/self-check-discovery.js.map +1 -0
- package/dist/learning/skill-store.d.ts.map +1 -1
- package/dist/learning/skill-store.js +6 -5
- package/dist/learning/skill-store.js.map +1 -1
- package/dist/skills/bundled-skills-phase3.d.ts +14 -0
- package/dist/skills/bundled-skills-phase3.d.ts.map +1 -1
- package/dist/skills/bundled-skills-phase3.js +50 -0
- package/dist/skills/bundled-skills-phase3.js.map +1 -1
- package/dist/tools/child-agent-runtime.js +19 -1
- package/dist/tools/child-agent-runtime.js.map +1 -1
- package/dist/tools/credentials-tool.d.ts.map +1 -1
- package/dist/tools/credentials-tool.js +4 -1
- package/dist/tools/credentials-tool.js.map +1 -1
- package/dist/tools/loop-project-context.d.ts +7 -0
- package/dist/tools/loop-project-context.d.ts.map +1 -1
- package/dist/tools/loop-project-context.js +44 -0
- package/dist/tools/loop-project-context.js.map +1 -1
- package/dist/tools/modality/motion-presets.d.ts +62 -0
- package/dist/tools/modality/motion-presets.d.ts.map +1 -0
- package/dist/tools/modality/motion-presets.js +399 -0
- package/dist/tools/modality/motion-presets.js.map +1 -0
- package/dist/tools/modality/motion-tool.d.ts +62 -0
- package/dist/tools/modality/motion-tool.d.ts.map +1 -0
- package/dist/tools/modality/motion-tool.js +220 -0
- package/dist/tools/modality/motion-tool.js.map +1 -0
- package/dist/tools/modality/motion.d.ts +298 -0
- package/dist/tools/modality/motion.d.ts.map +1 -0
- package/dist/tools/modality/motion.js +514 -0
- package/dist/tools/modality/motion.js.map +1 -0
- package/dist/tools/pipeline-tool.d.ts.map +1 -1
- package/dist/tools/pipeline-tool.js +19 -1
- package/dist/tools/pipeline-tool.js.map +1 -1
- package/dist/tools/plan-store.d.ts +90 -6
- package/dist/tools/plan-store.d.ts.map +1 -1
- package/dist/tools/plan-store.js +140 -13
- package/dist/tools/plan-store.js.map +1 -1
- package/dist/tools/publish-tool.d.ts.map +1 -1
- package/dist/tools/publish-tool.js +9 -0
- package/dist/tools/publish-tool.js.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +100 -3
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/tool-loop.d.ts +17 -0
- package/dist/tools/tool-loop.d.ts.map +1 -1
- package/dist/tools/tool-loop.js +70 -17
- package/dist/tools/tool-loop.js.map +1 -1
- package/dist/tools/toolsets.js +2 -2
- package/dist/tools/toolsets.js.map +1 -1
- package/dist/utils/workspace-need.d.ts +84 -0
- package/dist/utils/workspace-need.d.ts.map +1 -0
- package/dist/utils/workspace-need.js +227 -0
- package/dist/utils/workspace-need.js.map +1 -0
- package/dist/web-dashboard/chat-console.d.ts +22 -0
- package/dist/web-dashboard/chat-console.d.ts.map +1 -1
- package/dist/web-dashboard/chat-console.js +75 -11
- package/dist/web-dashboard/chat-console.js.map +1 -1
- package/dist/web-dashboard/hub-data.d.ts +9 -1
- package/dist/web-dashboard/hub-data.d.ts.map +1 -1
- package/dist/web-dashboard/hub-data.js +2 -0
- package/dist/web-dashboard/hub-data.js.map +1 -1
- package/dist/web-dashboard/project-context.d.ts +1 -1
- package/dist/web-dashboard/project-context.d.ts.map +1 -1
- package/dist/web-dashboard/project-context.js +13 -1
- package/dist/web-dashboard/project-context.js.map +1 -1
- package/dist/web-dashboard/server.d.ts +33 -0
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +229 -24
- package/dist/web-dashboard/server.js.map +1 -1
- package/dist/web-dashboard/src/types.d.ts +46 -0
- package/dist/web-dashboard/src/types.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.d.ts +20 -21
- package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.js +20 -153
- package/dist/web-dashboard/workspace-guard.js.map +1 -1
- package/dist/web-dashboard/workspace-resolution.d.ts +23 -5
- package/dist/web-dashboard/workspace-resolution.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-resolution.js +22 -4
- package/dist/web-dashboard/workspace-resolution.js.map +1 -1
- package/package.json +1 -1
- package/src/web-dashboard/public/assets/index-CGYIlLpO.js +236 -0
- package/src/web-dashboard/public/assets/index-CGYIlLpO.js.map +1 -0
- package/src/web-dashboard/public/assets/{index-DeSsNU94.css → index-CzRU84ET.css} +1 -1
- package/src/web-dashboard/public/index.html +2 -2
- package/src/web-dashboard/public/assets/index-Do_O67Kv.js +0 -236
- package/src/web-dashboard/public/assets/index-Do_O67Kv.js.map +0 -1
|
@@ -25,12 +25,13 @@ import { loadEnv } from '../utils/env.js';
|
|
|
25
25
|
import { ConfigManager } from '../config/manager.js';
|
|
26
26
|
import { parseRuntimeSpec } from '../mcp/runtime.js';
|
|
27
27
|
import { runAllChecks } from '../cli/doctor.js';
|
|
28
|
+
import { dependencySummary, probeRuntimeDependencies, } from '../learning/runtime-dependencies.js';
|
|
28
29
|
import { getAutoRouter } from '../learning/auto-router.js';
|
|
29
30
|
import { classifyEntries, } from '../learning/cost-classification.js';
|
|
30
31
|
import { acceptanceSummary, collectLabelledTurns, serializeLabelledTurns, parseCorpusText, detectCorpusFormat, mergeImportedLabels, } from '../learning/acceptance-model.js';
|
|
31
32
|
import { readDecisions, recallDecisions, reviseDecision } from '../learning/decision-log.js';
|
|
32
33
|
import { degradedCallsFromTraces } from '../learning/reasoning-trace.js';
|
|
33
|
-
import { withStrictModel } from '../inference/route-resolver.js';
|
|
34
|
+
import { strictPinPolicy, withStrictModel } from '../inference/route-resolver.js';
|
|
34
35
|
import { readRecallHits } from '../context/session-recall.js';
|
|
35
36
|
import { getRouterPromotion } from '../learning/router-promotion.js';
|
|
36
37
|
import { AUTH_CLEAR_THRESHOLD } from '../learning/key-hygiene.js';
|
|
@@ -55,6 +56,7 @@ import { isCuratedProcessEnvVar, normalizeProcessEnvValue, } from '../config/pro
|
|
|
55
56
|
import { FRESH_DAYS, FRESHNESS_COPY, REACHABILITY_COPY, classifyFreshness, classifyReachability, } from '../learning/model-reachability.js';
|
|
56
57
|
import { VERIFY_BACKLOG_DEFAULT_PER_RUN, VERIFY_BACKLOG_MAX_PER_RUN, getVerifyBacklogState, startVerifyBacklogRun, } from '../learning/model-verify-job.js';
|
|
57
58
|
import { setToolsetEnabled } from '../tools/toolsets.js';
|
|
59
|
+
import { ArtifactStore } from '../tools/artifact-store.js';
|
|
58
60
|
import { setSkillEnabled } from '../learning/hub-skill-catalog.js';
|
|
59
61
|
// I11 — dashboard channel send-test: the same registry/adapters `nuvira gateway
|
|
60
62
|
// send` uses, so a Channels-tab test message behaves identically to the CLI.
|
|
@@ -68,6 +70,7 @@ import { ChannelDirectory, PLATFORM_ENV_VARS, platformConfigVars } from '../gate
|
|
|
68
70
|
import { applyEnvToProcess, configurablePlatforms, envVarState, platformConfigStatus, platformEnvVarMeta, redactValue, writeEnvFile, } from '../gateway/platform-config.js';
|
|
69
71
|
import { SERVICE_CATALOG, getServiceDefinition, } from '../config/service-catalog.js';
|
|
70
72
|
import { probeService } from '../config/service-probe.js';
|
|
73
|
+
import { pruneScratchWorkspaces } from '../config/scratch-workspace.js';
|
|
71
74
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
72
75
|
const PORT = parseInt(envBuff('DASHBOARD_PORT') || '3030', 10);
|
|
73
76
|
const HOST = envBuff('DASHBOARD_HOST') || '127.0.0.1';
|
|
@@ -119,6 +122,40 @@ let alwaysWatchQuota = false;
|
|
|
119
122
|
* loadApiKeysFromConfig uses). Best-effort — a missing/corrupt config just
|
|
120
123
|
* keeps the default (false = arm-on-connect only).
|
|
121
124
|
*/
|
|
125
|
+
/**
|
|
126
|
+
* The content type for an artifact file: the recorded mime when there is one,
|
|
127
|
+
* otherwise the extension. Video and image types are the ones the panel renders
|
|
128
|
+
* inline, so a wrong type is a blank frame rather than a failed download.
|
|
129
|
+
*/
|
|
130
|
+
function artifactFileMime(file, recorded) {
|
|
131
|
+
if (recorded && /^[a-z]+\/[a-z0-9.+-]+$/i.test(recorded))
|
|
132
|
+
return recorded;
|
|
133
|
+
const ext = (file.split('.').pop() ?? '').toLowerCase();
|
|
134
|
+
const byExt = {
|
|
135
|
+
mp4: 'video/mp4',
|
|
136
|
+
webm: 'video/webm',
|
|
137
|
+
mov: 'video/quicktime',
|
|
138
|
+
mkv: 'video/x-matroska',
|
|
139
|
+
gif: 'image/gif',
|
|
140
|
+
png: 'image/png',
|
|
141
|
+
jpg: 'image/jpeg',
|
|
142
|
+
jpeg: 'image/jpeg',
|
|
143
|
+
webp: 'image/webp',
|
|
144
|
+
svg: 'image/svg+xml',
|
|
145
|
+
mp3: 'audio/mpeg',
|
|
146
|
+
wav: 'audio/wav',
|
|
147
|
+
m4a: 'audio/mp4',
|
|
148
|
+
srt: 'application/x-subrip',
|
|
149
|
+
vtt: 'text/vtt',
|
|
150
|
+
json: 'application/json',
|
|
151
|
+
md: 'text/markdown',
|
|
152
|
+
txt: 'text/plain',
|
|
153
|
+
log: 'text/plain',
|
|
154
|
+
csv: 'text/csv',
|
|
155
|
+
pdf: 'application/pdf',
|
|
156
|
+
};
|
|
157
|
+
return byExt[ext] ?? 'application/octet-stream';
|
|
158
|
+
}
|
|
122
159
|
function loadAlwaysWatchQuotaFlag() {
|
|
123
160
|
try {
|
|
124
161
|
const configPath = resolveBuffConfigPath();
|
|
@@ -2807,14 +2844,42 @@ function buildServiceRow(def, canReadValues) {
|
|
|
2807
2844
|
envVars,
|
|
2808
2845
|
};
|
|
2809
2846
|
}
|
|
2847
|
+
/**
|
|
2848
|
+
* One runtime tool as the panel's payload. Exported for the server test, which
|
|
2849
|
+
* asserts the panel is fed the same list `nuvira doctor` prints.
|
|
2850
|
+
*/
|
|
2851
|
+
export function runtimeToolSummary(status) {
|
|
2852
|
+
return {
|
|
2853
|
+
id: status.dependency.id,
|
|
2854
|
+
label: status.dependency.label,
|
|
2855
|
+
need: status.dependency.need,
|
|
2856
|
+
present: status.present,
|
|
2857
|
+
enables: status.dependency.enables,
|
|
2858
|
+
neededBy: status.dependency.neededBy,
|
|
2859
|
+
// A tool the machine ALREADY has gets no command on the wire, the same way
|
|
2860
|
+
// `nuvira doctor` prints a fix only for what is missing: sending one anyway
|
|
2861
|
+
// invites a user to reinstall something that is working.
|
|
2862
|
+
installCommand: status.present ? null : status.installCommand,
|
|
2863
|
+
note: status.dependency.note,
|
|
2864
|
+
needsHumanInput: status.needsHumanInput,
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2810
2867
|
/** Run ALL state checks — the dashboard command-runner payload. */
|
|
2811
2868
|
async function runAdminChecks() {
|
|
2812
2869
|
const configManager = new ConfigManager();
|
|
2813
2870
|
const { system, enterprise } = await runAllChecks(configManager);
|
|
2871
|
+
// The SAME declaration the CLI's `nuvira doctor`, the `--fix` installer and the
|
|
2872
|
+
// setup scripts read, probed once for this payload. The rows are also in
|
|
2873
|
+
// `system` (one check each) because a pass/warn verdict is what a check list
|
|
2874
|
+
// is for; this is the actionable half — the command to copy, the
|
|
2875
|
+
// recommended/optional split, and the caveat.
|
|
2876
|
+
const statuses = probeRuntimeDependencies();
|
|
2814
2877
|
return {
|
|
2815
2878
|
system,
|
|
2816
2879
|
enterprise,
|
|
2817
2880
|
providers: buildAdminProviderSummary(configManager),
|
|
2881
|
+
runtimeTools: statuses.map(runtimeToolSummary),
|
|
2882
|
+
runtimeToolsSummary: dependencySummary(statuses),
|
|
2818
2883
|
serverTime: Date.now(),
|
|
2819
2884
|
};
|
|
2820
2885
|
}
|
|
@@ -3312,6 +3377,52 @@ function handleRequest(req, res) {
|
|
|
3312
3377
|
})();
|
|
3313
3378
|
return;
|
|
3314
3379
|
}
|
|
3380
|
+
// ── An artifact's BYTES — how the Artifacts tab shows a rendered video rather
|
|
3381
|
+
// than a filename. The path is looked up in OUR OWN artifact store by (session,
|
|
3382
|
+
// artifact id) and is never taken from the query string, so a caller cannot ask
|
|
3383
|
+
// this endpoint for an arbitrary file; a bad pair is a 404, not a read.
|
|
3384
|
+
if (pathname === '/api/artifact-file') {
|
|
3385
|
+
const sessionId = url.searchParams.get('session') ?? '';
|
|
3386
|
+
const artifactId = url.searchParams.get('artifact') ?? '';
|
|
3387
|
+
const artifact = artifactId
|
|
3388
|
+
? new ArtifactStore().read(sessionId).find((a) => a.id === artifactId)
|
|
3389
|
+
: undefined;
|
|
3390
|
+
if (!artifact || !artifact.path || !existsSync(artifact.path) || !statSync(artifact.path).isFile()) {
|
|
3391
|
+
writeJson(res, 404, { ok: false, error: 'No such artifact file.' });
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
const size = statSync(artifact.path).size;
|
|
3395
|
+
const type = artifactFileMime(artifact.path, artifact.mime);
|
|
3396
|
+
// Range support, because a <video> that cannot seek cannot be reviewed: a
|
|
3397
|
+
// player asks for byte ranges the moment the user drags the scrubber.
|
|
3398
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? '');
|
|
3399
|
+
if (range) {
|
|
3400
|
+
const start = range[1] ? Number(range[1]) : 0;
|
|
3401
|
+
const end = range[2] ? Math.min(Number(range[2]), size - 1) : size - 1;
|
|
3402
|
+
if (start >= size || end < start) {
|
|
3403
|
+
res.writeHead(416, { 'Content-Range': `bytes */${size}` });
|
|
3404
|
+
res.end();
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
res.writeHead(206, {
|
|
3408
|
+
'Content-Type': type,
|
|
3409
|
+
'Content-Length': String(end - start + 1),
|
|
3410
|
+
'Content-Range': `bytes ${start}-${end}/${size}`,
|
|
3411
|
+
'Accept-Ranges': 'bytes',
|
|
3412
|
+
'Cache-Control': 'no-store',
|
|
3413
|
+
});
|
|
3414
|
+
createReadStream(artifact.path, { start, end }).pipe(res);
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
res.writeHead(200, {
|
|
3418
|
+
'Content-Type': type,
|
|
3419
|
+
'Content-Length': String(size),
|
|
3420
|
+
'Accept-Ranges': 'bytes',
|
|
3421
|
+
'Cache-Control': 'no-store',
|
|
3422
|
+
});
|
|
3423
|
+
createReadStream(artifact.path).pipe(res);
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3315
3426
|
if (pathname === '/api/benchmarks') {
|
|
3316
3427
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3317
3428
|
res.end(JSON.stringify(readBenchmarkData()));
|
|
@@ -3391,7 +3502,17 @@ function handleRequest(req, res) {
|
|
|
3391
3502
|
res.end(JSON.stringify(payload));
|
|
3392
3503
|
}).catch((err) => {
|
|
3393
3504
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3394
|
-
res.end(JSON.stringify({
|
|
3505
|
+
res.end(JSON.stringify({
|
|
3506
|
+
error: String(err),
|
|
3507
|
+
system: [],
|
|
3508
|
+
enterprise: [],
|
|
3509
|
+
providers: [],
|
|
3510
|
+
// An empty list, not a missing one: the panel must be able to say
|
|
3511
|
+
// "nothing reported" on a failed run rather than crash on undefined.
|
|
3512
|
+
runtimeTools: [],
|
|
3513
|
+
runtimeToolsSummary: 'unavailable — the checks run failed',
|
|
3514
|
+
serverTime: Date.now(),
|
|
3515
|
+
}));
|
|
3395
3516
|
});
|
|
3396
3517
|
return;
|
|
3397
3518
|
}
|
|
@@ -7533,7 +7654,24 @@ function handleRequest(req, res) {
|
|
|
7533
7654
|
// only" switch). Scoped to the turn with AsyncLocalStorage rather than a
|
|
7534
7655
|
// process env write, because two sessions can run concurrently and a
|
|
7535
7656
|
// global mutation would leak one user's pin into the other's turn.
|
|
7536
|
-
|
|
7657
|
+
//
|
|
7658
|
+
// READ AS A TRI-STATE (2026-10-16) — same three cases, same reasoning, as
|
|
7659
|
+
// `worktree`/`resume` below. `true` holds the pin; `false` is this turn's
|
|
7660
|
+
// EXPLICIT permission to substitute; ABSENT defers to the pin's own policy,
|
|
7661
|
+
// where a request that NAMED a provider+model HOLDS (the pair `pinOf`
|
|
7662
|
+
// records) and an Auto turn has no pair to hold.
|
|
7663
|
+
//
|
|
7664
|
+
// Collapsing absent into `false` — the previous `body?.strict === true` —
|
|
7665
|
+
// was wrong on both sides. A pinned API request that never mentioned the
|
|
7666
|
+
// flag was substituted by default, i.e. a pin read as a preference, which
|
|
7667
|
+
// is the contract `strictPinPolicy` exists to end. And because the scope
|
|
7668
|
+
// set here OUTRANKS the environment inside `withStrictModel`, a dashboard
|
|
7669
|
+
// deployment running with `NUVIRA_STRICT_MODEL=1` was silently un-strict
|
|
7670
|
+
// for every chat turn that left the flag out — the operator's own switch,
|
|
7671
|
+
// quietly ignored.
|
|
7672
|
+
const strict = typeof body?.strict === 'boolean'
|
|
7673
|
+
? body.strict
|
|
7674
|
+
: strictPinPolicy(typeof provider === 'string' && typeof model === 'string');
|
|
7537
7675
|
// WS5 (#27) — per-turn isolation and resume, from the GUI.
|
|
7538
7676
|
//
|
|
7539
7677
|
// Read as a strict TRI-state, because the three cases are genuinely
|
|
@@ -7598,13 +7736,51 @@ function handleRequest(req, res) {
|
|
|
7598
7736
|
*/
|
|
7599
7737
|
const configuredCwd = resolveDashboardCwd();
|
|
7600
7738
|
const { resolveTurnWorkspace, formatWorkspaceNoticeText } = await import('./workspace-resolution.js');
|
|
7739
|
+
const { workspaceNeedForMissingFolder } = await import('./workspace-guard.js');
|
|
7601
7740
|
const sessionProjectPath = chatConsole.get(sessionId)?.projectPath;
|
|
7602
|
-
|
|
7741
|
+
/**
|
|
7742
|
+
* The same ladder, asked twice when the last rung is needed.
|
|
7743
|
+
*
|
|
7744
|
+
* WHY TWO PASSES. The scratch folder is a real directory on disk, so it is
|
|
7745
|
+
* only created when the ladder has actually RUN OUT (nothing attached,
|
|
7746
|
+
* nothing named in the message, no configured default) AND the ask needs
|
|
7747
|
+
* somewhere to PUT new work. Creating it eagerly would leave an empty
|
|
7748
|
+
* temporary folder behind on every turn that already had a folder, and
|
|
7749
|
+
* handing it to a READ ask would be the kuttaaddon defect again — an empty
|
|
7750
|
+
* folder cannot answer "assess this project".
|
|
7751
|
+
*/
|
|
7752
|
+
const resolveWorkspaceNow = (scratchPath) => resolveTurnWorkspace({
|
|
7603
7753
|
...(attachedPath ? { attachedPath } : {}),
|
|
7604
7754
|
...(sessionProjectPath ? { sessionPath: sessionProjectPath } : {}),
|
|
7605
7755
|
...(configuredCwd ? { configuredCwd } : {}),
|
|
7606
7756
|
message,
|
|
7757
|
+
...(scratchPath ? { scratchPath } : {}),
|
|
7607
7758
|
});
|
|
7759
|
+
let workspace = resolveWorkspaceNow();
|
|
7760
|
+
/**
|
|
7761
|
+
* What this ask needs a folder FOR — `'produce'` is the case a temporary
|
|
7762
|
+
* folder can serve, `'read'` is the case it cannot (and keeps the folder
|
|
7763
|
+
* prompt), `null` is plain chat, which needs none.
|
|
7764
|
+
*/
|
|
7765
|
+
const missingWorkspaceNeed = workspace.path ? null : workspaceNeedForMissingFolder(message);
|
|
7766
|
+
if (missingWorkspaceNeed === 'produce') {
|
|
7767
|
+
// The scratch folder is offered through the SAME rule the gateway uses
|
|
7768
|
+
// (`scratchWorkspaceForTurn`), so the two surfaces cannot disagree about
|
|
7769
|
+
// when a temporary folder is the right answer — in particular about an
|
|
7770
|
+
// ISOLATED turn, which a scratch folder can never serve: it has no base
|
|
7771
|
+
// commit to diff against. There, this turn has no folder at all, so it
|
|
7772
|
+
// falls through to the folder request below rather than isolating a
|
|
7773
|
+
// directory the user never chose.
|
|
7774
|
+
const { scratchWorkspaceForTurn } = await import('../config/scratch-workspace.js');
|
|
7775
|
+
const { resolveIsolationRequest } = await import('../tools/worktree.js');
|
|
7776
|
+
const scratch = scratchWorkspaceForTurn({
|
|
7777
|
+
key: sessionId,
|
|
7778
|
+
need: missingWorkspaceNeed,
|
|
7779
|
+
isolationAsked: resolveIsolationRequest({ worktree }).asked,
|
|
7780
|
+
});
|
|
7781
|
+
if (scratch)
|
|
7782
|
+
workspace = resolveWorkspaceNow(scratch);
|
|
7783
|
+
}
|
|
7608
7784
|
if (workspace.path) {
|
|
7609
7785
|
projectPath = workspace.path;
|
|
7610
7786
|
const bundle = getProjectBundle(workspace.path);
|
|
@@ -7622,24 +7798,23 @@ function handleRequest(req, res) {
|
|
|
7622
7798
|
* needs no caption.
|
|
7623
7799
|
*/
|
|
7624
7800
|
const workspaceNotice = formatWorkspaceNoticeText(workspace);
|
|
7625
|
-
//
|
|
7626
|
-
//
|
|
7627
|
-
//
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
}
|
|
7801
|
+
// Still no workspace: either the ask must READ or CHANGE work that already
|
|
7802
|
+
// exists (a temporary folder holds none of it), or the scratch folder could
|
|
7803
|
+
// not be created. Both are the case for a folder request rather than an
|
|
7804
|
+
// answer about the wrong tree. General chat — `null` — is unaffected.
|
|
7805
|
+
if (!workspace.path && missingWorkspaceNeed !== null) {
|
|
7806
|
+
const { projectAttachmentPrompt, projectAttachmentFollowups } = await import('./workspace-guard.js');
|
|
7807
|
+
writeJson(res, 200, {
|
|
7808
|
+
ok: true,
|
|
7809
|
+
sessionId,
|
|
7810
|
+
content: projectAttachmentPrompt(),
|
|
7811
|
+
followups: projectAttachmentFollowups(),
|
|
7812
|
+
provider: null,
|
|
7813
|
+
model: null,
|
|
7814
|
+
generationFailed: false,
|
|
7815
|
+
needsProject: true,
|
|
7816
|
+
});
|
|
7817
|
+
return;
|
|
7643
7818
|
}
|
|
7644
7819
|
// P4 (revised) — a client DISCONNECT no longer cancels the turn. It used
|
|
7645
7820
|
// to: the server treated `res` close as an abort, which meant a page
|
|
@@ -7677,6 +7852,14 @@ function handleRequest(req, res) {
|
|
|
7677
7852
|
const result = await withStrictModel(strict, () => chatConsole.answer(sessionId, message, {
|
|
7678
7853
|
provider,
|
|
7679
7854
|
model,
|
|
7855
|
+
// P8b — the STRICT half of the pin rides into the console so the
|
|
7856
|
+
// CONVERSATION can remember it. `withStrictModel` above scopes it to
|
|
7857
|
+
// this turn, but a conversation whose lock is forgotten between turns
|
|
7858
|
+
// is a pin the user cannot actually rely on: measured live
|
|
7859
|
+
// (trace-1792068695325-42wb1j) the pin survived a reload and the lock
|
|
7860
|
+
// did not, so the engine handed the turn to another model with the
|
|
7861
|
+
// user's "this model only" instruction never in force.
|
|
7862
|
+
strict,
|
|
7680
7863
|
projectContext,
|
|
7681
7864
|
projectPath,
|
|
7682
7865
|
// Cluster G — carry the "no folder attached" state into the turn so a
|
|
@@ -7742,10 +7925,20 @@ function handleRequest(req, res) {
|
|
|
7742
7925
|
`your pinned ${requested} is ${pinnedClass} class, and this turn ran on ${served} (${servedClass} class).`;
|
|
7743
7926
|
}
|
|
7744
7927
|
else if (substituted) {
|
|
7745
|
-
|
|
7928
|
+
// Reachable now only when the turn EXPLICITLY allowed a substitute
|
|
7929
|
+
// (`strict: false`, the chat's lock switched off) or the deployment set
|
|
7930
|
+
// `NUVIRA_STRICT_MODEL=0`, because a named pair holds by default. So the
|
|
7931
|
+
// instruction names the two controls that actually release it, instead
|
|
7932
|
+
// of telling the user to enable something that is already the default.
|
|
7933
|
+
routingNotice = `Auto routing took over: this turn ran on ${served} because your pinned ${requested} was not available. To work with ${requested} only, hold the pin: turn the chat's lock back on, or set NUVIRA_STRICT_MODEL=1.`;
|
|
7746
7934
|
}
|
|
7747
7935
|
else if (result.generationFailed === true) {
|
|
7748
|
-
|
|
7936
|
+
// The turn ran on the pair the user pinned and stopped there. Saying
|
|
7937
|
+
// "auto routing may hand the work to another model" described the old
|
|
7938
|
+
// behaviour (a pinned turn followed the health ranking the moment its
|
|
7939
|
+
// pair returned an error) and would now be a lie: a pin waits on itself
|
|
7940
|
+
// and then asks. See `pinnedEscalation`.
|
|
7941
|
+
routingNotice = `Your pinned ${requested} did not answer, and nothing was sent to another model — the turn stopped instead of substituting. Retry it, or switch models.`;
|
|
7749
7942
|
}
|
|
7750
7943
|
}
|
|
7751
7944
|
// Did the offer we are about to send get a mechanism behind it? The client
|
|
@@ -8348,6 +8541,18 @@ export function createDashboardServer(opts) {
|
|
|
8348
8541
|
handle.ipv6Twin = undefined;
|
|
8349
8542
|
}
|
|
8350
8543
|
}
|
|
8544
|
+
// Scratch workspaces are temporary BY DEFINITION, so the ones nobody came back
|
|
8545
|
+
// to are cleaned on boot rather than accumulating in the user's home for the
|
|
8546
|
+
// life of the install (see `pruneScratchWorkspaces`). Best-effort: a failed
|
|
8547
|
+
// prune must never stop the server from coming up.
|
|
8548
|
+
try {
|
|
8549
|
+
const removed = pruneScratchWorkspaces();
|
|
8550
|
+
if (removed > 0)
|
|
8551
|
+
console.log(` 🧹 Removed ${removed} expired scratch workspace(s)`);
|
|
8552
|
+
}
|
|
8553
|
+
catch {
|
|
8554
|
+
/* best-effort */
|
|
8555
|
+
}
|
|
8351
8556
|
server.listen(bindPort, bindHost, () => {
|
|
8352
8557
|
console.log(`\n 🌐 Agent-Nuvira Dashboard`);
|
|
8353
8558
|
console.log(` ─────────────────────────`);
|