codehere 0.6.0 → 0.6.2
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/README.md +8 -8
- package/dist/application/use-cases/generate-final-report.js +57 -2
- package/dist/application/use-cases/generate-narration.js +9 -3
- package/dist/index.js +34 -4
- package/dist/infrastructure/cost/budget-config.js +1 -1
- package/dist/infrastructure/cost/spend-aggregator.js +5 -5
- package/dist/infrastructure/hooks/claude-sdk-hook.js +48 -2
- package/dist/infrastructure/orchestration/codex-adapter.js +11 -6
- package/dist/infrastructure/orchestration/context-handoff.js +1 -1
- package/dist/infrastructure/orchestration/gemini-adapter.js +3 -15
- package/dist/infrastructure/orchestration/graphify-context.js +6 -13
- package/dist/infrastructure/orchestration/memory-injector.js +11 -16
- package/dist/infrastructure/orchestration/opencode-adapter.js +42 -1
- package/dist/infrastructure/orchestration/progress-aggregator.js +1 -2
- package/dist/infrastructure/orchestration/session-manager.js +178 -76
- package/dist/infrastructure/protocols/cloud-sync.js +9 -5
- package/dist/infrastructure/protocols/mcp-server-impl.js +368 -334
- package/dist/infrastructure/storage/cross-tool-agents.js +19 -2
- package/dist/infrastructure/storage/project-thread-migration.js +12 -7
- package/dist/infrastructure/storage/project-thread-reader.js +11 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +6 -2
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/init-command.js +5 -4
- package/dist/presentation/cli/commands/memory-command.js +29 -15
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
- package/dist/presentation/cli/repl.js +20 -1
- package/dist/ui.js +36 -28
- package/dist/utils/typo-guard.js +1 -1
- package/package.json +2 -1
|
@@ -32,7 +32,7 @@ import { isAbsolute } from 'path';
|
|
|
32
32
|
import { readCrossToolAgents } from '../storage/cross-tool-agents.js';
|
|
33
33
|
import { listProjects, readProjectThread } from '../storage/project-thread-reader.js';
|
|
34
34
|
import { mirrorReplyToThread, renameProject } from '../storage/project-thread-mirror.js';
|
|
35
|
-
import { randomUUID } from 'crypto';
|
|
35
|
+
import { randomUUID, timingSafeEqual } from 'crypto';
|
|
36
36
|
import { installAgent } from '../orchestration/agent-installer.js';
|
|
37
37
|
import { detectReadiness } from '../orchestration/readiness.js';
|
|
38
38
|
import { getHookMode } from '../hooks/hook-mode.js';
|
|
@@ -300,15 +300,39 @@ function dispatch(req) {
|
|
|
300
300
|
return { jsonrpc: '2.0', id, error: { code: -32603, message: String(e) } };
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
+
const MAX_BODY_SIZE = 2_097_152; // 2 MiB — covers 1MB code-scan payloads + framing
|
|
304
|
+
function readBody(req, res) {
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
let body = '';
|
|
307
|
+
req.on('data', (c) => {
|
|
308
|
+
body += c;
|
|
309
|
+
if (body.length > MAX_BODY_SIZE) {
|
|
310
|
+
req.pause();
|
|
311
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
312
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
313
|
+
reject(new Error('oversized'));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
req.on('end', () => resolve(body));
|
|
317
|
+
req.on('error', () => reject(new Error('request aborted')));
|
|
318
|
+
});
|
|
319
|
+
}
|
|
303
320
|
export function startMCPServer(options = {}) {
|
|
304
321
|
const port = options.port ?? 3773;
|
|
305
322
|
const host = options.host ?? '127.0.0.1'; // localhost only by default
|
|
306
323
|
const token = options.token;
|
|
307
324
|
const server = http.createServer((req, res) => {
|
|
308
|
-
// Auth check
|
|
325
|
+
// Auth check — timing-safe comparison
|
|
309
326
|
if (token) {
|
|
310
327
|
const authHeader = req.headers.authorization ?? '';
|
|
311
|
-
|
|
328
|
+
const expected = `Bearer ${token}`;
|
|
329
|
+
const cmpLen = Math.max(expected.length, authHeader.length);
|
|
330
|
+
const bufExpected = Buffer.alloc(cmpLen);
|
|
331
|
+
const bufActual = Buffer.alloc(cmpLen);
|
|
332
|
+
bufExpected.write(expected);
|
|
333
|
+
bufActual.write(authHeader);
|
|
334
|
+
const ok = timingSafeEqual(bufExpected, bufActual);
|
|
335
|
+
if (!ok) {
|
|
312
336
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
313
337
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
314
338
|
return;
|
|
@@ -415,7 +439,7 @@ export function startMCPServer(options = {}) {
|
|
|
415
439
|
// Send current session state on connect
|
|
416
440
|
try {
|
|
417
441
|
const manager = SessionManager.getInstance();
|
|
418
|
-
const sessions = manager.list().map(s => ({
|
|
442
|
+
const sessions = manager.list().map((s) => ({
|
|
419
443
|
sessionId: s.sessionId,
|
|
420
444
|
status: s.status,
|
|
421
445
|
agent: s.agent,
|
|
@@ -431,7 +455,9 @@ export function startMCPServer(options = {}) {
|
|
|
431
455
|
try {
|
|
432
456
|
lastSize = statSync(SESSIONS_FILE).size;
|
|
433
457
|
}
|
|
434
|
-
catch {
|
|
458
|
+
catch {
|
|
459
|
+
/* first time */
|
|
460
|
+
}
|
|
435
461
|
}
|
|
436
462
|
const onChange = () => {
|
|
437
463
|
try {
|
|
@@ -443,7 +469,9 @@ export function startMCPServer(options = {}) {
|
|
|
443
469
|
lastSize = currentSize;
|
|
444
470
|
res.write(`data: ${JSON.stringify({ type: 'change' })}\n\n`);
|
|
445
471
|
}
|
|
446
|
-
catch {
|
|
472
|
+
catch {
|
|
473
|
+
/* transient */
|
|
474
|
+
}
|
|
447
475
|
};
|
|
448
476
|
watchFile(SESSIONS_FILE, { interval: 500 }, onChange);
|
|
449
477
|
const ping = setInterval(() => res.write(': ping\n\n'), 15000);
|
|
@@ -503,41 +531,46 @@ export function startMCPServer(options = {}) {
|
|
|
503
531
|
return;
|
|
504
532
|
}
|
|
505
533
|
if (req.method === 'POST' && req.url === '/api/permissions') {
|
|
506
|
-
|
|
507
|
-
req.on('data', (c) => { body += c; });
|
|
508
|
-
req.on('end', () => {
|
|
509
|
-
let parsed;
|
|
510
|
-
try {
|
|
511
|
-
parsed = JSON.parse(body || '{}');
|
|
512
|
-
}
|
|
513
|
-
catch (e) {
|
|
514
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
515
|
-
res.end(JSON.stringify({ error: 'Invalid JSON body', detail: String(e) }));
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
const cwd = parsed.cwd;
|
|
519
|
-
const pattern = parsed.pattern;
|
|
520
|
-
const bucket = parsed.bucket === 'deny' ? 'deny' : 'allow';
|
|
521
|
-
if (!cwd || typeof cwd !== 'string' || !isAbsolute(cwd) || cwd.includes('\0')) {
|
|
522
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
523
|
-
res.end(JSON.stringify({ error: 'cwd must be an absolute path with no NUL bytes' }));
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
if (!pattern || typeof pattern !== 'string') {
|
|
527
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
528
|
-
res.end(JSON.stringify({ error: 'pattern (non-empty string) required' }));
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
534
|
+
(async () => {
|
|
531
535
|
try {
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
536
|
+
const body = await readBody(req, res);
|
|
537
|
+
let parsed;
|
|
538
|
+
try {
|
|
539
|
+
parsed = JSON.parse(body || '{}');
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
543
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body', detail: String(e) }));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const cwd = parsed.cwd;
|
|
547
|
+
const pattern = parsed.pattern;
|
|
548
|
+
const bucket = parsed.bucket === 'deny' ? 'deny' : 'allow';
|
|
549
|
+
if (!cwd || typeof cwd !== 'string' || !isAbsolute(cwd) || cwd.includes('\0')) {
|
|
550
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
551
|
+
res.end(JSON.stringify({ error: 'cwd must be an absolute path with no NUL bytes' }));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
555
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
556
|
+
res.end(JSON.stringify({ error: 'pattern (non-empty string) required' }));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
const result = addPermissionEntry({ cwd, pattern, bucket, reason: parsed.reason });
|
|
561
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
562
|
+
res.end(JSON.stringify(result));
|
|
563
|
+
}
|
|
564
|
+
catch (e) {
|
|
565
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
566
|
+
res.end(JSON.stringify({ error: 'write_failed', detail: String(e?.message || e) }));
|
|
567
|
+
}
|
|
535
568
|
}
|
|
536
|
-
catch
|
|
537
|
-
|
|
538
|
-
|
|
569
|
+
catch {
|
|
570
|
+
if (process.env.CODEHERE_DEBUG === '1')
|
|
571
|
+
console.error('[codehere] readBody rejected (oversized or aborted)');
|
|
539
572
|
}
|
|
540
|
-
});
|
|
573
|
+
})();
|
|
541
574
|
return;
|
|
542
575
|
}
|
|
543
576
|
if (req.method === 'DELETE' && req.url?.startsWith('/api/permissions?')) {
|
|
@@ -807,49 +840,48 @@ export function startMCPServer(options = {}) {
|
|
|
807
840
|
// titled "Scan Auth File" forever even after 100+ unrelated tasks.
|
|
808
841
|
if (req.method === 'POST' && req.url?.match(/^\/api\/projects\/[^/]+\/rename$/)) {
|
|
809
842
|
const slug = req.url.split('/')[3];
|
|
810
|
-
|
|
811
|
-
req.on('data', (c) => {
|
|
812
|
-
body += c;
|
|
813
|
-
});
|
|
814
|
-
req.on('end', () => {
|
|
815
|
-
let parsed;
|
|
843
|
+
(async () => {
|
|
816
844
|
try {
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
845
|
+
const body = await readBody(req, res);
|
|
846
|
+
let parsed;
|
|
847
|
+
try {
|
|
848
|
+
const raw = JSON.parse(body);
|
|
849
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
850
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
851
|
+
res.end(JSON.stringify({ error: 'invalid_body' }));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
parsed = raw;
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
822
857
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
823
|
-
res.end(JSON.stringify({ error: '
|
|
858
|
+
res.end(JSON.stringify({ error: 'invalid_json' }));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (typeof parsed.title !== 'string') {
|
|
862
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
863
|
+
res.end(JSON.stringify({ error: 'title_required' }));
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const result = renameProject(slug, parsed.title);
|
|
867
|
+
if (!result.ok) {
|
|
868
|
+
let status = 400;
|
|
869
|
+
if (result.error === 'Project not found')
|
|
870
|
+
status = 404;
|
|
871
|
+
else if (result.error === 'Write failed')
|
|
872
|
+
status = 500;
|
|
873
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
874
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
824
875
|
return;
|
|
825
876
|
}
|
|
826
|
-
|
|
877
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
878
|
+
res.end(JSON.stringify({ ok: true, slug, title: parsed.title.trim() }));
|
|
827
879
|
}
|
|
828
880
|
catch {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
if (typeof parsed.title !== 'string') {
|
|
834
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
835
|
-
res.end(JSON.stringify({ error: 'title_required' }));
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const result = renameProject(slug, parsed.title);
|
|
839
|
-
if (!result.ok) {
|
|
840
|
-
// 404 = project missing; 500 = fs append failed; 400 = validation.
|
|
841
|
-
let status = 400;
|
|
842
|
-
if (result.error === 'Project not found')
|
|
843
|
-
status = 404;
|
|
844
|
-
else if (result.error === 'Write failed')
|
|
845
|
-
status = 500;
|
|
846
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
847
|
-
res.end(JSON.stringify({ error: result.error }));
|
|
848
|
-
return;
|
|
881
|
+
if (process.env.CODEHERE_DEBUG === '1')
|
|
882
|
+
console.error('[codehere] readBody rejected (oversized or aborted)');
|
|
849
883
|
}
|
|
850
|
-
|
|
851
|
-
res.end(JSON.stringify({ ok: true, slug, title: parsed.title.trim() }));
|
|
852
|
-
});
|
|
884
|
+
})();
|
|
853
885
|
return;
|
|
854
886
|
}
|
|
855
887
|
// ── F1 task orchestration routes ─────────────────────────────────────────
|
|
@@ -867,207 +899,198 @@ export function startMCPServer(options = {}) {
|
|
|
867
899
|
// order; per-task failures are surfaced as { error, agent } entries
|
|
868
900
|
// so a partial-failure batch still returns useful info.
|
|
869
901
|
if (req.method === 'POST' && req.url === '/api/task/batch') {
|
|
870
|
-
|
|
871
|
-
req.on('data', (c) => { body += c; });
|
|
872
|
-
req.on('end', () => {
|
|
873
|
-
let parsed;
|
|
902
|
+
(async () => {
|
|
874
903
|
try {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
res.end(JSON.stringify({ error: 'Invalid JSON body', detail: String(e) }));
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
const tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
|
|
883
|
-
if (tasks.length === 0) {
|
|
884
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
885
|
-
res.end(JSON.stringify({ error: 'tasks (non-empty array) required' }));
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
if (tasks.length > 5) {
|
|
889
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
890
|
-
res.end(JSON.stringify({ error: 'batch size capped at 5', requested: tasks.length }));
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
const validAgentIds = new Set(allAdapters().map((a) => a.id));
|
|
894
|
-
// v0.6 S3 slice 2 — pre-spawn budget gate, applied per-task. Fan-out
|
|
895
|
-
// is the most likely way to overshoot a daily budget (5 paid spawns
|
|
896
|
-
// at once); checking once at batch entry then re-checking per task
|
|
897
|
-
// is overkill, so we snapshot the budget + spend ONCE and let each
|
|
898
|
-
// paid spawn that would push us over fail with the same error shape
|
|
899
|
-
// /api/task uses. Free-tier spawns always pass.
|
|
900
|
-
const batchBudget = loadBudget();
|
|
901
|
-
const batchSpendNow = batchBudget.dailyUsd !== null ? computeSpendToday().totalUsd : 0;
|
|
902
|
-
const results = [];
|
|
903
|
-
for (const t of tasks) {
|
|
904
|
-
const prompt = t.prompt;
|
|
905
|
-
if (!prompt || typeof prompt !== 'string') {
|
|
906
|
-
results.push({ error: 'prompt (string) required', agent: t.agent ?? null });
|
|
907
|
-
continue;
|
|
904
|
+
const body = await readBody(req, res);
|
|
905
|
+
let parsed;
|
|
906
|
+
try {
|
|
907
|
+
parsed = JSON.parse(body || '{}');
|
|
908
908
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
909
|
+
catch (e) {
|
|
910
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
911
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body', detail: String(e) }));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
|
|
915
|
+
if (tasks.length === 0) {
|
|
916
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
917
|
+
res.end(JSON.stringify({ error: 'tasks (non-empty array) required' }));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (tasks.length > 5) {
|
|
921
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
922
|
+
res.end(JSON.stringify({ error: 'batch size capped at 5', requested: tasks.length }));
|
|
923
|
+
return;
|
|
913
924
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
925
|
+
const validAgentIds = new Set(allAdapters().map((a) => a.id));
|
|
926
|
+
// v0.6 S3 slice 2 — pre-spawn budget gate, applied per-task. Fan-out
|
|
927
|
+
// is the most likely way to overshoot a daily budget (5 paid spawns
|
|
928
|
+
// at once); checking once at batch entry then re-checking per task
|
|
929
|
+
// is overkill, so we snapshot the budget + spend ONCE and let each
|
|
930
|
+
// paid spawn that would push us over fail with the same error shape
|
|
931
|
+
// /api/task uses. Free-tier spawns always pass.
|
|
932
|
+
const batchBudget = loadBudget();
|
|
933
|
+
const batchSpendNow = batchBudget.dailyUsd !== null ? computeSpendToday().totalUsd : 0;
|
|
934
|
+
const results = [];
|
|
935
|
+
for (const t of tasks) {
|
|
936
|
+
const prompt = t.prompt;
|
|
937
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
938
|
+
results.push({ error: 'prompt (string) required', agent: t.agent ?? null });
|
|
917
939
|
continue;
|
|
918
940
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
941
|
+
const agentId = typeof t.agent === 'string' && t.agent.length > 0 ? t.agent : 'conductor';
|
|
942
|
+
if (!validAgentIds.has(agentId)) {
|
|
943
|
+
results.push({ error: `Unknown agent: ${agentId}`, agent: agentId });
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (t.cwd !== undefined) {
|
|
947
|
+
if (typeof t.cwd !== 'string' || t.cwd.includes('\0') || !isAbsolute(t.cwd)) {
|
|
948
|
+
results.push({ error: 'cwd must be absolute, no NUL bytes', agent: agentId });
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// Per-task budget check (paid agents only).
|
|
953
|
+
if (batchBudget.dailyUsd !== null) {
|
|
954
|
+
const probe = estimateCostUsd(agentId, { inputTokens: 1, outputTokens: 1 });
|
|
955
|
+
if (probe > 0 && isOverBudget(batchSpendNow, batchBudget)) {
|
|
956
|
+
results.push({
|
|
957
|
+
error: 'daily_budget_exhausted',
|
|
958
|
+
agent: agentId,
|
|
959
|
+
spentTodayUsd: batchSpendNow,
|
|
960
|
+
dailyBudgetUsd: batchBudget.dailyUsd,
|
|
961
|
+
});
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
const session = manager.spawn(prompt, agentId, { cwd: t.cwd });
|
|
924
967
|
results.push({
|
|
925
|
-
|
|
926
|
-
agent:
|
|
927
|
-
|
|
928
|
-
|
|
968
|
+
sessionId: session.sessionId,
|
|
969
|
+
agent: session.agent,
|
|
970
|
+
status: session.status,
|
|
971
|
+
startedAt: session.startedAt,
|
|
929
972
|
});
|
|
930
|
-
|
|
973
|
+
}
|
|
974
|
+
catch (e) {
|
|
975
|
+
const msg = String(e?.message || e);
|
|
976
|
+
const code = msg.includes('pre-prompt scan blocked') ? 'pre_prompt_blocked' : 'spawn_failed';
|
|
977
|
+
results.push({ error: code, reason: msg, agent: agentId });
|
|
931
978
|
}
|
|
932
979
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
results.push({
|
|
936
|
-
sessionId: session.sessionId,
|
|
937
|
-
agent: session.agent,
|
|
938
|
-
status: session.status,
|
|
939
|
-
startedAt: session.startedAt,
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
catch (e) {
|
|
943
|
-
const msg = String(e?.message || e);
|
|
944
|
-
const code = msg.includes('pre-prompt scan blocked') ? 'pre_prompt_blocked' : 'spawn_failed';
|
|
945
|
-
results.push({ error: code, reason: msg, agent: agentId });
|
|
946
|
-
}
|
|
980
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
981
|
+
res.end(JSON.stringify({ tasks: results, count: results.length }));
|
|
947
982
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
983
|
+
catch {
|
|
984
|
+
if (process.env.CODEHERE_DEBUG === '1')
|
|
985
|
+
console.error('[codehere] readBody rejected (oversized or aborted)');
|
|
986
|
+
}
|
|
987
|
+
})();
|
|
951
988
|
return;
|
|
952
989
|
}
|
|
953
990
|
if (req.method === 'POST' && req.url === '/api/task') {
|
|
954
|
-
|
|
955
|
-
req.on('data', (c) => {
|
|
956
|
-
body += c;
|
|
957
|
-
});
|
|
958
|
-
req.on('end', () => {
|
|
959
|
-
// Two-phase try: JSON parse failures get one error shape, spawn
|
|
960
|
-
// failures get a different one. Pre-fix the catch wrapped every
|
|
961
|
-
// failure as "Invalid JSON body" — including the pre-prompt scan
|
|
962
|
-
// throw — which read as a parse bug to the client.
|
|
963
|
-
let parsed;
|
|
964
|
-
try {
|
|
965
|
-
parsed = JSON.parse(body || '{}');
|
|
966
|
-
}
|
|
967
|
-
catch (e) {
|
|
968
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
969
|
-
res.end(JSON.stringify({ error: 'Invalid JSON body', detail: String(e) }));
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
991
|
+
(async () => {
|
|
972
992
|
try {
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
// future-proofs against partial-match exploits, and fails fast
|
|
982
|
-
// with a clear 400 instead of letting SessionManager.spawn throw.
|
|
983
|
-
//
|
|
984
|
-
// Apr 29: default flipped from 'claude-code' to 'conductor'. The web
|
|
985
|
-
// UI is the Conductor surface; tasks submitted without an explicit
|
|
986
|
-
// agent route through the supervisor (rule-based routing today,
|
|
987
|
-
// LLM classifier in v0.6). Empty string is treated like undefined
|
|
988
|
-
// so the form submitting `agent: ""` (Auto option) hits the same
|
|
989
|
-
// path. Specialists remain explicit overrides via the dropdown.
|
|
990
|
-
// Type-check is preserved: non-string non-undefined values still 400.
|
|
991
|
-
if (agent !== undefined && agent !== null && typeof agent !== 'string') {
|
|
992
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
993
|
-
res.end(JSON.stringify({ error: 'agent must be a string or omitted' }));
|
|
994
|
-
return;
|
|
993
|
+
const body = await readBody(req, res);
|
|
994
|
+
// Two-phase try: JSON parse failures get one error shape, spawn
|
|
995
|
+
// failures get a different one. Pre-fix the catch wrapped every
|
|
996
|
+
// failure as "Invalid JSON body" — including the pre-prompt scan
|
|
997
|
+
// throw — which read as a parse bug to the client.
|
|
998
|
+
let parsed;
|
|
999
|
+
try {
|
|
1000
|
+
parsed = JSON.parse(body || '{}');
|
|
995
1001
|
}
|
|
996
|
-
|
|
997
|
-
const agentId = typeof agent === 'string' && agent.length > 0 ? agent : 'conductor';
|
|
998
|
-
if (!validAgentIds.has(agentId)) {
|
|
1002
|
+
catch (e) {
|
|
999
1003
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1000
|
-
res.end(JSON.stringify({
|
|
1001
|
-
error: `Unknown agent: ${typeof agentId === 'string' ? agentId.slice(0, 64) : '<non-string>'}`,
|
|
1002
|
-
valid: [...validAgentIds],
|
|
1003
|
-
}));
|
|
1004
|
+
res.end(JSON.stringify({ error: 'Invalid JSON body', detail: String(e) }));
|
|
1004
1005
|
return;
|
|
1005
1006
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
if (typeof cwd !== 'string' || cwd.includes('\0') || !isAbsolute(cwd)) {
|
|
1007
|
+
try {
|
|
1008
|
+
const { prompt, cwd, agent } = parsed;
|
|
1009
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
1010
1010
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1011
|
-
res.end(JSON.stringify({ error: '
|
|
1011
|
+
res.end(JSON.stringify({ error: 'prompt (string) required' }));
|
|
1012
1012
|
return;
|
|
1013
1013
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1014
|
+
// CRIT-5 fix · whitelist agent ID against the registry BEFORE any
|
|
1015
|
+
// log/spawn. Stops log injection (`agent: "claude-code\n[FAKE]"`),
|
|
1016
|
+
// future-proofs against partial-match exploits, and fails fast
|
|
1017
|
+
// with a clear 400 instead of letting SessionManager.spawn throw.
|
|
1018
|
+
//
|
|
1019
|
+
// Apr 29: default flipped from 'claude-code' to 'conductor'. The web
|
|
1020
|
+
// UI is the Conductor surface; tasks submitted without an explicit
|
|
1021
|
+
// agent route through the supervisor (rule-based routing today,
|
|
1022
|
+
// LLM classifier in v0.6). Empty string is treated like undefined
|
|
1023
|
+
// so the form submitting `agent: ""` (Auto option) hits the same
|
|
1024
|
+
// path. Specialists remain explicit overrides via the dropdown.
|
|
1025
|
+
// Type-check is preserved: non-string non-undefined values still 400.
|
|
1026
|
+
if (agent !== undefined && agent !== null && typeof agent !== 'string') {
|
|
1027
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1028
|
+
res.end(JSON.stringify({ error: 'agent must be a string or omitted' }));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const validAgentIds = new Set(allAdapters().map((a) => a.id));
|
|
1032
|
+
const agentId = typeof agent === 'string' && agent.length > 0 ? agent : 'conductor';
|
|
1033
|
+
if (!validAgentIds.has(agentId)) {
|
|
1034
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1035
|
+
res.end(JSON.stringify({
|
|
1036
|
+
error: `Unknown agent: ${typeof agentId === 'string' ? agentId.slice(0, 64) : '<non-string>'}`,
|
|
1037
|
+
valid: [...validAgentIds],
|
|
1038
|
+
}));
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
// Path-traversal + null-byte defense · cwd must be absolute and
|
|
1042
|
+
// free of NUL bytes (some filesystems treat \0 as terminator).
|
|
1043
|
+
if (cwd !== undefined) {
|
|
1044
|
+
if (typeof cwd !== 'string' || cwd.includes('\0') || !isAbsolute(cwd)) {
|
|
1045
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1046
|
+
res.end(JSON.stringify({ error: 'cwd must be an absolute path with no NUL bytes' }));
|
|
1036
1047
|
return;
|
|
1037
1048
|
}
|
|
1038
1049
|
}
|
|
1050
|
+
const budgetForSpawn = loadBudget();
|
|
1051
|
+
if (budgetForSpawn.dailyUsd !== null) {
|
|
1052
|
+
const probe = estimateCostUsd(agentId, { inputTokens: 1, outputTokens: 1 });
|
|
1053
|
+
const isPaidSpawn = probe > 0;
|
|
1054
|
+
if (isPaidSpawn) {
|
|
1055
|
+
const todaySpend = computeSpendToday();
|
|
1056
|
+
if (isOverBudget(todaySpend.totalUsd, budgetForSpawn)) {
|
|
1057
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
1058
|
+
res.end(JSON.stringify({
|
|
1059
|
+
error: 'daily_budget_exhausted',
|
|
1060
|
+
message: `Today's estimated spend ($${todaySpend.totalUsd.toFixed(2)}) has reached the daily budget ($${budgetForSpawn.dailyUsd.toFixed(2)}). Switch to a free agent (gemini, opencode) or raise the budget with \`codehere budget set <amount>\`.`,
|
|
1061
|
+
spentTodayUsd: todaySpend.totalUsd,
|
|
1062
|
+
dailyBudgetUsd: budgetForSpawn.dailyUsd,
|
|
1063
|
+
}));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const session = manager.spawn(prompt, agentId, { cwd });
|
|
1069
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1070
|
+
res.end(JSON.stringify({
|
|
1071
|
+
sessionId: session.sessionId,
|
|
1072
|
+
startedAt: session.startedAt,
|
|
1073
|
+
status: session.status,
|
|
1074
|
+
agent: session.agent,
|
|
1075
|
+
canAppendMessage: session.canAppendMessage === true,
|
|
1076
|
+
}));
|
|
1039
1077
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
// first render. Without this, ALL just-spawned sessions render
|
|
1050
|
-
// with reply disabled even when the adapter supports it.
|
|
1051
|
-
canAppendMessage: session.canAppendMessage === true,
|
|
1052
|
-
}));
|
|
1053
|
-
}
|
|
1054
|
-
catch (e) {
|
|
1055
|
-
// Spawn-phase failure. Distinguish the cross-vendor pre-prompt
|
|
1056
|
-
// scan refusal (#110) — actionable safety block — from generic
|
|
1057
|
-
// adapter / install / SessionManager errors. The pre-prompt
|
|
1058
|
-
// case gets 422 (Unprocessable Entity, "we understood but
|
|
1059
|
-
// refused") + a structured 'pre_prompt_blocked' code so the
|
|
1060
|
-
// web UI can surface a recovery affordance.
|
|
1061
|
-
const msg = String(e?.message || e);
|
|
1062
|
-
if (msg.includes('pre-prompt scan blocked')) {
|
|
1063
|
-
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1064
|
-
res.end(JSON.stringify({ error: 'pre_prompt_blocked', reason: msg }));
|
|
1065
|
-
return;
|
|
1078
|
+
catch (e) {
|
|
1079
|
+
const msg = String(e?.message || e);
|
|
1080
|
+
if (msg.includes('pre-prompt scan blocked')) {
|
|
1081
|
+
res.writeHead(422, { 'Content-Type': 'application/json' });
|
|
1082
|
+
res.end(JSON.stringify({ error: 'pre_prompt_blocked', reason: msg }));
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1086
|
+
res.end(JSON.stringify({ error: 'spawn_failed', detail: msg }));
|
|
1066
1087
|
}
|
|
1067
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1068
|
-
res.end(JSON.stringify({ error: 'spawn_failed', detail: msg }));
|
|
1069
1088
|
}
|
|
1070
|
-
|
|
1089
|
+
catch {
|
|
1090
|
+
if (process.env.CODEHERE_DEBUG === '1')
|
|
1091
|
+
console.error('[codehere] readBody rejected (oversized or aborted)');
|
|
1092
|
+
}
|
|
1093
|
+
})();
|
|
1071
1094
|
return;
|
|
1072
1095
|
}
|
|
1073
1096
|
if (req.method === 'GET' && req.url === '/api/tasks') {
|
|
@@ -1208,54 +1231,53 @@ export function startMCPServer(options = {}) {
|
|
|
1208
1231
|
res.end(JSON.stringify({ error: `Agent ${session.agent} does not support follow-up messages yet.` }));
|
|
1209
1232
|
return;
|
|
1210
1233
|
}
|
|
1211
|
-
|
|
1212
|
-
req.on('data', (c) => {
|
|
1213
|
-
body += c.toString('utf-8');
|
|
1214
|
-
});
|
|
1215
|
-
req.on('end', async () => {
|
|
1234
|
+
(async () => {
|
|
1216
1235
|
try {
|
|
1217
|
-
const
|
|
1218
|
-
const text = (parsed.text ?? '').trim();
|
|
1219
|
-
if (!text) {
|
|
1220
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1221
|
-
res.end(JSON.stringify({ error: 'text is required' }));
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
const replyStartedAt = Date.now();
|
|
1225
|
-
const out = await session.appendMessage(text);
|
|
1226
|
-
const replyCompletedAt = Date.now();
|
|
1227
|
-
// v0.6.x — forward-mirror the reply turn into the project thread
|
|
1228
|
-
// so /threads sees follow-ups as visible chronological turns
|
|
1229
|
-
// (not just the original spawn). Best-effort: mirror failures
|
|
1230
|
-
// never break the reply path.
|
|
1236
|
+
const body = await readBody(req, res);
|
|
1231
1237
|
try {
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1238
|
+
const parsed = JSON.parse(body || '{}');
|
|
1239
|
+
const text = (parsed.text ?? '').trim();
|
|
1240
|
+
if (!text) {
|
|
1241
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1242
|
+
res.end(JSON.stringify({ error: 'text is required' }));
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const replyStartedAt = Date.now();
|
|
1246
|
+
const out = await session.appendMessage(text);
|
|
1247
|
+
const replyCompletedAt = Date.now();
|
|
1248
|
+
try {
|
|
1249
|
+
const replyCwd = manager.getCwd(sessionId);
|
|
1250
|
+
if (replyCwd) {
|
|
1251
|
+
mirrorReplyToThread({
|
|
1252
|
+
parentSessionId: sessionId,
|
|
1253
|
+
replySessionId: randomUUID(),
|
|
1254
|
+
agent: session.agent,
|
|
1255
|
+
prompt: text,
|
|
1256
|
+
cwd: replyCwd,
|
|
1257
|
+
startedAt: replyStartedAt,
|
|
1258
|
+
completedAt: replyCompletedAt,
|
|
1259
|
+
durationMs: out.durationMs,
|
|
1260
|
+
result: out.result,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1245
1263
|
}
|
|
1264
|
+
catch {
|
|
1265
|
+
/* mirror is best-effort */
|
|
1266
|
+
}
|
|
1267
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1268
|
+
res.end(JSON.stringify({ sessionId, ...out }));
|
|
1246
1269
|
}
|
|
1247
|
-
catch {
|
|
1248
|
-
|
|
1270
|
+
catch (err) {
|
|
1271
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1272
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1273
|
+
res.end(JSON.stringify({ error: msg }));
|
|
1249
1274
|
}
|
|
1250
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1251
|
-
res.end(JSON.stringify({ sessionId, ...out }));
|
|
1252
1275
|
}
|
|
1253
|
-
catch
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
res.end(JSON.stringify({ error: msg }));
|
|
1276
|
+
catch {
|
|
1277
|
+
if (process.env.CODEHERE_DEBUG === '1')
|
|
1278
|
+
console.error('[codehere] readBody rejected (oversized or aborted)');
|
|
1257
1279
|
}
|
|
1258
|
-
});
|
|
1280
|
+
})();
|
|
1259
1281
|
return;
|
|
1260
1282
|
}
|
|
1261
1283
|
}
|
|
@@ -1278,45 +1300,54 @@ export function startMCPServer(options = {}) {
|
|
|
1278
1300
|
// replacement session on the target agent, preserving the
|
|
1279
1301
|
// original prompt + cwd.
|
|
1280
1302
|
if (req.method === 'POST' && req.url === '/api/failover-accept') {
|
|
1281
|
-
|
|
1282
|
-
req.on('data', (c) => { body += c; });
|
|
1283
|
-
req.on('end', () => {
|
|
1284
|
-
let parsed;
|
|
1303
|
+
(async () => {
|
|
1285
1304
|
try {
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1305
|
+
const body = await readBody(req, res);
|
|
1306
|
+
let parsed;
|
|
1307
|
+
try {
|
|
1308
|
+
const raw = JSON.parse(body);
|
|
1309
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1310
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1311
|
+
res.end(JSON.stringify({ error: 'invalid_body' }));
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
parsed = raw;
|
|
1315
|
+
}
|
|
1316
|
+
catch {
|
|
1288
1317
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1289
1318
|
res.end(JSON.stringify({ error: 'invalid_body' }));
|
|
1290
1319
|
return;
|
|
1291
1320
|
}
|
|
1292
|
-
parsed
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
res.end(JSON.stringify({ error: 'invalid_body' }));
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
if (typeof parsed.sessionId !== 'string' || typeof parsed.targetAgent !== 'string') {
|
|
1300
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1301
|
-
res.end(JSON.stringify({ error: 'missing_sessionId_or_targetAgent' }));
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
try {
|
|
1305
|
-
const result = SessionManager.getInstance().acceptFailover(parsed.sessionId, parsed.targetAgent);
|
|
1306
|
-
if (result) {
|
|
1307
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1308
|
-
res.end(JSON.stringify({ ok: true, newSessionId: result.sessionId }));
|
|
1321
|
+
if (typeof parsed.sessionId !== 'string' || typeof parsed.targetAgent !== 'string') {
|
|
1322
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1323
|
+
res.end(JSON.stringify({ error: 'missing_sessionId_or_targetAgent' }));
|
|
1324
|
+
return;
|
|
1309
1325
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1326
|
+
try {
|
|
1327
|
+
const result = SessionManager.getInstance().acceptFailover(parsed.sessionId, parsed.targetAgent);
|
|
1328
|
+
if (result) {
|
|
1329
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1330
|
+
res.end(JSON.stringify({ ok: true, newSessionId: result.sessionId }));
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1334
|
+
res.end(JSON.stringify({
|
|
1335
|
+
ok: false,
|
|
1336
|
+
error: 'target_unavailable',
|
|
1337
|
+
detail: `Could not spawn ${parsed.targetAgent}. Check if it's installed and authenticated.`,
|
|
1338
|
+
}));
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
catch (e) {
|
|
1342
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1343
|
+
res.end(JSON.stringify({ error: 'failover_failed', detail: String(e) }));
|
|
1313
1344
|
}
|
|
1314
1345
|
}
|
|
1315
|
-
catch
|
|
1316
|
-
|
|
1317
|
-
|
|
1346
|
+
catch {
|
|
1347
|
+
if (process.env.CODEHERE_DEBUG === '1')
|
|
1348
|
+
console.error('[codehere] readBody rejected (oversized or aborted)');
|
|
1318
1349
|
}
|
|
1319
|
-
});
|
|
1350
|
+
})();
|
|
1320
1351
|
return;
|
|
1321
1352
|
}
|
|
1322
1353
|
// MCP endpoint — POST jsonrpc; reject other verbs with 405 + Allow header
|
|
@@ -1327,25 +1358,28 @@ export function startMCPServer(options = {}) {
|
|
|
1327
1358
|
res.end(JSON.stringify({ error: 'MCP endpoint accepts POST only', method: req.method }));
|
|
1328
1359
|
return;
|
|
1329
1360
|
}
|
|
1330
|
-
|
|
1331
|
-
req.on('data', (chunk) => {
|
|
1332
|
-
body += chunk;
|
|
1333
|
-
});
|
|
1334
|
-
req.on('end', () => {
|
|
1361
|
+
(async () => {
|
|
1335
1362
|
try {
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1363
|
+
const body = await readBody(req, res);
|
|
1364
|
+
try {
|
|
1365
|
+
const mcpReq = JSON.parse(body);
|
|
1366
|
+
const mcpRes = dispatch(mcpReq);
|
|
1367
|
+
res.writeHead(200, {
|
|
1368
|
+
'Content-Type': 'application/json',
|
|
1369
|
+
'Access-Control-Allow-Origin': 'http://localhost',
|
|
1370
|
+
});
|
|
1371
|
+
res.end(JSON.stringify(mcpRes));
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1375
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1376
|
+
}
|
|
1343
1377
|
}
|
|
1344
1378
|
catch {
|
|
1345
|
-
|
|
1346
|
-
|
|
1379
|
+
if (process.env.CODEHERE_DEBUG === '1')
|
|
1380
|
+
console.error('[codehere] readBody rejected (oversized or aborted)');
|
|
1347
1381
|
}
|
|
1348
|
-
});
|
|
1382
|
+
})();
|
|
1349
1383
|
return;
|
|
1350
1384
|
}
|
|
1351
1385
|
// /api/version — small completeness item for ops monitoring (rc.17).
|