@orion-agents/orion-code 0.3.4 → 0.3.7

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 (86) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +61 -5
  3. package/README.zh-CN.md +7 -3
  4. package/dist/commands/context-tool-command-handlers.js +1 -5
  5. package/dist/commands/registry.js +1 -4
  6. package/dist/harness/intent.js +69 -16
  7. package/dist/harness/turn-summary.js +3 -1
  8. package/dist/memory/embeddings.js +2 -3
  9. package/dist/memory/prompts.js +1 -1
  10. package/dist/memory/relevant-finder.js +125 -15
  11. package/dist/memory/vector-store.js +13 -8
  12. package/dist/migration/migrate.js +8 -12
  13. package/dist/runtime/composer/history.js +1 -3
  14. package/dist/runtime/composer/layout.js +7 -4
  15. package/dist/runtime/legacy-thread-materializer.d.ts +3 -1
  16. package/dist/runtime/legacy-thread-materializer.js +15 -3
  17. package/dist/runtime/product-bootstrap.d.ts +12 -0
  18. package/dist/runtime/product-bootstrap.js +37 -22
  19. package/dist/runtime/rich-text/ansi-parser.js +12 -2
  20. package/dist/runtime/rich-text/layout.js +15 -19
  21. package/dist/runtime/subagents/budget.js +4 -1
  22. package/dist/runtime/subagents/policy.js +11 -4
  23. package/dist/runtime/subagents/provider-gate.js +6 -4
  24. package/dist/runtime/subagents/research-quality.js +3 -1
  25. package/dist/runtime/subagents/research-renderer.js +9 -2
  26. package/dist/runtime/subagents/result-parser.js +18 -7
  27. package/dist/runtime/thread-event-store.d.ts +5 -3
  28. package/dist/runtime/thread-event-store.js +29 -8
  29. package/dist/runtime/thread-projection.d.ts +17 -0
  30. package/dist/runtime/thread-projection.js +42 -1
  31. package/dist/runtime/tool-output-presentation.js +7 -7
  32. package/dist/runtime/turn-controller.js +1 -2
  33. package/dist/services/auth/auth.js +1 -4
  34. package/dist/services/auth/aws.js +24 -14
  35. package/dist/services/auto-fix/autoFixRunner.js +1 -1
  36. package/dist/services/auto-fix/index.d.ts +1 -1
  37. package/dist/services/diagnostic-tracking/diagnosticTracking.js +4 -2
  38. package/dist/services/diagnostic-tracking/index.d.ts +1 -1
  39. package/dist/services/effort.js +1 -9
  40. package/dist/services/file-glob.js +3 -4
  41. package/dist/services/mcp/transports.js +4 -4
  42. package/dist/services/model-catalog.js +179 -30
  43. package/dist/services/model-context.js +1 -1
  44. package/dist/services/provider-resilience/stream-recovery.js +1 -3
  45. package/dist/services/session-storage.d.ts +28 -3
  46. package/dist/services/session-storage.js +70 -6
  47. package/dist/services/smart-routing.js +19 -11
  48. package/dist/services/verification-profile.js +17 -22
  49. package/dist/services/web-search-provider.js +21 -9
  50. package/dist/services/workspace-diff.js +10 -4
  51. package/dist/skills/builtin/code-review/SKILL.md +1 -1
  52. package/dist/skills/builtin/security-check/SKILL.md +1 -1
  53. package/dist/skills/builtin/test-gen/SKILL.md +1 -1
  54. package/dist/terminal-ui/editor.js +6 -2
  55. package/dist/terminal-ui/output-queue.js +9 -3
  56. package/dist/tools/lsp.js +12 -6
  57. package/dist/tui-core/frame.js +9 -7
  58. package/dist/tui-ui/transcript-cache.js +6 -6
  59. package/dist/tui-ui/transcript-inspector-surface.js +4 -1
  60. package/dist/tui-ui/transcript-inspector.js +3 -8
  61. package/dist/ui/box.js +14 -7
  62. package/dist/ui/markdown.js +4 -2
  63. package/dist/ui/shared/input-frame.js +12 -8
  64. package/dist/ui/stream-markdown.js +1 -1
  65. package/dist/ui/tool-preview.js +11 -3
  66. package/dist/ui/user-input.js +16 -10
  67. package/dist/web/protocol.d.ts +2 -0
  68. package/dist/web/protocol.js +1 -0
  69. package/dist/web/server.js +69 -0
  70. package/dist/web/session-runtime-registry.d.ts +14 -0
  71. package/dist/web/session-runtime-registry.js +23 -0
  72. package/dist/web/workbench-controller.d.ts +55 -0
  73. package/dist/web/workbench-controller.js +224 -14
  74. package/dist/web-client/assets/{DiffViewer-D6mG729y.js → DiffViewer-DqbZ02RN.js} +1 -1
  75. package/dist/web-client/assets/{FilesPanel-CCDoAZ1J.js → FilesPanel-Bu1Ab2kJ.js} +1 -1
  76. package/dist/web-client/assets/{GitPanel-DtNgiH7A.js → GitPanel-BixIWb4k.js} +1 -1
  77. package/dist/web-client/assets/ReviewPanel-DsqJpku_.js +1 -0
  78. package/dist/web-client/assets/{TerminalPanel-CNxMaBLp.js → TerminalPanel-DdjQP2mp.js} +1 -1
  79. package/dist/web-client/assets/index-30GT9Vnx.js +17 -0
  80. package/dist/web-client/assets/index-DQfpqPTD.css +1 -0
  81. package/dist/web-client/index.html +2 -2
  82. package/npm-shrinkwrap.json +356 -120
  83. package/package.json +3 -1
  84. package/dist/web-client/assets/ReviewPanel-CKEBSt6P.js +0 -1
  85. package/dist/web-client/assets/index-BBvNg3By.css +0 -1
  86. package/dist/web-client/assets/index-C6TDJwMC.js +0 -17
@@ -97,12 +97,15 @@ function unquoteGitPath(path) {
97
97
  function parseNameStatus(output) {
98
98
  if (!output.trim())
99
99
  return [];
100
- return output.split('\n')
100
+ return output
101
+ .split('\n')
101
102
  .filter(Boolean)
102
103
  .map(line => {
103
104
  const parts = line.split('\t');
104
105
  const status = parts[0] || '?';
105
- const rawPath = parts.length > 2 ? `${parts[1]} -> ${parts[2]}` : parts[1] || line.slice(status.length).trim();
106
+ const rawPath = parts.length > 2
107
+ ? `${parts[1]} -> ${parts[2]}`
108
+ : parts[1] || line.slice(status.length).trim();
106
109
  const path = parts.length > 2
107
110
  ? `${unquoteGitPath(parts[1])} -> ${unquoteGitPath(parts[2])}`
108
111
  : unquoteGitPath(rawPath);
@@ -112,7 +115,8 @@ function parseNameStatus(output) {
112
115
  function parseUntracked(output) {
113
116
  if (!output.trim())
114
117
  return [];
115
- return output.split('\n')
118
+ return output
119
+ .split('\n')
116
120
  .filter(Boolean)
117
121
  .map(path => ({ status: '??', path: unquoteGitPath(path) }));
118
122
  }
@@ -178,7 +182,9 @@ function formatWorkspaceDiff(report, options = {}) {
178
182
  `CWD ${report.cwd}`,
179
183
  'Status not a git repository',
180
184
  report.error ? `Error ${report.error}` : '',
181
- ].filter(Boolean).join('\n');
185
+ ]
186
+ .filter(Boolean)
187
+ .join('\n');
182
188
  }
183
189
  const lines = [
184
190
  'Workspace Diff',
@@ -48,4 +48,4 @@ Trigger: `/review [path]` or `/review all`
48
48
 
49
49
  - Summary of findings
50
50
  - Specific recommendations
51
- - Priority level (Critical/High/Medium/Low)
51
+ - Priority level (Critical/High/Medium/Low)
@@ -51,4 +51,4 @@ Trigger: `/security [path]`
51
51
  - Vulnerability type
52
52
  - Location in code
53
53
  - Severity (Critical/High/Medium/Low)
54
- - Remediation steps
54
+ - Remediation steps
@@ -46,4 +46,4 @@ Trigger: `/test-gen [path]` or `/test-gen [function]`
46
46
  - Test file path
47
47
  - Test cases generated
48
48
  - Coverage estimate
49
- - Recommended assertions
49
+ - Recommended assertions
@@ -22,7 +22,9 @@ function openExternalEditor(options = {}) {
22
22
  try {
23
23
  (0, fs_1.chmodSync)(file, 0o600);
24
24
  }
25
- catch { /* best effort */ }
25
+ catch {
26
+ /* best effort */
27
+ }
26
28
  const result = spawn(editor, [file], {
27
29
  stdio: 'inherit',
28
30
  shell: true,
@@ -47,7 +49,9 @@ function openExternalEditor(options = {}) {
47
49
  try {
48
50
  (0, fs_1.rmSync)(dir, { recursive: true, force: true });
49
51
  }
50
- catch { /* best effort */ }
52
+ catch {
53
+ /* best effort */
54
+ }
51
55
  }
52
56
  }
53
57
  //# sourceMappingURL=editor.js.map
@@ -86,13 +86,17 @@ class TerminalOutputQueue {
86
86
  try {
87
87
  this.writer.off('drain', drainListener);
88
88
  }
89
- catch { /* ok */ }
89
+ catch {
90
+ /* ok */
91
+ }
90
92
  drainListener = null;
91
93
  }
92
94
  try {
93
95
  this.writer.off('error', onError);
94
96
  }
95
- catch { /* ok */ }
97
+ catch {
98
+ /* ok */
99
+ }
96
100
  };
97
101
  const onError = (error) => {
98
102
  cleanup();
@@ -114,7 +118,9 @@ class TerminalOutputQueue {
114
118
  try {
115
119
  this.writer.off('drain', listener);
116
120
  }
117
- catch { /* ok */ }
121
+ catch {
122
+ /* ok */
123
+ }
118
124
  writeNext();
119
125
  };
120
126
  this.writer.on('drain', drainListener);
package/dist/tools/lsp.js CHANGED
@@ -32,7 +32,9 @@ class LspClient extends events_1.EventEmitter {
32
32
  * Uses spawnSync with `which`/`where` to avoid async ENOENT race.
33
33
  */
34
34
  static probeBinary(cmd) {
35
- const res = (0, child_process_1.spawnSync)(process.platform === 'win32' ? 'where' : 'which', [cmd], { stdio: 'ignore' });
35
+ const res = (0, child_process_1.spawnSync)(process.platform === 'win32' ? 'where' : 'which', [cmd], {
36
+ stdio: 'ignore',
37
+ });
36
38
  return res.status === 0;
37
39
  }
38
40
  async start() {
@@ -267,7 +269,7 @@ class LspManager {
267
269
  const client = new LspClient(language, projectRoot);
268
270
  // Fallback error listener — ensures the client NEVER crashes the process
269
271
  // due to an unhandled 'error' event emission.
270
- client.on('error', (msg) => {
272
+ client.on('error', msg => {
271
273
  console.warn(`[LSP:${language}] ${msg}`);
272
274
  });
273
275
  try {
@@ -326,12 +328,14 @@ function formatLocationResult(location) {
326
328
  const locations = Array.isArray(location) ? location : [location];
327
329
  if (locations.length === 0)
328
330
  return 'No definition found';
329
- return locations.map(loc => {
331
+ return locations
332
+ .map(loc => {
330
333
  const path = loc.uri.replace('file://', '');
331
334
  const line = loc.range.start.line + 1;
332
335
  const char = loc.range.start.character + 1;
333
336
  return `${path}:${line}:${char}`;
334
- }).join('\n');
337
+ })
338
+ .join('\n');
335
339
  }
336
340
  /**
337
341
  * 解析 Hover 结果
@@ -543,12 +547,14 @@ exports.lspGetDiagnosticsTool = (0, tool_1.buildTool)({
543
547
  3: 'Information',
544
548
  4: 'Hint',
545
549
  };
546
- const output = diagnostics.map(d => {
550
+ const output = diagnostics
551
+ .map(d => {
547
552
  const sev = severityMap[d.severity] || 'Unknown';
548
553
  const line = d.range.start.line + 1;
549
554
  const char = d.range.start.character + 1;
550
555
  return `[${sev}] ${file_path}:${line}:${char}: ${d.message}`;
551
- }).join('\n');
556
+ })
557
+ .join('\n');
552
558
  return { success: true, output };
553
559
  }
554
560
  catch (err) {
@@ -58,9 +58,7 @@ function setFrameCursor(frame, row, column, visible = true) {
58
58
  };
59
59
  }
60
60
  function renderFrameRows(frame) {
61
- return frame.rows.map(row => row
62
- .map(cell => cell.width === 0 ? '' : cell.char)
63
- .join(''));
61
+ return frame.rows.map(row => row.map(cell => (cell.width === 0 ? '' : cell.char)).join(''));
64
62
  }
65
63
  function diffTuiFrames(previous, next) {
66
64
  if (!previous || previous.width !== next.width || previous.height !== next.height) {
@@ -78,9 +76,9 @@ function diffTuiFrames(previous, next) {
78
76
  }
79
77
  return {
80
78
  changedRows,
81
- cursorChanged: previous.cursor.row !== next.cursor.row
82
- || previous.cursor.column !== next.cursor.column
83
- || previous.cursor.visible !== next.cursor.visible,
79
+ cursorChanged: previous.cursor.row !== next.cursor.row ||
80
+ previous.cursor.column !== next.cursor.column ||
81
+ previous.cursor.visible !== next.cursor.visible,
84
82
  };
85
83
  }
86
84
  function rowsDifferent(prev, next) {
@@ -97,7 +95,11 @@ function rowsDifferent(prev, next) {
97
95
  return false;
98
96
  }
99
97
  function createEmptyRow(width) {
100
- return Array.from({ length: width }, () => ({ char: ' ', width: 1, style: style_1.DEFAULT_STYLE }));
98
+ return Array.from({ length: width }, () => ({
99
+ char: ' ',
100
+ width: 1,
101
+ style: style_1.DEFAULT_STYLE,
102
+ }));
101
103
  }
102
104
  function charWidth(char) {
103
105
  const width = (0, string_width_1.default)(char);
@@ -22,9 +22,9 @@ class TranscriptLayoutCache {
22
22
  }
23
23
  /** Get cached rows for an entry, or null if not cached / stale. */
24
24
  get(id, revision, generation, width, themeId = 'default') {
25
- if (generation !== this.lastGeneration
26
- || width !== this.lastWidth
27
- || themeId !== this.lastThemeId) {
25
+ if (generation !== this.lastGeneration ||
26
+ width !== this.lastWidth ||
27
+ themeId !== this.lastThemeId) {
28
28
  this.invalidate(generation, width, themeId);
29
29
  return null;
30
30
  }
@@ -38,9 +38,9 @@ class TranscriptLayoutCache {
38
38
  }
39
39
  /** Store layout result for an entry. */
40
40
  set(id, revision, rows, generation, width, themeId = 'default') {
41
- if (generation !== this.lastGeneration
42
- || width !== this.lastWidth
43
- || themeId !== this.lastThemeId) {
41
+ if (generation !== this.lastGeneration ||
42
+ width !== this.lastWidth ||
43
+ themeId !== this.lastThemeId) {
44
44
  this.invalidate(generation, width, themeId);
45
45
  }
46
46
  // Evict oldest if at capacity.
@@ -72,7 +72,10 @@ class TranscriptInspectorSurface {
72
72
  this.output.off('error', onError);
73
73
  this.output.off('close', onClose);
74
74
  };
75
- const onDrain = () => { cleanup(); resolve(); };
75
+ const onDrain = () => {
76
+ cleanup();
77
+ resolve();
78
+ };
76
79
  const onError = (error) => {
77
80
  cleanup();
78
81
  reject(error instanceof Error ? error : new Error(String(error ?? 'terminal output error')));
@@ -14,12 +14,7 @@ class TranscriptInspectorController {
14
14
  view(entries, inspector) {
15
15
  const query = inspector.searchQuery.trim().toLowerCase();
16
16
  const filtered = query
17
- ? entries.filter(entry => [
18
- entry.toolName,
19
- entry.callId,
20
- entry.summary,
21
- this.details.get(entry.callId)?.content,
22
- ]
17
+ ? entries.filter(entry => [entry.toolName, entry.callId, entry.summary, this.details.get(entry.callId)?.content]
23
18
  .filter(Boolean)
24
19
  .some(value => String(value).toLowerCase().includes(query)))
25
20
  : entries;
@@ -43,7 +38,7 @@ class TranscriptInspectorController {
43
38
  return;
44
39
  const generation = ++this.requestGeneration;
45
40
  this.details.set(entry.callId, {
46
- content: append ? current?.content ?? '' : '',
41
+ content: append ? (current?.content ?? '') : '',
47
42
  totalBytes: current?.totalBytes ?? entry.outputBytes,
48
43
  nextOffsetBytes: current?.nextOffsetBytes,
49
44
  redacted: current?.redacted ?? false,
@@ -72,7 +67,7 @@ class TranscriptInspectorController {
72
67
  if (generation !== this.requestGeneration)
73
68
  return;
74
69
  this.details.set(entry.callId, {
75
- content: append ? current?.content ?? '' : '',
70
+ content: append ? (current?.content ?? '') : '',
76
71
  totalBytes: current?.totalBytes ?? entry.outputBytes,
77
72
  redacted: current?.redacted ?? false,
78
73
  loading: false,
package/dist/ui/box.js CHANGED
@@ -40,18 +40,25 @@ function renderHeaderBox(config) {
40
40
  // Model name
41
41
  parts.push(ACCENT(config.model));
42
42
  // Provider (shortened)
43
- const providerShort = config.provider === 'Alibaba Cloud' ? 'Qwen'
44
- : config.provider === 'Anthropic' ? 'Anthropic'
45
- : config.provider === 'OpenAI' ? 'OpenAI'
43
+ const providerShort = config.provider === 'Alibaba Cloud'
44
+ ? 'Qwen'
45
+ : config.provider === 'Anthropic'
46
+ ? 'Anthropic'
47
+ : config.provider === 'OpenAI'
48
+ ? 'OpenAI'
46
49
  : config.provider;
47
50
  if (providerShort) {
48
51
  parts.push(DIM(providerShort));
49
52
  }
50
53
  // Status
51
- const statusIcon = config.status === 'ready' ? GREEN('●')
52
- : config.status === 'loading' ? YELLOW('')
53
- : config.status === 'error' ? RED('●')
54
- : config.status === 'processing' ? ACCENT('')
54
+ const statusIcon = config.status === 'ready'
55
+ ? GREEN('')
56
+ : config.status === 'loading'
57
+ ? YELLOW('')
58
+ : config.status === 'error'
59
+ ? RED('●')
60
+ : config.status === 'processing'
61
+ ? ACCENT('◌')
55
62
  : DIM('○');
56
63
  parts.push(statusIcon);
57
64
  return ` ${BRAND('orion')} ${DIM('v' + config.version)} ${DIM('│')} ${parts.join(` ${DIM('│')} `)}`;
@@ -149,8 +149,10 @@ function renderMarkdownFallback(text, maxWidth = DEFAULT_MAX_WIDTH) {
149
149
  const level = headingMatch[1].length;
150
150
  const headingText = headingMatch[2].trim();
151
151
  const prefix = ' '.repeat(level - 1);
152
- const styled = level <= 2 ? BOLD(CYAN(prefix + ' ' + headingText))
153
- : level <= 4 ? BOLD(CYAN(prefix + headingText))
152
+ const styled = level <= 2
153
+ ? BOLD(CYAN(prefix + ' ' + headingText))
154
+ : level <= 4
155
+ ? BOLD(CYAN(prefix + headingText))
154
156
  : BOLD(DIM(headingText));
155
157
  result.push(styled);
156
158
  result.push(DIM('─'.repeat(Math.min(headingText.length + prefix.length + 2, maxWidth - 2))));
@@ -9,13 +9,15 @@ exports.renderFramedInputFrame = renderFramedInputFrame;
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
10
  const text_1 = require("./text");
11
11
  const NO_COLOR = process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb';
12
- const colorize = NO_COLOR ? {
13
- accent: (s) => s,
14
- dim: (s) => s,
15
- } : {
16
- accent: chalk_1.default.hex('#00D4AA'),
17
- dim: chalk_1.default.dim,
18
- };
12
+ const colorize = NO_COLOR
13
+ ? {
14
+ accent: (s) => s,
15
+ dim: (s) => s,
16
+ }
17
+ : {
18
+ accent: chalk_1.default.hex('#00D4AA'),
19
+ dim: chalk_1.default.dim,
20
+ };
19
21
  const INPUT_BG = '\x1b[48;2;56;56;56m';
20
22
  const INPUT_FG = '\x1b[38;2;226;232;240m';
21
23
  const RESET_COLORS = '\x1b[39;49m';
@@ -34,7 +36,9 @@ function renderFramedInputFrame(options) {
34
36
  const prefix = index === 0 ? firstPrompt : continuationPrompt;
35
37
  return renderInputLine((0, text_1.truncateVisible)(prefix + line, inputWidth));
36
38
  });
37
- const statusLine = options.statusText ? renderInputStatusLine(inputWidth, options.statusText) : undefined;
39
+ const statusLine = options.statusText
40
+ ? renderInputStatusLine(inputWidth, options.statusText)
41
+ : undefined;
38
42
  const rowSpans = logicalLines.map((line, index) => {
39
43
  const prefix = index === 0 ? firstPrompt : continuationPrompt;
40
44
  return terminalRowsFor((0, text_1.visualWidth)(prefix) + (0, text_1.visualWidth)(line), width);
@@ -157,7 +157,7 @@ class StreamMarkdownRenderer {
157
157
  // Ordered list
158
158
  const orderedMatch = line.match(/^(\s*)(\d+)\.\s+(.*)$/);
159
159
  if (orderedMatch) {
160
- return orderedMatch[1] + CYAN(orderedMatch[2] + '.') + ' ' + this.renderInline(orderedMatch[3]);
160
+ return (orderedMatch[1] + CYAN(orderedMatch[2] + '.') + ' ' + this.renderInline(orderedMatch[3]));
161
161
  }
162
162
  return this.renderInline(line);
163
163
  }
@@ -46,7 +46,9 @@ function renderToolCard(tool) {
46
46
  lines.push(DIM('│ ') + truncated.padEnd(maxWidth - 4) + DIM(' │'));
47
47
  }
48
48
  if (tool.result.split('\n').length > 6) {
49
- lines.push(DIM('│ ') + DIM(`... ${tool.result.split('\n').length - 6} more lines`).padEnd(maxWidth - 4) + DIM(' │'));
49
+ lines.push(DIM('│ ') +
50
+ DIM(`... ${tool.result.split('\n').length - 6} more lines`).padEnd(maxWidth - 4) +
51
+ DIM(' │'));
50
52
  }
51
53
  // 底部
52
54
  lines.push(DIM('└') + '─'.repeat(maxWidth - 2) + DIM('┘'));
@@ -92,7 +94,11 @@ function renderReadPreview(path, content, success) {
92
94
  const maxWidth = 60;
93
95
  const icon = success ? SUCCESS('📖') : ERROR('📖');
94
96
  const title = `${icon} Read: ${path}`;
95
- lines.push(DIM('┌─ ') + title.slice(0, maxWidth - 5) + DIM(' ') + '─'.repeat(Math.max(0, maxWidth - title.length - 5)) + DIM('┐'));
97
+ lines.push(DIM('┌─ ') +
98
+ title.slice(0, maxWidth - 5) +
99
+ DIM(' ') +
100
+ '─'.repeat(Math.max(0, maxWidth - title.length - 5)) +
101
+ DIM('┐'));
96
102
  if (success && content) {
97
103
  const contentLines = content.split('\n').slice(0, 8);
98
104
  for (const line of contentLines) {
@@ -100,7 +106,9 @@ function renderReadPreview(path, content, success) {
100
106
  lines.push(DIM('│ ') + truncated.padEnd(maxWidth - 4) + DIM(' │'));
101
107
  }
102
108
  if (content.split('\n').length > 8) {
103
- lines.push(DIM('│ ') + DIM(`... ${content.split('\n').length - 8} more lines`).padEnd(maxWidth - 4) + DIM(' │'));
109
+ lines.push(DIM('│ ') +
110
+ DIM(`... ${content.split('\n').length - 8} more lines`).padEnd(maxWidth - 4) +
111
+ DIM(' │'));
104
112
  }
105
113
  }
106
114
  lines.push(DIM('└') + '─'.repeat(maxWidth - 2) + DIM('┘'));
@@ -33,16 +33,22 @@ function visualWidth(text) {
33
33
  let width = 0;
34
34
  for (const ch of text) {
35
35
  const cp = ch.codePointAt(0) || 0;
36
- width += (cp >= 0x1100 && (cp <= 0x115f || cp === 0x2329 || cp === 0x232a ||
37
- (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
38
- (cp >= 0xac00 && cp <= 0xd7a3) ||
39
- (cp >= 0xf900 && cp <= 0xfaff) ||
40
- (cp >= 0xfe10 && cp <= 0xfe19) ||
41
- (cp >= 0xfe30 && cp <= 0xfe6f) ||
42
- (cp >= 0xff01 && cp <= 0xff60) ||
43
- (cp >= 0xffe0 && cp <= 0xffe6) ||
44
- (cp >= 0x20000 && cp <= 0x2fffd) ||
45
- (cp >= 0x30000 && cp <= 0x3fffd))) ? 2 : 1;
36
+ width +=
37
+ cp >= 0x1100 &&
38
+ (cp <= 0x115f ||
39
+ cp === 0x2329 ||
40
+ cp === 0x232a ||
41
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
42
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
43
+ (cp >= 0xf900 && cp <= 0xfaff) ||
44
+ (cp >= 0xfe10 && cp <= 0xfe19) ||
45
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
46
+ (cp >= 0xff01 && cp <= 0xff60) ||
47
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
48
+ (cp >= 0x20000 && cp <= 0x2fffd) ||
49
+ (cp >= 0x30000 && cp <= 0x3fffd))
50
+ ? 2
51
+ : 1;
46
52
  }
47
53
  return width;
48
54
  }
@@ -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
  });
@@ -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) {