maka-agent 0.2.0-dev.40.20260917 → 0.2.0-dev.41.20260918
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/dist/runtime-host-session-driver.js +3 -8
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/shell-run.js +11 -0
- package/node_modules/@maka/runtime/dist/pty-screen-collector.js +32 -6
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +52 -21
- package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +4 -2
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +3 -1
- package/node_modules/@maka/runtime-host/dist/protocol/runtime-resource.js +4 -17
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +9 -1
- package/node_modules/@maka/runtime-host/dist/server/runtime-resource-coordinator.js +13 -23
- package/node_modules/@maka/runtime-host/dist/server/runtime-resource-projection.js +5 -16
- package/package.json +1 -1
|
@@ -1386,17 +1386,12 @@ class RuntimeHostMakaSessionDriverImpl {
|
|
|
1386
1386
|
async #stopUserCommand(ref, owner) {
|
|
1387
1387
|
if (this.#activeUserCommands.get(ref) !== owner)
|
|
1388
1388
|
return;
|
|
1389
|
-
|
|
1389
|
+
await this.#request('runtime.resource.stop', {
|
|
1390
1390
|
sessionId: owner.sessionId,
|
|
1391
1391
|
ref,
|
|
1392
1392
|
});
|
|
1393
|
-
this.#
|
|
1394
|
-
|
|
1395
|
-
ownership: { kind: 'local' },
|
|
1396
|
-
sourceTurnId: owner.commandId,
|
|
1397
|
-
sourceToolCallId: owner.commandId,
|
|
1398
|
-
result: stopped.resource,
|
|
1399
|
-
});
|
|
1393
|
+
this.#activeUserCommands.delete(ref);
|
|
1394
|
+
this.#publishRuntimeResource(owner.sessionId, ref);
|
|
1400
1395
|
}
|
|
1401
1396
|
#publishShellRunUpdate(update) {
|
|
1402
1397
|
const owner = this.#activeUserCommands.get(update.result.ref);
|
|
Binary file
|
|
@@ -75,6 +75,17 @@ export function isTerminalShellRunStatus(value) {
|
|
|
75
75
|
export function isActiveShellRunStatus(value) {
|
|
76
76
|
return SHELL_RUN_ACTIVE_STATUSES.includes(value);
|
|
77
77
|
}
|
|
78
|
+
/** Desktop interactive-terminal launch identity, persisted as the run's source ids. */
|
|
79
|
+
export const DESKTOP_TERMINAL_LAUNCH_PREFIX = 'desktop-terminal-';
|
|
80
|
+
/**
|
|
81
|
+
* A Desktop-owned interactive terminal carries no transcript tool call, so
|
|
82
|
+
* nothing consumes the output on its update and wire projections.
|
|
83
|
+
*/
|
|
84
|
+
export function isDesktopTerminalShellRun(source) {
|
|
85
|
+
return (source.mode === 'pty' &&
|
|
86
|
+
source.sourceTurnId === source.sourceToolCallId &&
|
|
87
|
+
source.sourceTurnId.startsWith(DESKTOP_TERMINAL_LAUNCH_PREFIX));
|
|
88
|
+
}
|
|
78
89
|
export function isValidShellRunStatusTransition(current, next) {
|
|
79
90
|
if (current === next)
|
|
80
91
|
return true;
|
|
@@ -66,6 +66,9 @@ export class PtyScreenCollector {
|
|
|
66
66
|
this.installProtocolBoundary();
|
|
67
67
|
this.lastGood = blankPtyOutput(cols, rows);
|
|
68
68
|
}
|
|
69
|
+
get available() {
|
|
70
|
+
return !this.failure && !this.disposed;
|
|
71
|
+
}
|
|
69
72
|
accept(data) {
|
|
70
73
|
if (!data)
|
|
71
74
|
return;
|
|
@@ -75,23 +78,46 @@ export class PtyScreenCollector {
|
|
|
75
78
|
}
|
|
76
79
|
const generation = ++this.admittedGeneration;
|
|
77
80
|
const bytes = Buffer.byteLength(data, 'utf8');
|
|
78
|
-
|
|
81
|
+
// node-pty delivers many tiny events per tick; each queued parse is paced
|
|
82
|
+
// by the headless terminal's write scheduler, so queue depth — not byte
|
|
83
|
+
// volume — is what stalls cut-bound operations. Merge unstarted data into
|
|
84
|
+
// the tail entry: byte order is preserved and protocol replies already
|
|
85
|
+
// batch per write call.
|
|
86
|
+
const tail = this.pending.at(-1);
|
|
87
|
+
if (tail &&
|
|
88
|
+
!tail.dropped &&
|
|
89
|
+
!tail.started &&
|
|
90
|
+
tail.bytes + bytes <= PTY_PARSER_HIGH_WATER_BYTES) {
|
|
91
|
+
tail.data += data;
|
|
92
|
+
tail.bytes += bytes;
|
|
93
|
+
tail.generation = generation;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.enqueue({ data, bytes, generation, dropped: false, started: false });
|
|
97
|
+
}
|
|
79
98
|
this.pendingBytes += bytes;
|
|
80
|
-
this.pending.push(entry);
|
|
81
99
|
this.evictOldestIfOverBudget();
|
|
82
100
|
this.options.onDirty(generation);
|
|
83
|
-
|
|
101
|
+
}
|
|
102
|
+
enqueue(entry) {
|
|
103
|
+
this.pending.push(entry);
|
|
104
|
+
const parse = this.sequence.then(() => {
|
|
105
|
+
if (entry.dropped)
|
|
106
|
+
return undefined;
|
|
107
|
+
entry.started = true;
|
|
108
|
+
return this.write(entry.data);
|
|
109
|
+
});
|
|
84
110
|
this.sequence = parse.then(() => {
|
|
85
111
|
if (entry.dropped)
|
|
86
112
|
return;
|
|
87
|
-
this.parsedGeneration = generation;
|
|
88
|
-
this.pendingBytes -= bytes;
|
|
113
|
+
this.parsedGeneration = entry.generation;
|
|
114
|
+
this.pendingBytes -= entry.bytes;
|
|
89
115
|
const index = this.pending.indexOf(entry);
|
|
90
116
|
if (index >= 0)
|
|
91
117
|
this.pending.splice(index, 1);
|
|
92
118
|
}, (error) => {
|
|
93
119
|
if (!entry.dropped)
|
|
94
|
-
this.pendingBytes -= bytes;
|
|
120
|
+
this.pendingBytes -= entry.bytes;
|
|
95
121
|
this.fail(asError(error, 'PTY parser failed'));
|
|
96
122
|
});
|
|
97
123
|
}
|
|
@@ -241,7 +241,7 @@ export class ShellRunProcessManager {
|
|
|
241
241
|
let resizeChanged = false;
|
|
242
242
|
let operationFailed = false;
|
|
243
243
|
let exitBeforeControlCut = false;
|
|
244
|
-
const
|
|
244
|
+
const mutation = () => {
|
|
245
245
|
if (input.abortSignal?.aborted) {
|
|
246
246
|
throw abortError('WriteStdin aborted before the control operation was committed');
|
|
247
247
|
}
|
|
@@ -287,8 +287,18 @@ export class ShellRunProcessManager {
|
|
|
287
287
|
this.handleIntegrityFailure(live, asError(error, 'PTY input write failed'));
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
}
|
|
291
|
-
|
|
290
|
+
};
|
|
291
|
+
// Client control replies carry no output, so the keystroke path only needs
|
|
292
|
+
// the ordered mutation, not the snapshot + persist. The record still
|
|
293
|
+
// refreshes through output-driven flushes; a resize changes the screen
|
|
294
|
+
// without output, so that one is persisted eagerly.
|
|
295
|
+
const clientControl = input.caller === 'client';
|
|
296
|
+
const controlCut = clientControl
|
|
297
|
+
? live.collector.mutateAtCut(mutation).then(() => undefined)
|
|
298
|
+
: live.collector.mutateAndSnapshotAtCut(mutation);
|
|
299
|
+
const persistedControl = clientControl
|
|
300
|
+
? undefined
|
|
301
|
+
: this.persistObservation(live, controlCut.then((snapshot) => (operationFailed || exitBeforeControlCut ? undefined : snapshot), () => undefined));
|
|
292
302
|
try {
|
|
293
303
|
await controlCut;
|
|
294
304
|
}
|
|
@@ -312,20 +322,27 @@ export class ShellRunProcessManager {
|
|
|
312
322
|
return shellRunContent(record, operation);
|
|
313
323
|
}
|
|
314
324
|
let record;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
catch (error) {
|
|
319
|
-
if (live.integrityFailure && !live.persistFailure) {
|
|
320
|
-
record = await this.markObserved(await live.finished.join());
|
|
321
|
-
return shellRunContent(record, ptyControlOperation(input, {
|
|
322
|
-
inputQueued,
|
|
323
|
-
resizeApplied,
|
|
324
|
-
resizeChanged,
|
|
325
|
-
failed: true,
|
|
326
|
-
}));
|
|
325
|
+
if (persistedControl) {
|
|
326
|
+
try {
|
|
327
|
+
record = await persistedControl;
|
|
327
328
|
}
|
|
328
|
-
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (live.integrityFailure && !live.persistFailure) {
|
|
331
|
+
record = await this.markObserved(await live.finished.join());
|
|
332
|
+
return shellRunContent(record, ptyControlOperation(input, {
|
|
333
|
+
inputQueued,
|
|
334
|
+
resizeApplied,
|
|
335
|
+
resizeChanged,
|
|
336
|
+
failed: true,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
record = live.record;
|
|
344
|
+
if (resizeChanged)
|
|
345
|
+
void this.persistObservation(live).catch(() => undefined);
|
|
329
346
|
}
|
|
330
347
|
if (live.integrityFailure && !live.persistFailure) {
|
|
331
348
|
record = await this.markObserved(await live.finished.join());
|
|
@@ -336,7 +353,6 @@ export class ShellRunProcessManager {
|
|
|
336
353
|
failed: true,
|
|
337
354
|
}));
|
|
338
355
|
}
|
|
339
|
-
// persistObservation decides whether to join finalization at call time.
|
|
340
356
|
// A real PTY can exit while that persist is still in flight, leaving a
|
|
341
357
|
// running snapshot here even though finalizeOnce has already started.
|
|
342
358
|
if (live.driverExit || live.finalizeOnce) {
|
|
@@ -345,7 +361,7 @@ export class ShellRunProcessManager {
|
|
|
345
361
|
}
|
|
346
362
|
if (isTerminalShellRunStatus(record.status))
|
|
347
363
|
record = await this.markObserved(record);
|
|
348
|
-
return shellRunContent(record, operation);
|
|
364
|
+
return clientControl ? compactShellRunContent(record) : shellRunContent(record, operation);
|
|
349
365
|
}
|
|
350
366
|
async readRuntimeResource(sessionId, ref, abortSignal) {
|
|
351
367
|
return this.resourceDetail(sessionId, ref, true, abortSignal, true);
|
|
@@ -428,11 +444,21 @@ export class ShellRunProcessManager {
|
|
|
428
444
|
const live = this.live.get(target.shellRunId);
|
|
429
445
|
if (!live || live.sessionId !== sessionId || live.mode !== 'pty')
|
|
430
446
|
return null;
|
|
447
|
+
// The collector dies before the process exit lands: an integrity failure or
|
|
448
|
+
// startup cleanup leaves it throwing while `live` still looks attachable.
|
|
449
|
+
// Report the resource as gone so the caller repairs the stale record.
|
|
450
|
+
if (live.driverExit || live.finalizeOnce || live.integrityFailure)
|
|
451
|
+
return null;
|
|
452
|
+
if (!live.collector.available)
|
|
453
|
+
return null;
|
|
454
|
+
// Flush pending bytes first so the snapshot sequence always names the last
|
|
455
|
+
// published event the buffer already contains.
|
|
456
|
+
this.publishPtyData(live);
|
|
431
457
|
return {
|
|
432
458
|
sessionId,
|
|
433
459
|
ref,
|
|
434
460
|
sequence: live.rawSequence,
|
|
435
|
-
buffer: live.rawBuffer,
|
|
461
|
+
buffer: live.rawBuffer.slice(-PTY_RAW_REPLAY_CHARS),
|
|
436
462
|
size: live.collector.currentSize(),
|
|
437
463
|
};
|
|
438
464
|
}
|
|
@@ -762,14 +788,18 @@ export class ShellRunProcessManager {
|
|
|
762
788
|
onPtyData(live, data) {
|
|
763
789
|
if (live.driverExit || live.finalizeOnce)
|
|
764
790
|
return;
|
|
765
|
-
|
|
791
|
+
// Amortize the tail trim: slicing on every tiny node-pty event copies the
|
|
792
|
+
// whole 16K replay buffer per event.
|
|
793
|
+
live.rawBuffer += data;
|
|
794
|
+
if (live.rawBuffer.length > PTY_RAW_REPLAY_CHARS * 2) {
|
|
795
|
+
live.rawBuffer = live.rawBuffer.slice(-PTY_RAW_REPLAY_CHARS);
|
|
796
|
+
}
|
|
766
797
|
live.collector.accept(data);
|
|
767
798
|
for (const chunk of splitPtyData(data)) {
|
|
768
799
|
const combined = `${live.pendingRawData}${chunk}`;
|
|
769
800
|
if (live.pendingRawData && encodedPtyDataBytes(combined) > PTY_RAW_PUBLISH_MAX_BYTES) {
|
|
770
801
|
this.publishPtyData(live);
|
|
771
802
|
}
|
|
772
|
-
live.rawSequence += 1;
|
|
773
803
|
live.pendingRawData += chunk;
|
|
774
804
|
if (encodedPtyDataBytes(live.pendingRawData) >= PTY_RAW_PUBLISH_TARGET_BYTES) {
|
|
775
805
|
this.publishPtyData(live);
|
|
@@ -791,6 +821,7 @@ export class ShellRunProcessManager {
|
|
|
791
821
|
if (!data)
|
|
792
822
|
return;
|
|
793
823
|
live.pendingRawData = '';
|
|
824
|
+
live.rawSequence += 1;
|
|
794
825
|
const event = {
|
|
795
826
|
sessionId: live.sessionId,
|
|
796
827
|
ref: shellRunResourceRef(live.shellRunId),
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
19
|
import { encodedTerminalInputActionsByteLength } from '@maka/core/terminal-input';
|
|
20
|
-
import { isActiveShellRunStatus } from '@maka/core/shell-run';
|
|
20
|
+
import { isActiveShellRunStatus, isDesktopTerminalShellRun } from '@maka/core/shell-run';
|
|
21
21
|
import { shellRunResourceRef } from './shell-run-contract.js';
|
|
22
22
|
import { truncateToolOutput } from './tool-output.js';
|
|
23
23
|
import { isLikelySandboxDenial } from './sandbox/detect.js';
|
|
@@ -29,7 +29,9 @@ export function shellRunUpdate(record) {
|
|
|
29
29
|
ownership: { kind: 'local' },
|
|
30
30
|
sourceTurnId: record.sourceTurnId,
|
|
31
31
|
sourceToolCallId: record.sourceToolCallId,
|
|
32
|
-
result:
|
|
32
|
+
result: isDesktopTerminalShellRun({ ...record, mode: record.output.mode })
|
|
33
|
+
? shellRunStateContent(record)
|
|
34
|
+
: shellRunSnapshotContent(record),
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
export function terminalContent(record) {
|
|
@@ -62,7 +62,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1;
|
|
|
62
62
|
export const RUNTIME_HOST_PROTOCOL_VERSION = 0;
|
|
63
63
|
// Increment when the same protocol version no longer guarantees safe Client-Host
|
|
64
64
|
// interoperability. Mismatches are rejected before domain commands are admitted.
|
|
65
|
-
export const RUNTIME_HOST_COMPATIBILITY_EPOCH =
|
|
65
|
+
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 162;
|
|
66
|
+
// 162: Runtime Resource control and stop replies drop the unused resource
|
|
67
|
+
// snapshot; start replies allow compact state. Older peers require snapshots.
|
|
66
68
|
// 161: Session transcript reads return the whole transcript under a byte budget,
|
|
67
69
|
// and every page says whether it stops between two Turns. The windowed read's
|
|
68
70
|
// range edges are gone, and the Turn landmark query takes a Turn to look up, so
|
|
@@ -111,7 +111,7 @@ export function decodeRuntimeResourceStartInput(value) {
|
|
|
111
111
|
}
|
|
112
112
|
export function decodeRuntimeResourceStartResult(value) {
|
|
113
113
|
const result = requireExactRecord(value, 'Runtime Resource start result', ['resource']);
|
|
114
|
-
const decoded = { resource:
|
|
114
|
+
const decoded = { resource: decodeRuntimeResourceState(result.resource) };
|
|
115
115
|
requireEncodedByteLimit(decoded, 'Runtime Resource start result', RUNTIME_RESOURCE_RESULT_MAX_BYTES);
|
|
116
116
|
return decoded;
|
|
117
117
|
}
|
|
@@ -268,15 +268,11 @@ export function decodeRuntimeResourceControllerControlResult(value) {
|
|
|
268
268
|
const result = requireExactRecord(value, 'Runtime Resource controller control result', [
|
|
269
269
|
'controllerId',
|
|
270
270
|
'sequence',
|
|
271
|
-
'resource',
|
|
272
271
|
]);
|
|
273
|
-
|
|
272
|
+
return {
|
|
274
273
|
controllerId: requireEntityId(result.controllerId, 'controllerId'),
|
|
275
274
|
sequence: controlSequence(result.sequence, 'controller sequence'),
|
|
276
|
-
resource: decodeRuntimeResourceSnapshot(result.resource),
|
|
277
275
|
};
|
|
278
|
-
requireEncodedByteLimit(decoded, 'Runtime Resource controller control result', RUNTIME_RESOURCE_RESULT_MAX_BYTES);
|
|
279
|
-
return decoded;
|
|
280
276
|
}
|
|
281
277
|
export function decodeRuntimeResourceControllerReleaseInput(value) {
|
|
282
278
|
return decodeControllerIdentity(value, 'Runtime Resource controller release input');
|
|
@@ -302,10 +298,8 @@ export function decodeRuntimeResourceStopInput(value) {
|
|
|
302
298
|
};
|
|
303
299
|
}
|
|
304
300
|
export function decodeRuntimeResourceStopResult(value) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
requireEncodedByteLimit(decoded, 'Runtime Resource stop result', RUNTIME_RESOURCE_RESULT_MAX_BYTES);
|
|
308
|
-
return decoded;
|
|
301
|
+
requireExactRecord(value, 'Runtime Resource stop result', []);
|
|
302
|
+
return {};
|
|
309
303
|
}
|
|
310
304
|
export function decodeRuntimeResourceUpdate(value) {
|
|
311
305
|
const update = requireExactRecord(value, 'Runtime Resource update', [
|
|
@@ -332,13 +326,6 @@ export function decodeRuntimeResourceState(value) {
|
|
|
332
326
|
}
|
|
333
327
|
return decoded.content;
|
|
334
328
|
}
|
|
335
|
-
export function decodeRuntimeResourceSnapshot(value) {
|
|
336
|
-
const decoded = decodeRuntimeResourceState(value);
|
|
337
|
-
if (decoded.output === undefined) {
|
|
338
|
-
throw invalidProtocolFrame('Invalid Runtime Resource snapshot');
|
|
339
|
-
}
|
|
340
|
-
return decoded;
|
|
341
|
-
}
|
|
342
329
|
function decodeControllerIdentity(value, label) {
|
|
343
330
|
const input = requireExactRecord(value, label, ['sessionId', 'ref', 'controllerId']);
|
|
344
331
|
return {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { JsonArrayPageBudget } from './json-array-page-budget.js';
|
|
20
20
|
import { ExternalSessionCatalogCursorError, ExternalSessionLimitError, ExternalSessionNotFoundError, } from '@maka/core/external-session';
|
|
21
|
+
import { redactSecrets } from '@maka/core/redaction';
|
|
21
22
|
import { ExternalSessionImporter } from '@maka/storage/external-sessions';
|
|
22
23
|
import { EXTERNAL_SESSION_CWD_MAX_BYTES, EXTERNAL_SESSION_IMPORTED_SESSION_IDS_MAX_ITEMS, EXTERNAL_SESSION_NAME_MAX_BYTES, EXTERNAL_SESSION_PAGE_MAX_ITEMS, EXTERNAL_SESSION_RESULT_MAX_BYTES, EXTERNAL_SESSION_SOURCE_MAX_ITEMS, EXTERNAL_SESSION_SOURCE_SESSION_ID_MAX_BYTES, } from '../protocol/index.js';
|
|
23
24
|
import { projectSessionCatalogRecord, SessionOperationFailure, NoUsableImportModelError, } from './session-catalog-coordinator.js';
|
|
@@ -70,7 +71,14 @@ export class HostExternalSessionCoordinator {
|
|
|
70
71
|
const headers = await this.#sessions.listHeaders();
|
|
71
72
|
for (const header of headers) {
|
|
72
73
|
if (header.transcriptLedgerVersion === 0) {
|
|
73
|
-
|
|
74
|
+
try {
|
|
75
|
+
await this.#prepareStagedSession(header.id);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// One staged Session can remain unpublished for a later recovery
|
|
79
|
+
// attempt without preventing unrelated Sessions or Host startup.
|
|
80
|
+
console.error(`[runtime-host] staged import recovery deferred (${header.id}): ${redactSecrets(error instanceof Error ? error.message : String(error))}`);
|
|
81
|
+
}
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
}
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { userInfo } from 'node:os';
|
|
21
21
|
import { isActiveShellRunStatus } from '@maka/core/shell-run';
|
|
22
|
+
import { shellRunStateProjection } from '@maka/core/shell-run-result';
|
|
22
23
|
import { ShellRunPtyControlClosedError, isShellRunResourceRef, } from '@maka/runtime/shell-run-contract';
|
|
23
24
|
import { defaultShellPlan, ShellPreferenceError } from '@maka/runtime/shell-detect';
|
|
24
25
|
import { isSessionNotFoundError } from '@maka/storage/execution-stores';
|
|
25
26
|
import { decodeRuntimeResourceControllerAcquireResult, decodeRuntimeResourceControllerControlResult, decodeRuntimeResourceQueryResult, decodeRuntimeResourceStopResult, decodeRuntimeResourceStartResult, RUNTIME_RESOURCE_CONTROLLER_ACQUIRE_RESULT_MAX_BYTES, RUNTIME_RESOURCE_MAX_CONTROL_SEQUENCE, } from '../protocol/index.js';
|
|
26
27
|
import { boundedFailureDiagnostic } from './failure-diagnostic.js';
|
|
27
|
-
import {
|
|
28
|
+
import { boundedRuntimeResourceState, canonicalRuntimeResources, createRuntimeResourcePage, runtimeResourceRevision, } from './runtime-resource-projection.js';
|
|
28
29
|
const MAX_CONTROL_REPLAYS = 128;
|
|
29
30
|
/** Owns Host Shell/PTY tools plus the connection-scoped Client controller fence. */
|
|
30
31
|
export class HostRuntimeResourceCoordinator {
|
|
@@ -347,22 +348,22 @@ export class HostRuntimeResourceCoordinator {
|
|
|
347
348
|
return {
|
|
348
349
|
ok: true,
|
|
349
350
|
result: decodeRuntimeResourceStartResult({
|
|
350
|
-
resource:
|
|
351
|
+
resource: boundedRuntimeResourceState(shellRunStateProjection(launched)),
|
|
351
352
|
}),
|
|
352
353
|
};
|
|
353
354
|
}
|
|
354
|
-
catch (
|
|
355
|
+
catch (replyError) {
|
|
355
356
|
// The command is already live but the operation must not report a
|
|
356
357
|
// success it cannot honor: stop it so a client retry cannot
|
|
357
358
|
// double-execute (#3210 review). Best-effort — the surfaced error
|
|
358
|
-
// stays the
|
|
359
|
+
// stays the reply failure.
|
|
359
360
|
try {
|
|
360
361
|
await this.#manager.stopBackgroundTask(input.sessionId, launched.ref, new AbortController().signal, 'client');
|
|
361
362
|
}
|
|
362
363
|
catch {
|
|
363
|
-
/* keep the
|
|
364
|
+
/* keep the reply failure as the surfaced cause */
|
|
364
365
|
}
|
|
365
|
-
throw
|
|
366
|
+
throw replyError;
|
|
366
367
|
}
|
|
367
368
|
});
|
|
368
369
|
}
|
|
@@ -388,8 +389,11 @@ export class HostRuntimeResourceCoordinator {
|
|
|
388
389
|
if (sessionFailure)
|
|
389
390
|
return mutationFailure('runtime.resource.controller.acquire', sessionFailure);
|
|
390
391
|
try {
|
|
391
|
-
const
|
|
392
|
-
if (
|
|
392
|
+
const pty = this.#manager.getLivePtySnapshot(input.sessionId, input.ref);
|
|
393
|
+
if (!pty) {
|
|
394
|
+
// No live handle: read through the manager so a stale active record
|
|
395
|
+
// is repaired to orphaned; the reply is a conflict either way.
|
|
396
|
+
await this.#manager.inspectResource(input.sessionId, input.ref);
|
|
393
397
|
return mutationFailure('runtime.resource.controller.acquire', {
|
|
394
398
|
code: 'operation_conflict',
|
|
395
399
|
message: 'Only an active PTY Runtime Resource can be controlled',
|
|
@@ -420,14 +424,6 @@ export class HostRuntimeResourceCoordinator {
|
|
|
420
424
|
};
|
|
421
425
|
this.#controllers.set(key, controller);
|
|
422
426
|
this.#controllerResources.set(identity, key);
|
|
423
|
-
const pty = this.#manager.getLivePtySnapshot(input.sessionId, input.ref);
|
|
424
|
-
if (!pty) {
|
|
425
|
-
this.#releaseController(key);
|
|
426
|
-
return mutationFailure('runtime.resource.controller.acquire', {
|
|
427
|
-
code: 'operation_conflict',
|
|
428
|
-
message: 'Runtime Resource PTY is no longer available',
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
427
|
return {
|
|
432
428
|
ok: true,
|
|
433
429
|
result: boundedControllerAcquireResult(controller.controllerId, controller.nextSequence, pty),
|
|
@@ -485,7 +481,6 @@ export class HostRuntimeResourceCoordinator {
|
|
|
485
481
|
const result = decodeRuntimeResourceControllerControlResult({
|
|
486
482
|
controllerId: input.controllerId,
|
|
487
483
|
sequence: input.sequence,
|
|
488
|
-
resource: boundedRuntimeResourceSnapshot(runtimeResourceSnapshotFromResult(controlled)),
|
|
489
484
|
});
|
|
490
485
|
this.#rememberReplay({
|
|
491
486
|
connectionId: context.connectionId,
|
|
@@ -555,12 +550,7 @@ export class HostRuntimeResourceCoordinator {
|
|
|
555
550
|
try {
|
|
556
551
|
const result = await this.#manager.stopBackgroundTask(input.sessionId, input.ref, new AbortController().signal, 'client');
|
|
557
552
|
this.#releaseControllerIfTerminal(input.sessionId, input.ref, result);
|
|
558
|
-
return {
|
|
559
|
-
ok: true,
|
|
560
|
-
result: decodeRuntimeResourceStopResult({
|
|
561
|
-
resource: boundedRuntimeResourceSnapshot(runtimeResourceSnapshotFromResult(result)),
|
|
562
|
-
}),
|
|
563
|
-
};
|
|
553
|
+
return { ok: true, result: decodeRuntimeResourceStopResult({}) };
|
|
564
554
|
}
|
|
565
555
|
catch (error) {
|
|
566
556
|
return this.#resourceFailure('runtime.resource.stop', error);
|
|
@@ -28,8 +28,11 @@ export function canonicalRuntimeResources(resources) {
|
|
|
28
28
|
left.sourceToolCallId.localeCompare(right.sourceToolCallId));
|
|
29
29
|
}
|
|
30
30
|
function boundedRuntimeResourceUpdate(update) {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
return { ...update, result: boundedRuntimeResourceState(update.result) };
|
|
32
|
+
}
|
|
33
|
+
export function boundedRuntimeResourceState(state) {
|
|
34
|
+
const bounded = structuredClone(state);
|
|
35
|
+
shrinkStateToFit(bounded);
|
|
33
36
|
return bounded;
|
|
34
37
|
}
|
|
35
38
|
export function runtimeResourceRevision(resources) {
|
|
@@ -68,20 +71,6 @@ export function createRuntimeResourcePage(sessionId, revision, resources, offset
|
|
|
68
71
|
nextCursor: nextOffset < resources.length ? String(nextOffset) : null,
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
|
-
export function boundedRuntimeResourceSnapshot(snapshot) {
|
|
72
|
-
const bounded = structuredClone(snapshot);
|
|
73
|
-
shrinkStateToFit(bounded);
|
|
74
|
-
if (bounded.output === undefined)
|
|
75
|
-
throw new Error('Runtime Resource snapshot lost its output');
|
|
76
|
-
return bounded;
|
|
77
|
-
}
|
|
78
|
-
export function runtimeResourceSnapshotFromResult(result) {
|
|
79
|
-
if (result.kind !== 'shell_run' || result.output === undefined) {
|
|
80
|
-
throw new Error('Runtime Resource operation did not produce a ShellRun snapshot');
|
|
81
|
-
}
|
|
82
|
-
const { operation: _operation, ...snapshot } = result;
|
|
83
|
-
return snapshot;
|
|
84
|
-
}
|
|
85
74
|
function shrinkStateToFit(state) {
|
|
86
75
|
while (Buffer.byteLength(JSON.stringify(state), 'utf8') > RUNTIME_RESOURCE_SNAPSHOT_MAX_BYTES) {
|
|
87
76
|
const fields = mutableTextFields(state).sort((left, right) => Buffer.byteLength(right.value(), 'utf8') - Buffer.byteLength(left.value(), 'utf8'));
|