@fastagent-sh/fastagent 0.18.0 → 0.20.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/README.md +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +23 -0
- package/dist/atomic-write.js +53 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +55 -0
- package/dist/channels/agentcore-service.js +107 -0
- package/dist/channels/agentcore.d.ts +22 -20
- package/dist/channels/agentcore.js +20 -11
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +55 -7
- package/dist/channels/control.js +312 -67
- package/dist/channels/discover.d.ts +59 -0
- package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +6 -9
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +41 -24
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
- package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
- package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
- package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +2 -2
- package/dist/channels/slack/invoke-turn.js +13 -6
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +23 -55
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +20 -20
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +67 -13
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +3 -3
- package/dist/channels/telegram/telegram.js +60 -72
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +25 -40
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -5
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +80 -121
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +22 -70
- package/dist/cli/serve.js +66 -257
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +53 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +11 -6
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +295 -0
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +5 -5
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +67 -42
- package/dist/engines/pi/create.js +142 -72
- package/dist/engines/pi/definition.d.ts +42 -8
- package/dist/engines/pi/definition.js +114 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +39 -20
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +10 -8
- package/dist/engines/pi/open.js +21 -11
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +27 -0
- package/dist/engines/pi/session-builder.js +87 -118
- package/dist/engines/pi/session-control.d.ts +28 -14
- package/dist/engines/pi/session-control.js +570 -438
- package/dist/engines/pi/session-inheritance.d.ts +30 -0
- package/dist/engines/pi/session-inheritance.js +261 -0
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +15 -3
- package/dist/engines/pi/session-settings.js +22 -1
- package/dist/engines/pi/session-store.d.ts +140 -0
- package/dist/engines/pi/session-store.js +666 -0
- package/dist/engines/pi/tool-context.d.ts +36 -13
- package/dist/engines/pi/tool-context.js +51 -4
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +3 -3
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/env.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +37 -2
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +2 -2
- package/dist/schedule/wakeups.js +11 -8
- package/dist/service.d.ts +157 -0
- package/dist/service.js +376 -0
- package/dist/session-remote.d.ts +8 -3
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +229 -93
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +14 -5
- package/dist/engines/pi/channel.d.ts +0 -37
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
package/dist/session-remote.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SSE_HEARTBEAT_MS } from "./channels/http.js";
|
|
2
2
|
import { abortFirstIterator } from "./collect.js";
|
|
3
|
+
import { isAddressableSession, } from "./session.js";
|
|
3
4
|
/** Dead-connection watchdog for SSE reads: the server heartbeats every SSE_HEARTBEAT_MS, so a
|
|
4
5
|
* PENDING READ seeing no bytes (of ANY kind — comments included) for this many missed beats
|
|
5
6
|
* means the connection is a black hole. The stream is aborted and surfaced as an error, so a
|
|
@@ -31,11 +32,34 @@ function idleWatchdog(abort) {
|
|
|
31
32
|
* trouble branches on `status`, never on message prose. */
|
|
32
33
|
export class ControlRequestError extends Error {
|
|
33
34
|
status;
|
|
34
|
-
|
|
35
|
+
/** The plane's own error code, when the reply carried one (`sessions()` is the only read that
|
|
36
|
+
* does today — design §13). Absent for a plain-text rejection (401) or a proxy's page: a caller
|
|
37
|
+
* distinguishing "this deployment cannot list sessions" from "the endpoint is unreachable" reads
|
|
38
|
+
* THIS, not the status. */
|
|
39
|
+
code;
|
|
40
|
+
constructor(status, body, code) {
|
|
35
41
|
super(`control request failed: ${status} ${body}`);
|
|
36
42
|
this.status = status;
|
|
43
|
+
if (code !== undefined)
|
|
44
|
+
this.code = code;
|
|
37
45
|
}
|
|
38
46
|
}
|
|
47
|
+
/** A non-2xx reply as an error, carrying the plane's code when the reply declared one. */
|
|
48
|
+
async function controlError(res) {
|
|
49
|
+
const body = await res.text();
|
|
50
|
+
if (!res.headers.get("content-type")?.includes("application/json"))
|
|
51
|
+
return new ControlRequestError(res.status, body);
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(body);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// The reply declared JSON and is not — a protocol fault worth seeing, but not worth losing the
|
|
58
|
+
// status over: both travel in one error rather than a bare SyntaxError from a rejection path.
|
|
59
|
+
return new ControlRequestError(res.status, `${body} (declared application/json but did not parse)`);
|
|
60
|
+
}
|
|
61
|
+
return new ControlRequestError(res.status, body, typeof parsed.code === "string" ? parsed.code : undefined);
|
|
62
|
+
}
|
|
39
63
|
/**
|
|
40
64
|
* Connect and return a remote `SessionControl`. Async because `capabilities()` is synchronous in
|
|
41
65
|
* the contract: the static declaration is fetched ONCE here and served from memory — which also
|
|
@@ -59,10 +83,104 @@ export async function connectSessionControl(options) {
|
|
|
59
83
|
const get = async (path, timeoutMs = REQUEST_TIMEOUT_MS) => {
|
|
60
84
|
const res = await fetchFn(`${base}${path}`, { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
61
85
|
if (!res.ok)
|
|
62
|
-
throw
|
|
86
|
+
throw await controlError(res);
|
|
63
87
|
return (await res.json());
|
|
64
88
|
};
|
|
65
89
|
const capabilities = await get("/control/capabilities");
|
|
90
|
+
const eventsOf = (session) => {
|
|
91
|
+
// Each ITERATION opens its own connection (gen/abort created inside asyncIterator), matching
|
|
92
|
+
// the local hub's "every iteration is a fresh subscription" — a shared single-use generator
|
|
93
|
+
// would make the second for-await silently empty, breaking local/remote isomorphism.
|
|
94
|
+
// The abort controller lives OUTSIDE the generator: a consumer's `return()`/`break` while the
|
|
95
|
+
// generator is suspended on a quiet SSE read must abort the fetch FIRST — an async generator's
|
|
96
|
+
// own finally only runs after the pending await settles, which a silent stream never does.
|
|
97
|
+
const openStream = (abort) => (async function* iterate() {
|
|
98
|
+
// Armed BEFORE the fetch: the connect phase (headers never arriving from a black-holed
|
|
99
|
+
// endpoint) is otherwise a window no timeout covers — the same watchdog terminates it,
|
|
100
|
+
// with headers-arrival counting as the first sign of life.
|
|
101
|
+
const watchdog = idleWatchdog(abort);
|
|
102
|
+
watchdog.arm(); // the connect await is a pending read
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetchFn(`${base}/control/sessions/${encodeURIComponent(session)}/events`, {
|
|
105
|
+
headers,
|
|
106
|
+
signal: abort.signal,
|
|
107
|
+
});
|
|
108
|
+
watchdog.disarm(); // headers arrived
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
// The error body is a pending read too — a half-dead tunnel serving 4xx headers then
|
|
111
|
+
// black-holing the body must not hang the round outside every budget. Re-armed: the
|
|
112
|
+
// watchdog aborts the read and the round fails with the dead-connection diagnosis.
|
|
113
|
+
watchdog.arm();
|
|
114
|
+
throw new ControlRequestError(res.status, await res.text());
|
|
115
|
+
}
|
|
116
|
+
if (!res.body)
|
|
117
|
+
throw new Error("control events: response has no body");
|
|
118
|
+
let nextSeq = 0;
|
|
119
|
+
for await (const data of sseData(res.body, watchdog)) {
|
|
120
|
+
// Parse discipline, same as the other two wire planes (dispatch parses, invoke
|
|
121
|
+
// classifies drift): a non-JSON or non-envelope payload is PROTOCOL MISMATCH —
|
|
122
|
+
// thrown, so a consumer's failure budget applies — never misdiagnosed as an
|
|
123
|
+
// in-transit gap whose remedy (reconnect) can never fix it.
|
|
124
|
+
let wire;
|
|
125
|
+
try {
|
|
126
|
+
// The ONE envelope type (control.ts's WireEvent) — an inline shape would let the
|
|
127
|
+
// envelope drift server-side while this cast silently kept the old fields.
|
|
128
|
+
wire = JSON.parse(data);
|
|
129
|
+
}
|
|
130
|
+
catch (parseError) {
|
|
131
|
+
throw new Error(`control events: non-JSON data on the stream (${String(parseError)}) — protocol mismatch?`);
|
|
132
|
+
}
|
|
133
|
+
if (typeof wire.seq !== "number" || typeof wire.event !== "object" || wire.event === null) {
|
|
134
|
+
throw new Error("control events: malformed envelope — the endpoint does not speak this protocol version");
|
|
135
|
+
}
|
|
136
|
+
// Envelope checks — consumed HERE. (epoch is not compared: it cannot change within
|
|
137
|
+
// one connection — see the header note.) A gap THROWS like a protocol mismatch: the
|
|
138
|
+
// consumer's failure path (budget, its own io) owns the diagnostic — a library-level
|
|
139
|
+
// log would bypass consumer output discipline, and a silent clean end would be
|
|
140
|
+
// indistinguishable from the server closing normally.
|
|
141
|
+
if (wire.seq !== nextSeq) {
|
|
142
|
+
throw new Error(`control events: sequence gap (expected ${nextSeq}, got ${wire.seq}) — events were lost in transit; resync via entries()`);
|
|
143
|
+
}
|
|
144
|
+
nextSeq = wire.seq + 1;
|
|
145
|
+
yield wire.event;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (abort.signal.aborted) {
|
|
150
|
+
if (watchdog.stale()) {
|
|
151
|
+
throw new Error(`control events: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection; resync via entries()`);
|
|
152
|
+
}
|
|
153
|
+
return; // the consumer walked away — clean end, not an error
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
watchdog.stop();
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
return {
|
|
162
|
+
[Symbol.asyncIterator]() {
|
|
163
|
+
const abort = new AbortController();
|
|
164
|
+
// Abort-first cancellation (see abortFirstIterator): aborting the connection unblocks a
|
|
165
|
+
// generator suspended on a quiet stream read.
|
|
166
|
+
return abortFirstIterator(openStream(abort), () => abort.abort());
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
/** A write that answers a `SessionResult`: the result rides HTTP 200 either way (`ok: false` is a
|
|
171
|
+
* protocol answer, not a transport failure), so a non-2xx here is a REAL transport/auth fault. */
|
|
172
|
+
const write = async (path, method, body) => {
|
|
173
|
+
const res = await fetchFn(`${base}${path}`, {
|
|
174
|
+
method,
|
|
175
|
+
headers: body === undefined ? headers : { ...headers, "content-type": "application/json" },
|
|
176
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
177
|
+
signal: AbortSignal.timeout(PAYLOAD_TIMEOUT_MS),
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok)
|
|
180
|
+
throw await controlError(res);
|
|
181
|
+
return (await res.json());
|
|
182
|
+
};
|
|
183
|
+
const id = (session) => encodeURIComponent(session);
|
|
66
184
|
return {
|
|
67
185
|
capabilities: () => capabilities,
|
|
68
186
|
// NOT prefetched like capabilities: a live definition can grow a skill between calls, so the
|
|
@@ -82,98 +200,50 @@ export async function connectSessionControl(options) {
|
|
|
82
200
|
throw error;
|
|
83
201
|
}
|
|
84
202
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Each ITERATION opens its own connection (gen/abort created inside asyncIterator), matching
|
|
100
|
-
// the local hub's "every iteration is a fresh subscription" — a shared single-use generator
|
|
101
|
-
// would make the second for-await silently empty, breaking local/remote isomorphism.
|
|
102
|
-
// The abort controller lives OUTSIDE the generator: a consumer's `return()`/`break` while the
|
|
103
|
-
// generator is suspended on a quiet SSE read must abort the fetch FIRST — an async generator's
|
|
104
|
-
// own finally only runs after the pending await settles, which a silent stream never does.
|
|
105
|
-
const openStream = (abort) => (async function* iterate() {
|
|
106
|
-
// Armed BEFORE the fetch: the connect phase (headers never arriving from a black-holed
|
|
107
|
-
// endpoint) is otherwise a window no timeout covers — the same watchdog terminates it,
|
|
108
|
-
// with headers-arrival counting as the first sign of life.
|
|
109
|
-
const watchdog = idleWatchdog(abort);
|
|
110
|
-
watchdog.arm(); // the connect await is a pending read
|
|
111
|
-
try {
|
|
112
|
-
const res = await fetchFn(`${base}/control/events?session=${encodeURIComponent(session)}`, {
|
|
113
|
-
headers,
|
|
114
|
-
signal: abort.signal,
|
|
115
|
-
});
|
|
116
|
-
watchdog.disarm(); // headers arrived
|
|
117
|
-
if (!res.ok) {
|
|
118
|
-
// The error body is a pending read too — a half-dead tunnel serving 4xx headers then
|
|
119
|
-
// black-holing the body must not hang the round outside every budget. Re-armed: the
|
|
120
|
-
// watchdog aborts the read and the round fails with the dead-connection diagnosis.
|
|
121
|
-
watchdog.arm();
|
|
122
|
-
throw new ControlRequestError(res.status, await res.text());
|
|
123
|
-
}
|
|
124
|
-
if (!res.body)
|
|
125
|
-
throw new Error("control events: response has no body");
|
|
126
|
-
let nextSeq = 0;
|
|
127
|
-
for await (const data of sseData(res.body, watchdog)) {
|
|
128
|
-
// Parse discipline, same as the other two wire planes (dispatch parses, invoke
|
|
129
|
-
// classifies drift): a non-JSON or non-envelope payload is PROTOCOL MISMATCH —
|
|
130
|
-
// thrown, so a consumer's failure budget applies — never misdiagnosed as an
|
|
131
|
-
// in-transit gap whose remedy (reconnect) can never fix it.
|
|
132
|
-
let wire;
|
|
133
|
-
try {
|
|
134
|
-
// The ONE envelope type (control.ts's WireEvent) — an inline shape would let the
|
|
135
|
-
// envelope drift server-side while this cast silently kept the old fields.
|
|
136
|
-
wire = JSON.parse(data);
|
|
137
|
-
}
|
|
138
|
-
catch (parseError) {
|
|
139
|
-
throw new Error(`control events: non-JSON data on the stream (${String(parseError)}) — protocol mismatch?`);
|
|
140
|
-
}
|
|
141
|
-
if (typeof wire.seq !== "number" || typeof wire.event !== "object" || wire.event === null) {
|
|
142
|
-
throw new Error("control events: malformed envelope — the endpoint does not speak this protocol version");
|
|
143
|
-
}
|
|
144
|
-
// Envelope checks — consumed HERE. (epoch is not compared: it cannot change within
|
|
145
|
-
// one connection — see the header note.) A gap THROWS like a protocol mismatch: the
|
|
146
|
-
// consumer's failure path (budget, its own io) owns the diagnostic — a library-level
|
|
147
|
-
// log would bypass consumer output discipline, and a silent clean end would be
|
|
148
|
-
// indistinguishable from the server closing normally.
|
|
149
|
-
if (wire.seq !== nextSeq) {
|
|
150
|
-
throw new Error(`control events: sequence gap (expected ${nextSeq}, got ${wire.seq}) — events were lost in transit; resync via entries()`);
|
|
151
|
-
}
|
|
152
|
-
nextSeq = wire.seq + 1;
|
|
153
|
-
yield wire.event;
|
|
154
|
-
}
|
|
203
|
+
sessions: {
|
|
204
|
+
// Rejects when the deployment cannot enumerate its store — the coded 503 arrives as a
|
|
205
|
+
// ControlRequestError carrying `sessions_unavailable`, so a client can tell it from an
|
|
206
|
+
// unreachable endpoint instead of retrying forever.
|
|
207
|
+
list: () => get("/control/sessions", PAYLOAD_TIMEOUT_MS),
|
|
208
|
+
// PUT: the fork is idempotent, and so is the request that carries it. `into` becomes a path
|
|
209
|
+
// segment exactly like `get`'s id, so it is refused on the same rule — without this the local
|
|
210
|
+
// plane answers `invalid_command` while the wire answers 404 from a URL that normalised away.
|
|
211
|
+
// ASYNC, so the guard REJECTS rather than throwing out of a method typed `Promise`: a caller
|
|
212
|
+
// that wrote `.catch(…)` — or handed this to `Promise.all` — must not be surprised by a
|
|
213
|
+
// synchronous throw. (`get` may throw: it is synchronous by signature.)
|
|
214
|
+
fork: async ({ from, at, into }) => {
|
|
215
|
+
if (!isAddressableSession(into)) {
|
|
216
|
+
throw new Error(`session id ${JSON.stringify(into)} cannot travel as a URL path segment — this transport cannot address it`);
|
|
155
217
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
218
|
+
return write(`/control/sessions/${id(into)}`, "PUT", { from, at });
|
|
219
|
+
},
|
|
220
|
+
// The local hub's handle is a pure binding; so is this one — an id and the transport above it.
|
|
221
|
+
// Nothing is FETCHED here, which is what keeps the two isomorphic. What is checked is the one
|
|
222
|
+
// thing the wire cannot express: `.` and `..` survive `encodeURIComponent` and are then
|
|
223
|
+
// normalised away by URL parsing, so every call on such a handle would arrive at a DIFFERENT
|
|
224
|
+
// route — `.` reads as the collection (200 JSON, which the SSE reader ends as a silently empty
|
|
225
|
+
// stream) and `..` as a 404 the local plane answers normally. Refused at the binding, where a
|
|
226
|
+
// caller can see it, rather than once per call in a place it looks like a server answer.
|
|
227
|
+
get: (session) => {
|
|
228
|
+
if (!isAddressableSession(session)) {
|
|
229
|
+
throw new Error(`session id ${JSON.stringify(session)} cannot travel as a URL path segment — this transport cannot address it`);
|
|
167
230
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
231
|
+
return {
|
|
232
|
+
id: session,
|
|
233
|
+
state: () => get(`/control/sessions/${id(session)}`),
|
|
234
|
+
entries: (options) => get(`/control/sessions/${id(session)}/entries${options?.since !== undefined ? `?since=${encodeURIComponent(options.since)}` : ""}`, PAYLOAD_TIMEOUT_MS),
|
|
235
|
+
events: () => eventsOf(session),
|
|
236
|
+
update: (patch) => write(`/control/sessions/${id(session)}`, "PATCH", patch),
|
|
237
|
+
steer: (prompt) => write(`/control/sessions/${id(session)}/actions`, "POST", { type: "steer", prompt }),
|
|
238
|
+
followUp: (prompt) => write(`/control/sessions/${id(session)}/actions`, "POST", { type: "follow_up", prompt }),
|
|
239
|
+
abort: () => write(`/control/sessions/${id(session)}/actions`, "POST", { type: "abort" }),
|
|
240
|
+
compact: (options) => write(`/control/sessions/${id(session)}/actions`, "POST", {
|
|
241
|
+
type: "compact",
|
|
242
|
+
...(options?.instructions !== undefined ? { instructions: options.instructions } : {}),
|
|
243
|
+
}),
|
|
244
|
+
delete: () => write(`/control/sessions/${id(session)}`, "DELETE"),
|
|
245
|
+
};
|
|
246
|
+
},
|
|
177
247
|
},
|
|
178
248
|
};
|
|
179
249
|
}
|