@getpaseo/server 0.1.89 → 0.1.90

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 (46) hide show
  1. package/dist/server/server/agent/agent-prompt.js +4 -1
  2. package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
  3. package/dist/server/server/agent/agent-storage.js +2 -9
  4. package/dist/server/server/agent/create-agent/create.js +10 -2
  5. package/dist/server/server/agent/create-agent-mode.d.ts +3 -8
  6. package/dist/server/server/agent/create-agent-mode.js +16 -2
  7. package/dist/server/server/agent/import-sessions.js +1 -1
  8. package/dist/server/server/agent/provider-snapshot-manager.d.ts +2 -1
  9. package/dist/server/server/agent/provider-snapshot-manager.js +18 -2
  10. package/dist/server/server/agent/providers/acp-agent.d.ts +3 -3
  11. package/dist/server/server/agent/providers/acp-agent.js +18 -13
  12. package/dist/server/server/agent/providers/codex-app-server-agent.js +16 -22
  13. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
  14. package/dist/server/server/agent/providers/mock-load-test-agent.js +69 -2
  15. package/dist/server/server/agent/providers/opencode-agent.js +19 -8
  16. package/dist/server/server/agent/timeline-projection.js +30 -1
  17. package/dist/server/server/atomic-file.d.ts +3 -0
  18. package/dist/server/server/atomic-file.js +19 -0
  19. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +4 -1
  20. package/dist/server/server/bootstrap.js +2 -0
  21. package/dist/server/server/chat/chat-service.js +2 -4
  22. package/dist/server/server/daemon-keypair.js +2 -2
  23. package/dist/server/server/loop-service.d.ts +4 -0
  24. package/dist/server/server/loop-service.js +27 -9
  25. package/dist/server/server/persisted-config.js +3 -3
  26. package/dist/server/server/private-files.d.ts +0 -1
  27. package/dist/server/server/private-files.js +0 -5
  28. package/dist/server/server/schedule/service.d.ts +6 -0
  29. package/dist/server/server/schedule/service.js +41 -18
  30. package/dist/server/server/schedule/store.js +3 -2
  31. package/dist/server/server/server-id.js +3 -3
  32. package/dist/server/server/session.d.ts +5 -15
  33. package/dist/server/server/session.js +184 -107
  34. package/dist/server/server/speech/providers/local/worker-client.js +1 -11
  35. package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
  36. package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
  37. package/dist/server/server/workspace-directory.d.ts +8 -0
  38. package/dist/server/server/workspace-directory.js +141 -15
  39. package/dist/server/server/workspace-registry.js +2 -6
  40. package/dist/server/utils/checkout-git.d.ts +0 -1
  41. package/dist/server/utils/checkout-git.js +23 -31
  42. package/dist/src/server/persisted-config.js +3 -3
  43. package/dist/src/server/private-files.js +0 -5
  44. package/package.json +9 -7
  45. package/dist/server/server/editor-targets.d.ts +0 -18
  46. package/dist/server/server/editor-targets.js +0 -109
@@ -1,13 +1,11 @@
1
1
  import equal from "fast-deep-equal";
2
2
  import { v4 as uuidv4 } from "uuid";
3
- import { TTLCache } from "@isaacs/ttlcache";
4
- import pMemoize from "p-memoize";
5
3
  import { realpathSync } from "node:fs";
6
4
  import { basename, resolve, sep } from "path";
7
5
  import { homedir } from "node:os";
8
6
  import { z } from "zod";
9
7
  import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
10
- import { isLegacyEditorTargetId, serializeAgentStreamEvent, } from "./messages.js";
8
+ import { serializeAgentStreamEvent, } from "./messages.js";
11
9
  import { TerminalSessionController } from "../terminal/terminal-session-controller.js";
12
10
  import { encodeFileTransferFrame, FileTransferOpcode, } from "@getpaseo/protocol/binary-frames/index";
13
11
  import { CursorError } from "./pagination/cursor.js";
@@ -16,7 +14,6 @@ import { TTSManager } from "./agent/tts-manager.js";
16
14
  import { STTManager } from "./agent/stt-manager.js";
17
15
  import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
18
16
  import { isPaseoDictationDebugEnabled } from "./agent/recordings-debug.js";
19
- import { listAvailableEditorTargets, openInEditorTarget } from "./editor-targets.js";
20
17
  import { getPidLockInfo } from "./pid-lock.js";
21
18
  import { generateLocalPairingOffer } from "./pairing-offer.js";
22
19
  import { DictationStreamManager, } from "./dictation/dictation-stream-manager.js";
@@ -65,6 +62,7 @@ import { notifyChatMentions, prepareChatMentionFanout } from "./chat/chat-mentio
65
62
  import { execCommand } from "../utils/spawn.js";
66
63
  import { assertPullRequestAutoMergeDisableReady, assertPullRequestAutoMergeEnableReady, createGitHubService, } from "../services/github-service.js";
67
64
  import { summarizeFetchWorkspacesEntries, WorkspaceDirectory, } from "./workspace-directory.js";
65
+ import { shouldEmitPendingBootstrapUpdate } from "./workspace-bootstrap-dedupe.js";
68
66
  import { attemptFirstAgentBranchAutoName, createPaseoWorktree, } from "./paseo-worktree-service.js";
69
67
  import { generateBranchNameFromFirstAgentContext } from "./worktree-branch-name-generator.js";
70
68
  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";
@@ -114,7 +112,6 @@ const LEGACY_MODE_ICONS = new Set([
114
112
  "ShieldQuestionMark",
115
113
  ]);
116
114
  const MIN_VERSION_ALL_PROVIDERS = "0.1.45";
117
- const MIN_VERSION_FLEXIBLE_EDITOR_IDS = "0.1.50";
118
115
  function errorToFriendlyMessage(error) {
119
116
  if (error instanceof Error)
120
117
  return error.message;
@@ -190,9 +187,6 @@ function isAppVersionAtLeast(appVersion, minVersion) {
190
187
  function clientSupportsAllProviders(appVersion) {
191
188
  return isAppVersionAtLeast(appVersion, MIN_VERSION_ALL_PROVIDERS);
192
189
  }
193
- function clientSupportsFlexibleEditorIds(appVersion) {
194
- return isAppVersionAtLeast(appVersion, MIN_VERSION_FLEXIBLE_EDITOR_IDS);
195
- }
196
190
  function beginAgentDeleteIfSupported(agentStorage, agentId) {
197
191
  if ("beginDelete" in agentStorage && typeof agentStorage.beginDelete === "function") {
198
192
  agentStorage.beginDelete(agentId);
@@ -220,8 +214,6 @@ const PCM_BYTES_PER_MS = (PCM_SAMPLE_RATE * PCM_CHANNELS * (PCM_BITS_PER_SAMPLE
220
214
  const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
221
215
  const MIN_STREAMING_SEGMENT_BYTES = Math.round(PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS);
222
216
  const AgentIdSchema = z.string().uuid();
223
- const AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS = 60000;
224
- const AVAILABLE_EDITOR_TARGETS_CACHE_KEY = "available";
225
217
  class VoiceFeatureUnavailableError extends Error {
226
218
  constructor(context) {
227
219
  super(context.message);
@@ -296,15 +288,6 @@ export class Session {
296
288
  this.unsubscribeProviderSnapshotEvents = null;
297
289
  this.inflightRequests = 0;
298
290
  this.peakInflightRequests = 0;
299
- this.availableEditorTargetsCache = new TTLCache({
300
- ttl: AVAILABLE_EDITOR_TARGETS_CACHE_TTL_MS,
301
- max: 1,
302
- checkAgeOnGet: true,
303
- });
304
- this.getMemoizedAvailableEditorTargets = pMemoize(async () => this.resolveAvailableEditorTargets(), {
305
- cache: this.availableEditorTargetsCache,
306
- cacheKey: () => AVAILABLE_EDITOR_TARGETS_CACHE_KEY,
307
- });
308
291
  this.checkoutDiffSubscriptions = new Map();
309
292
  this.workspaceGitWatchTargets = new Map();
310
293
  this.workspaceGitFetchSubscriptions = new Map();
@@ -764,12 +747,6 @@ export class Session {
764
747
  }
765
748
  return LEGACY_PROVIDER_IDS.has(provider);
766
749
  }
767
- filterEditorsForClient(editors) {
768
- if (clientSupportsFlexibleEditorIds(this.appVersion)) {
769
- return editors;
770
- }
771
- return editors.filter((editor) => isLegacyEditorTargetId(editor.id));
772
- }
773
750
  agentThinkingOptionMatchesFilter(agent, filter) {
774
751
  if (filter.thinkingOptionId === undefined) {
775
752
  return true;
@@ -1328,14 +1305,17 @@ export class Session {
1328
1305
  return this.handleCreatePaseoWorktreeRequest(msg);
1329
1306
  case "workspace_setup_status_request":
1330
1307
  return this.handleWorkspaceSetupStatusRequest(msg);
1308
+ // COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
1331
1309
  case "list_available_editors_request":
1332
- return this.handleListAvailableEditorsRequest(msg);
1310
+ return this.handleLegacyListAvailableEditorsRequest(msg);
1333
1311
  case "open_in_editor_request":
1334
- return this.handleOpenInEditorRequest(msg);
1312
+ return this.handleLegacyOpenInEditorRequest(msg);
1335
1313
  case "open_project_request":
1336
1314
  return this.handleOpenProjectRequest(msg);
1337
1315
  case "archive_workspace_request":
1338
1316
  return this.handleArchiveWorkspaceRequest(msg);
1317
+ case "workspace.clear_attention.request":
1318
+ return this.handleWorkspaceClearAttentionRequest(msg);
1339
1319
  case "file_explorer_request":
1340
1320
  return this.handleFileExplorerRequest(msg);
1341
1321
  case "project_icon_request":
@@ -4532,7 +4512,7 @@ export class Session {
4532
4512
  // Filter by labels if filter provided
4533
4513
  if (filter?.labels) {
4534
4514
  const filterLabels = filter.labels;
4535
- agents = agents.filter((agent) => Object.entries(filterLabels).every(([key, value]) => agent.labels[key] === value));
4515
+ agents = agents.filter((agent) => Object.entries(filterLabels).every(([key, _value]) => agent.labels[key] === filterLabels[key]));
4536
4516
  }
4537
4517
  return agents;
4538
4518
  }
@@ -4734,6 +4714,7 @@ export class Session {
4734
4714
  name: workspace.displayName,
4735
4715
  archivingAt: null,
4736
4716
  status: "done",
4717
+ statusEnteredAt: null,
4737
4718
  activityAt: null,
4738
4719
  diffStat,
4739
4720
  scripts: this.serviceProxy && this.scriptRuntimeStore
@@ -4809,6 +4790,7 @@ export class Session {
4809
4790
  name: result.worktree.branchName || result.workspace.displayName,
4810
4791
  archivingAt: null,
4811
4792
  status: "done",
4793
+ statusEnteredAt: null,
4812
4794
  activityAt: null,
4813
4795
  diffStat: { additions: 0, deletions: 0 },
4814
4796
  scripts: [],
@@ -4879,15 +4861,26 @@ export class Session {
4879
4861
  subscription.pendingUpdatesByWorkspaceId.clear();
4880
4862
  for (const payload of pending) {
4881
4863
  if (payload.kind === "upsert") {
4882
- const snapshotLatestActivity = options?.snapshotLatestActivityByWorkspaceId?.get(payload.workspace.id);
4883
- if (typeof snapshotLatestActivity === "number") {
4884
- const updateLatestActivity = payload.workspace.activityAt
4885
- ? Date.parse(payload.workspace.activityAt)
4886
- : Number.NEGATIVE_INFINITY;
4887
- if (!Number.isNaN(updateLatestActivity) &&
4888
- updateLatestActivity <= snapshotLatestActivity) {
4889
- continue;
4890
- }
4864
+ const snapshot = options?.snapshotByWorkspaceId?.get(payload.workspace.id);
4865
+ const updateActivityAtMs = payload.workspace.activityAt
4866
+ ? Date.parse(payload.workspace.activityAt)
4867
+ : null;
4868
+ const shouldEmit = shouldEmitPendingBootstrapUpdate({
4869
+ snapshot: snapshot
4870
+ ? {
4871
+ status: snapshot.status,
4872
+ statusEnteredAt: snapshot.statusEnteredAt,
4873
+ activityAtMs: snapshot.activityAtMs,
4874
+ }
4875
+ : null,
4876
+ update: {
4877
+ status: payload.workspace.status,
4878
+ statusEnteredAt: payload.workspace.statusEnteredAt ?? null,
4879
+ activityAtMs: Number.isNaN(updateActivityAtMs) ? null : updateActivityAtMs,
4880
+ },
4881
+ });
4882
+ if (!shouldEmit) {
4883
+ continue;
4891
4884
  }
4892
4885
  }
4893
4886
  this.emit({
@@ -5317,15 +5310,7 @@ export class Session {
5317
5310
  pageInfo: payload.pageInfo,
5318
5311
  payload: summarizeFetchWorkspacesEntries(payload.entries),
5319
5312
  }, "fetch_workspaces_response_ready");
5320
- const snapshotLatestActivityByWorkspaceId = new Map();
5321
- for (const entry of payload.entries) {
5322
- const parsedLatestActivity = entry.activityAt
5323
- ? Date.parse(entry.activityAt)
5324
- : Number.NEGATIVE_INFINITY;
5325
- if (!Number.isNaN(parsedLatestActivity)) {
5326
- snapshotLatestActivityByWorkspaceId.set(entry.id, parsedLatestActivity);
5327
- }
5328
- }
5313
+ const snapshot = this.buildBootstrapSnapshot(payload.entries);
5329
5314
  this.emit({
5330
5315
  type: "fetch_workspaces_response",
5331
5316
  payload: {
@@ -5335,7 +5320,7 @@ export class Session {
5335
5320
  },
5336
5321
  });
5337
5322
  if (subscriptionId && this.workspaceUpdatesSubscription?.subscriptionId === subscriptionId) {
5338
- this.flushBootstrappedWorkspaceUpdates({ snapshotLatestActivityByWorkspaceId });
5323
+ this.flushBootstrappedWorkspaceUpdates(snapshot);
5339
5324
  void this.reconcileAndEmitWorkspaceUpdates();
5340
5325
  }
5341
5326
  }
@@ -5357,6 +5342,24 @@ export class Session {
5357
5342
  });
5358
5343
  }
5359
5344
  }
5345
+ // Build the bootstrap snapshot used by `flushBootstrappedWorkspaceUpdates`
5346
+ // to decide which pending updates to drop. Captures the status,
5347
+ // statusEnteredAt, and activityAt (parsed to ms) for each workspace entry
5348
+ // so a status-only change (e.g. the unmask case), a statusEnteredAt-only
5349
+ // change (e.g. a fresh unmask time), AND a fresher activity all still
5350
+ // ship to the client.
5351
+ buildBootstrapSnapshot(entries) {
5352
+ const snapshotByWorkspaceId = new Map();
5353
+ for (const entry of entries) {
5354
+ const parsedActivity = entry.activityAt ? Date.parse(entry.activityAt) : null;
5355
+ snapshotByWorkspaceId.set(entry.id, {
5356
+ status: entry.status,
5357
+ statusEnteredAt: entry.statusEnteredAt ?? null,
5358
+ activityAtMs: Number.isNaN(parsedActivity) ? null : parsedActivity,
5359
+ });
5360
+ }
5361
+ return { snapshotByWorkspaceId };
5362
+ }
5360
5363
  async registerWorkspaceForImportedAgent(cwd) {
5361
5364
  try {
5362
5365
  const workspace = await this.findOrCreateWorkspaceForDirectory(cwd);
@@ -5440,15 +5443,6 @@ export class Session {
5440
5443
  },
5441
5444
  });
5442
5445
  }
5443
- async resolveAvailableEditorTargets() {
5444
- return listAvailableEditorTargets();
5445
- }
5446
- async getAvailableEditorTargets() {
5447
- return this.filterEditorsForClient(await this.getMemoizedAvailableEditorTargets());
5448
- }
5449
- async openEditorTarget(options) {
5450
- await openInEditorTarget(options);
5451
- }
5452
5446
  async handleStartWorkspaceScriptRequest(request) {
5453
5447
  try {
5454
5448
  if (!this.terminalManager || !this.serviceProxy || !this.scriptRuntimeStore) {
@@ -5507,58 +5501,25 @@ export class Session {
5507
5501
  });
5508
5502
  }
5509
5503
  }
5510
- async handleListAvailableEditorsRequest(request) {
5511
- try {
5512
- const editors = await this.getAvailableEditorTargets();
5513
- this.emit({
5514
- type: "list_available_editors_response",
5515
- payload: {
5516
- requestId: request.requestId,
5517
- editors,
5518
- error: null,
5519
- },
5520
- });
5521
- }
5522
- catch (error) {
5523
- const message = error instanceof Error ? error.message : "Failed to list available editors";
5524
- this.sessionLogger.error({ err: error, requestType: request.type }, "Failed to list available editors");
5525
- this.emit({
5526
- type: "list_available_editors_response",
5527
- payload: {
5528
- requestId: request.requestId,
5529
- editors: [],
5530
- error: message,
5531
- },
5532
- });
5533
- }
5504
+ // COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs.
5505
+ async handleLegacyListAvailableEditorsRequest(request) {
5506
+ this.emit({
5507
+ type: "list_available_editors_response",
5508
+ payload: {
5509
+ requestId: request.requestId,
5510
+ editors: [],
5511
+ error: "Editor opening moved to the desktop app and is no longer supported by the daemon",
5512
+ },
5513
+ });
5534
5514
  }
5535
- async handleOpenInEditorRequest(request) {
5536
- try {
5537
- await this.openEditorTarget({ editorId: request.editorId, path: request.path });
5538
- this.emit({
5539
- type: "open_in_editor_response",
5540
- payload: {
5541
- requestId: request.requestId,
5542
- error: null,
5543
- },
5544
- });
5545
- }
5546
- catch (error) {
5547
- const message = error instanceof Error ? error.message : "Failed to open in editor";
5548
- this.sessionLogger.error({
5549
- err: error,
5550
- editorId: request.editorId,
5551
- path: request.path,
5552
- requestType: request.type,
5553
- }, "Failed to open in editor");
5554
- this.emit({
5555
- type: "open_in_editor_response",
5556
- payload: {
5557
- requestId: request.requestId,
5558
- error: message,
5559
- },
5560
- });
5561
- }
5515
+ async handleLegacyOpenInEditorRequest(request) {
5516
+ this.emit({
5517
+ type: "open_in_editor_response",
5518
+ payload: {
5519
+ requestId: request.requestId,
5520
+ error: "Editor opening moved to the desktop app and is no longer supported by the daemon",
5521
+ },
5522
+ });
5562
5523
  }
5563
5524
  async handleCreatePaseoWorktreeRequest(request) {
5564
5525
  return handleCreateWorktreeRequest({
@@ -5637,6 +5598,122 @@ export class Session {
5637
5598
  });
5638
5599
  }
5639
5600
  }
5601
+ async handleWorkspaceClearAttentionRequest(request) {
5602
+ const { requestId, workspaceId } = request;
5603
+ const requestedWorkspaceIds = Array.isArray(workspaceId) ? workspaceId : [workspaceId];
5604
+ let agents;
5605
+ try {
5606
+ agents = await this.listAgentPayloads();
5607
+ }
5608
+ catch (error) {
5609
+ const message = getErrorMessage(error);
5610
+ const results = requestedWorkspaceIds.map((requestedWorkspaceId) => ({
5611
+ workspaceId: requestedWorkspaceId,
5612
+ clearedAgentIds: [],
5613
+ success: false,
5614
+ error: message,
5615
+ }));
5616
+ this.emit({
5617
+ type: "workspace.clear_attention.response",
5618
+ payload: {
5619
+ requestId,
5620
+ workspaceId,
5621
+ clearedAgentIds: [],
5622
+ results,
5623
+ success: false,
5624
+ error: message,
5625
+ },
5626
+ });
5627
+ return;
5628
+ }
5629
+ const results = [];
5630
+ for (const requestedWorkspaceId of requestedWorkspaceIds) {
5631
+ const clearedAgentIds = [];
5632
+ try {
5633
+ const workspace = await this.workspaceRegistry.get(requestedWorkspaceId);
5634
+ if (!workspace || workspace.archivedAt) {
5635
+ throw new Error(`Workspace not found: ${requestedWorkspaceId}`);
5636
+ }
5637
+ const workspaceCwd = normalizePersistedWorkspaceId(workspace.cwd);
5638
+ const clearableAgentIds = agents
5639
+ .filter((agent) => !agent.archivedAt)
5640
+ .filter((agent) => normalizePersistedWorkspaceId(agent.cwd) === workspaceCwd)
5641
+ .filter((agent) => agent.requiresAttention === true)
5642
+ .filter((agent) => (agent.pendingPermissions?.length ?? 0) === 0)
5643
+ .filter((agent) => agent.attentionReason !== "permission")
5644
+ .map((agent) => agent.id);
5645
+ for (const agentId of clearableAgentIds) {
5646
+ const liveAgent = this.agentManager.getAgent(agentId);
5647
+ if (liveAgent) {
5648
+ await this.agentManager.clearAgentAttention(agentId);
5649
+ clearedAgentIds.push(agentId);
5650
+ continue;
5651
+ }
5652
+ const record = await this.agentStorage.get(agentId);
5653
+ if (!record ||
5654
+ record.internal ||
5655
+ record.archivedAt ||
5656
+ record.requiresAttention !== true) {
5657
+ continue;
5658
+ }
5659
+ const nextRecord = {
5660
+ ...record,
5661
+ updatedAt: new Date().toISOString(),
5662
+ requiresAttention: false,
5663
+ attentionReason: null,
5664
+ attentionTimestamp: null,
5665
+ };
5666
+ await this.agentStorage.upsert(nextRecord);
5667
+ const agent = this.buildStoredAgentPayload(nextRecord);
5668
+ const project = await this.buildProjectPlacementForCwd(agent.cwd);
5669
+ this.emit({
5670
+ type: "agent_update",
5671
+ payload: {
5672
+ kind: "upsert",
5673
+ agent,
5674
+ project,
5675
+ },
5676
+ });
5677
+ clearedAgentIds.push(agentId);
5678
+ }
5679
+ await this.emitWorkspaceUpdateForWorkspaceId(workspace.workspaceId);
5680
+ results.push({
5681
+ workspaceId: requestedWorkspaceId,
5682
+ clearedAgentIds,
5683
+ success: true,
5684
+ error: null,
5685
+ });
5686
+ }
5687
+ catch (error) {
5688
+ const message = getErrorMessage(error);
5689
+ this.sessionLogger.error({ err: error, workspaceId: requestedWorkspaceId }, "Failed to clear workspace attention");
5690
+ results.push({
5691
+ workspaceId: requestedWorkspaceId,
5692
+ clearedAgentIds,
5693
+ success: false,
5694
+ error: message,
5695
+ });
5696
+ }
5697
+ }
5698
+ const clearedAgentIds = results.flatMap((result) => result.clearedAgentIds);
5699
+ const failedResults = results.filter((result) => !result.success);
5700
+ this.emit({
5701
+ type: "workspace.clear_attention.response",
5702
+ payload: {
5703
+ requestId,
5704
+ workspaceId,
5705
+ clearedAgentIds,
5706
+ results,
5707
+ success: failedResults.length === 0,
5708
+ error: failedResults.length === 0
5709
+ ? null
5710
+ : failedResults
5711
+ .map((result) => result.error)
5712
+ .filter((error) => error !== null)
5713
+ .join("; "),
5714
+ },
5715
+ });
5716
+ }
5640
5717
  async handleFetchAgent(agentIdOrIdentifier, requestId) {
5641
5718
  const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier);
5642
5719
  if (!resolved.ok) {
@@ -174,7 +174,7 @@ export class LocalSpeechWorkerClient {
174
174
  reject,
175
175
  timeout,
176
176
  });
177
- const sent = worker.send(message, (error) => {
177
+ worker.send(message, (error) => {
178
178
  if (!error) {
179
179
  return;
180
180
  }
@@ -188,16 +188,6 @@ export class LocalSpeechWorkerClient {
188
188
  this.scheduleIdleShutdownIfReady();
189
189
  pending.reject(error);
190
190
  });
191
- if (!sent) {
192
- const pending = this.pendingRequests.get(requestId);
193
- if (pending) {
194
- clearTimeout(pending.timeout);
195
- this.pendingRequests.delete(requestId);
196
- this.inFlightRequests = Math.max(0, this.inFlightRequests - 1);
197
- this.scheduleIdleShutdownIfReady();
198
- pending.reject(new Error("Local speech worker IPC channel is not writable"));
199
- }
200
- }
201
191
  });
202
192
  }
203
193
  ensureWorker() {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pure dedupe decision for the bootstrap flush.
3
+ *
4
+ * During the bootstrap window the session buffers workspace updates that
5
+ * race with the initial `fetch_workspaces_response`. The flush step decides
6
+ * which buffered updates still carry new information and which are
7
+ * redundant with what the client just received in the snapshot.
8
+ *
9
+ * Returns `true` (emit) when ANY of:
10
+ * - the status changed from the snapshot
11
+ * - the statusEnteredAt changed from the snapshot (including the
12
+ * null↔value transition that the unmask case produces)
13
+ * - the update's activityAtMs is strictly newer than the snapshot's
14
+ * - the snapshot has no activityAtMs and the update has one (new activity
15
+ * where there was none)
16
+ *
17
+ * Returns `false` (drop) when the status pair is unchanged AND the update
18
+ * is not strictly newer than the snapshot in activity. The both-null
19
+ * activity case falls through to drop — there is genuinely no new info.
20
+ */
21
+ export interface BootstrapUpdateSnapshot {
22
+ status: string;
23
+ statusEnteredAt: string | null;
24
+ activityAtMs: number | null;
25
+ }
26
+ export interface BootstrapUpdateCheckInput {
27
+ /** Snapshot captured from the fetch_workspaces_response. `null` means
28
+ * the workspace was not in the snapshot (first-time subscription). */
29
+ snapshot: BootstrapUpdateSnapshot | null;
30
+ /** Pending update buffered during the bootstrap window. */
31
+ update: BootstrapUpdateSnapshot;
32
+ }
33
+ export declare function shouldEmitPendingBootstrapUpdate(input: BootstrapUpdateCheckInput): boolean;
34
+ //# sourceMappingURL=workspace-bootstrap-dedupe.d.ts.map
@@ -0,0 +1,23 @@
1
+ export function shouldEmitPendingBootstrapUpdate(input) {
2
+ const { snapshot, update } = input;
3
+ if (!snapshot) {
4
+ return true;
5
+ }
6
+ if (snapshot.status !== update.status) {
7
+ return true;
8
+ }
9
+ const snapshotEnteredAt = snapshot.statusEnteredAt ?? null;
10
+ const updateEnteredAt = update.statusEnteredAt ?? null;
11
+ if (snapshotEnteredAt !== updateEnteredAt) {
12
+ return true;
13
+ }
14
+ // Status pair is unchanged. The only remaining signal is activity.
15
+ if (update.activityAtMs === null) {
16
+ return false;
17
+ }
18
+ if (snapshot.activityAtMs === null) {
19
+ return true;
20
+ }
21
+ return update.activityAtMs > snapshot.activityAtMs;
22
+ }
23
+ //# sourceMappingURL=workspace-bootstrap-dedupe.js.map
@@ -44,6 +44,12 @@ export declare function summarizeFetchWorkspacesEntries(entries: Iterable<FetchW
44
44
  export declare class WorkspaceDirectory {
45
45
  private readonly deps;
46
46
  private readonly archivingByWorkspaceId;
47
+ /**
48
+ * Per-workspace last-seen winning bucket + entered-at. Persists across
49
+ * `buildDescriptorMap` calls inside the daemon process; reset on cold start.
50
+ * Server-internal; never crosses the wire.
51
+ */
52
+ private readonly bucketHistoryByWorkspaceId;
47
53
  private readonly pager;
48
54
  constructor(deps: WorkspaceDirectoryDeps);
49
55
  markArchiving(workspaceIds: Iterable<string>, archivingAt: string): void;
@@ -52,6 +58,8 @@ export declare class WorkspaceDirectory {
52
58
  includeGitData: boolean;
53
59
  workspaceIds?: Iterable<string>;
54
60
  }): Promise<Map<string, WorkspaceDescriptorPayload>>;
61
+ private resolveStatusEnteredAt;
62
+ private findNewestAgentTimestampInBucket;
55
63
  resolveRegisteredWorkspaceIdForCwd(cwd: string, workspaces: PersistedWorkspaceRecord[]): string;
56
64
  listDescriptors(): Promise<WorkspaceDescriptorPayload[]>;
57
65
  matchesFilter(input: {