@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.
- package/dist/server/server/agent/agent-prompt.js +4 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
- package/dist/server/server/agent/agent-storage.js +2 -9
- package/dist/server/server/agent/create-agent/create.js +10 -2
- package/dist/server/server/agent/create-agent-mode.d.ts +3 -8
- package/dist/server/server/agent/create-agent-mode.js +16 -2
- package/dist/server/server/agent/import-sessions.js +1 -1
- package/dist/server/server/agent/provider-snapshot-manager.d.ts +2 -1
- package/dist/server/server/agent/provider-snapshot-manager.js +18 -2
- package/dist/server/server/agent/providers/acp-agent.d.ts +3 -3
- package/dist/server/server/agent/providers/acp-agent.js +18 -13
- package/dist/server/server/agent/providers/codex-app-server-agent.js +16 -22
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.js +69 -2
- package/dist/server/server/agent/providers/opencode-agent.js +19 -8
- package/dist/server/server/agent/timeline-projection.js +30 -1
- package/dist/server/server/atomic-file.d.ts +3 -0
- package/dist/server/server/atomic-file.js +19 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +4 -1
- package/dist/server/server/bootstrap.js +2 -0
- package/dist/server/server/chat/chat-service.js +2 -4
- package/dist/server/server/daemon-keypair.js +2 -2
- package/dist/server/server/loop-service.d.ts +4 -0
- package/dist/server/server/loop-service.js +27 -9
- package/dist/server/server/persisted-config.js +3 -3
- package/dist/server/server/private-files.d.ts +0 -1
- package/dist/server/server/private-files.js +0 -5
- package/dist/server/server/schedule/service.d.ts +6 -0
- package/dist/server/server/schedule/service.js +41 -18
- package/dist/server/server/schedule/store.js +3 -2
- package/dist/server/server/server-id.js +3 -3
- package/dist/server/server/session.d.ts +5 -15
- package/dist/server/server/session.js +184 -107
- package/dist/server/server/speech/providers/local/worker-client.js +1 -11
- package/dist/server/server/workspace-bootstrap-dedupe.d.ts +34 -0
- package/dist/server/server/workspace-bootstrap-dedupe.js +23 -0
- package/dist/server/server/workspace-directory.d.ts +8 -0
- package/dist/server/server/workspace-directory.js +141 -15
- package/dist/server/server/workspace-registry.js +2 -6
- package/dist/server/utils/checkout-git.d.ts +0 -1
- package/dist/server/utils/checkout-git.js +23 -31
- package/dist/src/server/persisted-config.js +3 -3
- package/dist/src/server/private-files.js +0 -5
- package/package.json +9 -7
- package/dist/server/server/editor-targets.d.ts +0 -18
- 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 {
|
|
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.
|
|
1310
|
+
return this.handleLegacyListAvailableEditorsRequest(msg);
|
|
1333
1311
|
case "open_in_editor_request":
|
|
1334
|
-
return this.
|
|
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,
|
|
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
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
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
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
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
|
-
|
|
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: {
|