@getpaseo/server 0.1.91-beta.1 → 0.1.91

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 (50) hide show
  1. package/dist/scripts/supervisor.js +21 -0
  2. package/dist/server/server/agent/agent-manager.d.ts +1 -1
  3. package/dist/server/server/agent/agent-manager.js +23 -48
  4. package/dist/server/server/agent/agent-sdk-types.d.ts +15 -0
  5. package/dist/server/server/agent/prompt-attachments.js +8 -0
  6. package/dist/server/server/agent/provider-registry.d.ts +0 -1
  7. package/dist/server/server/agent/provider-registry.js +22 -4
  8. package/dist/server/server/agent/provider-snapshot-manager.js +19 -1
  9. package/dist/server/server/agent/providers/claude/agent.d.ts +5 -2
  10. package/dist/server/server/agent/providers/claude/agent.js +6 -2
  11. package/dist/server/server/agent/providers/claude/models.d.ts +1 -1
  12. package/dist/server/server/agent/providers/claude/models.js +6 -6
  13. package/dist/server/server/agent/providers/codex-app-server-agent.js +9 -5
  14. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.d.ts +1 -0
  15. package/dist/server/server/agent/providers/opencode/test-utils/test-opencode-runtime.js +4 -0
  16. package/dist/server/server/agent/providers/opencode-agent.d.ts +16 -2
  17. package/dist/server/server/agent/providers/opencode-agent.js +75 -4
  18. package/dist/server/server/agent/providers/pi/agent.d.ts +23 -1
  19. package/dist/server/server/agent/providers/pi/agent.js +219 -13
  20. package/dist/server/server/agent/providers/pi/cli-runtime.js +9 -0
  21. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +9 -0
  22. package/dist/server/server/agent/providers/pi/runtime.d.ts +2 -0
  23. package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +12 -0
  24. package/dist/server/server/agent/providers/pi/session-descriptor.js +304 -0
  25. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.d.ts +8 -0
  26. package/dist/server/server/agent/providers/pi/test-utils/fake-pi.js +22 -0
  27. package/dist/server/server/agent/runtime-mcp-config.d.ts +8 -0
  28. package/dist/server/server/agent/runtime-mcp-config.js +50 -0
  29. package/dist/server/server/auth.js +16 -1
  30. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +2 -2
  31. package/dist/server/server/daemon-worker.js +84 -1
  32. package/dist/server/server/file-upload/index.d.ts +27 -0
  33. package/dist/server/server/file-upload/index.js +158 -0
  34. package/dist/server/server/loop-service.d.ts +12 -12
  35. package/dist/server/server/persisted-config.d.ts +11 -0
  36. package/dist/server/server/persisted-config.js +2 -1
  37. package/dist/server/server/persistence-hooks.js +6 -4
  38. package/dist/server/server/session.d.ts +5 -2
  39. package/dist/server/server/session.js +20 -2
  40. package/dist/server/server/speech/providers/local/runtime.js +1 -0
  41. package/dist/server/server/speech/providers/local/worker-client.d.ts +14 -1
  42. package/dist/server/server/speech/providers/local/worker-client.js +169 -7
  43. package/dist/server/server/websocket-server.d.ts +2 -0
  44. package/dist/server/server/websocket-server.js +20 -7
  45. package/dist/server/server/workspace-registry.d.ts +4 -4
  46. package/dist/server/utils/directory-suggestions.js +10 -5
  47. package/dist/server/utils/worktree.d.ts +4 -0
  48. package/dist/server/utils/worktree.js +19 -2
  49. package/dist/src/server/persisted-config.js +2 -1
  50. package/package.json +5 -5
@@ -8,6 +8,8 @@ import { bufferToWorkerBytes, workerBytesToBuffer } from "./worker-bytes.js";
8
8
  const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
9
9
  const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
10
10
  const DEFAULT_LOCAL_SAMPLE_RATE = 16000;
11
+ const STDERR_TAIL_MAX_CHARS = 8000;
12
+ const USER_ERROR_STDERR_MAX_CHARS = 1000;
11
13
  function resolveWorkerUrl() {
12
14
  const currentUrl = import.meta.url;
13
15
  if (currentUrl.endsWith(".ts")) {
@@ -38,21 +40,81 @@ function forkLocalSpeechWorker() {
38
40
  env,
39
41
  execArgv: resolveWorkerExecArgv(),
40
42
  serialization: "advanced",
41
- stdio: ["ignore", "ignore", "inherit", "ipc"],
43
+ stdio: ["ignore", "ignore", "pipe", "ipc"],
42
44
  });
43
45
  }
44
46
  function isResponse(message) {
45
47
  return message.type === "response";
46
48
  }
49
+ function truncateStart(value, maxChars) {
50
+ if (value.length <= maxChars) {
51
+ return value;
52
+ }
53
+ return value.slice(value.length - maxChars);
54
+ }
55
+ function summarizeWorkerRequest(message) {
56
+ switch (message.type) {
57
+ case "session.create":
58
+ return { kind: message.kind, sessionId: message.sessionId };
59
+ case "session.append":
60
+ return { sessionId: message.sessionId, audioBytes: message.audio.byteLength };
61
+ case "session.commit":
62
+ case "session.clear":
63
+ case "session.flush":
64
+ case "session.reset":
65
+ case "session.close":
66
+ return { sessionId: message.sessionId };
67
+ case "stt.transcribe":
68
+ return {
69
+ model: message.model,
70
+ audioBytes: message.audio.byteLength,
71
+ format: message.format,
72
+ };
73
+ case "tts.synthesize":
74
+ return { textLength: message.text.length };
75
+ }
76
+ }
77
+ function formatExitStatus(code, signal) {
78
+ const parts = [];
79
+ if (code !== null) {
80
+ parts.push(`code ${code}`);
81
+ }
82
+ if (signal) {
83
+ parts.push(`signal ${signal}`);
84
+ }
85
+ return parts.length > 0 ? parts.join(", ") : "unknown exit status";
86
+ }
87
+ function formatPendingRequestForMessage(request) {
88
+ const type = typeof request.type === "string" ? request.type : "unknown request";
89
+ const kind = typeof request.kind === "string" ? ` (${request.kind})` : "";
90
+ return `${type}${kind}`;
91
+ }
92
+ function buildWorkerExitMessage(params) {
93
+ const pending = params.pendingRequests.length > 0
94
+ ? ` while handling ${params.pendingRequests
95
+ .slice(0, 3)
96
+ .map(formatPendingRequestForMessage)
97
+ .join(", ")}`
98
+ : "";
99
+ const stderr = params.stderrTail
100
+ ? ` Last stderr: ${truncateStart(params.stderrTail, USER_ERROR_STDERR_MAX_CHARS)}`
101
+ : " Check daemon.log and macOS DiagnosticReports for Paseo Voice crash details.";
102
+ return `Local speech worker exited (${formatExitStatus(params.code, params.signal)})${pending}.${stderr}`;
103
+ }
47
104
  export class LocalSpeechWorkerClient {
48
105
  constructor(options) {
49
106
  this.pendingRequests = new Map();
50
107
  this.activeSessionIds = new Set();
51
108
  this.sessionEmitters = new Map();
52
109
  this.worker = null;
110
+ this.workerPid = null;
111
+ this.stderrTail = "";
112
+ this.stderrLineBuffer = "";
53
113
  this.inFlightRequests = 0;
54
114
  this.idleTimer = null;
115
+ this.intentionalWorkerCloses = new WeakSet();
55
116
  this.config = options.config;
117
+ this.logger = options.logger.child({ component: "local-speech-worker-client" });
56
118
  this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
57
119
  this.idleTtlMs = options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
58
120
  this.forkWorker = options.forkWorker ?? forkLocalSpeechWorker;
@@ -141,7 +203,9 @@ export class LocalSpeechWorkerClient {
141
203
  this.sessionEmitters.clear();
142
204
  const worker = this.worker;
143
205
  this.worker = null;
206
+ this.workerPid = null;
144
207
  if (worker && !worker.killed) {
208
+ this.intentionalWorkerCloses.add(worker);
145
209
  try {
146
210
  worker.disconnect();
147
211
  }
@@ -160,6 +224,7 @@ export class LocalSpeechWorkerClient {
160
224
  const worker = this.ensureWorker();
161
225
  const requestId = randomUUID();
162
226
  const message = { ...input, requestId };
227
+ const requestSummary = summarizeWorkerRequest(message);
163
228
  this.inFlightRequests++;
164
229
  this.clearIdleTimer();
165
230
  return new Promise((resolve, reject) => {
@@ -173,6 +238,9 @@ export class LocalSpeechWorkerClient {
173
238
  resolve: (value) => resolve(value),
174
239
  reject,
175
240
  timeout,
241
+ type: message.type,
242
+ summary: requestSummary,
243
+ startedAt: Date.now(),
176
244
  });
177
245
  worker.send(message, (error) => {
178
246
  if (!error) {
@@ -196,10 +264,35 @@ export class LocalSpeechWorkerClient {
196
264
  }
197
265
  const worker = this.forkWorker();
198
266
  this.worker = worker;
267
+ this.workerPid = worker.pid ?? null;
268
+ this.stderrTail = "";
269
+ this.stderrLineBuffer = "";
270
+ this.logger.info({
271
+ workerPid: this.workerPid,
272
+ modelsDir: this.config.modelsDir,
273
+ voiceSttModel: this.config.voiceSttModel,
274
+ dictationSttModel: this.config.dictationSttModel,
275
+ voiceTtsModel: this.config.voiceTtsModel,
276
+ }, "Local speech worker spawned");
277
+ worker.stderr?.on("data", (chunk) => this.handleWorkerStderr(chunk));
199
278
  worker.on("message", (message) => this.handleWorkerMessage(message));
200
- worker.on("exit", () => this.handleWorkerExit());
279
+ worker.on("close", (code, signal) => this.handleWorkerExit(worker, code, signal));
201
280
  return worker;
202
281
  }
282
+ handleWorkerStderr(chunk) {
283
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
284
+ this.stderrTail = truncateStart(this.stderrTail + text, STDERR_TAIL_MAX_CHARS);
285
+ this.stderrLineBuffer += text;
286
+ const lines = this.stderrLineBuffer.split(/\r?\n/);
287
+ this.stderrLineBuffer = lines.pop() ?? "";
288
+ for (const line of lines) {
289
+ const trimmed = line.trim();
290
+ if (!trimmed) {
291
+ continue;
292
+ }
293
+ this.logger.warn({ workerPid: this.workerPid, stderr: trimmed }, "Local speech worker stderr");
294
+ }
295
+ }
203
296
  handleWorkerMessage(message) {
204
297
  if (isResponse(message)) {
205
298
  const pending = this.pendingRequests.get(message.requestId);
@@ -240,19 +333,81 @@ export class LocalSpeechWorkerClient {
240
333
  return;
241
334
  }
242
335
  }
243
- handleWorkerExit() {
244
- this.worker = null;
336
+ handleWorkerExit(worker, code, signal) {
337
+ const wasCurrentWorker = this.worker === worker;
338
+ const wasIntentionalClose = this.intentionalWorkerCloses.has(worker);
339
+ this.intentionalWorkerCloses.delete(worker);
340
+ const workerPid = worker.pid ?? (wasCurrentWorker ? this.workerPid : null);
341
+ const pendingRequests = this.describePendingRequests();
342
+ const activeSessionCount = this.activeSessionIds.size;
343
+ const stderrTail = this.getStderrTail();
344
+ if (wasIntentionalClose) {
345
+ this.logger.info({
346
+ workerPid,
347
+ code,
348
+ signal,
349
+ pendingRequests,
350
+ activeSessionCount,
351
+ stderrTail: stderrTail || null,
352
+ }, "Local speech worker closed after shutdown");
353
+ if (wasCurrentWorker) {
354
+ this.worker = null;
355
+ this.workerPid = null;
356
+ }
357
+ return;
358
+ }
359
+ if (!wasCurrentWorker) {
360
+ this.logger.warn({
361
+ workerPid,
362
+ code,
363
+ signal,
364
+ pendingRequests,
365
+ activeSessionCount,
366
+ stderrTail: stderrTail || null,
367
+ }, "Stale local speech worker closed");
368
+ return;
369
+ }
370
+ const error = new Error(buildWorkerExitMessage({
371
+ code,
372
+ signal,
373
+ pendingRequests,
374
+ stderrTail,
375
+ }));
376
+ this.logger.error({
377
+ err: error,
378
+ workerPid,
379
+ code,
380
+ signal,
381
+ pendingRequests,
382
+ activeSessionCount,
383
+ stderrTail: stderrTail || null,
384
+ }, "Local speech worker exited");
385
+ if (wasCurrentWorker) {
386
+ this.worker = null;
387
+ this.workerPid = null;
388
+ }
245
389
  this.clearIdleTimer();
246
- this.rejectAllPending(new Error("Local speech worker exited"));
390
+ this.rejectAllPending(error);
247
391
  for (const [sessionId, emitter] of this.sessionEmitters) {
248
392
  if (this.activeSessionIds.has(sessionId)) {
249
- emitter.emit("error", new Error("Local speech worker exited"));
393
+ this.emitErrorIfObserved(sessionId, emitter, error, workerPid);
250
394
  }
251
395
  }
252
396
  this.activeSessionIds.clear();
253
397
  this.sessionEmitters.clear();
254
398
  this.inFlightRequests = 0;
255
399
  }
400
+ describePendingRequests() {
401
+ const now = Date.now();
402
+ return Array.from(this.pendingRequests.entries()).map(([requestId, request]) => Object.assign({
403
+ requestId,
404
+ type: request.type,
405
+ ageMs: Math.max(0, now - request.startedAt),
406
+ }, request.summary));
407
+ }
408
+ getStderrTail() {
409
+ return truncateStart(this.stderrTail.trim(), STDERR_TAIL_MAX_CHARS);
410
+ }
256
411
  rejectAllPending(error) {
257
412
  for (const [requestId, pending] of this.pendingRequests) {
258
413
  clearTimeout(pending.timeout);
@@ -265,7 +420,14 @@ export class LocalSpeechWorkerClient {
265
420
  if (!emitter) {
266
421
  return;
267
422
  }
268
- emitter.emit("error", error instanceof Error ? error : new Error(String(error)));
423
+ this.emitErrorIfObserved(sessionId, emitter, error instanceof Error ? error : new Error(String(error)));
424
+ }
425
+ emitErrorIfObserved(sessionId, emitter, error, workerPid = this.workerPid) {
426
+ if (emitter.listenerCount("error") > 0) {
427
+ emitter.emit("error", error);
428
+ return;
429
+ }
430
+ this.logger.warn({ err: error, workerPid, sessionId }, "Local speech worker session error had no listener");
269
431
  }
270
432
  scheduleIdleShutdownIfReady() {
271
433
  if (!this.worker || this.inFlightRequests > 0 || this.activeSessionIds.size > 0) {
@@ -51,6 +51,7 @@ export declare class VoiceAssistantWebSocketServer {
51
51
  private readonly pendingConnections;
52
52
  private readonly sessions;
53
53
  private readonly externalSessionsByKey;
54
+ private readonly socketMessageQueues;
54
55
  private readonly serverId;
55
56
  private readonly daemonVersion;
56
57
  private readonly daemonRuntimeConfig;
@@ -129,6 +130,7 @@ export declare class VoiceAssistantWebSocketServer {
129
130
  private broadcastCapabilitiesUpdate;
130
131
  private broadcastDaemonConfigChanged;
131
132
  private bindSocketHandlers;
133
+ private enqueueRawMessage;
132
134
  resolveVoiceSpeakHandler(callerAgentId: string): VoiceSpeakHandler | null;
133
135
  resolveVoiceCallerContext(callerAgentId: string): VoiceCallerContext | null;
134
136
  private detachSocket;
@@ -2,7 +2,7 @@ import { WebSocketServer } from "ws";
2
2
  import { basename, join } from "path";
3
3
  import { hostname as getHostname } from "node:os";
4
4
  import { WSInboundMessageSchema, wrapSessionMessage, } from "./messages.js";
5
- import { asUint8Array, decodeTerminalStreamFrame } from "@getpaseo/protocol/binary-frames/index";
5
+ import { asUint8Array, decodeBinaryFrame } from "@getpaseo/protocol/binary-frames/index";
6
6
  import { isHostnameAllowed } from "./hostnames.js";
7
7
  import { Session } from "./session.js";
8
8
  import { buildWorkspaceGitMetadataFromSnapshot } from "./workspace-git-metadata.js";
@@ -206,6 +206,7 @@ export class VoiceAssistantWebSocketServer {
206
206
  this.pendingConnections = new Map();
207
207
  this.sessions = new Map();
208
208
  this.externalSessionsByKey = new Map();
209
+ this.socketMessageQueues = new Map();
209
210
  this.voiceSpeakHandlers = new Map();
210
211
  this.voiceCallerContexts = new Map();
211
212
  this.workspaceSetupSnapshots = new Map();
@@ -756,7 +757,7 @@ export class VoiceAssistantWebSocketServer {
756
757
  bindSocketHandlers(ws) {
757
758
  ws.on("message", (...args) => {
758
759
  const data = args[0];
759
- void this.handleRawMessage(ws, data);
760
+ this.enqueueRawMessage(ws, data);
760
761
  });
761
762
  ws.on("close", async (...args) => {
762
763
  const code = args[0];
@@ -776,6 +777,18 @@ export class VoiceAssistantWebSocketServer {
776
777
  await this.detachSocket(ws, { error: err });
777
778
  });
778
779
  }
780
+ enqueueRawMessage(ws, data) {
781
+ const previous = this.socketMessageQueues.get(ws) ?? Promise.resolve();
782
+ const next = previous.then(() => this.handleRawMessage(ws, data), () => this.handleRawMessage(ws, data));
783
+ this.socketMessageQueues.set(ws, next);
784
+ void next
785
+ .catch(() => undefined)
786
+ .finally(() => {
787
+ if (this.socketMessageQueues.get(ws) === next) {
788
+ this.socketMessageQueues.delete(ws);
789
+ }
790
+ });
791
+ }
779
792
  resolveVoiceSpeakHandler(callerAgentId) {
780
793
  return this.voiceSpeakHandlers.get(callerAgentId) ?? null;
781
794
  }
@@ -897,14 +910,14 @@ export class VoiceAssistantWebSocketServer {
897
910
  },
898
911
  }));
899
912
  }
900
- maybeHandleBinaryFrame(params) {
913
+ async maybeHandleBinaryFrame(params) {
901
914
  const { ws, buffer, activeConnection, log } = params;
902
915
  const asBytes = asUint8Array(buffer);
903
916
  if (!asBytes) {
904
917
  return false;
905
918
  }
906
- const frame = decodeTerminalStreamFrame(asBytes);
907
- if (!frame) {
919
+ const decodedFrame = decodeBinaryFrame(asBytes);
920
+ if (!decodedFrame) {
908
921
  return false;
909
922
  }
910
923
  if (!activeConnection) {
@@ -919,7 +932,7 @@ export class VoiceAssistantWebSocketServer {
919
932
  }
920
933
  return true;
921
934
  }
922
- activeConnection.session.handleBinaryFrame(frame);
935
+ await activeConnection.session.handleBinaryFrame(decodedFrame);
923
936
  return true;
924
937
  }
925
938
  handlePendingConnectionMessage(params) {
@@ -950,7 +963,7 @@ export class VoiceAssistantWebSocketServer {
950
963
  const log = activeConnection?.connectionLogger ?? pendingConnection?.connectionLogger ?? this.logger;
951
964
  try {
952
965
  const buffer = bufferFromWsData(data);
953
- const binaryHandled = this.maybeHandleBinaryFrame({
966
+ const binaryHandled = await this.maybeHandleBinaryFrame({
954
967
  ws,
955
968
  buffer,
956
969
  activeConnection,
@@ -16,8 +16,8 @@ declare const PersistedProjectRecordSchema: z.ZodObject<{
16
16
  archivedAt: string | null;
17
17
  kind: "git" | "non_git";
18
18
  projectId: string;
19
- rootPath: string;
20
19
  displayName: string;
20
+ rootPath: string;
21
21
  customName: string | null;
22
22
  }, {
23
23
  createdAt: string;
@@ -25,8 +25,8 @@ declare const PersistedProjectRecordSchema: z.ZodObject<{
25
25
  archivedAt: string | null;
26
26
  kind: "git" | "non_git";
27
27
  projectId: string;
28
- rootPath: string;
29
28
  displayName: string;
29
+ rootPath: string;
30
30
  customName?: string | null | undefined;
31
31
  }>;
32
32
  declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
@@ -44,7 +44,7 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
44
44
  createdAt: string;
45
45
  updatedAt: string;
46
46
  archivedAt: string | null;
47
- kind: "worktree" | "local_checkout" | "directory";
47
+ kind: "local_checkout" | "worktree" | "directory";
48
48
  projectId: string;
49
49
  displayName: string;
50
50
  }, {
@@ -53,7 +53,7 @@ declare const PersistedWorkspaceRecordSchema: z.ZodObject<{
53
53
  createdAt: string;
54
54
  updatedAt: string;
55
55
  archivedAt: string | null;
56
- kind: "worktree" | "local_checkout" | "directory";
56
+ kind: "local_checkout" | "worktree" | "directory";
57
57
  projectId: string;
58
58
  displayName: string;
59
59
  }>;
@@ -12,8 +12,11 @@ const NO_SEGMENT_INDEX = Number.MAX_SAFE_INTEGER;
12
12
  const NO_MATCH_OFFSET = Number.MAX_SAFE_INTEGER;
13
13
  const NO_FUZZY_SCORE = Number.MAX_SAFE_INTEGER;
14
14
  const NO_WORKSPACE_MATCH_TIER = 5;
15
- const WORKSPACE_IGNORED_DIRECTORY_NAMES = new Set([
15
+ const IGNORED_SUGGESTION_DIRECTORY_NAMES = new Set([
16
16
  "node_modules",
17
+ "venv",
18
+ "env",
19
+ "virtualenv",
17
20
  "dist",
18
21
  "build",
19
22
  "target",
@@ -589,7 +592,9 @@ async function listChildDirectories(input) {
589
592
  return cached.entries;
590
593
  }
591
594
  const dirents = await readdir(input.directory, { withFileTypes: true }).catch(() => []);
592
- const candidates = dirents.filter((dirent) => !isHiddenDirectoryName(dirent.name) && (dirent.isDirectory() || dirent.isSymbolicLink()));
595
+ const candidates = dirents.filter((dirent) => !isHiddenDirectoryName(dirent.name) &&
596
+ !isIgnoredSuggestionDirectoryName(dirent.name) &&
597
+ (dirent.isDirectory() || dirent.isSymbolicLink()));
593
598
  const resolved = await Promise.all(candidates.map(async (dirent) => {
594
599
  const candidatePath = path.join(input.directory, dirent.name);
595
600
  const absolutePath = await resolveDirectoryCandidate({
@@ -613,7 +618,7 @@ async function listWorkspaceChildEntries(input) {
613
618
  return cached.entries;
614
619
  }
615
620
  const dirents = await readdir(input.directory, { withFileTypes: true }).catch(() => []);
616
- const candidates = dirents.filter((dirent) => !isHiddenDirectoryName(dirent.name) && !isIgnoredWorkspaceDirectoryName(dirent.name));
621
+ const candidates = dirents.filter((dirent) => !isHiddenDirectoryName(dirent.name) && !isIgnoredSuggestionDirectoryName(dirent.name));
617
622
  const resolved = await Promise.all(candidates.map(async (dirent) => {
618
623
  const candidatePath = path.join(input.directory, dirent.name);
619
624
  const entry = await resolveWorkspaceCandidate({
@@ -682,8 +687,8 @@ async function resolveWorkspaceCandidate(input) {
682
687
  function isHiddenDirectoryName(name) {
683
688
  return name.startsWith(".");
684
689
  }
685
- function isIgnoredWorkspaceDirectoryName(name) {
686
- return WORKSPACE_IGNORED_DIRECTORY_NAMES.has(name);
690
+ function isIgnoredSuggestionDirectoryName(name) {
691
+ return IGNORED_SUGGESTION_DIRECTORY_NAMES.has(name);
687
692
  }
688
693
  function setDirectoryListCache(cacheKey, entry) {
689
694
  directoryListCache.set(cacheKey, entry);
@@ -128,6 +128,10 @@ export declare class UnknownBranchError extends Error {
128
128
  cwd: string;
129
129
  });
130
130
  }
131
+ export declare class InvalidGitBranchNameError extends Error {
132
+ readonly branchName: string;
133
+ constructor(branchName: string);
134
+ }
131
135
  export type ReadPaseoConfigResult = {
132
136
  ok: true;
133
137
  config: PaseoConfig | null;
@@ -55,6 +55,13 @@ export class UnknownBranchError extends Error {
55
55
  this.cwd = params.cwd;
56
56
  }
57
57
  }
58
+ export class InvalidGitBranchNameError extends Error {
59
+ constructor(branchName) {
60
+ super(`Invalid branch name: Git rejected ref name '${branchName}'`);
61
+ this.name = "InvalidGitBranchNameError";
62
+ this.branchName = branchName;
63
+ }
64
+ }
58
65
  export function readPaseoConfig(repoRoot) {
59
66
  try {
60
67
  const json = readPaseoConfigJson(repoRoot);
@@ -898,7 +905,7 @@ async function resolveWorktreeSourcePlan({ cwd, source, desiredSlug, }) {
898
905
  };
899
906
  }
900
907
  case "checkout-branch": {
901
- validateWorktreeBranchName(source.branchName);
908
+ await validateExistingWorktreeBranchName(cwd, source.branchName);
902
909
  if (!(await localBranchExists(cwd, source.branchName))) {
903
910
  try {
904
911
  await runGitCommand(["fetch", "origin", `${source.branchName}:${source.branchName}`], {
@@ -921,7 +928,7 @@ async function resolveWorktreeSourcePlan({ cwd, source, desiredSlug, }) {
921
928
  }
922
929
  case "checkout-github-pr": {
923
930
  const localBranchCandidate = source.localBranchName ?? source.headRef;
924
- validateWorktreeBranchName(localBranchCandidate);
931
+ await validateExistingWorktreeBranchName(cwd, localBranchCandidate);
925
932
  const localBranchName = await resolveUniqueLocalBranchName(cwd, localBranchCandidate);
926
933
  const normalizedBaseRefName = normalizeRequiredBaseBranch(source.baseRefName);
927
934
  await runGitCommand([
@@ -966,6 +973,16 @@ function validateWorktreeBranchName(branchName) {
966
973
  throw new Error(`Invalid branch name: ${validation.error}`);
967
974
  }
968
975
  }
976
+ async function validateExistingWorktreeBranchName(cwd, branchName) {
977
+ const result = await runGitCommand(["check-ref-format", "--branch", branchName], {
978
+ cwd,
979
+ timeout: 30000,
980
+ acceptExitCodes: [0, 1, 128],
981
+ });
982
+ if (result.exitCode !== 0) {
983
+ throw new InvalidGitBranchNameError(branchName);
984
+ }
985
+ }
969
986
  function normalizeRequiredBaseBranch(baseBranch) {
970
987
  const normalizedBaseBranch = normalizeBaseRefName(baseBranch);
971
988
  if (!normalizedBaseBranch) {
@@ -129,7 +129,7 @@ const AgentMetadataGenerationSchema = z
129
129
  providers: z.array(StructuredGenerationProviderConfigSchema).optional(),
130
130
  })
131
131
  .strict();
132
- const BUILTIN_PROVIDER_IDS = ["claude", "codex", "copilot", "opencode", "pi"];
132
+ const BUILTIN_PROVIDER_IDS = ["claude", "codex", "copilot", "opencode", "pi", "omp"];
133
133
  function isLegacyProviderEntry(value) {
134
134
  if (!value || typeof value !== "object" || Array.isArray(value)) {
135
135
  return false;
@@ -169,6 +169,7 @@ function normalizeAgentProviders(value) {
169
169
  }
170
170
  export const PersistedConfigSchema = z
171
171
  .object({
172
+ $schema: z.string().optional(),
172
173
  // v1 schema marker
173
174
  version: z.literal(1).optional(),
174
175
  // v1 config layout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpaseo/server",
3
- "version": "0.1.91-beta.1",
3
+ "version": "0.1.91",
4
4
  "description": "Paseo backend server",
5
5
  "files": [
6
6
  "dist/server",
@@ -59,10 +59,10 @@
59
59
  "dependencies": {
60
60
  "@agentclientprotocol/sdk": "^0.17.1",
61
61
  "@anthropic-ai/claude-agent-sdk": "^0.2.133",
62
- "@getpaseo/client": "0.1.91-beta.1",
63
- "@getpaseo/highlight": "0.1.91-beta.1",
64
- "@getpaseo/protocol": "0.1.91-beta.1",
65
- "@getpaseo/relay": "0.1.91-beta.1",
62
+ "@getpaseo/client": "0.1.91",
63
+ "@getpaseo/highlight": "0.1.91",
64
+ "@getpaseo/protocol": "0.1.91",
65
+ "@getpaseo/relay": "0.1.91",
66
66
  "@isaacs/ttlcache": "^2.1.4",
67
67
  "@modelcontextprotocol/sdk": "^1.20.1",
68
68
  "@opencode-ai/sdk": "1.14.46",