svamp-cli 0.2.91 → 0.2.92

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.
@@ -6,7 +6,6 @@ import { fileURLToPath } from 'url';
6
6
  import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
- import vm from 'node:vm';
10
9
  import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
11
10
  import { WebSocket } from 'ws';
12
11
  import { promisify } from 'util';
@@ -380,54 +379,6 @@ function buildSkillsPromptSection(skills) {
380
379
  ].join("\n");
381
380
  }
382
381
 
383
- function buildSvampApi(deps) {
384
- return {
385
- bash: (command, opts) => deps.runBash(command, opts),
386
- context: () => deps.getContext(),
387
- ask: (question) => deps.askSession(question),
388
- summarize: (question) => deps.summarizeSession(question),
389
- send: (message) => deps.sessionSend(message)
390
- };
391
- }
392
- async function runJs(code, deps, opts = {}) {
393
- return runJsWithApi(code, buildSvampApi(deps), opts);
394
- }
395
- async function runJsWithApi(code, api, opts = {}) {
396
- const timeoutMs = opts.timeoutMs ?? 5e3;
397
- const logs = [];
398
- const capture = (...a) => {
399
- logs.push(a.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join(" "));
400
- };
401
- const sandbox = /* @__PURE__ */ Object.create(null);
402
- sandbox.svamp = api;
403
- sandbox.console = { log: capture, error: capture, warn: capture, info: capture };
404
- const context = vm.createContext(sandbox, { name: "wise-run_js" });
405
- const wrapped = `(async () => {
406
- ${code}
407
- })()`;
408
- let script;
409
- try {
410
- script = new vm.Script(wrapped, { filename: "wise-run_js.js" });
411
- } catch (e) {
412
- return { result: void 0, logs, error: "compile error: " + e.message };
413
- }
414
- let timer;
415
- try {
416
- const ran = script.runInContext(context, { timeout: timeoutMs });
417
- const result = await Promise.race([
418
- Promise.resolve(ran),
419
- new Promise((_, reject) => {
420
- timer = setTimeout(() => reject(new Error("run_js timed out")), timeoutMs);
421
- })
422
- ]);
423
- return { result, logs };
424
- } catch (e) {
425
- return { result: void 0, logs, error: e.message };
426
- } finally {
427
- if (timer) clearTimeout(timer);
428
- }
429
- }
430
-
431
382
  const READ_ONLY_TOOLS = ["get_context", "ask_session", "summarize_session", "use_skill"];
432
383
  const str$1 = (v) => v == null ? "" : String(v);
433
384
  function buildTools(deps, skills) {
@@ -483,16 +434,6 @@ function buildTools(deps, skills) {
483
434
  }
484
435
  return "(sent to the coding agent)";
485
436
  }
486
- },
487
- {
488
- name: "run_js",
489
- readOnly: false,
490
- description: "Run JavaScript with an async `svamp` API to read state and compose several steps in one call: svamp.bash(cmd), svamp.context(), svamp.ask(q), svamp.summarize(q), svamp.send(msg). Use `await` and `return` a value; console.log is captured. Sandboxed, ~5s limit.",
491
- parameters: { type: "object", properties: { code: { type: "string", description: "JS body; may use await and return." } }, required: ["code"], additionalProperties: false },
492
- run: async (a) => {
493
- const r = await runJs(str$1(a?.code), deps, { timeoutMs: 5e3 });
494
- return JSON.stringify({ result: r.result, logs: r.logs, error: r.error });
495
- }
496
437
  }
497
438
  ];
498
439
  }
@@ -619,7 +560,6 @@ You are WISE Agent, a fast, text-mode companion to the deep coding agent (Claude
619
560
  - summarize_session \u2014 a cheap subagent that summarizes the deep agent's transcript for a specific question.
620
561
  - use_skill \u2014 load a project skill's full steps by name, then carry them out with run_bash.
621
562
  - run_bash \u2014 run a shell command on the session's machine (when granted).
622
- - run_js \u2014 JavaScript with an async \`svamp\` API to compose several steps in one call (when granted).
623
563
  - send_to_session \u2014 hand a clear, reformulated instruction to the deep coding agent (when granted); pass wait=true to block for its reply.
624
564
 
625
565
  # Instructions
@@ -733,7 +673,7 @@ async function runWiseAgent(args) {
733
673
  }
734
674
 
735
675
  const MACHINE_READ_ONLY = ["daemon_status", "list_sessions", "read_session", "use_skill"];
736
- const MACHINE_MUTATING = ["run_bash", "run_js", "send_to_session", "spawn_session"];
676
+ const MACHINE_MUTATING = ["run_bash", "send_to_session", "spawn_session"];
737
677
  const ALL_MACHINE_TOOLS = [...MACHINE_READ_ONLY, ...MACHINE_MUTATING];
738
678
  const str = (v) => v == null ? "" : String(v);
739
679
  function machineToolsForRole(role) {
@@ -741,16 +681,6 @@ function machineToolsForRole(role) {
741
681
  if (role === "interact") return [...MACHINE_READ_ONLY, "send_to_session"];
742
682
  return [...MACHINE_READ_ONLY];
743
683
  }
744
- function machineSvampApi(deps) {
745
- return {
746
- bash: (command, opts) => deps.runBash(command, opts),
747
- status: () => deps.daemonStatus(),
748
- listSessions: () => deps.listSessions(),
749
- readSession: (id, opts) => deps.readSession(id, opts),
750
- sendToSession: (id, message) => deps.sendToSession(id, message),
751
- spawnSession: (directory) => deps.spawnSession(directory)
752
- };
753
- }
754
684
  function buildMachineTools(deps, skills) {
755
685
  return [
756
686
  {
@@ -804,16 +734,6 @@ function buildMachineTools(deps, skills) {
804
734
  description: "Start a new agent session in a directory on this machine.",
805
735
  parameters: { type: "object", properties: { directory: { type: "string" } }, required: ["directory"], additionalProperties: false },
806
736
  run: async (a) => JSON.stringify(await deps.spawnSession(str(a?.directory)))
807
- },
808
- {
809
- name: "run_js",
810
- readOnly: false,
811
- description: "Run JavaScript with an async `svamp` machine API to compose steps: svamp.bash(cmd), svamp.status(), svamp.listSessions(), svamp.readSession(id), svamp.sendToSession(id,msg), svamp.spawnSession(dir). Use await + return; console.log captured. Sandboxed, ~5s.",
812
- parameters: { type: "object", properties: { code: { type: "string" } }, required: ["code"], additionalProperties: false },
813
- run: async (a) => {
814
- const r = await runJsWithApi(str(a?.code), machineSvampApi(deps), { timeoutMs: 5e3 });
815
- return JSON.stringify({ result: r.result, logs: r.logs, error: r.error });
816
- }
817
737
  }
818
738
  ];
819
739
  }
@@ -848,7 +768,7 @@ You are WISE in machine-manager (global) mode \u2014 a fast cockpit over the sva
848
768
  # Tools
849
769
  - daemon_status \u2014 daemon health / version / session count. Use first for machine status.
850
770
  - list_sessions / read_session \u2014 see all sessions and inspect any one.
851
- - run_bash / run_js \u2014 run quick commands / scripts on the machine.
771
+ - run_bash \u2014 run quick commands / scripts on the machine.
852
772
  - send_to_session(id, message) \u2014 delegate a task to a specific session's Claude agent.
853
773
  - spawn_session(directory) \u2014 start a new session.
854
774
  - use_skill \u2014 load a machine-level procedure.
@@ -1022,7 +942,7 @@ function buildSessionDeps(rpc, opts = {}) {
1022
942
  }
1023
943
 
1024
944
  function toolsForRole(role) {
1025
- if (role === "admin") return [...READ_ONLY_TOOLS, "run_bash", "send_to_session", "run_js"];
945
+ if (role === "admin") return [...READ_ONLY_TOOLS, "run_bash", "send_to_session"];
1026
946
  if (role === "interact") return [...READ_ONLY_TOOLS, "send_to_session"];
1027
947
  return [...READ_ONLY_TOOLS];
1028
948
  }
@@ -1033,6 +953,7 @@ function buildVoiceSessionUpdate(instructions, tools, opts) {
1033
953
  return {
1034
954
  type: "session.update",
1035
955
  session: {
956
+ type: "realtime",
1036
957
  instructions,
1037
958
  tools: toRealtimeTools(tools),
1038
959
  tool_choice: "auto",
@@ -1094,20 +1015,88 @@ var sideband = /*#__PURE__*/Object.freeze({
1094
1015
 
1095
1016
  const realFactory = (url, headers) => new WebSocket(url, { headers });
1096
1017
  let _testFactory;
1018
+ const NOISY_EVENTS = /* @__PURE__ */ new Set([
1019
+ "response.audio.delta",
1020
+ "response.output_audio.delta",
1021
+ "response.audio_transcript.delta",
1022
+ "response.output_audio_transcript.delta",
1023
+ "response.text.delta",
1024
+ "response.output_text.delta",
1025
+ "response.function_call_arguments.delta",
1026
+ "input_audio_buffer.append",
1027
+ "rate_limits.updated",
1028
+ "conversation.item.input_audio_transcription.delta"
1029
+ ]);
1030
+ const tag = (callId) => `[wise-voice callId=${callId.slice(0, 14)}\u2026]`;
1097
1031
  async function openSideband(opts) {
1098
1032
  const base = (opts.baseUrl || "https://api.openai.com").replace(/^http/, "ws").replace(/\/+$/, "");
1099
1033
  const url = `${base}/v1/realtime?call_id=${encodeURIComponent(opts.callId)}`;
1100
1034
  const { tools, sessionUpdate } = opts.prepared ?? await initVoiceSession(opts.init);
1035
+ const T = tag(opts.callId);
1036
+ const toolNames = tools.map((t) => t.name);
1037
+ console.log(`${T} OPENING sideband \u2192 ${base}/v1/realtime (key=${opts.apiKey ? "set:" + opts.apiKey.slice(0, 7) + "\u2026" : "MISSING"}) | ${toolNames.length} tools: [${toolNames.join(", ")}]`);
1101
1038
  const ws = (opts.wsFactory || _testFactory || realFactory)(url, { Authorization: `Bearer ${opts.apiKey}` });
1102
1039
  const send = (e) => {
1103
1040
  try {
1104
1041
  ws.send(JSON.stringify(e));
1105
- } catch {
1042
+ } catch (err) {
1043
+ console.error(`${T} send failed (socket gone): ${err?.message || err}`);
1106
1044
  }
1107
1045
  };
1108
1046
  const nameByCallId = /* @__PURE__ */ new Map();
1109
- ws.on("open", () => send(sessionUpdate));
1110
- const wantTools = new Set(tools.map((t) => t.name));
1047
+ let eventCount = 0;
1048
+ let toolCalls = 0;
1049
+ let closed = false;
1050
+ const idleMs = opts.idleTimeoutMs ?? 12e4;
1051
+ const lifeMs = opts.maxLifetimeMs ?? 30 * 6e4;
1052
+ let idleTimer;
1053
+ let lifeTimer;
1054
+ const clearTimers = () => {
1055
+ if (idleTimer) clearTimeout(idleTimer);
1056
+ if (lifeTimer) clearTimeout(lifeTimer);
1057
+ };
1058
+ const closeWith = (why) => {
1059
+ if (closed) return;
1060
+ console.log(`${T} CLOSING (${why})`);
1061
+ clearTimers();
1062
+ try {
1063
+ ws.close();
1064
+ } catch {
1065
+ }
1066
+ };
1067
+ const bumpIdle = () => {
1068
+ if (idleTimer) clearTimeout(idleTimer);
1069
+ idleTimer = setTimeout(() => closeWith(`idle ${idleMs / 1e3}s \u2014 no events, treating call as abandoned`), idleMs);
1070
+ };
1071
+ lifeTimer = setTimeout(() => closeWith(`max lifetime ${lifeMs / 6e4}min reached`), lifeMs);
1072
+ bumpIdle();
1073
+ let settled = false;
1074
+ let resolveOpen;
1075
+ let rejectOpen;
1076
+ const opened = new Promise((res, rej) => {
1077
+ resolveOpen = res;
1078
+ rejectOpen = rej;
1079
+ });
1080
+ const connectTimer = setTimeout(() => {
1081
+ if (!settled) {
1082
+ settled = true;
1083
+ rejectOpen(new Error("sideband connect timeout (10s)"));
1084
+ try {
1085
+ ws.close();
1086
+ } catch {
1087
+ }
1088
+ }
1089
+ }, 1e4);
1090
+ ws.on("open", () => {
1091
+ console.log(`${T} WS OPEN \u2713 \u2014 OpenAI Realtime connected; sending session.update (instructions + ${toolNames.length} tools)`);
1092
+ send(sessionUpdate);
1093
+ if (!settled) {
1094
+ settled = true;
1095
+ clearTimeout(connectTimer);
1096
+ resolveOpen();
1097
+ }
1098
+ });
1099
+ const wantTools = new Set(toolNames);
1111
1100
  let reasserts = 0;
1112
1101
  ws.on("message", async (data) => {
1113
1102
  let ev;
@@ -1116,29 +1105,73 @@ async function openSideband(opts) {
1116
1105
  } catch {
1117
1106
  return;
1118
1107
  }
1108
+ eventCount++;
1109
+ bumpIdle();
1110
+ if (ev?.type && !NOISY_EVENTS.has(ev.type)) {
1111
+ if (ev.type === "error") {
1112
+ console.error(`${T} \u26A0\uFE0F OpenAI error event: ${JSON.stringify(ev.error || ev).slice(0, 400)}`);
1113
+ } else if (ev.type === "response.output_item.added" && ev.item?.type === "function_call") {
1114
+ console.log(`${T} \u2190 model requested tool "${ev.item?.name}" (call_id=${ev.item?.call_id})`);
1115
+ try {
1116
+ opts.onTool?.({ callId: ev.item.call_id, name: ev.item.name || "", args: "", output: "", phase: "start" });
1117
+ } catch {
1118
+ }
1119
+ } else if (ev.type === "response.function_call_arguments.done") {
1120
+ toolCalls++;
1121
+ console.log(`${T} \u2190 function_call_arguments.done call_id=${ev.call_id} name=${ev.name || nameByCallId.get(ev.call_id) || "?"} args=${String(ev.arguments || "").slice(0, 200)}`);
1122
+ } else if (ev.type === "session.updated") {
1123
+ const have = (ev.session?.tools || []).map((t) => t?.name);
1124
+ console.log(`${T} session.updated \u2014 server now has tools: [${have.join(", ") || "NONE"}]`);
1125
+ } else {
1126
+ console.log(`${T} \u2190 ${ev.type}`);
1127
+ }
1128
+ }
1119
1129
  if (ev?.type === "session.updated" && wantTools.size) {
1120
1130
  const have = new Set((ev.session?.tools || []).map((t) => t?.name));
1121
1131
  const missing = [...wantTools].some((n) => !have.has(n));
1122
1132
  if (missing && reasserts < 10) {
1123
1133
  reasserts++;
1134
+ console.log(`${T} \u27F3 tools were clobbered (peer session.update) \u2014 re-asserting #${reasserts}`);
1124
1135
  send(sessionUpdate);
1125
1136
  return;
1126
1137
  }
1127
1138
  }
1128
1139
  try {
1129
- await handleRealtimeEvent(ev, tools, send, nameByCallId);
1140
+ const r = await handleRealtimeEvent(ev, tools, send, nameByCallId);
1141
+ if (r) {
1142
+ console.log(`${T} \u2192 tool "${r.name}" executed \u2192 ${String(r.output).slice(0, 160).replace(/\n/g, " ")}`);
1143
+ try {
1144
+ opts.onTool?.({ callId: ev.call_id, name: r.name, args: String(ev.arguments || ""), output: r.output, phase: "done" });
1145
+ } catch {
1146
+ }
1147
+ }
1130
1148
  } catch (e) {
1149
+ console.error(`${T} dispatch error: ${e?.message || e}`);
1131
1150
  opts.onError?.(e instanceof Error ? e : new Error(String(e)));
1132
1151
  }
1133
1152
  });
1134
- ws.on("close", () => opts.onClose?.());
1135
- ws.on("error", (e) => opts.onError?.(e instanceof Error ? e : new Error(String(e))));
1136
- return { callId: opts.callId, close: () => {
1137
- try {
1138
- ws.close();
1139
- } catch {
1140
- }
1141
- } };
1153
+ ws.on("close", (code, reason) => {
1154
+ closed = true;
1155
+ clearTimers();
1156
+ clearTimeout(connectTimer);
1157
+ console.log(`${T} WS CLOSED (code=${code ?? "?"} reason=${reason ? String(reason).slice(0, 120) : ""}) \u2014 ${eventCount} events, ${toolCalls} tool calls`);
1158
+ if (!settled) {
1159
+ settled = true;
1160
+ rejectOpen(new Error(`sideband closed before open (code ${code ?? "?"})`));
1161
+ }
1162
+ opts.onClose?.();
1163
+ });
1164
+ ws.on("error", (e) => {
1165
+ console.error(`${T} WS ERROR: ${e?.message || e}`);
1166
+ clearTimeout(connectTimer);
1167
+ if (!settled) {
1168
+ settled = true;
1169
+ rejectOpen(e instanceof Error ? e : new Error(String(e)));
1170
+ }
1171
+ opts.onError?.(e instanceof Error ? e : new Error(String(e)));
1172
+ });
1173
+ await opened;
1174
+ return { callId: opts.callId, close: () => closeWith("explicit close (release / new attach / shutdown)") };
1142
1175
  }
1143
1176
 
1144
1177
  const execFileAsync$1 = promisify(execFile);
@@ -1265,6 +1298,31 @@ function filterTerminalResponses(data) {
1265
1298
  function getMachineMetadataPath(svampHomeDir) {
1266
1299
  return join(svampHomeDir, "machine-metadata.json");
1267
1300
  }
1301
+ async function mintRealtimeEphemeralKey(baseUrl, apiKey, opts) {
1302
+ const realtimeBase = baseUrl || "https://api.openai.com";
1303
+ const ctrl = new AbortController();
1304
+ const timer = setTimeout(() => ctrl.abort(), 15e3);
1305
+ try {
1306
+ const response = await fetch(`${realtimeBase}/v1/realtime/client_secrets`, {
1307
+ method: "POST",
1308
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
1309
+ body: JSON.stringify({
1310
+ session: {
1311
+ type: "realtime",
1312
+ model: opts.model || "gpt-realtime-mini",
1313
+ ...opts.voice ? { audio: { output: { voice: opts.voice } } } : {}
1314
+ }
1315
+ }),
1316
+ signal: ctrl.signal
1317
+ });
1318
+ if (!response.ok) throw new Error(`OpenAI client_secrets error: ${response.status}`);
1319
+ const result = await response.json();
1320
+ if (!result.value) throw new Error("client_secrets returned no value");
1321
+ return result.value;
1322
+ } finally {
1323
+ clearTimeout(timer);
1324
+ }
1325
+ }
1268
1326
  function loadPersistedMachineMetadata(svampHomeDir) {
1269
1327
  try {
1270
1328
  const data = readFileSync$1(getMachineMetadataPath(svampHomeDir), "utf-8");
@@ -1295,6 +1353,50 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1295
1353
  };
1296
1354
  const listeners = [];
1297
1355
  const voiceSidebands = /* @__PURE__ */ new Map();
1356
+ const prepareVoiceSession = async (params, context) => {
1357
+ const senderName = context?.user?.email || context?.user?.id || "user";
1358
+ if (params.sessionId) {
1359
+ const rpc = handlers.getSessionRPCHandlers?.(params.sessionId);
1360
+ if (!rpc) return { ok: false, error: "Session not found on this machine" };
1361
+ let cwd;
1362
+ let owner;
1363
+ let role2;
1364
+ try {
1365
+ role2 = (await rpc.getEffectiveRole(context))?.role ?? null;
1366
+ if (!role2) return { ok: false, error: "Not authorized for this session" };
1367
+ const metaRes = await rpc.getMetadata(context);
1368
+ cwd = metaRes?.metadata?.path;
1369
+ owner = metaRes?.metadata?.sharing?.owner;
1370
+ } catch {
1371
+ return { ok: false, error: "Not authorized for this session" };
1372
+ }
1373
+ try {
1374
+ const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
1375
+ const prepared = await initVoiceSession({ deps, config: { tools: toolsForRole(role2) }, sender: { name: senderName, kind: "user", verified: true }, voice: params.voice });
1376
+ return { ok: true, prepared, role: role2, scope: "session" };
1377
+ } catch (e) {
1378
+ return { ok: false, error: e?.message || "Failed to prepare voice session" };
1379
+ }
1380
+ }
1381
+ const role = getEffectiveRole(context, currentMetadata.sharing);
1382
+ if (!role) return { ok: false, error: "Not authorized on this machine" };
1383
+ try {
1384
+ const machineDeps = buildMachineDeps(
1385
+ {
1386
+ getSessionIds: handlers.getSessionIds,
1387
+ getSessionRPCHandlers: handlers.getSessionRPCHandlers,
1388
+ getTrackedSessions: handlers.getTrackedSessions,
1389
+ spawnSession: handlers.spawnSession,
1390
+ getDaemonStatus: () => ({ status: currentDaemonState.status, machineId, pid: currentDaemonState.pid })
1391
+ },
1392
+ { cwd: process.env.HOME, ownerEmail: currentMetadata.sharing?.owner }
1393
+ );
1394
+ const prepared = await initMachineVoiceSession({ deps: machineDeps, role, voice: params.voice });
1395
+ return { ok: true, prepared, role, scope: "machine" };
1396
+ } catch (e) {
1397
+ return { ok: false, error: e?.message || "Failed to prepare voice session" };
1398
+ }
1399
+ };
1298
1400
  const removeListener = (listener, reason) => {
1299
1401
  const idx = listeners.indexOf(listener);
1300
1402
  if (idx >= 0) {
@@ -2230,7 +2332,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2230
2332
  const tunnels = handlers.tunnels;
2231
2333
  if (!tunnels) throw new Error("Tunnel management not available");
2232
2334
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2233
- const { FrpcTunnel } = await import('./frpc-BBNWJChC.mjs');
2335
+ const { FrpcTunnel } = await import('./frpc-D_1pEpUY.mjs');
2234
2336
  const tunnel = new FrpcTunnel({
2235
2337
  name: params.name,
2236
2338
  ports: params.ports,
@@ -2380,35 +2482,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2380
2482
  if (misconfig || !resolved.apiKey) {
2381
2483
  return { success: false, error: misconfig || "WISE voice is not configured: no OpenAI API key on this machine. Run `svamp wise-agent auth use-openai <KEY>` (or set OPENAI_API_KEY), then `svamp daemon restart`." };
2382
2484
  }
2383
- const realtimeBase = resolved.baseUrl || "https://api.openai.com";
2384
2485
  try {
2385
- const wisCtrl = new AbortController();
2386
- const wisTimer = setTimeout(() => wisCtrl.abort(), 15e3);
2387
- let response;
2388
- try {
2389
- response = await fetch(`${realtimeBase}/v1/realtime/client_secrets`, {
2390
- method: "POST",
2391
- headers: {
2392
- "Authorization": `Bearer ${resolved.apiKey}`,
2393
- "Content-Type": "application/json"
2394
- },
2395
- body: JSON.stringify({
2396
- session: {
2397
- type: "realtime",
2398
- model: params.model || "gpt-realtime-mini",
2399
- ...params.voice ? { audio: { output: { voice: params.voice } } } : {}
2400
- }
2401
- }),
2402
- signal: wisCtrl.signal
2403
- });
2404
- } finally {
2405
- clearTimeout(wisTimer);
2406
- }
2407
- if (!response.ok) {
2408
- return { success: false, error: `OpenAI API error: ${response.status}` };
2409
- }
2410
- const result = await response.json();
2411
- return { success: true, clientSecret: result.value };
2486
+ const clientSecret = await mintRealtimeEphemeralKey(resolved.baseUrl, resolved.apiKey, { model: params.model, voice: params.voice });
2487
+ return { success: true, clientSecret };
2412
2488
  } catch (error) {
2413
2489
  return { success: false, error: error instanceof Error ? error.message : "Failed to create token" };
2414
2490
  }
@@ -2429,54 +2505,22 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2429
2505
  return { success: false, error: misconfig || "WISE voice is not configured on this machine." };
2430
2506
  }
2431
2507
  const userKey = (context?.user?.email || context?.user?.id || "anon").toLowerCase();
2432
- const senderName = context?.user?.email || context?.user?.id || "user";
2433
- let prepared;
2434
- let role;
2435
- if (params.sessionId) {
2436
- const rpc = handlers.getSessionRPCHandlers?.(params.sessionId);
2437
- if (!rpc) return { success: false, error: "Session not found on this machine" };
2438
- let cwd;
2439
- let owner;
2440
- try {
2441
- role = (await rpc.getEffectiveRole(context))?.role ?? null;
2442
- if (!role) return { success: false, error: "Not authorized for this session" };
2443
- const metaRes = await rpc.getMetadata(context);
2444
- cwd = metaRes?.metadata?.path;
2445
- owner = metaRes?.metadata?.sharing?.owner;
2446
- } catch {
2447
- return { success: false, error: "Not authorized for this session" };
2448
- }
2449
- try {
2450
- const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2451
- prepared = await initVoiceSession({ deps, config: { tools: toolsForRole(role) }, sender: { name: senderName, kind: "user", verified: true }, voice: params.voice });
2452
- } catch (e) {
2453
- return { success: false, error: e?.message || "Failed to prepare voice session" };
2454
- }
2455
- } else {
2456
- role = getEffectiveRole(context, currentMetadata.sharing);
2457
- if (!role) return { success: false, error: "Not authorized on this machine" };
2458
- try {
2459
- const machineDeps = buildMachineDeps(
2460
- {
2461
- getSessionIds: handlers.getSessionIds,
2462
- getSessionRPCHandlers: handlers.getSessionRPCHandlers,
2463
- getTrackedSessions: handlers.getTrackedSessions,
2464
- spawnSession: handlers.spawnSession,
2465
- getDaemonStatus: () => ({ status: currentDaemonState.status, machineId, pid: currentDaemonState.pid })
2466
- },
2467
- { cwd: process.env.HOME, ownerEmail: currentMetadata.sharing?.owner }
2468
- );
2469
- prepared = await initMachineVoiceSession({ deps: machineDeps, role, voice: params.voice });
2470
- } catch (e) {
2471
- return { success: false, error: e?.message || "Failed to prepare voice session" };
2472
- }
2508
+ const prep = await prepareVoiceSession(params, context);
2509
+ if (!prep.ok) return { success: false, error: prep.error };
2510
+ const { prepared, role } = prep;
2511
+ const scope = params.sessionId ? `session ${params.sessionId}` : "machine-global";
2512
+ console.log(`[wise-voice] ATTACH request user=${userKey} scope=${scope} role=${role} callId=${params.callId.slice(0, 16)}\u2026 tools=[${(prepared.tools || []).map((t) => t.name).join(", ")}]`);
2513
+ const prior = voiceSidebands.get(userKey);
2514
+ if (prior) {
2515
+ console.log(`[wise-voice] superseding prior sideband for user=${userKey} (callId=${prior.callId.slice(0, 16)}\u2026) \u2014 closing to avoid a double bill`);
2516
+ prior.close();
2517
+ voiceSidebands.delete(userKey);
2473
2518
  }
2474
- voiceSidebands.get(userKey)?.close();
2475
- voiceSidebands.delete(userKey);
2476
2519
  try {
2520
+ const ephemeralKey = await mintRealtimeEphemeralKey(resolved.baseUrl, resolved.apiKey, { model: params.model, voice: params.voice });
2477
2521
  const handle = await openSideband({
2478
2522
  callId: params.callId,
2479
- apiKey: resolved.apiKey,
2523
+ apiKey: ephemeralKey,
2480
2524
  baseUrl: resolved.baseUrl,
2481
2525
  prepared,
2482
2526
  onClose: () => {
@@ -2484,16 +2528,117 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2484
2528
  }
2485
2529
  });
2486
2530
  voiceSidebands.set(userKey, handle);
2531
+ console.log(`[wise-voice] ATTACH ok user=${userKey} scope=${scope} \u2014 sideband live (active sidebands: ${voiceSidebands.size})`);
2487
2532
  return { success: true, role, scope: params.sessionId ? "session" : "machine" };
2488
2533
  } catch (e) {
2534
+ console.error(`[wise-voice] ATTACH FAILED user=${userKey}: ${e?.message || e}`);
2489
2535
  return { success: false, error: e?.message || "Failed to attach voice sideband" };
2490
2536
  }
2491
2537
  },
2492
- // Explicit teardown of the caller's active voice sideband (e.g. on hang-up).
2538
+ // UNIFIED INTERFACE (fixes the call_id 404): the DAEMON creates the WebRTC
2539
+ // call so it OWNS it and can attach the sideband WS. The browser sends its SDP
2540
+ // offer here (via the connect-time fetch reroute); we POST it to
2541
+ // /v1/realtime/calls with the machine key + WISE session config (tools +
2542
+ // instructions baked in), open the sideband to execute tools, and return the
2543
+ // SDP answer + call_id for the browser to finish the WebRTC handshake.
2544
+ wiseCreateCall: async (params, context) => {
2545
+ trackInbound();
2546
+ if (context && (!context.user || context.user.is_anonymous)) {
2547
+ return { success: false, error: "Sign in to use WISE voice." };
2548
+ }
2549
+ if (!params?.sdp) return { success: false, error: "sdp offer is required" };
2550
+ const resolved = resolveModel({ provider: "openai" }, process.env);
2551
+ const misconfig = describeMisconfiguration(resolved);
2552
+ if (misconfig || !resolved.apiKey) {
2553
+ return { success: false, error: misconfig || "WISE voice is not configured on this machine." };
2554
+ }
2555
+ const userKey = (context?.user?.email || context?.user?.id || "anon").toLowerCase();
2556
+ const prep = await prepareVoiceSession(params, context);
2557
+ if (!prep.ok) return { success: false, error: prep.error };
2558
+ const { prepared, role } = prep;
2559
+ const scope = params.sessionId ? `session ${params.sessionId}` : "machine-global";
2560
+ const model = params.model || "gpt-realtime-mini";
2561
+ console.log(`[wise-voice] CREATE-CALL request user=${userKey} scope=${scope} role=${role} model=${model} tools=[${(prepared.tools || []).map((t) => t.name).join(", ")}]`);
2562
+ const base = resolved.baseUrl || "https://api.openai.com";
2563
+ let answerSdp;
2564
+ let callId;
2565
+ try {
2566
+ const sessionConfig = { ...prepared.sessionUpdate.session, model };
2567
+ const fd = new FormData();
2568
+ fd.set("sdp", params.sdp);
2569
+ fd.set("session", JSON.stringify(sessionConfig));
2570
+ const ctrl = new AbortController();
2571
+ const timer = setTimeout(() => ctrl.abort(), 2e4);
2572
+ let resp;
2573
+ try {
2574
+ resp = await fetch(`${base}/v1/realtime/calls`, {
2575
+ method: "POST",
2576
+ headers: { "Authorization": `Bearer ${resolved.apiKey}` },
2577
+ body: fd,
2578
+ signal: ctrl.signal
2579
+ });
2580
+ } finally {
2581
+ clearTimeout(timer);
2582
+ }
2583
+ if (!resp.ok) {
2584
+ const body = await resp.text().catch(() => "");
2585
+ console.error(`[wise-voice] CREATE-CALL OpenAI error ${resp.status}: ${body.slice(0, 300)}`);
2586
+ return { success: false, error: `OpenAI call create failed (HTTP ${resp.status})${body ? `: ${body.slice(0, 160)}` : ""}` };
2587
+ }
2588
+ answerSdp = await resp.text();
2589
+ const loc = resp.headers.get("Location") || "";
2590
+ callId = loc.split("/").filter(Boolean).pop() || "";
2591
+ console.log(`[wise-voice] CREATE-CALL ok callId=${callId.slice(0, 16)}\u2026 (SDP answer ${answerSdp.length}b, Location="${loc}")`);
2592
+ } catch (e) {
2593
+ console.error(`[wise-voice] CREATE-CALL failed: ${e?.message || e}`);
2594
+ return { success: false, error: e?.message || "Failed to create voice call" };
2595
+ }
2596
+ let sidebandOk = false;
2597
+ let sidebandError;
2598
+ if (callId) {
2599
+ voiceSidebands.get(userKey)?.close();
2600
+ voiceSidebands.delete(userKey);
2601
+ try {
2602
+ const handle = await openSideband({
2603
+ callId,
2604
+ apiKey: resolved.apiKey,
2605
+ baseUrl: resolved.baseUrl,
2606
+ prepared,
2607
+ onClose: () => {
2608
+ if (voiceSidebands.get(userKey)?.callId === callId) voiceSidebands.delete(userKey);
2609
+ },
2610
+ // Push each tool's name/args/output to the browser (fire-and-forget)
2611
+ // so it can render accurate tool rows the SDK can't provide.
2612
+ onTool: params.onToolEvent ? (ev) => {
2613
+ try {
2614
+ const p = params.onToolEvent(ev);
2615
+ if (p && typeof p.catch === "function") p.catch(() => {
2616
+ });
2617
+ } catch {
2618
+ }
2619
+ } : void 0
2620
+ });
2621
+ voiceSidebands.set(userKey, handle);
2622
+ sidebandOk = true;
2623
+ console.log(`[wise-voice] CREATE-CALL sideband live user=${userKey} (active: ${voiceSidebands.size})`);
2624
+ } catch (e) {
2625
+ sidebandError = e?.message || String(e);
2626
+ console.error(`[wise-voice] CREATE-CALL sideband FAILED: ${sidebandError}`);
2627
+ }
2628
+ } else {
2629
+ sidebandError = "no call_id in OpenAI response";
2630
+ }
2631
+ return { success: true, sdp: answerSdp, callId, sidebandOk, sidebandError, role, scope: params.sessionId ? "session" : "machine" };
2632
+ },
2633
+ // Explicit teardown of the caller's active voice sideband (e.g. on hang-up,
2634
+ // Stop, tab close/unload). The frontend calls this so we don't wait for the
2635
+ // idle backstop — release the billable OpenAI session immediately.
2493
2636
  wiseReleaseVoice: async (_params, context) => {
2494
2637
  trackInbound();
2495
2638
  const userKey = (context?.user?.email || context?.user?.id || "anon").toLowerCase();
2496
- voiceSidebands.get(userKey)?.close();
2639
+ const had = voiceSidebands.get(userKey);
2640
+ if (had) console.log(`[wise-voice] RELEASE user=${userKey} callId=${had.callId.slice(0, 16)}\u2026 (explicit hang-up)`);
2641
+ had?.close();
2497
2642
  voiceSidebands.delete(userKey);
2498
2643
  return { success: true };
2499
2644
  },
@@ -2587,6 +2732,19 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2587
2732
  if (!rpc?.channelDescribe) return { error: "channel not found" };
2588
2733
  return rpc.channelDescribe(kwargs.channel);
2589
2734
  },
2735
+ // Clean GET endpoint for the self-contained skill markdown. Param is
2736
+ // named `channel` (not `kwargs`) so the Hypha HTTP gateway maps a plain
2737
+ // `?channel=<id>` query → a tidy, shareable SKILL.md URL.
2738
+ skill: async (channel) => {
2739
+ const id = channel;
2740
+ if (!id) return "# error\nMissing ?channel=<id>";
2741
+ const rpc = await findChannelOwner(id);
2742
+ if (!rpc?.channelDescribe) return `# error
2743
+ channel "${id}" not found`;
2744
+ const d = await rpc.channelDescribe(id);
2745
+ return d?.skill?.body || `# error
2746
+ ${d?.error || "not found"}`;
2747
+ },
2590
2748
  send: async (kwargs = {}, context) => {
2591
2749
  trackInbound();
2592
2750
  const rpc = await findChannelOwner(kwargs.channel);
@@ -2955,7 +3113,7 @@ function validateChannel(c) {
2955
3113
  if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
2956
3114
  errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
2957
3115
  if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
2958
- const MUTATING = ["run_bash", "send_to_session", "run_js"];
3116
+ const MUTATING = ["run_bash", "send_to_session"];
2959
3117
  const ag = c.action.agent || {};
2960
3118
  const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
2961
3119
  if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
@@ -3041,25 +3199,53 @@ class ChannelStore {
3041
3199
  return caller;
3042
3200
  }
3043
3201
  }
3044
- function generateSkillBody(channel, urlBase) {
3045
- const url = `${"https://<svamp-tunnel>"}/channel/${channel.id}`;
3202
+ function gatewayBase(channelsServiceId, baseUrl) {
3203
+ const slash = channelsServiceId.indexOf("/");
3204
+ if (slash < 0) return `${baseUrl.replace(/\/$/, "")}/services/${channelsServiceId}`;
3205
+ const ws = channelsServiceId.slice(0, slash);
3206
+ const clientSvc = channelsServiceId.slice(slash + 1);
3207
+ return `${baseUrl.replace(/\/$/, "")}/${ws}/services/${clientSvc}`;
3208
+ }
3209
+ function generateSkillBody(channel, ctx) {
3210
+ const svc = ctx?.channelsServiceId || "<workspace>/<machine>:channels";
3211
+ const base = ctx?.baseUrl || "https://hypha.aicell.io";
3212
+ const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
3213
+ const skillUrl = `${gw}/skill?channel=${channel.id}`;
3214
+ const sendUrl = `${gw}/send`;
3215
+ const key = ctx?.key || "<your-key>";
3046
3216
  const isAgent = channel.action?.kind === "agent";
3047
- const replyNote = isAgent ? `
3048
- This is a WISE Agent channel: send() runs a fast assistant against the session's tools/skills and **returns its answer synchronously** in the result \`reply\`.` : `
3049
- Delivery is fire-and-forget; the message lands in the agent's inbox tagged with your identity (from/verified).`;
3217
+ const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
3218
+ const name = channel.skill?.name || channel.name;
3219
+ const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
3220
+ const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
3221
+ const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
3050
3222
  return `---
3051
- name: ${channel.skill?.name || channel.name}
3052
- description: ${channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`}
3223
+ name: ${name}
3224
+ description: ${desc}
3053
3225
  ---
3054
- # ${channel.name}
3226
+ # ${name}
3055
3227
  ${channel.description || ""}
3056
3228
 
3057
- Self-contained guide for messaging another agent \u2014 share this (or its URL,
3058
- ${url}/skill.md) with an agent so it knows how to reach me.
3059
- ${replyNote}
3229
+ Self-contained guide for messaging the **${channel.name}** channel. ${replyNote}
3230
+ This skill (with every value below already filled in) is served at:
3231
+ ${skillUrl}
3232
+
3233
+ ${rpcLine}
3234
+ \`\`\`js
3235
+ await get_service("${svc}").send({
3236
+ channel: "${channel.id}",
3237
+ message: "your message here",
3238
+ from: "your-name",
3239
+ });
3240
+ \`\`\`
3241
+
3242
+ **HTTP** \u2014 any client, no Hypha SDK needed (Hypha gateway wraps args under \`kwargs\`):
3243
+ \`\`\`
3244
+ POST ${sendUrl}
3245
+ Content-Type: application/json
3060
3246
 
3061
- Hypha RPC (preferred, verified identity, no key): get_service("<ws>/<machine>:channels").send({ channel: "${channel.id}", message: "..." })
3062
- HTTP: POST ${url} with header Authorization: Bearer <your-key>, body { "message": "...", "from": "..." }`;
3247
+ {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"}}
3248
+ \`\`\``;
3063
3249
  }
3064
3250
 
3065
3251
  function resolveSender(channel, input = {}) {
@@ -3337,6 +3523,19 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3337
3523
  callbacks.onMetadataUpdate?.(metadata);
3338
3524
  };
3339
3525
  const channelStore = new ChannelStore(initialMetadata.path);
3526
+ const cfg = server?.config || {};
3527
+ const channelsServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:channels` : void 0;
3528
+ const channelsBaseUrl = cfg.public_base_url || process.env.HYPHA_SERVER_URL || "https://hypha.aicell.io";
3529
+ const skillCtxFor = (c) => ({
3530
+ channelsServiceId,
3531
+ baseUrl: channelsBaseUrl,
3532
+ key: c.identity?.shared_key || c.identity?.callers?.[0]?.key
3533
+ });
3534
+ const skillUrlsFor = (c) => {
3535
+ if (!channelsServiceId) return { skillUrl: void 0, sendUrl: void 0 };
3536
+ const gw = gatewayBase(channelsServiceId, channelsBaseUrl);
3537
+ return { skillUrl: `${gw}/describe?channel=${c.id}`, sendUrl: `${gw}/send` };
3538
+ };
3340
3539
  const syncChannelsToMetadata = () => {
3341
3540
  metadata.channels = channelStore.list();
3342
3541
  metadataVersion++;
@@ -3550,7 +3749,17 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3550
3749
  authorizeRequest(context, metadata.sharing, "view");
3551
3750
  const c = channelStore.get(id);
3552
3751
  if (!c) return { error: "not found" };
3553
- return { skill: generateSkillBody(c) };
3752
+ const { skillUrl, sendUrl } = skillUrlsFor(c);
3753
+ return {
3754
+ skill: generateSkillBody(c, skillCtxFor(c)),
3755
+ channelsServiceId,
3756
+ channelId: c.id,
3757
+ name: c.name,
3758
+ skillUrl,
3759
+ sendUrl,
3760
+ key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
3761
+ hyphaKeyless: (c.identity?.hypha_allow || []).length > 0
3762
+ };
3554
3763
  },
3555
3764
  // Public channel discovery (no session authz — channels are deliberately
3556
3765
  // published endpoints; each send() is gated by the channel's OWN identity).
@@ -3560,7 +3769,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3560
3769
  channelDescribe: async (id) => {
3561
3770
  const c = channelStore.get(id);
3562
3771
  if (!c || c.enabled === false) return { error: "not found" };
3563
- return { ...channelPublicView(c), skill: { body: generateSkillBody(c) } };
3772
+ return { ...channelPublicView(c), skill: { body: generateSkillBody(c, skillCtxFor(c)) } };
3564
3773
  },
3565
3774
  channelSend: async (params, context) => {
3566
3775
  const c = channelStore.get(params.channel);
@@ -10207,7 +10416,7 @@ async function startDaemon(options) {
10207
10416
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10208
10417
  saveExposedTunnels(list);
10209
10418
  }
10210
- const { ServeManager } = await import('./serveManager-Dn9GyzAG.mjs');
10419
+ const { ServeManager } = await import('./serveManager-DNlqB0r6.mjs');
10211
10420
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10212
10421
  ensureAutoInstalledSkills(logger).catch(() => {
10213
10422
  });
@@ -12835,7 +13044,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12835
13044
  const specs = loadExposedTunnels();
12836
13045
  if (specs.length === 0) return;
12837
13046
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12838
- const { FrpcTunnel } = await import('./frpc-BBNWJChC.mjs');
13047
+ const { FrpcTunnel } = await import('./frpc-D_1pEpUY.mjs');
12839
13048
  for (const spec of specs) {
12840
13049
  if (tunnels.has(spec.name)) continue;
12841
13050
  try {
@@ -13607,4 +13816,4 @@ var run = /*#__PURE__*/Object.freeze({
13607
13816
  writeStopMarker: writeStopMarker
13608
13817
  });
13609
13818
 
13610
- export { normalizeAllowedUser as A, loadSecurityContextConfig as B, resolveSecurityContext as C, buildSecurityContextFromFlags as D, mergeSecurityContexts as E, buildSessionShareUrl as F, computeOutboundHop as G, buildMachineShareUrl as H, generateHookSettings as I, DefaultTransport$1 as J, acpBackend as K, acpAgentConfig as L, codexMcpBackend as M, GeminiTransport$1 as N, claudeAuth as O, instanceConfig as P, api as Q, RoutineStore as R, ServeAuth as S, run as T, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, getHyphaServerUrl as k, hasCookieToken as l, RoutineRunner as m, getSkillsServer as n, getSkillsWorkspaceName as o, parseFrontmatter as p, getSkillsCollectionName as q, registerMachineService as r, startDaemon as s, fetchWithTimeout as t, searchSkills as u, SKILLS_DIR as v, getSkillInfo as w, downloadSkillFile as x, listSkillFiles as y, resolveModel as z };
13819
+ export { normalizeAllowedUser as A, loadSecurityContextConfig as B, resolveSecurityContext as C, buildSecurityContextFromFlags as D, mergeSecurityContexts as E, buildSessionShareUrl as F, computeOutboundHop as G, buildMachineShareUrl as H, handleRealtimeEvent as I, describeMisconfiguration as J, buildMachineDeps as K, initMachineVoiceSession as L, generateHookSettings as M, DefaultTransport$1 as N, acpBackend as O, acpAgentConfig as P, codexMcpBackend as Q, RoutineStore as R, ServeAuth as S, GeminiTransport$1 as T, claudeAuth as U, instanceConfig as V, api as W, run as X, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, getHyphaServerUrl as k, hasCookieToken as l, RoutineRunner as m, getSkillsServer as n, getSkillsWorkspaceName as o, parseFrontmatter as p, getSkillsCollectionName as q, registerMachineService as r, startDaemon as s, fetchWithTimeout as t, searchSkills as u, SKILLS_DIR as v, getSkillInfo as w, downloadSkillFile as x, listSkillFiles as y, resolveModel as z };