@naswerks/periscope 1.0.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/SECURITY.md +315 -0
- package/dist/bin/command.d.ts +97 -0
- package/dist/bin/command.js +141 -0
- package/dist/bin/config.d.ts +7 -0
- package/dist/bin/config.js +90 -0
- package/dist/bin/login.d.ts +48 -0
- package/dist/bin/login.js +82 -0
- package/dist/bin/main.d.ts +33 -0
- package/dist/bin/main.js +173 -0
- package/dist/bin/pair.d.ts +28 -0
- package/dist/bin/pair.js +198 -0
- package/dist/bin/periscope.d.ts +3 -0
- package/dist/bin/periscope.js +4 -0
- package/dist/bin/posture.d.ts +64 -0
- package/dist/bin/posture.js +157 -0
- package/dist/bin/reconfigure.d.ts +64 -0
- package/dist/bin/reconfigure.js +195 -0
- package/dist/bin/serve.d.ts +103 -0
- package/dist/bin/serve.js +441 -0
- package/dist/bin/status.d.ts +11 -0
- package/dist/bin/status.js +49 -0
- package/dist/bin/workspaces.d.ts +109 -0
- package/dist/bin/workspaces.js +215 -0
- package/dist/control/backoff.d.ts +25 -0
- package/dist/control/backoff.js +20 -0
- package/dist/control/codec.d.ts +15 -0
- package/dist/control/codec.js +501 -0
- package/dist/control/credential.d.ts +23 -0
- package/dist/control/credential.js +8 -0
- package/dist/control/frames.d.ts +1180 -0
- package/dist/control/frames.js +441 -0
- package/dist/control/link-state.d.ts +32 -0
- package/dist/control/link-state.js +39 -0
- package/dist/control/link.d.ts +109 -0
- package/dist/control/link.js +736 -0
- package/dist/control/queue.d.ts +110 -0
- package/dist/control/queue.js +174 -0
- package/dist/control/seq.d.ts +49 -0
- package/dist/control/seq.js +52 -0
- package/dist/control/stream-routing.d.ts +200 -0
- package/dist/control/stream-routing.js +167 -0
- package/dist/control/stream.d.ts +43 -0
- package/dist/control/stream.js +64 -0
- package/dist/core/async-queue.d.ts +26 -0
- package/dist/core/async-queue.js +63 -0
- package/dist/core/failure.d.ts +22 -0
- package/dist/core/failure.js +53 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/index.js +7 -0
- package/dist/core/keyed-turns.d.ts +28 -0
- package/dist/core/keyed-turns.js +44 -0
- package/dist/core/paths.d.ts +25 -0
- package/dist/core/paths.js +77 -0
- package/dist/core/refusal.d.ts +43 -0
- package/dist/core/refusal.js +399 -0
- package/dist/core/result.d.ts +22 -0
- package/dist/core/result.js +16 -0
- package/dist/core/time.d.ts +19 -0
- package/dist/core/time.js +25 -0
- package/dist/core/vocab.d.ts +23 -0
- package/dist/core/vocab.js +37 -0
- package/dist/core/workspace-id.d.ts +12 -0
- package/dist/core/workspace-id.js +12 -0
- package/dist/gate/command.d.ts +136 -0
- package/dist/gate/command.js +551 -0
- package/dist/gate/decision.d.ts +111 -0
- package/dist/gate/decision.js +97 -0
- package/dist/gate/escalate.d.ts +82 -0
- package/dist/gate/escalate.js +92 -0
- package/dist/gate/gate.d.ts +173 -0
- package/dist/gate/gate.js +417 -0
- package/dist/gate/index.d.ts +39 -0
- package/dist/gate/index.js +31 -0
- package/dist/gate/jail.d.ts +92 -0
- package/dist/gate/jail.js +131 -0
- package/dist/gate/local.d.ts +69 -0
- package/dist/gate/local.js +51 -0
- package/dist/gate/outcome.d.ts +72 -0
- package/dist/gate/outcome.js +91 -0
- package/dist/gate/shell.d.ts +36 -0
- package/dist/gate/shell.js +226 -0
- package/dist/host/agent-process.d.ts +452 -0
- package/dist/host/agent-process.js +382 -0
- package/dist/host/bulk-post.d.ts +44 -0
- package/dist/host/bulk-post.js +114 -0
- package/dist/host/claude-transcripts.d.ts +84 -0
- package/dist/host/claude-transcripts.js +339 -0
- package/dist/host/config-file.d.ts +60 -0
- package/dist/host/config-file.js +216 -0
- package/dist/host/git-facts.d.ts +7 -0
- package/dist/host/git-facts.js +106 -0
- package/dist/host/hooks.d.ts +56 -0
- package/dist/host/hooks.js +75 -0
- package/dist/host/host.d.ts +356 -0
- package/dist/host/host.js +1294 -0
- package/dist/host/index.d.ts +35 -0
- package/dist/host/index.js +22 -0
- package/dist/host/link-state-file.d.ts +18 -0
- package/dist/host/link-state-file.js +66 -0
- package/dist/host/loopback.d.ts +38 -0
- package/dist/host/loopback.js +122 -0
- package/dist/host/machine.d.ts +9 -0
- package/dist/host/machine.js +19 -0
- package/dist/host/mcp-server.d.ts +11 -0
- package/dist/host/mcp-server.js +48 -0
- package/dist/host/package-facts.d.ts +3 -0
- package/dist/host/package-facts.js +26 -0
- package/dist/host/paired-credential-store.d.ts +32 -0
- package/dist/host/paired-credential-store.js +112 -0
- package/dist/host/paths.d.ts +80 -0
- package/dist/host/paths.js +165 -0
- package/dist/host/repository-read.d.ts +33 -0
- package/dist/host/repository-read.js +201 -0
- package/dist/host/session-store.d.ts +53 -0
- package/dist/host/session-store.js +100 -0
- package/dist/host/sign-in.d.ts +90 -0
- package/dist/host/sign-in.js +239 -0
- package/dist/host/telemetry.d.ts +42 -0
- package/dist/host/telemetry.js +74 -0
- package/dist/host/token-cache.d.ts +62 -0
- package/dist/host/token-cache.js +185 -0
- package/dist/host/transcript-fs.d.ts +4 -0
- package/dist/host/transcript-fs.js +104 -0
- package/dist/host/wire-request.d.ts +80 -0
- package/dist/host/wire-request.js +196 -0
- package/dist/host/workspace-fs.d.ts +14 -0
- package/dist/host/workspace-fs.js +84 -0
- package/dist/host/workspace-trust.d.ts +23 -0
- package/dist/host/workspace-trust.js +80 -0
- package/dist/identity/authorize.d.ts +59 -0
- package/dist/identity/authorize.js +72 -0
- package/dist/identity/config.d.ts +98 -0
- package/dist/identity/config.js +130 -0
- package/dist/identity/credential.d.ts +91 -0
- package/dist/identity/credential.js +76 -0
- package/dist/identity/device-code.d.ts +115 -0
- package/dist/identity/device-code.js +134 -0
- package/dist/identity/index.d.ts +33 -0
- package/dist/identity/index.js +10 -0
- package/dist/identity/mode.d.ts +106 -0
- package/dist/identity/mode.js +69 -0
- package/dist/identity/paired-credential.d.ts +52 -0
- package/dist/identity/paired-credential.js +43 -0
- package/dist/identity/pkce.d.ts +50 -0
- package/dist/identity/pkce.js +94 -0
- package/dist/identity/store.d.ts +43 -0
- package/dist/identity/store.js +43 -0
- package/dist/identity/token.d.ts +58 -0
- package/dist/identity/token.js +149 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +53 -0
- package/dist/mcp/descriptor.d.ts +59 -0
- package/dist/mcp/descriptor.js +14 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/schema.d.ts +37 -0
- package/dist/mcp/schema.js +175 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/server.js +66 -0
- package/dist/persistence/entry.d.ts +74 -0
- package/dist/persistence/entry.js +105 -0
- package/dist/persistence/index.d.ts +22 -0
- package/dist/persistence/index.js +8 -0
- package/dist/persistence/key.d.ts +46 -0
- package/dist/persistence/key.js +33 -0
- package/dist/persistence/mirror.d.ts +71 -0
- package/dist/persistence/mirror.js +57 -0
- package/dist/persistence/receipt.d.ts +111 -0
- package/dist/persistence/receipt.js +85 -0
- package/dist/persistence/retention.d.ts +68 -0
- package/dist/persistence/retention.js +68 -0
- package/dist/persistence/store.d.ts +106 -0
- package/dist/persistence/store.js +86 -0
- package/dist/persistence/transition-log.d.ts +73 -0
- package/dist/persistence/transition-log.js +133 -0
- package/dist/protocol.d.ts +27 -0
- package/dist/protocol.js +12 -0
- package/dist/sessions/index.d.ts +8 -0
- package/dist/sessions/index.js +4 -0
- package/dist/sessions/registry.d.ts +186 -0
- package/dist/sessions/registry.js +190 -0
- package/dist/sessions/session.d.ts +178 -0
- package/dist/sessions/session.js +288 -0
- package/dist/sessions/spawn-env.d.ts +88 -0
- package/dist/sessions/spawn-env.js +277 -0
- package/dist/state/coverage.d.ts +308 -0
- package/dist/state/coverage.js +315 -0
- package/dist/state/index.d.ts +12 -0
- package/dist/state/index.js +7 -0
- package/dist/state/machine.d.ts +112 -0
- package/dist/state/machine.js +237 -0
- package/dist/state/model.d.ts +233 -0
- package/dist/state/model.js +214 -0
- package/dist/state/observer.d.ts +55 -0
- package/dist/state/observer.js +413 -0
- package/dist/state/reporter.d.ts +45 -0
- package/dist/state/reporter.js +35 -0
- package/dist/state/store.d.ts +56 -0
- package/dist/state/store.js +120 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.js +2 -0
- package/dist/telemetry/usage.d.ts +78 -0
- package/dist/telemetry/usage.js +69 -0
- package/dist/workspace/git-worktree.d.ts +150 -0
- package/dist/workspace/git-worktree.js +417 -0
- package/dist/workspace/index.d.ts +9 -0
- package/dist/workspace/index.js +4 -0
- package/dist/workspace/plain-dir.d.ts +34 -0
- package/dist/workspace/plain-dir.js +90 -0
- package/dist/workspace/provider.d.ts +152 -0
- package/dist/workspace/provider.js +2 -0
- package/dist/workspace/worktree-porcelain.d.ts +29 -0
- package/dist/workspace/worktree-porcelain.js +100 -0
- package/docs/architecture.md +277 -0
- package/docs/configuration.md +187 -0
- package/docs/gate.md +219 -0
- package/docs/identity.md +107 -0
- package/docs/protocol.md +501 -0
- package/docs/state-machine.md +160 -0
- package/examples/README.md +141 -0
- package/examples/minimal-controller/controller.ts +38 -0
- package/examples/parallel-run-proof/permission-mode-probe.ts +129 -0
- package/examples/parallel-run-proof/run.ts +850 -0
- package/examples/test-controller/controller.ts +655 -0
- package/examples/test-controller/serve.ts +29 -0
- package/examples/tsconfig.json +30 -0
- package/package.json +98 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { isRefusalReason } from '../core/refusal.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bumped when a change is not backward-compatible. Exchanged in the hello handshake.
|
|
4
|
+
*
|
|
5
|
+
* The rule: a new payload kind, a new member on an existing kind, a member whose nullability
|
|
6
|
+
* changed, or a new hello member is a bump, even when the change is additive in practice. The
|
|
7
|
+
* handshake's job is to let a peer refuse a version it does not know rather than to guess whether
|
|
8
|
+
* a difference happened to be survivable: every declared member is required on the wire, so a
|
|
9
|
+
* peer one version behind refuses the whole frame, and because refusals are not acknowledged that
|
|
10
|
+
* refusal would be a permanent silent wedge on the session lane. The handshake refuses by version
|
|
11
|
+
* instead, by name, once. A new value in the open `capabilities` list is not a bump; a new
|
|
12
|
+
* `RefusalReason` is one, because a strict encoder on the other side must learn it first.
|
|
13
|
+
*
|
|
14
|
+
* Every bump re-approves `contracts/wire-vectors/` (`npm run contracts:update`) and regenerates
|
|
15
|
+
* any consumer's readers.
|
|
16
|
+
*/
|
|
17
|
+
export const PROTOCOL_VERSION = 9;
|
|
18
|
+
/**
|
|
19
|
+
* The oldest protocol version this build still speaks. A hello advertises the window
|
|
20
|
+
* `[PROTOCOL_VERSION_MIN, PROTOCOL_VERSION]` beside `protocolVersion`; the controller answers with
|
|
21
|
+
* its choice inside the overlap and the host accepts any version in its own window. The window
|
|
22
|
+
* opens at the first negotiated version; from the next bump on it is one minor wide, the version
|
|
23
|
+
* before the current one staying supported for one release. A hello with no range does not
|
|
24
|
+
* decode, so a version older than the first negotiated one cannot be inside the window.
|
|
25
|
+
*/
|
|
26
|
+
export const PROTOCOL_VERSION_MIN = 9;
|
|
27
|
+
/**
|
|
28
|
+
* A frame larger than this is refused by the codec.
|
|
29
|
+
*
|
|
30
|
+
* This is the mechanical half of "commands only, never payloads": bulk bytes cannot ride the link
|
|
31
|
+
* even by accident, because a frame carrying them will not encode. The refusal names the bulk lane
|
|
32
|
+
* so the failure teaches the fix rather than just reporting a size.
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_FRAME_BYTES = 64 * 1024;
|
|
35
|
+
/** Build the update carrying a transition. The only supported way to put one on the wire. */
|
|
36
|
+
export function stateTransitionUpdate(transition) {
|
|
37
|
+
const body = { update: 'state_transition', transition };
|
|
38
|
+
return { kind: 'session_update', body: body };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read a transition back out, or null when the body is some other kind of update.
|
|
42
|
+
*
|
|
43
|
+
* Null rather than a throw: an older host receiving a body it does not model must keep going, and
|
|
44
|
+
* a consumer that gets null knows to leave the frame alone rather than to treat it as corrupt.
|
|
45
|
+
*/
|
|
46
|
+
export function readStateTransition(body) {
|
|
47
|
+
const update = body;
|
|
48
|
+
if (update.update !== 'state_transition')
|
|
49
|
+
return null;
|
|
50
|
+
const transition = update.transition;
|
|
51
|
+
if (transition === undefined || typeof transition.seq !== 'number' || transition.cause === undefined) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return transition;
|
|
55
|
+
}
|
|
56
|
+
/** Build the update carrying an agent message. The only supported way to put one on the wire. */
|
|
57
|
+
export function agentMessageUpdate(message) {
|
|
58
|
+
const body = { update: 'agent_message', message };
|
|
59
|
+
return { kind: 'session_update', body: body };
|
|
60
|
+
}
|
|
61
|
+
/** Read a forwarded message back out, or null when the body is some other kind of update. */
|
|
62
|
+
export function readAgentMessage(body) {
|
|
63
|
+
const update = body;
|
|
64
|
+
if (update.update !== 'agent_message')
|
|
65
|
+
return null;
|
|
66
|
+
const message = update.message;
|
|
67
|
+
if (typeof message !== 'object' || message === null || Array.isArray(message))
|
|
68
|
+
return null;
|
|
69
|
+
return message;
|
|
70
|
+
}
|
|
71
|
+
/** Build the update carrying a refusal about the peer's own traffic. The only supported way. */
|
|
72
|
+
export function wireRefusalUpdate(refusal, expected, received) {
|
|
73
|
+
const body = { update: 'wire_refusal', refusal, expected, received };
|
|
74
|
+
return { kind: 'session_update', body: body };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Read a wire refusal back out, or null when the body is some other kind of update.
|
|
78
|
+
*
|
|
79
|
+
* Null rather than a throw, for the same reason `readStateTransition` returns null: a peer that does
|
|
80
|
+
* not model this member must keep going rather than treat the frame as corrupt. A v1 controller
|
|
81
|
+
* reaching this body gets null from all three readers and leaves the frame alone — which is a
|
|
82
|
+
* degraded outcome, not a broken one, and is the honest cost the version bump exists to announce.
|
|
83
|
+
*/
|
|
84
|
+
export function readWireRefusal(body) {
|
|
85
|
+
const update = body;
|
|
86
|
+
if (update.update !== 'wire_refusal')
|
|
87
|
+
return null;
|
|
88
|
+
if (typeof update.expected !== 'number' || typeof update.received !== 'number')
|
|
89
|
+
return null;
|
|
90
|
+
const refusal = update.refusal;
|
|
91
|
+
if (refusal === undefined || typeof refusal.reason !== 'string' || typeof refusal.detail !== 'string') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
update: 'wire_refusal',
|
|
96
|
+
refusal: { reason: refusal.reason, detail: refusal.detail },
|
|
97
|
+
expected: update.expected,
|
|
98
|
+
received: update.received,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build a delta carrying one message verbatim.
|
|
103
|
+
*
|
|
104
|
+
* The body is the same shape a `session_update` carries, deliberately: the lanes differ in what
|
|
105
|
+
* losing a frame costs, not in what a frame says, so `readAgentMessage` reads either. A consumer
|
|
106
|
+
* that had to parse two shapes for one idea would have been handed the drift this package's
|
|
107
|
+
* vocabulary rules exist to prevent.
|
|
108
|
+
*/
|
|
109
|
+
export function agentMessageDelta(message) {
|
|
110
|
+
const body = { update: 'agent_message', message };
|
|
111
|
+
return { kind: 'session_delta', body: body };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build a `session_new`. The only supported way to put one on the wire.
|
|
115
|
+
*
|
|
116
|
+
* It exists for the reason `stateTransitionUpdate` does, and for one more. The wire's rule is that
|
|
117
|
+
* absent is `null` and never an omitted property, but a hand-written literal makes that a
|
|
118
|
+
* discipline every author has to remember, and the moment one is forgotten the frame carries
|
|
119
|
+
* `undefined`, which `JSON.stringify` deletes. The field then arrives absent, and "absent" and
|
|
120
|
+
* "explicitly unset" are the two states this file spends its opening paragraph refusing to
|
|
121
|
+
* conflate. Passing `asked` as a partial and filling every remaining key with `null` here makes
|
|
122
|
+
* the rule a property of construction instead of a rule in a comment.
|
|
123
|
+
*
|
|
124
|
+
* The argument is a partial of the wire type; what comes out is fully populated. So a caller writes
|
|
125
|
+
* only what it means, and no caller can produce a frame that says nothing where it meant nothing.
|
|
126
|
+
*/
|
|
127
|
+
export function sessionNew(cwd, asked = {}) {
|
|
128
|
+
return {
|
|
129
|
+
kind: 'session_new',
|
|
130
|
+
cwd,
|
|
131
|
+
workspaceKey: asked.workspaceKey ?? null,
|
|
132
|
+
correlationId: asked.correlationId ?? null,
|
|
133
|
+
request: asked.request ?? null,
|
|
134
|
+
gate: asked.gate ?? null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build a `session_new.request`. Same argument as `sessionNew`: every unstated key becomes `null`.
|
|
139
|
+
*
|
|
140
|
+
* The partial is the convenience and the full object is the contract. Nothing about the wire is
|
|
141
|
+
* relaxed here: a controller in another language builds the whole object, and this is the shortcut
|
|
142
|
+
* for the one that happens to be written in this one.
|
|
143
|
+
*/
|
|
144
|
+
export function sessionNewRequest(asked = {}) {
|
|
145
|
+
return {
|
|
146
|
+
resume: asked.resume ?? null,
|
|
147
|
+
fork: asked.fork ?? null,
|
|
148
|
+
settingSources: asked.settingSources ?? null,
|
|
149
|
+
plugins: asked.plugins ?? null,
|
|
150
|
+
mcpServers: asked.mcpServers ?? null,
|
|
151
|
+
strictMcpConfig: asked.strictMcpConfig ?? null,
|
|
152
|
+
includePartialMessages: asked.includePartialMessages ?? null,
|
|
153
|
+
thinking: asked.thinking ?? null,
|
|
154
|
+
forwardSubagentText: asked.forwardSubagentText ?? null,
|
|
155
|
+
env: asked.env ?? null,
|
|
156
|
+
model: asked.model ?? null,
|
|
157
|
+
systemPrompt: asked.systemPrompt ?? null,
|
|
158
|
+
effort: asked.effort ?? null,
|
|
159
|
+
permissionMode: asked.permissionMode ?? null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Every key a `session_new.request` carries, as data, so a check can enumerate them at run time.
|
|
164
|
+
*
|
|
165
|
+
* It exists so a pin cannot quietly stop covering something. The egress pin walks a populated
|
|
166
|
+
* request looking for a field that could name a transcript destination, and a walk is only as
|
|
167
|
+
* complete as the object it is handed: a hand-built fixture silently stops being full the moment a
|
|
168
|
+
* later change adds a key, and the pin then passes while covering less, with nothing to say so.
|
|
169
|
+
*
|
|
170
|
+
* `satisfies Record<keyof SessionNewRequest, true>` makes the compiler the enforcer: adding a field
|
|
171
|
+
* to the type without adding it here does not build. So the pin's fixture is checked against a list
|
|
172
|
+
* that cannot fall behind the type it describes.
|
|
173
|
+
*/
|
|
174
|
+
export const SESSION_NEW_REQUEST_KEYS = {
|
|
175
|
+
resume: true,
|
|
176
|
+
fork: true,
|
|
177
|
+
settingSources: true,
|
|
178
|
+
plugins: true,
|
|
179
|
+
mcpServers: true,
|
|
180
|
+
strictMcpConfig: true,
|
|
181
|
+
includePartialMessages: true,
|
|
182
|
+
thinking: true,
|
|
183
|
+
forwardSubagentText: true,
|
|
184
|
+
env: true,
|
|
185
|
+
model: true,
|
|
186
|
+
systemPrompt: true,
|
|
187
|
+
effort: true,
|
|
188
|
+
permissionMode: true,
|
|
189
|
+
};
|
|
190
|
+
/** The same, for the payload itself. Same argument, one level up. */
|
|
191
|
+
export const SESSION_NEW_KEYS = {
|
|
192
|
+
kind: true,
|
|
193
|
+
cwd: true,
|
|
194
|
+
workspaceKey: true,
|
|
195
|
+
correlationId: true,
|
|
196
|
+
request: true,
|
|
197
|
+
gate: true,
|
|
198
|
+
};
|
|
199
|
+
/** Build a `bulk_delivered`. Fills the stat pair with null so an absent value is stated, not deleted. */
|
|
200
|
+
export function bulkDelivered(deliveryId, byteCount, stat = {}) {
|
|
201
|
+
return {
|
|
202
|
+
kind: 'bulk_delivered',
|
|
203
|
+
deliveryId,
|
|
204
|
+
byteCount,
|
|
205
|
+
sizeBytes: stat.sizeBytes ?? null,
|
|
206
|
+
mtimeMs: stat.mtimeMs ?? null,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The most releases one `workspace_release_bulk` may carry: one inventory page, so a controller
|
|
211
|
+
* can clean what it was just shown in one ask, and an answer of that size stays well under the
|
|
212
|
+
* frame cap.
|
|
213
|
+
*/
|
|
214
|
+
export const MAX_BULK_RELEASES = 25;
|
|
215
|
+
/** Build a `workspace_release`. A string target is a key; `{ path }` addresses by path. */
|
|
216
|
+
export function workspaceRelease(requestId, target, flags = {}) {
|
|
217
|
+
return {
|
|
218
|
+
kind: 'workspace_release',
|
|
219
|
+
requestId,
|
|
220
|
+
workspaceKey: typeof target === 'string' ? target : null,
|
|
221
|
+
path: typeof target === 'string' ? null : target.path,
|
|
222
|
+
deleteBranch: flags.deleteBranch ?? false,
|
|
223
|
+
force: flags.force ?? false,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Build a `workspace_release_result`. Every omitted member takes the released-or-absent value. */
|
|
227
|
+
export function workspaceReleaseResult(requestId, outcome = {}) {
|
|
228
|
+
return { kind: 'workspace_release_result', requestId, ...workspaceReleaseEntryResult(outcome) };
|
|
229
|
+
}
|
|
230
|
+
/** Build one entry result with the released-or-absent defaults. */
|
|
231
|
+
export function workspaceReleaseEntryResult(outcome = {}) {
|
|
232
|
+
return {
|
|
233
|
+
workspaceKey: outcome.workspaceKey ?? null,
|
|
234
|
+
path: outcome.path ?? null,
|
|
235
|
+
directoryRemoved: outcome.directoryRemoved ?? false,
|
|
236
|
+
branchDeleted: outcome.branchDeleted ?? false,
|
|
237
|
+
refusal: outcome.refusal ?? null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/** Build a `workspace_release_bulk`. */
|
|
241
|
+
export function workspaceReleaseBulk(requestId, releases) {
|
|
242
|
+
return { kind: 'workspace_release_bulk', requestId, releases };
|
|
243
|
+
}
|
|
244
|
+
/** Build a `workspace_release_bulk_result`. */
|
|
245
|
+
export function workspaceReleaseBulkResult(requestId, results) {
|
|
246
|
+
return { kind: 'workspace_release_bulk_result', requestId, results };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The most entries one `host_configure` may carry: the closed config key set is smaller than this,
|
|
250
|
+
* so a legal ask never meets the bound and an ask past it is malformed rather than large.
|
|
251
|
+
*/
|
|
252
|
+
export const MAX_CONFIGURE_ENTRIES = 8;
|
|
253
|
+
/**
|
|
254
|
+
* How many worktrees one `workspace_list_result` carries. A path is bounded at 1000 characters, a
|
|
255
|
+
* branch at 400 and a key at 200, so a full page stays under the frame cap with the same margin
|
|
256
|
+
* the transcript page keeps.
|
|
257
|
+
*/
|
|
258
|
+
export const WORKSPACE_PAGE_SIZE = 25;
|
|
259
|
+
/**
|
|
260
|
+
* How many entries one `transcript_list_result` carries.
|
|
261
|
+
*
|
|
262
|
+
* Bounded so the frame stays well under `MAX_FRAME_BYTES` (64 KiB): an entry is a slug (measured
|
|
263
|
+
* up to about 80 characters on one real 139-slug corpus), a 36-character session id and two
|
|
264
|
+
* integers, comfortably under 300 bytes of JSON each, so 100 entries is under half the cap even
|
|
265
|
+
* with slugs twice as long as any measured one.
|
|
266
|
+
*/
|
|
267
|
+
export const TRANSCRIPT_PAGE_SIZE = 100;
|
|
268
|
+
/**
|
|
269
|
+
* The bulk-lane locator namespace for a transcript read: `claude-transcript:{projectSlug}/{sessionId}`.
|
|
270
|
+
* A `bulk_request.what` that a host resolves to the agent CLI's own transcript files starts with
|
|
271
|
+
* this prefix; the slug is the CLI's flattened project directory name and is opaque.
|
|
272
|
+
*/
|
|
273
|
+
export const TRANSCRIPT_WHAT_PREFIX = 'claude-transcript:';
|
|
274
|
+
/** Build a `workspace_list`. The only supported way to put one on the wire. */
|
|
275
|
+
export function workspaceList(requestId, fromIndex = 0) {
|
|
276
|
+
return { kind: 'workspace_list', requestId, fromIndex };
|
|
277
|
+
}
|
|
278
|
+
/** Build a `workspace_list_result`. Fills `refusal` with null, the answered page. */
|
|
279
|
+
export function workspaceListResult(requestId, entries, page, refusal) {
|
|
280
|
+
return {
|
|
281
|
+
kind: 'workspace_list_result',
|
|
282
|
+
requestId,
|
|
283
|
+
entries,
|
|
284
|
+
totalCount: page.totalCount,
|
|
285
|
+
nextIndex: page.nextIndex ?? null,
|
|
286
|
+
defaultBranch: page.defaultBranch ?? null,
|
|
287
|
+
refusal: refusal ?? null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/** Build a `host_configure`. The only supported way to put one on the wire. */
|
|
291
|
+
export function hostConfigure(requestId, entries) {
|
|
292
|
+
return { kind: 'host_configure', requestId, entries };
|
|
293
|
+
}
|
|
294
|
+
/** Build a `host_configure_result`. Fills `refusal` with null, the applied answer. */
|
|
295
|
+
export function hostConfigureResult(requestId, configuration, overriddenByEnvironment, refusal, pendingRestart = []) {
|
|
296
|
+
return {
|
|
297
|
+
kind: 'host_configure_result',
|
|
298
|
+
requestId,
|
|
299
|
+
configuration,
|
|
300
|
+
overriddenByEnvironment,
|
|
301
|
+
pendingRestart,
|
|
302
|
+
refusal: refusal ?? null,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** Build a `session_list`. The only supported way to put one on the wire. */
|
|
306
|
+
export function sessionList(requestId) {
|
|
307
|
+
return { kind: 'session_list', requestId };
|
|
308
|
+
}
|
|
309
|
+
/** Build a `session_list_result`. */
|
|
310
|
+
export function sessionListResult(requestId, sessions, counts) {
|
|
311
|
+
return {
|
|
312
|
+
kind: 'session_list_result',
|
|
313
|
+
requestId,
|
|
314
|
+
sessions,
|
|
315
|
+
liveCount: counts.liveCount,
|
|
316
|
+
provisioningCount: counts.provisioningCount,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/** Build a `transcript_list`. `fromIndex` defaults to the start. */
|
|
320
|
+
export function transcriptList(requestId, fromIndex = 0) {
|
|
321
|
+
return { kind: 'transcript_list', requestId, fromIndex };
|
|
322
|
+
}
|
|
323
|
+
/** Build a `transcript_list_result`. Fills `nextIndex` with null so absence is stated, not deleted. */
|
|
324
|
+
export function transcriptListResult(requestId, entries, totals) {
|
|
325
|
+
return {
|
|
326
|
+
kind: 'transcript_list_result',
|
|
327
|
+
requestId,
|
|
328
|
+
entries,
|
|
329
|
+
totalCount: totals.totalCount,
|
|
330
|
+
nextIndex: totals.nextIndex ?? null,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/** Build a `transcript_tail`. Fills `needle` with null, meaning "any user-text entry". */
|
|
334
|
+
export function transcriptTail(requestId, projectSlug, sessionId, fromOffset, needle) {
|
|
335
|
+
return { kind: 'transcript_tail', requestId, projectSlug, sessionId, fromOffset, needle: needle ?? null };
|
|
336
|
+
}
|
|
337
|
+
/** Build a `transcript_tail_result`. Fills the stat pair with null so absence is stated. */
|
|
338
|
+
export function transcriptTailResult(requestId, answer) {
|
|
339
|
+
return {
|
|
340
|
+
kind: 'transcript_tail_result',
|
|
341
|
+
requestId,
|
|
342
|
+
found: answer.found,
|
|
343
|
+
absent: answer.absent,
|
|
344
|
+
newOffset: answer.newOffset,
|
|
345
|
+
sizeBytes: answer.sizeBytes ?? null,
|
|
346
|
+
mtimeMs: answer.mtimeMs ?? null,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Build a `transcript_failed`. */
|
|
350
|
+
export function transcriptFailed(requestId, refusal) {
|
|
351
|
+
return { kind: 'transcript_failed', requestId, refusal };
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// The repository read: a controller reading the operator's checkout, jailed and bounded.
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
/**
|
|
357
|
+
* How many entries one `repository_list_result` carries. A directory with more than this many
|
|
358
|
+
* children is listed to the cap and marked `truncated`; names are bounded by the filesystem, so a
|
|
359
|
+
* full page stays well under the frame cap.
|
|
360
|
+
*/
|
|
361
|
+
export const MAX_REPOSITORY_ENTRIES = 500;
|
|
362
|
+
/**
|
|
363
|
+
* The most text one `repository_read_result` carries. The frame cap is 64 KiB and the envelope plus
|
|
364
|
+
* JSON escaping must fit inside it, so the text is bounded below the cap with margin; a longer file
|
|
365
|
+
* is answered to this many bytes and marked `truncated`.
|
|
366
|
+
*/
|
|
367
|
+
export const MAX_REPOSITORY_READ_BYTES = 48 * 1024;
|
|
368
|
+
/** Build a `repository_list`. The only supported way to put one on the wire. */
|
|
369
|
+
export function repositoryList(requestId, path = '') {
|
|
370
|
+
return { kind: 'repository_list', requestId, path };
|
|
371
|
+
}
|
|
372
|
+
/** Build a `repository_list_result`. Fills `refusal` with null, the listed answer. */
|
|
373
|
+
export function repositoryListResult(requestId, entries, truncated = false, refusal) {
|
|
374
|
+
return { kind: 'repository_list_result', requestId, entries, truncated, refusal: refusal ?? null };
|
|
375
|
+
}
|
|
376
|
+
/** Build a `repository_read`. Fills `maxBytes` with the cap. */
|
|
377
|
+
export function repositoryRead(requestId, path, maxBytes = MAX_REPOSITORY_READ_BYTES) {
|
|
378
|
+
return { kind: 'repository_read', requestId, path, maxBytes };
|
|
379
|
+
}
|
|
380
|
+
/** Build a `repository_read_result`. Fills `refusal` with null, the read answer. */
|
|
381
|
+
export function repositoryReadResult(requestId, read, refusal) {
|
|
382
|
+
return {
|
|
383
|
+
kind: 'repository_read_result',
|
|
384
|
+
requestId,
|
|
385
|
+
text: read.text,
|
|
386
|
+
sizeBytes: read.sizeBytes,
|
|
387
|
+
truncated: read.truncated,
|
|
388
|
+
refusal: refusal ?? null,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
export function readRefusal(wire) {
|
|
392
|
+
if (isRefusalReason(wire.reason)) {
|
|
393
|
+
return { recognised: true, refusal: { reason: wire.reason, detail: wire.detail } };
|
|
394
|
+
}
|
|
395
|
+
return { recognised: false, raw: wire.reason, detail: wire.detail };
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* What may be dropped when the offline queue is full, stated as data so the decision lives in one
|
|
399
|
+
* place instead of inside a queue method. Everything absent from this list is a transition or a
|
|
400
|
+
* receipt, and losing one of those is a lie about what happened.
|
|
401
|
+
*/
|
|
402
|
+
export const DROPPABLE_KINDS = ['session_delta'];
|
|
403
|
+
export function isDroppable(kind) {
|
|
404
|
+
return DROPPABLE_KINDS.includes(kind);
|
|
405
|
+
}
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Control payloads — the link's own lifecycle. Never sequenced, never replayed.
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
/**
|
|
410
|
+
* The longest value `HostConfiguration` carries. Paths and URLs are host-controlled text on a
|
|
411
|
+
* frame the controller must accept before anything else, so each is bounded here rather than
|
|
412
|
+
* trusted; a controller stores them under the same cap.
|
|
413
|
+
*/
|
|
414
|
+
export const MAX_CONFIGURATION_VALUE_LENGTH = 1000;
|
|
415
|
+
/** A `HostConfiguration` with nothing set: what a host composed without one reports. */
|
|
416
|
+
export function unsetHostConfiguration() {
|
|
417
|
+
return {
|
|
418
|
+
repositoryRoot: null,
|
|
419
|
+
workspaceRoot: null,
|
|
420
|
+
branchScheme: null,
|
|
421
|
+
transcriptsRoot: null,
|
|
422
|
+
controllerUrl: null,
|
|
423
|
+
decisionUrl: null,
|
|
424
|
+
agentHome: null,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export function isSessionFrame(frame) {
|
|
428
|
+
return frame.frame === 'session';
|
|
429
|
+
}
|
|
430
|
+
export function isControlFrame(frame) {
|
|
431
|
+
return frame.frame === 'control';
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* A frame's identity, and the reason `seq` is shaped this way: this string is exactly what an SSE
|
|
435
|
+
* `Last-Event-ID` header carries. Moving to one stream per session later needs no change to the
|
|
436
|
+
* frame; the cursor set decomposes into per-stream ids one for one.
|
|
437
|
+
*/
|
|
438
|
+
export function frameId(frame) {
|
|
439
|
+
return `${frame.sessionId}/${frame.seq}`;
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=frames.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The link's own state, as transitions that carry why.
|
|
3
|
+
*
|
|
4
|
+
* Reconnects are the most common thing that happens to an unattended run, so the trace has to show
|
|
5
|
+
* them — and a state change without a cause is exactly as useful as no state change at all when
|
|
6
|
+
* you are reading back a night nobody watched. `cause` is required by the type, so a transition
|
|
7
|
+
* without one does not compile.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* `open` is the socket; `accepted` is the controller's welcome. They are two states because they are
|
|
11
|
+
* two facts an operator asks about separately: a link that opens and is never welcomed is a version
|
|
12
|
+
* or credential problem, not a network one, and a reporter that saw only `open` could not tell them
|
|
13
|
+
* apart.
|
|
14
|
+
*/
|
|
15
|
+
export type LinkState = 'idle' | 'connecting' | 'open' | 'accepted' | 'backoff' | 'closed';
|
|
16
|
+
export declare const LINK_CAUSES: readonly ["start_requested", "socket_connected", "hello_completed", "socket_error", "socket_closed", "heartbeat_timeout", "connect_timeout", "protocol_version_rejected", "replay_requested", "credential_unavailable", "credential_rejected", "retry_scheduled", "shutdown_requested"];
|
|
17
|
+
export type LinkCause = (typeof LINK_CAUSES)[number];
|
|
18
|
+
export interface LinkTransition {
|
|
19
|
+
readonly from: LinkState;
|
|
20
|
+
readonly to: LinkState;
|
|
21
|
+
readonly cause: LinkCause;
|
|
22
|
+
readonly at: string;
|
|
23
|
+
/** Free text for a human reading a log. Never branched on. */
|
|
24
|
+
readonly detail: string | null;
|
|
25
|
+
}
|
|
26
|
+
export declare class LinkStateMachine {
|
|
27
|
+
#private;
|
|
28
|
+
get state(): LinkState;
|
|
29
|
+
/** Moves, and returns the transition. Returns null when already in `to` — no self-loops. */
|
|
30
|
+
to(next: LinkState, cause: LinkCause, at: string, detail?: string | null): LinkTransition | null;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=link-state.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const LINK_CAUSES = [
|
|
2
|
+
'start_requested',
|
|
3
|
+
'socket_connected',
|
|
4
|
+
'hello_completed',
|
|
5
|
+
'socket_error',
|
|
6
|
+
'socket_closed',
|
|
7
|
+
'heartbeat_timeout',
|
|
8
|
+
// The dial never completed its upgrade inside `connectTimeoutMs`; abandoned and retried.
|
|
9
|
+
'connect_timeout',
|
|
10
|
+
'protocol_version_rejected',
|
|
11
|
+
// The controller closed the socket because a session's seq jumped past what it holds; the next
|
|
12
|
+
// dial replays from the cursor it reports. Distinct from a version rejection, which a retry cannot heal.
|
|
13
|
+
'replay_requested',
|
|
14
|
+
'credential_unavailable',
|
|
15
|
+
// Not the same event as `credential_unavailable`; the difference is whether a retry can help.
|
|
16
|
+
// `credential_unavailable` means this attempt had no header to present and the next one may;
|
|
17
|
+
// `credential_rejected` means the identity provider has refused the material outright and will
|
|
18
|
+
// keep refusing until a person signs in. The first belongs in `backoff`. The second is the only
|
|
19
|
+
// cause on this list that ends in `closed` without a shutdown having been requested, because
|
|
20
|
+
// retrying it is a loop with no exit that reports success when the process finally drains.
|
|
21
|
+
'credential_rejected',
|
|
22
|
+
'retry_scheduled',
|
|
23
|
+
'shutdown_requested',
|
|
24
|
+
];
|
|
25
|
+
export class LinkStateMachine {
|
|
26
|
+
#state = 'idle';
|
|
27
|
+
get state() {
|
|
28
|
+
return this.#state;
|
|
29
|
+
}
|
|
30
|
+
/** Moves, and returns the transition. Returns null when already in `to` — no self-loops. */
|
|
31
|
+
to(next, cause, at, detail = null) {
|
|
32
|
+
if (this.#state === next)
|
|
33
|
+
return null;
|
|
34
|
+
const transition = { from: this.#state, to: next, cause, at, detail };
|
|
35
|
+
this.#state = next;
|
|
36
|
+
return transition;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=link-state.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Clock, Ticker } from '../core/time.js';
|
|
2
|
+
import type { Refusal } from '../core/refusal.js';
|
|
3
|
+
import type { Result } from '../core/result.js';
|
|
4
|
+
import type { ControllerCredential } from './credential.js';
|
|
5
|
+
import type { HostConfiguration, SessionCursor, SessionFrame, SessionPayload } from './frames.js';
|
|
6
|
+
import type { BackoffOptions } from './backoff.js';
|
|
7
|
+
import type { LinkTransition } from './link-state.js';
|
|
8
|
+
export interface LinkHandlers {
|
|
9
|
+
/** Every state change, with its cause. */
|
|
10
|
+
onTransition(transition: LinkTransition): void;
|
|
11
|
+
/** An in-order session frame from the controller. Duplicates never reach here. */
|
|
12
|
+
onSessionFrame(frame: SessionFrame): void;
|
|
13
|
+
/** Frames were lost between the two ends. Loud by design; replay exists to make this not happen. */
|
|
14
|
+
onGap(sessionId: string, expected: number, received: number): void;
|
|
15
|
+
/** Anything the link declined to do, named. */
|
|
16
|
+
onRefusal(refused: Refusal): void;
|
|
17
|
+
}
|
|
18
|
+
export interface LinkOptions {
|
|
19
|
+
readonly url: string;
|
|
20
|
+
readonly hostId: string;
|
|
21
|
+
readonly handlers: LinkHandlers;
|
|
22
|
+
readonly credential?: ControllerCredential;
|
|
23
|
+
readonly backoff?: BackoffOptions;
|
|
24
|
+
readonly clock?: Clock;
|
|
25
|
+
readonly ticker?: Ticker;
|
|
26
|
+
readonly random?: () => number;
|
|
27
|
+
/** Frames held while the link is down. */
|
|
28
|
+
readonly queueCapacity?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Extra capability markers for the `link_hello`, beside the built-in `bulk-post`.
|
|
31
|
+
* Values in an open string list: a peer that does not know one ignores it, so declaring a new
|
|
32
|
+
* marker is not a protocol change. The list is declarative: the controller is not obliged to act
|
|
33
|
+
* on any of it (see `LinkWelcome`; negotiation is declared on both sides, honoured by neither).
|
|
34
|
+
*/
|
|
35
|
+
readonly capabilities?: readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* The values this host runs with, reported on every `link_hello`. Omitted, the hello
|
|
38
|
+
* reports every member as null: a link composed without one has nothing to say, and says so
|
|
39
|
+
* rather than guessing.
|
|
40
|
+
*/
|
|
41
|
+
readonly configuration?: HostConfiguration;
|
|
42
|
+
/** The keys whose file value is not in effect. Omitted, the hello reports none. */
|
|
43
|
+
readonly pendingRestart?: readonly string[];
|
|
44
|
+
readonly heartbeatIntervalMs?: number;
|
|
45
|
+
/** No pong inside this window and the socket is declared dead, however alive TCP thinks it is. */
|
|
46
|
+
readonly heartbeatTimeoutMs?: number;
|
|
47
|
+
/** Above this many buffered bytes the link stops writing and queues instead. */
|
|
48
|
+
readonly highWaterMarkBytes?: number;
|
|
49
|
+
/**
|
|
50
|
+
* How long an ended session's written-but-unacked frames are held. See `ENDED_RETENTION_MS`.
|
|
51
|
+
*
|
|
52
|
+
* An option for the same reason the heartbeat's two timings are: the default is the decision, and
|
|
53
|
+
* a test that had to wait a real minute to observe a bound would be a test nobody runs.
|
|
54
|
+
*/
|
|
55
|
+
readonly endedRetentionMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* How long a dial may sit in `connecting` before it is abandoned and retried. Without it a
|
|
58
|
+
* controller that accepts the TCP handshake but never completes the upgrade (an API mid-restart)
|
|
59
|
+
* holds this link in `connecting` for minutes, until the OS gives up, while sessions wait.
|
|
60
|
+
*/
|
|
61
|
+
readonly connectTimeoutMs?: number;
|
|
62
|
+
}
|
|
63
|
+
export declare class ControllerLink {
|
|
64
|
+
#private;
|
|
65
|
+
constructor(options: LinkOptions);
|
|
66
|
+
/**
|
|
67
|
+
* Replace what the next `link_hello` declares. A host that reconfigured itself dials the same
|
|
68
|
+
* controller with the same identity; only the description moves, and it moves at the next
|
|
69
|
+
* hello rather than on a frame of its own, because the hello is where a controller reads it.
|
|
70
|
+
*/
|
|
71
|
+
announce(capabilities: readonly string[], configuration: HostConfiguration, pendingRestart?: readonly string[]): void;
|
|
72
|
+
get state(): import("./link-state.js").LinkState;
|
|
73
|
+
/** The version the controller chose at the last accepted handshake; null before one. */
|
|
74
|
+
get negotiatedVersion(): number | null;
|
|
75
|
+
get queueStats(): import("./queue.js").QueueStats;
|
|
76
|
+
/** Cursors of what this side has received: sent to the controller so it replays the right frames. */
|
|
77
|
+
cursors(): SessionCursor[];
|
|
78
|
+
start(): void;
|
|
79
|
+
/** Graceful close. Idempotent, and clears every timer: a stopped link holds no handles. */
|
|
80
|
+
stop(detail?: string): void;
|
|
81
|
+
/**
|
|
82
|
+
* Accept a payload for delivery: retained until acked, written as soon as the link allows.
|
|
83
|
+
*
|
|
84
|
+
* A `seq` is minted only when the frame is first written to the wire, so nothing refused or
|
|
85
|
+
* discarded before that point ever had one — the wire's numbering stays dense whatever happens
|
|
86
|
+
* here. `ok` means accepted and retained; a refusal names why nothing was accepted, and it is
|
|
87
|
+
* the caller's pressure signal — a dropped delta and a refused transition are different events
|
|
88
|
+
* and only the caller knows which one it can live with.
|
|
89
|
+
*/
|
|
90
|
+
send(sessionId: string, payload: SessionPayload): Result<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Release what this session holds. Called at session end, always.
|
|
93
|
+
*
|
|
94
|
+
* It does not discard frames the controller has not acked; see `BoundedFrameQueue.forget`.
|
|
95
|
+
* A session's last frames are its most important ones and are exactly the ones still unacked if
|
|
96
|
+
* the link is down when it ends. What is released here is everything that cannot still be owed:
|
|
97
|
+
* pending entries, which never had a seq, and the inbound cursor, which is about what the
|
|
98
|
+
* controller sent this side rather than what this side owes it.
|
|
99
|
+
*
|
|
100
|
+
* The outbound counter outlives the call while anything is still retained, and that is not an
|
|
101
|
+
* oversight. Resetting it while stamped frames for the same key are awaiting replay would let a
|
|
102
|
+
* later frame on that key be numbered below one already on the wire: a seq regression, which the
|
|
103
|
+
* receiver reads as a hole it can never fill. The counter is released by the ack that empties the
|
|
104
|
+
* retention instead, so the bound this method exists to keep is kept a moment later rather than
|
|
105
|
+
* abandoned.
|
|
106
|
+
*/
|
|
107
|
+
forgetSession(sessionId: string): void;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=link.d.ts.map
|