@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.
- package/README.md +2 -2
- package/dist/src/broker/broker.d.ts +21 -1
- package/dist/src/broker/broker.js +101 -11
- package/dist/src/broker/operator-projection.d.ts +22 -0
- package/dist/src/broker/operator-projection.js +40 -0
- package/dist/src/broker/protocol.d.ts +1 -1
- package/dist/src/broker/protocol.js +1 -1
- package/dist/src/broker/schema.d.ts +2 -2
- package/dist/src/broker/schema.js +12 -3
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/operator/commands.d.ts +2 -0
- package/dist/src/operator/commands.js +57 -21
- package/dist/src/operator/protocol.d.ts +32 -0
- package/dist/src/operator/protocol.js +9 -0
- package/dist/src/operator/transport.js +10 -5
- package/dist/src/operator/updates.js +7 -1
- package/dist/src/operator/validation.js +35 -1
- package/dist/src/tui/agent-tree.d.ts +8 -5
- package/dist/src/tui/agent-tree.js +193 -20
- package/dist/src/tui/app.js +6 -2
- package/dist/src/tui/message-views.js +4 -3
- package/dist/src/tui/model.d.ts +25 -1
- package/dist/src/tui/model.js +33 -9
- package/dist/src/tui/presentation.d.ts +11 -2
- package/dist/src/tui/presentation.js +26 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,9 +52,9 @@ pager status
|
|
|
52
52
|
pager <implementation-id> [--session-name <name>] -- <implementation-arguments>
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The bare implementation form starts or adopts the loopback service automatically. A command already running in a hosted terminal keeps the implementation in that terminal with inherited stdio. Otherwise, it prefers an available discovered terminal provider and falls back to inherited stdio; implementation exit status is propagated to the invoking shell. A session name is also the agent's routable alias and is unique within its workspace across implementations. A session's workspace is the Git repository its working directory belongs to, so the main worktree, every linked worktree, and every subdirectory of them share one; a directory outside any repository is a workspace of its own. Agents in one workspace address each other by bare alias. Supplying it again rejoins that broker identity once its holder's harness has shut down gracefully and its connector has said so, its attachment heartbeat lease has lapsed, or the holder was archived; while the holder is attached, the launch exits non-zero naming the holder and the harness does not start. Omitting it creates an ephemeral identity that is reaped once its attachment heartbeat lease lapses, or, when a conversation key makes it resumable, once it has been detached for the resumable retention window, shortened to the unused window while it has never sent a message or received one addressed to it alone. An ephemeral identity that never attached is reaped once its attach reservations have lapsed. Archiving an identity from the operator TUI ends its attachments and hides it, while messages sent to it wait; attaching again un-archives it. A named identity detached for the named archive window is archived too, and comes back when its conversation attaches or its holder resumes its attachment. Otherwise an archived identity, ephemeral or named, is reaped once the archive retention window has passed since its first archive, and reaping dead-letters its unsettled mail. The first sweep after an upgrade reaps every identity already archived for longer than that window.
|
|
55
|
+
The bare implementation form starts or adopts the loopback service automatically. A command already running in a hosted terminal keeps the implementation in that terminal with inherited stdio. Otherwise, it prefers an available discovered terminal provider and falls back to inherited stdio; implementation exit status is propagated to the invoking shell. A session name is also the agent's routable alias and is unique within its workspace across implementations. A session's workspace is the Git repository its working directory belongs to, so the main worktree, every linked worktree, and every subdirectory of them share one; a directory outside any repository is a workspace of its own. Agents in one workspace address each other by bare alias. Supplying it again rejoins that broker identity once its holder's harness has shut down gracefully and its connector has said so, its attachment heartbeat lease has lapsed, or the holder was archived; while the holder is attached, the launch exits non-zero naming the holder and the harness does not start. Omitting it creates an ephemeral identity that is reaped once its attachment heartbeat lease lapses, or, when a conversation key makes it resumable, once it has been detached for the resumable retention window, shortened to the unused window while it has never sent a message or received one addressed to it alone. An ephemeral identity that never attached is reaped once its attach reservations have lapsed. Archiving an identity from the operator TUI ends its attachments and hides it, while messages sent to it wait; attaching again un-archives it. Purging an identity from the operator TUI, on its own or with others in its workspace's archive section, deletes it and ends any session still attached to it; a message it exchanged with an identity that remains stays in that identity's history, and any still waiting in its inbox is dead-lettered. A named identity detached for the named archive window is archived too, and comes back when its conversation attaches or its holder resumes its attachment. Otherwise an archived identity, ephemeral or named, is reaped once the archive retention window has passed since its first archive, and reaping dead-letters its unsettled mail. The first sweep after an upgrade reaps every identity already archived for longer than that window.
|
|
56
56
|
|
|
57
|
-
The authenticated `whoami` MCP tool returns the caller's identity and the address that reaches it from any workspace. [`src/messaging/tools.ts`](src/messaging/tools.ts) describes what each tool returns. `update_agent_alias` atomically replaces the current alias and durable session name; assigning one to an unnamed agent makes that identity durable. `move_to_workspace` moves an agent into another workspace, keeping its identity and inbox. Prior names and addresses remain reserved. Sending to one is rejected with `recipient_alias_changed` and the current address rather than silently redirecting the message. An alias another agent holds is taken over only with `force`, which hands that agent's identity to the caller, tells its session and ends it, so that session's next call runs as a fresh unnamed agent.
|
|
57
|
+
The authenticated `whoami` MCP tool returns the caller's identity and the address that reaches it from any workspace. [`src/messaging/tools.ts`](src/messaging/tools.ts) describes what each tool returns. `update_agent_alias` atomically replaces the current alias and durable session name; assigning one to an unnamed agent makes that identity durable. `move_to_workspace` moves an agent into another workspace, keeping its identity and inbox. Prior names and addresses remain reserved until the identity is purged. Sending to one is rejected with `recipient_alias_changed` and the current address rather than silently redirecting the message. An alias another agent holds is taken over only with `force`, which hands that agent's identity to the caller, tells its session and ends it, so that session's next call runs as a fresh unnamed agent.
|
|
58
58
|
|
|
59
59
|
The package also exposes its Pi integration as a Pi package. `pi install npm:@haywoodmarx/agent-pager` enables directly started Pi sessions to discover or start the broker and register themselves; `pager pi -- <pi-arguments>` loads the same extension explicitly for a wrapper-managed session.
|
|
60
60
|
|
|
@@ -616,7 +616,8 @@ export declare class Broker {
|
|
|
616
616
|
operationalEvents(): BrokerOperationalEvent[];
|
|
617
617
|
/**
|
|
618
618
|
* Append an operational event recording the outcome of a delivery notification attempt.
|
|
619
|
-
* @param messageId - Message the notification outcome is recorded against
|
|
619
|
+
* @param messageId - Message the notification outcome is recorded against; an outcome for a
|
|
620
|
+
* message deleted while its notification was in flight is dropped.
|
|
620
621
|
* @param outcome - Observed notification result recorded for diagnostics.
|
|
621
622
|
*/
|
|
622
623
|
recordNotification(messageId: string, outcome: NotificationOutcome): void;
|
|
@@ -672,6 +673,25 @@ export declare class Broker {
|
|
|
672
673
|
* @returns - Durable acceptance, reporting whether this call created it.
|
|
673
674
|
*/
|
|
674
675
|
acceptOperatorArchive(commandId: string, agentId: AgentId): BrokerOperatorCommandAcceptance;
|
|
676
|
+
/**
|
|
677
|
+
* Accept one operator purge request, deleting the named identities and their records.
|
|
678
|
+
*
|
|
679
|
+
* A message is deleted with its events and claims only when neither end is an identity that
|
|
680
|
+
* outlives the purge. A message exchanged with one that does stays in that identity's history,
|
|
681
|
+
* naming the purged end by its raw id, and is dead-lettered if it was still waiting in a purged
|
|
682
|
+
* inbox. Operator command records keep the ids they targeted, so a retried command is still
|
|
683
|
+
* answered from its record, and the identity's sessions are kept ended, so a process still
|
|
684
|
+
* attached to it is told it was ended. Claims a purged identity made on a kept message stay as
|
|
685
|
+
* that message's delivery attempts, and operator stream events already published about the
|
|
686
|
+
* identity stay in the replay journal until it trims them. Every other row naming a purged identity
|
|
687
|
+
* is deleted.
|
|
688
|
+
* Workspace rows are kept even when no identity remains in them, because previous addresses
|
|
689
|
+
* into a workspace and the short names of other workspaces are resolved from them.
|
|
690
|
+
* @param commandId - Correlates retries of the same operator request.
|
|
691
|
+
* @param agentIds - Distinct agents the operator asked to purge, in any lifecycle state.
|
|
692
|
+
* @returns - Durable acceptance, reporting whether this call created it.
|
|
693
|
+
*/
|
|
694
|
+
acceptOperatorPurge(commandId: string, agentIds: readonly AgentId[]): BrokerOperatorCommandAcceptance;
|
|
675
695
|
/**
|
|
676
696
|
* Returns the broker snapshot, optionally scoped to one workspace.
|
|
677
697
|
*
|
|
@@ -6,7 +6,7 @@ import { PAGER_DEFAULTS } from "../configuration-defaults.js";
|
|
|
6
6
|
import { publicEpoch } from "./snapshot.js";
|
|
7
7
|
import { formatAgentAddress, normalizeAlias, parseAgentAddress, parseWorkspaceName, uniquePathNames, validateRegistrationSessionName, workspaceIdentity, workspaceTeamId, } from "./address.js";
|
|
8
8
|
import { ATTACHMENT_CHANNELS, BrokerSchema, MESSAGE_STATES, } from "./schema.js";
|
|
9
|
-
import { assertDurableOperatorId, readStoredAgentId, readStoredEnum, BrokerOperatorProjection, } from "./operator-projection.js";
|
|
9
|
+
import { assertDurableOperatorId, encodePurgeTargets, samePurgeTargets, readStoredAgentId, readStoredEnum, BrokerOperatorProjection, } from "./operator-projection.js";
|
|
10
10
|
const notificationDisplay = {
|
|
11
11
|
delivered: { text: "notify delivered", tone: "ok" },
|
|
12
12
|
shed: { text: "notify shed — budget exceeded", tone: "warn" },
|
|
@@ -842,15 +842,20 @@ export class Broker {
|
|
|
842
842
|
const settled = this.#db
|
|
843
843
|
.prepare("select id from messages where state in ('acknowledged', 'expired', 'dead_lettered', 'failed') and created_at <= ?")
|
|
844
844
|
.all(cutoff);
|
|
845
|
-
for (const row of settled)
|
|
846
|
-
|
|
847
|
-
this.#write("delete from message_events where message_id = ?", messageId);
|
|
848
|
-
this.#write("delete from claims where message_id = ?", messageId);
|
|
849
|
-
this.#write("delete from idempotency where message_id = ?", messageId);
|
|
850
|
-
this.#write("delete from messages where id = ?", messageId);
|
|
851
|
-
}
|
|
845
|
+
for (const row of settled)
|
|
846
|
+
this.#deleteMessage(String(row.id));
|
|
852
847
|
});
|
|
853
848
|
}
|
|
849
|
+
/**
|
|
850
|
+
* Delete one message and every row that belongs to it.
|
|
851
|
+
* @param messageId - Message whose events, claims and idempotency keys go with it.
|
|
852
|
+
*/
|
|
853
|
+
#deleteMessage(messageId) {
|
|
854
|
+
this.#write("delete from message_events where message_id = ?", messageId);
|
|
855
|
+
this.#write("delete from claims where message_id = ?", messageId);
|
|
856
|
+
this.#write("delete from idempotency where message_id = ?", messageId);
|
|
857
|
+
this.#write("delete from messages where id = ?", messageId);
|
|
858
|
+
}
|
|
854
859
|
/**
|
|
855
860
|
* Resume an MCP attachment while rotating a lapsed or prior-epoch session generation.
|
|
856
861
|
* @param request - Session, fencing proof, and last-seen generation the caller resumes with.
|
|
@@ -941,8 +946,8 @@ export class Broker {
|
|
|
941
946
|
* Name why a known session no longer resolves.
|
|
942
947
|
* @param row - The session row the caller presented, inactive or past its lease.
|
|
943
948
|
* @returns - `ended` when the newest generation of the row's rotation lineage was deactivated by an
|
|
944
|
-
* operator archive or by a newer attachment of the same identity and channel, otherwise
|
|
945
|
-
* which includes a lineage whose end cause is a claim of its identity.
|
|
949
|
+
* operator archive or purge or by a newer attachment of the same identity and channel, otherwise
|
|
950
|
+
* `expired`, which includes a lineage whose end cause is a claim of its identity.
|
|
946
951
|
*/
|
|
947
952
|
#sessionEndedError(row) {
|
|
948
953
|
const newest = this.#newestGeneration(row);
|
|
@@ -950,6 +955,8 @@ export class Broker {
|
|
|
950
955
|
return new ProtocolError("expired", SESSION_EXPIRED_MESSAGE);
|
|
951
956
|
if (newest.end_cause === "claimed")
|
|
952
957
|
return new ProtocolError("expired", SESSION_EXPIRED_MESSAGE);
|
|
958
|
+
if (newest.end_cause === "purged")
|
|
959
|
+
return new ProtocolError("ended", "MCP session ended because an operator purged this agent");
|
|
953
960
|
const archived = this.#db.prepare("select archived_at from agents where id = ?").get(String(newest.agent_id));
|
|
954
961
|
if (typeof archived?.archived_at === "number")
|
|
955
962
|
return new ProtocolError("ended", "MCP session ended because an operator archived this agent");
|
|
@@ -1533,12 +1540,15 @@ export class Broker {
|
|
|
1533
1540
|
}
|
|
1534
1541
|
/**
|
|
1535
1542
|
* Append an operational event recording the outcome of a delivery notification attempt.
|
|
1536
|
-
* @param messageId - Message the notification outcome is recorded against
|
|
1543
|
+
* @param messageId - Message the notification outcome is recorded against; an outcome for a
|
|
1544
|
+
* message deleted while its notification was in flight is dropped.
|
|
1537
1545
|
* @param outcome - Observed notification result recorded for diagnostics.
|
|
1538
1546
|
*/
|
|
1539
1547
|
recordNotification(messageId, outcome) {
|
|
1540
1548
|
const display = notificationDisplay[outcome];
|
|
1541
1549
|
this.#transaction(() => {
|
|
1550
|
+
if (this.#db.prepare("select 1 as present from messages where id = ?").get(messageId) === undefined)
|
|
1551
|
+
return;
|
|
1542
1552
|
this.#appendEvent(messageId, "notify", `notify_${outcome}`, display.text, display.tone);
|
|
1543
1553
|
});
|
|
1544
1554
|
}
|
|
@@ -1660,6 +1670,86 @@ export class Broker {
|
|
|
1660
1670
|
});
|
|
1661
1671
|
return acceptance;
|
|
1662
1672
|
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Accept one operator purge request, deleting the named identities and their records.
|
|
1675
|
+
*
|
|
1676
|
+
* A message is deleted with its events and claims only when neither end is an identity that
|
|
1677
|
+
* outlives the purge. A message exchanged with one that does stays in that identity's history,
|
|
1678
|
+
* naming the purged end by its raw id, and is dead-lettered if it was still waiting in a purged
|
|
1679
|
+
* inbox. Operator command records keep the ids they targeted, so a retried command is still
|
|
1680
|
+
* answered from its record, and the identity's sessions are kept ended, so a process still
|
|
1681
|
+
* attached to it is told it was ended. Claims a purged identity made on a kept message stay as
|
|
1682
|
+
* that message's delivery attempts, and operator stream events already published about the
|
|
1683
|
+
* identity stay in the replay journal until it trims them. Every other row naming a purged identity
|
|
1684
|
+
* is deleted.
|
|
1685
|
+
* Workspace rows are kept even when no identity remains in them, because previous addresses
|
|
1686
|
+
* into a workspace and the short names of other workspaces are resolved from them.
|
|
1687
|
+
* @param commandId - Correlates retries of the same operator request.
|
|
1688
|
+
* @param agentIds - Distinct agents the operator asked to purge, in any lifecycle state.
|
|
1689
|
+
* @returns - Durable acceptance, reporting whether this call created it.
|
|
1690
|
+
*/
|
|
1691
|
+
acceptOperatorPurge(commandId, agentIds) {
|
|
1692
|
+
assertDurableOperatorId(commandId, "command");
|
|
1693
|
+
const targetId = encodePurgeTargets(agentIds);
|
|
1694
|
+
return this.#transaction(() => {
|
|
1695
|
+
const prior = this.operatorCommand(commandId);
|
|
1696
|
+
if (prior) {
|
|
1697
|
+
if (prior.type !== "purge_agents" || !samePurgeTargets(prior.targetIds, agentIds)) {
|
|
1698
|
+
throw new ProtocolError("conflict", "Operator command already exists");
|
|
1699
|
+
}
|
|
1700
|
+
return { status: "existing", record: prior };
|
|
1701
|
+
}
|
|
1702
|
+
const targets = new Set(agentIds);
|
|
1703
|
+
if (agentIds.length === 0 || targets.size !== agentIds.length)
|
|
1704
|
+
throw new ProtocolError("validation_failed", "Purge targets must be distinct and non-empty");
|
|
1705
|
+
const present = this.#db.prepare("select 1 as present from agents where id = ?");
|
|
1706
|
+
for (const agentId of targets) {
|
|
1707
|
+
if (present.get(agentId) === undefined)
|
|
1708
|
+
throw new ProtocolError("validation_failed", "Unknown agent");
|
|
1709
|
+
}
|
|
1710
|
+
const survivors = new Map();
|
|
1711
|
+
const outlives = (agentId) => {
|
|
1712
|
+
if (typeof agentId !== "string" || targets.has(agentId))
|
|
1713
|
+
return false;
|
|
1714
|
+
let known = survivors.get(agentId);
|
|
1715
|
+
if (known === undefined) {
|
|
1716
|
+
known = present.get(agentId) !== undefined;
|
|
1717
|
+
survivors.set(agentId, known);
|
|
1718
|
+
}
|
|
1719
|
+
return known;
|
|
1720
|
+
};
|
|
1721
|
+
const involving = this.#db.prepare("select id, sender_id, recipient_id, state from messages where sender_id = ? or recipient_id = ? order by sequence");
|
|
1722
|
+
for (const agentId of targets) {
|
|
1723
|
+
const waiting = [];
|
|
1724
|
+
for (const row of involving.all(agentId, agentId)) {
|
|
1725
|
+
if (!outlives(row.sender_id) && !outlives(row.recipient_id))
|
|
1726
|
+
this.#deleteMessage(String(row.id));
|
|
1727
|
+
else if (row.recipient_id === agentId && (row.state === "queued" || row.state === "claimed"))
|
|
1728
|
+
waiting.push(row);
|
|
1729
|
+
}
|
|
1730
|
+
this.#deadLetter(waiting, "dead-lettered — recipient was purged");
|
|
1731
|
+
}
|
|
1732
|
+
for (const agentId of targets) {
|
|
1733
|
+
this.#write("delete from idempotency where agent_id = ? or recipient_id = ?", agentId, agentId);
|
|
1734
|
+
this.#write("delete from alias_history where agent_id = ?", agentId);
|
|
1735
|
+
// Kept, ended, so a process still attached is told it was ended rather than that its session
|
|
1736
|
+
// lapsed, which a connector answers by registering again under the same alias. A session a
|
|
1737
|
+
// claim already ended keeps that cause, which tells its process to register again.
|
|
1738
|
+
this.#write("update sessions set active = 0, end_cause = coalesce(end_cause, 'purged') where agent_id = ?", agentId);
|
|
1739
|
+
this.#write("delete from reservations where agent_id = ?", agentId);
|
|
1740
|
+
this.#write("delete from wakes where agent_id = ?", agentId);
|
|
1741
|
+
this.#write("delete from operator_ping_audits where target_agent_id = ?", agentId);
|
|
1742
|
+
this.#write("update agents set parent_id = null where parent_id = ?", agentId);
|
|
1743
|
+
this.#write("delete from agents where id = ?", agentId);
|
|
1744
|
+
}
|
|
1745
|
+
const acceptedAt = this.#now();
|
|
1746
|
+
this.#write("insert into operator_commands(command_id, type, target_id, accepted_at, control_id) values (?, 'purge_agents', ?, ?, null)", commandId, targetId, acceptedAt);
|
|
1747
|
+
return {
|
|
1748
|
+
status: "created",
|
|
1749
|
+
record: { type: "purge_agents", commandId, targetIds: [...agentIds], acceptedAt },
|
|
1750
|
+
};
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1663
1753
|
/**
|
|
1664
1754
|
* Returns the broker snapshot, optionally scoped to one workspace.
|
|
1665
1755
|
*
|
|
@@ -187,6 +187,15 @@ export type BrokerOperatorCommandRecord = {
|
|
|
187
187
|
targetId: string;
|
|
188
188
|
/** Records broker acceptance time. */
|
|
189
189
|
acceptedAt: number;
|
|
190
|
+
} | {
|
|
191
|
+
/** Identifies an agent-purge request. */
|
|
192
|
+
type: "purge_agents";
|
|
193
|
+
/** Correlates retries of the same request. */
|
|
194
|
+
commandId: string;
|
|
195
|
+
/** Identifies the purged agents, in the order the request named them. */
|
|
196
|
+
targetIds: readonly AgentId[];
|
|
197
|
+
/** Records broker acceptance time. */
|
|
198
|
+
acceptedAt: number;
|
|
190
199
|
};
|
|
191
200
|
/** Distinguishes a newly committed command from an exact durable retry. */
|
|
192
201
|
export type BrokerOperatorCommandAcceptance = {
|
|
@@ -219,6 +228,19 @@ export declare function readStoredAgentId(value: unknown, column: string): Agent
|
|
|
219
228
|
* @returns - The value, narrowed to a validated durable identity string.
|
|
220
229
|
*/
|
|
221
230
|
export declare function assertDurableOperatorId(value: unknown, label: string): string;
|
|
231
|
+
/**
|
|
232
|
+
* Encode the agents one purge names into the single `operator_commands.target_id` column.
|
|
233
|
+
* @param agentIds - Purged agents, in the order the request named them.
|
|
234
|
+
* @returns - Stored text that `decodePurgeTargets` reads back to the same list.
|
|
235
|
+
*/
|
|
236
|
+
export declare function encodePurgeTargets(agentIds: readonly AgentId[]): string;
|
|
237
|
+
/**
|
|
238
|
+
* Report whether two purges name the same agents in the same order, as a retry of one must.
|
|
239
|
+
* @param left - Targets in the order one request listed them; order is significant.
|
|
240
|
+
* @param right - Targets in the order the other request listed them.
|
|
241
|
+
* @returns - Whether a command id recorded for one purge answers the other.
|
|
242
|
+
*/
|
|
243
|
+
export declare function samePurgeTargets(left: readonly AgentId[], right: readonly AgentId[]): boolean;
|
|
222
244
|
/**
|
|
223
245
|
* Reads durable operator-facing state over a broker connection: traffic records, command
|
|
224
246
|
* idempotency, and snapshot projections. Holds the in-memory operational event log, which only this
|
|
@@ -190,6 +190,38 @@ function assertDurableAgentId(value) {
|
|
|
190
190
|
if (!isAgentId(value))
|
|
191
191
|
throw new ProtocolError("validation_failed", "Operator command target is corrupt");
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Encode the agents one purge names into the single `operator_commands.target_id` column.
|
|
195
|
+
* @param agentIds - Purged agents, in the order the request named them.
|
|
196
|
+
* @returns - Stored text that `decodePurgeTargets` reads back to the same list.
|
|
197
|
+
*/
|
|
198
|
+
export function encodePurgeTargets(agentIds) {
|
|
199
|
+
return JSON.stringify(agentIds);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Report whether two purges name the same agents in the same order, as a retry of one must.
|
|
203
|
+
* @param left - Targets in the order one request listed them; order is significant.
|
|
204
|
+
* @param right - Targets in the order the other request listed them.
|
|
205
|
+
* @returns - Whether a command id recorded for one purge answers the other.
|
|
206
|
+
*/
|
|
207
|
+
export function samePurgeTargets(left, right) {
|
|
208
|
+
return left.length === right.length && left.every((agentId, index) => agentId === right[index]);
|
|
209
|
+
}
|
|
210
|
+
function decodePurgeTargets(value) {
|
|
211
|
+
let decoded;
|
|
212
|
+
try {
|
|
213
|
+
decoded = typeof value === "string" ? JSON.parse(value) : null;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
decoded = null;
|
|
217
|
+
}
|
|
218
|
+
if (!Array.isArray(decoded) || decoded.length === 0)
|
|
219
|
+
throw new ProtocolError("validation_failed", "Operator command target is corrupt");
|
|
220
|
+
return decoded.map((entry) => {
|
|
221
|
+
assertDurableAgentId(entry);
|
|
222
|
+
return entry;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
193
225
|
/**
|
|
194
226
|
* Reads durable operator-facing state over a broker connection: traffic records, command
|
|
195
227
|
* idempotency, and snapshot projections. Holds the in-memory operational event log, which only this
|
|
@@ -393,6 +425,14 @@ export class BrokerOperatorProjection {
|
|
|
393
425
|
assertDurableAgentId(row.target_id);
|
|
394
426
|
return { type: "archive_agent", commandId: String(row.command_id), targetId: row.target_id, acceptedAt };
|
|
395
427
|
}
|
|
428
|
+
if (row.type === "purge_agents") {
|
|
429
|
+
return {
|
|
430
|
+
type: "purge_agents",
|
|
431
|
+
commandId: String(row.command_id),
|
|
432
|
+
targetIds: decodePurgeTargets(row.target_id),
|
|
433
|
+
acceptedAt,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
396
436
|
if (row.type !== "expire_message") {
|
|
397
437
|
throw new ProtocolError("validation_failed", "Operator command is corrupt");
|
|
398
438
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Declared here rather than beside the schema it names, because the service lock compares it and is
|
|
5
5
|
* loaded by adapters running inside hosts that have no `bun:sqlite`.
|
|
6
6
|
*/
|
|
7
|
-
export declare const SCHEMA_MARKER = "agent-session-key+message-event-index+queryable-timestamps+no-body-absence-reason+message-type+session-resume-lineage+session-supersession-timestamp+message-type-required+single-delivery-state-home+checked-stored-columns+unorderable-position-index+orderable-position-predicate+no-occupancy-contract+single-session-encoding+no-reservation-bindings+reservation-id-column+repository-workspace+session-end-cause";
|
|
7
|
+
export declare const SCHEMA_MARKER = "agent-session-key+message-event-index+queryable-timestamps+no-body-absence-reason+message-type+session-resume-lineage+session-supersession-timestamp+message-type-required+single-delivery-state-home+checked-stored-columns+unorderable-position-index+orderable-position-predicate+no-occupancy-contract+single-session-encoding+no-reservation-bindings+reservation-id-column+repository-workspace+session-end-cause+operator-purge";
|
|
8
8
|
/**
|
|
9
9
|
* Identifies the only protocol version accepted by this process.
|
|
10
10
|
*
|
|
@@ -5,7 +5,7 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
5
5
|
* Declared here rather than beside the schema it names, because the service lock compares it and is
|
|
6
6
|
* loaded by adapters running inside hosts that have no `bun:sqlite`.
|
|
7
7
|
*/
|
|
8
|
-
export const SCHEMA_MARKER = "agent-session-key+message-event-index+queryable-timestamps+no-body-absence-reason+message-type+session-resume-lineage+session-supersession-timestamp+message-type-required+single-delivery-state-home+checked-stored-columns+unorderable-position-index+orderable-position-predicate+no-occupancy-contract+single-session-encoding+no-reservation-bindings+reservation-id-column+repository-workspace+session-end-cause";
|
|
8
|
+
export const SCHEMA_MARKER = "agent-session-key+message-event-index+queryable-timestamps+no-body-absence-reason+message-type+session-resume-lineage+session-supersession-timestamp+message-type-required+single-delivery-state-home+checked-stored-columns+unorderable-position-index+orderable-position-predicate+no-occupancy-contract+single-session-encoding+no-reservation-bindings+reservation-id-column+repository-workspace+session-end-cause+operator-purge";
|
|
9
9
|
/**
|
|
10
10
|
* Identifies the only protocol version accepted by this process.
|
|
11
11
|
*
|
|
@@ -35,9 +35,9 @@ export declare const MESSAGE_STATES: readonly MessageState[];
|
|
|
35
35
|
/** Names one lifecycle state of an agent; `AGENT_STATES` lists them. */
|
|
36
36
|
export type AgentLifecycleState = "active" | "terminal" | "reaped";
|
|
37
37
|
/** Names why a session was ended on purpose rather than lapsing; `SESSION_END_CAUSES` lists them. */
|
|
38
|
-
export type SessionEndCause = "claimed";
|
|
38
|
+
export type SessionEndCause = "claimed" | "purged";
|
|
39
39
|
/** Every cause `sessions.end_cause` may hold. */
|
|
40
|
-
export declare const SESSION_END_CAUSES: readonly
|
|
40
|
+
export declare const SESSION_END_CAUSES: readonly SessionEndCause[];
|
|
41
41
|
/** Every lifecycle state `agents.state` may hold. */
|
|
42
42
|
export declare const AGENT_STATES: readonly AgentLifecycleState[];
|
|
43
43
|
/** Every channel a reservation or session may occupy. */
|
|
@@ -74,7 +74,7 @@ export const MESSAGE_STATES = vocabulary({
|
|
|
74
74
|
failed: true,
|
|
75
75
|
});
|
|
76
76
|
/** Every cause `sessions.end_cause` may hold. */
|
|
77
|
-
export const SESSION_END_CAUSES = vocabulary({ claimed: true });
|
|
77
|
+
export const SESSION_END_CAUSES = vocabulary({ claimed: true, purged: true });
|
|
78
78
|
/** Every lifecycle state `agents.state` may hold. */
|
|
79
79
|
export const AGENT_STATES = vocabulary({
|
|
80
80
|
active: true,
|
|
@@ -304,7 +304,7 @@ export class BrokerSchema {
|
|
|
304
304
|
[
|
|
305
305
|
"operator_commands",
|
|
306
306
|
commandSql,
|
|
307
|
-
"typein('ping_agent','expire_message','archive_agent')",
|
|
307
|
+
"typein('ping_agent','expire_message','archive_agent','purge_agents')",
|
|
308
308
|
"does not constrain its command types",
|
|
309
309
|
],
|
|
310
310
|
[
|
|
@@ -411,20 +411,29 @@ export class BrokerSchema {
|
|
|
411
411
|
create table if not exists agents(id text primary key, team_id text not null, workspace_id text not null, working_directory text not null, branch text, parent_id text, state text not null ${vocabularyClause("state", AGENT_STATES)}, created_at integer not null, implementation_id text, implementation_version text, registration_name text, registration_ephemeral integer not null default 0, session_key text, archived_at integer, check(implementation_id is not null or implementation_version is null));
|
|
412
412
|
create unique index if not exists agent_registration_name on agents(workspace_id, registration_name) where registration_name is not null and state != 'reaped';
|
|
413
413
|
create unique index if not exists agent_session_key on agents(working_directory, implementation_id, session_key) where session_key is not null and state = 'active';
|
|
414
|
+
create index if not exists agent_parent on agents(parent_id) where parent_id is not null;
|
|
414
415
|
create table if not exists alias_history(id integer primary key autoincrement, workspace_id text not null, alias text not null, agent_id text not null, unique(workspace_id, alias));
|
|
416
|
+
create index if not exists alias_history_agent on alias_history(agent_id);
|
|
415
417
|
create table if not exists reservations(reservation_id text primary key, agent_id text not null, channel text not null ${vocabularyClause("channel", ATTACHMENT_CHANNELS)}, expires_at integer not null, consumed_at integer);
|
|
418
|
+
create index if not exists reservation_agent on reservations(agent_id);
|
|
416
419
|
create table if not exists sessions(id text primary key, agent_id text not null, channel text not null ${vocabularyClause("channel", ATTACHMENT_CHANNELS)}, fencing_token text not null, expires_at integer not null, active integer not null, created_at integer not null, superseded_by text, epoch text not null, superseded_at integer, end_cause text ${vocabularyClause("end_cause", SESSION_END_CAUSES)});
|
|
420
|
+
create index if not exists session_agent on sessions(agent_id);
|
|
417
421
|
create table if not exists traffic_positions(id integer primary key autoincrement);
|
|
418
422
|
create table if not exists messages(id text primary key, team_id text not null, workspace_id text not null, sender_id text not null, recipient_id text not null, addressed_to text not null, address_outcome text not null, subject text not null, type text not null, body text, body_size integer, content_type text, state text not null ${vocabularyClause("state", MESSAGE_STATES)}, created_at integer not null, sequence integer not null, claim_token text, claim_expires_at integer);
|
|
419
423
|
create unique index if not exists message_traffic_position on messages(sequence);
|
|
424
|
+
create index if not exists message_sender on messages(sender_id);
|
|
425
|
+
create index if not exists message_recipient on messages(recipient_id);
|
|
420
426
|
create table if not exists message_events(id integer primary key autoincrement, message_id text not null, at integer not null, source text not null ${vocabularyClause("source", MESSAGE_EVENT_SOURCES)}, type text not null, text text not null, tone text not null ${vocabularyClause("tone", MESSAGE_EVENT_TONES)});
|
|
421
427
|
create index if not exists message_events_message on message_events(message_id, id);
|
|
422
428
|
create table if not exists claims(message_id text not null, token text not null, agent_id text not null, expires_at integer not null, created_at integer not null, primary key(message_id, token));
|
|
423
429
|
create unique index if not exists claim_token on claims(token);
|
|
424
430
|
create table if not exists wakes(agent_id text primary key, attempts integer not null, next_wake_at integer not null);
|
|
425
431
|
create table if not exists idempotency(agent_id text not null, recipient_id text not null, key text not null, message_id text not null, created_at integer not null, primary key(agent_id, recipient_id, key));
|
|
426
|
-
create
|
|
432
|
+
create index if not exists idempotency_recipient on idempotency(recipient_id);
|
|
433
|
+
create index if not exists idempotency_message on idempotency(message_id);
|
|
434
|
+
create table if not exists operator_commands(command_id text primary key, type text not null check(type in ('ping_agent','expire_message','archive_agent','purge_agents')), target_id text not null, accepted_at integer not null, control_id text unique, check((type = 'ping_agent' and control_id is not null) or (type != 'ping_agent' and control_id is null)));
|
|
427
435
|
create table if not exists operator_ping_audits(audit_id text primary key, command_id text not null unique references operator_commands(command_id), control_id text not null unique, target_agent_id text not null, actor text not null check(actor = 'operator'), accepted_at integer not null, traffic_position integer not null unique);
|
|
436
|
+
create index if not exists operator_ping_audit_target on operator_ping_audits(target_agent_id);
|
|
428
437
|
create table if not exists operator_stream_state(singleton integer primary key check(singleton = 1), epoch text not null, sequence integer not null check(sequence >= 0));
|
|
429
438
|
create table if not exists operator_stream_events(epoch text not null, sequence integer not null check(sequence > 0), payload text not null, unique(epoch, sequence));
|
|
430
439
|
`);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -22,6 +22,6 @@ export { PushDeliveryService, type DeliveryAddressRegistration, type PushDeliver
|
|
|
22
22
|
export { MCP_TOOLS, type McpToolDefinition } from "./messaging/tools.js";
|
|
23
23
|
export { PAGER_WAKE_TYPE, wakeLine, wakeMetadata } from "./messaging/wake.js";
|
|
24
24
|
export { classifyOperatorEvent, validateOperatorAgent, validateOperatorCommand, validateOperatorCommandResult, validateOperatorCommandResultForCommand, validateOperatorControlId, validateOperatorCursor, validateOperatorDiagnostic, validateOperatorEventEnvelope, validateOperatorMessageBody, validateOperatorRepository, validateOperatorSnapshot, validateOperatorSnapshotByteLength, validateOperatorTraffic, validateOperatorTransportRequest, OperatorValidationError, OPERATOR_ARRAY_LENGTH_LIMIT, } from "./operator/validation.js";
|
|
25
|
-
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, type OperatorAcceptedDelivery, type OperatorAdapterState, type OperatorAgent, type OperatorCommand, type OperatorCommandEnvelope, type OperatorCommandEnvelopeFor, type OperatorCommandResult, type OperatorCorrelatedCommandResult, type OperatorCursor, type OperatorCursorResult, type OperatorDeliveryLifecycleEntry, type OperatorDiagnostic, type OperatorDiagnosticSeverity, type OperatorEventEnvelope, type OperatorImplementation, type OperatorImplementationVersion, type OperatorJsonObject, type OperatorJsonValue, type OperatorMessageBody, type OperatorPingAudit, type OperatorRejectionReason, type OperatorRepository, type OperatorResyncReason, type OperatorSnapshot, type OperatorStateEvent, type OperatorTraffic, type OperatorTransportFrame, type OperatorTransportRequest, type OperatorWorktree, } from "./operator/protocol.js";
|
|
25
|
+
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_PURGE_TARGET_LIMIT, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES, type OperatorAcceptedDelivery, type OperatorAdapterState, type OperatorAgent, type OperatorCommand, type OperatorCommandEnvelope, type OperatorCommandEnvelopeFor, type OperatorCommandResult, type OperatorCorrelatedCommandResult, type OperatorCursor, type OperatorCursorResult, type OperatorDeliveryLifecycleEntry, type OperatorDiagnostic, type OperatorDiagnosticSeverity, type OperatorEventEnvelope, type OperatorImplementation, type OperatorImplementationVersion, type OperatorJsonObject, type OperatorJsonValue, type OperatorMessageBody, type OperatorPingAudit, type OperatorRejectionReason, type OperatorRepository, type OperatorResyncReason, type OperatorSnapshot, type OperatorStateEvent, type OperatorTraffic, type OperatorTransportFrame, type OperatorTransportRequest, type OperatorWorktree, } from "./operator/protocol.js";
|
|
26
26
|
export { PAGER_VERSION } from "./version.js";
|
|
27
27
|
export { shellQuote } from "./shared/shell.js";
|
package/dist/src/index.js
CHANGED
|
@@ -21,6 +21,6 @@ export { PushDeliveryService, } from "./messaging/push-delivery.js";
|
|
|
21
21
|
export { MCP_TOOLS } from "./messaging/tools.js";
|
|
22
22
|
export { PAGER_WAKE_TYPE, wakeLine, wakeMetadata } from "./messaging/wake.js";
|
|
23
23
|
export { classifyOperatorEvent, validateOperatorAgent, validateOperatorCommand, validateOperatorCommandResult, validateOperatorCommandResultForCommand, validateOperatorControlId, validateOperatorCursor, validateOperatorDiagnostic, validateOperatorEventEnvelope, validateOperatorMessageBody, validateOperatorRepository, validateOperatorSnapshot, validateOperatorSnapshotByteLength, validateOperatorTraffic, validateOperatorTransportRequest, OperatorValidationError, OPERATOR_ARRAY_LENGTH_LIMIT, } from "./operator/validation.js";
|
|
24
|
-
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, } from "./operator/protocol.js";
|
|
24
|
+
export { OPERATOR_DIAGNOSTIC_TEXT_LIMIT, OPERATOR_PROTOCOL_VERSION, OPERATOR_PURGE_TARGET_LIMIT, OPERATOR_REJECTION_REASONS, OPERATOR_SNAPSHOT_CHUNK_BYTES, OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES, OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES, OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES, } from "./operator/protocol.js";
|
|
25
25
|
export { PAGER_VERSION } from "./version.js";
|
|
26
26
|
export { shellQuote } from "./shared/shell.js";
|
|
@@ -12,6 +12,8 @@ export interface OperatorCommandBrokerPort {
|
|
|
12
12
|
acceptOperatorExpiry(commandId: string, messageId: string): BrokerOperatorCommandAcceptance;
|
|
13
13
|
/** Mark an agent archived and persist command acceptance in one transaction, ending its attachments but leaving its inbox and addressability intact. */
|
|
14
14
|
acceptOperatorArchive(commandId: string, targetAgentId: AgentId): BrokerOperatorCommandAcceptance;
|
|
15
|
+
/** Delete agents and their records, and persist command acceptance, in one transaction. */
|
|
16
|
+
acceptOperatorPurge(commandId: string, targetAgentIds: readonly AgentId[]): BrokerOperatorCommandAcceptance;
|
|
15
17
|
}
|
|
16
18
|
/** Supplies non-reentrant notification admission around durable ping acceptance. */
|
|
17
19
|
export interface OperatorPingSchedulerPort {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { samePurgeTargets } from "../broker/operator-projection.js";
|
|
1
2
|
import { ProtocolError } from "../broker/protocol.js";
|
|
2
3
|
import { OPERATOR_PROTOCOL_VERSION, } from "./protocol.js";
|
|
3
4
|
import { validateOperatorCommand, validateOperatorCommandResult, validateOperatorControlId } from "./validation.js";
|
|
@@ -37,6 +38,8 @@ export class OperatorCommandService {
|
|
|
37
38
|
return this.#expire({ ...command, command: command.command });
|
|
38
39
|
case "archive_agent":
|
|
39
40
|
return this.#archive({ ...command, command: command.command });
|
|
41
|
+
case "purge_agents":
|
|
42
|
+
return this.#purge({ ...command, command: command.command });
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
#ping(command) {
|
|
@@ -90,31 +93,16 @@ export class OperatorCommandService {
|
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
#archive(command) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
throw new Error("Broker returned a mismatched archive acceptance");
|
|
98
|
-
}
|
|
99
|
-
return validateOperatorCommandResult({
|
|
100
|
-
protocolVersion: OPERATOR_PROTOCOL_VERSION,
|
|
101
|
-
type: "archive_accepted",
|
|
102
|
-
status: "accepted",
|
|
103
|
-
commandId: command.commandId,
|
|
104
|
-
acceptedAt: accepted.acceptedAt,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
return rejected(command.commandId, rejectionReason(error));
|
|
109
|
-
}
|
|
96
|
+
return agentCommandResult(command, () => this.#broker.acceptOperatorArchive(command.commandId, command.command.targetAgentId));
|
|
97
|
+
}
|
|
98
|
+
#purge(command) {
|
|
99
|
+
return agentCommandResult(command, () => this.#broker.acceptOperatorPurge(command.commandId, command.command.targetAgentIds));
|
|
110
100
|
}
|
|
111
101
|
#priorResult(command, prior) {
|
|
112
|
-
const expectedTarget = command.command.type === "expire_message" ? command.command.messageId : command.command.targetAgentId;
|
|
113
102
|
if (prior.commandId !== command.commandId)
|
|
114
103
|
throw new Error("Broker returned a mismatched command lookup");
|
|
115
|
-
if (
|
|
104
|
+
if (!sameTarget(command.command, prior))
|
|
116
105
|
return rejected(command.commandId, "command_id_conflict");
|
|
117
|
-
}
|
|
118
106
|
if (prior.type === "ping_agent")
|
|
119
107
|
validateOperatorControlId(prior.controlId);
|
|
120
108
|
if (prior.type === "ping_agent")
|
|
@@ -128,13 +116,61 @@ export class OperatorCommandService {
|
|
|
128
116
|
});
|
|
129
117
|
return validateOperatorCommandResult({
|
|
130
118
|
protocolVersion: OPERATOR_PROTOCOL_VERSION,
|
|
131
|
-
type: prior.type
|
|
119
|
+
type: ACCEPTED_RESULT_TYPES[prior.type],
|
|
132
120
|
status: "accepted",
|
|
133
121
|
commandId: prior.commandId,
|
|
134
122
|
acceptedAt: prior.acceptedAt,
|
|
135
123
|
});
|
|
136
124
|
}
|
|
137
125
|
}
|
|
126
|
+
/** Names the result each command kind without a control correlation is accepted with. */
|
|
127
|
+
const ACCEPTED_RESULT_TYPES = {
|
|
128
|
+
expire_message: "expiry_accepted",
|
|
129
|
+
archive_agent: "archive_accepted",
|
|
130
|
+
purge_agents: "purge_accepted",
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Run one durable agent command and answer it with its acceptance or a rejection.
|
|
134
|
+
* @param command - Command being admitted, whose kind and target the broker's record must match.
|
|
135
|
+
* @param accept - Broker transaction that applies the command and records its acceptance.
|
|
136
|
+
* @returns - The acceptance, or a rejection naming why the broker refused or failed.
|
|
137
|
+
*/
|
|
138
|
+
function agentCommandResult(command, accept) {
|
|
139
|
+
try {
|
|
140
|
+
const { record, status } = accept();
|
|
141
|
+
assertAcceptanceStatus(status);
|
|
142
|
+
if (record.commandId !== command.commandId || !sameTarget(command.command, record)) {
|
|
143
|
+
throw new Error(`Broker returned a mismatched ${command.command.type} acceptance`);
|
|
144
|
+
}
|
|
145
|
+
return validateOperatorCommandResult({
|
|
146
|
+
protocolVersion: OPERATOR_PROTOCOL_VERSION,
|
|
147
|
+
type: ACCEPTED_RESULT_TYPES[command.command.type],
|
|
148
|
+
status: "accepted",
|
|
149
|
+
commandId: command.commandId,
|
|
150
|
+
acceptedAt: record.acceptedAt,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return rejected(command.commandId, rejectionReason(error));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Report whether a stored acceptance answers the same request a retried command makes.
|
|
159
|
+
* @param command - Command carried by the retry.
|
|
160
|
+
* @param prior - Acceptance stored under the retry's command id.
|
|
161
|
+
* @returns - Whether both name the same command kind and the same target.
|
|
162
|
+
*/
|
|
163
|
+
function sameTarget(command, prior) {
|
|
164
|
+
switch (command.type) {
|
|
165
|
+
case "ping_agent":
|
|
166
|
+
case "archive_agent":
|
|
167
|
+
return prior.type === command.type && prior.targetId === command.targetAgentId;
|
|
168
|
+
case "expire_message":
|
|
169
|
+
return prior.type === command.type && prior.targetId === command.messageId;
|
|
170
|
+
case "purge_agents":
|
|
171
|
+
return prior.type === command.type && samePurgeTargets(prior.targetIds, command.targetAgentIds);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
138
174
|
function pingAccepted(commandId, accepted) {
|
|
139
175
|
if (accepted.type !== "ping_agent" || accepted.commandId !== commandId)
|
|
140
176
|
throw new Error("Broker returned the wrong ping command acceptance");
|
|
@@ -29,6 +29,15 @@ export declare const OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES: number;
|
|
|
29
29
|
export declare const OPERATOR_SNAPSHOT_CHUNK_BYTES: number;
|
|
30
30
|
/** Bounds every newline-delimited server frame after JSON encoding. */
|
|
31
31
|
export declare const OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES: number;
|
|
32
|
+
/** Bounds every newline-delimited client request after JSON encoding; a longer one ends the connection. */
|
|
33
|
+
export declare const OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES: number;
|
|
34
|
+
/**
|
|
35
|
+
* Bounds how many agents one purge command names.
|
|
36
|
+
*
|
|
37
|
+
* A purge naming this many agents at the longest agent id still fits in one request under
|
|
38
|
+
* `OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const OPERATOR_PURGE_TARGET_LIMIT = 256;
|
|
32
41
|
/** Represents JSON data that is safe to cross the operator boundary. */
|
|
33
42
|
export type OperatorJsonValue = null | boolean | number | string | OperatorJsonValue[] | OperatorJsonObject;
|
|
34
43
|
/** Represents an object whose values are safe to cross the operator boundary. */
|
|
@@ -310,6 +319,11 @@ export type OperatorCommand = {
|
|
|
310
319
|
type: "archive_agent";
|
|
311
320
|
/** Identifies the target using broker identity, not repository projection. */
|
|
312
321
|
targetAgentId: AgentId;
|
|
322
|
+
} | {
|
|
323
|
+
/** Requests that agent identities and their records be deleted in one transaction. */
|
|
324
|
+
type: "purge_agents";
|
|
325
|
+
/** Distinct broker identities to delete, at least one; a purge of any that is already gone is refused whole. */
|
|
326
|
+
targetAgentIds: readonly AgentId[];
|
|
313
327
|
};
|
|
314
328
|
/** Carries one versioned operator command request. */
|
|
315
329
|
export type OperatorCommandEnvelope = {
|
|
@@ -367,6 +381,17 @@ export type OperatorCommandResult = {
|
|
|
367
381
|
commandId: string;
|
|
368
382
|
/** Records broker acceptance time. */
|
|
369
383
|
acceptedAt: number;
|
|
384
|
+
} | {
|
|
385
|
+
/** Identifies the operator wire contract. */
|
|
386
|
+
protocolVersion: typeof OPERATOR_PROTOCOL_VERSION;
|
|
387
|
+
/** Identifies agent-purge command acceptance. */
|
|
388
|
+
type: "purge_accepted";
|
|
389
|
+
/** Reports that the broker accepted the command request. */
|
|
390
|
+
status: "accepted";
|
|
391
|
+
/** Correlates the result with the submitted command. */
|
|
392
|
+
commandId: string;
|
|
393
|
+
/** Records broker acceptance time. */
|
|
394
|
+
acceptedAt: number;
|
|
370
395
|
} | {
|
|
371
396
|
/** Identifies the operator wire contract. */
|
|
372
397
|
protocolVersion: typeof OPERATOR_PROTOCOL_VERSION;
|
|
@@ -408,6 +433,13 @@ export type OperatorCorrelatedCommandResult = {
|
|
|
408
433
|
result: Extract<OperatorCommandResult, {
|
|
409
434
|
type: "archive_accepted" | "command_rejected";
|
|
410
435
|
}>;
|
|
436
|
+
} | {
|
|
437
|
+
/** Carries the originating purge request. */
|
|
438
|
+
command: OperatorCommandEnvelopeFor<"purge_agents">;
|
|
439
|
+
/** Carries purge acceptance or a command-agnostic rejection. */
|
|
440
|
+
result: Extract<OperatorCommandResult, {
|
|
441
|
+
type: "purge_accepted" | "command_rejected";
|
|
442
|
+
}>;
|
|
411
443
|
};
|
|
412
444
|
/** Classifies one event relative to a consumer cursor without conflating recovery actions. */
|
|
413
445
|
export type OperatorCursorResult = {
|
|
@@ -28,6 +28,15 @@ export const OPERATOR_SNAPSHOT_TOTAL_LIMIT_BYTES = 512 * 1_024 * 1_024;
|
|
|
28
28
|
export const OPERATOR_SNAPSHOT_CHUNK_BYTES = 48 * 1_024;
|
|
29
29
|
/** Bounds every newline-delimited server frame after JSON encoding. */
|
|
30
30
|
export const OPERATOR_TRANSPORT_FRAME_LIMIT_BYTES = 128 * 1_024;
|
|
31
|
+
/** Bounds every newline-delimited client request after JSON encoding; a longer one ends the connection. */
|
|
32
|
+
export const OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES = 64 * 1_024;
|
|
33
|
+
/**
|
|
34
|
+
* Bounds how many agents one purge command names.
|
|
35
|
+
*
|
|
36
|
+
* A purge naming this many agents at the longest agent id still fits in one request under
|
|
37
|
+
* `OPERATOR_TRANSPORT_REQUEST_LIMIT_BYTES`.
|
|
38
|
+
*/
|
|
39
|
+
export const OPERATOR_PURGE_TARGET_LIMIT = 256;
|
|
31
40
|
/**
|
|
32
41
|
* Names every stable category a command rejection can carry.
|
|
33
42
|
*
|