@hyperdrive.bot/fleet-server 0.3.126 → 0.3.128

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.
@@ -1146,7 +1146,33 @@ export class Session {
1146
1146
  const record = await this.agentStorage.get(agentId);
1147
1147
  if (!record)
1148
1148
  return;
1149
- const agent = this.buildStoredAgentPayload(record);
1149
+ /*
1150
+ * The LIVE payload when the agent is live.
1151
+ *
1152
+ * This always built the stored one, and `buildStoredAgentPayload`
1153
+ * hardcodes `pendingPermissions: []`, `availableModes: []` and the
1154
+ * default capabilities, because a stored record genuinely has none of
1155
+ * those. Broadcasting that for a LIVE agent is a lie the app believes:
1156
+ * `writeStoredMetadata` stamps a strictly newer `updatedAt`, so the
1157
+ * client's staleness guard waves it through, and
1158
+ * `applyAuthoritativeAgentSnapshot` then deletes the agent's permission
1159
+ * keys because the incoming list is empty.
1160
+ *
1161
+ * The user-visible cost was specific and bad: a permission-blocked
1162
+ * session ranks FIRST in the triage ring, and snoozing it - an ordinary
1163
+ * move - erased the approval prompt app-wide, dropped the agent out of
1164
+ * `needs_input`, and downgraded its mode list. The daemon stays blocked
1165
+ * waiting on that approval and a blocked agent emits nothing further, so
1166
+ * only a reconnect brought it back.
1167
+ *
1168
+ * Introduced by the round-35 broadcast fix, which is the shape this log
1169
+ * keeps naming: a fix verified against the case that motivated it (an
1170
+ * idle session) and not the space around it (a live one).
1171
+ */
1172
+ const liveAgent = this.agentManager.getAgent(agentId);
1173
+ const agent = liveAgent
1174
+ ? await this.buildAgentPayload(liveAgent)
1175
+ : this.buildStoredAgentPayload(record);
1150
1176
  // `workspaceId` is optional on the record: an agent outside a workspace has
1151
1177
  // no project placement, and the payload's `project` is optional too.
1152
1178
  const project = record.workspaceId
@@ -2866,10 +2892,60 @@ export class Session {
2866
2892
  await this.agentManager.updateAgentMetadata(winnerId, {
2867
2893
  digest: { ...winnerDigest, links: [...(winnerDigest.links ?? []), ...newLinks] },
2868
2894
  });
2895
+ /*
2896
+ * `archiveAgentCommand`, like every other archive caller in the repo.
2897
+ *
2898
+ * This called `agentManager.archiveAgent` directly, whose first statement
2899
+ * is `requireAgent`, which THROWS `Unknown agent '<id>'` when the agent is
2900
+ * not in memory. Most of a triage backlog is exactly that, and consolidate
2901
+ * is the verb the refused-snooze copy points the user at: the hold
2902
+ * completed, the sheet showed a raw uuid in an error, and nothing was
2903
+ * archived.
2904
+ *
2905
+ * Worse on a mixed set: the links above were already written, on the
2906
+ * winner AND on every source, so the live sources got archived, the
2907
+ * stored-only ones did not, and the response said `accepted: false`. That
2908
+ * is the half-consolidated fleet the single-call design exists to
2909
+ * prevent, and a retry appends the links again because they are not
2910
+ * deduped.
2911
+ *
2912
+ * The command branches on `getAgent()` and falls back to
2913
+ * `archiveStoredAgent`, which is the same fallback `archiveAgentForClose`
2914
+ * uses a few hundred lines up.
2915
+ */
2916
+ const archivedAtById = new Map();
2869
2917
  for (const sourceId of sources) {
2870
- await this.agentManager.archiveAgent(sourceId);
2918
+ const { archivedAt } = await archiveAgentCommand({
2919
+ agentManager: this.agentManager,
2920
+ agentStorage: this.agentStorage,
2921
+ logger: this.sessionLogger,
2922
+ }, sourceId);
2923
+ archivedAtById.set(sourceId, archivedAt);
2871
2924
  archivedIds.push(sourceId);
2872
2925
  }
2926
+ /*
2927
+ * Tell every client the sources are archived.
2928
+ *
2929
+ * This emitted only `agent.consolidate.response`, which is point to
2930
+ * point: nothing set `archivedAt` in the app's store, so
2931
+ * `selectFeedCandidates`' archived clause could not drop them and the N
2932
+ * sources came back in the ring - the exact outcome
2933
+ * `reels-feed-source.ts` says it closed. `handleArchiveAgentRequest`
2934
+ * emits `agent_archived` for the single-session case; consolidate is the
2935
+ * batch of the same operation and owes the same event.
2936
+ */
2937
+ for (const sourceId of archivedIds) {
2938
+ const archivedAt = archivedAtById.get(sourceId);
2939
+ if (!archivedAt)
2940
+ continue;
2941
+ // `requestId` is the consolidate request's: the archive is part of it,
2942
+ // and the schema requires the correlation id.
2943
+ this.emit({
2944
+ type: "agent_archived",
2945
+ payload: { agentId: sourceId, archivedAt, requestId },
2946
+ });
2947
+ }
2948
+ await this.emitAgentUpsertFromStorage(winnerId);
2873
2949
  this.emit({
2874
2950
  type: "agent.consolidate.response",
2875
2951
  payload: { requestId, winnerId, accepted: true, archivedIds, error: null },
@@ -3057,6 +3133,25 @@ export class Session {
3057
3133
  });
3058
3134
  return;
3059
3135
  }
3136
+ /*
3137
+ * Broadcast it, exactly like the snooze and decision handlers.
3138
+ *
3139
+ * `emitAgentUpsertFromStorage`'s own docblock names a kanban move on an
3140
+ * idle session as one of the two symptoms it exists to fix, and then only
3141
+ * the decision half was wired. This handler is the SOLE path for
3142
+ * `setKanban` and `setTags`, and it emitted a point-to-point response and
3143
+ * nothing else. For an agent the daemon has not loaded,
3144
+ * `updateAgentMetadata` falls through to `writeStoredMetadata`, which
3145
+ * emits nothing at all.
3146
+ *
3147
+ * What that cost, beyond the stale chip: reels reads the WIP occupancy
3148
+ * from the same store labels, so a column at its limit kept counting the
3149
+ * cards it had before the moves, and the gate that was added to refuse a
3150
+ * fourth card went blind for the rest of the sitting. Nothing recovers
3151
+ * within a session: the authoritative revalidation fires on connection
3152
+ * events, not on a timer.
3153
+ */
3154
+ await this.emitAgentUpsertFromStorage(agentId);
3060
3155
  this.emit({
3061
3156
  type: "update_agent_response",
3062
3157
  payload: { requestId, agentId, accepted: true, error: null },