@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +59 -5
  3. package/README.zh-CN.md +6 -3
  4. package/dist/cli.js +58 -32
  5. package/dist/commands/context-tool-command-handlers.js +1 -5
  6. package/dist/commands/registry.js +1 -4
  7. package/dist/harness/intent.js +69 -16
  8. package/dist/harness/turn-summary.js +3 -1
  9. package/dist/memory/embeddings.js +2 -3
  10. package/dist/memory/prompts.js +1 -1
  11. package/dist/memory/relevant-finder.js +125 -15
  12. package/dist/memory/vector-store.js +13 -8
  13. package/dist/migration/migrate.js +8 -12
  14. package/dist/runtime/composer/history.js +1 -3
  15. package/dist/runtime/composer/layout.js +7 -4
  16. package/dist/runtime/legacy-thread-materializer.d.ts +3 -1
  17. package/dist/runtime/legacy-thread-materializer.js +15 -3
  18. package/dist/runtime/product-bootstrap.d.ts +12 -0
  19. package/dist/runtime/product-bootstrap.js +37 -22
  20. package/dist/runtime/rich-text/ansi-parser.js +12 -2
  21. package/dist/runtime/rich-text/layout.js +15 -19
  22. package/dist/runtime/subagents/budget.js +4 -1
  23. package/dist/runtime/subagents/policy.js +11 -4
  24. package/dist/runtime/subagents/provider-gate.js +6 -4
  25. package/dist/runtime/subagents/research-quality.js +3 -1
  26. package/dist/runtime/subagents/research-renderer.js +9 -2
  27. package/dist/runtime/subagents/result-parser.js +18 -7
  28. package/dist/runtime/thread-event-store.d.ts +5 -3
  29. package/dist/runtime/thread-event-store.js +29 -8
  30. package/dist/runtime/thread-projection.d.ts +17 -0
  31. package/dist/runtime/thread-projection.js +42 -1
  32. package/dist/runtime/tool-output-presentation.js +7 -7
  33. package/dist/runtime/turn-controller.js +1 -2
  34. package/dist/services/auth/auth.js +1 -4
  35. package/dist/services/auth/aws.js +24 -14
  36. package/dist/services/auto-fix/autoFixRunner.js +1 -1
  37. package/dist/services/auto-fix/index.d.ts +1 -1
  38. package/dist/services/diagnostic-tracking/diagnosticTracking.js +4 -2
  39. package/dist/services/diagnostic-tracking/index.d.ts +1 -1
  40. package/dist/services/effort.js +1 -9
  41. package/dist/services/file-glob.js +3 -4
  42. package/dist/services/mcp/transports.js +4 -4
  43. package/dist/services/model-catalog.js +179 -30
  44. package/dist/services/model-context.js +1 -1
  45. package/dist/services/provider-resilience/stream-recovery.js +1 -3
  46. package/dist/services/redaction.js +8 -0
  47. package/dist/services/session-storage.d.ts +28 -3
  48. package/dist/services/session-storage.js +70 -6
  49. package/dist/services/smart-routing.js +19 -11
  50. package/dist/services/verification-profile.js +17 -22
  51. package/dist/services/web-search-provider.js +21 -9
  52. package/dist/services/workspace-diff.js +10 -4
  53. package/dist/skills/builtin/code-review/SKILL.md +1 -1
  54. package/dist/skills/builtin/security-check/SKILL.md +1 -1
  55. package/dist/skills/builtin/test-gen/SKILL.md +1 -1
  56. package/dist/terminal-ui/editor.js +6 -2
  57. package/dist/terminal-ui/output-queue.js +9 -3
  58. package/dist/tools/lsp.js +12 -6
  59. package/dist/tui-core/frame.js +9 -7
  60. package/dist/tui-ui/transcript-cache.js +6 -6
  61. package/dist/tui-ui/transcript-inspector-surface.js +4 -1
  62. package/dist/tui-ui/transcript-inspector.js +3 -8
  63. package/dist/ui/box.js +14 -7
  64. package/dist/ui/markdown.js +4 -2
  65. package/dist/ui/shared/input-frame.js +12 -8
  66. package/dist/ui/stream-markdown.js +1 -1
  67. package/dist/ui/tool-preview.js +11 -3
  68. package/dist/ui/user-input.js +16 -10
  69. package/dist/web/cli-options.d.ts +13 -0
  70. package/dist/web/cli-options.js +47 -0
  71. package/dist/web/host-daemon.d.ts +61 -0
  72. package/dist/web/host-daemon.js +193 -0
  73. package/dist/web/launch.d.ts +3 -0
  74. package/dist/web/launch.js +24 -0
  75. package/dist/web/protocol.d.ts +2 -0
  76. package/dist/web/protocol.js +1 -0
  77. package/dist/web/review-service.js +41 -13
  78. package/dist/web/server.js +69 -0
  79. package/dist/web/session-runtime-registry.d.ts +14 -0
  80. package/dist/web/session-runtime-registry.js +23 -0
  81. package/dist/web/workbench-controller.d.ts +55 -0
  82. package/dist/web/workbench-controller.js +224 -14
  83. package/dist/web-client/assets/{DiffViewer-D6mG729y.js → DiffViewer-MMhvMif-.js} +1 -1
  84. package/dist/web-client/assets/{FilesPanel-CCDoAZ1J.js → FilesPanel-4litEN1k.js} +1 -1
  85. package/dist/web-client/assets/{GitPanel-DtNgiH7A.js → GitPanel-BVwlDRVo.js} +1 -1
  86. package/dist/web-client/assets/ReviewPanel-ChA0Zd2g.js +1 -0
  87. package/dist/web-client/assets/{TerminalPanel-CNxMaBLp.js → TerminalPanel-D8C1uybv.js} +1 -1
  88. package/dist/web-client/assets/index-DQfpqPTD.css +1 -0
  89. package/dist/web-client/assets/index-Dv_uE9dk.js +17 -0
  90. package/dist/web-client/index.html +2 -2
  91. package/npm-shrinkwrap.json +356 -120
  92. package/package.json +3 -1
  93. package/dist/web-client/assets/ReviewPanel-CKEBSt6P.js +0 -1
  94. package/dist/web-client/assets/index-BBvNg3By.css +0 -1
  95. 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
@@ -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
  }
@@ -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) {
@@ -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;
@@ -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
- const [status, receiptRefs] = await Promise.all([
13
- this.git.status({ pageSize: 2000 }),
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 changedFiles = uniqueFiles([
17
- ...status.conflicted,
18
- ...status.staged,
19
- ...status.unstaged,
20
- ...status.untracked,
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: status.staged.length,
50
- unstagedCount: status.unstaged.length,
51
- untrackedCount: status.untracked.length,
52
- conflictCount: status.conflicted.length,
53
- truncated: status.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
@@ -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;