@haywoodmarx/agent-pager 0.2.0 → 0.2.1

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.
@@ -1,7 +1,20 @@
1
- import { archivedOperatorRepositoryIds, selectOperatorAgentHealth, } from "../operator/agent-projection.js";
1
+ import { archivedOperatorRepositoryIds, isOperatorAgentArchived, selectOperatorAgentHealth, } from "../operator/agent-projection.js";
2
2
  import { OPERATOR_PROTOCOL_VERSION, } from "../operator/protocol.js";
3
3
  import { classifyOperatorEvent, validateOperatorSnapshot } from "../operator/validation.js";
4
4
  import { isTuiExpirable } from "../operator/agent-projection.js";
5
+ /**
6
+ * List the agents a purge confirmation still deletes.
7
+ * @param target - What the confirmation names.
8
+ * @param agents - Agents the current projection holds.
9
+ * @returns - The named agent while it is projected, or those the archive section held when the
10
+ * confirmation opened that are still archived in that workspace.
11
+ */
12
+ export function selectTuiPurgeableAgents(target, agents) {
13
+ if (target.kind === "agent")
14
+ return agents.filter((agent) => agent.id === target.agentId);
15
+ const named = new Set(target.agentIds);
16
+ return agents.filter((agent) => named.has(agent.id) && agent.repository.repositoryId === target.repositoryId && isOperatorAgentArchived(agent));
17
+ }
5
18
  /* eslint-enable jsdoc/require-jsdoc -- The state field declarations end here. */
6
19
  /** Entries the diagnostics log keeps before discarding its oldest. */
7
20
  const TUI_DIAGNOSTIC_LIMIT = 200;
@@ -657,14 +670,19 @@ function reconcileView(view, snapshot, previous) {
657
670
  const messageIds = acceptedIds(snapshot.traffic);
658
671
  const trafficIds = new Set(snapshot.traffic.map((traffic) => traffic.trafficId));
659
672
  const hiddenRepositories = new Set([...view.hiddenRepositories].filter((id) => repositoryIds.has(id)));
660
- const selection = selectionSurviving(view.selection, repositoryIds, agentIds, hiddenRepositories);
673
+ // A repository with no agent keeps its view state but has no row to select.
674
+ const occupied = new Set(snapshot.agents.map((agent) => agent.repository.repositoryId));
675
+ const sections = new Set(snapshot.agents.filter(isOperatorAgentArchived).map((agent) => agent.repository.repositoryId));
676
+ const selection = selectionSurviving(view.selection, occupied, sections, agentIds, new Set([...hiddenRepositories].filter((id) => occupied.has(id))));
661
677
  const trafficId = view.trafficId !== null && !trafficIds.has(view.trafficId) ? null : view.trafficId;
662
678
  return {
663
679
  ...view,
664
680
  selection,
665
681
  trafficId,
666
- overlay: reconcileOverlay(view.overlay, repositoryIds, agentIds, messageIds, expirableIds(snapshot.traffic)),
667
- collapsedRepositories: new Set([...collapsedRepositories].filter((id) => repositoryIds.has(id))),
682
+ overlay: reconcileOverlay(view.overlay, snapshot.agents, agentIds, occupied, sections, messageIds, expirableIds(snapshot.traffic)),
683
+ // An empty workspace's collapse is dropped, so one that was collapsed for being archived opens for
684
+ // the agent that next starts in it.
685
+ collapsedRepositories: new Set([...collapsedRepositories].filter((id) => occupied.has(id))),
668
686
  hiddenRepositories,
669
687
  archivedSections: new Map([...view.archivedSections].filter(([id]) => repositoryIds.has(id))),
670
688
  };
@@ -672,16 +690,18 @@ function reconcileView(view, snapshot, previous) {
672
690
  /**
673
691
  * Keep a tree selection only while the row it names can still be shown.
674
692
  * @param selection - Selection held before the projection changed.
675
- * @param repositoryIds - Repositories the new projection holds.
693
+ * @param repositoryIds - Repositories the new projection shows a row for.
694
+ * @param archivedSections - Repositories the new projection shows an archive section for.
676
695
  * @param agentIds - Agents the new projection holds.
677
- * @param hiddenRepositories - Hidden repositories that survive in the new projection; the hidden section exists only while any do.
696
+ * @param hiddenRepositories - Hidden repositories the new projection shows; the hidden section exists only while any do.
678
697
  * @returns The same selection, or null when its row is gone.
679
698
  */
680
- function selectionSurviving(selection, repositoryIds, agentIds, hiddenRepositories) {
699
+ function selectionSurviving(selection, repositoryIds, archivedSections, agentIds, hiddenRepositories) {
681
700
  switch (selection?.kind) {
682
701
  case "workspace":
683
- case "archived":
684
702
  return repositoryIds.has(selection.repositoryId) ? selection : null;
703
+ case "archived":
704
+ return archivedSections.has(selection.repositoryId) ? selection : null;
685
705
  case "agent":
686
706
  return agentIds.has(selection.agentId) ? selection : null;
687
707
  case "hidden":
@@ -693,13 +713,15 @@ function selectionSurviving(selection, repositoryIds, agentIds, hiddenRepositori
693
713
  function expirableIds(traffic) {
694
714
  return new Set(traffic.flatMap((entry) => (entry.kind === "accepted_delivery" && isTuiExpirable(entry) ? [entry.messageId] : [])));
695
715
  }
696
- function reconcileOverlay(overlay, repositoryIds, agentIds, messageIds, expirable) {
716
+ function reconcileOverlay(overlay, agents, agentIds, repositoryIds, archivedSections, messageIds, expirable) {
697
717
  const closed = { kind: "none" };
698
718
  switch (overlay.kind) {
699
719
  case "agent_identity":
700
720
  return agentIds.has(overlay.agentId) ? overlay : closed;
701
721
  case "archive_confirm":
702
722
  return agentIds.has(overlay.agentId) ? overlay : closed;
723
+ case "purge_confirm":
724
+ return selectTuiPurgeableAgents(overlay.target, agents).length > 0 ? overlay : closed;
703
725
  case "full_message":
704
726
  return messageIds.has(overlay.messageId) ? overlay : closed;
705
727
  case "expire_confirm":
@@ -710,6 +732,8 @@ function reconcileOverlay(overlay, repositoryIds, agentIds, messageIds, expirabl
710
732
  return agentIds.has(overlay.agentId) ? overlay : closed;
711
733
  if (overlay.target === "message")
712
734
  return messageIds.has(overlay.messageId) ? overlay : closed;
735
+ if (overlay.target === "archived")
736
+ return archivedSections.has(overlay.repositoryId) ? overlay : closed;
713
737
  return repositoryIds.has(overlay.repositoryId) ? overlay : closed;
714
738
  case "none":
715
739
  case "actions":
@@ -119,12 +119,21 @@ export declare function formatDiagnostic(diagnostic: OperatorDiagnostic, width:
119
119
  * An answer is admission, not the resulting state change, so an accepted command is worded as
120
120
  * accepted. A command that got no answer at all is worded as unknown, because the broker may have
121
121
  * applied it.
122
- * @param state - Current projection, which names the agent a command targeted.
122
+ * @param target - What the command acted on, as `describeCommandTarget` named it when the command was submitted.
123
123
  * @param command - Command the operator submitted.
124
124
  * @param result - The broker's admission answer, or null when none came back.
125
125
  * @returns Severity and content-free summary for the log row.
126
126
  */
127
- export declare function describeCommandOutcome(state: TuiOperatorModelState, command: OperatorCommand, result: OperatorCommandResult | null): Pick<OperatorDiagnostic, "severity" | "summary">;
127
+ export declare function describeCommandOutcome(target: string, command: OperatorCommand, result: OperatorCommandResult | null): Pick<OperatorDiagnostic, "severity" | "summary">;
128
+ /**
129
+ * Name what one operator command acts on, in the words the diagnostics log uses.
130
+ *
131
+ * Read at submission: a purge removes its agents from the projection before its answer arrives.
132
+ * @param state - Current projection, which names the agents a command targets while they remain in it.
133
+ * @param command - Command the operator is submitting.
134
+ * @returns - The message id, the agent handle, or the number of agents a multi-agent purge names.
135
+ */
136
+ export declare function describeCommandTarget(state: TuiOperatorModelState, command: OperatorCommand): string;
128
137
  /**
129
138
  * Select the colour role one diagnostic severity carries.
130
139
  * @param severity - Attention the condition asks for.
@@ -135,20 +135,42 @@ export function formatDiagnostic(diagnostic, width) {
135
135
  * An answer is admission, not the resulting state change, so an accepted command is worded as
136
136
  * accepted. A command that got no answer at all is worded as unknown, because the broker may have
137
137
  * applied it.
138
- * @param state - Current projection, which names the agent a command targeted.
138
+ * @param target - What the command acted on, as `describeCommandTarget` named it when the command was submitted.
139
139
  * @param command - Command the operator submitted.
140
140
  * @param result - The broker's admission answer, or null when none came back.
141
141
  * @returns Severity and content-free summary for the log row.
142
142
  */
143
- export function describeCommandOutcome(state, command, result) {
144
- const verb = { ping_agent: "ping", archive_agent: "archive", expire_message: "expire" }[command.type];
143
+ export function describeCommandOutcome(target, command, result) {
144
+ const verb = { ping_agent: "ping", archive_agent: "archive", expire_message: "expire", purge_agents: "purge" }[command.type];
145
145
  if (result === null)
146
146
  return { severity: "error", summary: `${verb} outcome unknown: no answer came back from the broker` };
147
147
  if (result.status === "rejected")
148
148
  return { severity: "warning", summary: `${verb} refused: ${result.reason}` };
149
- const target = command.type === "expire_message" ? command.messageId : agentHandle(state, command.targetAgentId);
150
149
  return { severity: "info", summary: `${verb} accepted for ${target}` };
151
150
  }
151
+ /**
152
+ * Name what one operator command acts on, in the words the diagnostics log uses.
153
+ *
154
+ * Read at submission: a purge removes its agents from the projection before its answer arrives.
155
+ * @param state - Current projection, which names the agents a command targets while they remain in it.
156
+ * @param command - Command the operator is submitting.
157
+ * @returns - The message id, the agent handle, or the number of agents a multi-agent purge names.
158
+ */
159
+ export function describeCommandTarget(state, command) {
160
+ switch (command.type) {
161
+ case "expire_message":
162
+ return command.messageId;
163
+ case "ping_agent":
164
+ case "archive_agent":
165
+ return agentHandle(state, command.targetAgentId);
166
+ case "purge_agents": {
167
+ const [only] = command.targetAgentIds;
168
+ return command.targetAgentIds.length === 1 && only !== undefined
169
+ ? agentHandle(state, only)
170
+ : `${String(command.targetAgentIds.length)} agents`;
171
+ }
172
+ }
173
+ }
152
174
  /**
153
175
  * Select the colour role one diagnostic severity carries.
154
176
  * @param severity - Attention the condition asks for.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haywoodmarx/agent-pager",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Durable local messaging between agent implementations.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",