fraim-hub 2.0.335 → 2.0.338

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.
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ // Read the shared author's current entry format for identity and update guards.
4
+ function learningEntryBlocks(body) {
5
+ const headings = [...body.matchAll(/^#{2,6}\s+\[P-[A-Z]+\].*$/gm)];
6
+ return headings.map((heading, index) => {
7
+ const start = heading.index;
8
+ const next = headings[index + 1]?.index ?? body.length;
9
+ const ownDepth = heading[0].match(/^#+/)[0].length;
10
+ const afterHeading = start + heading[0].length;
11
+ const shallower = [...body.slice(afterHeading, next).matchAll(/^(#{1,6})\s+.*$/gm)].find(m => m[1].length <= ownDepth);
12
+ const limit = shallower ? afterHeading + shallower.index : next;
13
+ const text = body.slice(start, limit).trimEnd();
14
+ return { start, end: start + text.length, text, id: text.match(/^\*\*Id\*\*:\s*(L-[a-f0-9]{10})\s*$/m)?.[1] ?? null };
15
+ });
16
+ }
17
+
18
+ function learningEntryMetadata(content) {
19
+ const metadata = {};
20
+ for (const match of content.matchAll(/^\*\*(Recurrences|First recorded|First synthesized)\*\*:\s*([^\r\n]+)$/gm)) {
21
+ if (metadata[match[1]] !== undefined && metadata[match[1]] !== match[2]) throw new Error('entry-metadata-conflict');
22
+ metadata[match[1]] = match[2];
23
+ }
24
+ return metadata;
25
+ }
26
+
27
+ module.exports = { learningEntryMetadata, learningEntryBlocks };
@@ -0,0 +1,78 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "mapVersion": 1,
4
+ "domains": [
5
+ "engineering",
6
+ "product",
7
+ "go-to-market",
8
+ "people-ops",
9
+ "finance",
10
+ "legal",
11
+ "compliance",
12
+ "customer",
13
+ "company-building",
14
+ "operations"
15
+ ],
16
+ "jobDomainMap": {
17
+ "product-building": "engineering",
18
+ "dev-ops": "engineering",
19
+ "quality-assurance": "engineering",
20
+ "migration": "engineering",
21
+ "replicate": "engineering",
22
+ "reviewer": "engineering",
23
+ "security": "engineering",
24
+ "delivery-ops": "engineering",
25
+ "salesforce": "engineering",
26
+ "ai-engineering": "engineering",
27
+ "product-management": "product",
28
+ "customer-development": "product",
29
+ "brainstorming": "product",
30
+ "marketing": "go-to-market",
31
+ "branding": "go-to-market",
32
+ "sales": "go-to-market",
33
+ "go-to-market": "go-to-market",
34
+ "business-development": "go-to-market",
35
+ "commercial-ops": "go-to-market",
36
+ "people-ops": "people-ops",
37
+ "inclusion-ops": "people-ops",
38
+ "career": "people-ops",
39
+ "finance": "finance",
40
+ "invoicing": "finance",
41
+ "banking": "finance",
42
+ "legal": "legal",
43
+ "compliance": "compliance",
44
+ "customer-success": "customer",
45
+ "customer-communication": "customer",
46
+ "company-creation": "company-building",
47
+ "bootstrap": "company-building",
48
+ "fundraising": "company-building",
49
+ "business-ops": "operations",
50
+ "procurement": "operations",
51
+ "project-management": "operations"
52
+ },
53
+ "globalJobCategories": [
54
+ "manager",
55
+ "fraim",
56
+ "1-1",
57
+ "coaching",
58
+ "delegation",
59
+ "hiring",
60
+ "productivity",
61
+ "project-setup",
62
+ "setup-guardrails",
63
+ "team-learning",
64
+ "verification"
65
+ ],
66
+ "domainVoice": {
67
+ "engineering": "Precise and technical; file paths, commands, and test names are appropriate.",
68
+ "product": "Product and user-outcome language; behavior and flow, not code symbols.",
69
+ "go-to-market": "Marketing and sales language; campaigns, funnels, messaging, audience.",
70
+ "people-ops": "Plain HR and recruiting language; no engineering jargon.",
71
+ "finance": "Finance and accounting language; figures, close, forecasts.",
72
+ "legal": "Contract and IP language; obligations, clauses, risk.",
73
+ "compliance": "Controls, evidence, and regulation language.",
74
+ "customer": "Customer-relationship and support language.",
75
+ "company-building": "Founder and operations language; setup, entity, funding.",
76
+ "operations": "Business-operations language; process, vendors, planning, coordination."
77
+ }
78
+ }
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -30,7 +63,6 @@ exports.safeAttachmentMetaPath = safeAttachmentMetaPath;
30
63
  * path its own file-read tool opens.
31
64
  */
32
65
  const path_1 = __importDefault(require("path"));
33
- const sharp_1 = __importDefault(require("sharp"));
34
66
  /** Accepted upload MIME types (spec R1/R11). */
35
67
  exports.ACCEPTED_ATTACHMENT_MIME_TYPES = new Set([
36
68
  'image/png',
@@ -90,8 +122,9 @@ function pdfContainsScriptMarker(buffer) {
90
122
  async function sanitizeAttachmentBuffer(buffer, mimeType) {
91
123
  if (IMAGE_MIME_TYPES.has(mimeType)) {
92
124
  try {
125
+ const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
93
126
  const format = mimeType === 'image/jpeg' ? 'jpeg' : mimeType === 'image/gif' ? 'gif' : 'png';
94
- return await (0, sharp_1.default)(buffer).toFormat(format).toBuffer();
127
+ return await sharp(buffer).toFormat(format).toBuffer();
95
128
  }
96
129
  catch (err) {
97
130
  throw new AttachmentRejectedError(415, `Not a valid ${mimeType} image.`);
@@ -11,6 +11,7 @@ const electron_1 = require("electron");
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const server_1 = require("./server");
14
+ const native_voice_input_1 = require("./native-voice-input");
14
15
  const cert_store_1 = require("./cert-store");
15
16
  const office_sideload_1 = require("./office-sideload");
16
17
  const office_setup_policy_1 = require("./office-setup-policy");
@@ -672,6 +673,10 @@ async function launchDesktopShell(options) {
672
673
  return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
673
674
  },
674
675
  restartToLatest: restartToLatestNow,
676
+ voiceInput: (0, native_voice_input_1.createNativeVoiceInputController)({
677
+ getWindow: () => mainWindow,
678
+ sendActionToFirstResponder: (action) => electron_1.Menu.sendActionToFirstResponder(action),
679
+ }),
675
680
  hubMainDiagnostics: {
676
681
  snapshot: () => (0, hub_main_diagnostics_1.getHubMainDiagnosticsSnapshot)(buildDesktopDebugSnapshot(runtimeId)),
677
682
  },
@@ -110,15 +110,27 @@ function buildBrowserLaunchArgs(opts) {
110
110
  * baked into any registry job or skill. Returns '' when the shared browser is not
111
111
  * available (so the note only appears when there is something to connect to).
112
112
  */
113
- function buildBrowserContextNote(cdpEndpoint, hubBaseUrl) {
113
+ function buildBrowserContextNote(cdpEndpoint, hubBaseUrl, conversationId, page) {
114
114
  if (!cdpEndpoint || !hubBaseUrl)
115
115
  return '';
116
+ const trimmedConversationId = typeof conversationId === 'string' ? conversationId.trim() : '';
117
+ const pageGuidance = trimmedConversationId
118
+ ? [
119
+ `Browser work for this Hub conversation must use the conversation page for \`${trimmedConversationId}\`, not the globally active tab.`,
120
+ `Before using a browser, ensure it is running and get this conversation's page: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` with JSON body \`{"conversationId":"${trimmedConversationId}"}\` (idempotent - it reuses the running browser and this conversation's page, or relaunches/recreates them if they were closed).`,
121
+ page?.targetId
122
+ ? `Current allocated page target: \`${page.targetId}\`${page.webSocketDebuggerUrl ? ` (${page.webSocketDebuggerUrl})` : ''}.`
123
+ : 'Use the returned `page.targetId` / `page.webSocketDebuggerUrl` from that endpoint when your browser tooling lets you choose a CDP target.',
124
+ ]
125
+ : [
126
+ `Before using a browser, ensure it is running: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` (idempotent - it reuses the running browser, or relaunches it if it was closed).`,
127
+ ];
116
128
  return [
117
129
  '',
118
- '[FRAIM shared browser] A persistent shared browser is available — use it instead of launching your own.',
119
- `Before using a browser, ensure it is running: \`POST ${hubBaseUrl}/api/ai-hub/browser/start\` (idempotent — it reuses the running browser, or relaunches it if it was closed).`,
120
- `Then drive the shared browser at ${cdpEndpoint}: your Playwright/browser tools are pointed at it on supported agents; if your tools would otherwise open their own browser, connect to the shared one with \`connectOverCDP("${cdpEndpoint}")\` and reuse its existing context/tab.`,
121
- 'Never close the shared browser or sign me out. If a site needs login, open it there and ask me to log in — the session persists to your next turn.',
130
+ '[FRAIM shared browser] A persistent shared browser is available; use it instead of launching your own.',
131
+ ...pageGuidance,
132
+ `Then drive the shared browser at ${cdpEndpoint}: your Playwright/browser tools are pointed at it on supported agents; if your tools would otherwise open their own browser, connect to the shared one with \`connectOverCDP("${cdpEndpoint}")\` and use the conversation page target instead of an arbitrary existing tab.`,
133
+ 'Never close the shared browser or sign me out. If a site needs login, open it there and ask me to log in; the session persists to your next turn.',
122
134
  ].join('\n');
123
135
  }
124
136
  /** Default persistent profile directory — sandboxed from the user's daily browser. */
@@ -168,6 +180,7 @@ class ManagedBrowser {
168
180
  this.child = null;
169
181
  this.ownsProcess = false;
170
182
  this.resolved = null;
183
+ this.conversationPages = new Map();
171
184
  this.opts = opts;
172
185
  this.channel = opts.channel || 'auto';
173
186
  this.port = opts.port || DEFAULT_PORT;
@@ -180,6 +193,11 @@ class ManagedBrowser {
180
193
  probe() {
181
194
  return this.opts.probeFn ? this.opts.probeFn(this.cdpEndpoint()) : probeCdpEndpoint(this.cdpEndpoint());
182
195
  }
196
+ async cdpRequest(requestPath, method = 'GET') {
197
+ if (this.opts.cdpRequestFn)
198
+ return this.opts.cdpRequestFn(requestPath, method);
199
+ return cdpJsonRequest(this.cdpEndpoint(), requestPath, method);
200
+ }
183
201
  /** Live check — does a Chromium CDP endpoint answer on the port right now. */
184
202
  isRunning() {
185
203
  return this.probe();
@@ -232,6 +250,71 @@ class ManagedBrowser {
232
250
  await this.waitForRunning();
233
251
  return { endpoint: this.cdpEndpoint(), reused: false, executable: exe.path, channel: exe.channel };
234
252
  }
253
+ async listPageTargets() {
254
+ const raw = await this.cdpRequest('/json/list');
255
+ if (!Array.isArray(raw))
256
+ return [];
257
+ return raw
258
+ .filter((target) => Boolean(target && typeof target === 'object' && typeof target.id === 'string'))
259
+ .filter((target) => !target.type || target.type === 'page');
260
+ }
261
+ pageFromTarget(conversationId, target) {
262
+ return {
263
+ conversationId,
264
+ targetId: target.id,
265
+ url: target.url || 'about:blank',
266
+ webSocketDebuggerUrl: target.webSocketDebuggerUrl || null,
267
+ endpoint: this.cdpEndpoint(),
268
+ };
269
+ }
270
+ async ensureConversationPage(conversationId, startUrl = 'about:blank') {
271
+ const id = conversationId.trim();
272
+ if (!id)
273
+ throw new Error('conversationId is required for browser page allocation.');
274
+ await this.start();
275
+ const existing = this.conversationPages.get(id);
276
+ const targets = await this.listPageTargets();
277
+ if (existing && targets.some((target) => target.id === existing.targetId)) {
278
+ try {
279
+ await this.cdpRequest(`/json/activate/${encodeURIComponent(existing.targetId)}`);
280
+ }
281
+ catch { /* best effort */ }
282
+ return existing;
283
+ }
284
+ const created = await this.cdpRequest(`/json/new?${encodeURIComponent(startUrl)}`, 'PUT');
285
+ if (!created || typeof created.id !== 'string') {
286
+ throw new Error('Browser did not return a page target for the conversation.');
287
+ }
288
+ const page = this.pageFromTarget(id, created);
289
+ this.conversationPages.set(id, page);
290
+ try {
291
+ await this.cdpRequest(`/json/activate/${encodeURIComponent(page.targetId)}`);
292
+ }
293
+ catch { /* best effort */ }
294
+ return page;
295
+ }
296
+ async releaseConversationPage(conversationId) {
297
+ const id = conversationId.trim();
298
+ if (!id)
299
+ return { released: false, conversationId: id, targetId: null };
300
+ const existing = this.conversationPages.get(id);
301
+ this.conversationPages.delete(id);
302
+ if (!existing)
303
+ return { released: false, conversationId: id, targetId: null };
304
+ if (await this.probe()) {
305
+ try {
306
+ await this.cdpRequest(`/json/close/${encodeURIComponent(existing.targetId)}`);
307
+ }
308
+ catch {
309
+ // The user may have closed the tab or browser manually. Dropping the
310
+ // mapping is the important cleanup invariant.
311
+ }
312
+ }
313
+ return { released: true, conversationId: id, targetId: existing.targetId };
314
+ }
315
+ conversationPage(conversationId) {
316
+ return this.conversationPages.get(conversationId.trim()) || null;
317
+ }
235
318
  async waitForRunning() {
236
319
  const timeoutMs = this.opts.waitTimeoutMs ?? 15000;
237
320
  const intervalMs = this.opts.waitIntervalMs ?? 250;
@@ -263,7 +346,49 @@ class ManagedBrowser {
263
346
  channel: this.resolved?.channel || this.channel,
264
347
  userDataDir: this.userDataDir,
265
348
  executable: this.resolved?.path || null,
349
+ conversationPages: Array.from(this.conversationPages.values()).map((page) => ({
350
+ conversationId: page.conversationId,
351
+ targetId: page.targetId,
352
+ })),
266
353
  };
267
354
  }
268
355
  }
269
356
  exports.ManagedBrowser = ManagedBrowser;
357
+ function cdpJsonRequest(endpoint, requestPath, method) {
358
+ return new Promise((resolve, reject) => {
359
+ let url;
360
+ try {
361
+ url = new URL(requestPath, endpoint);
362
+ }
363
+ catch (error) {
364
+ reject(error);
365
+ return;
366
+ }
367
+ const req = http_1.default.request(url, { method, timeout: 3000 }, (res) => {
368
+ let body = '';
369
+ res.on('data', (chunk) => { body += chunk.toString(); });
370
+ res.on('end', () => {
371
+ if ((res.statusCode || 500) >= 400) {
372
+ reject(new Error(`CDP ${method} ${requestPath} failed with HTTP ${res.statusCode}`));
373
+ return;
374
+ }
375
+ if (!body.trim()) {
376
+ resolve({});
377
+ return;
378
+ }
379
+ try {
380
+ resolve(JSON.parse(body));
381
+ }
382
+ catch {
383
+ resolve(body);
384
+ }
385
+ });
386
+ });
387
+ req.on('error', reject);
388
+ req.on('timeout', () => {
389
+ req.destroy();
390
+ reject(new Error(`CDP ${method} ${requestPath} timed out.`));
391
+ });
392
+ req.end();
393
+ });
394
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WINDOWS_VOICE_INPUT_PROGRAM = exports.NativeVoiceInputError = void 0;
7
+ exports.getVoiceInputCapability = getVoiceInputCapability;
8
+ exports.requestWindowsVoiceInput = requestWindowsVoiceInput;
9
+ exports.createNativeVoiceInputController = createNativeVoiceInputController;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ class NativeVoiceInputError extends Error {
13
+ constructor(code) {
14
+ super(code);
15
+ this.code = code;
16
+ this.name = 'NativeVoiceInputError';
17
+ }
18
+ }
19
+ exports.NativeVoiceInputError = NativeVoiceInputError;
20
+ function getVoiceInputCapability(platform = process.platform, native = false) {
21
+ if (platform === 'win32') {
22
+ return {
23
+ platform: 'windows', mode: native ? 'native' : 'manual', shortcutLabel: 'Win+H',
24
+ guidance: 'Press Win+H to start Windows Voice Typing. If it does not appear, check Windows Settings > Privacy & security > Speech and Microphone.',
25
+ };
26
+ }
27
+ if (platform === 'darwin') {
28
+ return {
29
+ platform: 'macos', mode: native ? 'native' : 'manual', shortcutLabel: 'Dictation shortcut',
30
+ guidance: 'Use the microphone key or your configured Dictation shortcut. If Dictation does not appear, enable it in System Settings > Keyboard > Dictation.',
31
+ };
32
+ }
33
+ return { platform: 'other', mode: 'unsupported', guidance: 'Voice input is available through system dictation on Windows and macOS.' };
34
+ }
35
+ // Fixed program only: no renderer or request data enters the native command.
36
+ // The 32-byte union and individually paced events were validated in the #1844 spike.
37
+ exports.WINDOWS_VOICE_INPUT_PROGRAM = String.raw `$ErrorActionPreference = 'Stop'
38
+ $source = @'
39
+ using System;
40
+ using System.ComponentModel;
41
+ using System.Runtime.InteropServices;
42
+ public static class FraimVoiceTyping {
43
+ [StructLayout(LayoutKind.Sequential)]
44
+ private struct INPUT { public uint type; public InputUnion data; }
45
+ [StructLayout(LayoutKind.Explicit, Size = 32)]
46
+ private struct InputUnion { [FieldOffset(0)] public KEYBDINPUT keyboard; }
47
+ [StructLayout(LayoutKind.Sequential)]
48
+ private struct KEYBDINPUT {
49
+ public ushort virtualKey; public ushort scanCode; public uint flags;
50
+ public uint time; public UIntPtr extraInfo;
51
+ }
52
+ [DllImport("user32.dll", SetLastError = true)]
53
+ private static extern uint SendInput(uint count, INPUT[] inputs, int size);
54
+ private static bool Emit(ushort key, uint flags) {
55
+ INPUT input = new INPUT { type = 1, data = new InputUnion {
56
+ keyboard = new KEYBDINPUT { virtualKey = key, flags = flags }
57
+ }};
58
+ return SendInput(1, new INPUT[] { input }, Marshal.SizeOf(typeof(INPUT))) == 1;
59
+ }
60
+ private static void Send(ushort key, uint flags) {
61
+ if (!Emit(key, flags)) throw new Win32Exception(Marshal.GetLastWin32Error());
62
+ }
63
+ public static int Invoke() {
64
+ bool winDown = false, hDown = false;
65
+ try {
66
+ Send(0x5B, 0); winDown = true;
67
+ System.Threading.Thread.Sleep(40);
68
+ Send(0x48, 0); hDown = true;
69
+ System.Threading.Thread.Sleep(40);
70
+ Send(0x48, 2); hDown = false;
71
+ System.Threading.Thread.Sleep(40);
72
+ Send(0x5B, 2); winDown = false;
73
+ return 4;
74
+ } finally {
75
+ // Release each key independently even after partial emission.
76
+ if (hDown) Emit(0x48, 2);
77
+ if (winDown) Emit(0x5B, 2);
78
+ }
79
+ }
80
+ }
81
+ '@
82
+ Add-Type -TypeDefinition $source -Language CSharp
83
+ Write-Output ([FraimVoiceTyping]::Invoke())`;
84
+ function requestWindowsVoiceInput(execute = node_child_process_1.execFile) {
85
+ return new Promise((resolve, reject) => {
86
+ const fail = () => reject(new NativeVoiceInputError('launch_failed'));
87
+ try {
88
+ execute(node_path_1.default.win32.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(exports.WINDOWS_VOICE_INPUT_PROGRAM, 'utf16le').toString('base64')],
89
+ // execFile kills only its child on timeout and calls back after close.
90
+ { windowsHide: true, timeout: 5000, maxBuffer: 4096, encoding: 'utf8' }, (error, stdout) => {
91
+ if (error || stdout.trim() !== '4')
92
+ fail();
93
+ else
94
+ resolve();
95
+ });
96
+ }
97
+ catch {
98
+ fail();
99
+ }
100
+ });
101
+ }
102
+ function createNativeVoiceInputController(options) {
103
+ const capability = getVoiceInputCapability(options.platform ?? process.platform, true);
104
+ let inFlight = false;
105
+ return {
106
+ capability,
107
+ async start() {
108
+ if (capability.mode === 'unsupported')
109
+ throw new NativeVoiceInputError('unsupported_platform');
110
+ if (inFlight)
111
+ throw new NativeVoiceInputError('launch_failed');
112
+ inFlight = true;
113
+ try {
114
+ try {
115
+ const win = options.getWindow();
116
+ if (!win || win.isDestroyed() || win.webContents.isDestroyed())
117
+ throw new Error('unavailable');
118
+ win.show();
119
+ win.focus();
120
+ win.webContents.focus();
121
+ }
122
+ catch {
123
+ throw new NativeVoiceInputError('window_unavailable');
124
+ }
125
+ try {
126
+ if (capability.platform === 'windows')
127
+ await (options.requestWindows ?? requestWindowsVoiceInput)();
128
+ else
129
+ options.sendActionToFirstResponder('startDictation:');
130
+ }
131
+ catch {
132
+ throw new NativeVoiceInputError('launch_failed');
133
+ }
134
+ return { status: 'requested', capability, message: `System dictation requested. ${capability.guidance}` };
135
+ }
136
+ finally {
137
+ inFlight = false;
138
+ }
139
+ },
140
+ };
141
+ }