@zgeoff/atc 2.11.0 → 2.13.0

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 (119) hide show
  1. package/README.md +10 -10
  2. package/package.json +2 -1
  3. package/src/agents/agent-adapter.ts +98 -10
  4. package/src/agents/build-args-without-flags.ts +25 -0
  5. package/src/agents/build-atc-bridge-files.ts +13 -0
  6. package/src/agents/build-claude-override-args.ts +26 -0
  7. package/src/agents/build-claude-query-options.ts +72 -0
  8. package/src/agents/build-cli-command.ts +7 -5
  9. package/src/{daemon → agents}/build-headless-env.ts +12 -6
  10. package/src/agents/build-hook-settings.ts +8 -2
  11. package/src/agents/build-restore-mode-args.ts +23 -0
  12. package/src/agents/claude-adapter.ts +112 -18
  13. package/src/agents/claude-effort-levels.ts +5 -0
  14. package/src/agents/codex-adapter.ts +48 -2
  15. package/src/agents/find-claude-permission-mode.ts +24 -0
  16. package/src/agents/find-flag-value.ts +27 -0
  17. package/src/agents/gateway-adapter.ts +83 -13
  18. package/src/agents/grok-adapter.ts +24 -1
  19. package/src/agents/make-claude-headless-runner.ts +46 -0
  20. package/src/agents/plan-pasted-line-input.ts +22 -0
  21. package/src/agents/plan-typed-line-input.ts +8 -0
  22. package/src/agents/resolve-claude-permission-mode.ts +13 -0
  23. package/src/{daemon/start-headless-run.ts → agents/start-claude-headless-run.ts} +6 -42
  24. package/src/agents/write-atc-bridge.ts +2 -6
  25. package/src/cli.ts +23 -9
  26. package/src/client/daemon-client.ts +14 -2
  27. package/src/client/index.ts +50 -1
  28. package/src/client/spawn-picker.ts +27 -4
  29. package/src/client/ui.ts +8 -0
  30. package/src/daemon/build-agent-list.ts +37 -1
  31. package/src/daemon/build-config-revision.ts +27 -0
  32. package/src/daemon/build-execution-targets.ts +73 -0
  33. package/src/daemon/build-fleet-events.ts +2 -1
  34. package/src/daemon/build-imp-provider.ts +68 -0
  35. package/src/daemon/build-payload-hash.ts +31 -0
  36. package/src/daemon/build-report-trail-entry.ts +4 -2
  37. package/src/daemon/build-scoped-context.ts +235 -0
  38. package/src/daemon/build-session-lifecycle.ts +52 -0
  39. package/src/daemon/build-tar-archive.ts +85 -0
  40. package/src/daemon/build-target-access.ts +33 -0
  41. package/src/daemon/build-target-forbidden-error.ts +13 -0
  42. package/src/daemon/build-target-identity.ts +22 -0
  43. package/src/daemon/build-target-list.ts +50 -0
  44. package/src/daemon/daemon-connection.ts +425 -96
  45. package/src/daemon/daemon.ts +730 -96
  46. package/src/daemon/effect-remains-error.ts +13 -0
  47. package/src/daemon/execution-provider.ts +208 -0
  48. package/src/daemon/find-execution-refusal.ts +104 -0
  49. package/src/daemon/hooks.ts +5 -17
  50. package/src/daemon/idempotency-ledger.ts +164 -0
  51. package/src/daemon/imp-client-port.ts +343 -0
  52. package/src/daemon/imp-harness.ts +618 -0
  53. package/src/daemon/imp-port-error.ts +18 -0
  54. package/src/daemon/imp-port.ts +246 -0
  55. package/src/daemon/imp-provider.ts +601 -0
  56. package/src/daemon/is-binding-current.ts +43 -0
  57. package/src/daemon/local-pty-provider.ts +142 -0
  58. package/src/daemon/materialize-workspace.ts +571 -0
  59. package/src/daemon/mint-session-id.ts +5 -5
  60. package/src/daemon/parse-hook-line.ts +31 -0
  61. package/src/daemon/parse-spawn-overrides.ts +94 -0
  62. package/src/daemon/permission-registry.ts +14 -4
  63. package/src/daemon/pick-session-state.ts +15 -0
  64. package/src/daemon/restore-fleet.ts +103 -38
  65. package/src/daemon/screen-model.ts +7 -0
  66. package/src/daemon/session-runtime.ts +10 -0
  67. package/src/daemon/sessions.ts +835 -105
  68. package/src/daemon/start-headless-turn.ts +12 -3
  69. package/src/daemon/start-session-bridge.ts +294 -0
  70. package/src/daemon/target-access.ts +36 -0
  71. package/src/mcp/answer-mcp-request.ts +12 -4
  72. package/src/mcp/answer-rpc-request.ts +28 -1
  73. package/src/mcp/build-principal-caller.ts +15 -0
  74. package/src/mcp/build-spawn-descriptions.ts +45 -0
  75. package/src/mcp/build-tool-list.ts +32 -4
  76. package/src/mcp/mcp-tools.ts +107 -10
  77. package/src/mcp/parse-idempotency-key.ts +29 -0
  78. package/src/mcp/reconnecting-caller.ts +39 -7
  79. package/src/mcp/require-daemon-features.ts +10 -0
  80. package/src/mcp/run-tool.ts +87 -16
  81. package/src/mcp/types.ts +2 -0
  82. package/src/protocol/daemon-error.ts +5 -1
  83. package/src/protocol/daemon-features.ts +35 -0
  84. package/src/protocol/protocol.ts +67 -11
  85. package/src/protocol/request-param-schemas.ts +126 -10
  86. package/src/report.ts +37 -2
  87. package/src/run-bridge-tap.ts +241 -0
  88. package/src/shared/agent-session-id.ts +1 -1
  89. package/src/shared/collect-clean-env.ts +9 -1
  90. package/src/shared/collect-principals.ts +51 -0
  91. package/src/shared/collect-targets.ts +144 -0
  92. package/src/shared/config.ts +139 -14
  93. package/src/shared/daemon-id.ts +8 -0
  94. package/src/shared/format-json-kind.ts +21 -0
  95. package/src/shared/open-bridge-socket.ts +86 -0
  96. package/src/shared/send-bridge-request.ts +47 -0
  97. package/src/shared/sort-json-keys.ts +19 -0
  98. package/src/shared/to-daemon-id.ts +11 -0
  99. package/src/statusline.ts +34 -3
  100. package/src/store/fleet-entry.ts +61 -9
  101. package/src/store/idempotency-record.ts +48 -0
  102. package/src/store/message-owner.ts +1 -1
  103. package/src/store/run-migrations.ts +330 -5
  104. package/src/store/state-store.ts +607 -44
  105. package/src/store/trail-entry.ts +4 -0
  106. package/src/store/workspace-materialization.ts +70 -0
  107. package/src/tap.ts +11 -0
  108. package/src/workspace/check-url-credentials.ts +59 -0
  109. package/src/workspace/check-workspace-completeness.ts +90 -0
  110. package/src/workspace/create-workspace-clone.ts +251 -0
  111. package/src/workspace/normalize-git-url.ts +59 -0
  112. package/src/workspace/read-workspace-tar.ts +38 -0
  113. package/src/workspace/repository-env-vars.ts +22 -0
  114. package/src/workspace/resolve-path-source.ts +170 -0
  115. package/src/workspace/run-git.ts +87 -0
  116. package/src/workspace/sanitize-workspace-clone.ts +146 -0
  117. package/src/workspace/workspace-provenance.ts +11 -0
  118. package/src/workspace/workspace-source.ts +8 -0
  119. /package/src/{daemon → agents}/resolve-headless-executable.ts +0 -0
@@ -1,12 +1,12 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import type { SessionID } from '../shared/session-id';
2
3
 
3
- let counter = 0;
4
-
5
4
  /**
6
- * A fresh atc session id: an incrementing counter joined to the current time
7
- * in base36. Every session the session manager spawns or restores gets one.
5
+ * A fresh atc session id: a random uuid. Every session the session manager
6
+ * spawns gets one, and the fleet row keeps it, so the id stays the same
7
+ * across daemon restarts and fleet restores.
8
8
  */
9
9
  export function mintSessionID(): SessionID {
10
10
  // oxlint-disable-next-line no-unsafe-type-assertion -- this is where atc mints a session id
11
- return `s${++counter}-${Date.now().toString(36)}` as SessionID;
11
+ return randomUUID() as SessionID;
12
12
  }
@@ -0,0 +1,31 @@
1
+ import { isRecord } from '../shared/report';
2
+ import { toSessionID } from '../shared/to-session-id';
3
+ import type { HookEvent } from './hooks';
4
+
5
+ /**
6
+ * One reporter line as a hook event, or null for a line that is not one.
7
+ */
8
+ export function parseHookLine(line: string): HookEvent | null {
9
+ let parsed: unknown;
10
+
11
+ try {
12
+ parsed = JSON.parse(line);
13
+ } catch {
14
+ return null;
15
+ }
16
+
17
+ if (
18
+ !isRecord(parsed) ||
19
+ typeof parsed['atcId'] !== 'string' ||
20
+ typeof parsed['event'] !== 'string' ||
21
+ !isRecord(parsed['payload'])
22
+ ) {
23
+ return null;
24
+ }
25
+
26
+ return {
27
+ atcId: toSessionID(parsed['atcId']),
28
+ event: parsed['event'],
29
+ payload: parsed['payload'],
30
+ };
31
+ }
@@ -0,0 +1,94 @@
1
+ import type { SpawnOverrides } from '../agents/agent-adapter';
2
+ import type { AgentEntry, SpawnOptionEntry } from './build-agent-list';
3
+
4
+ interface RequestedOverrides {
5
+ readonly model?: string | undefined;
6
+ readonly effort?: string | undefined;
7
+ }
8
+
9
+ interface SpawnRefusal {
10
+ readonly ok: false;
11
+ readonly code: 'bad_args' | 'unsupported';
12
+ readonly message: string;
13
+ }
14
+
15
+ type ParsedOverrides = { readonly ok: true; readonly overrides: SpawnOverrides } | SpawnRefusal;
16
+
17
+ /**
18
+ * Checks a spawn's model and effort against the options the agent list
19
+ * shows for that agent, so a spawn accepts exactly what `agents.list`
20
+ * advertises. An option the agent cannot take on this host is unsupported,
21
+ * never ignored. A value outside an option's closed set, or one that could
22
+ * pass for a flag, is bad_args.
23
+ */
24
+ export function parseSpawnOverrides(
25
+ agent: AgentEntry,
26
+ requested: RequestedOverrides,
27
+ ): ParsedOverrides {
28
+ const model = requested.model;
29
+ const effort = requested.effort;
30
+
31
+ const refusal =
32
+ (model === undefined
33
+ ? null
34
+ : findOptionRefusal(agent.id, 'model', agent.spawnOptions.model, model)) ??
35
+ (effort === undefined
36
+ ? null
37
+ : findOptionRefusal(agent.id, 'effort', agent.spawnOptions.effort, effort));
38
+
39
+ if (refusal !== null) {
40
+ return refusal;
41
+ }
42
+
43
+ return {
44
+ ok: true,
45
+ overrides: {
46
+ ...(model === undefined ? {} : { model }),
47
+ ...(effort === undefined ? {} : { effort }),
48
+ },
49
+ };
50
+ }
51
+
52
+ const MAX_VALUE_LENGTH = 200;
53
+
54
+ // oxlint-disable-next-line no-control-regex -- control characters are what a value is refused for
55
+ const CONTROL_CHARACTER = /[\u0000-\u001F\u007F]/u;
56
+
57
+ function findOptionRefusal(
58
+ agentID: string,
59
+ name: 'model' | 'effort',
60
+ option: SpawnOptionEntry,
61
+ value: string,
62
+ ): SpawnRefusal | null {
63
+ if (!option.available) {
64
+ const reason = option.supported ? 'is not installed on this host' : `takes no ${name}`;
65
+
66
+ return { ok: false, code: 'unsupported', message: `agent '${agentID}' ${reason}` };
67
+ }
68
+
69
+ if (value === '' || value.length > MAX_VALUE_LENGTH) {
70
+ return {
71
+ ok: false,
72
+ code: 'bad_args',
73
+ message: `${name} must be 1 to ${MAX_VALUE_LENGTH} characters`,
74
+ };
75
+ }
76
+
77
+ if (value.startsWith('-') || CONTROL_CHARACTER.test(value)) {
78
+ return {
79
+ ok: false,
80
+ code: 'bad_args',
81
+ message: `${name} must not start with '-' or hold control characters`,
82
+ };
83
+ }
84
+
85
+ if (option.values !== null && !option.values.includes(value)) {
86
+ return {
87
+ ok: false,
88
+ code: 'bad_args',
89
+ message: `agent '${agentID}' takes ${name} ${option.values.join(', ')}; got '${value}'`,
90
+ };
91
+ }
92
+
93
+ return null;
94
+ }
@@ -30,7 +30,9 @@ export class PermissionRegistry {
30
30
 
31
31
  private readonly pending = new Map<string, PendingRequest>();
32
32
 
33
- private readonly resolved = new Set<string>();
33
+ // Each answered request's session, kept to tell already_answered from
34
+ // unknown.
35
+ private readonly resolved = new Map<string, SessionID>();
34
36
 
35
37
  private counter = 0;
36
38
 
@@ -72,6 +74,14 @@ export class PermissionRegistry {
72
74
  return 'ok';
73
75
  }
74
76
 
77
+ /**
78
+ * The session a request belongs to, answered or not, or null for a
79
+ * request this registry never opened.
80
+ */
81
+ findSessionID(id: string): SessionID | null {
82
+ return this.pending.get(id)?.req.sessionID ?? this.resolved.get(id) ?? null;
83
+ }
84
+
75
85
  answerAll(sessionID: SessionID, decision: string): void {
76
86
  for (const [id, entry] of this.pending) {
77
87
  if (entry.req.sessionID === sessionID) {
@@ -90,12 +100,12 @@ export class PermissionRegistry {
90
100
  clearTimeout(entry.timer);
91
101
 
92
102
  this.pending.delete(id);
93
- this.resolved.add(id);
103
+ this.resolved.set(id, entry.req.sessionID);
94
104
 
95
- // The resolved set only exists to tell already_answered from unknown;
105
+ // The resolved map only exists to tell already_answered from unknown;
96
106
  // cap it so a long-lived daemon cannot grow it without bound.
97
107
  if (this.resolved.size > 1000) {
98
- for (const oldest of this.resolved) {
108
+ for (const oldest of this.resolved.keys()) {
99
109
  this.resolved.delete(oldest);
100
110
  break;
101
111
  }
@@ -0,0 +1,15 @@
1
+ import type { SessionLifecycle } from './build-session-lifecycle';
2
+
3
+ type SessionState = 'running' | 'needs_you' | 'done' | 'exited';
4
+
5
+ /**
6
+ * The state a session lists with: `exited` whenever its harness is not
7
+ * running, suspended inside a sleeping host included, and otherwise the
8
+ * attention its hooks last reported.
9
+ */
10
+ export function pickSessionState(
11
+ lifecycle: Readonly<SessionLifecycle>,
12
+ attention: SessionState,
13
+ ): SessionState {
14
+ return lifecycle.harness === 'running' ? attention : 'exited';
15
+ }
@@ -1,7 +1,7 @@
1
- import type { AgentSessionID } from '../shared/agent-session-id';
1
+ import { DaemonError } from '../protocol/daemon-error';
2
2
  import type { SessionID } from '../shared/session-id';
3
+ import type { FleetEntry } from '../store/fleet-entry';
3
4
  import type { StateStore } from '../store/state-store';
4
- import { ScreenModel } from './screen-model';
5
5
  import type { SessionRuntime } from './session-runtime';
6
6
  import type { Session, SessionManager } from './sessions';
7
7
 
@@ -33,15 +33,17 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
33
33
  const rows = params.rows;
34
34
  const capMs = params.capMs;
35
35
 
36
- const hasLiveSession = (agentSessionID: AgentSessionID) =>
36
+ const hasLiveSession = (entry: FleetEntry) =>
37
37
  mgr.sessions.some(
38
38
  (s) =>
39
- s.agentSessionID === agentSessionID &&
39
+ isSameSession(s, entry) &&
40
40
  (s.pty !== null || (s.kind === 'headless' && s.state !== 'exited')),
41
41
  );
42
42
 
43
- const hasAnySession = (agentSessionID: AgentSessionID) =>
44
- mgr.sessions.some((s) => s.agentSessionID === agentSessionID);
43
+ const hasAnySession = (entry: FleetEntry) => mgr.sessions.some((s) => isSameSession(s, entry));
44
+
45
+ const findRecency = (entry: FleetEntry) =>
46
+ entry.agentSessionID === undefined ? '' : (recency.get(entry.agentSessionID) ?? '');
45
47
 
46
48
  const recency = await store.collectFleetRecency();
47
49
  const stored = await store.loadFleet();
@@ -52,14 +54,8 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
52
54
  // dedupe against every listed session, so repeated restores never double
53
55
  // up the killed archive.
54
56
  const kept = stored
55
- .filter((entry) =>
56
- entry.exited === true
57
- ? !hasAnySession(entry.agentSessionID)
58
- : !hasLiveSession(entry.agentSessionID),
59
- )
60
- .toSorted((a, b) =>
61
- (recency.get(b.agentSessionID) ?? '').localeCompare(recency.get(a.agentSessionID) ?? ''),
62
- );
57
+ .filter((entry) => (entry.exited === true ? !hasAnySession(entry) : !hasLiveSession(entry)))
58
+ .toSorted((a, b) => findRecency(b).localeCompare(findRecency(a)));
63
59
 
64
60
  // Sub-sessions register after every top-level entry, so each one links
65
61
  // to a parent that is already listed; the wrangling session also boots
@@ -72,30 +68,41 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
72
68
  // The whole fleet registers as terminal-less sessions up front, so the
73
69
  // list shows every incoming session immediately instead of revealing them
74
70
  // one boot at a time. Exited entries only register — they stay killed
75
- // until revived by hand, so no terminal is adopted for them.
76
- const registered = entries.map((entry) => mgr.restore(entry));
77
- const queued = registered.filter((s) => s.state !== 'exited');
78
-
79
- const adoptQueued = (s: Session): boolean => {
80
- if (mgr.adoptTerminal(s.id, cols, rows) === null) {
81
- return false;
82
- }
83
-
84
- const runtime = findRuntime(s.id);
85
-
86
- if (runtime !== undefined) {
87
- runtime.dims = { cols, rows };
88
-
89
- runtime.screen ??= new ScreenModel(cols, rows);
90
- }
91
-
92
- return true;
71
+ // until revived by hand, so no terminal is adopted for them. An entry
72
+ // whose session is still listed, dead, revives that session in place
73
+ // rather than listing a second session under the same id.
74
+ const registered = entries.map((entry) => {
75
+ const listed = mgr.sessions.find((s) => s.id === entry.sessionID);
76
+
77
+ return listed === undefined
78
+ ? { session: mgr.restore(entry), revive: false }
79
+ : { session: listed, revive: true };
80
+ });
81
+
82
+ const queued = registered
83
+ .filter((r) => r.revive || r.session.state !== 'exited')
84
+ .map((r) => r.session);
85
+
86
+ // A session whose target refuses to start a terminal stays listed without
87
+ // one, and the restore moves on to the next.
88
+ const adoptQueued = async (s: Session): Promise<boolean> => {
89
+ const adopted = await tryAdoptTerminal(mgr, s.id, cols, rows);
90
+
91
+ return adopted !== null;
93
92
  };
94
93
 
94
+ // A session that announced itself, or whose terminal died, before the
95
+ // wait began has nothing left to wait for.
95
96
  const waitForBoot = (sessionID: SessionID): Promise<void> => {
96
97
  const runtime = findRuntime(sessionID);
97
-
98
- if (runtime === undefined) {
98
+ const session = mgr.sessions.find((s) => s.id === sessionID);
99
+
100
+ if (
101
+ runtime === undefined ||
102
+ runtime.startedAt !== null ||
103
+ session === undefined ||
104
+ session.pty === null
105
+ ) {
99
106
  return Promise.resolve();
100
107
  }
101
108
 
@@ -124,11 +131,26 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
124
131
  return registered.length;
125
132
  }
126
133
 
127
- // The first terminal attaches synchronously so a caller can attach at
128
- // once; each later one waits for the previous session to report it has
134
+ // The first terminal attaches before the restore answers so a caller can
135
+ // attach at once; each later one waits for the previous session to report it has
129
136
  // booted, so a heavy fleet comes up one process at a time instead of all
130
137
  // at once. A per-session cap keeps a session that never reports from
131
138
  // stalling the rest.
139
+ // A session whose revive fails is logged and left without a terminal,
140
+ // and the restore moves on: one host's failure never keeps the sessions on
141
+ // other hosts down, and a later one's runs where nothing awaits it.
142
+ const tryAdoptQueued = async (s: Session): Promise<boolean> => {
143
+ try {
144
+ return await adoptQueued(s);
145
+ } catch (error) {
146
+ mgr.log(
147
+ `atc could not revive session ${s.id} (${error instanceof Error ? error.message : String(error)})`,
148
+ );
149
+
150
+ return false;
151
+ }
152
+ };
153
+
132
154
  const adoptRest = async (previous: Session | null) => {
133
155
  let prev = previous;
134
156
 
@@ -137,13 +159,56 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
137
159
  await waitForBoot(prev.id);
138
160
  }
139
161
 
140
- prev = adoptQueued(s) ? s : null;
162
+ const booted = await tryAdoptQueued(s);
163
+
164
+ prev = booted ? s : null;
141
165
  }
142
166
  };
143
167
 
144
- const firstBooted = adoptQueued(first) ? first : null;
168
+ const firstAdopted = await tryAdoptQueued(first);
169
+
170
+ const firstBooted = firstAdopted ? first : null;
145
171
 
146
172
  void adoptRest(firstBooted);
147
173
 
148
174
  return registered.length;
149
175
  }
176
+
177
+ // A stored entry matches a listed session by its atc session id, or by its
178
+ // agent session id when a listed session resumed the same agent session.
179
+ function isSameSession(s: Session, entry: FleetEntry): boolean {
180
+ return (
181
+ s.id === entry.sessionID ||
182
+ (entry.agentSessionID !== undefined && s.agentSessionID === entry.agentSessionID)
183
+ );
184
+ }
185
+
186
+ // The refusals that leave one session without a terminal: its target is
187
+ // misconfigured, gone from the config, changed, has no provider here, or
188
+ // cannot start a terminal.
189
+ const REFUSED_ADOPT_CODES: ReadonlySet<string> = new Set([
190
+ 'unsupported_operation',
191
+ 'unknown_target',
192
+ 'target_unavailable',
193
+ 'target_changed',
194
+ 'target_config_invalid',
195
+ 'host_unavailable',
196
+ 'auth_not_configured',
197
+ ]);
198
+
199
+ async function tryAdoptTerminal(
200
+ mgr: SessionManager,
201
+ id: SessionID,
202
+ cols: number,
203
+ rows: number,
204
+ ): Promise<Session | null> {
205
+ try {
206
+ return await mgr.adoptTerminal(id, cols, rows);
207
+ } catch (error) {
208
+ if (error instanceof DaemonError && REFUSED_ADOPT_CODES.has(error.code)) {
209
+ return null;
210
+ }
211
+
212
+ throw error;
213
+ }
214
+ }
@@ -99,6 +99,13 @@ export class ScreenModel {
99
99
  return RESET_INPUT_MODES + this.renderVisibleScreen() + this.renderInputModes();
100
100
  }
101
101
 
102
+ // Whether the TUI has turned bracketed paste on, as of the output parsed
103
+ // so far. It reads the mode without waiting, so output still queued for
104
+ // parsing never delays the caller; a TUI turns the mode on once, at start.
105
+ hasBracketedPaste(): boolean {
106
+ return this.term.modes.bracketedPasteMode;
107
+ }
108
+
102
109
  // The visible rows of whichever buffer the session is showing, as plain
103
110
  // text with no escape sequences: one line per row, trailing blanks
104
111
  // trimmed from each row, trailing blank rows dropped. Drains pending
@@ -46,6 +46,16 @@ export class SessionRuntime {
46
46
  // later dropped may be restarting, so it keeps the inbox open.
47
47
  tapAttached = false;
48
48
 
49
+ // Returns the boot-scoped state to how a fresh terminal starts, at the
50
+ // dims it boots with: no SessionStart yet and no tap since. Every path
51
+ // that boots a new terminal for an existing session runs it, so a revived
52
+ // session never inherits its previous process's message gate.
53
+ resetBoot(dims: Dims): void {
54
+ this.dims = dims;
55
+ this.startedAt = null;
56
+ this.tapAttached = false;
57
+ }
58
+
49
59
  dispose(): void {
50
60
  clearTimeout(this.resizeTimer);
51
61
  clearTimeout(this.detectTimer);