@deepseek-ai/dsh-api-terminal-controller 0.1.6-alpha.1 → 0.1.6-alpha.2
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.i18n.yaml +2 -2
- package/README.md +14 -5
- package/README.zh.md +14 -5
- package/lib/client.js +249 -19
- package/lib/index.js +263 -61
- package/lib/typert.host.js +165 -78
- package/lib/typert.remote-client.d.ts +3 -1
- package/lib/typert.remote-client.js +152 -76
- package/lib/types/client/bindings.d.ts +31 -0
- package/lib/types/client/bindings.js +71 -0
- package/lib/types/client/index.d.ts +22 -4
- package/lib/types/client/index.js +83 -8
- package/lib/types/client/model.d.ts +3 -1
- package/lib/types/client/model.js +20 -7
- package/lib/types/client/retention.d.ts +35 -0
- package/lib/types/client/retention.js +77 -0
- package/lib/types/index.d.ts +17 -3
- package/lib/types/index.js +49 -43
- package/lib/types/retention.d.ts +53 -0
- package/lib/types/retention.js +153 -0
- package/lib/types/terminal.d.ts +23 -1
- package/lib/types/terminal.js +40 -9
- package/lib/types/types.d.ts +6 -0
- package/package.json +17 -16
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import z from "@deepseek-ai/schemastery";
|
|
3
2
|
import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
3
|
import { SubprocessExecutableNotFoundError } from "@deepseek-ai/dsh-subprocess";
|
|
4
|
+
import { createLazyRequire } from "@deepseek-ai/dsh-lazy-require";
|
|
5
5
|
import { Deque } from "@deepseek-ai/dsh-deque";
|
|
6
6
|
//#region lib/types/shells.js
|
|
7
7
|
/** Shell selection and executable verification use the target execution provider. */
|
|
@@ -139,18 +139,161 @@ var TerminalFollower = class {
|
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
141
|
//#endregion
|
|
142
|
+
//#region lib/types/retention.js
|
|
143
|
+
/** Window holds and conservative idle reclamation for one terminal owner. */
|
|
144
|
+
/** Exactly one owner orders holds, observation, and retryable process cleanup. */
|
|
145
|
+
var TerminalRetention = class {
|
|
146
|
+
policy;
|
|
147
|
+
inspect;
|
|
148
|
+
terminate;
|
|
149
|
+
failed;
|
|
150
|
+
lifetime = new AbortController();
|
|
151
|
+
holders = /* @__PURE__ */ new Set();
|
|
152
|
+
epoch = 0;
|
|
153
|
+
timer;
|
|
154
|
+
observation;
|
|
155
|
+
idle;
|
|
156
|
+
closing = false;
|
|
157
|
+
disposed = false;
|
|
158
|
+
cleanup;
|
|
159
|
+
/**
|
|
160
|
+
* @param policy - deployment timing choices.
|
|
161
|
+
* @param inspect - fresh shell and owned-job observation.
|
|
162
|
+
* @param terminate - mark the identity closed, await process quiescence, and remove its owner record.
|
|
163
|
+
* @param failed - diagnostic sink for failed automatic cleanup.
|
|
164
|
+
*/
|
|
165
|
+
constructor(policy, inspect, terminate, failed) {
|
|
166
|
+
this.policy = policy;
|
|
167
|
+
this.inspect = inspect;
|
|
168
|
+
this.terminate = terminate;
|
|
169
|
+
this.failed = failed;
|
|
170
|
+
this.schedule(0);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Hold one terminal for one physical Remote stream, independently of screen subscriptions.
|
|
174
|
+
* @param signal - transport generation lifetime.
|
|
175
|
+
* @returns acknowledgement followed by an open stream until cancellation or terminal closure.
|
|
176
|
+
*/
|
|
177
|
+
async *retain(signal) {
|
|
178
|
+
signal.throwIfAborted();
|
|
179
|
+
if (this.closing || this.disposed) throw new RemoteError("terminal/unavailable", "Terminal is closing or unavailable", {});
|
|
180
|
+
const holder = {};
|
|
181
|
+
const ended = Promise.withResolvers();
|
|
182
|
+
const combined = AbortSignal.any([signal, this.lifetime.signal]);
|
|
183
|
+
const release = () => {
|
|
184
|
+
if (!this.holders.delete(holder)) return;
|
|
185
|
+
combined.removeEventListener("abort", release);
|
|
186
|
+
this.invalidate();
|
|
187
|
+
ended.resolve();
|
|
188
|
+
this.schedule(0);
|
|
189
|
+
};
|
|
190
|
+
this.holders.add(holder);
|
|
191
|
+
this.invalidate();
|
|
192
|
+
this.cancelTimer();
|
|
193
|
+
combined.addEventListener("abort", release, { once: true });
|
|
194
|
+
try {
|
|
195
|
+
yield { type: "retained" };
|
|
196
|
+
await ended.promise;
|
|
197
|
+
} finally {
|
|
198
|
+
release();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Invalidate outstanding idle observations before accepting input. */
|
|
202
|
+
invalidate() {
|
|
203
|
+
this.epoch++;
|
|
204
|
+
this.idle = void 0;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Start or join cleanup; failure keeps the identity closed and schedules one retry.
|
|
208
|
+
* @returns after owned process cleanup succeeds, or rejects with its failure.
|
|
209
|
+
*/
|
|
210
|
+
close() {
|
|
211
|
+
if (this.cleanup !== void 0) return this.cleanup;
|
|
212
|
+
this.closing = true;
|
|
213
|
+
this.invalidate();
|
|
214
|
+
this.lifetime.abort(/* @__PURE__ */ new Error("Terminal closed"));
|
|
215
|
+
this.cancelTimer();
|
|
216
|
+
this.cleanup = this.terminate().catch((error) => {
|
|
217
|
+
this.cleanup = void 0;
|
|
218
|
+
this.schedule(this.policy.cleanupRetryMs);
|
|
219
|
+
throw error;
|
|
220
|
+
});
|
|
221
|
+
return this.cleanup;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Stop timers and streams and await both observation and final cleanup.
|
|
225
|
+
* @returns after process quiescence; cleanup failure is reported to the disposing owner.
|
|
226
|
+
*/
|
|
227
|
+
async dispose() {
|
|
228
|
+
this.disposed = true;
|
|
229
|
+
this.cancelTimer();
|
|
230
|
+
const observation = this.observation;
|
|
231
|
+
try {
|
|
232
|
+
await this.close();
|
|
233
|
+
} finally {
|
|
234
|
+
await observation;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
cancelTimer() {
|
|
238
|
+
clearTimeout(this.timer);
|
|
239
|
+
this.timer = void 0;
|
|
240
|
+
}
|
|
241
|
+
schedule(delay) {
|
|
242
|
+
if (this.disposed || this.timer !== void 0) return;
|
|
243
|
+
if (!this.closing && (this.holders.size > 0 || this.policy.unattendedTimeoutMs === 0)) return;
|
|
244
|
+
const due = performance.now() + delay;
|
|
245
|
+
this.timer = setTimeout(() => {
|
|
246
|
+
this.timer = void 0;
|
|
247
|
+
const remaining = due - performance.now();
|
|
248
|
+
if (remaining > 0) {
|
|
249
|
+
this.schedule(remaining);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (this.closing) {
|
|
253
|
+
this.close().catch(this.failed);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.observe();
|
|
257
|
+
}, Math.min(delay, 2147483647));
|
|
258
|
+
this.timer.unref();
|
|
259
|
+
}
|
|
260
|
+
observe() {
|
|
261
|
+
if (this.observation !== void 0) return;
|
|
262
|
+
const epoch = this.epoch;
|
|
263
|
+
this.observation = (async () => {
|
|
264
|
+
let activity;
|
|
265
|
+
try {
|
|
266
|
+
activity = await this.inspect();
|
|
267
|
+
} catch (_activityUnavailable) {
|
|
268
|
+
activity = {
|
|
269
|
+
state: "unknown",
|
|
270
|
+
revision: 0
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (this.disposed || this.closing || this.holders.size > 0 || epoch !== this.epoch) return;
|
|
274
|
+
const now = performance.now();
|
|
275
|
+
if (activity.state !== "idle") {
|
|
276
|
+
this.idle = void 0;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (this.idle?.revision !== activity.revision || now - this.idle.observedAt > this.policy.activityPollIntervalMs * 2) this.idle = {
|
|
280
|
+
since: now,
|
|
281
|
+
observedAt: now,
|
|
282
|
+
revision: activity.revision
|
|
283
|
+
};
|
|
284
|
+
else this.idle.observedAt = now;
|
|
285
|
+
if (now - this.idle.since >= this.policy.unattendedTimeoutMs) await this.close();
|
|
286
|
+
})().catch(this.failed).finally(() => {
|
|
287
|
+
this.observation = void 0;
|
|
288
|
+
if (!this.closing) this.schedule(this.policy.activityPollIntervalMs);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
//#endregion
|
|
142
293
|
//#region lib/types/terminal.js
|
|
143
294
|
/** One PTY, a bounded terminal emulator and its detachable browser followers. */
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const require = createRequire(import.meta.url);
|
|
147
|
-
const { Terminal } = require("@xterm/headless");
|
|
148
|
-
const { SerializeAddon } = require("@xterm/addon-serialize");
|
|
149
|
-
return {
|
|
150
|
-
Terminal,
|
|
151
|
-
SerializeAddon
|
|
152
|
-
};
|
|
153
|
-
}
|
|
295
|
+
const requireHeadless = createLazyRequire("@xterm/headless", import.meta.url);
|
|
296
|
+
const requireSerialize = createLazyRequire("@xterm/addon-serialize", import.meta.url);
|
|
154
297
|
/** Process lifetime is independent of follower and component lifetimes. */
|
|
155
298
|
var BrowserTerminal = class {
|
|
156
299
|
handle;
|
|
@@ -163,6 +306,7 @@ var BrowserTerminal = class {
|
|
|
163
306
|
operations = Promise.resolve();
|
|
164
307
|
drained;
|
|
165
308
|
closing;
|
|
309
|
+
retention;
|
|
166
310
|
controller;
|
|
167
311
|
/**
|
|
168
312
|
* @param handle - allocated terminal process range.
|
|
@@ -174,6 +318,8 @@ var BrowserTerminal = class {
|
|
|
174
318
|
this.handle = handle;
|
|
175
319
|
this.info = info;
|
|
176
320
|
this.maxBufferedBytes = maxBufferedBytes;
|
|
321
|
+
const { Terminal } = requireHeadless();
|
|
322
|
+
const { SerializeAddon } = requireSerialize();
|
|
177
323
|
this.screen = new Terminal({
|
|
178
324
|
cols: info.cols,
|
|
179
325
|
rows: info.rows,
|
|
@@ -185,6 +331,29 @@ var BrowserTerminal = class {
|
|
|
185
331
|
this.drained = this.consume();
|
|
186
332
|
}
|
|
187
333
|
/**
|
|
334
|
+
* Start monitoring after this allocation is committed to its Session owner.
|
|
335
|
+
* @param policy - validated Host timing policy.
|
|
336
|
+
* @param closing - closes the id before any asynchronous termination.
|
|
337
|
+
* @param closed - removes the exact successfully terminated owner record.
|
|
338
|
+
* @param failed - diagnostic sink for background cleanup failure.
|
|
339
|
+
*/
|
|
340
|
+
monitor(policy, closing, closed, failed) {
|
|
341
|
+
this.retention = new TerminalRetention(policy, () => this.handle.inspectActivity(), async () => {
|
|
342
|
+
closing();
|
|
343
|
+
await this.closeProcess();
|
|
344
|
+
closed();
|
|
345
|
+
}, failed);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Retain this committed process independently of output attachment.
|
|
349
|
+
* @param signal - physical window stream lifetime.
|
|
350
|
+
* @returns its hold acknowledgement and lifetime.
|
|
351
|
+
*/
|
|
352
|
+
retain(signal) {
|
|
353
|
+
if (this.retention === void 0) throw new Error("Terminal has not been committed");
|
|
354
|
+
return this.retention.retain(signal);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
188
357
|
* Attach with exclusive input control; an older attachment becomes read-only.
|
|
189
358
|
* @param id - browser attachment identity.
|
|
190
359
|
* @param signal - attachment cancellation; never terminates the process.
|
|
@@ -240,6 +409,7 @@ var BrowserTerminal = class {
|
|
|
240
409
|
* @returns when the provider accepts the input.
|
|
241
410
|
*/
|
|
242
411
|
write(id, data) {
|
|
412
|
+
this.retention?.invalidate();
|
|
243
413
|
return this.enqueue(async () => {
|
|
244
414
|
this.requireController(id);
|
|
245
415
|
await this.handle.write(data);
|
|
@@ -287,6 +457,16 @@ var BrowserTerminal = class {
|
|
|
287
457
|
* @returns after process cleanup and final output drainage; failures remain retryable.
|
|
288
458
|
*/
|
|
289
459
|
close() {
|
|
460
|
+
return this.retention?.close() ?? this.closeProcess();
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Stop unattended cleanup scheduling and await final process cleanup.
|
|
464
|
+
* @returns after terminal and monitor quiescence.
|
|
465
|
+
*/
|
|
466
|
+
dispose() {
|
|
467
|
+
return this.retention?.dispose() ?? this.closeProcess();
|
|
468
|
+
}
|
|
469
|
+
closeProcess() {
|
|
290
470
|
if (this.closing !== void 0) return this.closing;
|
|
291
471
|
this.closing = (async () => {
|
|
292
472
|
await this.handle.terminate();
|
|
@@ -402,6 +582,7 @@ let TerminalController = (() => {
|
|
|
402
582
|
let _shells_decorators;
|
|
403
583
|
let _list_decorators;
|
|
404
584
|
let _create_decorators;
|
|
585
|
+
let _retain_decorators;
|
|
405
586
|
let _follow_decorators;
|
|
406
587
|
let _write_decorators;
|
|
407
588
|
let _resize_decorators;
|
|
@@ -414,6 +595,7 @@ let TerminalController = (() => {
|
|
|
414
595
|
_shells_decorators = [Remote];
|
|
415
596
|
_list_decorators = [Remote];
|
|
416
597
|
_create_decorators = [Remote];
|
|
598
|
+
_retain_decorators = [Remote({ mode: "stream" })];
|
|
417
599
|
_follow_decorators = [Remote({ mode: "stream" })];
|
|
418
600
|
_write_decorators = [Remote];
|
|
419
601
|
_resize_decorators = [Remote];
|
|
@@ -463,6 +645,17 @@ let TerminalController = (() => {
|
|
|
463
645
|
},
|
|
464
646
|
metadata: _metadata
|
|
465
647
|
}, null, _instanceExtraInitializers);
|
|
648
|
+
__esDecorate(this, null, _retain_decorators, {
|
|
649
|
+
kind: "method",
|
|
650
|
+
name: "retain",
|
|
651
|
+
static: false,
|
|
652
|
+
private: false,
|
|
653
|
+
access: {
|
|
654
|
+
has: (obj) => "retain" in obj,
|
|
655
|
+
get: (obj) => obj.retain
|
|
656
|
+
},
|
|
657
|
+
metadata: _metadata
|
|
658
|
+
}, null, _instanceExtraInitializers);
|
|
466
659
|
__esDecorate(this, null, _follow_decorators, {
|
|
467
660
|
kind: "method",
|
|
468
661
|
name: "follow",
|
|
@@ -529,7 +722,6 @@ let TerminalController = (() => {
|
|
|
529
722
|
static inject = [
|
|
530
723
|
"subprocess",
|
|
531
724
|
"sandboxPolicy",
|
|
532
|
-
"sessionProjections",
|
|
533
725
|
"typert"
|
|
534
726
|
];
|
|
535
727
|
static Config = z.object({
|
|
@@ -552,7 +744,10 @@ let TerminalController = (() => {
|
|
|
552
744
|
scrollback: z.number().step(1).min(0).default(1e3),
|
|
553
745
|
maxBufferedBytes: z.number().step(1).min(1024).default(2 * 1024 * 1024),
|
|
554
746
|
maxInputBytes: z.number().step(1).min(1).default(64 * 1024),
|
|
555
|
-
disposeGraceMs: z.number().step(1).min(1).default(1e3)
|
|
747
|
+
disposeGraceMs: z.number().step(1).min(1).default(1e3),
|
|
748
|
+
unattendedTimeoutMs: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(72e5),
|
|
749
|
+
activityPollIntervalMs: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(3e4),
|
|
750
|
+
cleanupRetryMs: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(6e4)
|
|
556
751
|
});
|
|
557
752
|
owners = /* @__PURE__ */ new Map();
|
|
558
753
|
lifetime = new AbortController();
|
|
@@ -563,15 +758,6 @@ let TerminalController = (() => {
|
|
|
563
758
|
constructor(ctx, config) {
|
|
564
759
|
super(ctx, "terminalController", { namespace: "terminal" });
|
|
565
760
|
this.config = config;
|
|
566
|
-
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
567
|
-
if (eventName !== "session/event") return;
|
|
568
|
-
const [session, event] = args;
|
|
569
|
-
if (event.type !== "sandbox/mode") return;
|
|
570
|
-
const owner = this.owners.get(session.id);
|
|
571
|
-
if (owner === void 0 || owner.terminals.size + owner.pending.size + owner.allocations.size === 0) return;
|
|
572
|
-
const current = ctx.sessionProjections.stateOf(session, "sandboxMode") ?? ctx.sandboxPolicy.defaultMode;
|
|
573
|
-
if (event.data.mode !== current) throw new Error("Close browser terminals before changing the Session sandbox mode");
|
|
574
|
-
}, { global: true });
|
|
575
761
|
ctx.effect(() => async () => {
|
|
576
762
|
this.lifetime.abort(/* @__PURE__ */ new Error("Terminal controller disposed"));
|
|
577
763
|
const errors = (await Promise.allSettled([...this.owners].map(([id, owner]) => this.disposeOwner(id, owner)))).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
@@ -588,7 +774,7 @@ let TerminalController = (() => {
|
|
|
588
774
|
signal.throwIfAborted();
|
|
589
775
|
const { sandboxPolicy } = this.execution(agent);
|
|
590
776
|
return {
|
|
591
|
-
cwd:
|
|
777
|
+
cwd: agent.session.header.cwd ?? sandboxPolicy.workspaceRoot,
|
|
592
778
|
maxInputBytes: this.config.maxInputBytes,
|
|
593
779
|
maxCols: this.config.maxCols,
|
|
594
780
|
maxRows: this.config.maxRows,
|
|
@@ -616,7 +802,7 @@ let TerminalController = (() => {
|
|
|
616
802
|
return [...owner.terminals.values(), ...owner.allocations.values()].map((terminal) => terminal.info);
|
|
617
803
|
}
|
|
618
804
|
/**
|
|
619
|
-
* Allocate
|
|
805
|
+
* Allocate a user shell once for a caller-generated identity, without Agent sandbox or approval restrictions.
|
|
620
806
|
* @param agent - Session owner supplied by the Gateway.
|
|
621
807
|
* @param request - initial dimensions and idempotency identity.
|
|
622
808
|
* @param signal - allocation cancellation; committed terminals survive disconnection.
|
|
@@ -637,7 +823,6 @@ let TerminalController = (() => {
|
|
|
637
823
|
this.requireOpen(owner, request.id);
|
|
638
824
|
return terminal.info;
|
|
639
825
|
}
|
|
640
|
-
if (owner.allocations.has(request.id)) throw new Error("Close the failed terminal allocation before creating it again");
|
|
641
826
|
if (new Set([
|
|
642
827
|
...owner.terminals.keys(),
|
|
643
828
|
...owner.pending.keys(),
|
|
@@ -653,6 +838,13 @@ let TerminalController = (() => {
|
|
|
653
838
|
const terminal = await allocation;
|
|
654
839
|
owner.terminals.set(request.id, terminal);
|
|
655
840
|
owner.allocations.delete(request.id);
|
|
841
|
+
terminal.monitor(this.config, () => {
|
|
842
|
+
owner.closedIds.add(request.id);
|
|
843
|
+
}, () => {
|
|
844
|
+
owner.terminals.delete(request.id);
|
|
845
|
+
}, (error) => {
|
|
846
|
+
this.ctx.logger.error("Browser terminal cleanup failed", error);
|
|
847
|
+
});
|
|
656
848
|
this.requireOpen(owner, request.id);
|
|
657
849
|
return terminal.info;
|
|
658
850
|
} finally {
|
|
@@ -660,6 +852,19 @@ let TerminalController = (() => {
|
|
|
660
852
|
}
|
|
661
853
|
}
|
|
662
854
|
/**
|
|
855
|
+
* Retain an existing terminal for a window without activating its Agent or taking input control.
|
|
856
|
+
* @param sessionId - owning Session identity, including an inactive saved layout.
|
|
857
|
+
* @param id - retained Host terminal identity.
|
|
858
|
+
* @param signal - physical Remote stream cancellation.
|
|
859
|
+
* @returns a hold acknowledgement followed by an open lifetime stream.
|
|
860
|
+
*/
|
|
861
|
+
retain(sessionId, id, signal) {
|
|
862
|
+
const owner = this.owners.get(sessionId);
|
|
863
|
+
const terminal = owner?.terminals.get(id);
|
|
864
|
+
if (terminal === void 0 || owner?.closedIds.has(id) === true || owner?.lifetime.signal.aborted === true) throw new RemoteError("terminal/unavailable", "Terminal is closing or unavailable", {});
|
|
865
|
+
return terminal.retain(signal);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
663
868
|
* Attach to a terminal without binding its process lifetime to the transport.
|
|
664
869
|
* @param agent - Session owner supplied by the Gateway.
|
|
665
870
|
* @param id - terminal identity.
|
|
@@ -723,7 +928,7 @@ let TerminalController = (() => {
|
|
|
723
928
|
} else {
|
|
724
929
|
const allocation = owner.allocations.get(id);
|
|
725
930
|
if (allocation === void 0) return;
|
|
726
|
-
await allocation.
|
|
931
|
+
await allocation.cleanup.close();
|
|
727
932
|
owner.allocations.delete(id);
|
|
728
933
|
}
|
|
729
934
|
}
|
|
@@ -750,7 +955,7 @@ let TerminalController = (() => {
|
|
|
750
955
|
owner.lifetime.abort(/* @__PURE__ */ new Error("Terminal Session owner disposed"));
|
|
751
956
|
owner.cleanup = (async () => {
|
|
752
957
|
await Promise.allSettled(owner.pending.values());
|
|
753
|
-
const errors = (await Promise.allSettled([...[...owner.terminals.values()].map((terminal) => terminal.
|
|
958
|
+
const errors = (await Promise.allSettled([...[...owner.terminals.values()].map((terminal) => terminal.dispose()), ...[...owner.allocations.values()].map((allocation) => allocation.cleanup.dispose())])).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
754
959
|
if (errors.length > 0) throw new AggregateError(errors, "Session terminal cleanup failed");
|
|
755
960
|
owner.terminals.clear();
|
|
756
961
|
owner.allocations.clear();
|
|
@@ -763,11 +968,12 @@ let TerminalController = (() => {
|
|
|
763
968
|
}
|
|
764
969
|
terminal(agent, id) {
|
|
765
970
|
const terminal = this.owners.get(agent.id)?.terminals.get(id);
|
|
766
|
-
if (terminal === void 0) throw new
|
|
971
|
+
if (terminal === void 0) throw new RemoteError("terminal/unavailable", "Terminal no longer exists in this Session", {});
|
|
972
|
+
this.requireOpen(this.owners.get(agent.id), id);
|
|
767
973
|
return terminal;
|
|
768
974
|
}
|
|
769
975
|
requireOpen(owner, id) {
|
|
770
|
-
if (owner.closedIds.has(id)) throw new
|
|
976
|
+
if (owner.closedIds.has(id)) throw new RemoteError("terminal/unavailable", "Terminal was closed in this Session", {});
|
|
771
977
|
}
|
|
772
978
|
dimensions(cols, rows) {
|
|
773
979
|
if (!Number.isSafeInteger(cols) || cols < 2 || cols > this.config.maxCols || !Number.isSafeInteger(rows) || rows < 1 || rows > this.config.maxRows) throw new Error("Terminal dimensions exceed the configured limits");
|
|
@@ -783,55 +989,51 @@ let TerminalController = (() => {
|
|
|
783
989
|
}
|
|
784
990
|
async spawn(agent, owner, request, signal) {
|
|
785
991
|
const environment = this.environment(agent, signal);
|
|
786
|
-
const { subprocess
|
|
992
|
+
const { subprocess } = this.execution(agent);
|
|
787
993
|
const shell = request.shellPath === void 0 ? await resolveShell(subprocess, this.config.shell, signal) : (await this.shells(agent, signal)).find((candidate) => candidate.path === request.shellPath);
|
|
788
994
|
if (shell === void 0) throw new Error("Selected shell is not available in this execution environment");
|
|
789
|
-
const policy = sandboxPolicy.resolve({ session: agent.session });
|
|
790
|
-
let argv = [shell.path, ...shell.args];
|
|
791
|
-
if (policy.mode !== "danger-full-access") {
|
|
792
|
-
const sandbox = agent.ctx.get("sandbox");
|
|
793
|
-
if (sandbox === void 0) throw new Error("The Session sandbox mode requires an execution sandbox provider");
|
|
794
|
-
argv = (await sandbox.confine(argv, {
|
|
795
|
-
...policy,
|
|
796
|
-
mode: policy.mode
|
|
797
|
-
}, signal)).argv;
|
|
798
|
-
}
|
|
799
995
|
const handle = await subprocess.spawnTerminal({
|
|
800
|
-
argv,
|
|
996
|
+
argv: [shell.path, ...shell.args],
|
|
801
997
|
cwd: environment.cwd,
|
|
802
998
|
cols: request.cols,
|
|
803
999
|
rows: request.rows,
|
|
804
1000
|
terminalType: "xterm-256color",
|
|
805
1001
|
env: { DSH_SESSION_ID: agent.id },
|
|
1002
|
+
shellActivity: true,
|
|
806
1003
|
graceMs: this.config.disposeGraceMs,
|
|
807
1004
|
signal
|
|
808
1005
|
});
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
state: "running",
|
|
819
|
-
exitCode: null
|
|
820
|
-
}
|
|
1006
|
+
const info = {
|
|
1007
|
+
id: request.id,
|
|
1008
|
+
shell,
|
|
1009
|
+
title: shell.name,
|
|
1010
|
+
cwd: environment.cwd,
|
|
1011
|
+
cols: request.cols,
|
|
1012
|
+
rows: request.rows,
|
|
1013
|
+
state: "running",
|
|
1014
|
+
exitCode: null
|
|
821
1015
|
};
|
|
822
|
-
owner.allocations.set(request.id, allocation);
|
|
823
1016
|
try {
|
|
824
1017
|
signal.throwIfAborted();
|
|
825
|
-
return new BrowserTerminal(handle,
|
|
1018
|
+
return new BrowserTerminal(handle, info, this.config.scrollback, this.config.maxBufferedBytes);
|
|
826
1019
|
} catch (error) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
state: "failed",
|
|
830
|
-
error: error instanceof Error ? error.message : String(error)
|
|
831
|
-
};
|
|
832
|
-
try {
|
|
1020
|
+
const cleanup = new TerminalRetention(this.config, handle.inspectActivity.bind(handle), async () => {
|
|
1021
|
+
owner.closedIds.add(request.id);
|
|
833
1022
|
await handle.terminate();
|
|
834
1023
|
owner.allocations.delete(request.id);
|
|
1024
|
+
}, (cleanupError) => {
|
|
1025
|
+
this.ctx.logger.error("Browser terminal allocation cleanup failed", cleanupError);
|
|
1026
|
+
});
|
|
1027
|
+
owner.allocations.set(request.id, {
|
|
1028
|
+
info: {
|
|
1029
|
+
...info,
|
|
1030
|
+
state: "failed",
|
|
1031
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1032
|
+
},
|
|
1033
|
+
cleanup
|
|
1034
|
+
});
|
|
1035
|
+
try {
|
|
1036
|
+
await cleanup.close();
|
|
835
1037
|
} catch (cleanupError) {
|
|
836
1038
|
throw new AggregateError([error, cleanupError], "Terminal allocation cleanup failed");
|
|
837
1039
|
}
|