@aexol/spectral 0.9.129 → 0.9.131
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/extensions/code-order/analyzer.d.ts +47 -0
- package/dist/extensions/code-order/analyzer.d.ts.map +1 -0
- package/dist/extensions/code-order/analyzer.js +315 -0
- package/dist/extensions/code-order/heuristics.d.ts +37 -0
- package/dist/extensions/code-order/heuristics.d.ts.map +1 -0
- package/dist/extensions/code-order/heuristics.js +188 -0
- package/dist/extensions/code-order/index.d.ts +15 -0
- package/dist/extensions/code-order/index.d.ts.map +1 -0
- package/dist/extensions/code-order/index.js +76 -0
- package/dist/extensions/code-order/report.d.ts +14 -0
- package/dist/extensions/code-order/report.d.ts.map +1 -0
- package/dist/extensions/code-order/report.js +140 -0
- package/dist/extensions/code-order/types.d.ts +63 -0
- package/dist/extensions/code-order/types.d.ts.map +1 -0
- package/dist/extensions/code-order/types.js +7 -0
- package/dist/mcp/ui-stream-types.d.ts +2 -2
- package/dist/relay/dispatcher.d.ts +2 -2
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +84 -50
- package/dist/relay/history-chunker.d.ts +46 -0
- package/dist/relay/history-chunker.d.ts.map +1 -0
- package/dist/relay/history-chunker.js +60 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
- package/dist/sdk/coding-agent/core/system-prompt.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/system-prompt.js +2 -0
- package/dist/sdk/coding-agent/core/tools/bash-blocklist.d.ts +42 -0
- package/dist/sdk/coding-agent/core/tools/bash-blocklist.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/bash-blocklist.js +141 -0
- package/dist/sdk/coding-agent/core/tools/bash.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/bash.js +5 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +6 -3
- package/dist/server/error-humanizer.d.ts +31 -0
- package/dist/server/error-humanizer.d.ts.map +1 -0
- package/dist/server/error-humanizer.js +77 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +7 -6
- package/dist/server/wire.d.ts +40 -0
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — native spectral extension.
|
|
3
|
+
*
|
|
4
|
+
* Registers a single tool, `code_order_report`, that scans the current
|
|
5
|
+
* project's directory tree and returns a Markdown cleanup report: oversized
|
|
6
|
+
* files, deep nesting, empty folders/files, duplicate sibling names and
|
|
7
|
+
* single-child folders. Language-agnostic, read-only, zero configuration.
|
|
8
|
+
*
|
|
9
|
+
* The report is input for the agent — it deduces what to split or flatten
|
|
10
|
+
* outside of this extension. Tree-like structure is enforced separately via
|
|
11
|
+
* the system prompt guidelines (see system-prompt.ts).
|
|
12
|
+
*/
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { walkTree, computeMetrics } from "./analyzer.js";
|
|
15
|
+
import { runAllHeuristics } from "./heuristics.js";
|
|
16
|
+
import { renderReport } from "./report.js";
|
|
17
|
+
export default async function codeOrderExtension(ext) {
|
|
18
|
+
const tool = {
|
|
19
|
+
name: "code_order_report",
|
|
20
|
+
label: "Code Order Report",
|
|
21
|
+
description: "Analyze the current project's folder structure and file sizes, " +
|
|
22
|
+
"then generate a cleanup report with actionable findings: " +
|
|
23
|
+
"oversized files, deep nesting, empty folders, duplicate sibling names, single-child folders. " +
|
|
24
|
+
"Read-only, zero config. Use this to get input data before reorganizing code.",
|
|
25
|
+
promptSnippet: "`code_order_report` — analyze folder structure & sizes, generate cleanup report",
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
path: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Directory to analyze. Default: current working directory.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async execute(_toolCallId, params) {
|
|
36
|
+
const rawPath = params.path ?? process.cwd();
|
|
37
|
+
const root = resolve(rawPath);
|
|
38
|
+
try {
|
|
39
|
+
const { tree, allFiles, allFolders, maxDepth, fileCount } = await walkTree(root);
|
|
40
|
+
const metrics = computeMetrics(allFiles, allFolders, maxDepth);
|
|
41
|
+
const issues = runAllHeuristics(tree, allFiles, allFolders, metrics);
|
|
42
|
+
const result = {
|
|
43
|
+
root,
|
|
44
|
+
generatedAt: new Date().toISOString(),
|
|
45
|
+
metrics,
|
|
46
|
+
issues,
|
|
47
|
+
};
|
|
48
|
+
const report = renderReport(result);
|
|
49
|
+
const summary = `Analyzed ${fileCount.toLocaleString()} files across ` +
|
|
50
|
+
`${metrics.totalFolders.toLocaleString()} folders ` +
|
|
51
|
+
`(${(metrics.totalSize / (1024 * 1024)).toFixed(1)} MB). ` +
|
|
52
|
+
`Found ${issues.length} structural issue(s).`;
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: report }],
|
|
55
|
+
details: {
|
|
56
|
+
summary,
|
|
57
|
+
fileCount,
|
|
58
|
+
folderCount: metrics.totalFolders,
|
|
59
|
+
totalSize: metrics.totalSize,
|
|
60
|
+
issueCount: issues.length,
|
|
61
|
+
root,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: `❌ Code Order analysis failed: ${msg}` }],
|
|
69
|
+
details: { isError: true, error: msg, root },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
ext.registerTool(tool);
|
|
75
|
+
process.stderr.write("[code-order] Registered code_order_report tool.\n");
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — Markdown report generator.
|
|
3
|
+
*
|
|
4
|
+
* Turns an {@link AnalysisResult} into a human/agent-readable Markdown report
|
|
5
|
+
* with a summary, prioritized issues, top largest files, and the extension
|
|
6
|
+
* distribution. The report is returned to the session so the LLM can deduce
|
|
7
|
+
* what to reorganize next.
|
|
8
|
+
*/
|
|
9
|
+
import type { AnalysisResult } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Render the full cleanup report as Markdown.
|
|
12
|
+
*/
|
|
13
|
+
export declare function renderReport(result: AnalysisResult): string;
|
|
14
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../../src/extensions/code-order/report.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyB,MAAM,YAAY,CAAC;AAgDxE;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA0F3D"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — Markdown report generator.
|
|
3
|
+
*
|
|
4
|
+
* Turns an {@link AnalysisResult} into a human/agent-readable Markdown report
|
|
5
|
+
* with a summary, prioritized issues, top largest files, and the extension
|
|
6
|
+
* distribution. The report is returned to the session so the LLM can deduce
|
|
7
|
+
* what to reorganize next.
|
|
8
|
+
*/
|
|
9
|
+
function formatBytes(n) {
|
|
10
|
+
if (n < 1024)
|
|
11
|
+
return `${n} B`;
|
|
12
|
+
if (n < 1024 * 1024)
|
|
13
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
14
|
+
if (n < 1024 * 1024 * 1024)
|
|
15
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
16
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
17
|
+
}
|
|
18
|
+
function severityEmoji(sev) {
|
|
19
|
+
switch (sev) {
|
|
20
|
+
case "critical":
|
|
21
|
+
return "🔴";
|
|
22
|
+
case "warning":
|
|
23
|
+
return "🟡";
|
|
24
|
+
default:
|
|
25
|
+
return "ℹ️";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function groupByType(issues) {
|
|
29
|
+
const out = new Map();
|
|
30
|
+
for (const i of issues) {
|
|
31
|
+
const arr = out.get(i.type) ?? [];
|
|
32
|
+
arr.push(i);
|
|
33
|
+
out.set(i.type, arr);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
const ISSUE_TITLES = {
|
|
38
|
+
oversized_file: "Oversized files",
|
|
39
|
+
deep_nesting: "Deep nesting",
|
|
40
|
+
empty_folder: "Empty folders",
|
|
41
|
+
empty_file: "Empty files",
|
|
42
|
+
duplicate_names: "Duplicate sibling folder names",
|
|
43
|
+
single_child_folder: "Single-child folders",
|
|
44
|
+
};
|
|
45
|
+
const ISSUE_ORDER = [
|
|
46
|
+
"oversized_file",
|
|
47
|
+
"deep_nesting",
|
|
48
|
+
"duplicate_names",
|
|
49
|
+
"empty_folder",
|
|
50
|
+
"empty_file",
|
|
51
|
+
"single_child_folder",
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Render the full cleanup report as Markdown.
|
|
55
|
+
*/
|
|
56
|
+
export function renderReport(result) {
|
|
57
|
+
const { root, generatedAt, metrics, issues } = result;
|
|
58
|
+
const lines = [];
|
|
59
|
+
lines.push(`# Code Order Report — ${root}`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
lines.push(`_Generated: ${generatedAt}_`);
|
|
62
|
+
lines.push("");
|
|
63
|
+
// Summary
|
|
64
|
+
lines.push("## Summary");
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(`**${metrics.totalFiles.toLocaleString()} files** | ` +
|
|
67
|
+
`**${metrics.totalFolders.toLocaleString()} folders** | ` +
|
|
68
|
+
`**${formatBytes(metrics.totalSize)} total** | ` +
|
|
69
|
+
`max depth ${metrics.maxDepth}`);
|
|
70
|
+
lines.push("");
|
|
71
|
+
// Issues
|
|
72
|
+
if (issues.length === 0) {
|
|
73
|
+
lines.push("## Issues");
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("✅ No structural issues detected. The project looks tidy.");
|
|
76
|
+
lines.push("");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const grouped = groupByType(issues);
|
|
80
|
+
const counts = { critical: 0, warning: 0, info: 0 };
|
|
81
|
+
for (const i of issues)
|
|
82
|
+
counts[i.severity]++;
|
|
83
|
+
lines.push(`## Issues (${issues.length})`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push(`**${counts.critical} critical** · **${counts.warning} warnings** · **${counts.info} info**`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
for (const type of ISSUE_ORDER) {
|
|
88
|
+
const group = grouped.get(type);
|
|
89
|
+
if (!group || group.length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
const sev = group[0].severity;
|
|
92
|
+
lines.push(`### ${severityEmoji(sev)} ${ISSUE_TITLES[type]} (${group.length})`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
if (type === "oversized_file") {
|
|
95
|
+
lines.push("| Size | Path | Suggestion |");
|
|
96
|
+
lines.push("|------|------|------------|");
|
|
97
|
+
for (const i of group) {
|
|
98
|
+
const size = i.message.match(/is ([\d.]+ [KMG]?B)/)?.[1] ?? "?";
|
|
99
|
+
lines.push(`| ${size} | \`${i.path}\` | ${i.suggestion ?? ""} |`);
|
|
100
|
+
}
|
|
101
|
+
lines.push("");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
for (const i of group) {
|
|
105
|
+
lines.push(`- \`${i.path}\` — ${i.message}`);
|
|
106
|
+
if (i.suggestion)
|
|
107
|
+
lines.push(` - ${i.suggestion}`);
|
|
108
|
+
}
|
|
109
|
+
lines.push("");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Top largest files
|
|
114
|
+
if (metrics.topLargestFiles.length > 0) {
|
|
115
|
+
lines.push(`## Top ${metrics.topLargestFiles.length} largest files`);
|
|
116
|
+
lines.push("");
|
|
117
|
+
lines.push("| Size | Path |");
|
|
118
|
+
lines.push("|------|------|");
|
|
119
|
+
for (const f of metrics.topLargestFiles) {
|
|
120
|
+
lines.push(`| ${formatBytes(f.size)} | \`${f.path}\` |`);
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
}
|
|
124
|
+
// Extension distribution
|
|
125
|
+
if (metrics.extensions.length > 0) {
|
|
126
|
+
lines.push("## Extension distribution");
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push("| Extension | Files | Total size |");
|
|
129
|
+
lines.push("|-----------|-------|------------|");
|
|
130
|
+
for (const e of metrics.extensions.slice(0, 20)) {
|
|
131
|
+
lines.push(`| .${e.extension} | ${e.count} | ${formatBytes(e.size)} |`);
|
|
132
|
+
}
|
|
133
|
+
if (metrics.extensions.length > 20) {
|
|
134
|
+
lines.push(`| … | … | … |`);
|
|
135
|
+
lines.push(`| _total_ | ${metrics.totalFiles} | ${formatBytes(metrics.totalSize)} |`);
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Order — shared types.
|
|
3
|
+
*
|
|
4
|
+
* Tree-like representation of a scanned directory. Every node is either a file
|
|
5
|
+
* or a folder; folders aggregate size/file/folder counts from their children.
|
|
6
|
+
*/
|
|
7
|
+
export interface FileNode {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly size: number;
|
|
11
|
+
/** Lowercase extension without the leading dot. Empty string when none. */
|
|
12
|
+
readonly extension: string;
|
|
13
|
+
readonly isFile: true;
|
|
14
|
+
readonly mtime: number;
|
|
15
|
+
}
|
|
16
|
+
export interface FolderNode {
|
|
17
|
+
path: string;
|
|
18
|
+
name: string;
|
|
19
|
+
isFile: false;
|
|
20
|
+
totalSize: number;
|
|
21
|
+
fileCount: number;
|
|
22
|
+
folderCount: number;
|
|
23
|
+
maxDepth: number;
|
|
24
|
+
children: TreeNode[];
|
|
25
|
+
}
|
|
26
|
+
export type TreeNode = FileNode | FolderNode;
|
|
27
|
+
export type IssueType = "oversized_file" | "deep_nesting" | "empty_folder" | "empty_file" | "duplicate_names" | "single_child_folder";
|
|
28
|
+
export type IssueSeverity = "info" | "warning" | "critical";
|
|
29
|
+
export interface Issue {
|
|
30
|
+
type: IssueType;
|
|
31
|
+
severity: IssueSeverity;
|
|
32
|
+
path: string;
|
|
33
|
+
message: string;
|
|
34
|
+
suggestion?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ExtensionEntry {
|
|
37
|
+
extension: string;
|
|
38
|
+
count: number;
|
|
39
|
+
size: number;
|
|
40
|
+
}
|
|
41
|
+
export interface ProjectMetrics {
|
|
42
|
+
totalFiles: number;
|
|
43
|
+
totalFolders: number;
|
|
44
|
+
totalSize: number;
|
|
45
|
+
maxDepth: number;
|
|
46
|
+
avgFileSize: number;
|
|
47
|
+
medianFileSize: number;
|
|
48
|
+
extensions: ExtensionEntry[];
|
|
49
|
+
topLargestFiles: FileNode[];
|
|
50
|
+
emptyFolders: string[];
|
|
51
|
+
emptyFiles: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface AnalysisResult {
|
|
54
|
+
root: string;
|
|
55
|
+
generatedAt: string;
|
|
56
|
+
metrics: ProjectMetrics;
|
|
57
|
+
issues: Issue[];
|
|
58
|
+
}
|
|
59
|
+
export interface AnalysisOptions {
|
|
60
|
+
/** Absolute directory to analyze. Defaults to process.cwd(). */
|
|
61
|
+
root: string;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/extensions/code-order/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,EAAE,CAAC;CACrB;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE7C,MAAM,MAAM,SAAS,GAClB,gBAAgB,GAChB,cAAc,GACd,cAAc,GACd,YAAY,GACZ,iBAAiB,GACjB,qBAAqB,CAAC;AAEzB,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAE5D,MAAM,WAAW,KAAK;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,KAAK,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;CACb"}
|
|
@@ -43,7 +43,7 @@ export declare const visualizationStreamEnvelopeSchema: z.ZodObject<{
|
|
|
43
43
|
streamId: string;
|
|
44
44
|
sequence: number;
|
|
45
45
|
frameType: "patch" | "checkpoint" | "final";
|
|
46
|
-
phase: "
|
|
46
|
+
phase: "structure" | "settled" | "shell" | "narrative" | "detail";
|
|
47
47
|
message?: string | undefined;
|
|
48
48
|
spec?: Record<string, unknown> | undefined;
|
|
49
49
|
checkpoint?: Record<string, unknown> | undefined;
|
|
@@ -52,7 +52,7 @@ export declare const visualizationStreamEnvelopeSchema: z.ZodObject<{
|
|
|
52
52
|
streamId: string;
|
|
53
53
|
sequence: number;
|
|
54
54
|
frameType: "patch" | "checkpoint" | "final";
|
|
55
|
-
phase: "
|
|
55
|
+
phase: "structure" | "settled" | "shell" | "narrative" | "detail";
|
|
56
56
|
message?: string | undefined;
|
|
57
57
|
spec?: Record<string, unknown> | undefined;
|
|
58
58
|
checkpoint?: Record<string, unknown> | undefined;
|
|
@@ -163,7 +163,7 @@ export interface ClientMessageDeps {
|
|
|
163
163
|
* - `manager.prompt()` rejection → logged; the manager itself broadcasts
|
|
164
164
|
* an `error` event to subscribers, so we don't double-report.
|
|
165
165
|
*/
|
|
166
|
-
export declare function handleClientMessage(frame: ClientMessageFrame, deps: ClientMessageDeps): void
|
|
166
|
+
export declare function handleClientMessage(frame: ClientMessageFrame, deps: ClientMessageDeps): Promise<void>;
|
|
167
167
|
/**
|
|
168
168
|
* Dispatch a `subscribe` frame from the backend. Handles the case where a
|
|
169
169
|
* browser enters an old session — we load history from SQLite immediately
|
|
@@ -175,7 +175,7 @@ export declare function handleClientMessage(frame: ClientMessageFrame, deps: Cli
|
|
|
175
175
|
* tab needs it). The `ready.catch` handler is only registered once — on the
|
|
176
176
|
* first subscriber creation — to avoid duplicate error events.
|
|
177
177
|
*/
|
|
178
|
-
export declare function handleSubscribe(frame: SubscribeFrame, deps: ClientMessageDeps): void
|
|
178
|
+
export declare function handleSubscribe(frame: SubscribeFrame, deps: ClientMessageDeps): Promise<void>;
|
|
179
179
|
/**
|
|
180
180
|
* Dispatch a `cancel_turn` frame. Disposes the session's agent bridge and
|
|
181
181
|
* removes the stream so the next user message creates a fresh one. The
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAQH,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAK/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAgDzD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -58,8 +58,10 @@ import { handleGetAgentSettings, handleListAgents, handlePutAgentSettings, } fro
|
|
|
58
58
|
import { handleGetSettings, handlePutSettings, } from "../server/handlers/settings.js";
|
|
59
59
|
import { handleGetPromptMutationSettings, handlePutPromptMutationSettings, } from "../server/handlers/prompt-mutation-settings.js";
|
|
60
60
|
import { shutdownState } from "../server/shutdown.js";
|
|
61
|
+
import { humanizeProviderError } from "../server/error-humanizer.js";
|
|
61
62
|
import { handleAutoResearch } from "./auto-research.js";
|
|
62
63
|
import { handleAutoOptimize } from "./auto-optimizer.js";
|
|
64
|
+
import { chunkHistory } from "./history-chunker.js";
|
|
63
65
|
/**
|
|
64
66
|
* Inline path matcher. Returns `null` for any path/method combination we
|
|
65
67
|
* don't recognise; the caller turns that into a `404 Unknown route`.
|
|
@@ -1104,6 +1106,72 @@ function makeRelaySubscriber(sessionId, relay) {
|
|
|
1104
1106
|
},
|
|
1105
1107
|
};
|
|
1106
1108
|
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Emit the chunked `session_ready_*` sequence for a freshly-attached session.
|
|
1111
|
+
*
|
|
1112
|
+
* Replaces the legacy single `session_ready` frame with:
|
|
1113
|
+
* 1. `session_ready_start` — metadata + in-flight turn + chunk totals
|
|
1114
|
+
* 2. N × `history_chunk` — atomic slices of the history tail
|
|
1115
|
+
* 3. `session_ready_end` — completion + integrity total
|
|
1116
|
+
*
|
|
1117
|
+
* A `setImmediate` yield follows every `history_chunk` emit so chunk builds
|
|
1118
|
+
* and Redis publishes are naturally spaced and don't block other CLI work.
|
|
1119
|
+
*
|
|
1120
|
+
* All metadata fields carried by the legacy `session_ready` event are
|
|
1121
|
+
* preserved verbatim on `session_ready_start`; none are dropped.
|
|
1122
|
+
*/
|
|
1123
|
+
async function emitChunkedSessionReady(relay, sessionId, history, attachResult) {
|
|
1124
|
+
const chunks = chunkHistory(history);
|
|
1125
|
+
relay.send({
|
|
1126
|
+
kind: "ws_event",
|
|
1127
|
+
sessionId,
|
|
1128
|
+
event: {
|
|
1129
|
+
type: "session_ready_start",
|
|
1130
|
+
sessionId,
|
|
1131
|
+
currentTurn: attachResult.currentTurn,
|
|
1132
|
+
totalChunks: chunks.length,
|
|
1133
|
+
totalMessages: history.length,
|
|
1134
|
+
totalMessageCount: attachResult.totalMessageCount,
|
|
1135
|
+
loadedMessageCount: attachResult.loadedMessageCount,
|
|
1136
|
+
hasEarlierMessages: attachResult.hasEarlierMessages || undefined,
|
|
1137
|
+
forkCompactPending: attachResult.forkCompactPending || undefined,
|
|
1138
|
+
compacting: attachResult.compacting || undefined,
|
|
1139
|
+
contextWindowUsed: attachResult.contextWindowUsed,
|
|
1140
|
+
contextWindowMax: attachResult.contextWindowMax,
|
|
1141
|
+
intervalActive: attachResult.intervalActive || undefined,
|
|
1142
|
+
intervalMinutes: attachResult.intervalMinutes,
|
|
1143
|
+
intervalNextTickAt: attachResult.intervalNextTickAt,
|
|
1144
|
+
intervalLaunchCount: attachResult.intervalLaunchCount,
|
|
1145
|
+
intervalHasLoop: attachResult.intervalHasLoop || undefined,
|
|
1146
|
+
intervalGoal: attachResult.intervalGoal,
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
for (const chunk of chunks) {
|
|
1150
|
+
relay.send({
|
|
1151
|
+
kind: "ws_event",
|
|
1152
|
+
sessionId,
|
|
1153
|
+
event: {
|
|
1154
|
+
type: "history_chunk",
|
|
1155
|
+
sessionId,
|
|
1156
|
+
index: chunk.index,
|
|
1157
|
+
total: chunk.total,
|
|
1158
|
+
messages: chunk.messages,
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
1161
|
+
// Yield to the event loop so chunk serialization and Redis publishes
|
|
1162
|
+
// are spaced out and don't block other CLI work.
|
|
1163
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1164
|
+
}
|
|
1165
|
+
relay.send({
|
|
1166
|
+
kind: "ws_event",
|
|
1167
|
+
sessionId,
|
|
1168
|
+
event: {
|
|
1169
|
+
type: "session_ready_end",
|
|
1170
|
+
sessionId,
|
|
1171
|
+
totalMessages: history.length,
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1107
1175
|
/**
|
|
1108
1176
|
* Dispatch a `client_message` frame. Idempotent w.r.t. attach: the same
|
|
1109
1177
|
* `Subscriber` is reused across messages for a given session.
|
|
@@ -1118,7 +1186,7 @@ function makeRelaySubscriber(sessionId, relay) {
|
|
|
1118
1186
|
* - `manager.prompt()` rejection → logged; the manager itself broadcasts
|
|
1119
1187
|
* an `error` event to subscribers, so we don't double-report.
|
|
1120
1188
|
*/
|
|
1121
|
-
export function handleClientMessage(frame, deps) {
|
|
1189
|
+
export async function handleClientMessage(frame, deps) {
|
|
1122
1190
|
const { sessionId, message, modelId, reasoningEffort } = frame;
|
|
1123
1191
|
const { manager, relay, subscribers } = deps;
|
|
1124
1192
|
const logger = deps.logger ?? console;
|
|
@@ -1212,37 +1280,16 @@ export function handleClientMessage(frame, deps) {
|
|
|
1212
1280
|
relay.send({
|
|
1213
1281
|
kind: "ws_event",
|
|
1214
1282
|
sessionId,
|
|
1215
|
-
event: { type: "error", message:
|
|
1283
|
+
event: { type: "error", message: humanizeProviderError(err) },
|
|
1216
1284
|
});
|
|
1217
1285
|
return;
|
|
1218
1286
|
}
|
|
1219
1287
|
subscribers.set(sessionId, subscriber);
|
|
1220
|
-
// Synthesize the initial
|
|
1221
|
-
// browser's protocol layer expects `
|
|
1222
|
-
// event on a new stream
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
sessionId,
|
|
1226
|
-
event: {
|
|
1227
|
-
type: "session_ready",
|
|
1228
|
-
sessionId,
|
|
1229
|
-
history: attachResult.history,
|
|
1230
|
-
totalMessageCount: attachResult.totalMessageCount,
|
|
1231
|
-
loadedMessageCount: attachResult.loadedMessageCount,
|
|
1232
|
-
hasEarlierMessages: attachResult.hasEarlierMessages || undefined,
|
|
1233
|
-
currentTurn: attachResult.currentTurn,
|
|
1234
|
-
forkCompactPending: attachResult.forkCompactPending || undefined,
|
|
1235
|
-
contextWindowUsed: attachResult.contextWindowUsed,
|
|
1236
|
-
contextWindowMax: attachResult.contextWindowMax,
|
|
1237
|
-
compacting: attachResult.compacting || undefined,
|
|
1238
|
-
intervalActive: attachResult.intervalActive || undefined,
|
|
1239
|
-
intervalMinutes: attachResult.intervalMinutes,
|
|
1240
|
-
intervalNextTickAt: attachResult.intervalNextTickAt,
|
|
1241
|
-
intervalLaunchCount: attachResult.intervalLaunchCount,
|
|
1242
|
-
intervalHasLoop: attachResult.intervalHasLoop || undefined,
|
|
1243
|
-
intervalGoal: attachResult.intervalGoal,
|
|
1244
|
-
},
|
|
1245
|
-
});
|
|
1288
|
+
// Synthesize the initial frames the WS route used to send. The
|
|
1289
|
+
// browser's protocol layer expects `session_ready_start` as the first
|
|
1290
|
+
// event on a new stream, followed by N `history_chunk` frames and a
|
|
1291
|
+
// final `session_ready_end`.
|
|
1292
|
+
await emitChunkedSessionReady(relay, sessionId, attachResult.history, attachResult);
|
|
1246
1293
|
// Surface bridge-start failures as `error` events; otherwise the
|
|
1247
1294
|
// browser would sit on a `session_ready` with no further frames.
|
|
1248
1295
|
attachResult.ready.catch((err) => {
|
|
@@ -1250,7 +1297,7 @@ export function handleClientMessage(frame, deps) {
|
|
|
1250
1297
|
relay.send({
|
|
1251
1298
|
kind: "ws_event",
|
|
1252
1299
|
sessionId,
|
|
1253
|
-
event: { type: "error", message:
|
|
1300
|
+
event: { type: "error", message: humanizeProviderError(err) },
|
|
1254
1301
|
});
|
|
1255
1302
|
});
|
|
1256
1303
|
}
|
|
@@ -1291,7 +1338,7 @@ export function handleClientMessage(frame, deps) {
|
|
|
1291
1338
|
* tab needs it). The `ready.catch` handler is only registered once — on the
|
|
1292
1339
|
* first subscriber creation — to avoid duplicate error events.
|
|
1293
1340
|
*/
|
|
1294
|
-
export function handleSubscribe(frame, deps) {
|
|
1341
|
+
export async function handleSubscribe(frame, deps) {
|
|
1295
1342
|
const { sessionId } = frame;
|
|
1296
1343
|
const { manager, relay, subscribers } = deps;
|
|
1297
1344
|
const logger = deps.logger ?? console;
|
|
@@ -1311,29 +1358,16 @@ export function handleSubscribe(frame, deps) {
|
|
|
1311
1358
|
relay.send({
|
|
1312
1359
|
kind: "ws_event",
|
|
1313
1360
|
sessionId,
|
|
1314
|
-
event: { type: "error", message:
|
|
1361
|
+
event: { type: "error", message: humanizeProviderError(err) },
|
|
1315
1362
|
});
|
|
1316
1363
|
return;
|
|
1317
1364
|
}
|
|
1318
1365
|
// Send history to all browser subscribers. The backend's fan-out
|
|
1319
|
-
// delivers this to the newly-subscribed tab (and any others).
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
type: "session_ready",
|
|
1325
|
-
sessionId,
|
|
1326
|
-
history: attachResult.history,
|
|
1327
|
-
totalMessageCount: attachResult.totalMessageCount,
|
|
1328
|
-
loadedMessageCount: attachResult.loadedMessageCount,
|
|
1329
|
-
hasEarlierMessages: attachResult.hasEarlierMessages || undefined,
|
|
1330
|
-
currentTurn: attachResult.currentTurn,
|
|
1331
|
-
forkCompactPending: attachResult.forkCompactPending || undefined,
|
|
1332
|
-
contextWindowUsed: attachResult.contextWindowUsed,
|
|
1333
|
-
contextWindowMax: attachResult.contextWindowMax,
|
|
1334
|
-
compacting: attachResult.compacting || undefined,
|
|
1335
|
-
},
|
|
1336
|
-
});
|
|
1366
|
+
// delivers this to the newly-subscribed tab (and any others). The
|
|
1367
|
+
// chunked `session_ready_*` sequence replaces the legacy single
|
|
1368
|
+
// `session_ready` frame so large history tails are streamed in
|
|
1369
|
+
// bounded slices rather than one Redis publish.
|
|
1370
|
+
await emitChunkedSessionReady(relay, sessionId, attachResult.history, attachResult);
|
|
1337
1371
|
if (isNewSubscriber) {
|
|
1338
1372
|
// Surface bridge-start failures as `error` events; otherwise the
|
|
1339
1373
|
// browser would sit on a `session_ready` with no further frames.
|
|
@@ -1342,7 +1376,7 @@ export function handleSubscribe(frame, deps) {
|
|
|
1342
1376
|
relay.send({
|
|
1343
1377
|
kind: "ws_event",
|
|
1344
1378
|
sessionId,
|
|
1345
|
-
event: { type: "error", message:
|
|
1379
|
+
event: { type: "error", message: humanizeProviderError(err) },
|
|
1346
1380
|
});
|
|
1347
1381
|
});
|
|
1348
1382
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, synchronous history chunker for the chunked `session_ready` protocol.
|
|
3
|
+
*
|
|
4
|
+
* Splits a `WireMessage[]` history tail into atomic chunks bounded by both a
|
|
5
|
+
* max-message count and a max-byte size (whichever triggers first). A single
|
|
6
|
+
* message is never split across chunks — if one message alone exceeds
|
|
7
|
+
* `maxBytes`, it is emitted as its own chunk and a `console.warn` is logged
|
|
8
|
+
* (anomaly, not fatal).
|
|
9
|
+
*
|
|
10
|
+
* This module has no I/O and no side effects beyond the documented `console.warn`
|
|
11
|
+
* for oversize messages. It is safe to unit-test in isolation.
|
|
12
|
+
*/
|
|
13
|
+
import type { WireMessage } from "../server/wire.js";
|
|
14
|
+
/** Maximum number of messages packed into a single `history_chunk`. */
|
|
15
|
+
export declare const MAX_MESSAGES_PER_CHUNK = 25;
|
|
16
|
+
/** Maximum serialized byte size of a single `history_chunk`'s messages. */
|
|
17
|
+
export declare const MAX_BYTES_PER_CHUNK: number;
|
|
18
|
+
/** A single atomic slice of the history tail. */
|
|
19
|
+
export interface HistoryChunk {
|
|
20
|
+
/** 0-based position of this chunk in the emitted sequence. */
|
|
21
|
+
index: number;
|
|
22
|
+
/** Total number of chunks that will be emitted for this history. */
|
|
23
|
+
total: number;
|
|
24
|
+
/** Atomic message slice. Never split mid-message. */
|
|
25
|
+
messages: WireMessage[];
|
|
26
|
+
}
|
|
27
|
+
export interface ChunkHistoryOptions {
|
|
28
|
+
maxMessages?: number;
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Split `history` into atomic chunks bounded by `maxMessages` and `maxBytes`.
|
|
33
|
+
*
|
|
34
|
+
* - Empty history → `[]` (the caller still emits `session_ready_start` +
|
|
35
|
+
* `session_ready_end` with zero `history_chunk` frames in between).
|
|
36
|
+
* - `index` is 0-based; `total` is the final chunk count and is identical on
|
|
37
|
+
* every chunk in the returned array (computed after packing so it is always
|
|
38
|
+
* accurate, even when oversize messages force solo chunks).
|
|
39
|
+
* - Messages are atomic: a single message is never split across two chunks.
|
|
40
|
+
* - If a single message exceeds `maxBytes`, it is emitted alone in its own
|
|
41
|
+
* chunk and `console.warn` is called with the message id and index.
|
|
42
|
+
*
|
|
43
|
+
* Pure and synchronous — no I/O, no async, no external state.
|
|
44
|
+
*/
|
|
45
|
+
export declare function chunkHistory(history: WireMessage[], opts?: ChunkHistoryOptions): HistoryChunk[];
|
|
46
|
+
//# sourceMappingURL=history-chunker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-chunker.d.ts","sourceRoot":"","sources":["../../src/relay/history-chunker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,QAAa,CAAC;AAE9C,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,WAAW,EAAE,EACtB,IAAI,GAAE,mBAAwB,GAC7B,YAAY,EAAE,CA6ChB"}
|