@zgeoff/atc 2.10.4 → 2.12.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 (86) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/agents/agent-adapter.ts +72 -0
  4. package/src/agents/atc-bridge-files.ts +1 -1
  5. package/src/agents/build-args-without-flags.ts +25 -0
  6. package/src/agents/build-claude-override-args.ts +26 -0
  7. package/src/agents/claude-adapter.ts +52 -1
  8. package/src/agents/claude-effort-levels.ts +5 -0
  9. package/src/agents/codex-adapter.ts +46 -1
  10. package/src/agents/find-flag-value.ts +27 -0
  11. package/src/agents/gateway-adapter.ts +75 -1
  12. package/src/agents/grok-adapter.ts +22 -0
  13. package/src/cli.ts +22 -0
  14. package/src/client/boot-daemon.ts +6 -0
  15. package/src/client/daemon-client.ts +14 -2
  16. package/src/client/spawn-picker.ts +27 -4
  17. package/src/daemon/build-agent-list.ts +94 -0
  18. package/src/daemon/build-config-revision.ts +27 -0
  19. package/src/daemon/build-execution-targets.ts +34 -0
  20. package/src/daemon/build-fleet-events.ts +2 -1
  21. package/src/daemon/build-payload-hash.ts +31 -0
  22. package/src/daemon/build-scoped-context.ts +232 -0
  23. package/src/daemon/build-target-access.ts +33 -0
  24. package/src/daemon/build-target-forbidden-error.ts +13 -0
  25. package/src/daemon/build-target-identity.ts +22 -0
  26. package/src/daemon/build-target-list.ts +50 -0
  27. package/src/daemon/daemon-connection.ts +371 -99
  28. package/src/daemon/daemon.ts +537 -90
  29. package/src/daemon/effect-remains-error.ts +13 -0
  30. package/src/daemon/execution-provider.ts +103 -0
  31. package/src/daemon/find-execution-refusal.ts +104 -0
  32. package/src/daemon/idempotency-ledger.ts +164 -0
  33. package/src/daemon/local-pty-provider.ts +83 -0
  34. package/src/daemon/mint-session-id.ts +5 -5
  35. package/src/daemon/parse-report.ts +29 -3
  36. package/src/daemon/parse-spawn-overrides.ts +94 -0
  37. package/src/daemon/permission-registry.ts +14 -4
  38. package/src/daemon/restore-fleet.ts +66 -19
  39. package/src/daemon/session-runtime.ts +10 -0
  40. package/src/daemon/sessions.ts +287 -65
  41. package/src/daemon/start-headless-run.ts +11 -0
  42. package/src/daemon/start-headless-turn.ts +11 -2
  43. package/src/daemon/target-access.ts +36 -0
  44. package/src/mcp/answer-mcp-request.ts +12 -4
  45. package/src/mcp/answer-rpc-request.ts +40 -6
  46. package/src/mcp/build-principal-caller.ts +15 -0
  47. package/src/mcp/build-spawn-descriptions.ts +45 -0
  48. package/src/mcp/build-tool-list.ts +88 -7
  49. package/src/mcp/mcp-tools.ts +245 -24
  50. package/src/mcp/parse-idempotency-key.ts +29 -0
  51. package/src/mcp/reconnecting-caller.ts +77 -15
  52. package/src/mcp/require-daemon-features.ts +40 -0
  53. package/src/mcp/run-tool.ts +139 -35
  54. package/src/mcp/start-mcp-http-server.ts +130 -24
  55. package/src/mcp/types.ts +9 -1
  56. package/src/mcp-http-server.ts +6 -1
  57. package/src/mcp-server.ts +13 -1
  58. package/src/protocol/daemon-error.ts +5 -1
  59. package/src/protocol/daemon-features.ts +47 -0
  60. package/src/protocol/parse-daemon-features.ts +19 -0
  61. package/src/protocol/protocol.ts +44 -11
  62. package/src/protocol/request-param-schemas.ts +78 -12
  63. package/src/report.ts +20 -3
  64. package/src/shared/agent-session-id.ts +1 -1
  65. package/src/shared/collect-principals.ts +51 -0
  66. package/src/shared/collect-targets.ts +144 -0
  67. package/src/shared/config.ts +139 -14
  68. package/src/shared/daemon-id.ts +8 -0
  69. package/src/shared/format-json-kind.ts +21 -0
  70. package/src/shared/sort-json-keys.ts +19 -0
  71. package/src/shared/to-daemon-id.ts +11 -0
  72. package/src/store/fleet-entry.ts +42 -9
  73. package/src/store/idempotency-record.ts +48 -0
  74. package/src/store/message-owner.ts +1 -1
  75. package/src/store/message-record.ts +3 -0
  76. package/src/store/run-migrations.ts +272 -7
  77. package/src/store/state-store.ts +503 -34
  78. package/src/workspace/check-workspace-completeness.ts +90 -0
  79. package/src/workspace/create-workspace-clone.ts +239 -0
  80. package/src/workspace/normalize-git-url.ts +59 -0
  81. package/src/workspace/read-workspace-tar.ts +38 -0
  82. package/src/workspace/resolve-path-source.ts +170 -0
  83. package/src/workspace/run-git.ts +103 -0
  84. package/src/workspace/sanitize-workspace-clone.ts +146 -0
  85. package/src/workspace/workspace-provenance.ts +11 -0
  86. package/src/workspace/workspace-source.ts +8 -0
@@ -1,5 +1,6 @@
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
5
  import { ScreenModel } from './screen-model';
5
6
  import type { SessionRuntime } from './session-runtime';
@@ -33,15 +34,17 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
33
34
  const rows = params.rows;
34
35
  const capMs = params.capMs;
35
36
 
36
- const hasLiveSession = (agentSessionID: AgentSessionID) =>
37
+ const hasLiveSession = (entry: FleetEntry) =>
37
38
  mgr.sessions.some(
38
39
  (s) =>
39
- s.agentSessionID === agentSessionID &&
40
+ isSameSession(s, entry) &&
40
41
  (s.pty !== null || (s.kind === 'headless' && s.state !== 'exited')),
41
42
  );
42
43
 
43
- const hasAnySession = (agentSessionID: AgentSessionID) =>
44
- mgr.sessions.some((s) => s.agentSessionID === agentSessionID);
44
+ const hasAnySession = (entry: FleetEntry) => mgr.sessions.some((s) => isSameSession(s, entry));
45
+
46
+ const findRecency = (entry: FleetEntry) =>
47
+ entry.agentSessionID === undefined ? '' : (recency.get(entry.agentSessionID) ?? '');
45
48
 
46
49
  const recency = await store.collectFleetRecency();
47
50
  const stored = await store.loadFleet();
@@ -52,14 +55,8 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
52
55
  // dedupe against every listed session, so repeated restores never double
53
56
  // up the killed archive.
54
57
  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
- );
58
+ .filter((entry) => (entry.exited === true ? !hasAnySession(entry) : !hasLiveSession(entry)))
59
+ .toSorted((a, b) => findRecency(b).localeCompare(findRecency(a)));
63
60
 
64
61
  // Sub-sessions register after every top-level entry, so each one links
65
62
  // to a parent that is already listed; the wrangling session also boots
@@ -72,19 +69,32 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
72
69
  // The whole fleet registers as terminal-less sessions up front, so the
73
70
  // list shows every incoming session immediately instead of revealing them
74
71
  // 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
-
72
+ // until revived by hand, so no terminal is adopted for them. An entry
73
+ // whose session is still listed, dead, revives that session in place
74
+ // rather than listing a second session under the same id.
75
+ const registered = entries.map((entry) => {
76
+ const listed = mgr.sessions.find((s) => s.id === entry.sessionID);
77
+
78
+ return listed === undefined
79
+ ? { session: mgr.restore(entry), revive: false }
80
+ : { session: listed, revive: true };
81
+ });
82
+
83
+ const queued = registered
84
+ .filter((r) => r.revive || r.session.state !== 'exited')
85
+ .map((r) => r.session);
86
+
87
+ // A session whose target refuses to start a terminal stays listed without
88
+ // one, and the restore moves on to the next.
79
89
  const adoptQueued = (s: Session): boolean => {
80
- if (mgr.adoptTerminal(s.id, cols, rows) === null) {
90
+ if (tryAdoptTerminal(mgr, s.id, cols, rows) === null) {
81
91
  return false;
82
92
  }
83
93
 
84
94
  const runtime = findRuntime(s.id);
85
95
 
86
96
  if (runtime !== undefined) {
87
- runtime.dims = { cols, rows };
97
+ runtime.resetBoot({ cols, rows });
88
98
 
89
99
  runtime.screen ??= new ScreenModel(cols, rows);
90
100
  }
@@ -147,3 +157,40 @@ export async function restoreFleet(params: RestoreFleetParams): Promise<number>
147
157
 
148
158
  return registered.length;
149
159
  }
160
+
161
+ // A stored entry matches a listed session by its atc session id, or by its
162
+ // agent session id when a listed session resumed the same agent session.
163
+ function isSameSession(s: Session, entry: FleetEntry): boolean {
164
+ return (
165
+ s.id === entry.sessionID ||
166
+ (entry.agentSessionID !== undefined && s.agentSessionID === entry.agentSessionID)
167
+ );
168
+ }
169
+
170
+ // The refusals that leave one session without a terminal: its target is
171
+ // misconfigured, gone from the config, changed, has no provider here, or
172
+ // cannot start a terminal.
173
+ const REFUSED_ADOPT_CODES: ReadonlySet<string> = new Set([
174
+ 'unsupported_operation',
175
+ 'unknown_target',
176
+ 'target_unavailable',
177
+ 'target_changed',
178
+ 'target_config_invalid',
179
+ ]);
180
+
181
+ function tryAdoptTerminal(
182
+ mgr: SessionManager,
183
+ id: SessionID,
184
+ cols: number,
185
+ rows: number,
186
+ ): Session | null {
187
+ try {
188
+ return mgr.adoptTerminal(id, cols, rows);
189
+ } catch (error) {
190
+ if (error instanceof DaemonError && REFUSED_ADOPT_CODES.has(error.code)) {
191
+ return null;
192
+ }
193
+
194
+ throw error;
195
+ }
196
+ }
@@ -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);