agent-nuvira 3.3.9 → 3.3.11
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/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +6 -0
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +27 -17
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/config.d.ts +28 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +155 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/eval.d.ts +8 -0
- package/dist/cli/eval.d.ts.map +1 -1
- package/dist/cli/eval.js +64 -0
- package/dist/cli/eval.js.map +1 -1
- package/dist/cli/loop-executor.d.ts +13 -0
- package/dist/cli/loop-executor.d.ts.map +1 -1
- package/dist/cli/loop-executor.js +22 -18
- package/dist/cli/loop-executor.js.map +1 -1
- package/dist/config/capability-mode.d.ts +122 -0
- package/dist/config/capability-mode.d.ts.map +1 -0
- package/dist/config/capability-mode.js +132 -0
- package/dist/config/capability-mode.js.map +1 -0
- package/dist/config/limits.d.ts +47 -0
- package/dist/config/limits.d.ts.map +1 -0
- package/dist/config/limits.js +64 -0
- package/dist/config/limits.js.map +1 -0
- package/dist/config/process-env.d.ts.map +1 -1
- package/dist/config/process-env.js +44 -0
- package/dist/config/process-env.js.map +1 -1
- package/dist/config/types.d.ts +77 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/work-digest.d.ts +46 -0
- package/dist/config/work-digest.d.ts.map +1 -0
- package/dist/config/work-digest.js +80 -0
- package/dist/config/work-digest.js.map +1 -0
- package/dist/gateway/inbound-media.d.ts +18 -0
- package/dist/gateway/inbound-media.d.ts.map +1 -1
- package/dist/gateway/inbound-media.js +96 -2
- package/dist/gateway/inbound-media.js.map +1 -1
- package/dist/inference/anthropic-adapter.d.ts +6 -0
- package/dist/inference/anthropic-adapter.d.ts.map +1 -1
- package/dist/inference/anthropic-adapter.js +79 -8
- package/dist/inference/anthropic-adapter.js.map +1 -1
- package/dist/inference/gemini-adapter.d.ts +6 -0
- package/dist/inference/gemini-adapter.d.ts.map +1 -1
- package/dist/inference/gemini-adapter.js +73 -8
- package/dist/inference/gemini-adapter.js.map +1 -1
- package/dist/inference/groq-adapter.d.ts +3 -0
- package/dist/inference/groq-adapter.d.ts.map +1 -1
- package/dist/inference/groq-adapter.js +83 -40
- package/dist/inference/groq-adapter.js.map +1 -1
- package/dist/inference/interface.d.ts +7 -0
- package/dist/inference/interface.d.ts.map +1 -1
- package/dist/inference/model-probe.d.ts +17 -0
- package/dist/inference/model-probe.d.ts.map +1 -1
- package/dist/inference/model-probe.js +58 -1
- package/dist/inference/model-probe.js.map +1 -1
- package/dist/inference/model-validator.d.ts +16 -1
- package/dist/inference/model-validator.d.ts.map +1 -1
- package/dist/inference/model-validator.js +83 -2
- package/dist/inference/model-validator.js.map +1 -1
- package/dist/inference/nim-adapter.js +4 -4
- package/dist/inference/nim-adapter.js.map +1 -1
- package/dist/inference/openai-compat-adapter.d.ts +9 -0
- package/dist/inference/openai-compat-adapter.d.ts.map +1 -1
- package/dist/inference/openai-compat-adapter.js +101 -51
- package/dist/inference/openai-compat-adapter.js.map +1 -1
- package/dist/inference/openrouter-adapter.d.ts +3 -0
- package/dist/inference/openrouter-adapter.d.ts.map +1 -1
- package/dist/inference/openrouter-adapter.js +134 -57
- package/dist/inference/openrouter-adapter.js.map +1 -1
- package/dist/inference/reasoning-effort.d.ts +130 -0
- package/dist/inference/reasoning-effort.d.ts.map +1 -0
- package/dist/inference/reasoning-effort.js +237 -0
- package/dist/inference/reasoning-effort.js.map +1 -0
- package/dist/inference/route-resolver.d.ts +7 -0
- package/dist/inference/route-resolver.d.ts.map +1 -1
- package/dist/inference/route-resolver.js +2 -1
- package/dist/inference/route-resolver.js.map +1 -1
- package/dist/inference/sse.d.ts +6 -0
- package/dist/inference/sse.d.ts.map +1 -1
- package/dist/inference/sse.js +1 -0
- package/dist/inference/sse.js.map +1 -1
- package/dist/inference/tools.d.ts +13 -0
- package/dist/inference/tools.d.ts.map +1 -1
- package/dist/inference/tools.js +20 -2
- package/dist/inference/tools.js.map +1 -1
- package/dist/learning/autonomy-policy.d.ts +8 -0
- package/dist/learning/autonomy-policy.d.ts.map +1 -1
- package/dist/learning/autonomy-policy.js +34 -0
- package/dist/learning/autonomy-policy.js.map +1 -1
- package/dist/learning/capability-parity.d.ts +108 -0
- package/dist/learning/capability-parity.d.ts.map +1 -0
- package/dist/learning/capability-parity.js +154 -0
- package/dist/learning/capability-parity.js.map +1 -0
- package/dist/learning/cost-tracker.d.ts +28 -4
- package/dist/learning/cost-tracker.d.ts.map +1 -1
- package/dist/learning/cost-tracker.js +58 -8
- package/dist/learning/cost-tracker.js.map +1 -1
- package/dist/learning/eval-framework.d.ts +17 -0
- package/dist/learning/eval-framework.d.ts.map +1 -1
- package/dist/learning/eval-framework.js +63 -2
- package/dist/learning/eval-framework.js.map +1 -1
- package/dist/learning/model-capability.d.ts +94 -0
- package/dist/learning/model-capability.d.ts.map +1 -0
- package/dist/learning/model-capability.js +172 -0
- package/dist/learning/model-capability.js.map +1 -0
- package/dist/learning/model-registry.d.ts +33 -0
- package/dist/learning/model-registry.d.ts.map +1 -1
- package/dist/learning/model-registry.js +79 -0
- package/dist/learning/model-registry.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +1 -1
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/learning/resolve-options.d.ts.map +1 -1
- package/dist/learning/resolve-options.js +16 -3
- package/dist/learning/resolve-options.js.map +1 -1
- package/dist/learning/run-trace.d.ts +68 -0
- package/dist/learning/run-trace.d.ts.map +1 -1
- package/dist/learning/run-trace.js +101 -0
- package/dist/learning/run-trace.js.map +1 -1
- package/dist/tools/loop-skill-hint.d.ts +27 -0
- package/dist/tools/loop-skill-hint.d.ts.map +1 -1
- package/dist/tools/loop-skill-hint.js +189 -5
- package/dist/tools/loop-skill-hint.js.map +1 -1
- package/dist/tools/read-extract.d.ts +9 -3
- package/dist/tools/read-extract.d.ts.map +1 -1
- package/dist/tools/read-extract.js +70 -25
- package/dist/tools/read-extract.js.map +1 -1
- package/dist/tools/tool-loop.d.ts +120 -0
- package/dist/tools/tool-loop.d.ts.map +1 -1
- package/dist/tools/tool-loop.js +447 -5
- package/dist/tools/tool-loop.js.map +1 -1
- package/dist/web-dashboard/attachment-extract.d.ts +8 -1
- package/dist/web-dashboard/attachment-extract.d.ts.map +1 -1
- package/dist/web-dashboard/attachment-extract.js +14 -3
- package/dist/web-dashboard/attachment-extract.js.map +1 -1
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +160 -21
- package/dist/web-dashboard/server.js.map +1 -1
- package/dist/web-dashboard/src/types.d.ts +26 -0
- package/dist/web-dashboard/src/types.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.js +44 -4
- package/dist/web-dashboard/workspace-guard.js.map +1 -1
- package/package.json +1 -1
- package/src/web-dashboard/public/assets/index-BdKf5Xw2.js +207 -0
- package/src/web-dashboard/public/assets/index-BdKf5Xw2.js.map +1 -0
- package/src/web-dashboard/public/assets/index-C9eBskc9.css +1 -0
- package/src/web-dashboard/public/index.html +2 -2
- package/src/web-dashboard/public/assets/index-Beportyl.js +0 -207
- package/src/web-dashboard/public/assets/index-Beportyl.js.map +0 -1
- package/src/web-dashboard/public/assets/index-gtQyg9nm.css +0 -1
|
@@ -28,7 +28,7 @@ import { getAutoRouter } from '../learning/auto-router.js';
|
|
|
28
28
|
import { readRecallHits } from '../context/session-recall.js';
|
|
29
29
|
import { getRouterPromotion } from '../learning/router-promotion.js';
|
|
30
30
|
import { AUTH_CLEAR_THRESHOLD } from '../learning/key-hygiene.js';
|
|
31
|
-
import { ACTION_LOG_FILENAME, aggregateActionTelemetry, readActionTelemetryFile, getModelRegistry, } from '../learning/model-registry.js';
|
|
31
|
+
import { ACTION_LOG_FILENAME, DEFAULT_STALE_MS, aggregateActionTelemetry, readActionTelemetryFile, getModelRegistry, } from '../learning/model-registry.js';
|
|
32
32
|
import { classifyModelEntitlement } from '../inference/model-entitlement.js';
|
|
33
33
|
import { listUnattendedJobs } from '../learning/unattended-job.js';
|
|
34
34
|
import { AdminSessions, countAdminRoleUsers, isAdminConfigured, listAdminUsers, mustChangePasswordFor, removeAdminUser, roleForUser, verifyAdmin, writeAdminUser, MIN_ADMIN_PASSWORD_LENGTH, } from './src/admin-auth.js';
|
|
@@ -1212,6 +1212,48 @@ async function readModelsHealth() {
|
|
|
1212
1212
|
registryVerified,
|
|
1213
1213
|
};
|
|
1214
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* The ONE source of truth for the model/provider counts every page shows.
|
|
1217
|
+
*
|
|
1218
|
+
* Two pairs, deliberately distinct and always LABELLED:
|
|
1219
|
+
* - `tracked*` — the Model Availability Registry (what routing consults).
|
|
1220
|
+
* - `listed*` — the live provider probe (what each provider says it offers).
|
|
1221
|
+
*
|
|
1222
|
+
* These are different questions and their answers legitimately differ (a
|
|
1223
|
+
* provider lists ids the router can never use, and the registry tracks pairs
|
|
1224
|
+
* that no provider currently advertises). Showing them together, from here,
|
|
1225
|
+
* is what stops Overview / Timeline / Models from each headlining a different
|
|
1226
|
+
* number under one ambiguous word like "models".
|
|
1227
|
+
*
|
|
1228
|
+
* The live probe is EXPENSIVE (one HTTP call per provider), so its result is
|
|
1229
|
+
* cached for a minute and shared by all three pages rather than re-probed per
|
|
1230
|
+
* page. `listingAt` is returned so a page can say when the listing was taken.
|
|
1231
|
+
*/
|
|
1232
|
+
let modelListingCache = null;
|
|
1233
|
+
const MODEL_LISTING_TTL_MS = 60_000;
|
|
1234
|
+
async function readModelCounts() {
|
|
1235
|
+
const registry = readModelRegistryData();
|
|
1236
|
+
const trackedModels = registry.total ?? 0;
|
|
1237
|
+
const trackedProviders = Array.isArray(registry.providers) ? registry.providers.length : 0;
|
|
1238
|
+
const now = Date.now();
|
|
1239
|
+
if (!modelListingCache || now - modelListingCache.at >= MODEL_LISTING_TTL_MS) {
|
|
1240
|
+
try {
|
|
1241
|
+
const health = await readModelsHealth();
|
|
1242
|
+
modelListingCache = { at: now, models: health.totalModels, providers: health.providers.length };
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
// A failed probe keeps the previous listing rather than zeroing it:
|
|
1246
|
+
// "we could not reach the providers just now" is not "you have none".
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
trackedModels,
|
|
1251
|
+
trackedProviders,
|
|
1252
|
+
listedModels: modelListingCache?.models ?? 0,
|
|
1253
|
+
listedProviders: modelListingCache?.providers ?? 0,
|
|
1254
|
+
listingAt: modelListingCache?.at ?? 0,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1215
1257
|
/** Check local Ollama provider — no rate limits to parse */
|
|
1216
1258
|
async function checkLocalProvider() {
|
|
1217
1259
|
const result = {
|
|
@@ -1463,6 +1505,8 @@ function readJSON(filePath) {
|
|
|
1463
1505
|
function readCostData() {
|
|
1464
1506
|
const data = readJSON(join(MEMORY_DIR, 'cost-tracker.json'));
|
|
1465
1507
|
if (!data?.entries) {
|
|
1508
|
+
// The empty state deliberately omits `recent` entirely (the dashboard reads
|
|
1509
|
+
// a missing list as "nothing recorded"), matching the other zero counters.
|
|
1466
1510
|
return { totalRequests: 0, totalCost: 0, byProvider: {}, byModel: {} };
|
|
1467
1511
|
}
|
|
1468
1512
|
const entries = data.entries;
|
|
@@ -1471,10 +1515,17 @@ function readCostData() {
|
|
|
1471
1515
|
const byProvider = {};
|
|
1472
1516
|
const byModel = {};
|
|
1473
1517
|
const byProviderMeasured = {};
|
|
1518
|
+
const byProviderReported = {};
|
|
1474
1519
|
let measuredCalls = 0;
|
|
1475
1520
|
let estimatedCalls = 0;
|
|
1476
1521
|
let measuredCost = 0;
|
|
1477
1522
|
let estimatedCost = 0;
|
|
1523
|
+
// Provider-REPORTED cost (costReported): the exact figure the endpoint billed
|
|
1524
|
+
// (e.g. OpenRouter's `usage.cost`), as opposed to a locally-priced figure. It
|
|
1525
|
+
// is a SUBSET of measured spend — reported cost always rides on measured
|
|
1526
|
+
// tokens — so the UI labels it "of which" rather than a third partition.
|
|
1527
|
+
let reportedCalls = 0;
|
|
1528
|
+
let reportedCost = 0;
|
|
1478
1529
|
for (const e of entries) {
|
|
1479
1530
|
const cost = typeof e.costUsd === 'number' ? e.costUsd : 0;
|
|
1480
1531
|
if (e.provider)
|
|
@@ -1493,6 +1544,13 @@ function readCostData() {
|
|
|
1493
1544
|
estimatedCalls += 1;
|
|
1494
1545
|
estimatedCost += cost;
|
|
1495
1546
|
}
|
|
1547
|
+
// Reported-cost split: the provider told us the exact bill for this call.
|
|
1548
|
+
if (e.costReported === true) {
|
|
1549
|
+
reportedCalls += 1;
|
|
1550
|
+
reportedCost += cost;
|
|
1551
|
+
if (e.provider)
|
|
1552
|
+
byProviderReported[e.provider] = (byProviderReported[e.provider] || 0) + cost;
|
|
1553
|
+
}
|
|
1496
1554
|
}
|
|
1497
1555
|
const recent = entries.slice(-50).reverse().map((e) => ({
|
|
1498
1556
|
provider: e.provider,
|
|
@@ -1501,6 +1559,7 @@ function readCostData() {
|
|
|
1501
1559
|
totalTokens: e.totalTokens,
|
|
1502
1560
|
timestamp: e.timestamp,
|
|
1503
1561
|
measured: e.measured === true,
|
|
1562
|
+
reported: e.costReported === true,
|
|
1504
1563
|
}));
|
|
1505
1564
|
return {
|
|
1506
1565
|
totalRequests: entries.length,
|
|
@@ -1509,10 +1568,13 @@ function readCostData() {
|
|
|
1509
1568
|
byProvider,
|
|
1510
1569
|
byModel,
|
|
1511
1570
|
byProviderMeasured,
|
|
1571
|
+
byProviderReported,
|
|
1512
1572
|
measuredCalls,
|
|
1513
1573
|
estimatedCalls,
|
|
1514
1574
|
measuredCost: Math.round(measuredCost * 100000) / 100000,
|
|
1515
1575
|
estimatedCost: Math.round(estimatedCost * 100000) / 100000,
|
|
1576
|
+
reportedCalls,
|
|
1577
|
+
reportedCost: Math.round(reportedCost * 100000) / 100000,
|
|
1516
1578
|
recent,
|
|
1517
1579
|
};
|
|
1518
1580
|
}
|
|
@@ -1825,10 +1887,21 @@ function readModelRegistryData() {
|
|
|
1825
1887
|
};
|
|
1826
1888
|
}).sort((a, b) => a.provider.localeCompare(b.provider));
|
|
1827
1889
|
const allModels = providers.flatMap((p) => p.models);
|
|
1890
|
+
// Two DIFFERENT questions, two numbers. `verifiedRoutable` = proven to work
|
|
1891
|
+
// (status verified, not parked); `routableNow` adds the staleness gate the
|
|
1892
|
+
// router itself applies (`isUsable`). They diverge the moment a proof ages
|
|
1893
|
+
// past 7 days, and reporting only the first under a "routable" label is what
|
|
1894
|
+
// let the Overview contradict the Discovery Timeline's "routable now".
|
|
1895
|
+
const routableNow = allModels.filter((m) => {
|
|
1896
|
+
const verifiedAt = Number(m.lastVerifiedAt ?? 0);
|
|
1897
|
+
return m.status === 'verified' && !m.parked && now - verifiedAt <= DEFAULT_STALE_MS;
|
|
1898
|
+
}).length;
|
|
1828
1899
|
return {
|
|
1829
1900
|
enabled: providers.length > 0,
|
|
1830
1901
|
total: allModels.length,
|
|
1831
1902
|
verified: allModels.filter((m) => m.status === 'verified' && !m.parked).length,
|
|
1903
|
+
/** The staleness-gated subset of `verified` — see the comment above. */
|
|
1904
|
+
routableNow,
|
|
1832
1905
|
unverified: allModels.filter((m) => m.status === 'unverified').length,
|
|
1833
1906
|
unavailable: allModels.filter((m) => m.status === 'unavailable').length,
|
|
1834
1907
|
parked: allModels.filter((m) => m.parked).length,
|
|
@@ -2902,8 +2975,18 @@ function readLargeJsonBody(req) {
|
|
|
2902
2975
|
}
|
|
2903
2976
|
/** Send a JSON response (the admin routes' single writer). */
|
|
2904
2977
|
function writeJson(res, status, data) {
|
|
2905
|
-
|
|
2906
|
-
|
|
2978
|
+
// The client may have gone away (a chat turn that outlives a page reload now
|
|
2979
|
+
// finishes and writes its response to a socket nobody is holding). Writing
|
|
2980
|
+
// there is a no-op, not an error — and it must never throw into the turn.
|
|
2981
|
+
if (res.writableEnded || res.destroyed)
|
|
2982
|
+
return;
|
|
2983
|
+
try {
|
|
2984
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2985
|
+
res.end(JSON.stringify(data));
|
|
2986
|
+
}
|
|
2987
|
+
catch {
|
|
2988
|
+
/* client disconnected mid-write — nothing to do */
|
|
2989
|
+
}
|
|
2907
2990
|
}
|
|
2908
2991
|
/**
|
|
2909
2992
|
* Turn a refused process-environment value into something a user can act on.
|
|
@@ -4725,6 +4808,15 @@ function handleRequest(req, res) {
|
|
|
4725
4808
|
});
|
|
4726
4809
|
return;
|
|
4727
4810
|
}
|
|
4811
|
+
// The SHARED model/provider counts every page (Overview, Timeline, Models)
|
|
4812
|
+
// renders — one endpoint, so the three cannot disagree. `tracked*` is the
|
|
4813
|
+
// registry (routing's truth), `listed*` is the cached live provider probe.
|
|
4814
|
+
if (pathname === '/api/model-counts' && req.method === 'GET') {
|
|
4815
|
+
readModelCounts()
|
|
4816
|
+
.then((counts) => writeJson(res, 200, counts))
|
|
4817
|
+
.catch(() => writeJson(res, 500, { error: 'Failed to read model counts' }));
|
|
4818
|
+
return;
|
|
4819
|
+
}
|
|
4728
4820
|
if (pathname === '/api/model-registry') {
|
|
4729
4821
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4730
4822
|
res.end(JSON.stringify(readModelRegistryData()));
|
|
@@ -4894,6 +4986,28 @@ function handleRequest(req, res) {
|
|
|
4894
4986
|
res.end(JSON.stringify(readRoutingInsights()));
|
|
4895
4987
|
return;
|
|
4896
4988
|
}
|
|
4989
|
+
// ── The two size limits the composer needs BEFORE it reads a file ──────
|
|
4990
|
+
// The browser blocks an over-cap file client-side, so it cannot discover the
|
|
4991
|
+
// cap from a refusal (the file never leaves the page). This is the one place
|
|
4992
|
+
// it can ask, and it keeps the client and the server from disagreeing about
|
|
4993
|
+
// the limit — the same class of bug the Process Environment page fixes for
|
|
4994
|
+
// the switches. Open, like the other read paths: it exposes a number, no data.
|
|
4995
|
+
if (pathname === '/api/limits') {
|
|
4996
|
+
void (async () => {
|
|
4997
|
+
const { resolveExtractMaxChars, resolveAttachmentMaxBytes, DEFAULT_EXTRACT_MAX_CHARS, DEFAULT_ATTACHMENT_MAX_BYTES } = await import('../config/limits.js');
|
|
4998
|
+
const attachmentMaxBytes = resolveAttachmentMaxBytes();
|
|
4999
|
+
writeJson(res, 200, {
|
|
5000
|
+
attachmentMaxBytes,
|
|
5001
|
+
attachmentMaxKb: Math.round(attachmentMaxBytes / 1024),
|
|
5002
|
+
extractMaxChars: resolveExtractMaxChars(),
|
|
5003
|
+
defaults: {
|
|
5004
|
+
attachmentMaxBytes: DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
5005
|
+
extractMaxChars: DEFAULT_EXTRACT_MAX_CHARS,
|
|
5006
|
+
},
|
|
5007
|
+
});
|
|
5008
|
+
})();
|
|
5009
|
+
return;
|
|
5010
|
+
}
|
|
4897
5011
|
if (pathname === '/api/all') {
|
|
4898
5012
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4899
5013
|
res.end(JSON.stringify({
|
|
@@ -6106,7 +6220,10 @@ function handleRequest(req, res) {
|
|
|
6106
6220
|
writeJson(res, 404, { ok: false, error: 'No such session.' });
|
|
6107
6221
|
return;
|
|
6108
6222
|
}
|
|
6109
|
-
|
|
6223
|
+
// `busy` lets a client that reloaded mid-turn know the answer is still being
|
|
6224
|
+
// produced, so it can keep the bubble alive and poll instead of showing a
|
|
6225
|
+
// finished-looking thread.
|
|
6226
|
+
writeJson(res, 200, { ok: true, session: { id, ...rec, busy: chatConsole.isBusy(id) } });
|
|
6110
6227
|
return;
|
|
6111
6228
|
}
|
|
6112
6229
|
// GET /api/continuity — the stored session snapshots + the semantic recall
|
|
@@ -6332,15 +6449,20 @@ function handleRequest(req, res) {
|
|
|
6332
6449
|
return;
|
|
6333
6450
|
}
|
|
6334
6451
|
}
|
|
6335
|
-
// P4 —
|
|
6336
|
-
//
|
|
6337
|
-
//
|
|
6338
|
-
//
|
|
6339
|
-
//
|
|
6340
|
-
//
|
|
6341
|
-
//
|
|
6342
|
-
//
|
|
6343
|
-
//
|
|
6452
|
+
// P4 (revised) — a client DISCONNECT no longer cancels the turn. It used
|
|
6453
|
+
// to: the server treated `res` close as an abort, which meant a page
|
|
6454
|
+
// reload or a closed browser tab killed the in-flight turn and discarded
|
|
6455
|
+
// its answer. Reloading mid-turn is common, and "my question just
|
|
6456
|
+
// vanished" is worse than a turn that keeps running. So the turn now runs
|
|
6457
|
+
// to completion and PERSISTS (chat-console records it), and a reloaded
|
|
6458
|
+
// client resumes the same session id and finds the answer there.
|
|
6459
|
+
//
|
|
6460
|
+
// Cancel is therefore EXPLICIT: the Cancel button calls
|
|
6461
|
+
// `POST /api/chat/cancel`, which invokes chatConsole.abort. There is no
|
|
6462
|
+
// longer any ambiguity between "the user cancelled" and "the connection
|
|
6463
|
+
// dropped" — they are different signals because they are now different
|
|
6464
|
+
// requests. The one cost: a turn whose client vanishes and is never
|
|
6465
|
+
// cancelled keeps spending provider quota until it finishes.
|
|
6344
6466
|
// A pending retry offer is answered HERE, before any turn runs: the reply
|
|
6345
6467
|
// belongs to the offer, not to the composer's next request. A message that
|
|
6346
6468
|
// is neither yes nor no falls through to normal handling.
|
|
@@ -6357,11 +6479,6 @@ function handleRequest(req, res) {
|
|
|
6357
6479
|
});
|
|
6358
6480
|
return;
|
|
6359
6481
|
}
|
|
6360
|
-
const onResClose = () => {
|
|
6361
|
-
if (!res.writableEnded)
|
|
6362
|
-
chatConsole.abort(sessionId);
|
|
6363
|
-
};
|
|
6364
|
-
res.on('close', onResClose);
|
|
6365
6482
|
// Mark the failover log BEFORE the turn so a failure can report which
|
|
6366
6483
|
// models were actually tried (same contract as the gateway).
|
|
6367
6484
|
const attemptMark = markFailoverAttempts();
|
|
@@ -6375,9 +6492,6 @@ function handleRequest(req, res) {
|
|
|
6375
6492
|
...(keepWorktree === undefined ? {} : { keepWorktree }),
|
|
6376
6493
|
...(resume === undefined ? {} : { resume }),
|
|
6377
6494
|
});
|
|
6378
|
-
// The response is written below — remove the disconnect listener so a
|
|
6379
|
-
// post-completion close can never touch the console again.
|
|
6380
|
-
res.off('close', onResClose);
|
|
6381
6495
|
if (!result.ok) {
|
|
6382
6496
|
writeJson(res, 400, {
|
|
6383
6497
|
ok: false,
|
|
@@ -6447,6 +6561,31 @@ function handleRequest(req, res) {
|
|
|
6447
6561
|
})();
|
|
6448
6562
|
return;
|
|
6449
6563
|
}
|
|
6564
|
+
// POST /api/chat/cancel — cancel a session's IN-FLIGHT turn { sessionId }.
|
|
6565
|
+
// Cancel is explicit since a dropped connection no longer means "cancel" (see
|
|
6566
|
+
// the chat handler): only this endpoint aborts the running turn.
|
|
6567
|
+
if (pathname === '/api/chat/cancel' && req.method === 'POST') {
|
|
6568
|
+
void (async () => {
|
|
6569
|
+
const session = adminSessions.validate(bearerToken(req));
|
|
6570
|
+
if (!session) {
|
|
6571
|
+
writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
|
|
6572
|
+
return;
|
|
6573
|
+
}
|
|
6574
|
+
if (!roleCan(session.role, 'routing.operate')) {
|
|
6575
|
+
writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot chat with the agent (requires admin or operator).` });
|
|
6576
|
+
return;
|
|
6577
|
+
}
|
|
6578
|
+
const body = await readJsonBody(req);
|
|
6579
|
+
const sessionId = typeof body?.sessionId === 'string' ? body.sessionId : '';
|
|
6580
|
+
if (!sessionId) {
|
|
6581
|
+
writeJson(res, 400, { ok: false, error: 'Missing sessionId.' });
|
|
6582
|
+
return;
|
|
6583
|
+
}
|
|
6584
|
+
const cancelled = chatConsole.abort(sessionId);
|
|
6585
|
+
writeJson(res, 200, { ok: true, cancelled });
|
|
6586
|
+
})();
|
|
6587
|
+
return;
|
|
6588
|
+
}
|
|
6450
6589
|
// POST /api/chat/reset — forget a session's history { sessionId }.
|
|
6451
6590
|
if (pathname === '/api/chat/reset' && req.method === 'POST') {
|
|
6452
6591
|
void (async () => {
|