@getpaseo/server 0.1.97-beta.3 → 0.1.98

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 (96) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +11 -3
  2. package/dist/server/server/agent/agent-manager.js +95 -23
  3. package/dist/server/server/agent/agent-prompt.d.ts +1 -1
  4. package/dist/server/server/agent/agent-prompt.js +3 -10
  5. package/dist/server/server/agent/agent-response-loop.js +9 -3
  6. package/dist/server/server/agent/agent-sdk-types.d.ts +9 -3
  7. package/dist/server/server/agent/agent-storage.d.ts +20 -240
  8. package/dist/server/server/agent/agent-storage.js +6 -6
  9. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  10. package/dist/server/server/agent/create-agent/create.js +8 -7
  11. package/dist/server/server/agent/lifecycle-command.d.ts +15 -1
  12. package/dist/server/server/agent/lifecycle-command.js +9 -2
  13. package/dist/server/server/agent/mcp-server.js +263 -119
  14. package/dist/server/server/agent/mcp-shared.d.ts +35 -179
  15. package/dist/server/server/agent/provider-notices.d.ts +3 -0
  16. package/dist/server/server/agent/provider-notices.js +5 -0
  17. package/dist/server/server/agent/provider-registry.d.ts +2 -0
  18. package/dist/server/server/agent/provider-registry.js +10 -3
  19. package/dist/server/server/agent/provider-snapshot-manager.d.ts +3 -0
  20. package/dist/server/server/agent/provider-snapshot-manager.js +11 -2
  21. package/dist/server/server/agent/providers/claude/agent.js +257 -143
  22. package/dist/server/server/agent/providers/claude/models.js +7 -3
  23. package/dist/server/server/agent/providers/claude/project-dir.js +9 -6
  24. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -22
  25. package/dist/server/server/agent/providers/codex/app-server-transport.d.ts +8 -118
  26. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +4 -3
  27. package/dist/server/server/agent/providers/codex-app-server-agent.js +43 -1
  28. package/dist/server/server/agent/providers/copilot-acp-agent.js +4 -1
  29. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +9 -0
  30. package/dist/server/server/agent/providers/diagnostic-utils.js +188 -0
  31. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +1 -5
  32. package/dist/server/server/agent/providers/mock-slow-provider.js +1 -1
  33. package/dist/server/server/agent/providers/opencode/server-manager.d.ts +29 -2
  34. package/dist/server/server/agent/providers/opencode/server-manager.js +83 -17
  35. package/dist/server/server/agent/providers/opencode-agent.d.ts +2 -0
  36. package/dist/server/server/agent/providers/opencode-agent.js +14 -9
  37. package/dist/server/server/agent/providers/pi/agent.d.ts +1 -5
  38. package/dist/server/server/agent/providers/pi/agent.js +27 -14
  39. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +391 -1261
  40. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +26 -16
  41. package/dist/server/server/bootstrap.d.ts +2 -0
  42. package/dist/server/server/bootstrap.js +32 -2
  43. package/dist/server/server/loop-service.d.ts +60 -359
  44. package/dist/server/server/managed-processes/managed-processes.d.ts +76 -0
  45. package/dist/server/server/managed-processes/managed-processes.js +326 -0
  46. package/dist/server/server/migrations/backfill-workspace-id.migration.js +10 -6
  47. package/dist/server/server/package-version.d.ts +1 -7
  48. package/dist/server/server/paseo-worktree-service.js +15 -1
  49. package/dist/server/server/persisted-config.d.ts +138 -1009
  50. package/dist/server/server/persisted-config.js +1 -1
  51. package/dist/server/server/pid-lock.d.ts +1 -15
  52. package/dist/server/server/resolve-worktree-creation-intent.d.ts +3 -0
  53. package/dist/server/server/resolve-worktree-creation-intent.js +3 -3
  54. package/dist/server/server/session.d.ts +18 -1
  55. package/dist/server/server/session.js +424 -64
  56. package/dist/server/server/speech/providers/local/sherpa/model-catalog.d.ts +2 -2
  57. package/dist/server/server/speech/providers/openai/runtime.js +3 -4
  58. package/dist/server/server/speech/speech-types.d.ts +9 -11
  59. package/dist/server/server/websocket-server.d.ts +1 -0
  60. package/dist/server/server/websocket-server.js +15 -0
  61. package/dist/server/server/workspace-archive-service.js +2 -3
  62. package/dist/server/server/workspace-directory.js +5 -5
  63. package/dist/server/server/workspace-reconciliation-service.js +2 -2
  64. package/dist/server/server/workspace-registry.d.ts +17 -48
  65. package/dist/server/server/workspace-registry.js +9 -0
  66. package/dist/server/server/worktree-core.d.ts +1 -0
  67. package/dist/server/server/worktree-core.js +5 -1
  68. package/dist/server/services/quota-fetcher/manifest.d.ts +4 -0
  69. package/dist/server/services/quota-fetcher/manifest.js +47 -0
  70. package/dist/server/services/quota-fetcher/provider.d.ts +17 -0
  71. package/dist/server/services/quota-fetcher/provider.js +2 -0
  72. package/dist/server/services/quota-fetcher/providers/claude.d.ts +26 -0
  73. package/dist/server/services/quota-fetcher/providers/claude.js +217 -0
  74. package/dist/server/services/quota-fetcher/providers/codex.d.ts +23 -0
  75. package/dist/server/services/quota-fetcher/providers/codex.js +211 -0
  76. package/dist/server/services/quota-fetcher/providers/copilot.d.ts +17 -0
  77. package/dist/server/services/quota-fetcher/providers/copilot.js +75 -0
  78. package/dist/server/services/quota-fetcher/providers/cursor.d.ts +17 -0
  79. package/dist/server/services/quota-fetcher/providers/cursor.js +123 -0
  80. package/dist/server/services/quota-fetcher/providers/grok.d.ts +18 -0
  81. package/dist/server/services/quota-fetcher/providers/grok.js +89 -0
  82. package/dist/server/services/quota-fetcher/providers/kimi.d.ts +20 -0
  83. package/dist/server/services/quota-fetcher/providers/kimi.js +89 -0
  84. package/dist/server/services/quota-fetcher/providers/zai.d.ts +17 -0
  85. package/dist/server/services/quota-fetcher/providers/zai.js +58 -0
  86. package/dist/server/services/quota-fetcher/service.d.ts +28 -0
  87. package/dist/server/services/quota-fetcher/service.js +58 -0
  88. package/dist/server/services/quota-fetcher/usage.d.ts +22 -0
  89. package/dist/server/services/quota-fetcher/usage.js +49 -0
  90. package/dist/server/terminal/terminal-session-controller.d.ts +8 -0
  91. package/dist/server/terminal/terminal-session-controller.js +23 -3
  92. package/dist/server/utils/checkout-git.js +36 -76
  93. package/dist/server/utils/directory-suggestions.js +98 -2
  94. package/dist/server/utils/worktree-metadata.d.ts +7 -59
  95. package/dist/src/server/persisted-config.js +1 -1
  96. package/package.json +9 -9
@@ -2,7 +2,7 @@ import equal from "fast-deep-equal";
2
2
  import { v4 as uuidv4 } from "uuid";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { stat } from "node:fs/promises";
5
- import { resolve, sep } from "path";
5
+ import { basename, normalize, resolve, sep } from "path";
6
6
  import { homedir } from "node:os";
7
7
  import { z } from "zod";
8
8
  import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
@@ -32,9 +32,10 @@ import { deriveProjectSlug } from "./workspace-git-metadata.js";
32
32
  import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
33
33
  import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
34
34
  import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
35
+ import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels";
35
36
  import { resolveSnapshotCwd } from "./agent/provider-snapshot-manager.js";
36
37
  import { createAgentCommand } from "./agent/create-agent/create.js";
37
- import { archiveAgentCommand, cancelAgentRunCommand, closeAgentCommand, setAgentModeCommand, updateAgentCommand, } from "./agent/lifecycle-command.js";
38
+ import { archiveAgentCommand, cancelAgentRunCommand, closeAgentCommand, detachAgentCommand, setAgentModeCommand, updateAgentCommand, } from "./agent/lifecycle-command.js";
38
39
  import { buildStoredAgentPayload, resolveEffectiveThinkingOptionId, resolveStoredAgentPayloadUpdatedAt, toAgentPayload, } from "./agent/agent-projections.js";
39
40
  import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
40
41
  import { projectTimelineRows, selectProjectedTimelinePage, } from "./agent/timeline-projection.js";
@@ -50,7 +51,7 @@ import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
50
51
  import { listDirectoryEntries, readExplorerFile, readExplorerFileBytes, getDownloadableFileInfo, } from "./file-explorer/service.js";
51
52
  import { readPaseoConfigForEdit, writePaseoConfigForEdit, } from "../utils/paseo-config-file.js";
52
53
  import { buildMetadataPrompt } from "../utils/build-metadata-prompt.js";
53
- import { archivePersistedWorkspaceRecord } from "./workspace-archive-service.js";
54
+ import { archivePersistedWorkspaceRecord, archiveWorkspaceContents, } from "./workspace-archive-service.js";
54
55
  import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
55
56
  import { checkoutResolvedBranch, commitChanges, mergeToBase, mergeFromBase, pullCurrentBranch, pushCurrentBranch, createPullRequest, renameCurrentBranch as renameCurrentBranchDefault, } from "../utils/checkout-git.js";
56
57
  import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
@@ -70,7 +71,9 @@ import { attemptFirstAgentBranchAutoName, createLocalCheckoutWorkspace, createPa
70
71
  import { generateBranchNameFromFirstAgentContext, } from "./worktree-branch-name-generator.js";
71
72
  import { assertSafeGitRef as assertWorktreeSafeGitRef, buildAgentSessionConfig as buildWorktreeAgentSessionConfig, createPaseoWorktreeWorkflow as createWorktreeWorkflow, handleCreatePaseoWorktreeRequest as handleCreateWorktreeRequest, handlePaseoWorktreeArchiveRequest as handleWorktreeArchiveRequest, handlePaseoWorktreeListRequest as handleWorktreeListRequest, handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage, } from "./worktree-session.js";
72
73
  import { archiveByScope } from "./workspace-archive-service.js";
73
- import { toWorktreeWireError } from "./worktree-errors.js";
74
+ import { WorktreeRequestError, toWorktreeRequestError, toWorktreeWireError, } from "./worktree-errors.js";
75
+ import { createWorktree } from "../utils/worktree.js";
76
+ import { runGitCommand } from "../utils/run-git-command.js";
74
77
  import { CreateAgentLifecycleDispatch } from "./agent/create-agent-lifecycle-dispatch.js";
75
78
  const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
76
79
  async function resolveKnownProjectRootForConfig(input) {
@@ -223,7 +226,7 @@ const PCM_BITS_PER_SAMPLE = 16;
223
226
  const PCM_BYTES_PER_MS = (PCM_SAMPLE_RATE * PCM_CHANNELS * (PCM_BITS_PER_SAMPLE / 8)) / 1000;
224
227
  const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
225
228
  const MIN_STREAMING_SEGMENT_BYTES = Math.round(PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS);
226
- const AgentIdSchema = z.string().uuid();
229
+ const AgentIdSchema = z.guid();
227
230
  const nodeSessionFileSystem = {
228
231
  async isDirectory(path) {
229
232
  const stats = await stat(path).catch(() => null);
@@ -340,7 +343,7 @@ export class Session {
340
343
  }
341
344
  },
342
345
  });
343
- const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, getTransportBufferedAmount, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, renameCurrentBranch, generateWorkspaceName, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
346
+ const { clientId, appVersion, clientCapabilities, onMessage, onBinaryMessage, getTransportBufferedAmount, onLifecycleIntent, logger, downloadTokenStore, pushTokenStore, paseoHome, worktreesRoot, agentManager, agentStorage, projectRegistry, workspaceRegistry, filesystem, chatService, scheduleService, loopService, checkoutDiffManager, github, renameCurrentBranch, generateWorkspaceName, workspaceGitService, daemonConfigStore, mcpBaseUrl, stt, sttLanguage, tts, terminalManager, providerSnapshotManager, providerUsageService, serviceProxy, scriptRuntimeStore, workspaceSetupSnapshots, onBranchChanged, getDaemonTcpPort, getDaemonTcpHost, serviceProxyPublicBaseUrl, resolveScriptHealth, voice, voiceBridge, dictation, serverId, daemonVersion, daemonRuntimeConfig, } = options;
344
347
  this.clientId = clientId;
345
348
  this.appVersion = appVersion ?? null;
346
349
  this.clientCapabilities = parseClientCapabilities(clientCapabilities);
@@ -382,12 +385,7 @@ export class Session {
382
385
  hasBinaryChannel: () => this.onBinaryMessage !== null,
383
386
  isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
384
387
  sessionLogger: this.sessionLogger,
385
- listTerminalWorkspaceRoots: async () => {
386
- const workspaces = await this.workspaceRegistry.list();
387
- return workspaces
388
- .filter((workspace) => !workspace.archivedAt)
389
- .map((workspace) => workspace.cwd);
390
- },
388
+ listTerminalWorkspaceRefs: () => this.listActiveWorkspaceRefs(),
391
389
  clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
392
390
  getClientBufferedAmount: () => this.getTransportBufferedAmount(),
393
391
  });
@@ -419,6 +417,7 @@ export class Session {
419
417
  logger: this.sessionLogger,
420
418
  });
421
419
  this.providerSnapshotManager = providerSnapshotManager;
420
+ this.providerUsageService = providerUsageService;
422
421
  this.serviceProxy = serviceProxy ?? null;
423
422
  this.scriptRuntimeStore = scriptRuntimeStore ?? null;
424
423
  this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
@@ -863,6 +862,39 @@ export class Session {
863
862
  payload,
864
863
  });
865
864
  }
865
+ async emitStoredAgentUpdate(record) {
866
+ const payload = this.buildStoredAgentPayload(record);
867
+ const subscription = this.agentUpdatesSubscription;
868
+ if (!subscription) {
869
+ return payload;
870
+ }
871
+ const project = payload.workspaceId
872
+ ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
873
+ : null;
874
+ if (!project) {
875
+ this.bufferOrEmitAgentUpdate(subscription, {
876
+ kind: "remove",
877
+ agentId: payload.id,
878
+ });
879
+ return payload;
880
+ }
881
+ const matches = this.matchesAgentFilter({
882
+ agent: payload,
883
+ project,
884
+ filter: subscription.filter,
885
+ });
886
+ this.bufferOrEmitAgentUpdate(subscription, matches
887
+ ? {
888
+ kind: "upsert",
889
+ agent: payload,
890
+ project,
891
+ }
892
+ : {
893
+ kind: "remove",
894
+ agentId: payload.id,
895
+ });
896
+ return payload;
897
+ }
866
898
  flushBootstrappedAgentUpdates(options) {
867
899
  const subscription = this.agentUpdatesSubscription;
868
900
  if (!subscription || !subscription.isBootstrapping) {
@@ -911,6 +943,7 @@ export class Session {
911
943
  return {
912
944
  projectKey: project.projectId,
913
945
  projectName: resolveProjectDisplayName(project),
946
+ workspaceName: resolveWorkspaceDisplayName(workspace),
914
947
  checkout,
915
948
  };
916
949
  }
@@ -920,6 +953,15 @@ export class Session {
920
953
  return null;
921
954
  return this.buildProjectPlacementForWorkspace(workspace);
922
955
  }
956
+ async buildProjectPlacementForExistingWorkspaceProject(workspaceId) {
957
+ const workspace = await this.workspaceRegistry.get(workspaceId);
958
+ if (!workspace)
959
+ return null;
960
+ const project = await this.projectRegistry.get(workspace.projectId);
961
+ if (!project)
962
+ return null;
963
+ return this.buildProjectPlacementForWorkspace(workspace, project);
964
+ }
923
965
  async forwardAgentUpdate(agent) {
924
966
  try {
925
967
  const subscription = this.agentUpdatesSubscription;
@@ -1019,6 +1061,7 @@ export class Session {
1019
1061
  async dispatchInboundMessage(msg) {
1020
1062
  const promise = this.dispatchVoiceAndControlMessage(msg) ??
1021
1063
  this.dispatchAgentRewindMessage(msg) ??
1064
+ this.dispatchAgentRelationshipMessage(msg) ??
1022
1065
  this.dispatchAgentLifecycleMessage(msg) ??
1023
1066
  this.dispatchAgentConfigMessage(msg) ??
1024
1067
  this.dispatchCheckoutMessage(msg) ??
@@ -1087,6 +1130,14 @@ export class Session {
1087
1130
  return undefined;
1088
1131
  }
1089
1132
  }
1133
+ dispatchAgentRelationshipMessage(msg) {
1134
+ switch (msg.type) {
1135
+ case "agent.detach.request":
1136
+ return this.handleDetachAgentRequest(msg.agentId, msg.requestId);
1137
+ default:
1138
+ return undefined;
1139
+ }
1140
+ }
1090
1141
  async handleDictationStreamStart(msg) {
1091
1142
  const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
1092
1143
  if (unavailable) {
@@ -1343,8 +1394,12 @@ export class Session {
1343
1394
  return this.handleLegacyOpenInEditorRequest(msg);
1344
1395
  case "open_project_request":
1345
1396
  return this.handleOpenProjectRequest(msg);
1397
+ case "project.add.request":
1398
+ return this.handleProjectAddRequest(msg);
1346
1399
  case "archive_workspace_request":
1347
1400
  return this.handleArchiveWorkspaceRequest(msg);
1401
+ case "project.remove.request":
1402
+ return this.handleProjectRemoveRequest(msg);
1348
1403
  case "workspace.create.request":
1349
1404
  return this.handleWorkspaceCreateRequest(msg);
1350
1405
  case "workspace.clear_attention.request":
@@ -1380,6 +1435,8 @@ export class Session {
1380
1435
  return this.handleRefreshProvidersSnapshotRequest(msg);
1381
1436
  case "provider_diagnostic_request":
1382
1437
  return this.handleProviderDiagnosticRequest(msg);
1438
+ case "provider.usage.list.request":
1439
+ return this.handleProviderUsageListRequest(msg);
1383
1440
  default:
1384
1441
  return undefined;
1385
1442
  }
@@ -1571,39 +1628,60 @@ export class Session {
1571
1628
  logger: this.sessionLogger,
1572
1629
  }, agentId);
1573
1630
  if (this.agentUpdatesSubscription) {
1574
- const payload = this.buildStoredAgentPayload(archivedRecord);
1575
- const project = payload.workspaceId
1576
- ? await this.buildProjectPlacementForWorkspaceId(payload.workspaceId)
1577
- : null;
1578
- if (project) {
1579
- const matches = this.matchesAgentFilter({
1580
- agent: payload,
1581
- project,
1582
- filter: this.agentUpdatesSubscription.filter,
1583
- });
1584
- this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, matches
1585
- ? {
1586
- kind: "upsert",
1587
- agent: payload,
1588
- project,
1589
- }
1590
- : {
1591
- kind: "remove",
1592
- agentId,
1593
- });
1594
- }
1595
- else {
1596
- this.bufferOrEmitAgentUpdate(this.agentUpdatesSubscription, {
1597
- kind: "remove",
1598
- agentId,
1599
- });
1600
- }
1631
+ const payload = await this.emitStoredAgentUpdate(archivedRecord);
1601
1632
  if (payload.workspaceId) {
1602
1633
  await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
1603
1634
  }
1604
1635
  }
1605
1636
  return { agentId, archivedAt };
1606
1637
  }
1638
+ async handleDetachAgentRequest(agentId, requestId) {
1639
+ this.sessionLogger.info({ agentId, requestId }, "Detaching agent from parent");
1640
+ try {
1641
+ const result = await detachAgentCommand({ agentManager: this.agentManager }, agentId);
1642
+ const affectedWorkspaceIds = new Set();
1643
+ if (!result.live) {
1644
+ const payload = await this.emitStoredAgentUpdate(result.record);
1645
+ if (payload.workspaceId) {
1646
+ affectedWorkspaceIds.add(payload.workspaceId);
1647
+ }
1648
+ }
1649
+ else if (result.record.workspaceId) {
1650
+ affectedWorkspaceIds.add(result.record.workspaceId);
1651
+ }
1652
+ if (result.previousParentAgentId) {
1653
+ const rootWorkspaceId = await this.resolveDelegationRootWorkspaceId(result.previousParentAgentId);
1654
+ if (rootWorkspaceId) {
1655
+ affectedWorkspaceIds.add(rootWorkspaceId);
1656
+ }
1657
+ }
1658
+ await this.emitWorkspaceUpdatesForWorkspaceIds(affectedWorkspaceIds, {
1659
+ skipReconcile: true,
1660
+ });
1661
+ this.emit({
1662
+ type: "agent.detach.response",
1663
+ payload: {
1664
+ requestId,
1665
+ agentId,
1666
+ accepted: true,
1667
+ error: null,
1668
+ },
1669
+ });
1670
+ }
1671
+ catch (error) {
1672
+ const message = getErrorMessageOr(error, "Failed to detach agent");
1673
+ this.sessionLogger.error({ err: error, agentId, requestId }, "Failed to detach agent");
1674
+ this.emit({
1675
+ type: "agent.detach.response",
1676
+ payload: {
1677
+ requestId,
1678
+ agentId,
1679
+ accepted: false,
1680
+ error: message,
1681
+ },
1682
+ });
1683
+ }
1684
+ }
1607
1685
  async handleCloseItemsRequest(msg) {
1608
1686
  const archiveResults = await Promise.allSettled(msg.agentIds.map((agentId) => this.archiveAgentForClose(agentId)));
1609
1687
  const agents = [];
@@ -1764,6 +1842,80 @@ export class Session {
1764
1842
  });
1765
1843
  }
1766
1844
  }
1845
+ async handleProjectRemoveRequest(request) {
1846
+ const { projectId, requestId } = request;
1847
+ this.sessionLogger.info({ projectId, requestId }, "session: project.remove.request");
1848
+ try {
1849
+ const projectWorkspaces = (await this.workspaceRegistry.list()).filter((workspace) => workspace.projectId === projectId);
1850
+ const activeWorkspaceIds = projectWorkspaces
1851
+ .filter((workspace) => !workspace.archivedAt)
1852
+ .map((workspace) => workspace.workspaceId);
1853
+ if (activeWorkspaceIds.length > 0) {
1854
+ this.markWorkspaceArchiving(activeWorkspaceIds, new Date().toISOString());
1855
+ await this.emitWorkspaceUpdatesForWorkspaceIds(activeWorkspaceIds, {
1856
+ skipReconcile: true,
1857
+ });
1858
+ }
1859
+ const removedWorkspaceIds = [];
1860
+ try {
1861
+ for (const workspaceId of activeWorkspaceIds) {
1862
+ await archiveWorkspaceContents({
1863
+ agentManager: this.agentManager,
1864
+ agentStorage: this.agentStorage,
1865
+ killTerminalsForWorkspace: (id) => this.terminalController.killTerminalsForWorkspace(id),
1866
+ sessionLogger: this.sessionLogger,
1867
+ }, workspaceId);
1868
+ await this.archiveWorkspaceRecord(workspaceId);
1869
+ removedWorkspaceIds.push(workspaceId);
1870
+ }
1871
+ await this.projectRegistry.remove(projectId);
1872
+ }
1873
+ finally {
1874
+ if (activeWorkspaceIds.length > 0) {
1875
+ this.clearWorkspaceArchiving(activeWorkspaceIds);
1876
+ }
1877
+ }
1878
+ const updateIds = removedWorkspaceIds.length > 0
1879
+ ? removedWorkspaceIds
1880
+ : [projectWorkspaces[0]?.workspaceId ?? projectId];
1881
+ await this.emitWorkspaceUpdatesForWorkspaceIds(updateIds, {
1882
+ skipReconcile: true,
1883
+ removedProjectId: projectId,
1884
+ });
1885
+ this.emit({
1886
+ type: "project.remove.response",
1887
+ payload: {
1888
+ requestId,
1889
+ projectId,
1890
+ accepted: true,
1891
+ removedWorkspaceIds,
1892
+ error: null,
1893
+ },
1894
+ });
1895
+ }
1896
+ catch (error) {
1897
+ this.sessionLogger.error({ err: error, projectId, requestId }, "session: project.remove.request error");
1898
+ this.emit({
1899
+ type: "activity_log",
1900
+ payload: {
1901
+ id: uuidv4(),
1902
+ timestamp: new Date(),
1903
+ type: "error",
1904
+ content: `Failed to remove project: ${getErrorMessage(error)}`,
1905
+ },
1906
+ });
1907
+ this.emit({
1908
+ type: "project.remove.response",
1909
+ payload: {
1910
+ requestId,
1911
+ projectId,
1912
+ accepted: false,
1913
+ removedWorkspaceIds: [],
1914
+ error: getErrorMessageOr(error, "Failed to remove project"),
1915
+ },
1916
+ });
1917
+ }
1918
+ }
1767
1919
  async handleWorkspaceTitleSetRequest(workspaceId, title, requestId) {
1768
1920
  this.sessionLogger.info({ workspaceId, requestId, hasTitle: typeof title === "string" }, "session: workspace.title.set.request");
1769
1921
  try {
@@ -2412,6 +2564,7 @@ export class Session {
2412
2564
  this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`);
2413
2565
  try {
2414
2566
  await unarchiveAgentState(this.agentStorage, this.agentManager, agentId);
2567
+ await this.unarchiveOwningWorkspaceForAgent(agentId);
2415
2568
  let snapshot;
2416
2569
  const existing = this.agentManager.getAgent(agentId);
2417
2570
  if (existing) {
@@ -2460,7 +2613,7 @@ export class Session {
2460
2613
  requestId,
2461
2614
  requestType: msg.type,
2462
2615
  error: message,
2463
- code: "agent_refresh_failed",
2616
+ code: error instanceof WorktreeRequestError ? error.code : "agent_refresh_failed",
2464
2617
  },
2465
2618
  });
2466
2619
  }
@@ -2991,6 +3144,32 @@ export class Session {
2991
3144
  });
2992
3145
  }
2993
3146
  }
3147
+ async handleProviderUsageListRequest(msg) {
3148
+ try {
3149
+ const usage = await this.providerUsageService.listUsage();
3150
+ this.emit({
3151
+ type: "provider.usage.list.response",
3152
+ payload: {
3153
+ requestId: msg.requestId,
3154
+ fetchedAt: usage.fetchedAt,
3155
+ providers: usage.providers,
3156
+ },
3157
+ });
3158
+ }
3159
+ catch (error) {
3160
+ const err = error instanceof Error ? error : new Error(String(error));
3161
+ this.sessionLogger.error({ err }, "Failed to list provider usage");
3162
+ this.emit({
3163
+ type: "rpc_error",
3164
+ payload: {
3165
+ requestId: msg.requestId,
3166
+ requestType: msg.type,
3167
+ error: `Failed to list provider usage: ${err.message}`,
3168
+ code: "provider_usage_list_failed",
3169
+ },
3170
+ });
3171
+ }
3172
+ }
2994
3173
  assertSafeGitRef(ref, label) {
2995
3174
  if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
2996
3175
  throw new Error(`Invalid ${label}: ${ref}`);
@@ -3225,11 +3404,11 @@ export class Session {
3225
3404
  async handleSetAgentModeRequest(agentId, modeId, requestId) {
3226
3405
  this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request");
3227
3406
  try {
3228
- await setAgentModeCommand({ agentManager: this.agentManager }, { agentId, modeId });
3407
+ const result = await setAgentModeCommand({ agentManager: this.agentManager }, { agentId, modeId });
3229
3408
  this.sessionLogger.info({ agentId, modeId, requestId }, "session: set_agent_mode_request success");
3230
3409
  this.emit({
3231
3410
  type: "set_agent_mode_response",
3232
- payload: { requestId, agentId, accepted: true, error: null },
3411
+ payload: { requestId, agentId, accepted: true, error: null, notice: result.notice },
3233
3412
  });
3234
3413
  }
3235
3414
  catch (error) {
@@ -3321,11 +3500,11 @@ export class Session {
3321
3500
  async handleSetAgentThinkingRequest(agentId, thinkingOptionId, requestId) {
3322
3501
  this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request");
3323
3502
  try {
3324
- await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
3503
+ const notice = await this.agentManager.setAgentThinkingOption(agentId, thinkingOptionId);
3325
3504
  this.sessionLogger.info({ agentId, thinkingOptionId, requestId }, "session: set_agent_thinking_request success");
3326
3505
  this.emit({
3327
3506
  type: "set_agent_thinking_response",
3328
- payload: { requestId, agentId, accepted: true, error: null },
3507
+ payload: { requestId, agentId, accepted: true, error: null, notice },
3329
3508
  });
3330
3509
  }
3331
3510
  catch (error) {
@@ -4880,6 +5059,29 @@ export class Session {
4880
5059
  const payload = this.buildStoredAgentPayload(record);
4881
5060
  return this.isProviderVisibleToClient(payload.provider) ? payload : null;
4882
5061
  }
5062
+ async resolveDelegationRootWorkspaceId(agentId) {
5063
+ const seen = new Set();
5064
+ let currentAgentId = agentId;
5065
+ while (true) {
5066
+ if (seen.has(currentAgentId)) {
5067
+ return null;
5068
+ }
5069
+ seen.add(currentAgentId);
5070
+ const live = this.agentManager.getAgent(currentAgentId);
5071
+ const source = live ?? (await this.agentStorage.get(currentAgentId));
5072
+ if (!source) {
5073
+ return null;
5074
+ }
5075
+ if ("archivedAt" in source && source.archivedAt) {
5076
+ return null;
5077
+ }
5078
+ const parentAgentId = getParentAgentIdFromLabels(source.labels);
5079
+ if (!parentAgentId) {
5080
+ return source.workspaceId ?? null;
5081
+ }
5082
+ currentAgentId = parentAgentId;
5083
+ }
5084
+ }
4883
5085
  async buildActiveProjectPlacementsByWorkspaceId() {
4884
5086
  const [persistedWorkspaces, persistedProjects] = await Promise.all([
4885
5087
  this.workspaceRegistry.list(),
@@ -4962,7 +5164,9 @@ export class Session {
4962
5164
  if (existing) {
4963
5165
  return existing;
4964
5166
  }
4965
- const placementPromise = this.buildProjectPlacementForWorkspaceId(workspaceId);
5167
+ const placementPromise = request.type === "fetch_agent_history_request"
5168
+ ? this.buildProjectPlacementForExistingWorkspaceProject(workspaceId)
5169
+ : this.buildProjectPlacementForWorkspaceId(workspaceId);
4966
5170
  placementByWorkspaceId.set(workspaceId, placementPromise);
4967
5171
  return placementPromise;
4968
5172
  };
@@ -5285,6 +5489,26 @@ export class Session {
5285
5489
  await this.workspaceRegistry.upsert(workspaceRecord);
5286
5490
  return workspaceRecord;
5287
5491
  }
5492
+ async findOrCreateProjectForDirectory(cwd) {
5493
+ const normalizedCwd = resolve(cwd);
5494
+ const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
5495
+ const membership = classifyDirectoryForProjectMembership({ cwd: normalizedCwd, checkout });
5496
+ const projectRecord = await this.resolveProjectRecordForPlacement({
5497
+ membership,
5498
+ timestamp: new Date().toISOString(),
5499
+ });
5500
+ await this.projectRegistry.upsert(projectRecord);
5501
+ return projectRecord;
5502
+ }
5503
+ buildProjectDescriptor(project) {
5504
+ return {
5505
+ projectId: project.projectId,
5506
+ projectDisplayName: resolveProjectDisplayName(project),
5507
+ projectCustomName: project.customName ?? null,
5508
+ projectRootPath: project.rootPath,
5509
+ projectKind: project.kind,
5510
+ };
5511
+ }
5288
5512
  async reclassifyOrUnarchiveWorkspaceForDirectory(input) {
5289
5513
  const checkout = await this.workspaceGitService.getCheckout(input.cwd);
5290
5514
  const membership = classifyDirectoryForProjectMembership({ cwd: input.cwd, checkout });
@@ -5299,6 +5523,9 @@ export class Session {
5299
5523
  if (input.workspace.projectId === projectId &&
5300
5524
  input.workspace.kind === kind &&
5301
5525
  input.workspace.displayName === displayName) {
5526
+ if (!input.project) {
5527
+ await this.projectRegistry.upsert(projectRecord);
5528
+ }
5302
5529
  return this.ensureWorkspaceRecordUnarchived(input.workspace);
5303
5530
  }
5304
5531
  await this.projectRegistry.upsert(projectRecord);
@@ -5340,6 +5567,78 @@ export class Session {
5340
5567
  updatedAt: input.timestamp,
5341
5568
  };
5342
5569
  }
5570
+ async unarchiveOwningWorkspaceForAgent(agentId) {
5571
+ const record = await this.agentStorage.get(agentId);
5572
+ if (!record?.workspaceId) {
5573
+ return;
5574
+ }
5575
+ const workspace = await this.workspaceRegistry.get(record.workspaceId);
5576
+ if (!workspace?.archivedAt) {
5577
+ return;
5578
+ }
5579
+ const directoryExists = await this.filesystem.isDirectory(record.cwd).catch(() => false);
5580
+ if (!directoryExists) {
5581
+ if (workspace.kind !== "worktree" || !workspace.branch) {
5582
+ return;
5583
+ }
5584
+ // Recreate the worktree directory from its kept branch BEFORE clearing
5585
+ // archivedAt — the reconciler re-archives workspaces whose directory is
5586
+ // missing, so the record must point at a real directory first.
5587
+ await this.recreateOwningWorktreeForRestore(workspace, workspace.branch);
5588
+ }
5589
+ await this.ensureWorkspaceRecordUnarchived(workspace);
5590
+ await this.emitWorkspaceUpdatesForWorkspaceIds([workspace.workspaceId]);
5591
+ }
5592
+ async recreateOwningWorktreeForRestore(workspace, branch) {
5593
+ const project = await this.projectRegistry.get(workspace.projectId);
5594
+ if (!project) {
5595
+ throw new WorktreeRequestError({
5596
+ code: "unknown",
5597
+ message: `Project ${workspace.projectId} not found for workspace ${workspace.workspaceId}`,
5598
+ });
5599
+ }
5600
+ const projectRootExists = await this.filesystem
5601
+ .isDirectory(project.rootPath)
5602
+ .catch(() => false);
5603
+ if (!projectRootExists) {
5604
+ throw new WorktreeRequestError({
5605
+ code: "unknown",
5606
+ message: `Project root is missing for ${workspace.projectId}: ${project.rootPath}`,
5607
+ });
5608
+ }
5609
+ // Archiving through the default path (scope "workspace", worktreePath only)
5610
+ // resolves repoRoot=null, so deletePaseoWorktree's `git worktree remove`/
5611
+ // `prune` is skipped and the admin registration survives — pinning the
5612
+ // branch as "already checked out". Prune here frees any stale registration
5613
+ // whose working dir is missing (a no-op for live worktrees) so the recreate
5614
+ // below succeeds regardless of how the worktree was archived.
5615
+ try {
5616
+ await runGitCommand(["worktree", "prune"], { cwd: project.rootPath, timeout: 30000 });
5617
+ }
5618
+ catch {
5619
+ // not critical; git will prune lazily
5620
+ }
5621
+ let result;
5622
+ try {
5623
+ result = await createWorktree({
5624
+ cwd: project.rootPath,
5625
+ worktreeSlug: basename(workspace.cwd),
5626
+ source: { kind: "checkout-branch", branchName: branch },
5627
+ runSetup: false,
5628
+ paseoHome: this.paseoHome,
5629
+ worktreesRoot: this.worktreesRoot,
5630
+ });
5631
+ }
5632
+ catch (error) {
5633
+ throw toWorktreeRequestError(error);
5634
+ }
5635
+ if (normalize(result.worktreePath) !== normalize(workspace.cwd)) {
5636
+ throw new WorktreeRequestError({
5637
+ code: "unknown",
5638
+ message: `Recreated worktree diverged from ${workspace.cwd}: ${result.worktreePath}`,
5639
+ });
5640
+ }
5641
+ }
5343
5642
  async ensureWorkspaceRecordUnarchived(workspace) {
5344
5643
  const project = await this.projectRegistry.get(workspace.projectId);
5345
5644
  if (!workspace.archivedAt && (!project || !project.archivedAt)) {
@@ -5501,21 +5800,10 @@ export class Session {
5501
5800
  this.shouldSkipWorkspaceGitWatchUpdate(workspaceId, nextWorkspace)) {
5502
5801
  continue;
5503
5802
  }
5504
- const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
5505
- if (watchTarget && this.onBranchChanged) {
5506
- const newBranchName = nextWorkspace?.name ?? null;
5507
- if (newBranchName !== watchTarget.lastBranchName) {
5508
- this.onBranchChanged(workspaceId, watchTarget.lastBranchName, newBranchName);
5509
- }
5510
- }
5511
- this.rememberWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
5803
+ this.recordWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
5512
5804
  if (!nextWorkspace) {
5513
5805
  subscription.lastEmittedByWorkspaceId.delete(workspaceId);
5514
- this.bufferOrEmitWorkspaceUpdate(subscription, {
5515
- kind: "remove",
5516
- id: workspaceId,
5517
- ...(await this.resolveEmptyProjectForArchivedWorkspace(workspaceId)),
5518
- });
5806
+ this.bufferOrEmitWorkspaceUpdate(subscription, await this.buildWorkspaceRemoveUpdatePayload(workspaceId, options?.removedProjectId));
5519
5807
  continue;
5520
5808
  }
5521
5809
  const nextPayload = {
@@ -5534,16 +5822,36 @@ export class Session {
5534
5822
  void this.reconcileAndEmitWorkspaceUpdates();
5535
5823
  }
5536
5824
  }
5537
- // When a workspace is archived its project may become empty. Resolve the
5538
- // now-empty project parent so the `remove` update can carry it, keeping the
5539
- // sidebar's empty project row in sync without a full re-hydration.
5540
- async resolveEmptyProjectForArchivedWorkspace(workspaceId) {
5825
+ recordWorkspaceGitDescriptorState(workspaceId, nextWorkspace) {
5826
+ const watchTarget = this.resolveWorkspaceGitWatchTarget(workspaceId);
5827
+ if (watchTarget && this.onBranchChanged) {
5828
+ const newBranchName = nextWorkspace?.name ?? null;
5829
+ if (newBranchName !== watchTarget.lastBranchName) {
5830
+ this.onBranchChanged(workspaceId, watchTarget.lastBranchName, newBranchName);
5831
+ }
5832
+ }
5833
+ this.rememberWorkspaceGitDescriptorState(workspaceId, nextWorkspace);
5834
+ }
5835
+ async buildWorkspaceRemoveUpdatePayload(workspaceId, removedProjectId) {
5836
+ if (removedProjectId) {
5837
+ return { kind: "remove", id: workspaceId, removedProjectId };
5838
+ }
5839
+ return {
5840
+ kind: "remove",
5841
+ id: workspaceId,
5842
+ ...(await this.resolveProjectWithoutActiveWorkspacesForArchivedWorkspace(workspaceId)),
5843
+ };
5844
+ }
5845
+ // When a workspace is archived its project may have no active workspaces left.
5846
+ // Resolve that project parent so the `remove` update can carry it, keeping the
5847
+ // sidebar in sync without a full re-hydration.
5848
+ async resolveProjectWithoutActiveWorkspacesForArchivedWorkspace(workspaceId) {
5541
5849
  const archivedWorkspace = await this.workspaceRegistry.get(workspaceId);
5542
5850
  if (!archivedWorkspace) {
5543
5851
  return null;
5544
5852
  }
5545
- const emptyProject = (await this.workspaceDirectory.listEmptyProjects()).find((project) => project.projectId === archivedWorkspace.projectId);
5546
- return emptyProject ? { emptyProject } : null;
5853
+ const projectWithoutActiveWorkspaces = (await this.workspaceDirectory.listEmptyProjects()).find((project) => project.projectId === archivedWorkspace.projectId);
5854
+ return projectWithoutActiveWorkspaces ? { emptyProject: projectWithoutActiveWorkspaces } : null;
5547
5855
  }
5548
5856
  async emitWorkspaceUpdateForTerminalContribution(event) {
5549
5857
  // A terminal's activity contributes only to the workspace it carries. A
@@ -5989,6 +6297,58 @@ export class Session {
5989
6297
  });
5990
6298
  }
5991
6299
  }
6300
+ async handleProjectAddRequest(request) {
6301
+ const requestedCwd = request.cwd;
6302
+ const cwd = expandTilde(requestedCwd);
6303
+ const directoryExists = await this.filesystem.isDirectory(cwd).catch(() => false);
6304
+ if (!directoryExists) {
6305
+ this.sessionLogger.info({ requestedCwd, resolvedCwd: cwd, reason: "directory_not_found" }, "Add project rejected");
6306
+ this.emit({
6307
+ type: "project.add.response",
6308
+ payload: {
6309
+ requestId: request.requestId,
6310
+ project: null,
6311
+ error: `Directory not found: ${cwd}`,
6312
+ errorCode: "directory_not_found",
6313
+ },
6314
+ });
6315
+ return;
6316
+ }
6317
+ try {
6318
+ const projectsBefore = new Map();
6319
+ for (const project of await this.projectRegistry.list()) {
6320
+ projectsBefore.set(project.projectId, project);
6321
+ }
6322
+ const project = await this.findOrCreateProjectForDirectory(cwd);
6323
+ this.sessionLogger.info({
6324
+ requestedCwd,
6325
+ resolvedCwd: cwd,
6326
+ projectId: project.projectId,
6327
+ projectKind: project.kind,
6328
+ projectTransition: describeRegistryTransition(projectsBefore.get(project.projectId) ?? null),
6329
+ }, "Project added");
6330
+ this.emit({
6331
+ type: "project.add.response",
6332
+ payload: {
6333
+ requestId: request.requestId,
6334
+ project: this.buildProjectDescriptor(project),
6335
+ error: null,
6336
+ },
6337
+ });
6338
+ }
6339
+ catch (error) {
6340
+ const message = error instanceof Error ? error.message : "Failed to add project";
6341
+ this.sessionLogger.error({ err: error, cwd }, "Failed to add project");
6342
+ this.emit({
6343
+ type: "project.add.response",
6344
+ payload: {
6345
+ requestId: request.requestId,
6346
+ project: null,
6347
+ error: message,
6348
+ },
6349
+ });
6350
+ }
6351
+ }
5992
6352
  buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory) {
5993
6353
  if (!this.serviceProxy || !this.scriptRuntimeStore) {
5994
6354
  return [];