@ai-development-environment/control-agent 0.0.130 → 0.0.132

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 (2) hide show
  1. package/dist/control-agent.js +275 -72
  2. package/package.json +1 -1
@@ -2288,7 +2288,7 @@ var require_websocket = __commonJS({
2288
2288
  var http = __require("http");
2289
2289
  var net = __require("net");
2290
2290
  var tls = __require("tls");
2291
- var { randomBytes, createHash: createHash8 } = __require("crypto");
2291
+ var { randomBytes, createHash: createHash9 } = __require("crypto");
2292
2292
  var { Duplex, Readable: Readable2 } = __require("stream");
2293
2293
  var { URL: URL2 } = __require("url");
2294
2294
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2969,7 +2969,7 @@ var require_websocket = __commonJS({
2969
2969
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2970
2970
  return;
2971
2971
  }
2972
- const digest = createHash8("sha1").update(key + GUID).digest("base64");
2972
+ const digest = createHash9("sha1").update(key + GUID).digest("base64");
2973
2973
  if (res.headers["sec-websocket-accept"] !== digest) {
2974
2974
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2975
2975
  return;
@@ -3338,7 +3338,7 @@ var require_websocket_server = __commonJS({
3338
3338
  var EventEmitter = __require("events");
3339
3339
  var http = __require("http");
3340
3340
  var { Duplex } = __require("stream");
3341
- var { createHash: createHash8 } = __require("crypto");
3341
+ var { createHash: createHash9 } = __require("crypto");
3342
3342
  var extension2 = require_extension();
3343
3343
  var PerMessageDeflate2 = require_permessage_deflate();
3344
3344
  var subprotocol2 = require_subprotocol();
@@ -3645,7 +3645,7 @@ var require_websocket_server = __commonJS({
3645
3645
  );
3646
3646
  }
3647
3647
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3648
- const digest = createHash8("sha1").update(key + GUID).digest("base64");
3648
+ const digest = createHash9("sha1").update(key + GUID).digest("base64");
3649
3649
  const headers = [
3650
3650
  "HTTP/1.1 101 Switching Protocols",
3651
3651
  "Upgrade: websocket",
@@ -6438,6 +6438,42 @@ var import_websocket = __toESM(require_websocket(), 1);
6438
6438
  var import_websocket_server = __toESM(require_websocket_server(), 1);
6439
6439
  var wrapper_default = import_websocket.default;
6440
6440
 
6441
+ // src/request-errors.ts
6442
+ var MAX_AGENT_REQUEST_ATTEMPTS = 5;
6443
+ var AgentRequestError = class extends Error {
6444
+ retryable;
6445
+ constructor(message2, status2, codes = []) {
6446
+ super(message2);
6447
+ this.name = "AgentRequestError";
6448
+ this.retryable = (status2 < 400 || status2 >= 500 || [408, 425, 429].includes(status2)) && !codes.some(
6449
+ (code) => [
6450
+ "UNAUTHENTICATED",
6451
+ "FORBIDDEN",
6452
+ "BAD_USER_INPUT",
6453
+ "GRAPHQL_VALIDATION_FAILED",
6454
+ "NOT_FOUND"
6455
+ ].includes(code)
6456
+ ) && !/^(?:Job|Run|Run attempt|Run command) not found(?: for this agent)?$/.test(
6457
+ message2
6458
+ );
6459
+ }
6460
+ };
6461
+ function isRetryableAgentError(error) {
6462
+ return !(error instanceof AgentRequestError) || error.retryable;
6463
+ }
6464
+ function waitForAgentRetry(delay3, signal2) {
6465
+ return new Promise((resolve11) => {
6466
+ const finish = () => {
6467
+ clearTimeout(timer);
6468
+ signal2.removeEventListener("abort", finish);
6469
+ resolve11();
6470
+ };
6471
+ const timer = setTimeout(finish, delay3);
6472
+ if (signal2.aborted) finish();
6473
+ else signal2.addEventListener("abort", finish, { once: true });
6474
+ });
6475
+ }
6476
+
6441
6477
  // src/graphql-client.ts
6442
6478
  async function writeArtifactTransferBytes(writer, bytes, position) {
6443
6479
  let written = 0;
@@ -6532,8 +6568,9 @@ var AgentGraphQLClient = class {
6532
6568
  }
6533
6569
  if (!response.ok || typeof body?.mode !== "string") {
6534
6570
  const detail = `HTTP ${response.status} ${response.statusText}`.trim();
6535
- throw new Error(
6536
- !body && raw ? `${detail}: ${raw.slice(0, 500)}` : detail
6571
+ throw new AgentRequestError(
6572
+ !body && raw ? `${detail}: ${raw.slice(0, 500)}` : detail,
6573
+ response.status
6537
6574
  );
6538
6575
  }
6539
6576
  return { mode: body.mode };
@@ -6557,8 +6594,12 @@ var AgentGraphQLClient = class {
6557
6594
  }
6558
6595
  if (!response.ok || body?.errors?.length || !body?.data) {
6559
6596
  const detail = body?.errors?.map((error) => error.message).join("; ") || `HTTP ${response.status} ${response.statusText}`.trim();
6560
- throw new Error(
6561
- !body && raw ? `${detail}: ${raw.slice(0, 500)}` : detail
6597
+ throw new AgentRequestError(
6598
+ !body && raw ? `${detail}: ${raw.slice(0, 500)}` : detail,
6599
+ response.status,
6600
+ body?.errors?.flatMap(
6601
+ ({ extensions }) => extensions?.code ? [extensions.code] : []
6602
+ )
6562
6603
  );
6563
6604
  }
6564
6605
  return body.data;
@@ -11105,7 +11146,7 @@ function tailscaleServeRemovePayload(value) {
11105
11146
  // package.json
11106
11147
  var package_default = {
11107
11148
  name: "@ai-development-environment/control-agent",
11108
- version: "0.0.130",
11149
+ version: "0.0.132",
11109
11150
  description: "Control agent for the AI Development Environment",
11110
11151
  license: "MIT",
11111
11152
  repository: {
@@ -45688,19 +45729,50 @@ function physicalDestinations(value, includeUnavailable = false) {
45688
45729
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
45689
45730
  const id2 = nestedString(raw, [
45690
45731
  ["identifier"],
45732
+ ["properties", "hardware", "udid"],
45691
45733
  ["hardwareProperties", "udid"],
45692
45734
  ["deviceProperties", "identifier"]
45693
45735
  ]);
45694
45736
  const platform2 = nestedString(raw, [
45737
+ ["properties", "hardware", "platform"],
45695
45738
  ["hardwareProperties", "platform"],
45696
45739
  ["deviceProperties", "osType"]
45697
45740
  ]);
45698
- if (!id2 || platform2 && !platform2.toLowerCase().includes("ios")) return [];
45699
- const state = nestedString(raw, [
45741
+ if (!id2 || platform2 && !["ios", "ipados"].includes(platform2.trim().toLowerCase()))
45742
+ return [];
45743
+ const reality = nestedString(raw, [
45744
+ ["properties", "hardware", "reality"],
45745
+ ["hardwareProperties", "reality"]
45746
+ ])?.trim().toLowerCase();
45747
+ const transport = nestedString(raw, [
45748
+ ["properties", "connection", "transportType"],
45749
+ ["connectionProperties", "transportType"]
45750
+ ])?.trim().toLowerCase();
45751
+ const visibility = nestedString(raw, [
45752
+ ["visibilityClass"],
45753
+ ["properties", "state", "visibilityClass"]
45754
+ ])?.trim().toLowerCase();
45755
+ if (reality === "simulated" || !reality && (visibility === "simulators" || transport === "samemachine"))
45756
+ return [];
45757
+ const connectionState = nestedString(raw, [
45758
+ ["properties", "connection", "state"],
45700
45759
  ["connectionProperties", "tunnelState"],
45701
45760
  ["deviceProperties", "connectionState"]
45702
- ]);
45703
- const available = !(state && /(disconnected|unavailable|not.?connected|offline)/i.test(state));
45761
+ ])?.trim().toLowerCase();
45762
+ const paired = nestedString(raw, [
45763
+ ["properties", "connection", "pairingState"],
45764
+ ["connectionProperties", "pairingState"]
45765
+ ])?.trim().toLowerCase() === "paired";
45766
+ const connected = connectionState === "connected";
45767
+ const available = connected || paired && (transport === "wired" || transport === "localnetwork");
45768
+ const state = connected ? "Connected" : available ? "Available (paired)" : connectionState && [
45769
+ "disconnected",
45770
+ "unavailable",
45771
+ "offline",
45772
+ "notconnected",
45773
+ "not_connected",
45774
+ "not connected"
45775
+ ].includes(connectionState) ? "Unavailable" : "Unknown";
45704
45776
  if (!available && !includeUnavailable) {
45705
45777
  return [];
45706
45778
  }
@@ -45708,9 +45780,15 @@ function physicalDestinations(value, includeUnavailable = false) {
45708
45780
  {
45709
45781
  type: "PHYSICAL_DEVICE",
45710
45782
  id: id2,
45711
- name: nestedString(raw, [["deviceProperties", "name"], ["name"]]) ?? id2,
45783
+ name: nestedString(raw, [
45784
+ ["properties", "state", "name"],
45785
+ ["deviceProperties", "name"],
45786
+ ["name"]
45787
+ ]) ?? id2,
45712
45788
  platform: "iOS",
45713
45789
  osVersion: nestedString(raw, [
45790
+ ["properties", "software", "osVersionNumber", "stringValue"],
45791
+ ["properties", "software", "osVersionNumber"],
45714
45792
  ["deviceProperties", "osVersionNumber"],
45715
45793
  ["deviceProperties", "osVersion"]
45716
45794
  ]),
@@ -45760,7 +45838,7 @@ var inspectBuildDestinations = async (payload, timeoutMs, signal2) => {
45760
45838
  signal2,
45761
45839
  folder
45762
45840
  ),
45763
- listPhysicalDevices(timeoutMs, signal2)
45841
+ listPhysicalDevices(timeoutMs, signal2, true)
45764
45842
  ]);
45765
45843
  return {
45766
45844
  ...successfulProcess3,
@@ -53986,6 +54064,7 @@ var JobExecutor = class {
53986
54064
  repositoryCoordinator;
53987
54065
  running = /* @__PURE__ */ new Map();
53988
54066
  stopping = false;
54067
+ shutdown = new AbortController();
53989
54068
  execute(job) {
53990
54069
  if (this.stopping || this.running.has(job.id)) return;
53991
54070
  const controller = new AbortController();
@@ -54001,6 +54080,7 @@ var JobExecutor = class {
54001
54080
  }
54002
54081
  async cancelAll() {
54003
54082
  this.stopping = true;
54083
+ this.shutdown.abort();
54004
54084
  closeAllWorktreeWatches();
54005
54085
  const jobs = [...this.running.values()];
54006
54086
  for (const { controller } of jobs) controller.abort();
@@ -54059,8 +54139,7 @@ var JobExecutor = class {
54059
54139
  status2 = controller.signal.aborted ? "CANCELLED" : "FAILED";
54060
54140
  console.error(`Job ${claimed.id} failed:`, completionError);
54061
54141
  }
54062
- let retry = 0;
54063
- while (true) {
54142
+ for (let attempt = 1; ; attempt++) {
54064
54143
  try {
54065
54144
  await this.client.completeJob(
54066
54145
  claimed.id,
@@ -54070,13 +54149,21 @@ var JobExecutor = class {
54070
54149
  );
54071
54150
  return;
54072
54151
  } catch (error) {
54152
+ if (this.stopping) return;
54153
+ if (!isRetryableAgentError(error)) {
54154
+ console.error(
54155
+ `Could not report completion for ${claimed.id}; giving up after ${attempt} attempts:`,
54156
+ error instanceof Error ? error.message : error
54157
+ );
54158
+ return;
54159
+ }
54073
54160
  console.error(
54074
54161
  `Could not report completion for ${claimed.id}; retrying:`,
54075
54162
  error instanceof Error ? error.message : error
54076
54163
  );
54164
+ const delay3 = Math.min(3e4, 1e3 * 2 ** Math.min(attempt - 1, 5));
54165
+ await waitForAgentRetry(delay3, this.shutdown.signal);
54077
54166
  if (this.stopping) return;
54078
- const delay3 = Math.min(3e4, 1e3 * 2 ** Math.min(retry++, 5));
54079
- await new Promise((resolve11) => setTimeout(resolve11, delay3));
54080
54167
  }
54081
54168
  }
54082
54169
  }
@@ -54232,18 +54319,22 @@ var CodebaseMonitor = class {
54232
54319
 
54233
54320
  // src/runs/run-manager.ts
54234
54321
  import { execFile as execFile5 } from "node:child_process";
54322
+ import { createHash as createHash8 } from "node:crypto";
54235
54323
  import { mkdir as mkdir10, rm as rm20 } from "node:fs/promises";
54236
54324
  import { dirname as dirname13, join as join23 } from "node:path";
54237
54325
  import { promisify as promisify5 } from "node:util";
54238
54326
 
54239
54327
  // src/runs/journal.ts
54240
54328
  import { randomUUID as randomUUID7 } from "node:crypto";
54329
+ import { createReadStream as createReadStream6 } from "node:fs";
54241
54330
  import {
54242
54331
  appendFile,
54243
54332
  mkdir as mkdir9,
54244
54333
  readFile as readFile12,
54245
54334
  rename as rename7,
54246
54335
  rm as rm19,
54336
+ stat as stat12,
54337
+ truncate,
54247
54338
  writeFile as writeFile11
54248
54339
  } from "node:fs/promises";
54249
54340
  import { dirname as dirname12, join as join22 } from "node:path";
@@ -54256,6 +54347,7 @@ var RunJournal = class {
54256
54347
  attemptId;
54257
54348
  nextSequence = 0;
54258
54349
  acknowledged = -1;
54350
+ acknowledgedOffset = 0;
54259
54351
  loaded = false;
54260
54352
  loading;
54261
54353
  appending = Promise.resolve();
@@ -54288,14 +54380,33 @@ var RunJournal = class {
54288
54380
  const state = JSON.parse(await readFile12(this.statePath, "utf8"));
54289
54381
  this.acknowledged = state.acknowledged ?? -1;
54290
54382
  this.nextSequence = state.nextSequence ?? 0;
54291
- } catch {
54292
- try {
54293
- const events = await this.readEvents();
54294
- this.nextSequence = (events.at(-1)?.sequence ?? -1) + 1;
54295
- } catch {
54296
- this.nextSequence = 0;
54383
+ this.acknowledgedOffset = state.acknowledgedOffset ?? 0;
54384
+ } catch (error) {
54385
+ if (!(error instanceof SyntaxError) && error.code !== "ENOENT")
54386
+ throw error;
54387
+ }
54388
+ const size = await stat12(this.eventsPath).then(
54389
+ ({ size: size2 }) => size2,
54390
+ (error) => {
54391
+ if (error.code === "ENOENT") return 0;
54392
+ throw error;
54297
54393
  }
54394
+ );
54395
+ if (!Number.isSafeInteger(this.acknowledgedOffset) || this.acknowledgedOffset < 0 || this.acknowledgedOffset > size)
54396
+ this.acknowledgedOffset = 0;
54397
+ this.nextSequence = Math.max(this.nextSequence, this.acknowledged + 1);
54398
+ let completeOffset = this.acknowledgedOffset;
54399
+ for await (const { event, endOffset } of this.readEvents(completeOffset)) {
54400
+ this.nextSequence = Math.max(this.nextSequence, event.sequence + 1);
54401
+ if (event.sequence <= this.acknowledged) {
54402
+ this.acknowledgedOffset = endOffset;
54403
+ }
54404
+ completeOffset = endOffset;
54405
+ }
54406
+ if (size > completeOffset) {
54407
+ await truncate(this.eventsPath, completeOffset);
54298
54408
  }
54409
+ await this.saveState();
54299
54410
  this.loaded = true;
54300
54411
  }
54301
54412
  async append(event) {
@@ -54326,27 +54437,28 @@ var RunJournal = class {
54326
54437
  return this.nextSequence - 1;
54327
54438
  }
54328
54439
  async flush(client) {
54329
- await this.load();
54330
- if (this.flushing) return this.flushing;
54331
- this.flushing = this.flushInternal(client).finally(() => {
54332
- this.flushing = void 0;
54333
- });
54334
- return this.flushing;
54440
+ const previous = this.flushing;
54441
+ const operation = (async () => {
54442
+ await previous?.catch(() => void 0);
54443
+ await this.load();
54444
+ await this.appending;
54445
+ await this.flushInternal(client);
54446
+ })();
54447
+ this.flushing = operation;
54448
+ try {
54449
+ await operation;
54450
+ } finally {
54451
+ if (this.flushing === operation) this.flushing = void 0;
54452
+ }
54335
54453
  }
54336
54454
  async flushInternal(client) {
54337
- const pending = (await this.readEvents()).filter(
54338
- ({ sequence }) => sequence > this.acknowledged
54339
- );
54340
- for (let index = 0; index < pending.length; ) {
54341
- const attemptId = pending[index].attemptId ?? this.attemptId;
54342
- let end = index + 1;
54343
- while (end < pending.length && end - index < 100 && (pending[end].attemptId ?? this.attemptId) === attemptId) {
54344
- end += 1;
54345
- }
54346
- const chunk = pending.slice(index, end);
54455
+ let chunk = [];
54456
+ let endOffset = this.acknowledgedOffset;
54457
+ const upload = async () => {
54458
+ if (!chunk.length) return;
54347
54459
  await client.appendRunEvents(
54348
54460
  this.runId,
54349
- attemptId,
54461
+ chunk[0].attemptId ?? this.attemptId,
54350
54462
  chunk.map((value) => {
54351
54463
  const event = { ...value };
54352
54464
  Reflect.deleteProperty(event, "attemptId");
@@ -54354,20 +54466,49 @@ var RunJournal = class {
54354
54466
  })
54355
54467
  );
54356
54468
  this.acknowledged = chunk.at(-1).sequence;
54469
+ this.acknowledgedOffset = endOffset;
54357
54470
  await this.saveState();
54358
- index = end;
54359
- }
54360
- }
54361
- async readEvents() {
54471
+ chunk = [];
54472
+ };
54473
+ for await (const entry of this.readEvents(this.acknowledgedOffset)) {
54474
+ if (chunk.length && (chunk.length === 100 || (chunk[0].attemptId ?? this.attemptId) !== (entry.event.attemptId ?? this.attemptId)))
54475
+ await upload();
54476
+ chunk.push(entry.event);
54477
+ endOffset = entry.endOffset;
54478
+ }
54479
+ await upload();
54480
+ }
54481
+ async *readEvents(start) {
54482
+ const stream = createReadStream6(this.eventsPath, { start });
54483
+ let pending = Buffer.alloc(0);
54484
+ let offset = start;
54362
54485
  try {
54363
- return (await readFile12(this.eventsPath, "utf8")).split("\n").filter(Boolean).map((line) => JSON.parse(line));
54486
+ for await (const bytes of stream) {
54487
+ const buffer = Buffer.concat([pending, bytes]);
54488
+ let index = 0;
54489
+ let newline;
54490
+ while ((newline = buffer.indexOf(10, index)) !== -1) {
54491
+ const line = buffer.subarray(index, newline);
54492
+ offset += newline - index + 1;
54493
+ index = newline + 1;
54494
+ if (line.length) {
54495
+ yield {
54496
+ event: JSON.parse(line.toString("utf8")),
54497
+ endOffset: offset
54498
+ };
54499
+ }
54500
+ }
54501
+ pending = buffer.subarray(index);
54502
+ }
54364
54503
  } catch (error) {
54365
- if (error.code === "ENOENT") return [];
54504
+ if (error.code === "ENOENT") return;
54366
54505
  throw error;
54506
+ } finally {
54507
+ stream.destroy();
54367
54508
  }
54368
54509
  }
54369
54510
  saveState() {
54370
- const contents = `${JSON.stringify({ acknowledged: this.acknowledged, nextSequence: this.nextSequence })}
54511
+ const contents = `${JSON.stringify({ acknowledged: this.acknowledged, nextSequence: this.nextSequence, acknowledgedOffset: this.acknowledgedOffset })}
54371
54512
  `;
54372
54513
  const operation = this.saving.then(async () => {
54373
54514
  const temporary = `${this.statePath}.${process.pid}.${randomUUID7()}.tmp`;
@@ -55548,11 +55689,14 @@ var RunManager = class {
55548
55689
  runQueues = /* @__PURE__ */ new Map();
55549
55690
  interruptionRequests = /* @__PURE__ */ new Map();
55550
55691
  importTimer;
55692
+ importTask;
55693
+ importedSnapshots = /* @__PURE__ */ new Map();
55551
55694
  commandTimer;
55552
55695
  stopped = false;
55696
+ shutdown = new AbortController();
55553
55697
  async start() {
55554
55698
  if (typeof this.client.pendingRunCommands !== "function") return;
55555
- await this.syncImports();
55699
+ void this.syncImports();
55556
55700
  try {
55557
55701
  const commands = await this.client.pendingRunCommands();
55558
55702
  await Promise.all(commands.map((command3) => this.execute(command3, true)));
@@ -55868,7 +56012,14 @@ var RunManager = class {
55868
56012
  journal,
55869
56013
  adapter,
55870
56014
  baseline
55871
- );
56015
+ ).catch((error) => {
56016
+ if (!this.stopped) {
56017
+ console.error(
56018
+ `Run ${run.id} finalization failed:`,
56019
+ error instanceof Error ? error.message : error
56020
+ );
56021
+ }
56022
+ });
55872
56023
  const requestedAfterStart = this.interruptionRequests.get(run.id);
55873
56024
  if (requestedAfterStart)
55874
56025
  await this.interruptActive(active, requestedAfterStart);
@@ -55890,7 +56041,7 @@ var RunManager = class {
55890
56041
  baseline,
55891
56042
  finalCheckpoint
55892
56043
  );
55893
- await this.retry(
56044
+ await this.retryFinalization(
55894
56045
  () => this.client.reportRunCheckpoint(run.id, attemptId, {
55895
56046
  ...finalCheckpoint,
55896
56047
  diffPatch: comparison.rollbackPatch
@@ -55908,14 +56059,14 @@ var RunManager = class {
55908
56059
  ).catch(() => void 0);
55909
56060
  }
55910
56061
  }
55911
- await this.retry(
56062
+ await this.retryFinalization(
55912
56063
  () => this.client.finishRunAttempt(attemptId, {
55913
56064
  status: completion.status,
55914
56065
  finalOutput: completion.finalOutput,
55915
56066
  error: completion.error
55916
56067
  })
55917
56068
  );
55918
- await this.retry(
56069
+ await this.retryFinalization(
55919
56070
  () => this.client.completeRunCommand(
55920
56071
  command3.id,
55921
56072
  completion.status === "FAILED" ? "FAILED" : "SUCCEEDED",
@@ -55923,13 +56074,14 @@ var RunManager = class {
55923
56074
  )
55924
56075
  );
55925
56076
  } catch (error) {
55926
- await this.retry(
56077
+ if (this.stopped) return;
56078
+ await this.retryFinalization(
55927
56079
  () => this.client.finishRunAttempt(attemptId, {
55928
56080
  status: "FAILED",
55929
56081
  error: error instanceof Error ? error.message : String(error)
55930
56082
  })
55931
56083
  );
55932
- await this.retry(
56084
+ await this.retryFinalization(
55933
56085
  () => this.client.completeRunCommand(
55934
56086
  command3.id,
55935
56087
  "FAILED",
@@ -56212,10 +56364,22 @@ var RunManager = class {
56212
56364
  }
56213
56365
  return null;
56214
56366
  }
56215
- async syncImports() {
56367
+ syncImports() {
56368
+ if (this.stopped) return Promise.resolve();
56369
+ if (!this.importTask) {
56370
+ this.importTask = this.syncImportsInternal().finally(() => {
56371
+ this.importTask = void 0;
56372
+ });
56373
+ }
56374
+ return this.importTask;
56375
+ }
56376
+ async syncImportsInternal() {
56216
56377
  if (this.stopped) return;
56378
+ if (this.importTimer) clearTimeout(this.importTimer);
56379
+ this.importTimer = void 0;
56217
56380
  try {
56218
56381
  const codebases = await this.client.agentCodebases();
56382
+ if (this.stopped) return;
56219
56383
  const worktrees = codebases.flatMap(
56220
56384
  (codebase) => codebase.worktrees.map((worktree) => ({
56221
56385
  id: worktree.id,
@@ -56224,29 +56388,59 @@ var RunManager = class {
56224
56388
  }))
56225
56389
  );
56226
56390
  for (const adapter of this.adapters.values()) {
56391
+ if (this.stopped) return;
56227
56392
  try {
56228
56393
  const catalog = await adapter.catalog?.(worktrees);
56394
+ if (this.stopped) return;
56229
56395
  await this.client.reportRunProviderImportStatus(
56230
56396
  adapter.key,
56231
56397
  "SYNCING",
56232
56398
  void 0,
56233
56399
  { ...catalog, capabilities: adapter.capabilities }
56234
56400
  );
56401
+ if (this.stopped) return;
56235
56402
  const runs = await adapter.discover(worktrees);
56403
+ if (this.stopped) return;
56236
56404
  let batch = [];
56237
56405
  let batchBytes = 0;
56406
+ let snapshots = [];
56407
+ const upload = async () => {
56408
+ await this.client.importProviderRuns(adapter.key, batch);
56409
+ for (const [key, digest] of snapshots) {
56410
+ this.importedSnapshots.set(key, digest);
56411
+ }
56412
+ batch = [];
56413
+ batchBytes = 0;
56414
+ snapshots = [];
56415
+ };
56238
56416
  for (const run of runs) {
56239
- const bytes = Buffer.byteLength(JSON.stringify(run), "utf8");
56417
+ if (this.stopped) return;
56418
+ const serialized2 = JSON.stringify(run);
56419
+ const key = JSON.stringify([
56420
+ adapter.key,
56421
+ run.nativeId,
56422
+ run.worktreeId
56423
+ ]);
56424
+ const digest = createHash8("sha256").update(serialized2).digest("hex");
56425
+ const active = [
56426
+ "ACTIVE",
56427
+ "RUNNING",
56428
+ "STARTING",
56429
+ "IN_PROGRESS",
56430
+ "WAITING_FOR_ANSWER"
56431
+ ].includes(run.status?.toUpperCase() ?? "");
56432
+ if (!active && this.importedSnapshots.get(key) === digest) continue;
56433
+ const bytes = Buffer.byteLength(serialized2, "utf8");
56240
56434
  if (batch.length && batchBytes + bytes > 4 * 1024 * 1024) {
56241
- await this.client.importProviderRuns(adapter.key, batch);
56242
- batch = [];
56243
- batchBytes = 0;
56435
+ await upload();
56244
56436
  }
56245
56437
  batch.push(run);
56246
56438
  batchBytes += bytes;
56439
+ snapshots.push([key, digest]);
56247
56440
  }
56248
- await this.client.importProviderRuns(adapter.key, batch);
56441
+ await upload();
56249
56442
  } catch (error) {
56443
+ if (this.stopped) return;
56250
56444
  await this.client.reportRunProviderImportStatus(
56251
56445
  adapter.key,
56252
56446
  "FAILED",
@@ -56259,10 +56453,12 @@ var RunManager = class {
56259
56453
  }
56260
56454
  }
56261
56455
  } catch (error) {
56262
- console.error(
56263
- "Provider history sync failed:",
56264
- error instanceof Error ? error.message : error
56265
- );
56456
+ if (!this.stopped) {
56457
+ console.error(
56458
+ "Provider history sync failed:",
56459
+ error instanceof Error ? error.message : error
56460
+ );
56461
+ }
56266
56462
  } finally {
56267
56463
  if (!this.stopped) {
56268
56464
  this.importTimer = setTimeout(
@@ -56273,14 +56469,19 @@ var RunManager = class {
56273
56469
  }
56274
56470
  }
56275
56471
  }
56276
- async retry(operation, attempts = Infinity) {
56472
+ retryFinalization(operation) {
56473
+ return this.retry(operation, Infinity);
56474
+ }
56475
+ async retry(operation, attempts = MAX_AGENT_REQUEST_ATTEMPTS) {
56277
56476
  let delay3 = 1e3;
56278
56477
  for (let attempt = 1; ; attempt += 1) {
56279
56478
  try {
56280
56479
  return await operation();
56281
56480
  } catch (error) {
56282
- if (this.stopped || attempt >= attempts) throw error;
56283
- await new Promise((resolve11) => setTimeout(resolve11, delay3));
56481
+ if (this.stopped || attempt >= attempts || !isRetryableAgentError(error))
56482
+ throw error;
56483
+ await waitForAgentRetry(delay3, this.shutdown.signal);
56484
+ if (this.stopped) throw error;
56284
56485
  delay3 = Math.min(delay3 * 2, 3e4);
56285
56486
  }
56286
56487
  }
@@ -56293,14 +56494,16 @@ var RunManager = class {
56293
56494
  }
56294
56495
  async close() {
56295
56496
  this.stopped = true;
56497
+ this.shutdown.abort();
56296
56498
  if (this.importTimer) clearTimeout(this.importTimer);
56297
56499
  if (this.commandTimer) clearTimeout(this.commandTimer);
56298
56500
  for (const active of this.active.values()) {
56299
56501
  await active.handle.interrupt("PAUSED").catch(() => void 0);
56300
56502
  }
56301
- await Promise.allSettled(
56302
- this.adapters.values().map((adapter) => adapter.close?.())
56303
- );
56503
+ await Promise.allSettled([
56504
+ this.importTask,
56505
+ ...this.adapters.values().map((adapter) => adapter.close?.())
56506
+ ]);
56304
56507
  }
56305
56508
  };
56306
56509
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-development-environment/control-agent",
3
- "version": "0.0.130",
3
+ "version": "0.0.132",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {