@haywoodmarx/agent-pager 0.1.0
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/src/broker/address.d.ts +49 -0
- package/dist/src/broker/address.js +110 -0
- package/dist/src/broker/broker.d.ts +665 -0
- package/dist/src/broker/broker.js +2270 -0
- package/dist/src/broker/database.d.ts +81 -0
- package/dist/src/broker/database.js +69 -0
- package/dist/src/broker/operator-projection.d.ts +291 -0
- package/dist/src/broker/operator-projection.js +722 -0
- package/dist/src/broker/protocol.d.ts +237 -0
- package/dist/src/broker/protocol.js +311 -0
- package/dist/src/broker/schema.d.ts +94 -0
- package/dist/src/broker/schema.js +454 -0
- package/dist/src/broker/snapshot.d.ts +171 -0
- package/dist/src/broker/snapshot.js +78 -0
- package/dist/src/configuration-defaults.d.ts +64 -0
- package/dist/src/configuration-defaults.js +48 -0
- package/dist/src/configuration.d.ts +76 -0
- package/dist/src/configuration.js +156 -0
- package/dist/src/implementations/claude/environment.d.ts +8 -0
- package/dist/src/implementations/claude/environment.js +9 -0
- package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
- package/dist/src/implementations/claude/gateway-cli.js +136 -0
- package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
- package/dist/src/implementations/claude/gateway-identity.js +103 -0
- package/dist/src/implementations/claude/gateway.d.ts +148 -0
- package/dist/src/implementations/claude/gateway.js +236 -0
- package/dist/src/implementations/claude/instructions.d.ts +33 -0
- package/dist/src/implementations/claude/instructions.js +64 -0
- package/dist/src/implementations/claude/manifest.d.ts +33 -0
- package/dist/src/implementations/claude/manifest.js +108 -0
- package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
- package/dist/src/implementations/claude/plugin-directory.js +137 -0
- package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
- package/dist/src/implementations/claude/session-continuation.js +125 -0
- package/dist/src/implementations/claude/session-identity.d.ts +25 -0
- package/dist/src/implementations/claude/session-identity.js +31 -0
- package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
- package/dist/src/implementations/claude/session-start-cli.js +16 -0
- package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
- package/dist/src/implementations/claude/socket-delivery.js +159 -0
- package/dist/src/implementations/model.d.ts +162 -0
- package/dist/src/implementations/model.js +1 -0
- package/dist/src/implementations/opencode/adapter.d.ts +71 -0
- package/dist/src/implementations/opencode/adapter.js +94 -0
- package/dist/src/implementations/opencode/environment.d.ts +8 -0
- package/dist/src/implementations/opencode/environment.js +9 -0
- package/dist/src/implementations/opencode/manifest.d.ts +30 -0
- package/dist/src/implementations/opencode/manifest.js +93 -0
- package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
- package/dist/src/implementations/opencode/plugin-api.js +1 -0
- package/dist/src/implementations/opencode/plugin.d.ts +38 -0
- package/dist/src/implementations/opencode/plugin.js +303 -0
- package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
- package/dist/src/implementations/opencode/self-close-tool.js +31 -0
- package/dist/src/implementations/orca/terminal.d.ts +48 -0
- package/dist/src/implementations/orca/terminal.js +154 -0
- package/dist/src/implementations/pi/broker-client.d.ts +56 -0
- package/dist/src/implementations/pi/broker-client.js +156 -0
- package/dist/src/implementations/pi/environment.d.ts +8 -0
- package/dist/src/implementations/pi/environment.js +9 -0
- package/dist/src/implementations/pi/extension.d.ts +57 -0
- package/dist/src/implementations/pi/extension.js +253 -0
- package/dist/src/implementations/pi/host-slots.d.ts +18 -0
- package/dist/src/implementations/pi/host-slots.js +27 -0
- package/dist/src/implementations/pi/manifest.d.ts +11 -0
- package/dist/src/implementations/pi/manifest.js +61 -0
- package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
- package/dist/src/implementations/pi/self-close-tool.js +27 -0
- package/dist/src/implementations/registry.d.ts +47 -0
- package/dist/src/implementations/registry.js +157 -0
- package/dist/src/implementations/session-key.d.ts +13 -0
- package/dist/src/implementations/session-key.js +19 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +26 -0
- package/dist/src/launch/broker-lock.d.ts +257 -0
- package/dist/src/launch/broker-lock.js +644 -0
- package/dist/src/launch/cli.d.ts +37 -0
- package/dist/src/launch/cli.js +317 -0
- package/dist/src/launch/environment.d.ts +45 -0
- package/dist/src/launch/environment.js +48 -0
- package/dist/src/launch/launch.d.ts +235 -0
- package/dist/src/launch/launch.js +223 -0
- package/dist/src/launch/on-demand.d.ts +202 -0
- package/dist/src/launch/on-demand.js +435 -0
- package/dist/src/launch/runtime-composition.d.ts +88 -0
- package/dist/src/launch/runtime-composition.js +253 -0
- package/dist/src/launch/runtime.d.ts +79 -0
- package/dist/src/launch/runtime.js +196 -0
- package/dist/src/launch/service-system.d.ts +11 -0
- package/dist/src/launch/service-system.js +39 -0
- package/dist/src/launch/service.d.ts +173 -0
- package/dist/src/launch/service.js +492 -0
- package/dist/src/launch/skill.d.ts +57 -0
- package/dist/src/launch/skill.js +81 -0
- package/dist/src/launch/terminal.d.ts +26 -0
- package/dist/src/launch/terminal.js +43 -0
- package/dist/src/launch/workspace-branch.d.ts +10 -0
- package/dist/src/launch/workspace-branch.js +29 -0
- package/dist/src/messaging/delivery-client.d.ts +144 -0
- package/dist/src/messaging/delivery-client.js +261 -0
- package/dist/src/messaging/delivery.d.ts +75 -0
- package/dist/src/messaging/delivery.js +22 -0
- package/dist/src/messaging/inbox-socket.d.ts +98 -0
- package/dist/src/messaging/inbox-socket.js +278 -0
- package/dist/src/messaging/mcp.d.ts +53 -0
- package/dist/src/messaging/mcp.js +501 -0
- package/dist/src/messaging/notifications.d.ts +231 -0
- package/dist/src/messaging/notifications.js +489 -0
- package/dist/src/messaging/push-delivery.d.ts +77 -0
- package/dist/src/messaging/push-delivery.js +187 -0
- package/dist/src/messaging/tools.d.ts +11 -0
- package/dist/src/messaging/tools.js +102 -0
- package/dist/src/messaging/wake.d.ts +23 -0
- package/dist/src/messaging/wake.js +39 -0
- package/dist/src/operator/agent-projection.d.ts +102 -0
- package/dist/src/operator/agent-projection.js +249 -0
- package/dist/src/operator/commands.d.ts +50 -0
- package/dist/src/operator/commands.js +199 -0
- package/dist/src/operator/diagnostics.d.ts +21 -0
- package/dist/src/operator/diagnostics.js +22 -0
- package/dist/src/operator/protocol.d.ts +511 -0
- package/dist/src/operator/protocol.js +49 -0
- package/dist/src/operator/repository-system.d.ts +14 -0
- package/dist/src/operator/repository-system.js +150 -0
- package/dist/src/operator/repository.d.ts +79 -0
- package/dist/src/operator/repository.js +72 -0
- package/dist/src/operator/traffic-projection.d.ts +19 -0
- package/dist/src/operator/traffic-projection.js +229 -0
- package/dist/src/operator/transport.d.ts +87 -0
- package/dist/src/operator/transport.js +549 -0
- package/dist/src/operator/update-store.d.ts +53 -0
- package/dist/src/operator/update-store.js +162 -0
- package/dist/src/operator/updates.d.ts +94 -0
- package/dist/src/operator/updates.js +280 -0
- package/dist/src/operator/validation.d.ts +114 -0
- package/dist/src/operator/validation.js +916 -0
- package/dist/src/shared/guards.d.ts +59 -0
- package/dist/src/shared/guards.js +119 -0
- package/dist/src/shared/http.d.ts +30 -0
- package/dist/src/shared/http.js +50 -0
- package/dist/src/shared/row.d.ts +2 -0
- package/dist/src/shared/row.js +1 -0
- package/dist/src/shared/shell.d.ts +6 -0
- package/dist/src/shared/shell.js +8 -0
- package/dist/src/tui/actions.d.ts +166 -0
- package/dist/src/tui/actions.js +253 -0
- package/dist/src/tui/agent-tree.d.ts +125 -0
- package/dist/src/tui/agent-tree.js +663 -0
- package/dist/src/tui/app.d.ts +38 -0
- package/dist/src/tui/app.js +378 -0
- package/dist/src/tui/chrome.d.ts +86 -0
- package/dist/src/tui/chrome.js +85 -0
- package/dist/src/tui/client.d.ts +41 -0
- package/dist/src/tui/client.js +279 -0
- package/dist/src/tui/composition.d.ts +41 -0
- package/dist/src/tui/composition.js +146 -0
- package/dist/src/tui/message-views.d.ts +94 -0
- package/dist/src/tui/message-views.js +515 -0
- package/dist/src/tui/model.d.ts +414 -0
- package/dist/src/tui/model.js +758 -0
- package/dist/src/tui/presentation.d.ts +350 -0
- package/dist/src/tui/presentation.js +541 -0
- package/dist/src/tui/production.d.ts +79 -0
- package/dist/src/tui/production.js +253 -0
- package/dist/src/tui/run-terminal.d.ts +32 -0
- package/dist/src/tui/run-terminal.js +54 -0
- package/dist/src/tui/runtime.d.ts +113 -0
- package/dist/src/tui/runtime.js +189 -0
- package/dist/src/tui/theme.d.ts +49 -0
- package/dist/src/tui/theme.js +55 -0
- package/dist/src/tui/transport.d.ts +31 -0
- package/dist/src/tui/transport.js +255 -0
- package/dist/src/version.d.ts +11 -0
- package/dist/src/version.js +52 -0
- package/package.json +142 -0
- package/skills/pager/SKILL.md +112 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { RepositoryDiscoveryError } from "./repository.js";
|
|
5
|
+
/** Bounds each Git command's complete captured output. */
|
|
6
|
+
export const REPOSITORY_GIT_OUTPUT_LIMIT = 65_536;
|
|
7
|
+
/**
|
|
8
|
+
* Compose production filesystem and Git discovery adapters.
|
|
9
|
+
* @param options - Optional executable override for isolated verification.
|
|
10
|
+
* @returns Dependencies whose process execution is isolated from pure projection logic.
|
|
11
|
+
*/
|
|
12
|
+
export function createSystemRepositoryProjectionDependencies(options = {}) {
|
|
13
|
+
const gitExecutable = options.gitExecutable ?? "git";
|
|
14
|
+
return {
|
|
15
|
+
fileSystem: {
|
|
16
|
+
canonicalPath: (path) => realpathSync(path),
|
|
17
|
+
exists: (path) => existsSync(path),
|
|
18
|
+
},
|
|
19
|
+
git: {
|
|
20
|
+
inspect: (workspacePath) => {
|
|
21
|
+
const topLevel = runGit(gitExecutable, ["-C", workspacePath, "rev-parse", "--show-toplevel"], true);
|
|
22
|
+
if (topLevel.kind === "not_git")
|
|
23
|
+
return topLevel;
|
|
24
|
+
const worktreeRoot = parseSingleAbsolutePath(topLevel.stdout, "Git top-level output is invalid");
|
|
25
|
+
const topology = runGit(gitExecutable, ["-C", workspacePath, "worktree", "list", "--porcelain", "-z"], false);
|
|
26
|
+
if (topology.kind === "not_git")
|
|
27
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology disappeared");
|
|
28
|
+
const repositoryRoot = parsePrimaryWorktree(topology.stdout, worktreeRoot);
|
|
29
|
+
return { kind: "git", repositoryRoot, worktreeRoot };
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function runGit(executable, args, allowNotGit) {
|
|
35
|
+
const result = spawnSync(executable, args, {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" },
|
|
38
|
+
maxBuffer: REPOSITORY_GIT_OUTPUT_LIMIT,
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
if (result.error) {
|
|
42
|
+
const code = "code" in result.error ? result.error.code : undefined;
|
|
43
|
+
if (code === "ENOBUFS")
|
|
44
|
+
throw new RepositoryDiscoveryError("output_limit", "Git discovery output exceeded its limit");
|
|
45
|
+
throw new RepositoryDiscoveryError("unavailable", "Git discovery process is unavailable");
|
|
46
|
+
}
|
|
47
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
48
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
49
|
+
if (Buffer.byteLength(stdout) > REPOSITORY_GIT_OUTPUT_LIMIT ||
|
|
50
|
+
Buffer.byteLength(stderr) > REPOSITORY_GIT_OUTPUT_LIMIT) {
|
|
51
|
+
throw new RepositoryDiscoveryError("output_limit", "Git discovery output exceeded its limit");
|
|
52
|
+
}
|
|
53
|
+
if (result.signal !== null)
|
|
54
|
+
throw new RepositoryDiscoveryError("unavailable", "Git discovery process was interrupted");
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
if (allowNotGit && result.status === 128 && /not a git repository/u.test(stderr))
|
|
57
|
+
return { kind: "not_git" };
|
|
58
|
+
throw new RepositoryDiscoveryError("unavailable", "Git discovery command failed");
|
|
59
|
+
}
|
|
60
|
+
return { kind: "ok", stdout };
|
|
61
|
+
}
|
|
62
|
+
function parseSingleAbsolutePath(output, message) {
|
|
63
|
+
const normalized = output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
64
|
+
if (!normalized || normalized.includes("\n") || normalized.includes("\r") || !isAbsolute(normalized)) {
|
|
65
|
+
throw new RepositoryDiscoveryError("invalid", message);
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
function parsePrimaryWorktree(output, currentWorktree) {
|
|
70
|
+
if (!output.endsWith("\0\0"))
|
|
71
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology output is truncated");
|
|
72
|
+
const tokens = output.split("\0");
|
|
73
|
+
tokens.pop();
|
|
74
|
+
const records = [];
|
|
75
|
+
let record = [];
|
|
76
|
+
for (const token of tokens) {
|
|
77
|
+
if (token === "") {
|
|
78
|
+
if (record.length === 0)
|
|
79
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology contains an empty record");
|
|
80
|
+
records.push(record);
|
|
81
|
+
record = [];
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (Buffer.byteLength(token) > REPOSITORY_GIT_OUTPUT_LIMIT / 2) {
|
|
85
|
+
throw new RepositoryDiscoveryError("output_limit", "Git topology field exceeded its limit");
|
|
86
|
+
}
|
|
87
|
+
record.push(token);
|
|
88
|
+
}
|
|
89
|
+
if (record.length > 0 || records.length === 0) {
|
|
90
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology record is incomplete");
|
|
91
|
+
}
|
|
92
|
+
const topology = records.map(validateWorktreeRecord);
|
|
93
|
+
const worktrees = new Set();
|
|
94
|
+
for (const [index, entry] of topology.entries()) {
|
|
95
|
+
if (worktrees.has(entry.path)) {
|
|
96
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology contains a duplicate worktree");
|
|
97
|
+
}
|
|
98
|
+
worktrees.add(entry.path);
|
|
99
|
+
if (entry.bare && index !== 0) {
|
|
100
|
+
throw new RepositoryDiscoveryError("invalid", "Only the primary Git topology record may be bare");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!worktrees.has(currentWorktree)) {
|
|
104
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology omitted the current worktree");
|
|
105
|
+
}
|
|
106
|
+
const primary = topology[0]?.path;
|
|
107
|
+
if (!primary)
|
|
108
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology omitted its primary worktree");
|
|
109
|
+
return primary;
|
|
110
|
+
}
|
|
111
|
+
function validateWorktreeRecord(fields) {
|
|
112
|
+
const first = fields[0];
|
|
113
|
+
if (!first?.startsWith("worktree ")) {
|
|
114
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology record does not start with a worktree");
|
|
115
|
+
}
|
|
116
|
+
const worktree = first.slice("worktree ".length);
|
|
117
|
+
if (!worktree || !isAbsolute(worktree)) {
|
|
118
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology worktree path is invalid");
|
|
119
|
+
}
|
|
120
|
+
if (fields[1] === "bare") {
|
|
121
|
+
if (fields.length !== 2)
|
|
122
|
+
throw new RepositoryDiscoveryError("invalid", "Bare Git topology record is invalid");
|
|
123
|
+
return { path: worktree, bare: true };
|
|
124
|
+
}
|
|
125
|
+
if (!/^HEAD [0-9a-f]{40}(?:[0-9a-f]{24})?$/u.test(fields[1] ?? "")) {
|
|
126
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology HEAD field is invalid");
|
|
127
|
+
}
|
|
128
|
+
if (!(fields[2]?.startsWith("branch refs/") || fields[2] === "detached")) {
|
|
129
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology branch field is invalid");
|
|
130
|
+
}
|
|
131
|
+
let locked = false;
|
|
132
|
+
let prunable = false;
|
|
133
|
+
for (const field of fields.slice(3)) {
|
|
134
|
+
if (field === "locked" || field.startsWith("locked ")) {
|
|
135
|
+
if (locked || prunable) {
|
|
136
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology locked field is duplicated or misplaced");
|
|
137
|
+
}
|
|
138
|
+
locked = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (field === "prunable" || field.startsWith("prunable ")) {
|
|
142
|
+
if (prunable)
|
|
143
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology prunable field is duplicated");
|
|
144
|
+
prunable = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
throw new RepositoryDiscoveryError("invalid", "Git topology contains an unknown field");
|
|
148
|
+
}
|
|
149
|
+
return { path: worktree, bare: false };
|
|
150
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { OperatorRepository, OperatorWorktree } from "./protocol.js";
|
|
2
|
+
/** Describes Git repository and worktree roots without performing discovery itself. */
|
|
3
|
+
export type RepositoryGitLocation = {
|
|
4
|
+
/** Supplies the root of the primary repository worktree. */
|
|
5
|
+
repositoryRoot: string;
|
|
6
|
+
/** Supplies the root of the worktree containing the inspected workspace. */
|
|
7
|
+
worktreeRoot: string;
|
|
8
|
+
};
|
|
9
|
+
/** Reports either confirmed Git topology or a confirmed non-Git workspace. */
|
|
10
|
+
export type RepositoryGitInspection = ({
|
|
11
|
+
kind: "git";
|
|
12
|
+
} & RepositoryGitLocation) | {
|
|
13
|
+
kind: "not_git";
|
|
14
|
+
};
|
|
15
|
+
/** Canonicalizes filesystem paths for stable presentation grouping. */
|
|
16
|
+
export interface RepositoryFileSystemPort {
|
|
17
|
+
/**
|
|
18
|
+
* Resolve aliases and symbolic links in one existing path.
|
|
19
|
+
* @param path - Absolute workspace or Git root to canonicalize.
|
|
20
|
+
* @returns The canonical absolute filesystem path.
|
|
21
|
+
*/
|
|
22
|
+
canonicalPath(path: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Report whether one recorded path still resolves on this machine.
|
|
25
|
+
* @param path - Absolute broker workspace path recorded when the agent registered.
|
|
26
|
+
* @returns True when the path resolves through any symbolic links, false when it has been removed, is a dangling link, or is unreachable.
|
|
27
|
+
*/
|
|
28
|
+
exists(path: string): boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Discovers Git topology without granting it broker routing authority. */
|
|
31
|
+
export interface RepositoryGitPort {
|
|
32
|
+
/**
|
|
33
|
+
* Inspect one canonical workspace for Git repository topology.
|
|
34
|
+
* @param workspacePath - Canonical absolute workspace path.
|
|
35
|
+
* @returns Confirmed Git topology or a confirmed non-Git result.
|
|
36
|
+
*/
|
|
37
|
+
inspect(workspacePath: string): RepositoryGitInspection;
|
|
38
|
+
}
|
|
39
|
+
/** Supplies filesystem and Git collaborators to repository projection. */
|
|
40
|
+
export type RepositoryProjectionDependencies = {
|
|
41
|
+
/** Canonicalizes discovered paths. */
|
|
42
|
+
fileSystem: RepositoryFileSystemPort;
|
|
43
|
+
/** Reports Git repository topology. */
|
|
44
|
+
git: RepositoryGitPort;
|
|
45
|
+
};
|
|
46
|
+
/** Describes one workspace's presentation-only repository grouping. */
|
|
47
|
+
export type ProjectedRepositoryLocation = {
|
|
48
|
+
/** Carries the shared repository identity and root, before a name unique across all repositories is known. */
|
|
49
|
+
repository: Omit<OperatorRepository, "name">;
|
|
50
|
+
/** Carries worktree detail for the inspected workspace. */
|
|
51
|
+
worktree: OperatorWorktree;
|
|
52
|
+
/** Reports whether the workspace path still resolves, which decides operator archive membership. */
|
|
53
|
+
available: boolean;
|
|
54
|
+
};
|
|
55
|
+
/** Reports invalid or unavailable repository projection inputs. */
|
|
56
|
+
declare class RepositoryProjectionError extends Error {
|
|
57
|
+
}
|
|
58
|
+
/** Reports bounded repository discovery failure without retaining process output. */
|
|
59
|
+
export declare class RepositoryDiscoveryError extends RepositoryProjectionError {
|
|
60
|
+
readonly code: "unavailable" | "invalid" | "output_limit";
|
|
61
|
+
/**
|
|
62
|
+
* Build one controlled discovery error.
|
|
63
|
+
* @param code - Stable failure class safe for callers and tests.
|
|
64
|
+
* @param message - Bounded diagnostic without child-process output.
|
|
65
|
+
*/
|
|
66
|
+
constructor(code: "unavailable" | "invalid" | "output_limit", message: string);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Project one broker workspace into repository grouping and worktree detail.
|
|
70
|
+
*
|
|
71
|
+
* A workspace whose path no longer resolves is projected from its recorded path alone. Neither
|
|
72
|
+
* canonicalization nor Git discovery can run against a removed directory, and both throw rather
|
|
73
|
+
* than reporting absence, so the removal is detected before either is attempted.
|
|
74
|
+
* @param workspacePath - Absolute broker workspace path to inspect.
|
|
75
|
+
* @param dependencies - Explicit filesystem and Git discovery collaborators.
|
|
76
|
+
* @returns Presentation-only repository and worktree values, and whether the workspace still resolves.
|
|
77
|
+
*/
|
|
78
|
+
export declare function projectRepositoryLocation(workspacePath: string, dependencies: RepositoryProjectionDependencies): ProjectedRepositoryLocation;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Reports invalid or unavailable repository projection inputs. */
|
|
3
|
+
class RepositoryProjectionError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/** Reports bounded repository discovery failure without retaining process output. */
|
|
6
|
+
export class RepositoryDiscoveryError extends RepositoryProjectionError {
|
|
7
|
+
code;
|
|
8
|
+
/**
|
|
9
|
+
* Build one controlled discovery error.
|
|
10
|
+
* @param code - Stable failure class safe for callers and tests.
|
|
11
|
+
* @param message - Bounded diagnostic without child-process output.
|
|
12
|
+
*/
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "RepositoryDiscoveryError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Project one broker workspace into repository grouping and worktree detail.
|
|
21
|
+
*
|
|
22
|
+
* A workspace whose path no longer resolves is projected from its recorded path alone. Neither
|
|
23
|
+
* canonicalization nor Git discovery can run against a removed directory, and both throw rather
|
|
24
|
+
* than reporting absence, so the removal is detected before either is attempted.
|
|
25
|
+
* @param workspacePath - Absolute broker workspace path to inspect.
|
|
26
|
+
* @param dependencies - Explicit filesystem and Git discovery collaborators.
|
|
27
|
+
* @returns Presentation-only repository and worktree values, and whether the workspace still resolves.
|
|
28
|
+
*/
|
|
29
|
+
export function projectRepositoryLocation(workspacePath, dependencies) {
|
|
30
|
+
if (!dependencies.fileSystem.exists(workspacePath)) {
|
|
31
|
+
if (!workspacePath.startsWith("/")) {
|
|
32
|
+
throw new RepositoryProjectionError("removed workspace must resolve to an absolute path");
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
repository: { id: repositoryProjectionId(workspacePath), rootPath: workspacePath },
|
|
36
|
+
worktree: { kind: "repository_root", path: null },
|
|
37
|
+
available: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const canonicalWorkspace = canonicalAbsolutePath(workspacePath, dependencies.fileSystem, "workspace");
|
|
41
|
+
const gitLocation = dependencies.git.inspect(canonicalWorkspace);
|
|
42
|
+
if (gitLocation.kind === "not_git") {
|
|
43
|
+
return {
|
|
44
|
+
repository: { id: repositoryProjectionId(canonicalWorkspace), rootPath: canonicalWorkspace },
|
|
45
|
+
worktree: { kind: "repository_root", path: null },
|
|
46
|
+
available: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const repositoryRoot = canonicalAbsolutePath(gitLocation.repositoryRoot, dependencies.fileSystem, "repository root");
|
|
50
|
+
const worktreeRoot = canonicalAbsolutePath(gitLocation.worktreeRoot, dependencies.fileSystem, "worktree root");
|
|
51
|
+
return {
|
|
52
|
+
repository: { id: repositoryProjectionId(repositoryRoot), rootPath: repositoryRoot },
|
|
53
|
+
worktree: repositoryRoot === worktreeRoot
|
|
54
|
+
? { kind: "repository_root", path: null }
|
|
55
|
+
: { kind: "worktree", path: worktreeRoot },
|
|
56
|
+
available: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Derive a stable presentation identity from a canonical repository root.
|
|
61
|
+
* @param repositoryRoot - Canonical absolute repository root.
|
|
62
|
+
* @returns A non-authoritative repository projection identity.
|
|
63
|
+
*/
|
|
64
|
+
function repositoryProjectionId(repositoryRoot) {
|
|
65
|
+
return `repository:${createHash("sha256").update(repositoryRoot).digest("hex").slice(0, 16)}`;
|
|
66
|
+
}
|
|
67
|
+
function canonicalAbsolutePath(path, fileSystem, label) {
|
|
68
|
+
const canonical = fileSystem.canonicalPath(path);
|
|
69
|
+
if (!canonical.startsWith("/"))
|
|
70
|
+
throw new RepositoryProjectionError(`${label} must resolve to an absolute path`);
|
|
71
|
+
return canonical;
|
|
72
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BrokerTrafficReadResult } from "../broker/broker.js";
|
|
2
|
+
import { type OperatorDiagnosticSink } from "./diagnostics.js";
|
|
3
|
+
import type { OperatorTraffic } from "./protocol.js";
|
|
4
|
+
/** Supplies bounded durable records without giving the projection access to broker storage. */
|
|
5
|
+
export interface OperatorTrafficSourcePort {
|
|
6
|
+
/**
|
|
7
|
+
* Read traffic records without claiming or consuming retained message bodies.
|
|
8
|
+
* @returns Durable source rows whose positions share one broker-owned sequence.
|
|
9
|
+
*/
|
|
10
|
+
operatorTrafficRecords(): BrokerTrafficReadResult;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Project durable broker traffic into the validated renderer-neutral operator contract.
|
|
14
|
+
* @param source - Injected observer-only source of durable traffic rows.
|
|
15
|
+
* @param diagnostics - Sink receiving rows the projection could not admit.
|
|
16
|
+
* @param now - Clock stamping those reports.
|
|
17
|
+
* @returns Traffic sorted by the common durable position shared by every traffic kind.
|
|
18
|
+
*/
|
|
19
|
+
export declare function projectOperatorTraffic(source: OperatorTrafficSourcePort, diagnostics?: OperatorDiagnosticSink, now?: () => number): OperatorTraffic[];
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { decodeMessageBody, lifecycleEventState, MESSAGE_BODY_LIMIT_BYTES, } from "../broker/protocol.js";
|
|
2
|
+
import { operatorDiagnostic, SILENT_OPERATOR_DIAGNOSTICS } from "./diagnostics.js";
|
|
3
|
+
import { OPERATOR_ARRAY_LENGTH_LIMIT, OperatorValidationError, validateOperatorMessageBody, validateOperatorTraffic, } from "./validation.js";
|
|
4
|
+
/** Reports invalid or inconsistent durable input without echoing body content. */
|
|
5
|
+
class OperatorTrafficProjectionError extends Error {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Project durable broker traffic into the validated renderer-neutral operator contract.
|
|
9
|
+
* @param source - Injected observer-only source of durable traffic rows.
|
|
10
|
+
* @param diagnostics - Sink receiving rows the projection could not admit.
|
|
11
|
+
* @param now - Clock stamping those reports.
|
|
12
|
+
* @returns Traffic sorted by the common durable position shared by every traffic kind.
|
|
13
|
+
*/
|
|
14
|
+
export function projectOperatorTraffic(source, diagnostics = SILENT_OPERATOR_DIAGNOSTICS, now = Date.now) {
|
|
15
|
+
const read = source.operatorTrafficRecords();
|
|
16
|
+
const records = [...read.records];
|
|
17
|
+
const report = (line) => {
|
|
18
|
+
diagnostics.report(operatorDiagnostic("broker", "warning", "traffic projection", line, now()));
|
|
19
|
+
};
|
|
20
|
+
for (const line of describeSkips(read.skips))
|
|
21
|
+
report(line);
|
|
22
|
+
if (records.length > OPERATOR_ARRAY_LENGTH_LIMIT) {
|
|
23
|
+
throw new OperatorTrafficProjectionError("operator traffic source exceeds the bounded limit");
|
|
24
|
+
}
|
|
25
|
+
const unpositioned = records.filter((record) => !Number.isSafeInteger(record.trafficPosition) || record.trafficPosition < 1);
|
|
26
|
+
const positioned = records.filter((record) => !unpositioned.includes(record));
|
|
27
|
+
const [firstUnpositioned] = unpositioned;
|
|
28
|
+
if (firstUnpositioned !== undefined) {
|
|
29
|
+
const detail = `${describeRecord(firstUnpositioned)} skipped: durable position ${String(firstUnpositioned.trafficPosition)} is not a position`;
|
|
30
|
+
report(unpositioned.length === 1 ? detail : `${String(unpositioned.length)} records skipped; first: ${detail}`);
|
|
31
|
+
}
|
|
32
|
+
positioned.sort((left, right) => left.trafficPosition - right.trafficPosition);
|
|
33
|
+
// Every claimant of a colliding position is dropped; neither table's uniqueness spans both.
|
|
34
|
+
const claimed = new Set();
|
|
35
|
+
const colliding = new Set();
|
|
36
|
+
for (const record of positioned) {
|
|
37
|
+
if (claimed.has(record.trafficPosition))
|
|
38
|
+
colliding.add(record.trafficPosition);
|
|
39
|
+
claimed.add(record.trafficPosition);
|
|
40
|
+
}
|
|
41
|
+
const [firstCollision] = colliding;
|
|
42
|
+
if (firstCollision !== undefined) {
|
|
43
|
+
report(colliding.size === 1
|
|
44
|
+
? `durable position ${String(firstCollision)} is claimed by more than one row: every claimant skipped`
|
|
45
|
+
: `${String(colliding.size)} durable positions are claimed by more than one row: every claimant skipped; first: position ${String(firstCollision)}`);
|
|
46
|
+
}
|
|
47
|
+
const traffic = [];
|
|
48
|
+
// Positions carrying a lifecycle entry that validation dropped for naming a type outside the
|
|
49
|
+
// closed vocabulary (see validateLifecycleEntry), keyed by that type name. Reported once per run
|
|
50
|
+
// rather than once per record.
|
|
51
|
+
const unrecognizedLifecycleTypes = new Map();
|
|
52
|
+
for (const record of positioned) {
|
|
53
|
+
if (colliding.has(record.trafficPosition))
|
|
54
|
+
continue;
|
|
55
|
+
let projected;
|
|
56
|
+
try {
|
|
57
|
+
projected = projectRecord(record);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
diagnostics.report(operatorDiagnostic("broker", "warning", "traffic projection", `position ${String(record.trafficPosition)} skipped: ${error instanceof Error ? error.message : "unknown projection failure"}`, now()));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
traffic.push(projected);
|
|
64
|
+
// A record that failed outright already reported why above, so only one that projected cleanly
|
|
65
|
+
// is credited to the unrecognized-type diagnostic.
|
|
66
|
+
if (record.kind === "accepted_delivery" && projected.kind === "accepted_delivery") {
|
|
67
|
+
for (const entry of record.lifecycle) {
|
|
68
|
+
if (lifecycleEventState(entry.type) !== undefined)
|
|
69
|
+
continue;
|
|
70
|
+
const positionsForType = unrecognizedLifecycleTypes.get(entry.type) ?? new Set();
|
|
71
|
+
positionsForType.add(record.trafficPosition);
|
|
72
|
+
unrecognizedLifecycleTypes.set(entry.type, positionsForType);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (unrecognizedLifecycleTypes.size > 0) {
|
|
77
|
+
diagnostics.report(operatorDiagnostic("broker", "warning", "traffic projection", describeUnrecognizedLifecycleTypes(unrecognizedLifecycleTypes), now()));
|
|
78
|
+
}
|
|
79
|
+
return traffic;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Describe every unrecognized lifecycle type dropped in one projection run.
|
|
83
|
+
*
|
|
84
|
+
* One line names each distinct rejected value and how many records it touched, rather than one
|
|
85
|
+
* line per affected record.
|
|
86
|
+
* @param unrecognizedLifecycleTypes - Rejected type name to the durable positions carrying it.
|
|
87
|
+
* @returns - Single diagnostic line naming the field path, each rejected value, and its record count.
|
|
88
|
+
*/
|
|
89
|
+
function describeUnrecognizedLifecycleTypes(unrecognizedLifecycleTypes) {
|
|
90
|
+
const perType = [...unrecognizedLifecycleTypes.entries()].map(([type, affectedPositions]) => {
|
|
91
|
+
const count = affectedPositions.size;
|
|
92
|
+
return `${JSON.stringify(type)} (${String(count)} record${count === 1 ? "" : "s"}: position${count === 1 ? "" : "s"} ${[...affectedPositions].sort((left, right) => left - right).join(", ")})`;
|
|
93
|
+
});
|
|
94
|
+
return `traffic.lifecycle.type: unrecognized value ${perType.join("; ")} — entry dropped, delivery retained`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Word the skips one traffic read reported: one line per kind of skipped row, and one per truncated
|
|
98
|
+
* table, rather than one per row.
|
|
99
|
+
*
|
|
100
|
+
* Each line is a frame on the live operator channel, whose queue a read naming thousands of rows
|
|
101
|
+
* would overrun.
|
|
102
|
+
* @param skips - Skips the read handed back beside its records.
|
|
103
|
+
* @returns - Diagnostic lines for the operator's log: one per skip kind, and one per truncated table.
|
|
104
|
+
*/
|
|
105
|
+
function describeSkips(skips) {
|
|
106
|
+
const messages = skips.flatMap((skip) => (skip.type === "traffic_row_skipped" ? [skip] : []));
|
|
107
|
+
const pings = skips.flatMap((skip) => (skip.type === "ping_row_skipped" ? [skip] : []));
|
|
108
|
+
const lines = [
|
|
109
|
+
describeRowSkips("message", messages.map((skip) => ({ id: skip.messageId, unreadable: skip.unreadable }))),
|
|
110
|
+
describeRowSkips("ping audit", pings.map((skip) => ({ id: skip.auditId, unreadable: skip.unreadable }))),
|
|
111
|
+
].filter((line) => line !== null);
|
|
112
|
+
for (const skip of skips) {
|
|
113
|
+
if (skip.type === "skips_truncated") {
|
|
114
|
+
lines.push(`${skip.table}: ${String(skip.total)} rows hold a durable position this build cannot order; ${String(skip.named)} named`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Word every skipped row of one kind as a single line.
|
|
121
|
+
*
|
|
122
|
+
* Names the first row and counts them all: a diagnostic summary is clamped, and a second identifier
|
|
123
|
+
* would be cut off.
|
|
124
|
+
* @param noun - What each row is, singular.
|
|
125
|
+
* @param rows - The skipped rows, each with the columns that could not be read.
|
|
126
|
+
* @returns - One line naming the row, or counting them and naming the first; null for none.
|
|
127
|
+
*/
|
|
128
|
+
function describeRowSkips(noun, rows) {
|
|
129
|
+
const [first] = rows;
|
|
130
|
+
if (first === undefined)
|
|
131
|
+
return null;
|
|
132
|
+
const detail = `${first.id} skipped: stored columns could not be read: ${first.unreadable.join(", ")}`;
|
|
133
|
+
return rows.length === 1 ? `${noun} ${detail}` : `${String(rows.length)} ${noun}s skipped; first: ${noun} ${detail}`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Name one durable record for a diagnostic, by whichever identity its kind carries.
|
|
137
|
+
* @param record - Durable record being reported on.
|
|
138
|
+
* @returns - Identity phrase for the diagnostic line.
|
|
139
|
+
*/
|
|
140
|
+
function describeRecord(record) {
|
|
141
|
+
return record.kind === "accepted_delivery" ? `message ${record.messageId}` : `ping audit ${record.auditId}`;
|
|
142
|
+
}
|
|
143
|
+
function projectRecord(record) {
|
|
144
|
+
switch (record.kind) {
|
|
145
|
+
case "accepted_delivery":
|
|
146
|
+
return validateProjectedTraffic({
|
|
147
|
+
kind: "accepted_delivery",
|
|
148
|
+
trafficId: record.messageId,
|
|
149
|
+
messageId: record.messageId,
|
|
150
|
+
trafficPosition: record.trafficPosition,
|
|
151
|
+
acceptedSequence: record.trafficPosition,
|
|
152
|
+
fromAgentId: record.senderAgentId,
|
|
153
|
+
toAgentId: record.recipientAgentId,
|
|
154
|
+
teamId: record.teamId,
|
|
155
|
+
workspaceId: record.workspaceId,
|
|
156
|
+
acceptedAt: record.acceptedAt,
|
|
157
|
+
subject: record.subject,
|
|
158
|
+
type: record.type,
|
|
159
|
+
state: record.state,
|
|
160
|
+
body: projectBody(record),
|
|
161
|
+
lifecycle: record.lifecycle.map((entry) => ({
|
|
162
|
+
...entry,
|
|
163
|
+
state: lifecycleState(entry.type),
|
|
164
|
+
})),
|
|
165
|
+
});
|
|
166
|
+
case "ping_audit":
|
|
167
|
+
return validateProjectedTraffic({
|
|
168
|
+
kind: "ping_audit",
|
|
169
|
+
trafficId: record.auditId,
|
|
170
|
+
auditId: record.auditId,
|
|
171
|
+
trafficPosition: record.trafficPosition,
|
|
172
|
+
controlId: record.controlId,
|
|
173
|
+
targetAgentId: record.targetAgentId,
|
|
174
|
+
actor: record.actor,
|
|
175
|
+
acceptedAt: record.acceptedAt,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function projectBody(record) {
|
|
180
|
+
const storedBytes = Buffer.byteLength(record.encodedBody, "utf8");
|
|
181
|
+
if (storedBytes !== record.bodySizeBytes || storedBytes > MESSAGE_BODY_LIMIT_BYTES) {
|
|
182
|
+
throw new OperatorTrafficProjectionError("retained traffic body metadata is inconsistent");
|
|
183
|
+
}
|
|
184
|
+
let decoded;
|
|
185
|
+
try {
|
|
186
|
+
decoded = decodeMessageBody(record.encodedBody);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw new OperatorTrafficProjectionError("retained traffic body is not valid JSON");
|
|
190
|
+
}
|
|
191
|
+
const expectedContentType = typeof decoded === "string" ? "text" : "json";
|
|
192
|
+
if (record.contentType !== expectedContentType) {
|
|
193
|
+
throw new OperatorTrafficProjectionError("retained traffic content type is inconsistent");
|
|
194
|
+
}
|
|
195
|
+
return validateOperatorMessageBody(retainedBody(decoded, record.contentType));
|
|
196
|
+
}
|
|
197
|
+
function retainedBody(content, contentType) {
|
|
198
|
+
const rawCopy = contentType === "text" && typeof content === "string" ? content : JSON.stringify(content);
|
|
199
|
+
if (typeof rawCopy !== "string") {
|
|
200
|
+
throw new OperatorTrafficProjectionError("retained traffic has no raw-copy representation");
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
content,
|
|
204
|
+
rawCopy,
|
|
205
|
+
sizeBytes: Buffer.byteLength(rawCopy, "utf8"),
|
|
206
|
+
contentType,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function validateProjectedTraffic(value) {
|
|
210
|
+
try {
|
|
211
|
+
return validateOperatorTraffic(value);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error instanceof OperatorValidationError) {
|
|
215
|
+
throw new OperatorTrafficProjectionError(`invalid durable traffic projection: ${error.message}`);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Read the state a durable lifecycle event type observes.
|
|
222
|
+
*
|
|
223
|
+
* A type outside the broker's known vocabulary is informational: it carries no state of its own.
|
|
224
|
+
* @param type - Durable lifecycle event type read back from storage.
|
|
225
|
+
* @returns - The state the type observes, or null when it is informational or unrecognized.
|
|
226
|
+
*/
|
|
227
|
+
function lifecycleState(type) {
|
|
228
|
+
return lifecycleEventState(type) ?? null;
|
|
229
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type OperatorCommandEnvelope, type OperatorCommandResult, type OperatorCursor } from "./protocol.js";
|
|
2
|
+
import type { OperatorOpenResult, OperatorPublication } from "./updates.js";
|
|
3
|
+
/** Schedules recurring liveness checks without binding deterministic tests to process timers. */
|
|
4
|
+
export interface OperatorTransportSchedulerPort {
|
|
5
|
+
/**
|
|
6
|
+
* Schedule one recurring callback.
|
|
7
|
+
* @param callback - Liveness work invoked at each interval.
|
|
8
|
+
* @param intervalMs - Positive interval in milliseconds.
|
|
9
|
+
* @returns A callback that cancels future invocations.
|
|
10
|
+
*/
|
|
11
|
+
recurring(callback: () => void, intervalMs: number): () => void;
|
|
12
|
+
}
|
|
13
|
+
/** Supplies explicit loopback, time, identity, and liveness policy. */
|
|
14
|
+
export type OperatorTransportOptions = {
|
|
15
|
+
/** Selects an interface that runtime validation restricts to numeric loopback. */ host: string;
|
|
16
|
+
/** Selects a port, with zero requesting an operating-system-assigned port. */ port: number;
|
|
17
|
+
/** Supplies transport-observation timestamps. */ now: () => number;
|
|
18
|
+
/** Issues public-safe opaque client diagnostic identities. */ issueClientId: () => string;
|
|
19
|
+
/** Owns recurring liveness scheduling. */ scheduler: OperatorTransportSchedulerPort;
|
|
20
|
+
/** Selects heartbeat cadence in milliseconds. */ heartbeatIntervalMs: number;
|
|
21
|
+
/** Selects the maximum interval without a valid acknowledgement. */ livenessTimeoutMs: number;
|
|
22
|
+
/** Selects the maximum interval before an initial subscription. Defaults to the liveness timeout. */
|
|
23
|
+
handshakeTimeoutMs?: number;
|
|
24
|
+
/** Bounds active accepted connections. Defaults to a finite production limit. */ maxConnections?: number;
|
|
25
|
+
/** Bounds distinct in-flight snapshot versions while allowing byte sharing between clients. */
|
|
26
|
+
maxSnapshotTransfers?: number;
|
|
27
|
+
/** Executes validated non-authoritative operator commands. */ commands: OperatorTransportCommandPort;
|
|
28
|
+
};
|
|
29
|
+
/** Supplies operator command admission without granting the transport presentation authority. */
|
|
30
|
+
export interface OperatorTransportCommandPort {
|
|
31
|
+
/**
|
|
32
|
+
* Execute one validated command.
|
|
33
|
+
* @param command - Command envelope received from the loopback client.
|
|
34
|
+
* @returns Correlated command admission outcome.
|
|
35
|
+
*/
|
|
36
|
+
execute(command: OperatorCommandEnvelope): OperatorCommandResult;
|
|
37
|
+
}
|
|
38
|
+
/** Reports bounded transport observations without addresses, payloads, or thrown values. */
|
|
39
|
+
export type OperatorTransportDiagnostic = {
|
|
40
|
+
/** Identifies the observed transport boundary. */
|
|
41
|
+
type: "connected" | "connection_rejected" | "subscribed" | "resync_required" | "incompatible_version" | "malformed_request" | "heartbeat_sent" | "liveness_acknowledged" | "handshake_timeout" | "liveness_timeout" | "slow_consumer" | "stream_unavailable" | "disconnected";
|
|
42
|
+
/** Identifies one connection without retaining its network address. */ clientId: string;
|
|
43
|
+
/** Records when the boundary was observed. */ at: number;
|
|
44
|
+
};
|
|
45
|
+
/** Owns the bound loopback endpoint and deterministic liveness controls. */
|
|
46
|
+
export type OperatorTransportHandle = {
|
|
47
|
+
/** Locates the newline-delimited TCP endpoint. */ url: string;
|
|
48
|
+
/**
|
|
49
|
+
* Return bounded transport observations.
|
|
50
|
+
* @returns Defensive diagnostic copies.
|
|
51
|
+
*/
|
|
52
|
+
diagnostics(): OperatorTransportDiagnostic[];
|
|
53
|
+
/** Run one liveness pass without waiting for the scheduler. */
|
|
54
|
+
pulse(): void;
|
|
55
|
+
/** Stop accepting clients and close all active connections. */
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
};
|
|
58
|
+
/** Supplies the authoritative update seam consumed by one transport instance. */
|
|
59
|
+
export interface OperatorTransportUpdatePort {
|
|
60
|
+
/**
|
|
61
|
+
* Establish replay and subsequent publication for one connection.
|
|
62
|
+
* @param cursor - Last incorporated position, or null for cold hydration.
|
|
63
|
+
* @param subscriber - Consumer of subsequent ordered publications.
|
|
64
|
+
* @returns Initial hydration state and a deterministic detach callback.
|
|
65
|
+
*/
|
|
66
|
+
open(cursor: OperatorCursor | null, subscriber: (publication: OperatorPublication) => void): {
|
|
67
|
+
result: OperatorOpenResult;
|
|
68
|
+
unsubscribe: () => void;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Return the latest authoritative position.
|
|
72
|
+
* @returns Cursor safe to report through liveness frames.
|
|
73
|
+
*/
|
|
74
|
+
cursor(): OperatorCursor;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start a loopback-only newline-delimited operator stream.
|
|
78
|
+
* @param updates - Authoritative snapshot, replay, and publication service.
|
|
79
|
+
* @param options - Explicit bind, clock, identity, scheduler, and liveness policy.
|
|
80
|
+
* @returns Bound transport handle.
|
|
81
|
+
*/
|
|
82
|
+
export declare function startOperatorTransport(updates: OperatorTransportUpdatePort, options: OperatorTransportOptions): Promise<OperatorTransportHandle>;
|
|
83
|
+
/**
|
|
84
|
+
* Issue one bounded public-safe transport diagnostic identity.
|
|
85
|
+
* @returns An opaque identity that contains no network address or process binding.
|
|
86
|
+
*/
|
|
87
|
+
export declare function issueOperatorClientId(): string;
|