refactor(pty): rename model-facing tools to terminal_* and harden teardown
Rename the six model-facing tools pty_* -> terminal_* and align every description, guidance section, ACP card title, and rendered result to terminal terminology. Package and service internals keep their technical PTY names (PtyService, "unknown PTY session", node-pty). Harden the local backend teardown: - a failed close is retryable: drop the memoized rejection so a later terminal_close re-runs against the live process table - service disposal clears the backend, reservation, and owner-cleanup registries even when a close fails - stop readiness polling before teardown so an in-flight send settles as session_exit instead of a mis-inferred wait reason - bound the sanitizer's pending buffer against unterminated escape runs Update the tool catalog, package READMEs, the bilingual Agent Note, and the acp/headless pty-tools snapshots to match.
This commit is contained in:
@@ -331,12 +331,18 @@ export class PtyService extends Service {
|
||||
private async disposeAll(): Promise<void> {
|
||||
this.disposing = true
|
||||
const records = [...this.sessions.values()]
|
||||
await this.closeRecords(records, 'PTY service disposed')
|
||||
this.backends.clear()
|
||||
this.reservedNames.clear()
|
||||
const cleanups = [...this.ownerCleanups.values()]
|
||||
this.ownerCleanups.clear()
|
||||
await Promise.all(cleanups.map(cleanup => Promise.resolve(cleanup())))
|
||||
// Teardown is best-effort: a close failure still clears registries and runs
|
||||
// owner cleanups before the aggregated error propagates, so one stuck
|
||||
// session cannot orphan backends, reservations, or owner detachers.
|
||||
try {
|
||||
await this.closeRecords(records, 'PTY service disposed')
|
||||
} finally {
|
||||
this.backends.clear()
|
||||
this.reservedNames.clear()
|
||||
const cleanups = [...this.ownerCleanups.values()]
|
||||
this.ownerCleanups.clear()
|
||||
await Promise.all(cleanups.map(cleanup => Promise.resolve(cleanup())))
|
||||
}
|
||||
}
|
||||
|
||||
private async closeRecords(records: SessionRecord[], reason: string): Promise<void> {
|
||||
|
||||
@@ -127,7 +127,7 @@ export interface PtySessionSnapshot {
|
||||
|
||||
/** Backend-owned live session retained by {@link PtyService}. */
|
||||
export interface PtyBackendSession {
|
||||
/** Initial bounded terminal output returned from `pty_spawn`. */
|
||||
/** Initial bounded terminal output returned from `terminal_open`. */
|
||||
readonly motd: string
|
||||
/** Top-level process id when one exists. */
|
||||
readonly pid?: number
|
||||
|
||||
@@ -343,4 +343,25 @@ describe('PtyService ownership and lifecycle', () => {
|
||||
await disposePtyService(ctx)
|
||||
await expect(service.spawn(owner, { type: 'stub' })).rejects.toMatchObject({ code: 'SERVICE_DISPOSING' })
|
||||
})
|
||||
|
||||
it('clears registries and runs owner cleanups even when a session close fails', async () => {
|
||||
const ctx = await harness()
|
||||
const service = ctx.pty
|
||||
const b = backend()
|
||||
service.registerBackend(b.provider)
|
||||
const owner = stubAgent(ctx, 'owner')
|
||||
ctx.agents.register(owner)
|
||||
await service.spawn(owner, { type: 'stub' })
|
||||
b.sessions[0]!.rejectClose = true
|
||||
const internal = service as unknown as {
|
||||
disposeAll(): Promise<void>
|
||||
backends: Map<string, unknown>
|
||||
ownerCleanups: Map<Agent, unknown>
|
||||
}
|
||||
// Teardown surfaces the close failure, but its finally still clears the
|
||||
// backend and owner-cleanup registries instead of orphaning them.
|
||||
await expect(internal.disposeAll()).rejects.toThrow('failed to close 1 PTY session')
|
||||
expect(internal.backends.size).toBe(0)
|
||||
expect(internal.ownerCleanups.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user