@robota-sdk/agent-transport 3.0.0-beta.100

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +1137 -0
  2. package/LICENSE +661 -0
  3. package/README.md +121 -0
  4. package/dist/browser/client.d.ts +2 -0
  5. package/dist/browser/client.js +1 -0
  6. package/dist/browser/index.d.ts +624 -0
  7. package/dist/browser/index.d.ts.map +1 -0
  8. package/dist/browser/index.js +2 -0
  9. package/dist/browser/index.js.map +1 -0
  10. package/dist/browser/observer-messages-4fRQZU2D.d.ts +499 -0
  11. package/dist/browser/observer-messages-4fRQZU2D.d.ts.map +1 -0
  12. package/dist/browser/observer-messages-O8jTPEQO.js +2 -0
  13. package/dist/browser/observer-messages-O8jTPEQO.js.map +1 -0
  14. package/dist/node/index.cjs +1 -0
  15. package/dist/node/index.d.cts +1115 -0
  16. package/dist/node/index.d.cts.map +1 -0
  17. package/dist/node/index.d.ts +1115 -0
  18. package/dist/node/index.d.ts.map +1 -0
  19. package/dist/node/index.js +2 -0
  20. package/dist/node/index.js.map +1 -0
  21. package/dist/node/node.cjs +1 -0
  22. package/dist/node/node.d.cts +317 -0
  23. package/dist/node/node.d.cts.map +1 -0
  24. package/dist/node/node.d.ts +317 -0
  25. package/dist/node/node.d.ts.map +1 -0
  26. package/dist/node/node.js +2 -0
  27. package/dist/node/node.js.map +1 -0
  28. package/package.json +122 -0
  29. package/src/__tests__/__fixtures__/korean-text.json +6 -0
  30. package/src/__tests__/access-token-verifier.test.ts +672 -0
  31. package/src/__tests__/admission.test.ts +130 -0
  32. package/src/__tests__/app-tool-messages.test.ts +463 -0
  33. package/src/__tests__/bearer-resource-gate.test.ts +120 -0
  34. package/src/__tests__/channel-frames.test.ts +215 -0
  35. package/src/__tests__/driver-attribution-capability.test.ts +83 -0
  36. package/src/__tests__/file-transfer.test.ts +281 -0
  37. package/src/__tests__/handoff-chunking.test.ts +261 -0
  38. package/src/__tests__/handoff-manifest.test.ts +52 -0
  39. package/src/__tests__/history-attached-budget.test.ts +216 -0
  40. package/src/__tests__/message-decoders.test.ts +870 -0
  41. package/src/__tests__/outbound-delivery.test.ts +558 -0
  42. package/src/__tests__/peer-message-ledger.test.ts +200 -0
  43. package/src/__tests__/permission-summary.test.ts +18 -0
  44. package/src/__tests__/personal-usage-report.test.ts +193 -0
  45. package/src/__tests__/project-read-messages.test.ts +118 -0
  46. package/src/__tests__/resume-buffer.test.ts +93 -0
  47. package/src/__tests__/session-detail-loop-status.test.ts +169 -0
  48. package/src/__tests__/session-directory-messages.test.ts +322 -0
  49. package/src/__tests__/session-error-classification.test.ts +113 -0
  50. package/src/__tests__/session-event-delivery.test.ts +101 -0
  51. package/src/__tests__/session-history-wire.test.ts +364 -0
  52. package/src/__tests__/session-message-handler-e5.test.ts +99 -0
  53. package/src/__tests__/session-message-handler-observe.test.ts +223 -0
  54. package/src/__tests__/session-message-handler.test.ts +726 -0
  55. package/src/__tests__/session-resume-bridge.test.ts +472 -0
  56. package/src/__tests__/settings-messages.test.ts +193 -0
  57. package/src/__tests__/stdio-carrier.test.ts +85 -0
  58. package/src/__tests__/submit-prompt-shape.test.ts +78 -0
  59. package/src/__tests__/usage-report-carrier.test.ts +98 -0
  60. package/src/__tests__/ws-broadcast-events.test.ts +94 -0
  61. package/src/app-tool-messages.ts +140 -0
  62. package/src/background-messages.ts +169 -0
  63. package/src/channel-frames.ts +182 -0
  64. package/src/client.ts +17 -0
  65. package/src/handoff-chunking.ts +275 -0
  66. package/src/index.ts +135 -0
  67. package/src/loop-control-messages.ts +34 -0
  68. package/src/message-decoders.ts +485 -0
  69. package/src/message-parser.ts +33 -0
  70. package/src/node/access-token-verifier.ts +363 -0
  71. package/src/node/admission.ts +153 -0
  72. package/src/node/bearer-resource-gate.ts +331 -0
  73. package/src/node/file-transfer.ts +530 -0
  74. package/src/node/handoff-manifest.ts +77 -0
  75. package/src/node/index.ts +45 -0
  76. package/src/node/stdio-carrier.ts +59 -0
  77. package/src/observer-messages.ts +47 -0
  78. package/src/open-prompts.ts +76 -0
  79. package/src/outbound-delivery.ts +240 -0
  80. package/src/peer-message-ledger.ts +193 -0
  81. package/src/permission-summary.ts +18 -0
  82. package/src/project-read-messages.ts +74 -0
  83. package/src/protocol-session.ts +56 -0
  84. package/src/resume-buffer.ts +123 -0
  85. package/src/session-directory-messages.ts +162 -0
  86. package/src/session-error-classification.ts +91 -0
  87. package/src/session-events.ts +290 -0
  88. package/src/session-message-handler.ts +413 -0
  89. package/src/session-query-messages.ts +160 -0
  90. package/src/session-resume-bridge.ts +275 -0
  91. package/src/settings-messages.ts +103 -0
  92. package/src/usage-messages.ts +195 -0
  93. package/src/wire-messages.ts +436 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * #3189: a history larger than an attached terminal's backpressure budget reaches that terminal
3
+ * whole, over a real socket, without the connection being cut.
4
+ *
5
+ * The carrier here is wired as the supervised attach carrier is: every reply goes through
6
+ * `createOutboundDelivery` measuring the socket's `writableLength` against
7
+ * `ATTACHED_SURFACE_MAX_PENDING_BYTES`, and a failure destroys the socket. The client sends what an
8
+ * attached terminal sends when it starts, all at once, and pages the history as it arrives.
9
+ */
10
+
11
+ import { mkdtempSync, rmSync } from 'node:fs';
12
+ import { createConnection, createServer } from 'node:net';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import { createTestInteractiveSession } from '@robota-sdk/agent-interface-session/testing';
17
+ import { afterEach, describe, expect, it } from 'vitest';
18
+
19
+ import {
20
+ ATTACHED_SURFACE_MAX_PENDING_BYTES,
21
+ createOutboundDelivery,
22
+ } from '../outbound-delivery.js';
23
+ import { createSessionMessageHandler } from '../session-message-handler.js';
24
+
25
+ import type { TClientMessage, TServerMessage } from '../wire-messages.js';
26
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
27
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-session';
28
+ import type { Server, Socket } from 'node:net';
29
+
30
+ type TListener = (...args: unknown[]) => void;
31
+
32
+ const ENTRY_BYTES = 20_000;
33
+ /** Half as much again as the budget, so one frame holding it all would trip it. */
34
+ const HISTORY_BYTES = ATTACHED_SURFACE_MAX_PENDING_BYTES * 1.5;
35
+
36
+ const STARTUP: readonly TClientMessage[] = [
37
+ { type: 'get-history' },
38
+ { type: 'get-context' },
39
+ { type: 'get-commands' },
40
+ { type: 'get-status' },
41
+ { type: 'get-executing' },
42
+ { type: 'get-pending' },
43
+ { type: 'get-execution-workspace' },
44
+ ];
45
+
46
+ function longHistory(): IHistoryEntry[] {
47
+ return Array.from({ length: Math.ceil(HISTORY_BYTES / ENTRY_BYTES) }, (_, index) => ({
48
+ id: `e${index}`,
49
+ timestamp: new Date('2026-09-26T00:00:00.000Z'),
50
+ category: 'chat',
51
+ type: 'assistant',
52
+ data: { role: 'assistant', content: 'x'.repeat(ENTRY_BYTES) },
53
+ }));
54
+ }
55
+
56
+ function emittingSession(
57
+ history: IHistoryEntry[],
58
+ ): IInteractiveSession & { emit: (event: string, payload: unknown) => void } {
59
+ const listeners = new Map<string, Set<TListener>>();
60
+ const session = createTestInteractiveSession({
61
+ getFullHistory: () => history,
62
+ on: ((event: string, handler: TListener) => {
63
+ if (!listeners.has(event)) listeners.set(event, new Set());
64
+ listeners.get(event)!.add(handler);
65
+ }) as IInteractiveSession['on'],
66
+ off: ((event: string, handler: TListener) => {
67
+ listeners.get(event)?.delete(handler);
68
+ }) as IInteractiveSession['off'],
69
+ });
70
+ return Object.assign(session, {
71
+ emit: (event: string, payload: unknown) => {
72
+ for (const handler of listeners.get(event) ?? []) handler(payload);
73
+ },
74
+ });
75
+ }
76
+
77
+ /** One connection's carrier, as the supervised attach carrier builds it. */
78
+ function serve(session: IInteractiveSession, socket: Socket, onCut: (error: Error) => void): void {
79
+ const deliver = createOutboundDelivery(
80
+ (message) => {
81
+ socket.write(`${JSON.stringify(message)}\n`);
82
+ },
83
+ (error) => {
84
+ onCut(error);
85
+ socket.destroy();
86
+ },
87
+ () => socket.writableLength,
88
+ ATTACHED_SURFACE_MAX_PENDING_BYTES,
89
+ );
90
+ const { onMessage, cleanup } = createSessionMessageHandler({ session, deliver });
91
+ let buffered = '';
92
+ socket.setEncoding('utf8');
93
+ socket.on('data', (chunk: string) => {
94
+ buffered += chunk;
95
+ // Every complete line of a chunk is handled in one pass, as the carrier's drain loop does.
96
+ for (let end = buffered.indexOf('\n'); end !== -1; end = buffered.indexOf('\n')) {
97
+ const line = buffered.slice(0, end);
98
+ buffered = buffered.slice(end + 1);
99
+ onMessage(line);
100
+ }
101
+ });
102
+ socket.on('close', cleanup);
103
+ }
104
+
105
+ interface IClient {
106
+ readonly frames: TServerMessage[];
107
+ readonly closed: Promise<void>;
108
+ send(message: TClientMessage): void;
109
+ next(predicate: (frame: TServerMessage) => boolean): Promise<TServerMessage>;
110
+ end(): void;
111
+ }
112
+
113
+ function connect(path: string): Promise<IClient> {
114
+ return new Promise((resolve, reject) => {
115
+ const socket = createConnection(path);
116
+ const frames: TServerMessage[] = [];
117
+ const waiters: {
118
+ predicate: (frame: TServerMessage) => boolean;
119
+ found: (f: TServerMessage) => void;
120
+ lost: (error: Error) => void;
121
+ }[] = [];
122
+ let buffered = '';
123
+ let markClosed: () => void = () => undefined;
124
+ const closed = new Promise<void>((done) => {
125
+ markClosed = done;
126
+ });
127
+ socket.setEncoding('utf8');
128
+ socket.on('data', (chunk: string) => {
129
+ buffered += chunk;
130
+ for (let end = buffered.indexOf('\n'); end !== -1; end = buffered.indexOf('\n')) {
131
+ const frame = JSON.parse(buffered.slice(0, end)) as TServerMessage;
132
+ buffered = buffered.slice(end + 1);
133
+ frames.push(frame);
134
+ for (const waiter of waiters.splice(0)) {
135
+ if (waiter.predicate(frame)) waiter.found(frame);
136
+ else waiters.push(waiter);
137
+ }
138
+ }
139
+ });
140
+ socket.on('close', () => {
141
+ markClosed();
142
+ for (const waiter of waiters.splice(0))
143
+ waiter.lost(new Error('the host closed the connection'));
144
+ });
145
+ socket.on('error', reject);
146
+ socket.on('connect', () =>
147
+ resolve({
148
+ frames,
149
+ closed,
150
+ send: (message) => socket.write(`${JSON.stringify(message)}\n`),
151
+ next: (predicate) =>
152
+ new Promise((found, lost) => {
153
+ waiters.push({ predicate, found, lost });
154
+ }),
155
+ end: () => socket.end(),
156
+ }),
157
+ );
158
+ });
159
+ }
160
+
161
+ function isHistory(frame: TServerMessage): frame is Extract<TServerMessage, { type: 'history' }> {
162
+ return frame.type === 'history';
163
+ }
164
+
165
+ describe('a history larger than the attached-surface budget (#3189)', () => {
166
+ let server: Server | undefined;
167
+ let dir: string | undefined;
168
+
169
+ afterEach(async () => {
170
+ await new Promise<void>((done) => (server ? server.close(() => done()) : done()));
171
+ if (dir) rmSync(dir, { recursive: true, force: true });
172
+ });
173
+
174
+ it('reaches an attached terminal whole, and the turn after it does not cut the terminal off', async () => {
175
+ const history = longHistory();
176
+ const session = emittingSession(history);
177
+ const cuts: Error[] = [];
178
+ dir = mkdtempSync(join(tmpdir(), 'rh-'));
179
+ const path = join(dir, 's.sock');
180
+ server = createServer((socket) => serve(session, socket, (error) => cuts.push(error)));
181
+ await new Promise<void>((done) => server!.listen(path, done));
182
+ const client = await connect(path);
183
+
184
+ // What an attached terminal sends when it starts, in one burst.
185
+ for (const message of STARTUP) client.send(message);
186
+ const received: string[] = [];
187
+ let page = (await client.next(isHistory)) as Extract<TServerMessage, { type: 'history' }>;
188
+ for (;;) {
189
+ received.push(...page.entries.map((entry) => entry.id));
190
+ const next = page.startIndex + page.entries.length;
191
+ if (next >= page.total) break;
192
+ const arriving = client.next(isHistory);
193
+ client.send({ type: 'get-history', fromIndex: next });
194
+ page = (await arriving) as Extract<TServerMessage, { type: 'history' }>;
195
+ }
196
+ expect(received).toEqual(history.map((entry) => entry.id));
197
+
198
+ // A turn ends: its result, then more frames at once, as the host sends them.
199
+ const thinking = client.next((frame) => frame.type === 'thinking');
200
+ session.emit('complete', {
201
+ response: 'done',
202
+ history,
203
+ toolSummaries: [],
204
+ contextState: { usedTokens: 1, maxTokens: 10, usedPercentage: 10 },
205
+ });
206
+ session.emit('thinking', false);
207
+ await thinking;
208
+
209
+ expect(cuts).toEqual([]);
210
+ expect(client.frames.map((frame) => frame.type)).toEqual(
211
+ expect.arrayContaining(['context', 'commands', 'session_status', 'pending', 'complete']),
212
+ );
213
+ client.end();
214
+ await client.closed;
215
+ }, 20_000);
216
+ });