@stage-labs/metro 0.1.0-beta.307 → 0.1.0-beta.309

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 (91) hide show
  1. package/dist/agent.js +23 -28
  2. package/dist/child.js +33 -0
  3. package/dist/claude-env.js +104 -0
  4. package/dist/cli.js +7 -21
  5. package/dist/lock.js +62 -0
  6. package/dist/memory.js +12 -27
  7. package/dist/root-release-io.js +1 -2
  8. package/dist/root-release.js +1 -1
  9. package/dist/route.js +0 -1
  10. package/hooks/guard.mjs +1 -5
  11. package/hooks/session-start.mjs +1 -1
  12. package/package.json +1 -1
  13. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +13 -38
  14. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +21 -1
  15. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -9
  16. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +14 -12
  17. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +6 -18
  18. package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +3 -7
  19. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +6 -5
  20. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +1 -23
  21. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +39 -118
  22. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup-api.ts +14 -61
  23. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +10 -42
  24. package/runtime/node_modules/@metro-labs/daemon/src/claude/usage-probe.ts +2 -2
  25. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +1 -6
  26. package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-logins.ts +53 -29
  27. package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-store.ts +55 -0
  28. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +40 -62
  29. package/runtime/node_modules/@metro-labs/daemon/src/gateway/delivered.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effective.ts +47 -0
  31. package/runtime/node_modules/@metro-labs/daemon/src/gateway/fallback.ts +18 -19
  32. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +0 -1
  33. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +37 -77
  34. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +86 -220
  35. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +80 -283
  36. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-import.ts +125 -0
  37. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-lists.ts +0 -17
  38. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-v2.ts +79 -0
  39. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +0 -79
  40. package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +231 -0
  41. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +4 -44
  42. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +0 -2
  43. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage-refresh.ts +37 -57
  44. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +0 -13
  45. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +5 -15
  46. package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +0 -11
  47. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +3 -7
  48. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -24
  49. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +1 -4
  50. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +1 -2
  51. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +3 -4
  52. package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +9 -92
  53. package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +1 -4
  54. package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +1 -1
  55. package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +0 -2
  56. package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +0 -4
  57. package/runtime/node_modules/@metro-labs/daemon/src/voice/store.ts +1 -14
  58. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +0 -1
  59. package/runtime/runtime.json +1 -1
  60. package/runtime/sdk-runner/runner.json +1 -1
  61. package/dist/background.js +0 -141
  62. package/dist/claude.js +0 -204
  63. package/dist/mcp-config.js +0 -27
  64. package/dist/native-recovery-scan.js +0 -246
  65. package/dist/native-recovery-state.js +0 -154
  66. package/dist/native-recovery.js +0 -164
  67. package/dist/onboarding.js +0 -44
  68. package/dist/voice.js +0 -56
  69. package/runtime/node_modules/@metro-labs/daemon/src/claude/channel-check.ts +0 -56
  70. package/runtime/node_modules/@metro-labs/daemon/src/claude/pinned.ts +0 -16
  71. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +0 -121
  72. package/runtime/node_modules/@metro-labs/daemon/src/claude/runner.ts +0 -45
  73. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-continuity.ts +0 -57
  74. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +0 -76
  75. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +0 -219
  76. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +0 -119
  77. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +0 -160
  78. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +0 -46
  79. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +0 -150
  80. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +0 -139
  81. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +0 -236
  82. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +0 -208
  83. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +0 -157
  84. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -83
  85. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-login.ts +0 -192
  86. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-signin.ts +0 -64
  87. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +0 -36
  88. package/runtime/node_modules/@metro-labs/daemon/src/voice/approvals.ts +0 -38
  89. package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +0 -146
  90. package/runtime/node_modules/@metro-labs/daemon/src/voice/speakable.ts +0 -54
  91. package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +0 -264
@@ -2,7 +2,6 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import {
3
3
  BodyTooLargeError,
4
4
  readBody as readBodyBuffer,
5
- VOICE_MCP_PATH,
6
5
  } from '../routes/http.js';
7
6
  import {
8
7
  authenticate,
@@ -97,9 +96,7 @@ async function serveGet(
97
96
  if (served) session.replayMissed();
98
97
  }
99
98
 
100
- const isVoicePath = (req: IncomingMessage): boolean => (req.url ?? '').split('?')[0] === VOICE_MCP_PATH;
101
-
102
- let activeSlots: SessionSlot[] = [];
99
+ let activeSlot: SessionSlot | undefined;
103
100
  let stopToolNotices: (() => void) | undefined;
104
101
 
105
102
  const MODEL_NOTICE = 'notifications/metro/model';
@@ -118,13 +115,12 @@ export async function createMetroMcp(options: { liveEvents?: boolean } = {}): Pr
118
115
  setLiveEvents: (on: boolean) => void;
119
116
  switchModel: (model: string | null) => boolean;
120
117
  }> {
121
- await Promise.all(activeSlots.map((s) => s.close()));
118
+ await activeSlot?.close();
122
119
  const slot = new SessionSlot(options.liveEvents);
123
- const voiceSlot = new SessionSlot(false, 'voice');
124
- activeSlots = [slot, voiceSlot];
120
+ activeSlot = slot;
125
121
  stopToolNotices?.();
126
122
  stopToolNotices = onConnectorToolsChanged(() => {
127
- for (const each of [slot, voiceSlot]) each.current?.toolsChanged();
123
+ slot.current?.toolsChanged();
128
124
  });
129
125
  connectCallBridge(() => slot.current);
130
126
  connectServerApprovals(() => slot.current);
@@ -142,13 +138,7 @@ export async function createMetroMcp(options: { liveEvents?: boolean } = {}): Pr
142
138
  const parsed = await readOrReject(req, res);
143
139
  if (!parsed.ok) return;
144
140
  await runWithIdentity(identity, async () => {
145
- const session = await resolveSession(
146
- isVoicePath(req) ? voiceSlot : slot,
147
- req,
148
- parsed.body,
149
- identity,
150
- res,
151
- );
141
+ const session = await resolveSession(slot, req, parsed.body, identity, res);
152
142
  if (!session) return;
153
143
  if (isStandaloneGet(req)) {
154
144
  await serveGet(session, req, res);
@@ -19,14 +19,3 @@ export const MCP_INSTRUCTIONS =
19
19
  'prompts are relayed to the chat; the owner answers "yes <id>"/"no <id>". Connectors ' +
20
20
  '(third-party tools like Notion, n8n, Gmail) are reached only through `tool_search` then ' +
21
21
  '`tool_call`, from a worker.';
22
-
23
- export const VOICE_MCP_INSTRUCTIONS =
24
- "Metro's chat tools, opened for a live voice call. The caller hears what you say, so do not " +
25
- 'answer them with `send` or `reply` and do not post the call in a chat; post, react or act on ' +
26
- 'a channel only when the caller asks for it, once. `read` with a `line` (passed verbatim) ' +
27
- 'shows that chat; `group_info` and `list_members` show who is in it; `get_profile` with a ' +
28
- '`from` names a sender. The owner may block a tool on an account or make it need approval ' +
29
- '(`list_accounts` shows each `policy`). A blocked call errors. A call needing approval must ' +
30
- 'run in a background worker: on the main thread it is refused, so delegate that exact call ' +
31
- 'and keep talking; the worker waits while the owner is asked in the chat of the call, where ' +
32
- 'they answer "yes <id>"/"no <id>". Connectors are reached through `tool_search` then `tool_call`.';
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { currentBusSeq } from '@metro-labs/core/events';
3
3
  import { errMsg } from '@metro-labs/core/log';
4
4
  import { newReplayLedger } from '../channels/relay.js';
5
- import { McpSession, channelLog, type SessionKind } from './session.js';
5
+ import { McpSession, channelLog } from './session.js';
6
6
  import { allowedAgents, type RequestIdentity } from './request-identity.js';
7
7
  import { diagnosticTag, type SessionDiagnostic, type SlotDiagnostic } from './diagnostics.js';
8
8
 
@@ -25,10 +25,7 @@ export class SessionSlot {
25
25
  private changing: Promise<void> = Promise.resolve();
26
26
  private readonly replacements: SlotReplacement[] = [];
27
27
 
28
- constructor(
29
- live = true,
30
- private readonly kind: SessionKind = 'chat',
31
- ) {
28
+ constructor(live = true) {
32
29
  this.live = live;
33
30
  }
34
31
 
@@ -111,7 +108,6 @@ export class SessionSlot {
111
108
  if (this.ledger.startAt < 0) this.ledger.startAt = currentBusSeq();
112
109
  const id = adoptId ?? randomUUID();
113
110
  const session = await McpSession.open({
114
- kind: this.kind,
115
111
  id,
116
112
  scope: allowedAgents(identity),
117
113
  adopted: adoptId !== undefined,
@@ -129,7 +125,7 @@ export class SessionSlot {
129
125
  this.session = session;
130
126
  this.recordReplacement(session, adoptId, previous);
131
127
  if (this.started && this.live) session.startChannel();
132
- channelLog('session: opened', 'id', id, 'kind', this.kind, 'adopted', adoptId !== undefined);
128
+ channelLog('session: opened', 'id', id, 'adopted', adoptId !== undefined);
133
129
  return session;
134
130
  }
135
131
 
@@ -8,7 +8,7 @@ import { errMsg } from '@metro-labs/core/log';
8
8
  import { allowlistForLine, mayApprove, senderPermitted } from '../agents/map.js';
9
9
  import { accountStationNames, stationByName } from '../stations/registry.js';
10
10
  import { eventInScope } from '../agents/scope.js';
11
- import { MCP_INSTRUCTIONS, VOICE_MCP_INSTRUCTIONS } from './instructions.js';
11
+ import { MCP_INSTRUCTIONS } from './instructions.js';
12
12
  import { BoundedEventStore } from './event-store.js';
13
13
  import { registerPermissionRelay } from './permission-relay.js';
14
14
  import { registerToolHandlers, toolSchemaSignature } from './tool-dispatch.js';
@@ -55,12 +55,7 @@ function makeTransport(
55
55
  return t;
56
56
  }
57
57
 
58
- export type SessionKind = 'chat' | 'voice';
59
-
60
- const CHANNEL_CAPABILITIES = { experimental: { 'claude/channel': {}, 'claude/channel/permission': {} } };
61
-
62
58
  export interface SessionInit {
63
- kind?: SessionKind;
64
59
  id: string;
65
60
  scope: Set<string>;
66
61
  adopted: boolean;
@@ -84,7 +79,6 @@ export class McpSession {
84
79
  private issuedSchema: string | undefined;
85
80
  private announcing = false;
86
81
  private readonly onClosed: (session: McpSession) => void;
87
- private readonly kind: SessionKind;
88
82
  private readonly openedAt = new Date().toISOString();
89
83
  private clientInitializedAt: string | null = null;
90
84
  private lastStreamAttachedAt: string | null = null;
@@ -92,20 +86,15 @@ export class McpSession {
92
86
 
93
87
  private constructor(init: SessionInit) {
94
88
  this.id = init.id;
95
- this.kind = init.kind ?? 'chat';
96
89
  this.scope = init.scope;
97
90
  this.live = init.live;
98
91
  this.issuedSchema = init.adopted ? undefined : toolSchemaSignature();
99
92
  this.onClosed = init.onClosed;
100
93
  this.eventStore = new BoundedEventStore();
101
94
  this.transport = makeTransport(init.id, this.eventStore, init.adopted);
102
- const voice = init.kind === 'voice';
103
95
  this.server = new Server(
104
96
  { name: 'metro', version: '0.1.0' },
105
- {
106
- capabilities: { ...(voice ? {} : CHANNEL_CAPABILITIES), tools: { listChanged: true } },
107
- instructions: voice ? VOICE_MCP_INSTRUCTIONS : MCP_INSTRUCTIONS,
108
- },
97
+ { capabilities: { tools: { listChanged: true } }, instructions: MCP_INSTRUCTIONS },
109
98
  );
110
99
  this.server.oninitialized = (): void => {
111
100
  this.clientInitializedAt = new Date().toISOString();
@@ -130,16 +119,15 @@ export class McpSession {
130
119
  approves,
131
120
  noteDirect,
132
121
  answerPermission,
133
- callMeta: (event) => voice ? () => ({}) : sharedCalls.bindChat(event, this.scope),
122
+ callMeta: (event) => sharedCalls.bindChat(event, this.scope),
123
+ });
124
+ registerPermissionRelay({
125
+ mcp: this.server,
126
+ relay: this.relay,
127
+ inScope: (line) => this.inScope(line),
128
+ live: this.live,
129
+ log: channelLog,
134
130
  });
135
- if (!voice)
136
- registerPermissionRelay({
137
- mcp: this.server,
138
- relay: this.relay,
139
- inScope: (line) => this.inScope(line),
140
- live: this.live,
141
- log: channelLog,
142
- });
143
131
  this.channel = new ChannelRelay({
144
132
  relay: this.relay,
145
133
  log: channelLog,
@@ -214,7 +202,7 @@ export class McpSession {
214
202
  }
215
203
 
216
204
  private announceCallState(): void {
217
- if (this.kind !== 'chat' || !this.streamAttached || this.server.getClientVersion()?.name !== 'metro-sdk-runner') return;
205
+ if (!this.streamAttached || this.server.getClientVersion()?.name !== 'metro-sdk-runner') return;
218
206
  const active = sharedCalls.snapshot();
219
207
  const route = active !== null && this.scope.has(active.agentId) && eventInScope(this.scope, active.line) ? active : null;
220
208
  this.server.notification({ method: CALL_STATE, params: { route, ...(route === null ? {} : { meta: { line: route.line } }) } }).catch((err: unknown) => {
@@ -264,7 +252,7 @@ export class McpSession {
264
252
  async close(): Promise<void> {
265
253
  if (this.closed) return;
266
254
  this.closed = true;
267
- if (this.kind === 'chat') sharedCalls.revoke();
255
+ sharedCalls.revoke();
268
256
  this.onClosed(this);
269
257
  forgetPromptsOf(this.server);
270
258
  this.dropStream();
@@ -6,9 +6,6 @@ import { bearerSession, clientId, jwksUrl, SigningKeys, workosBase, type KeyStor
6
6
  import { ensureSecureDir, writeSecure } from '@metro-labs/core/secure-fs';
7
7
 
8
8
  const KEYS_FILE = '.jwks';
9
- const OPERATOR_USER = 'user_01M2TN3K6NRVX675M617GR4WVE';
10
-
11
- export const isOperatorUser = (user: string): boolean => user === OPERATOR_USER;
12
9
 
13
10
  export function jwksStore(dir: string): KeyStore {
14
11
  const path = join(dir, KEYS_FILE);
@@ -28,7 +25,7 @@ export function bearerSessionsFor(owner: () => string | null, keys: SigningKeys)
28
25
  const held = owner();
29
26
  if (held === null || session.organization === null || session.organization !== held)
30
27
  throw new ApiError('this machine belongs to another organization', 403);
31
- return { subject: held, userId: session.userId, sessionId: session.sessionId, role: session.role === 'admin' ? 'admin' : 'member', ...(isOperatorUser(session.userId) ? { operator: true as const } : {}) };
28
+ return { subject: held, userId: session.userId, sessionId: session.sessionId, role: session.role === 'admin' ? 'admin' : 'member' };
32
29
  };
33
30
  }
34
31
 
@@ -38,7 +38,6 @@ import { applyMcpCors, handleMcpPreflight } from '@metro-labs/http/cors';
38
38
  import { METRO_VERSION } from '@metro-labs/core/version';
39
39
 
40
40
  const LRU_CAP = 2_000;
41
- export const VOICE_MCP_PATH = '/mcp/voice';
42
41
 
43
42
  function dedupKey(
44
43
  e: Pick<MetroEvent, 'station' | 'line' | 'messageId'>,
@@ -214,7 +213,7 @@ export async function startWebhookServer(
214
213
 
215
214
  function isMcpPath(req: IncomingMessage): boolean {
216
215
  const path = (req.url ?? '').split('?')[0];
217
- return path === '/' || path === '/mcp' || path === VOICE_MCP_PATH || path === MCP_STATUS_PATH;
216
+ return path === '/' || path === '/mcp' || path === MCP_STATUS_PATH;
218
217
  }
219
218
 
220
219
  function flatHeaders(req: IncomingMessage): Record<string, string> {
@@ -23,7 +23,7 @@ import {
23
23
  localOwner,
24
24
  readLocalAgentFile,
25
25
  } from '../agents/file-admin.js';
26
- import { readModelConfig } from '../gateway/model-config.js';
26
+ import { effectiveSetup } from '../gateway/effective.js';
27
27
  import type { StationName } from '@metro-labs/core/station-names';
28
28
 
29
29
  export interface LocalModeDeps {
@@ -36,7 +36,6 @@ export interface LocalModeDeps {
36
36
  toolGroups?: AgentApiDeps['toolGroups'];
37
37
  prepareAccount: AccountApiDeps['prepareAccount'];
38
38
  liveEvents?: (on: boolean) => void;
39
- switchModel?: (model: string | null) => boolean;
40
39
  }
41
40
 
42
41
  function attachSessions(deps: LocalModeDeps): AttachSessions {
@@ -142,8 +141,8 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
142
141
  controlApi: { restart: deps.restart, stop: deps.stop },
143
142
  ownerApi: { setOwner: setLocalOwner },
144
143
  machineApi: {},
145
- modelApi: { switchModel: deps.switchModel, owner: localOwner },
146
- gateway: { config: readModelConfig },
144
+ modelApi: {},
145
+ gateway: { setup: () => effectiveSetup() },
147
146
  terminalApi: {},
148
147
  mode: localModeInfo,
149
148
  };
@@ -1,20 +1,15 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import type { CallRoute } from '@metro-labs/core/call';
3
3
  import { errMsg, log } from '@metro-labs/core/log';
4
- import { isRecord } from '@metro-labs/core/is-record';
5
4
  import { forwardTrainCall } from '../stations/train-call.js';
6
- import { CallApprovals } from './approvals.js';
7
5
  import type { Playback } from './audio.js';
8
6
  import { Peer, type PeerEvents } from './peer.js';
9
7
  import { SharedTalk } from './shared-talk.js';
10
- import { endSharedCall, hearSharedCall, openSharedCall, sharedCallsSelected, type SharedCallTransport } from './shared.js';
11
- import { Talk } from './talk.js';
8
+ import { endSharedCall, hearSharedCall, openSharedCall, type SharedCallTransport } from './shared.js';
12
9
  import type { VoiceConfig } from './store.js';
13
10
 
14
11
  const OFFER_WAIT_MS = 45_000;
15
12
  const LONGEST_CALL_MS = 60 * 60_000;
16
- const CONTEXT_MESSAGES = 12;
17
- const CONTEXT_CHARS = 400;
18
13
  const LEAVE_RETRY_MS = 3_000;
19
14
  const SIGNAL_WAIT_MS = 2_000;
20
15
 
@@ -22,24 +17,18 @@ export interface CallStart {
22
17
  agentId: string;
23
18
  sourceId: string;
24
19
  line: string;
25
- lineName: string;
26
- direct: boolean;
27
20
  from: string;
28
- callerName: string;
29
21
  callId: string;
30
22
  callerPeer: string;
31
23
  }
32
24
 
33
- type CallAudio = Pick<Talk, 'connect' | 'hear' | 'finish'>;
25
+ type CallAudio = Pick<SharedTalk, 'connect' | 'hear' | 'finish'> & SharedCallTransport;
34
26
  type CallPeer = Pick<Peer, 'answer' | 'sendOpus' | 'close'>;
35
- type ContextStart = Omit<CallStart, 'agentId' | 'sourceId'>;
36
27
 
37
28
  export interface CallDependencies {
38
29
  train: typeof trainCall;
39
30
  peer(events: PeerEvents): CallPeer;
40
- cli(cfg: VoiceConfig, out: Playback['send'], ended: (reason: string) => void, approvals: CallApprovals): CallAudio & Pick<Talk, 'prime'>;
41
- shared(cfg: VoiceConfig, out: Playback['send'], ended: (reason: string) => void, heard: (text: string, sourceId: string) => void): CallAudio & SharedCallTransport;
42
- selected: typeof sharedCallsSelected;
31
+ shared(cfg: VoiceConfig, out: Playback['send'], ended: (reason: string) => void, heard: (text: string, sourceId: string) => void): CallAudio;
43
32
  open: typeof openSharedCall;
44
33
  heard: typeof hearSharedCall;
45
34
  ended: typeof endSharedCall;
@@ -47,8 +36,6 @@ export interface CallDependencies {
47
36
  leaveRetryMs: number;
48
37
  }
49
38
 
50
- const str = (value: unknown): string => (typeof value === 'string' ? value : '');
51
-
52
39
  export async function trainCall(action: string, args: Record<string, unknown>): Promise<unknown> {
53
40
  const answer = await forwardTrainCall('xmtp', action, args);
54
41
  if (answer.error !== undefined) throw new Error(answer.error);
@@ -58,9 +45,7 @@ export async function trainCall(action: string, args: Record<string, unknown>):
58
45
  const dependencies: CallDependencies = {
59
46
  train: trainCall,
60
47
  peer: (events) => new Peer(events),
61
- cli: (cfg, out, ended, approvals) => new Talk(cfg, out, ended, approvals),
62
48
  shared: (cfg, out, ended, heard) => new SharedTalk(cfg, out, ended, heard),
63
- selected: sharedCallsSelected,
64
49
  open: openSharedCall,
65
50
  heard: hearSharedCall,
66
51
  ended: endSharedCall,
@@ -68,51 +53,11 @@ const dependencies: CallDependencies = {
68
53
  leaveRetryMs: LEAVE_RETRY_MS,
69
54
  };
70
55
 
71
- function speaker(row: Record<string, unknown>, start: ContextStart): string {
72
- if (row.self === true) return 'you';
73
- return row.from === start.from ? start.callerName : `another member (${str(row.from)})`;
74
- }
75
-
76
- function contextLine(row: unknown, start: ContextStart): string | null {
77
- if (!isRecord(row) || typeof row.text !== 'string' || row.text.startsWith('[call')) return null;
78
- const who = speaker(row, start);
79
- return `${str(row.ts).slice(11, 16)} ${who}: ${row.text.replace(/\s+/g, ' ').slice(0, CONTEXT_CHARS)}`;
80
- }
81
-
82
- async function recentChat(start: CallStart, train: typeof trainCall): Promise<string> {
83
- try {
84
- const result = await train('read', { line: start.line, limit: CONTEXT_MESSAGES });
85
- const rows = isRecord(result) && Array.isArray(result.messages) ? result.messages : [];
86
- return rows.map((row) => contextLine(row, start)).filter((l): l is string => l !== null).join('\n');
87
- } catch (err) {
88
- log.warn({ err: errMsg(err) }, 'voice: could not read the chat before the call');
89
- return '';
90
- }
91
- }
92
-
93
- function where(start: ContextStart): string {
94
- if (start.lineName !== '') return `the group chat "${start.lineName}"`;
95
- return start.direct ? `your direct chat with ${start.callerName}` : 'a group chat with no name';
96
- }
97
-
98
- export function opening(start: ContextStart, chat: string): string {
99
- const recent = chat === '' ? 'No earlier messages were found there.' : `Recent messages there before the call, oldest first (times in UTC):\n${chat}`;
100
- return [
101
- `[A voice call with ${start.callerName} is starting on Stage, in ${where(start)}.`,
102
- `Line of this chat: ${start.line} (pass it verbatim to the metro tools, e.g. read, to look at or act on this chat).`,
103
- `Caller: ${start.callerName} (${start.from}).`,
104
- recent,
105
- `Greet ${start.callerName} in a few words.]`,
106
- ].join('\n');
107
- }
108
-
109
56
  export class Call {
110
57
  readonly route: CallRoute;
111
58
  private readonly selfPeer = randomUUID();
112
59
  private readonly startedAt = Date.now();
113
60
  private readonly talk: CallAudio;
114
- private readonly shared: SharedCallTransport | null;
115
- private readonly prime: ((context: string) => void) | null;
116
61
  private readonly deps: CallDependencies;
117
62
  private peer: CallPeer | null = null;
118
63
  private timer: ReturnType<typeof setTimeout> | null = null;
@@ -136,24 +81,9 @@ export class Call {
136
81
  const ended = (reason: string): void => {
137
82
  this.hangUp(reason);
138
83
  };
139
- if (this.deps.selected()) {
140
- const shared = this.deps.shared(cfg, out, ended, (text, sourceId) => {
141
- if (!this.over) this.deps.heard(this.route, text, sourceId);
142
- });
143
- this.talk = shared;
144
- this.shared = shared;
145
- this.prime = null;
146
- } else {
147
- const approvals = new CallApprovals(start.line, async (text) => {
148
- await this.deps.train('send', { line: start.line, text });
149
- });
150
- const cli = this.deps.cli(cfg, out, ended, approvals);
151
- this.talk = cli;
152
- this.shared = null;
153
- this.prime = (context) => {
154
- cli.prime(context);
155
- };
156
- }
84
+ this.talk = this.deps.shared(cfg, out, ended, (text, sourceId) => {
85
+ if (!this.over) this.deps.heard(this.route, text, sourceId);
86
+ });
157
87
  this.longest = setTimeout(() => {
158
88
  this.hangUp('the call reached one hour');
159
89
  }, LONGEST_CALL_MS);
@@ -163,15 +93,7 @@ export class Call {
163
93
  if (this.over || this.begun) return;
164
94
  this.begun = true;
165
95
  log.info({ line: this.start.line, callId: this.start.callId }, 'voice: answering a call');
166
- if (this.shared !== null) {
167
- if (!this.deps.open(this.route, this.start.sourceId, this.shared)) throw new Error('the shared conversation is unavailable');
168
- } else {
169
- recentChat(this.start, this.deps.train).then((chat) => {
170
- if (!this.over) this.prime?.(opening(this.start, chat));
171
- }).catch((err: unknown) => {
172
- log.warn({ err: errMsg(err) }, 'voice: could not start the agent on the call');
173
- });
174
- }
96
+ if (!this.deps.open(this.route, this.start.sourceId, this.talk)) throw new Error('the shared conversation is unavailable');
175
97
  await this.signal({ kind: 'join', callId: this.start.callId, from: this.selfPeer });
176
98
  if (this.over) {
177
99
  await this.sendLeave();
@@ -235,14 +157,9 @@ export class Call {
235
157
  log.info({ reason, seconds: Math.round((Date.now() - this.startedAt) / 1000) }, 'voice: call over');
236
158
  this.talk.finish();
237
159
  this.peer?.close();
238
- if (this.shared !== null) this.deps.ended(this.route);
160
+ this.deps.ended(this.route);
239
161
  this.onOver();
240
- this.left = this.sendLeave().then(async () => {
241
- if (this.shared !== null || reason.startsWith('the caller') || reason === 'no offer arrived') return;
242
- await this.deps.train('send', { line: this.start.line, text: `The voice call ended: ${reason}.` });
243
- }).catch((err: unknown) => {
244
- log.warn({ err: errMsg(err) }, 'voice: wrapping up the call failed');
245
- });
162
+ this.left = this.sendLeave();
246
163
  }
247
164
 
248
165
  private async sendLeave(): Promise<void> {
@@ -26,7 +26,6 @@ const str = (value: unknown): string => (typeof value === 'string' ? value : '')
26
26
 
27
27
  const contentTypeOf = (env: TrainEvent): string => str(record(env.payload).contentType);
28
28
  const sourceIdOf = (env: TrainEvent): string => str(env.message_id) || str(env.id) || randomUUID();
29
- const callerNameOf = (env: TrainEvent): string => str(env.from_display_name) || str(env.from_name) || 'the caller';
30
29
 
31
30
  export const isCallEvent = (env: TrainEvent): boolean => CALL_TYPES.has(contentTypeOf(env));
32
31
 
@@ -74,9 +73,7 @@ function invite(env: TrainEvent, body: Record<string, unknown>, create: MakeCall
74
73
  invites.add(key);
75
74
  if (invites.size > INVITES_MAX) invites.delete(invites.values().next().value ?? '');
76
75
  current?.hangUp('the caller started a new call');
77
- const callerName = callerNameOf(env);
78
- const sourceId = sourceIdOf(env);
79
- const start = { agentId, sourceId, line, lineName: str(env.line_name), direct: env.is_private === true, from, callerName, callId, callerPeer };
76
+ const start = { agentId, sourceId: sourceIdOf(env), line, from, callId, callerPeer };
80
77
  const call = create(start, readVoice(), () => {
81
78
  if (current === call) current = null;
82
79
  });
@@ -7,7 +7,7 @@ export const elevenLabs = (): string => process.env.METRO_ELEVENLABS_URL ?? 'wss
7
7
  const STT_RATE = 16_000;
8
8
  const CHUNK_SAMPLES = STT_RATE / 10;
9
9
  const HELD_SAMPLES = STT_RATE * 2;
10
- export const VAD_SILENCE_SECS = 0.5;
10
+ const VAD_SILENCE_SECS = 0.5;
11
11
 
12
12
  export interface Heard {
13
13
  partial(text: string): void;
@@ -4,7 +4,6 @@ import { str } from '@metro-labs/core/str';
4
4
  import { log } from '@metro-labs/core/log';
5
5
  import { agentIdForLine, allowlistForLine, lineReceives, mayApprove, senderPermitted, accountFromLine } from '../agents/map.js';
6
6
  import { eventInScope } from '../agents/scope.js';
7
- import { harnessRunner } from '../claude/runner.js';
8
7
  import { policyFor, type Access } from '../policy/policy.js';
9
8
 
10
9
  export interface SharedSpeechAction {
@@ -194,7 +193,6 @@ export class SharedCalls {
194
193
  }
195
194
 
196
195
  export const sharedCalls = new SharedCalls();
197
- export const sharedCallsSelected = (): boolean => harnessRunner() === 'sdk';
198
196
  export const openSharedCall = (route: CallRoute, sourceId: string, transport: SharedCallTransport): boolean => sharedCalls.open(route, sourceId, transport);
199
197
  export const hearSharedCall = (route: CallRoute, text: string, sourceId: string): void => { sharedCalls.heard(route, text, sourceId); };
200
198
  export const endSharedCall = (route: CallRoute): void => { sharedCalls.end(route); };
@@ -61,10 +61,6 @@ export class Utterance {
61
61
  });
62
62
  }
63
63
 
64
- get usable(): boolean {
65
- return !this.closed && this.listener === null && this.ws.readyState <= WebSocket.OPEN;
66
- }
67
-
68
64
  attach(listener: Voiced): void {
69
65
  if (!this.aborted && !this.finished) this.listener = listener;
70
66
  }
@@ -3,7 +3,6 @@ import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
3
3
  import { isRecord } from '@metro-labs/core/is-record';
4
4
  import { ApiError } from '@metro-labs/http/api-error';
5
5
  import { agentsDir } from '../agents/files.js';
6
- import { readModelConfig, type ModelConfig } from '../gateway/model-config.js';
7
6
 
8
7
  const VOICE_PROVIDERS = ['elevenlabs'] as const;
9
8
  type VoiceProvider = (typeof VOICE_PROVIDERS)[number];
@@ -12,7 +11,6 @@ export interface VoiceConfig {
12
11
  provider: VoiceProvider;
13
12
  apiKey: string;
14
13
  voiceId: string;
15
- model: string;
16
14
  language: string;
17
15
  enabled: boolean;
18
16
  }
@@ -22,7 +20,6 @@ const AUTO_LANGUAGE = 'auto';
22
20
  const LANGUAGE_RE = /^(?:[a-z]{2,3}|auto)$/;
23
21
 
24
22
  const DEFAULT_VOICE_ID = 'EXAVITQu4vr4xnSDxMaL';
25
- const DEFAULT_VOICE_MODEL = 'anthropic:claude-sonnet-5-5';
26
23
 
27
24
  const VOICE_FILE = 'voice.json';
28
25
  const MAX_FIELD = 512;
@@ -38,7 +35,6 @@ export function parseVoice(raw: unknown): VoiceConfig {
38
35
  provider: isProvider(r.provider) ? r.provider : 'elevenlabs',
39
36
  apiKey: text(r.apiKey),
40
37
  voiceId: text(r.voiceId),
41
- model: text(r.model),
42
38
  language: LANGUAGE_RE.test(text(r.language)) ? text(r.language) : '',
43
39
  enabled: r.enabled !== false,
44
40
  };
@@ -52,13 +48,6 @@ export function writeVoice(cfg: VoiceConfig, dir = agentsDir()): void {
52
48
  }
53
49
 
54
50
  export const voiceIdOf = (cfg: VoiceConfig): string => (cfg.voiceId === '' ? DEFAULT_VOICE_ID : cfg.voiceId);
55
- const voiceModelOf = (cfg: VoiceConfig): string => (cfg.model === '' ? DEFAULT_VOICE_MODEL : cfg.model);
56
- export function brainModel(cfg: VoiceConfig, models: ModelConfig = readModelConfig()): string {
57
- const model = voiceModelOf(cfg);
58
- const anthropic = /^anthropic:(.+)$/.exec(model)?.[1];
59
- if (anthropic === undefined) return model;
60
- return models.connections.some((c) => c.provider === 'anthropic') ? model : anthropic;
61
- }
62
51
 
63
52
  export function languageOf(cfg: VoiceConfig): string | null {
64
53
  const language = cfg.language === '' ? DEFAULT_LANGUAGE : cfg.language;
@@ -73,10 +62,9 @@ export function publicVoice(cfg: VoiceConfig): Record<string, unknown> {
73
62
  providers: VOICE_PROVIDERS,
74
63
  hasKey: cfg.apiKey !== '',
75
64
  voiceId: cfg.voiceId,
76
- model: cfg.model,
77
65
  language: cfg.language,
78
66
  enabled: cfg.enabled,
79
- defaults: { voiceId: DEFAULT_VOICE_ID, model: DEFAULT_VOICE_MODEL, language: DEFAULT_LANGUAGE },
67
+ defaults: { voiceId: DEFAULT_VOICE_ID, language: DEFAULT_LANGUAGE },
80
68
  };
81
69
  }
82
70
 
@@ -109,7 +97,6 @@ export function patchVoice(cfg: VoiceConfig, body: unknown): VoiceConfig {
109
97
  provider: isProvider(body.provider) ? body.provider : cfg.provider,
110
98
  apiKey: keyOf(body, cfg.apiKey),
111
99
  voiceId: field(body, 'voiceId', cfg.voiceId),
112
- model: field(body, 'model', cfg.model),
113
100
  language: languageField(body, cfg.language),
114
101
  enabled: typeof body.enabled === 'boolean' ? body.enabled : cfg.enabled,
115
102
  };
@@ -11,7 +11,6 @@ export interface ApiSession {
11
11
  userId?: string;
12
12
  sessionId?: string;
13
13
  role: Role;
14
- operator?: true;
15
14
  }
16
15
 
17
16
  export type BearerSessions = (req: IncomingMessage) => Promise<ApiSession | null>;
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.307"
2
+ "version": "0.1.0-beta.309"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.307",
2
+ "version": "0.1.0-beta.309",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",