@orion-agents/orion-code 0.3.4 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +157 -0
- package/README.md +59 -5
- package/README.zh-CN.md +6 -3
- package/dist/cli.js +58 -32
- package/dist/commands/context-tool-command-handlers.js +1 -5
- package/dist/commands/registry.js +1 -4
- package/dist/harness/intent.js +69 -16
- package/dist/harness/turn-summary.js +3 -1
- package/dist/memory/embeddings.js +2 -3
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/relevant-finder.js +125 -15
- package/dist/memory/vector-store.js +13 -8
- package/dist/migration/migrate.js +8 -12
- package/dist/runtime/composer/history.js +1 -3
- package/dist/runtime/composer/layout.js +7 -4
- package/dist/runtime/legacy-thread-materializer.d.ts +3 -1
- package/dist/runtime/legacy-thread-materializer.js +15 -3
- package/dist/runtime/product-bootstrap.d.ts +12 -0
- package/dist/runtime/product-bootstrap.js +37 -22
- package/dist/runtime/rich-text/ansi-parser.js +12 -2
- package/dist/runtime/rich-text/layout.js +15 -19
- package/dist/runtime/subagents/budget.js +4 -1
- package/dist/runtime/subagents/policy.js +11 -4
- package/dist/runtime/subagents/provider-gate.js +6 -4
- package/dist/runtime/subagents/research-quality.js +3 -1
- package/dist/runtime/subagents/research-renderer.js +9 -2
- package/dist/runtime/subagents/result-parser.js +18 -7
- package/dist/runtime/thread-event-store.d.ts +5 -3
- package/dist/runtime/thread-event-store.js +29 -8
- package/dist/runtime/thread-projection.d.ts +17 -0
- package/dist/runtime/thread-projection.js +42 -1
- package/dist/runtime/tool-output-presentation.js +7 -7
- package/dist/runtime/turn-controller.js +1 -2
- package/dist/services/auth/auth.js +1 -4
- package/dist/services/auth/aws.js +24 -14
- package/dist/services/auto-fix/autoFixRunner.js +1 -1
- package/dist/services/auto-fix/index.d.ts +1 -1
- package/dist/services/diagnostic-tracking/diagnosticTracking.js +4 -2
- package/dist/services/diagnostic-tracking/index.d.ts +1 -1
- package/dist/services/effort.js +1 -9
- package/dist/services/file-glob.js +3 -4
- package/dist/services/mcp/transports.js +4 -4
- package/dist/services/model-catalog.js +179 -30
- package/dist/services/model-context.js +1 -1
- package/dist/services/provider-resilience/stream-recovery.js +1 -3
- package/dist/services/redaction.js +8 -0
- package/dist/services/session-storage.d.ts +28 -3
- package/dist/services/session-storage.js +70 -6
- package/dist/services/smart-routing.js +19 -11
- package/dist/services/verification-profile.js +17 -22
- package/dist/services/web-search-provider.js +21 -9
- package/dist/services/workspace-diff.js +10 -4
- package/dist/skills/builtin/code-review/SKILL.md +1 -1
- package/dist/skills/builtin/security-check/SKILL.md +1 -1
- package/dist/skills/builtin/test-gen/SKILL.md +1 -1
- package/dist/terminal-ui/editor.js +6 -2
- package/dist/terminal-ui/output-queue.js +9 -3
- package/dist/tools/lsp.js +12 -6
- package/dist/tui-core/frame.js +9 -7
- package/dist/tui-ui/transcript-cache.js +6 -6
- package/dist/tui-ui/transcript-inspector-surface.js +4 -1
- package/dist/tui-ui/transcript-inspector.js +3 -8
- package/dist/ui/box.js +14 -7
- package/dist/ui/markdown.js +4 -2
- package/dist/ui/shared/input-frame.js +12 -8
- package/dist/ui/stream-markdown.js +1 -1
- package/dist/ui/tool-preview.js +11 -3
- package/dist/ui/user-input.js +16 -10
- package/dist/web/cli-options.d.ts +13 -0
- package/dist/web/cli-options.js +47 -0
- package/dist/web/host-daemon.d.ts +61 -0
- package/dist/web/host-daemon.js +193 -0
- package/dist/web/launch.d.ts +3 -0
- package/dist/web/launch.js +24 -0
- package/dist/web/protocol.d.ts +2 -0
- package/dist/web/protocol.js +1 -0
- package/dist/web/review-service.js +41 -13
- package/dist/web/server.js +69 -0
- package/dist/web/session-runtime-registry.d.ts +14 -0
- package/dist/web/session-runtime-registry.js +23 -0
- package/dist/web/workbench-controller.d.ts +55 -0
- package/dist/web/workbench-controller.js +224 -14
- package/dist/web-client/assets/{DiffViewer-D6mG729y.js → DiffViewer-MMhvMif-.js} +1 -1
- package/dist/web-client/assets/{FilesPanel-CCDoAZ1J.js → FilesPanel-4litEN1k.js} +1 -1
- package/dist/web-client/assets/{GitPanel-DtNgiH7A.js → GitPanel-BVwlDRVo.js} +1 -1
- package/dist/web-client/assets/ReviewPanel-ChA0Zd2g.js +1 -0
- package/dist/web-client/assets/{TerminalPanel-CNxMaBLp.js → TerminalPanel-D8C1uybv.js} +1 -1
- package/dist/web-client/assets/index-DQfpqPTD.css +1 -0
- package/dist/web-client/assets/index-Dv_uE9dk.js +17 -0
- package/dist/web-client/index.html +2 -2
- package/npm-shrinkwrap.json +356 -120
- package/package.json +3 -1
- package/dist/web-client/assets/ReviewPanel-CKEBSt6P.js +0 -1
- package/dist/web-client/assets/index-BBvNg3By.css +0 -1
- package/dist/web-client/assets/index-C6TDJwMC.js +0 -17
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DAEMON_POLL_MS = exports.DAEMON_READY_TIMEOUT_MS = exports.HOST_DAEMON_CHILD_ENV = exports.HOST_PIDFILE_BASE = void 0;
|
|
4
|
+
exports.hostLogsDirectory = hostLogsDirectory;
|
|
5
|
+
exports.hostPidfilePath = hostPidfilePath;
|
|
6
|
+
exports.parseHostPidfile = parseHostPidfile;
|
|
7
|
+
exports.readHostPidfile = readHostPidfile;
|
|
8
|
+
exports.writeHostPidfile = writeHostPidfile;
|
|
9
|
+
exports.clearHostPidfile = clearHostPidfile;
|
|
10
|
+
exports.isProcessAlive = isProcessAlive;
|
|
11
|
+
exports.hostDaemonStatus = hostDaemonStatus;
|
|
12
|
+
exports.spawnBackgroundHost = spawnBackgroundHost;
|
|
13
|
+
exports.stopBackgroundHost = stopBackgroundHost;
|
|
14
|
+
/**
|
|
15
|
+
* Native background/daemon lifecycle for `orion web` (issue #247, S1+S2).
|
|
16
|
+
*
|
|
17
|
+
* A foreground `orion web --background` parent spawns the real host detached,
|
|
18
|
+
* waits for it to publish a pidfile under the config home, then returns. The
|
|
19
|
+
* detached child redirects its stdio to per-host log files and keeps the
|
|
20
|
+
* fail-closed approval / context / sandbox semantics untouched — this is a
|
|
21
|
+
* process-lifecycle concern only, still bound to 127.0.0.1.
|
|
22
|
+
*
|
|
23
|
+
* The pidfile shape is intentionally tiny and human-readable:
|
|
24
|
+
*
|
|
25
|
+
* { "pid": 12345, "port": 3080, "url": "http://127.0.0.1:3080",
|
|
26
|
+
* "startedAt": 1725000000000, "workspace": "/abs/path" }
|
|
27
|
+
*/
|
|
28
|
+
const child_process_1 = require("child_process");
|
|
29
|
+
const fs_1 = require("fs");
|
|
30
|
+
const path_1 = require("path");
|
|
31
|
+
const paths_1 = require("../product/paths");
|
|
32
|
+
exports.HOST_PIDFILE_BASE = 'web-host';
|
|
33
|
+
exports.HOST_DAEMON_CHILD_ENV = 'ORION_WEB_DAEMON_CHILD';
|
|
34
|
+
exports.DAEMON_READY_TIMEOUT_MS = 8000;
|
|
35
|
+
exports.DAEMON_POLL_MS = 120;
|
|
36
|
+
function hostLogsDirectory() {
|
|
37
|
+
return (0, path_1.join)((0, paths_1.getConfigHome)(), 'logs');
|
|
38
|
+
}
|
|
39
|
+
function hostPidfilePath(port) {
|
|
40
|
+
return (0, path_1.join)((0, paths_1.getConfigHome)(), `${exports.HOST_PIDFILE_BASE}.${port}.pid`);
|
|
41
|
+
}
|
|
42
|
+
function parseHostPidfile(raw) {
|
|
43
|
+
try {
|
|
44
|
+
const value = JSON.parse(raw);
|
|
45
|
+
if (!value || typeof value !== 'object')
|
|
46
|
+
return null;
|
|
47
|
+
const record = value;
|
|
48
|
+
if (typeof record.pid !== 'number' ||
|
|
49
|
+
typeof record.port !== 'number' ||
|
|
50
|
+
typeof record.url !== 'string' ||
|
|
51
|
+
typeof record.workspace !== 'string' ||
|
|
52
|
+
typeof record.startedAt !== 'number') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
pid: record.pid,
|
|
57
|
+
port: record.port,
|
|
58
|
+
url: record.url,
|
|
59
|
+
workspace: record.workspace,
|
|
60
|
+
startedAt: record.startedAt,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readHostPidfile(port) {
|
|
68
|
+
const path = hostPidfilePath(port);
|
|
69
|
+
if (!(0, fs_1.existsSync)(path))
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
return parseHostPidfile((0, fs_1.readFileSync)(path, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function writeHostPidfile(pidfile) {
|
|
79
|
+
(0, fs_1.mkdirSync)((0, paths_1.getConfigHome)(), { recursive: true });
|
|
80
|
+
(0, fs_1.writeFileSync)(hostPidfilePath(pidfile.port), JSON.stringify(pidfile, null, 2), 'utf8');
|
|
81
|
+
}
|
|
82
|
+
function clearHostPidfile(port) {
|
|
83
|
+
try {
|
|
84
|
+
(0, fs_1.rmSync)(hostPidfilePath(port), { force: true });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Pidfile removal is best-effort: a locked/restricted filesystem must not
|
|
88
|
+
// turn `orion web stop` into a failure after the process is already gone.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Cross-platform process-liveness probe used by `status`. */
|
|
92
|
+
function isProcessAlive(pid) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 0);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return error.code === 'EPERM';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function hostDaemonStatus(port) {
|
|
102
|
+
const pidfile = readHostPidfile(port);
|
|
103
|
+
if (!pidfile)
|
|
104
|
+
return { state: 'stopped' };
|
|
105
|
+
if (isProcessAlive(pidfile.pid)) {
|
|
106
|
+
return { state: 'running', pidfile };
|
|
107
|
+
}
|
|
108
|
+
return { state: 'stale', pid: pidfile.pid, port };
|
|
109
|
+
}
|
|
110
|
+
/** Build the argument vector used to re-launch the current CLI detached. */
|
|
111
|
+
function childArgs(webArgs) {
|
|
112
|
+
const entry = process.argv[1];
|
|
113
|
+
if (!entry)
|
|
114
|
+
throw new Error('Unable to determine the CLI entry to relaunch.');
|
|
115
|
+
return [entry, 'web', ...webArgs];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parent-side entry for `--background`: spawn the host detached, redirect its
|
|
119
|
+
* stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
|
|
120
|
+
* foreground command can print the URL and exit cleanly.
|
|
121
|
+
*/
|
|
122
|
+
function spawnBackgroundHost(options) {
|
|
123
|
+
const logsDirectory = hostLogsDirectory();
|
|
124
|
+
(0, fs_1.mkdirSync)(logsDirectory, { recursive: true });
|
|
125
|
+
const logPath = (0, path_1.join)(logsDirectory, `web-${options.port}.log`);
|
|
126
|
+
const output = (0, fs_1.openSync)(logPath, 'a');
|
|
127
|
+
const child = (0, child_process_1.spawn)(process.execPath, childArgs(options.webArgs), {
|
|
128
|
+
cwd: options.cwd,
|
|
129
|
+
detached: true,
|
|
130
|
+
stdio: ['ignore', output, output],
|
|
131
|
+
env: { ...process.env, [exports.HOST_DAEMON_CHILD_ENV]: '1' },
|
|
132
|
+
});
|
|
133
|
+
child.unref();
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const deadline = Date.now() + exports.DAEMON_READY_TIMEOUT_MS;
|
|
136
|
+
const poll = () => {
|
|
137
|
+
const pidfile = readHostPidfile(options.port);
|
|
138
|
+
if (pidfile && pidfile.pid === child.pid) {
|
|
139
|
+
resolve({ child, pidfile });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
143
|
+
reject(new Error(`Background host exited before readiness (code ${child.exitCode ?? 'signal'}); see ${logPath}`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (Date.now() > deadline) {
|
|
147
|
+
reject(new Error(`Background host did not become ready within ${exports.DAEMON_READY_TIMEOUT_MS}ms; see ${logPath}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
setTimeout(poll, exports.DAEMON_POLL_MS);
|
|
151
|
+
};
|
|
152
|
+
poll();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Terminate a background host by pidfile (SIGTERM, then escalate to SIGKILL). */
|
|
156
|
+
function stopBackgroundHost(port) {
|
|
157
|
+
const status = hostDaemonStatus(port);
|
|
158
|
+
if (status.state !== 'running')
|
|
159
|
+
return Promise.resolve('not_running');
|
|
160
|
+
const { pidfile } = status;
|
|
161
|
+
return new Promise(resolve => {
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pidfile.pid, 'SIGTERM');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
clearHostPidfile(port);
|
|
167
|
+
resolve('stopped');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const deadline = Date.now() + 4000;
|
|
171
|
+
const poll = () => {
|
|
172
|
+
if (!isProcessAlive(pidfile.pid)) {
|
|
173
|
+
clearHostPidfile(port);
|
|
174
|
+
resolve('stopped');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (Date.now() > deadline) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pidfile.pid, 'SIGKILL');
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Already gone.
|
|
183
|
+
}
|
|
184
|
+
clearHostPidfile(port);
|
|
185
|
+
resolve('stopped');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
setTimeout(poll, 100);
|
|
189
|
+
};
|
|
190
|
+
poll();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=host-daemon.js.map
|
package/dist/web/launch.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ export interface RunOrionWebOptions {
|
|
|
2
2
|
readonly cwd: string;
|
|
3
3
|
readonly port?: number;
|
|
4
4
|
readonly open?: boolean;
|
|
5
|
+
readonly background?: boolean;
|
|
6
|
+
/** Full `orion web` arguments of the relaunched child (excludes --background). */
|
|
7
|
+
readonly backgroundArgs?: readonly string[];
|
|
5
8
|
readonly stdout?: Pick<NodeJS.WriteStream, 'write'>;
|
|
6
9
|
readonly stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
|
7
10
|
}
|
package/dist/web/launch.js
CHANGED
|
@@ -3,10 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runOrionWeb = runOrionWeb;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const server_1 = require("./server");
|
|
6
|
+
const host_daemon_1 = require("./host-daemon");
|
|
6
7
|
async function runOrionWeb(options) {
|
|
7
8
|
const stdout = options.stdout ?? process.stdout;
|
|
8
9
|
const stderr = options.stderr ?? process.stderr;
|
|
10
|
+
const isDaemonChild = process.env[host_daemon_1.HOST_DAEMON_CHILD_ENV] === '1';
|
|
11
|
+
const port = options.port ?? 3080;
|
|
12
|
+
if (options.background && !isDaemonChild) {
|
|
13
|
+
// Parent side: launch detached and wait for its pidfile, then print the URL.
|
|
14
|
+
const { pidfile } = await (0, host_daemon_1.spawnBackgroundHost)({
|
|
15
|
+
cwd: options.cwd,
|
|
16
|
+
port,
|
|
17
|
+
webArgs: options.backgroundArgs ?? [],
|
|
18
|
+
});
|
|
19
|
+
stdout.write(`Orion Code Web Workbench running in the background: ${pidfile.url}\n`);
|
|
20
|
+
stdout.write(`pid ${pidfile.pid} · logs ~/.orion-code/logs/web-${port}.log\n`);
|
|
21
|
+
stdout.write(`Manage it with: orion web status|stop [--port ${port}]\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
9
24
|
const handle = await (0, server_1.startOrionWebServer)({ cwd: options.cwd, port: options.port });
|
|
25
|
+
if (isDaemonChild) {
|
|
26
|
+
(0, host_daemon_1.writeHostPidfile)({
|
|
27
|
+
pid: process.pid,
|
|
28
|
+
port: handle.port,
|
|
29
|
+
url: handle.url,
|
|
30
|
+
workspace: handle.workbench.workspace,
|
|
31
|
+
startedAt: Date.now(),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
10
34
|
stdout.write(`Orion Code Web Workbench: ${handle.url}\n`);
|
|
11
35
|
stdout.write(`Workspace: ${handle.workbench.workspace}\n`);
|
|
12
36
|
if (options.open !== false) {
|
package/dist/web/protocol.d.ts
CHANGED
|
@@ -412,6 +412,8 @@ export interface WebSessionSummaryV1 {
|
|
|
412
412
|
readonly id: string;
|
|
413
413
|
readonly projectPath: string;
|
|
414
414
|
readonly name?: string;
|
|
415
|
+
/** v0.3.7 — User-assigned tags (optional, sorted as stored). */
|
|
416
|
+
readonly tags?: readonly string[];
|
|
415
417
|
readonly model: string;
|
|
416
418
|
readonly createdAt: string;
|
|
417
419
|
readonly updatedAt: string;
|
package/dist/web/protocol.js
CHANGED
|
@@ -464,6 +464,7 @@ function projectSessionSummary(session) {
|
|
|
464
464
|
messageCount: session.messageCount ?? 0,
|
|
465
465
|
}),
|
|
466
466
|
...(session.taskSummary ? { taskSummary: session.taskSummary } : {}),
|
|
467
|
+
...(session.tags && session.tags.length > 0 ? { tags: Object.freeze([...session.tags]) } : {}),
|
|
467
468
|
...(session.activeGoalId ? { activeGoalId: session.activeGoalId } : {}),
|
|
468
469
|
...(session.activeGoalObjective ? { activeGoalObjective: session.activeGoalObjective } : {}),
|
|
469
470
|
});
|
|
@@ -9,16 +9,26 @@ class ReviewServiceV1 {
|
|
|
9
9
|
this.listReceiptRefs = listReceiptRefs;
|
|
10
10
|
}
|
|
11
11
|
async snapshot() {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// v0.3.9 #237/#228 — collect every Git status page. A repository with more
|
|
13
|
+
// than the single pageSize of changed files was silently truncated before;
|
|
14
|
+
// the review overview must aggregate all pages (bounded by a guard that
|
|
15
|
+
// keeps runaway repositories from looping forever).
|
|
16
|
+
const [receiptRefs, statusPages] = await Promise.all([
|
|
14
17
|
this.listReceiptRefs(),
|
|
18
|
+
collectStatusPages((cursor) => this.git.status({ pageSize: 2000, ...(cursor ? { cursor } : {}) })),
|
|
15
19
|
]);
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const status = statusPages.at(-1) ?? (await this.git.status({ pageSize: 2000 }));
|
|
21
|
+
const conflicted = [];
|
|
22
|
+
const staged = [];
|
|
23
|
+
const unstaged = [];
|
|
24
|
+
const untracked = [];
|
|
25
|
+
for (const page of statusPages) {
|
|
26
|
+
conflicted.push(...page.conflicted);
|
|
27
|
+
staged.push(...page.staged);
|
|
28
|
+
unstaged.push(...page.unstaged);
|
|
29
|
+
untracked.push(...page.untracked);
|
|
30
|
+
}
|
|
31
|
+
const changedFiles = uniqueFiles([...conflicted, ...staged, ...unstaged, ...untracked]);
|
|
22
32
|
const verification = receiptRefs.slice(0, 100).map(receipt => Object.freeze({
|
|
23
33
|
callId: receipt.callId,
|
|
24
34
|
sessionId: receipt.sessionId,
|
|
@@ -46,11 +56,11 @@ class ReviewServiceV1 {
|
|
|
46
56
|
clean: status.clean,
|
|
47
57
|
changedFiles: Object.freeze(changedFiles),
|
|
48
58
|
totalChangedFiles: status.totalFiles,
|
|
49
|
-
stagedCount:
|
|
50
|
-
unstagedCount:
|
|
51
|
-
untrackedCount:
|
|
52
|
-
conflictCount:
|
|
53
|
-
truncated:
|
|
59
|
+
stagedCount: staged.length,
|
|
60
|
+
unstagedCount: unstaged.length,
|
|
61
|
+
untrackedCount: untracked.length,
|
|
62
|
+
conflictCount: conflicted.length,
|
|
63
|
+
truncated: statusPages.at(-1)?.truncated ?? false,
|
|
54
64
|
verification: Object.freeze(verification),
|
|
55
65
|
});
|
|
56
66
|
}
|
|
@@ -72,4 +82,22 @@ function uniqueFiles(files) {
|
|
|
72
82
|
byId.set(file.fileId, file);
|
|
73
83
|
return [...byId.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
74
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* v0.3.9 #237/#228 — page through Git status until the snapshot is fully
|
|
87
|
+
* collected. The bounded guard (64 pages x 2_000 = 128k files) exists to stop
|
|
88
|
+
* runaway repositories from looping; a repository exceeding it stays marked
|
|
89
|
+
* truncated so the UI can still communicate the limit.
|
|
90
|
+
*/
|
|
91
|
+
async function collectStatusPages(fetchPage) {
|
|
92
|
+
const pages = [];
|
|
93
|
+
let page = await fetchPage(undefined);
|
|
94
|
+
pages.push(page);
|
|
95
|
+
let guard = 0;
|
|
96
|
+
while (page.truncated && page.nextCursor && guard < 64) {
|
|
97
|
+
page = await fetchPage(page.nextCursor);
|
|
98
|
+
pages.push(page);
|
|
99
|
+
guard += 1;
|
|
100
|
+
}
|
|
101
|
+
return pages;
|
|
102
|
+
}
|
|
75
103
|
//# sourceMappingURL=review-service.js.map
|
package/dist/web/server.js
CHANGED
|
@@ -112,6 +112,13 @@ async function handleRequest(context) {
|
|
|
112
112
|
sendJson(response, 200, collectionPage(url, `workspace-sessions-${workspaceId}`, context.workbench.listWorkspaceSessions(workspaceId, contextGuard), session => session.id));
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
|
+
const archivedSessionsMatch = path.match(/^\/workspaces\/([^/]+)\/sessions\/archived$/);
|
|
116
|
+
if (method === 'GET' && archivedSessionsMatch) {
|
|
117
|
+
const workspaceId = safeDecodePathSegment(archivedSessionsMatch[1]);
|
|
118
|
+
const contextGuard = requireContextGuardQuery(url);
|
|
119
|
+
sendJson(response, 200, collectionPage(url, `workspace-sessions-archived-${workspaceId}`, context.workbench.listArchivedWorkspaceSessions(workspaceId, contextGuard), session => session.id));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
115
122
|
const workspaceSummaryMatch = path.match(/^\/workspaces\/([^/]+)\/summary$/);
|
|
116
123
|
if (method === 'GET' && workspaceSummaryMatch) {
|
|
117
124
|
const workspaceId = requireUuid(safeDecodePathSegment(workspaceSummaryMatch[1]), 'workspaceId');
|
|
@@ -279,6 +286,68 @@ async function handleRequest(context) {
|
|
|
279
286
|
sendJson(response, 200, result);
|
|
280
287
|
return;
|
|
281
288
|
}
|
|
289
|
+
// v0.3.7 — Session tags / archive / restore / delete. One mutation family:
|
|
290
|
+
// every body carries a requestId so the idempotency ledger dedupes retries.
|
|
291
|
+
const sessionTagsMatch = path.match(/^\/sessions\/([^/]+)\/tags$/);
|
|
292
|
+
if (method === 'POST' && sessionTagsMatch) {
|
|
293
|
+
assertMutation(request, context.nonce, context.origin);
|
|
294
|
+
const body = requireRecord(await readJson(request), 'Session tags request');
|
|
295
|
+
assertOnlyKeys(body, ['requestId', 'tags', 'expectedContextRevision', 'workspaceId']);
|
|
296
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
297
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
298
|
+
const sessionId = safeDecodePathSegment(sessionTagsMatch[1]);
|
|
299
|
+
const result = await context.workbench.executeMutation(requestId, 'session.tags', { sessionId, tags: body.tags, ...contextGuard }, () => ({
|
|
300
|
+
requestId,
|
|
301
|
+
session: context.workbench.setSessionTags(sessionId, body.tags, contextGuard),
|
|
302
|
+
}));
|
|
303
|
+
sendJson(response, 200, result);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const sessionArchiveMatch = path.match(/^\/sessions\/([^/]+)\/archive$/);
|
|
307
|
+
if (method === 'POST' && sessionArchiveMatch) {
|
|
308
|
+
assertMutation(request, context.nonce, context.origin);
|
|
309
|
+
const body = requireRecord(await readJson(request), 'Session archive request');
|
|
310
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
311
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
312
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
313
|
+
const sessionId = safeDecodePathSegment(sessionArchiveMatch[1]);
|
|
314
|
+
const result = await context.workbench.executeMutation(requestId, 'session.archive', { sessionId, ...contextGuard }, () => ({
|
|
315
|
+
requestId,
|
|
316
|
+
session: context.workbench.archiveSession(sessionId, contextGuard),
|
|
317
|
+
}));
|
|
318
|
+
sendJson(response, 200, result);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const sessionRestoreMatch = path.match(/^\/sessions\/([^/]+)\/restore$/);
|
|
322
|
+
if (method === 'POST' && sessionRestoreMatch) {
|
|
323
|
+
assertMutation(request, context.nonce, context.origin);
|
|
324
|
+
const body = requireRecord(await readJson(request), 'Session restore request');
|
|
325
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
326
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
327
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
328
|
+
const sessionId = safeDecodePathSegment(sessionRestoreMatch[1]);
|
|
329
|
+
const result = await context.workbench.executeMutation(requestId, 'session.restore', { sessionId, ...contextGuard }, () => ({
|
|
330
|
+
requestId,
|
|
331
|
+
session: context.workbench.restoreSession(sessionId, contextGuard),
|
|
332
|
+
}));
|
|
333
|
+
sendJson(response, 200, result);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const sessionDeleteMatch = path.match(/^\/sessions\/([^/]+)$/);
|
|
337
|
+
if (method === 'DELETE' && sessionDeleteMatch) {
|
|
338
|
+
assertMutation(request, context.nonce, context.origin);
|
|
339
|
+
const body = requireRecord(await readJson(request), 'Session delete request');
|
|
340
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
341
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
342
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
343
|
+
const sessionId = safeDecodePathSegment(sessionDeleteMatch[1]);
|
|
344
|
+
const result = await context.workbench.executeMutation(requestId, 'session.delete', { sessionId, ...contextGuard }, () => ({
|
|
345
|
+
requestId,
|
|
346
|
+
deleted: context.workbench.deleteSession(sessionId, contextGuard).deleted,
|
|
347
|
+
}));
|
|
348
|
+
sendJson(response, 200, result);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
282
351
|
if (method === 'POST' && path === '/commands') {
|
|
283
352
|
assertMutation(request, context.nonce, context.origin);
|
|
284
353
|
sendJson(response, 202, await context.workbench.dispatch(await readJson(request)));
|
|
@@ -65,9 +65,23 @@ export declare class WebSessionRuntimeRegistryV1<TActor extends object> {
|
|
|
65
65
|
private ordinal;
|
|
66
66
|
private runningCountValue;
|
|
67
67
|
private closed;
|
|
68
|
+
private actorsCreatedCount;
|
|
69
|
+
private actorsClosedCount;
|
|
70
|
+
private actorsEvictedCount;
|
|
68
71
|
constructor(options: WebSessionRuntimeRegistryOptionsV1<TActor>);
|
|
69
72
|
get runningCount(): number;
|
|
70
73
|
get residentCount(): number;
|
|
74
|
+
/**
|
|
75
|
+
* Cumulative actor pool counters since the registry was created. E2E and
|
|
76
|
+
* diagnostics compare two reads to prove a pure Session selection allocated
|
|
77
|
+
* no Session actor (actorsCreated delta 0) and performed no capacity
|
|
78
|
+
* eviction (actorsEvicted delta 0) while the pool keeps actors alive.
|
|
79
|
+
*/
|
|
80
|
+
stats(): {
|
|
81
|
+
readonly actorsCreated: number;
|
|
82
|
+
readonly actorsClosed: number;
|
|
83
|
+
readonly actorsEvicted: number;
|
|
84
|
+
};
|
|
71
85
|
summary(key: WebSessionActorKeyV1): WebSessionRuntimeSummaryV1;
|
|
72
86
|
summaries(): readonly WebSessionRuntimeSummaryV1[];
|
|
73
87
|
/**
|
|
@@ -21,6 +21,12 @@ class WebSessionRuntimeRegistryV1 {
|
|
|
21
21
|
this.ordinal = 0;
|
|
22
22
|
this.runningCountValue = 0;
|
|
23
23
|
this.closed = false;
|
|
24
|
+
// Host-level actor pool counters. Allocations are the authoritative signal
|
|
25
|
+
// for "pure selection must not start a Session Runtime" diagnostics: an E2E
|
|
26
|
+
// reads these before/after a switch and asserts a zero delta.
|
|
27
|
+
this.actorsCreatedCount = 0;
|
|
28
|
+
this.actorsClosedCount = 0;
|
|
29
|
+
this.actorsEvictedCount = 0;
|
|
24
30
|
this.maxRunningSessions = boundedInteger(options.maxRunningSessions, DEFAULT_MAX_RUNNING_SESSIONS, 1, 8, 'maxRunningSessions');
|
|
25
31
|
this.maxResidentSessionActors = boundedInteger(options.maxResidentSessionActors, DEFAULT_MAX_RESIDENT_SESSION_ACTORS, 1, 8, 'maxResidentSessionActors');
|
|
26
32
|
if (this.maxResidentSessionActors < this.maxRunningSessions) {
|
|
@@ -39,6 +45,19 @@ class WebSessionRuntimeRegistryV1 {
|
|
|
39
45
|
count += 1;
|
|
40
46
|
return count;
|
|
41
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Cumulative actor pool counters since the registry was created. E2E and
|
|
50
|
+
* diagnostics compare two reads to prove a pure Session selection allocated
|
|
51
|
+
* no Session actor (actorsCreated delta 0) and performed no capacity
|
|
52
|
+
* eviction (actorsEvicted delta 0) while the pool keeps actors alive.
|
|
53
|
+
*/
|
|
54
|
+
stats() {
|
|
55
|
+
return {
|
|
56
|
+
actorsCreated: this.actorsCreatedCount,
|
|
57
|
+
actorsClosed: this.actorsClosedCount,
|
|
58
|
+
actorsEvicted: this.actorsEvictedCount,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
42
61
|
summary(key) {
|
|
43
62
|
return this.snapshot(this.record(key));
|
|
44
63
|
}
|
|
@@ -190,6 +209,7 @@ class WebSessionRuntimeRegistryV1 {
|
|
|
190
209
|
await this.options.closeActor(actor, reason);
|
|
191
210
|
record.actor = undefined;
|
|
192
211
|
record.estimatedBytes = 0;
|
|
212
|
+
this.actorsClosedCount += 1;
|
|
193
213
|
this.transition(record, wasActive ? 'interrupted' : 'cold');
|
|
194
214
|
}
|
|
195
215
|
catch (error) {
|
|
@@ -247,6 +267,7 @@ class WebSessionRuntimeRegistryV1 {
|
|
|
247
267
|
record.actorReady = true;
|
|
248
268
|
actor = undefined;
|
|
249
269
|
record.failure = undefined;
|
|
270
|
+
this.actorsCreatedCount += 1;
|
|
250
271
|
this.transition(record, 'idle');
|
|
251
272
|
return record.actor;
|
|
252
273
|
}
|
|
@@ -286,6 +307,7 @@ class WebSessionRuntimeRegistryV1 {
|
|
|
286
307
|
throw new WebSessionRuntimeRegistryError(503, 'session_concurrency_limit', 'All resident Session actors are busy; the requested Session remains available read-only.');
|
|
287
308
|
}
|
|
288
309
|
await this.closeResidentLocked(candidate, 'idle Session actor evicted by LRU', 'cold');
|
|
310
|
+
this.actorsEvictedCount += 1;
|
|
289
311
|
}
|
|
290
312
|
isEvictable(record) {
|
|
291
313
|
return (Boolean(record.actor) &&
|
|
@@ -304,6 +326,7 @@ class WebSessionRuntimeRegistryV1 {
|
|
|
304
326
|
await this.options.closeActor(actor, reason);
|
|
305
327
|
record.actor = undefined;
|
|
306
328
|
record.estimatedBytes = 0;
|
|
329
|
+
this.actorsClosedCount += 1;
|
|
307
330
|
this.transition(record, terminalPhase);
|
|
308
331
|
}
|
|
309
332
|
catch (error) {
|
|
@@ -26,6 +26,13 @@ export declare class WebWorkbenchController {
|
|
|
26
26
|
private readonly createRuntime;
|
|
27
27
|
private readonly createSessionRuntime;
|
|
28
28
|
private sessionRegistry;
|
|
29
|
+
/**
|
|
30
|
+
* Workspace-scoped shared kernels owned by this Host. Control planes and
|
|
31
|
+
* Session actors both borrow from this pool, so a Workspace Context switch
|
|
32
|
+
* never tears down kernels that resident actors still reference. The pool is
|
|
33
|
+
* released only during Host shutdown.
|
|
34
|
+
*/
|
|
35
|
+
private readonly workspaceKernels;
|
|
29
36
|
private readonly workspaceRegistry;
|
|
30
37
|
readonly terminalManager: TerminalManagerV1;
|
|
31
38
|
private readonly mutationResults;
|
|
@@ -52,6 +59,13 @@ export declare class WebWorkbenchController {
|
|
|
52
59
|
private contextTransitionReserved;
|
|
53
60
|
private closed;
|
|
54
61
|
private shutdownResult?;
|
|
62
|
+
/**
|
|
63
|
+
* Host-observed Web activity counters. Exposed through diagnostics() so E2E
|
|
64
|
+
* and the diagnostics panel can prove that a pure Session selection issues
|
|
65
|
+
* no control-plane reinstall (controlPlaneInstalls delta 0) and that
|
|
66
|
+
* snapshots stay bounded and fast (snapshotTotalMs / snapshotLastMs).
|
|
67
|
+
*/
|
|
68
|
+
private readonly sessionActivity;
|
|
55
69
|
private workspaceWatchers;
|
|
56
70
|
private resourceInvalidationTimer?;
|
|
57
71
|
private constructor();
|
|
@@ -64,11 +78,15 @@ export declare class WebWorkbenchController {
|
|
|
64
78
|
settings(): WebSettingsSnapshotV1;
|
|
65
79
|
listWorkspaces(context?: WebContextGuardV1): readonly WebWorkspaceSummaryV1[];
|
|
66
80
|
listWorkspaceSessions(workspaceId: string, context?: WebContextGuardV1): readonly WebSessionSummaryV1[];
|
|
81
|
+
/** Archived sessions of a Workspace (v0.3.7), newest archive first. */
|
|
82
|
+
listArchivedWorkspaceSessions(workspaceId: string, context?: WebContextGuardV1): readonly WebSessionSummaryV1[];
|
|
67
83
|
workspaceProjectSummary(workspaceId: string, context?: WebContextGuardV1): Promise<WebWorkspaceProjectSummaryV1>;
|
|
68
84
|
setWorkspacePinned(workspaceId: string, pinned: boolean, context: WebContextGuardV1): WebWorkspaceSummaryV1;
|
|
69
85
|
removeWorkspace(workspaceId: string, context: WebContextGuardV1): void;
|
|
70
86
|
listSessions(context?: WebContextGuardV1): readonly WebSessionSummaryV1[];
|
|
71
87
|
sessionSnapshot(sessionId: string, cursor?: string, pageSize?: number, tail?: boolean, context?: WebContextGuardV1): WebSessionSnapshotV1;
|
|
88
|
+
private snapshotSessionV1;
|
|
89
|
+
private recordSessionSnapshotActivity;
|
|
72
90
|
composerState(sessionId: string, context?: WebContextGuardV1): WebComposerControlStateV1;
|
|
73
91
|
sessionRuntimeSummary(sessionId: string): WebSessionRuntimeSummaryV1;
|
|
74
92
|
waitForSessionIdle(sessionId: string): Promise<void>;
|
|
@@ -90,6 +108,22 @@ export declare class WebWorkbenchController {
|
|
|
90
108
|
createSession(name?: string, context?: WebContextGuardV1): Promise<WebSessionSummaryV1>;
|
|
91
109
|
activateSession(sessionId: string, context?: WebContextGuardV1): Promise<WebCommandResultV1>;
|
|
92
110
|
renameSession(sessionId: string, name: string, context?: WebContextGuardV1): WebSessionSummaryV1;
|
|
111
|
+
setSessionTags(sessionId: string, tags: unknown, context?: WebContextGuardV1): WebSessionSummaryV1;
|
|
112
|
+
archiveSession(sessionId: string, context?: WebContextGuardV1): WebSessionSummaryV1;
|
|
113
|
+
restoreSession(sessionId: string, context?: WebContextGuardV1): WebSessionSummaryV1;
|
|
114
|
+
deleteSession(sessionId: string, context?: WebContextGuardV1): {
|
|
115
|
+
readonly deleted: boolean;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* A busy Session (starting / queued / running / waiting approval / stopping)
|
|
119
|
+
* must not be archived or deleted while its actor is doing work.
|
|
120
|
+
*/
|
|
121
|
+
private assertSessionNotBusy;
|
|
122
|
+
/**
|
|
123
|
+
* Deletion removes files under the Session while a resident actor may still
|
|
124
|
+
* hold them open — require that no actor exists at all (phase 'cold').
|
|
125
|
+
*/
|
|
126
|
+
private assertSessionHasNoActor;
|
|
93
127
|
switchWorkspace(path: string, context: WebContextGuardV1): Promise<void>;
|
|
94
128
|
activateContext(input: Omit<WebContextActivateRequestV1, 'requestId'>): Promise<void>;
|
|
95
129
|
dispatch(raw: unknown): Promise<WebCommandResultV1>;
|
|
@@ -102,6 +136,27 @@ export declare class WebWorkbenchController {
|
|
|
102
136
|
diagnostics(context?: WebContextGuardV1): Promise<Record<string, unknown>>;
|
|
103
137
|
shutdown(): Promise<void>;
|
|
104
138
|
private shutdownOwnedResources;
|
|
139
|
+
/**
|
|
140
|
+
* Return the workspace kernel for `cwd`, creating and pooling one on first
|
|
141
|
+
* use. Control planes and Session actors borrow the same kernel instance per
|
|
142
|
+
* canonical Workspace; the pool is released only when the Host shuts down.
|
|
143
|
+
*/
|
|
144
|
+
private workspaceKernelFor;
|
|
145
|
+
/**
|
|
146
|
+
* Settings invalidations from pooled kernels are only forwarded while their
|
|
147
|
+
* Workspace is the active Context. A background Workspace's coordinator
|
|
148
|
+
* keeps watching its own durable file; its next activation publishes a fresh
|
|
149
|
+
* Settings document through `emitSettingsWorkspaceChange()`.
|
|
150
|
+
*/
|
|
151
|
+
private onWorkspaceKernelSettingsInvalidated;
|
|
152
|
+
/**
|
|
153
|
+
* Release the Host owner seat on every pooled kernel. Runtime shutdowns have
|
|
154
|
+
* already disposed their Settings participants (actors first, then the
|
|
155
|
+
* active control plane), so an unowned kernel with zero participants closes
|
|
156
|
+
* its Settings coordinator here. A kernel that still has a running actor
|
|
157
|
+
* participant survives until that actor actually terminates.
|
|
158
|
+
*/
|
|
159
|
+
private releaseWorkspaceKernels;
|
|
105
160
|
private createSessionRegistry;
|
|
106
161
|
private createSessionActor;
|
|
107
162
|
private closeSessionActor;
|