@hyperdrive.bot/fleet-server 0.3.159 → 0.3.160

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 (48) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +29 -0
  2. package/dist/server/server/agent/agent-manager.js +69 -5
  3. package/dist/server/server/agent/agent-timeline-store.d.ts +10 -0
  4. package/dist/server/server/agent/agent-timeline-store.js +54 -6
  5. package/dist/server/server/agent/blocker-log.d.ts +3 -1
  6. package/dist/server/server/agent/blocker-log.js +17 -5
  7. package/dist/server/server/agent/card-move-log.d.ts +3 -1
  8. package/dist/server/server/agent/card-move-log.js +21 -14
  9. package/dist/server/server/agent/import-sessions.js +1 -1
  10. package/dist/server/server/agent/jsonl-card-index.d.ts +53 -0
  11. package/dist/server/server/agent/jsonl-card-index.js +138 -0
  12. package/dist/server/server/agent/lifecycle-command.d.ts +18 -1
  13. package/dist/server/server/agent/lifecycle-command.js +21 -2
  14. package/dist/server/server/agent/mcp-shared.d.ts +1 -0
  15. package/dist/server/server/agent/provider-snapshot-manager.d.ts +9 -0
  16. package/dist/server/server/agent/provider-snapshot-manager.js +38 -1
  17. package/dist/server/server/agent/structured-generation-providers.d.ts +2 -1
  18. package/dist/server/server/agent/structured-generation-providers.js +21 -8
  19. package/dist/server/server/fleet/decision-service.d.ts +8 -0
  20. package/dist/server/server/fleet/decision-service.js +24 -1
  21. package/dist/server/server/fleet/fleet-controls.js +2 -1
  22. package/dist/server/server/fleet/fleet-reader.d.ts +12 -0
  23. package/dist/server/server/fleet/fleet-reader.js +48 -2
  24. package/dist/server/server/schedule/service.d.ts +4 -0
  25. package/dist/server/server/schedule/service.js +26 -5
  26. package/dist/server/server/schedule/store.d.ts +51 -0
  27. package/dist/server/server/schedule/store.js +184 -16
  28. package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +16 -1
  29. package/dist/server/server/session/agent-updates/agent-updates-service.js +59 -2
  30. package/dist/server/server/session/provider/provider-catalog-session.d.ts +8 -0
  31. package/dist/server/server/session/provider/provider-catalog-session.js +21 -1
  32. package/dist/server/server/session.d.ts +16 -0
  33. package/dist/server/server/session.js +115 -19
  34. package/dist/server/server/websocket-server.d.ts +5 -0
  35. package/dist/server/server/websocket-server.js +10 -0
  36. package/dist/server/server/workspace-directory.d.ts +4 -1
  37. package/dist/server/server/workspace-directory.js +3 -2
  38. package/dist/server/web-ui/_expo/static/js/web/{index-1420ca2d21c343afea2c8e2da752cd54.js → index-29775d90476d32ca65921d262ae06baa.js} +6 -6
  39. package/dist/server/web-ui/_expo/static/js/web/index-29775d90476d32ca65921d262ae06baa.js.br +0 -0
  40. package/dist/server/web-ui/_expo/static/js/web/index-29775d90476d32ca65921d262ae06baa.js.gz +0 -0
  41. package/dist/server/web-ui/_expo/static/js/web/{index-1420ca2d21c343afea2c8e2da752cd54.js.map.br → index-29775d90476d32ca65921d262ae06baa.js.map.br} +0 -0
  42. package/dist/server/web-ui/_expo/static/js/web/{index-1420ca2d21c343afea2c8e2da752cd54.js.map.gz → index-29775d90476d32ca65921d262ae06baa.js.map.gz} +0 -0
  43. package/dist/server/web-ui/index.html +1 -1
  44. package/dist/server/web-ui/index.html.br +0 -0
  45. package/dist/server/web-ui/index.html.gz +0 -0
  46. package/package.json +6 -6
  47. package/dist/server/web-ui/_expo/static/js/web/index-1420ca2d21c343afea2c8e2da752cd54.js.br +0 -0
  48. package/dist/server/web-ui/_expo/static/js/web/index-1420ca2d21c343afea2c8e2da752cd54.js.gz +0 -0
@@ -426,6 +426,57 @@ function fileAttachmentErrorMessage(error, fileId) {
426
426
  return `Attachment file is no longer available: ${fileId}`;
427
427
  }
428
428
  }
429
+ /**
430
+ * Stored-agent payload projection, memoized per record object and shared by
431
+ * every Session in the daemon. AgentStorage replaces a record object on every
432
+ * write (never mutates it), so identity plus the other projection inputs
433
+ * (registered providers, schedule facts) is an exact key. Before this, every
434
+ * agent_state event re-projected the same stored records once per connected
435
+ * client. Payloads are shared: treat them as read-only.
436
+ */
437
+ const storedAgentPayloadMemo = new WeakMap();
438
+ /**
439
+ * The stored records a workspace descriptor for `workspaceIds` can depend on:
440
+ * records owned by one of those workspaces, running delegated records (they roll
441
+ * up to their delegation root, see WorkspaceDirectory), and the ancestor chain of
442
+ * every delegated record or live agent so the root can be resolved.
443
+ */
444
+ export function selectRecordsForWorkspaces(records, liveAgents, workspaceIds) {
445
+ const selected = new Set();
446
+ const pendingParentIds = [];
447
+ for (const record of records) {
448
+ const parentId = getParentAgentIdFromLabels(record.labels);
449
+ const owned = record.workspaceId !== undefined && workspaceIds.has(record.workspaceId);
450
+ if (owned || (parentId !== null && record.lastStatus === "running")) {
451
+ selected.add(record);
452
+ if (parentId)
453
+ pendingParentIds.push(parentId);
454
+ }
455
+ }
456
+ for (const agent of liveAgents) {
457
+ const parentId = getParentAgentIdFromLabels(agent.labels);
458
+ if (parentId)
459
+ pendingParentIds.push(parentId);
460
+ }
461
+ if (pendingParentIds.length > 0) {
462
+ const byId = new Map(records.map((record) => [record.id, record]));
463
+ const seen = new Set();
464
+ while (pendingParentIds.length > 0) {
465
+ const id = pendingParentIds.pop();
466
+ if (seen.has(id))
467
+ continue;
468
+ seen.add(id);
469
+ const parent = byId.get(id);
470
+ if (!parent)
471
+ continue;
472
+ selected.add(parent);
473
+ const next = getParentAgentIdFromLabels(parent.labels);
474
+ if (next)
475
+ pendingParentIds.push(next);
476
+ }
477
+ }
478
+ return records.filter((record) => selected.has(record));
479
+ }
429
480
  /**
430
481
  * Session represents a single connected client session.
431
482
  * It owns all state management, orchestration logic, and message processing.
@@ -727,7 +778,7 @@ export class Session {
727
778
  logger: this.sessionLogger,
728
779
  projectRegistry: this.projectRegistry,
729
780
  workspaceRegistry: this.workspaceRegistry,
730
- listAgentPayloads: () => this.listAgentPayloads(),
781
+ listAgentPayloads: (filter) => this.listAgentPayloads(filter),
731
782
  listTerminalActivityContributions: () => this.listTerminalActivityContributions(),
732
783
  isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
733
784
  buildWorkspaceDescriptor: (input) => this.buildWorkspaceDescriptor(input),
@@ -1079,14 +1130,24 @@ export class Session {
1079
1130
  payload.snooze = storedRecord?.snooze ?? null;
1080
1131
  return payload;
1081
1132
  }
1082
- buildStoredAgentPayload(record, registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds())) {
1133
+ buildStoredAgentPayload(record, registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds()), providersKey) {
1083
1134
  const nextFireAt = this.scheduleService.peekNextFireAt(record.id);
1084
1135
  const hasRoutine = this.scheduleService.peekHasRoutine(record.id);
1136
+ const memoKey = providersKey ?? [...registeredProviderIds].join("\u0000");
1137
+ const memo = storedAgentPayloadMemo.get(record);
1138
+ if (memo &&
1139
+ memo.providersKey === memoKey &&
1140
+ memo.nextFireAt === nextFireAt &&
1141
+ memo.hasRoutine === hasRoutine) {
1142
+ return memo.payload;
1143
+ }
1085
1144
  const scheduleOptions = {
1086
1145
  ...(nextFireAt !== undefined ? { nextFireAt } : {}),
1087
1146
  ...(hasRoutine !== undefined ? { hasRoutine } : {}),
1088
1147
  };
1089
- return buildStoredAgentPayload(record, registeredProviderIds, Object.keys(scheduleOptions).length > 0 ? scheduleOptions : undefined);
1148
+ const payload = buildStoredAgentPayload(record, registeredProviderIds, Object.keys(scheduleOptions).length > 0 ? scheduleOptions : undefined);
1149
+ storedAgentPayloadMemo.set(record, { providersKey: memoKey, nextFireAt, hasRoutine, payload });
1150
+ return payload;
1090
1151
  }
1091
1152
  isProviderVisibleToClient(provider) {
1092
1153
  if (clientSupportsAllProviders(this.appVersion)) {
@@ -2551,7 +2612,7 @@ export class Session {
2551
2612
  }
2552
2613
  async handleArchiveAgentRequest(agentId, requestId) {
2553
2614
  this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`);
2554
- const { archivedAt } = await this.archiveAgentForClose(agentId);
2615
+ const { archivedAt, background } = await this.archiveAgentForClose(agentId);
2555
2616
  this.emit({
2556
2617
  type: "agent_archived",
2557
2618
  payload: {
@@ -2560,6 +2621,8 @@ export class Session {
2560
2621
  requestId,
2561
2622
  },
2562
2623
  });
2624
+ // Teardown and the workspace delta finish after the reply; failures are logged.
2625
+ void background;
2563
2626
  }
2564
2627
  /**
2565
2628
  * List background shells -- one agent's, or every agent's when `agentId` is
@@ -2711,19 +2774,44 @@ export class Session {
2711
2774
  payload: { agentId, taskId, dismissed, requestId },
2712
2775
  });
2713
2776
  }
2777
+ /**
2778
+ * Persist the archive and emit the archived agent, then hand back the rest
2779
+ * (process teardown, then the workspace delta) as `background` so the caller
2780
+ * can reply first. Stopping a busy agent and rebuilding its workspace used to
2781
+ * sit in front of the reply.
2782
+ */
2714
2783
  async archiveAgentForClose(agentId) {
2715
- const { archivedAt, record: archivedRecord } = await archiveAgentCommand({
2784
+ const { archivedAt, record: archivedRecord, teardown, } = await archiveAgentCommand({
2716
2785
  agentManager: this.agentManager,
2717
2786
  agentStorage: this.agentStorage,
2718
2787
  logger: this.sessionLogger,
2719
- }, agentId);
2720
- if (this.agentUpdates.hasSubscription()) {
2721
- const payload = await this.agentUpdates.emitStoredRecord(archivedRecord);
2722
- if (payload.workspaceId) {
2788
+ }, agentId, { deferTeardown: true });
2789
+ const payload = this.agentUpdates.hasSubscription()
2790
+ ? await this.agentUpdates.emitStoredRecord(archivedRecord)
2791
+ : null;
2792
+ const background = (async () => {
2793
+ const teardownError = await teardown;
2794
+ if (teardownError) {
2795
+ // Surface it: clients otherwise hold an archived record whose process is
2796
+ // still alive, and the only trace would be a daemon log line.
2797
+ this.emit({
2798
+ type: "rpc_error",
2799
+ payload: {
2800
+ requestId: `archive-teardown:${agentId}`,
2801
+ requestType: "archive_agent_request",
2802
+ error: `Archived, but stopping the agent failed: ${teardownError.message}`,
2803
+ code: "archive_teardown_failed",
2804
+ },
2805
+ });
2806
+ await this.emitAgentUpsertFromStorage(agentId);
2807
+ }
2808
+ if (payload?.workspaceId) {
2723
2809
  await this.emitWorkspaceUpdateForWorkspaceId(payload.workspaceId);
2724
2810
  }
2725
- }
2726
- return { agentId, archivedAt };
2811
+ })().catch((error) => {
2812
+ this.sessionLogger.error({ err: error, agentId }, "Failed to emit workspace update after archiving agent");
2813
+ });
2814
+ return { agentId, archivedAt, background };
2727
2815
  }
2728
2816
  async handleDetachAgentRequest(agentId, requestId) {
2729
2817
  this.sessionLogger.info({ agentId, requestId }, "Detaching agent from parent");
@@ -2775,10 +2863,12 @@ export class Session {
2775
2863
  async handleCloseItemsRequest(msg) {
2776
2864
  const archiveResults = await Promise.allSettled(msg.agentIds.map((agentId) => this.archiveAgentForClose(agentId)));
2777
2865
  const agents = [];
2866
+ const background = [];
2778
2867
  for (let i = 0; i < archiveResults.length; i += 1) {
2779
2868
  const result = archiveResults[i];
2780
2869
  if (result.status === "fulfilled") {
2781
- agents.push(result.value);
2870
+ agents.push({ agentId: result.value.agentId, archivedAt: result.value.archivedAt });
2871
+ background.push(result.value.background);
2782
2872
  }
2783
2873
  else {
2784
2874
  this.sessionLogger.warn({ err: result.reason, agentId: msg.agentIds[i], requestId: msg.requestId }, "Failed to archive agent during close_items batch");
@@ -2805,6 +2895,8 @@ export class Session {
2805
2895
  requestId: msg.requestId,
2806
2896
  },
2807
2897
  });
2898
+ // Teardown and the workspace deltas finish after the reply; failures are logged.
2899
+ void Promise.all(background);
2808
2900
  }
2809
2901
  async unarchiveAgentByHandle(handle) {
2810
2902
  const records = await this.agentStorage.list();
@@ -3585,7 +3677,7 @@ export class Session {
3585
3677
  this.sessionLogger.warn({ err, agentId: snapshot.id, resumeSessionId }, "resume timeline hydration failed");
3586
3678
  }
3587
3679
  }
3588
- await this.agentUpdates.forwardLiveAgent(snapshot);
3680
+ await this.agentUpdates.forwardLiveAgent(snapshot, { immediate: true });
3589
3681
  if (createdDirectoryWorkspaceForAgent && trimmedPrompt) {
3590
3682
  this.workspaceAutoName.scheduleForDirectory({
3591
3683
  workspaceId,
@@ -3664,8 +3756,8 @@ export class Session {
3664
3756
  const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
3665
3757
  await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
3666
3758
  await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
3667
- await this.agentUpdates.forwardLiveAgent(snapshot);
3668
- const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
3759
+ await this.agentUpdates.forwardLiveAgent(snapshot, { immediate: true });
3760
+ const timelineSize = this.agentManager.getTimelineSize(snapshot.id);
3669
3761
  if (requestId) {
3670
3762
  const agentPayload = await this.buildAgentPayload(snapshot);
3671
3763
  this.emit({
@@ -3805,8 +3897,8 @@ export class Session {
3805
3897
  snapshot = await this.agentManager.resumeAgentFromPersistence(handle, buildConfigOverrides(record), agentId, extractTimestamps(record));
3806
3898
  }
3807
3899
  await this.agentManager.hydrateTimelineFromProvider(agentId);
3808
- await this.agentUpdates.forwardLiveAgent(snapshot);
3809
- const timelineSize = this.agentManager.getTimeline(agentId).length;
3900
+ await this.agentUpdates.forwardLiveAgent(snapshot, { immediate: true });
3901
+ const timelineSize = this.agentManager.getTimelineSize(agentId);
3810
3902
  if (requestId) {
3811
3903
  this.emit({
3812
3904
  type: "status",
@@ -4855,9 +4947,13 @@ export class Session {
4855
4947
  const liveAgents = await Promise.all(agentSnapshots.map((agent) => this.buildAgentPayload(agent)));
4856
4948
  // Add persisted agents that have not been lazily initialized yet
4857
4949
  // (excluding internal agents which are for ephemeral system tasks)
4858
- const registryRecords = await this.agentStorage.list();
4950
+ const allRegistryRecords = await this.agentStorage.list();
4951
+ const registryRecords = filter?.workspaceIds
4952
+ ? selectRecordsForWorkspaces(allRegistryRecords, agentSnapshots, filter.workspaceIds)
4953
+ : allRegistryRecords;
4859
4954
  const liveIds = new Set(agentSnapshots.map((a) => a.id));
4860
4955
  const registeredProviderIds = new Set(this.providerSnapshotManager.listRegisteredProviderIds());
4956
+ const providersKey = [...registeredProviderIds].join("\u0000");
4861
4957
  const persistedAgents = registryRecords
4862
4958
  .filter((record) => !liveIds.has(record.id) && !record.internal)
4863
4959
  // Keep raw-record filters ahead of projection; seeded homes can carry thousands of archived agents.
@@ -4865,7 +4961,7 @@ export class Session {
4865
4961
  .filter((record) => labelEntries.every(([key, value]) => record.labels?.[key] === value))
4866
4962
  .filter((record) => filter?.includeUnavailablePersisted === true ||
4867
4963
  isStoredAgentProviderAvailable(record, registeredProviderIds))
4868
- .map((record) => this.buildStoredAgentPayload(record, registeredProviderIds));
4964
+ .map((record) => this.buildStoredAgentPayload(record, registeredProviderIds, providersKey));
4869
4965
  let agents = [...liveAgents, ...persistedAgents];
4870
4966
  agents = agents.filter((agent) => this.isProviderVisibleToClient(agent.provider));
4871
4967
  if (!includeArchived) {
@@ -50,6 +50,11 @@ interface WebSocketLike {
50
50
  on: (event: "message" | "close" | "error", listener: (...args: unknown[]) => void) => void;
51
51
  once: (event: "close" | "error", listener: (...args: unknown[]) => void) => void;
52
52
  }
53
+ /**
54
+ * Requests designed to block until something happens (a long-poll). They stay
55
+ * out of the request latency stats and the slow-request log.
56
+ */
57
+ export declare const LONG_POLL_REQUEST_TYPES: ReadonlySet<string>;
53
58
  export declare class MissingDaemonVersionError extends Error {
54
59
  constructor();
55
60
  }
@@ -197,6 +197,11 @@ function getBrowserHostCapability(capabilities) {
197
197
  return parsed.success ? parsed.data : null;
198
198
  }
199
199
  const SLOW_REQUEST_THRESHOLD_MS = 500;
200
+ /**
201
+ * Requests designed to block until something happens (a long-poll). They stay
202
+ * out of the request latency stats and the slow-request log.
203
+ */
204
+ export const LONG_POLL_REQUEST_TYPES = new Set(["wait_for_finish_request"]);
200
205
  const EXTERNAL_SESSION_DISCONNECT_GRACE_MS = 90000;
201
206
  const HELLO_TIMEOUT_MS = 15000;
202
207
  const WS_CLOSE_HELLO_TIMEOUT = 4001;
@@ -1385,6 +1390,11 @@ export class VoiceAssistantWebSocketServer {
1385
1390
  }
1386
1391
  const startMs = performance.now();
1387
1392
  await activeConnection.session.handleMessage(message.message);
1393
+ if (LONG_POLL_REQUEST_TYPES.has(message.message.type)) {
1394
+ // Blocking until the agent finishes IS the contract; counting it as request
1395
+ // latency (or as a slow request) reports the design as a regression.
1396
+ return;
1397
+ }
1388
1398
  const durationMs = performance.now() - startMs;
1389
1399
  this.recordRequestLatency(message.message.type, durationMs);
1390
1400
  if (durationMs >= SLOW_REQUEST_THRESHOLD_MS) {
@@ -21,7 +21,10 @@ export interface WorkspaceDirectoryDeps {
21
21
  workspaceRegistry: {
22
22
  list(): Promise<PersistedWorkspaceRecord[]>;
23
23
  };
24
- listAgentPayloads(): Promise<AgentSnapshotPayload[]>;
24
+ /** With `workspaceIds`, may omit stored agents no descriptor for them depends on. */
25
+ listAgentPayloads(options?: {
26
+ workspaceIds?: ReadonlySet<string>;
27
+ }): Promise<AgentSnapshotPayload[]>;
25
28
  listTerminalActivityContributions(): Promise<Array<{
26
29
  cwd: string;
27
30
  workspaceId?: string;
@@ -108,8 +108,9 @@ export class WorkspaceDirectory {
108
108
  }
109
109
  }
110
110
  async buildDescriptorMap(options) {
111
+ const requestedWorkspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
111
112
  const [agents, persistedWorkspaces, persistedProjects, terminalContributions] = await Promise.all([
112
- this.deps.listAgentPayloads(),
113
+ this.deps.listAgentPayloads(requestedWorkspaceIds ? { workspaceIds: requestedWorkspaceIds } : undefined),
113
114
  this.deps.workspaceRegistry.list(),
114
115
  this.deps.projectRegistry.list(),
115
116
  this.deps.listTerminalActivityContributions(),
@@ -120,7 +121,7 @@ export class WorkspaceDirectory {
120
121
  const archivedProjectIds = new Set(persistedProjects.filter((project) => project.archivedAt).map((project) => project.projectId));
121
122
  const activeRecords = persistedWorkspaces.filter((workspace) => !workspace.archivedAt && !archivedProjectIds.has(workspace.projectId));
122
123
  const descriptorsByWorkspaceId = new Map();
123
- const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
124
+ const workspaceIds = requestedWorkspaceIds;
124
125
  const activeWorkspaceIds = new Set(activeRecords.map((workspace) => workspace.workspaceId));
125
126
  const includedWorkspaces = activeRecords.filter((workspace) => !workspaceIds || workspaceIds.has(workspace.workspaceId));
126
127
  const activeRecordsByWorkspaceId = new Map(activeRecords.map((workspace) => [workspace.workspaceId, workspace]));