@code-yeongyu/senpi 2026.10.25-3 → 2026.10.25-4
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 +17 -0
- package/dist/bundle/chunks/{chunk-WLHNEUGD.js → chunk-2RB5EL4X.js} +1 -1
- package/dist/bundle/chunks/{chunk-MJ5NNWX4.js → chunk-3QCYBOKN.js} +1 -1
- package/dist/bundle/chunks/{chunk-FFQOEC76.js → chunk-4MOZXOJX.js} +3 -3
- package/dist/bundle/chunks/{chunk-ODTGC6YU.js → chunk-5JJDR6XI.js} +1 -1
- package/dist/bundle/chunks/chunk-6VWIMHMH.js +2 -0
- package/dist/bundle/chunks/{chunk-SWJXRC2I.js → chunk-BZ7726QO.js} +1 -1
- package/dist/bundle/chunks/{chunk-YRF4I43T.js → chunk-C2MQ4QXN.js} +12 -12
- package/dist/bundle/chunks/{chunk-7W7OJDIY.js → chunk-DODH3N6U.js} +4 -4
- package/dist/bundle/chunks/{chunk-EHQXLHRB.js → chunk-E6D4RIT7.js} +2 -2
- package/dist/bundle/chunks/{chunk-6O4SEW2W.js → chunk-J37GRED2.js} +1 -1
- package/dist/bundle/chunks/chunk-JMOUU75H.js +2 -0
- package/dist/bundle/chunks/{chunk-Z3UYNSVR.js → chunk-KKD4CYCI.js} +1 -1
- package/dist/bundle/chunks/{chunk-JA2R2ZSE.js → chunk-KNVNKDER.js} +1 -1
- package/dist/bundle/chunks/{chunk-CNL6QSUB.js → chunk-LOIF6PHI.js} +2 -2
- package/dist/bundle/chunks/{chunk-P3M2PF6K.js → chunk-N7IN46Q5.js} +1 -1
- package/dist/bundle/chunks/{chunk-C5UFJ7B3.js → chunk-PT6JJUN2.js} +1 -1
- package/dist/bundle/chunks/{chunk-O3Q2ET6W.js → chunk-SBGIXMX3.js} +1 -1
- package/dist/bundle/chunks/{chunk-HVJI5T75.js → chunk-STMXIBXF.js} +2 -2
- package/dist/bundle/chunks/{chunk-H3HNMY2W.js → chunk-TBU6VYHN.js} +1 -1
- package/dist/bundle/chunks/{chunk-UQNRW3VU.js → chunk-V2UYXTRR.js} +1 -1
- package/dist/bundle/chunks/chunk-XMJKZK5H.js +2 -0
- package/dist/bundle/chunks/cli-main-YYH3IBC6.js +3 -0
- package/dist/bundle/chunks/{dispatch-NYAULNVW.js → dispatch-MXG5YWLV.js} +1 -1
- package/dist/bundle/chunks/{host-command-Z3WPHJOW.js → host-command-TDLLHXVI.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-ONQIMWMX.js → host-lifecycle-3VHHIGDL.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-J65ENSV5.js → host-session-command-56XF6ACS.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-3CWE4265.js → host-socket-request-QL2WEZTN.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-OEPULLZK.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-YI2WZOET.js → multi-session-host-Z2CW4SDM.js} +2 -2
- package/dist/bundle/chunks/{package-manager-cli-GHZPJMRF.js → package-manager-cli-N55U6PEO.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-CBGCBYHV.js +2 -0
- package/dist/bundle/chunks/rpc-mode-ETXQWEB2.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-FFEQS744.js → rpc-monitor-source-AE3C6EQV.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-YWG6D6ZF.js → schedule-command-VS7CZC2J.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-4LEIDIVI.js +3 -0
- package/dist/bundle/chunks/session-picker-E4KUJDLO.js +2 -0
- package/dist/bundle/chunks/session-worker.js +45 -45
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/extensions/builtin/mcp/elicitation.js +2 -4
- package/dist/core/session-control-actions.js +0 -2
- package/dist/modes/interactive/session-control-attach.d.ts +20 -0
- package/dist/modes/interactive/session-control-attach.js +13 -0
- package/dist/modes/interactive/session-control-commands.js +16 -6
- package/dist/modes/interactive/session-control-registry.d.ts +2 -1
- package/dist/modes/interactive/session-control-registry.js +3 -2
- package/dist/modes/interactive/session-control-rpc-commands.d.ts +4 -2
- package/dist/modes/interactive/session-control-rpc-commands.js +114 -4
- package/dist/modes/interactive/session-control-server.js +9 -3
- package/dist/modes/rpc/connection-handler.js +6 -40
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/get-entries-command.d.ts +13 -0
- package/dist/modes/rpc/get-entries-command.js +61 -0
- package/dist/modes/rpc/get-entries-page.d.ts +22 -0
- package/dist/modes/rpc/get-entries-page.js +41 -2
- package/dist/modes/rpc/host-capabilities.js +4 -1
- package/dist/modes/rpc/host-observe-request.d.ts +2 -1
- package/dist/modes/rpc/host-observe-request.js +4 -1
- package/dist/modes/rpc/rpc-read-response-types.d.ts +2 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +8 -1
- package/docs/rpc.md +43 -11
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-3LNSLJ7N.js +0 -2
- package/dist/bundle/chunks/chunk-MSQCTH6V.js +0 -2
- package/dist/bundle/chunks/cli-main-HEKFFB5S.js +0 -3
- package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +0 -2
- package/dist/bundle/chunks/rotation-stream-ZPVCFOVD.js +0 -2
- package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +0 -3
- package/dist/bundle/chunks/session-picker-25H5KNYP.js +0 -2
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
* attaches by path or id, `subscribe_sessions`/`unsubscribe_sessions` run the host's status stream over
|
|
4
4
|
* this one session, `steer`/`follow_up`/`prompt` go through the session's external admission (the
|
|
5
5
|
* terminal's one delivery authority, so the user's draft still holds them) with the host's `whenIdle`
|
|
6
|
-
* meaning, and `get_effect_status` reads the same effect ledger.
|
|
6
|
+
* meaning, and `get_effect_status` reads the same effect ledger. The rest of the host's drive set answers with the
|
|
7
|
+
* host's contract (senpi#4267): `set_client_info` declares the connection's roles, `attach_session` and
|
|
8
|
+
* `release_attachment` take and drop a live attachment on the session's identity, `abort` runs the host's effect
|
|
9
|
+
* path and its role rule (a Stop needs `interactive_user`), and input sent with a `clientMessageId` is answered
|
|
10
|
+
* with an admission receipt that a retry gets again, marked `replayed`. A frame this file does not own answers
|
|
7
11
|
* `undefined` and falls through to the endpoint's other commands.
|
|
8
12
|
*/
|
|
9
13
|
import { randomUUID } from "node:crypto";
|
|
10
14
|
import { realpathSync } from "node:fs";
|
|
11
15
|
import { resolve } from "node:path";
|
|
12
|
-
import {
|
|
16
|
+
import { CONNECTION_ROLE_FIXED } from "../rpc/connection-roles.js";
|
|
17
|
+
import { INTERACTIVE_USER_CAPABILITY } from "../rpc/custom-capability.js";
|
|
18
|
+
import { runAbort, runGetEffectStatus } from "../rpc/effect-commands.js";
|
|
13
19
|
import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
|
|
14
20
|
import { answerSubscribeSessions, answerUnsubscribeSessions, refusal } from "../rpc/session-status-commands.js";
|
|
15
21
|
import { keyOf } from "./session-control-attach.js";
|
|
22
|
+
import { failure, success } from "./session-control-server.js";
|
|
16
23
|
export const TUI_RPC_COMMANDS = [
|
|
17
24
|
"open_session",
|
|
18
25
|
"subscribe_sessions",
|
|
@@ -21,7 +28,13 @@ export const TUI_RPC_COMMANDS = [
|
|
|
21
28
|
"follow_up",
|
|
22
29
|
"prompt",
|
|
23
30
|
"get_effect_status",
|
|
31
|
+
"set_client_info",
|
|
32
|
+
"attach_session",
|
|
33
|
+
"release_attachment",
|
|
34
|
+
"abort",
|
|
24
35
|
];
|
|
36
|
+
/** The host's answer to a Stop from a connection that never declared the user's role (senpi#3811). */
|
|
37
|
+
const STOP_REQUIRES_INTERACTIVE_CLIENT = "stop_requires_interactive_client: declare interactive_user with set_client_info first";
|
|
25
38
|
/** `null` = answered on the writer already; `undefined` = not a command this file owns. */
|
|
26
39
|
export async function runTuiRpcCommand(context, connection, command) {
|
|
27
40
|
switch (command.type) {
|
|
@@ -36,6 +49,14 @@ export async function runTuiRpcCommand(context, connection, command) {
|
|
|
36
49
|
return admitInput(context, command, command.type === "steer" ? "steer" : "followUp", command.whenIdle);
|
|
37
50
|
case "prompt":
|
|
38
51
|
return admitPrompt(context, command);
|
|
52
|
+
case "set_client_info":
|
|
53
|
+
return setClientInfo(context, connection, command);
|
|
54
|
+
case "attach_session":
|
|
55
|
+
return attachSession(context, connection, command);
|
|
56
|
+
case "release_attachment":
|
|
57
|
+
return releaseAttachment(context, connection, command);
|
|
58
|
+
case "abort":
|
|
59
|
+
return abort(context, connection, frame(command));
|
|
39
60
|
case "get_effect_status":
|
|
40
61
|
return runGetEffectStatus({
|
|
41
62
|
session: context.session,
|
|
@@ -74,6 +95,68 @@ function openSession(context, connection, command) {
|
|
|
74
95
|
});
|
|
75
96
|
return null;
|
|
76
97
|
}
|
|
98
|
+
/** Declares this connection's roles, fixed once declared, exactly as on a host; the other fields change nothing here. */
|
|
99
|
+
function setClientInfo(context, connection, command) {
|
|
100
|
+
const capabilities = command.capabilities;
|
|
101
|
+
if (capabilities === undefined)
|
|
102
|
+
return success(command.id, "set_client_info");
|
|
103
|
+
if (!Array.isArray(capabilities) || !capabilities.every((entry) => typeof entry === "string"))
|
|
104
|
+
return failure(command.id, "set_client_info", "capabilities must be an array of strings");
|
|
105
|
+
if (!context.attachments.roles.declare(keyOf(connection), capabilities))
|
|
106
|
+
return failure(command.id, "set_client_info", CONNECTION_ROLE_FIXED);
|
|
107
|
+
return success(command.id, "set_client_info");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A live attachment on the session's identity, as on a host: `sessionId` is this terminal's routing handle,
|
|
111
|
+
* `durableSessionId` its session id, and `expectedHostInstance`, when sent, this endpoint's instance id. The answer
|
|
112
|
+
* follows the replay of the in-flight snapshot, then the session's records reach the connection.
|
|
113
|
+
*/
|
|
114
|
+
function attachSession(context, connection, command) {
|
|
115
|
+
const { session, attachments } = context;
|
|
116
|
+
const id = idOf(command);
|
|
117
|
+
if (command.sessionId !== attachments.handle)
|
|
118
|
+
return refusal(id, "attach_session", "unknown_session", {
|
|
119
|
+
reason: "this terminal hosts one session and the request names another",
|
|
120
|
+
action: "attach with this terminal's sessionId from list_sessions",
|
|
121
|
+
});
|
|
122
|
+
const instanceMatches = command.expectedHostInstance === undefined || command.expectedHostInstance === context.instanceId;
|
|
123
|
+
if (command.durableSessionId !== session.sessionId || !instanceMatches)
|
|
124
|
+
return refusal(id, "attach_session", "session_identity_mismatch", {
|
|
125
|
+
reason: "durableSessionId or expectedHostInstance does not name this terminal's session",
|
|
126
|
+
action: "read list_sessions and get_protocol_info again, then attach with their ids",
|
|
127
|
+
});
|
|
128
|
+
attachments.run(connection, () => {
|
|
129
|
+
attachments.attach(connection);
|
|
130
|
+
attachments.writer.enqueue(attachments.handle, {
|
|
131
|
+
id: command.id,
|
|
132
|
+
type: "response",
|
|
133
|
+
command: "attach_session",
|
|
134
|
+
success: true,
|
|
135
|
+
data: { sessionId: attachments.handle, attached: true },
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function releaseAttachment(context, connection, command) {
|
|
141
|
+
if (command.sessionId !== context.attachments.handle || !context.attachments.detach(connection))
|
|
142
|
+
return refusal(idOf(command), "release_attachment", "session_not_attached", {
|
|
143
|
+
reason: "this connection is not attached to that session",
|
|
144
|
+
action: "attach first with attach_session or open_session",
|
|
145
|
+
});
|
|
146
|
+
// Nothing outlives the terminal's own attachment, so a release is never deferred.
|
|
147
|
+
return success(command.id, "release_attachment", { deferred: false });
|
|
148
|
+
}
|
|
149
|
+
/** The host's `abort`: the same effect receipts, and a Stop only from a connection that declared `interactive_user`. */
|
|
150
|
+
async function abort(context, connection, command) {
|
|
151
|
+
const key = keyOf(connection);
|
|
152
|
+
if (command.intent === "stop" && !context.attachments.roles.has(key, INTERACTIVE_USER_CAPABILITY))
|
|
153
|
+
return failure(command.id, "abort", STOP_REQUIRES_INTERACTIVE_CLIENT);
|
|
154
|
+
const { session } = context;
|
|
155
|
+
return runAbort({ session, by: { connectionId: key }, sessionId: context.attachments.handle }, command, () => (command.intent === "stop" ? session.stop() : session.abort()).catch((cause) => {
|
|
156
|
+
// Reported out of band, as on a host; the answer never waits on it.
|
|
157
|
+
connection.send({ type: "rpc_error", error: String(cause) });
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
77
160
|
function subscribe(context, connection, command) {
|
|
78
161
|
const { writer } = context.attachments;
|
|
79
162
|
const key = keyOf(connection);
|
|
@@ -145,6 +228,22 @@ function admitInput(context, command, deliverAs, whenIdle) {
|
|
|
145
228
|
});
|
|
146
229
|
const admission = context.session.externalAdmission;
|
|
147
230
|
const clientMessageId = typeof command.clientMessageId === "string" ? command.clientMessageId : undefined;
|
|
231
|
+
const fingerprint = JSON.stringify([command.type, message, deliverAs, whenIdle ?? null]);
|
|
232
|
+
const prior = clientMessageId === undefined ? undefined : context.attachments.receipts.get(clientMessageId);
|
|
233
|
+
if (prior !== undefined) {
|
|
234
|
+
if (prior.fingerprint !== fingerprint)
|
|
235
|
+
return refusal(id, command.type, "client_message_id_conflict", {
|
|
236
|
+
reason: `clientMessageId ${clientMessageId} was already used for different input`,
|
|
237
|
+
action: "send new input with a new clientMessageId",
|
|
238
|
+
});
|
|
239
|
+
return answered(id, command.type, {
|
|
240
|
+
disposition: prior.disposition,
|
|
241
|
+
clientMessageId,
|
|
242
|
+
replayed: true,
|
|
243
|
+
duplicate: true,
|
|
244
|
+
admission: prior.admission,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
148
247
|
const expected = typeof command.expected_turn_id === "number" ? command.expected_turn_id : admission.turnEpoch;
|
|
149
248
|
const result = admission.admit({
|
|
150
249
|
delivery_id: clientMessageId ?? `tui-rpc-${randomUUID()}`,
|
|
@@ -155,12 +254,23 @@ function admitInput(context, command, deliverAs, whenIdle) {
|
|
|
155
254
|
whenIdle: whenIdle === "start" ? "start" : "queue",
|
|
156
255
|
});
|
|
157
256
|
const identity = clientMessageId === undefined ? {} : { clientMessageId };
|
|
257
|
+
const admitted = (disposition) => {
|
|
258
|
+
if (clientMessageId === undefined)
|
|
259
|
+
return answered(id, command.type, { disposition });
|
|
260
|
+
const receipt = {
|
|
261
|
+
fingerprint,
|
|
262
|
+
disposition,
|
|
263
|
+
admission: { clientMessageId, durableSessionId: context.session.sessionId, disposition },
|
|
264
|
+
};
|
|
265
|
+
context.attachments.receipts.set(clientMessageId, receipt);
|
|
266
|
+
return answered(id, command.type, { disposition, clientMessageId, admission: receipt.admission });
|
|
267
|
+
};
|
|
158
268
|
switch (result.kind) {
|
|
159
269
|
case "started":
|
|
160
|
-
return
|
|
270
|
+
return admitted("started");
|
|
161
271
|
case "steered":
|
|
162
272
|
case "queued":
|
|
163
|
-
return
|
|
273
|
+
return admitted("queued");
|
|
164
274
|
case "already_admitted":
|
|
165
275
|
return answered(id, command.type, { disposition: "queued", duplicate: true, ...identity });
|
|
166
276
|
case "held_draft":
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
* (`authenticateSocket`, the handshake every RPC transport shares); a wrong secret is disconnected
|
|
4
4
|
* before a single JSONL line is read. After that it is the RPC framing: one LF-terminated JSON
|
|
5
5
|
* object per request, one response per request carrying its `id`, plus pushed feed events.
|
|
6
|
+
*
|
|
7
|
+
* The socket path is the endpoint's logical address on every platform (its secret lives beside it); on win32 the
|
|
8
|
+
* listener binds the named pipe the host transport derives from that path and the secret (senpi#4267), which is
|
|
9
|
+
* the address every client already connects to.
|
|
6
10
|
*/
|
|
7
11
|
import { chmod } from "node:fs/promises";
|
|
8
12
|
import { createServer } from "node:net";
|
|
9
13
|
import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS, serializeJsonLine } from "../rpc/jsonl.js";
|
|
10
14
|
import { rpcCommandShapeError } from "../rpc/rpc-input-validation.js";
|
|
11
15
|
import { socketSink } from "../rpc/socket-sink.js";
|
|
12
|
-
import { authenticateSocket } from "../rpc/socket-transport.js";
|
|
16
|
+
import { authenticateSocket, resolveSocketTransportAddress } from "../rpc/socket-transport.js";
|
|
13
17
|
export async function listenControlSocket(socketPath, secret, handlers) {
|
|
14
18
|
const clients = new Set();
|
|
15
19
|
let nextConnection = 0;
|
|
@@ -21,13 +25,15 @@ export async function listenControlSocket(socketPath, secret, handlers) {
|
|
|
21
25
|
});
|
|
22
26
|
await new Promise((resolve, reject) => {
|
|
23
27
|
server.once("error", reject);
|
|
24
|
-
server.listen(socketPath, () => {
|
|
28
|
+
server.listen(resolveSocketTransportAddress(socketPath, process.platform, secret), () => {
|
|
25
29
|
server.off("error", reject);
|
|
26
30
|
resolve();
|
|
27
31
|
});
|
|
28
32
|
});
|
|
29
33
|
server.on("error", (error) => handlers.listenerError(error));
|
|
30
|
-
|
|
34
|
+
// A named pipe has no file to restrict: the secret, 0600 beside the logical path, is its gate.
|
|
35
|
+
if (process.platform !== "win32")
|
|
36
|
+
await chmod(socketPath, 0o600);
|
|
31
37
|
return { close: () => closeServer(server, clients) };
|
|
32
38
|
}
|
|
33
39
|
function acceptConnection(socket, id, handlers) {
|
|
@@ -42,13 +42,13 @@ import { ClientAdmissions } from "./client-admissions.js";
|
|
|
42
42
|
import { handleClientInput } from "./client-input-handler.js";
|
|
43
43
|
import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } from "./connection-question-bridge.js";
|
|
44
44
|
import { CONNECTION_ROLE_FIXED, ConnectionRoles, roleConnection } from "./connection-roles.js";
|
|
45
|
-
import { AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
|
|
45
|
+
import { AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, ENTRIES_TAIL_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
|
|
46
46
|
import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetModel, runSetThinkingLevel, } from "./effect-commands.js";
|
|
47
47
|
import { EffectLedger } from "./effect-ledger.js";
|
|
48
48
|
import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
|
|
49
49
|
import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
|
|
50
50
|
import { settleExtensionUiResponse } from "./extension-ui-response.js";
|
|
51
|
-
import {
|
|
51
|
+
import { answerGetEntries } from "./get-entries-command.js";
|
|
52
52
|
import { runHoldInput } from "./held-input-commands.js";
|
|
53
53
|
import { HostSessionControl } from "./host-session-control.js";
|
|
54
54
|
import { createRpcLoginPromptCallbacks } from "./login-prompts.js";
|
|
@@ -57,7 +57,7 @@ import { protocolIdentity } from "./protocol-identity.js";
|
|
|
57
57
|
import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListDigest } from "./rpc-command-surface.js";
|
|
58
58
|
import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
|
|
59
59
|
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
60
|
-
import {
|
|
60
|
+
import { RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_MODEL_NOT_FOUND, STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY, } from "./rpc-types.js";
|
|
61
61
|
import { runPauseForRun, runResumeForRun } from "./run-hold-commands.js";
|
|
62
62
|
import { availableModelsData, interruptRunningTurn } from "./session-control-actions-data.js";
|
|
63
63
|
import { SessionExtensionUiRequests, sessionDialogRequests } from "./session-extension-ui-requests.js";
|
|
@@ -871,6 +871,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
871
871
|
TOOL_BARRIER_CAPABILITY,
|
|
872
872
|
USER_ACTIVITY_CAPABILITY,
|
|
873
873
|
RESUME_SESSION_CAPABILITY,
|
|
874
|
+
ENTRIES_TAIL_CAPABILITY,
|
|
874
875
|
...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
|
|
875
876
|
...(options.toolRuntime ? toolRuntimeCapabilities(options.toolRuntime) : []),
|
|
876
877
|
// Client flags cannot advertise a host guard this classic binding does not run, nor a tool runtime.
|
|
@@ -1457,43 +1458,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1457
1458
|
const messages = session.getUserMessagesForForking();
|
|
1458
1459
|
return success(id, "get_fork_messages", { messages });
|
|
1459
1460
|
}
|
|
1460
|
-
case "get_entries":
|
|
1461
|
-
|
|
1462
|
-
let entries = sessionManager.getEntries();
|
|
1463
|
-
if (command.since !== undefined) {
|
|
1464
|
-
const sinceIndex = entries.findIndex((e) => e.id === command.since);
|
|
1465
|
-
if (sinceIndex === -1) {
|
|
1466
|
-
return error(id, "get_entries", `Entry not found: ${command.since}`, RPC_ERROR_ENTRY_NOT_FOUND);
|
|
1467
|
-
}
|
|
1468
|
-
entries = entries.slice(sinceIndex + 1);
|
|
1469
|
-
}
|
|
1470
|
-
const bounds = entryPageBounds(command);
|
|
1471
|
-
if (!bounds.ok)
|
|
1472
|
-
return error(id, "get_entries", bounds.message, RPC_ERROR_INVALID_ARGUMENT);
|
|
1473
|
-
const page = pageEntries(entries, describeUsageLimitEntry, bounds);
|
|
1474
|
-
// A client walking pages asks again at once; holding the view until the last page spares each page a
|
|
1475
|
-
// full re-read of the file. The hold expires on its own, so a walk the client abandons on an idle
|
|
1476
|
-
// session does not pin the whole history; any other answer lets the view go now.
|
|
1477
|
-
if (bounds.paged && page.hasMore)
|
|
1478
|
-
holdEntryWalk(session, () => session.releaseFullHistoryView());
|
|
1479
|
-
else {
|
|
1480
|
-
endEntryWalk(session);
|
|
1481
|
-
session.releaseFullHistoryView();
|
|
1482
|
-
}
|
|
1483
|
-
const budget = currentResponseByteBudget();
|
|
1484
|
-
if (page.firstEntryBytes > budget) {
|
|
1485
|
-
return error(id, "get_entries", `Entry ${entries[0]?.id ?? ""} alone is larger than one answer can carry (${budget} bytes), so it cannot be sent over RPC.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
1486
|
-
}
|
|
1487
|
-
if (!bounds.paged && page.hasMore) {
|
|
1488
|
-
return error(id, "get_entries", `The session history is larger than one answer can carry (${budget} bytes). Read it in pages: pass limit or maxBytes, then since: nextSince until hasMore is false.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
1489
|
-
}
|
|
1490
|
-
return success(id, "get_entries", {
|
|
1491
|
-
entries: page.entries,
|
|
1492
|
-
leafId: sessionManager.getLeafId(),
|
|
1493
|
-
...(bounds.paged ? { hasMore: page.hasMore } : {}),
|
|
1494
|
-
...(page.nextSince === undefined ? {} : { nextSince: page.nextSince }),
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1461
|
+
case "get_entries":
|
|
1462
|
+
return answerGetEntries(session, command);
|
|
1497
1463
|
case "get_tree": {
|
|
1498
1464
|
const sessionManager = session.sessionManager;
|
|
1499
1465
|
const tree = sessionManager.getTree();
|
|
@@ -60,6 +60,12 @@ export declare const DETACH_CONNECTION_CAPABILITY = "detach_connection";
|
|
|
60
60
|
* client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
|
|
61
61
|
*/
|
|
62
62
|
export declare const INTERACTIVE_USER_CAPABILITY = "interactive_user";
|
|
63
|
+
/** `get_state` and `get_entries` accept `observe: true` and then never count as an attachment (senpi#4267). */
|
|
64
|
+
export declare const OBSERVE_READS_CAPABILITY = "observe_reads";
|
|
65
|
+
/** `get_entries` accepts `before` and `tail` and answers `next_before` (senpi#4267). */
|
|
66
|
+
export declare const ENTRIES_TAIL_CAPABILITY = "entries_tail";
|
|
67
|
+
/** A terminal control endpoint serves the host's observe-and-drive command set (senpi#4267). */
|
|
68
|
+
export declare const TUI_OBSERVER_CAPABILITY = "tui_observer";
|
|
63
69
|
/** The connection owns this session as a task child; only it may pause or resume the session for its run (senpi#3811). */
|
|
64
70
|
export declare const TASK_PARENT_CAPABILITY = "task_parent";
|
|
65
71
|
/**
|
|
@@ -60,6 +60,12 @@ export const DETACH_CONNECTION_CAPABILITY = "detach_connection";
|
|
|
60
60
|
* client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
|
|
61
61
|
*/
|
|
62
62
|
export const INTERACTIVE_USER_CAPABILITY = "interactive_user";
|
|
63
|
+
/** `get_state` and `get_entries` accept `observe: true` and then never count as an attachment (senpi#4267). */
|
|
64
|
+
export const OBSERVE_READS_CAPABILITY = "observe_reads";
|
|
65
|
+
/** `get_entries` accepts `before` and `tail` and answers `next_before` (senpi#4267). */
|
|
66
|
+
export const ENTRIES_TAIL_CAPABILITY = "entries_tail";
|
|
67
|
+
/** A terminal control endpoint serves the host's observe-and-drive command set (senpi#4267). */
|
|
68
|
+
export const TUI_OBSERVER_CAPABILITY = "tui_observer";
|
|
63
69
|
/** The connection owns this session as a task child; only it may pause or resume the session for its run (senpi#3811). */
|
|
64
70
|
export const TASK_PARENT_CAPABILITY = "task_parent";
|
|
65
71
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one `get_entries` answer: a host connection and a terminal control endpoint both read history through it, so
|
|
3
|
+
* the range rules (`since`, `before`), the forward (`limit`) and backward (`tail`, senpi#4267) pages and the byte
|
|
4
|
+
* budget (senpi#3534) are the same on every surface.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
7
|
+
import { type RpcCommand, type RpcResponse } from "./rpc-types.ts";
|
|
8
|
+
type GetEntries = Extract<RpcCommand, {
|
|
9
|
+
type: "get_entries";
|
|
10
|
+
}>;
|
|
11
|
+
export declare function answerGetEntries(session: AgentSession, command: GetEntries): RpcResponse;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=get-entries-command.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describeUsageLimitEntry } from "../../core/retry-fallback/usage-limit.js";
|
|
2
|
+
import { currentResponseByteBudget, endEntryWalk, entryPageBounds, holdEntryWalk, pageEntries, pageEntriesFromEnd, } from "./get-entries-page.js";
|
|
3
|
+
import { RPC_ERROR_ENTRY_NOT_FOUND, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_RESPONSE_TOO_LARGE, } from "./rpc-types.js";
|
|
4
|
+
function refuse(id, message, errorCode) {
|
|
5
|
+
return { id, type: "response", command: "get_entries", success: false, error: message, errorCode };
|
|
6
|
+
}
|
|
7
|
+
export function answerGetEntries(session, command) {
|
|
8
|
+
const { id } = command;
|
|
9
|
+
const sessionManager = session.sessionManager;
|
|
10
|
+
let entries = sessionManager.getEntries();
|
|
11
|
+
if (command.before !== undefined) {
|
|
12
|
+
const beforeIndex = entries.findIndex((e) => e.id === command.before);
|
|
13
|
+
if (beforeIndex === -1)
|
|
14
|
+
return refuse(id, `Entry not found: ${command.before}`, RPC_ERROR_ENTRY_NOT_FOUND);
|
|
15
|
+
entries = entries.slice(0, beforeIndex);
|
|
16
|
+
}
|
|
17
|
+
if (command.since !== undefined) {
|
|
18
|
+
const sinceIndex = sessionManager.getEntries().findIndex((e) => e.id === command.since);
|
|
19
|
+
if (sinceIndex === -1)
|
|
20
|
+
return refuse(id, `Entry not found: ${command.since}`, RPC_ERROR_ENTRY_NOT_FOUND);
|
|
21
|
+
entries = entries.slice(sinceIndex + 1);
|
|
22
|
+
}
|
|
23
|
+
const bounds = entryPageBounds(command);
|
|
24
|
+
if (!bounds.ok)
|
|
25
|
+
return refuse(id, bounds.message, RPC_ERROR_INVALID_ARGUMENT);
|
|
26
|
+
const backward = bounds.tail !== undefined;
|
|
27
|
+
const page = backward
|
|
28
|
+
? pageEntriesFromEnd(entries, describeUsageLimitEntry, bounds)
|
|
29
|
+
: pageEntries(entries, describeUsageLimitEntry, bounds);
|
|
30
|
+
// A client walking pages asks again at once; holding the view until the last page spares each page a
|
|
31
|
+
// full re-read of the file. The hold expires on its own, so a walk the client abandons on an idle
|
|
32
|
+
// session does not pin the whole history; any other answer lets the view go now.
|
|
33
|
+
if (bounds.paged && page.hasMore)
|
|
34
|
+
holdEntryWalk(session, () => session.releaseFullHistoryView());
|
|
35
|
+
else {
|
|
36
|
+
endEntryWalk(session);
|
|
37
|
+
session.releaseFullHistoryView();
|
|
38
|
+
}
|
|
39
|
+
const budget = currentResponseByteBudget();
|
|
40
|
+
if (page.firstEntryBytes > budget) {
|
|
41
|
+
const entry = backward ? entries.at(-1) : entries[0];
|
|
42
|
+
return refuse(id, `Entry ${entry?.id ?? ""} alone is larger than one answer can carry (${budget} bytes), so it cannot be sent over RPC.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
43
|
+
}
|
|
44
|
+
if (!bounds.paged && page.hasMore) {
|
|
45
|
+
return refuse(id, `The session history is larger than one answer can carry (${budget} bytes). Read it in pages: pass limit or maxBytes, then since: nextSince until hasMore is false.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
type: "response",
|
|
50
|
+
command: "get_entries",
|
|
51
|
+
success: true,
|
|
52
|
+
data: {
|
|
53
|
+
entries: page.entries,
|
|
54
|
+
leafId: sessionManager.getLeafId(),
|
|
55
|
+
...(bounds.paged ? { hasMore: page.hasMore } : {}),
|
|
56
|
+
...("nextSince" in page && page.nextSince !== undefined ? { nextSince: page.nextSince } : {}),
|
|
57
|
+
...("nextBefore" in page && page.nextBefore !== undefined ? { next_before: page.nextBefore } : {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=get-entries-command.js.map
|
|
@@ -14,6 +14,8 @@ export declare function currentResponseByteBudget(): number;
|
|
|
14
14
|
export declare function setResponseByteBudgetForTesting(bytes: number): () => void;
|
|
15
15
|
export interface EntryPageRequest {
|
|
16
16
|
readonly limit?: number;
|
|
17
|
+
/** The newest `tail` entries of the range, read backwards (senpi#4267); never combined with `limit`. */
|
|
18
|
+
readonly tail?: number;
|
|
17
19
|
readonly maxBytes?: number;
|
|
18
20
|
}
|
|
19
21
|
export interface EntryPage<T> {
|
|
@@ -27,6 +29,7 @@ export type EntryPageBounds = {
|
|
|
27
29
|
readonly ok: true;
|
|
28
30
|
readonly paged: boolean;
|
|
29
31
|
readonly limit?: number;
|
|
32
|
+
readonly tail?: number;
|
|
30
33
|
readonly maxBytes: number;
|
|
31
34
|
} | {
|
|
32
35
|
readonly ok: false;
|
|
@@ -44,6 +47,25 @@ export declare function pageEntries<S extends {
|
|
|
44
47
|
readonly limit?: number;
|
|
45
48
|
readonly maxBytes: number;
|
|
46
49
|
}): EntryPage<T>;
|
|
50
|
+
export interface TailEntryPage<T> {
|
|
51
|
+
/** In append order: the oldest of the page first. */
|
|
52
|
+
readonly entries: T[];
|
|
53
|
+
readonly hasMore: boolean;
|
|
54
|
+
/** The oldest entry id the page returned, while older entries remain: pass it as the next `before`. */
|
|
55
|
+
readonly nextBefore?: string;
|
|
56
|
+
/** Serialized size of the page's newest entry (0 for an empty page). */
|
|
57
|
+
readonly firstEntryBytes: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The newest entries first, until the count or the byte size would pass the bounds, answered in append order. The
|
|
61
|
+
* newest entry is always taken, so a backward walk always moves even when one entry alone is larger than `maxBytes`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pageEntriesFromEnd<S extends {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
}, T>(entries: readonly S[], project: (entry: S) => T, bounds: {
|
|
66
|
+
readonly tail?: number;
|
|
67
|
+
readonly maxBytes: number;
|
|
68
|
+
}): TailEntryPage<T>;
|
|
47
69
|
/**
|
|
48
70
|
* Keeps a page walk's full-history view for the next page, and lets it go after `ENTRY_WALK_HOLD_MS` without
|
|
49
71
|
* one: a client that stops mid-walk must not pin the whole history of a session that is already idle.
|
|
@@ -22,17 +22,22 @@ export function setResponseByteBudgetForTesting(bytes) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
export function entryPageBounds(request) {
|
|
25
|
-
const { limit, maxBytes } = request;
|
|
25
|
+
const { limit, tail, maxBytes } = request;
|
|
26
26
|
if (limit !== undefined && !(Number.isInteger(limit) && limit > 0))
|
|
27
27
|
return { ok: false, message: "limit must be a positive integer" };
|
|
28
|
+
if (tail !== undefined && !(Number.isInteger(tail) && tail > 0))
|
|
29
|
+
return { ok: false, message: "tail must be a positive integer" };
|
|
30
|
+
if (limit !== undefined && tail !== undefined)
|
|
31
|
+
return { ok: false, message: "limit and tail cannot be combined: limit pages forward, tail backward" };
|
|
28
32
|
if (maxBytes !== undefined && !(Number.isInteger(maxBytes) && maxBytes > 0))
|
|
29
33
|
return { ok: false, message: "maxBytes must be a positive integer" };
|
|
30
|
-
if (limit === undefined && maxBytes === undefined)
|
|
34
|
+
if (limit === undefined && tail === undefined && maxBytes === undefined)
|
|
31
35
|
return { ok: true, paged: false, maxBytes: responseByteBudget };
|
|
32
36
|
return {
|
|
33
37
|
ok: true,
|
|
34
38
|
paged: true,
|
|
35
39
|
...(limit === undefined ? {} : { limit }),
|
|
40
|
+
...(tail === undefined ? {} : { tail }),
|
|
36
41
|
maxBytes: Math.min(maxBytes ?? DEFAULT_ENTRY_PAGE_BYTES, responseByteBudget),
|
|
37
42
|
};
|
|
38
43
|
}
|
|
@@ -68,6 +73,40 @@ export function pageEntries(entries, project, bounds) {
|
|
|
68
73
|
firstEntryBytes,
|
|
69
74
|
};
|
|
70
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The newest entries first, until the count or the byte size would pass the bounds, answered in append order. The
|
|
78
|
+
* newest entry is always taken, so a backward walk always moves even when one entry alone is larger than `maxBytes`.
|
|
79
|
+
*/
|
|
80
|
+
export function pageEntriesFromEnd(entries, project, bounds) {
|
|
81
|
+
const page = [];
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
let firstEntryBytes = 0;
|
|
84
|
+
let index = entries.length - 1;
|
|
85
|
+
for (; index >= 0; index--) {
|
|
86
|
+
if (bounds.tail !== undefined && page.length >= bounds.tail)
|
|
87
|
+
break;
|
|
88
|
+
const entry = entries[index];
|
|
89
|
+
if (entry === undefined)
|
|
90
|
+
break;
|
|
91
|
+
const projected = project(entry);
|
|
92
|
+
const size = Buffer.byteLength(JSON.stringify(projected)) + 1;
|
|
93
|
+
if (page.length > 0 && bytes + size > bounds.maxBytes)
|
|
94
|
+
break;
|
|
95
|
+
if (page.length === 0)
|
|
96
|
+
firstEntryBytes = size;
|
|
97
|
+
page.push(projected);
|
|
98
|
+
bytes += size;
|
|
99
|
+
}
|
|
100
|
+
page.reverse();
|
|
101
|
+
const oldest = entries[index + 1];
|
|
102
|
+
const hasMore = index >= 0;
|
|
103
|
+
return {
|
|
104
|
+
entries: page,
|
|
105
|
+
hasMore,
|
|
106
|
+
...(hasMore && oldest !== undefined ? { nextBefore: oldest.id } : {}),
|
|
107
|
+
firstEntryBytes,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
71
110
|
const heldWalks = new WeakMap();
|
|
72
111
|
/**
|
|
73
112
|
* Keeps a page walk's full-history view for the next page, and lets it go after `ENTRY_WALK_HOLD_MS` without
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isHostOwnedToolRuntimeCapability, toolRuntimeCapabilities, } from "../../cli/tool-runtime.js";
|
|
2
2
|
import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY, AUTO_PERMISSION_PRESET_CAPABILITY, } from "../../core/extensions/builtin/permission-system/config.js";
|
|
3
3
|
import { DURABLE_CLIENT_MESSAGE_ID_CAPABILITY } from "./client-admission-record.js";
|
|
4
|
-
import { ATTENDED_CAPABILITY, AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DETACH_CONNECTION_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, EVAL_CELLS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, MOVED_PATH_GUARD_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_HELD_CAPABILITY, SESSION_KIND_CAPABILITY, SESSION_RELOCATION_CAPABILITY, SESSION_STATUS_CAPABILITY, TOOL_BARRIER_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
4
|
+
import { ATTENDED_CAPABILITY, AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, BROWSER_ENGINE_CAPABILITY, CONTINUE_FROM_LEAF_CAPABILITY, DETACH_CONNECTION_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, ENTRIES_TAIL_CAPABILITY, EVAL_CELLS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, MOVED_PATH_GUARD_CAPABILITY, OBSERVE_READS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, RETRY_FALLBACK_PROFILE_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_HELD_CAPABILITY, SESSION_KIND_CAPABILITY, SESSION_RELOCATION_CAPABILITY, SESSION_STATUS_CAPABILITY, TOOL_BARRIER_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
|
|
5
5
|
import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
|
|
6
6
|
import { STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY } from "./rpc-types.js";
|
|
7
7
|
import { MOVE_SESSION_CAPABILITY, MOVE_SESSION_REVOKE_CAPABILITY } from "./session-move-wire.js";
|
|
@@ -44,6 +44,9 @@ export function multiSessionHostCapabilities(options) {
|
|
|
44
44
|
SESSION_HELD_CAPABILITY,
|
|
45
45
|
RESUME_SESSION_CAPABILITY,
|
|
46
46
|
ATTENDED_CAPABILITY,
|
|
47
|
+
// `get_state`/`get_entries` marked `observe` never attach, and history reads page backward too (senpi#4267).
|
|
48
|
+
OBSERVE_READS_CAPABILITY,
|
|
49
|
+
ENTRIES_TAIL_CAPABILITY,
|
|
47
50
|
// Host capabilities, not client opt-ins: only a multi-session host owns the
|
|
48
51
|
// attachment refcount `open_session.retain_on_disconnect` detaches from, the
|
|
49
52
|
// per-session launch profile `context`/`auto_title` travel on, and the session
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* itself with `observe: true`; the supervisor classifies a connection by the requests it sends, and
|
|
8
8
|
* only a connection that sends something other than an observing read becomes an attachment.
|
|
9
9
|
*
|
|
10
|
-
* Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing
|
|
10
|
+
* Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing; `get_state` and
|
|
11
|
+
* `get_entries` read a session's state and history, senpi#4267): `observe: true` on any other command is ignored
|
|
11
12
|
* and the connection attaches, so a client cannot hold a session while its host idles out under it.
|
|
12
13
|
* `warm` never attaches, marked or not: it opens nothing, so a host that only received it idles out
|
|
13
14
|
* on its normal deadline (senpi#2314).
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* itself with `observe: true`; the supervisor classifies a connection by the requests it sends, and
|
|
8
8
|
* only a connection that sends something other than an observing read becomes an attachment.
|
|
9
9
|
*
|
|
10
|
-
* Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing
|
|
10
|
+
* Only the side-effect-free reads can observe (`get_effect_status` answers a receipt and changes nothing; `get_state` and
|
|
11
|
+
* `get_entries` read a session's state and history, senpi#4267): `observe: true` on any other command is ignored
|
|
11
12
|
* and the connection attaches, so a client cannot hold a session while its host idles out under it.
|
|
12
13
|
* `warm` never attaches, marked or not: it opens nothing, so a host that only received it idles out
|
|
13
14
|
* on its normal deadline (senpi#2314).
|
|
@@ -33,6 +34,8 @@ const OBSERVING_COMMANDS = new Set([
|
|
|
33
34
|
"get_effect_status",
|
|
34
35
|
"get_pending_inputs",
|
|
35
36
|
"get_subscription_limits",
|
|
37
|
+
"get_state",
|
|
38
|
+
"get_entries",
|
|
36
39
|
]);
|
|
37
40
|
export function isObservingRequest(line) {
|
|
38
41
|
let request;
|
|
@@ -388,11 +388,18 @@ export type RpcSessionCommand = {
|
|
|
388
388
|
} | {
|
|
389
389
|
id?: string;
|
|
390
390
|
type: "get_fork_messages";
|
|
391
|
-
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* `since`/`before` bound the range (strictly after / strictly before an entry id); `limit` pages it forward from
|
|
394
|
+
* its start, `tail` backward from its end (`next_before` names the next page, senpi#4267).
|
|
395
|
+
*/
|
|
396
|
+
| {
|
|
392
397
|
id?: string;
|
|
393
398
|
type: "get_entries";
|
|
394
399
|
since?: string;
|
|
400
|
+
before?: string;
|
|
395
401
|
limit?: number;
|
|
402
|
+
tail?: number;
|
|
396
403
|
maxBytes?: number;
|
|
397
404
|
} | {
|
|
398
405
|
id?: string;
|