@ai-development-environment/control-agent 0.0.143 → 0.0.144

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 +161 -20
  2. package/package.json +1 -1
@@ -7445,17 +7445,18 @@ var AgentGraphQLClient = class {
7445
7445
  throw new Error("Downloaded dSYM checksum did not match");
7446
7446
  }
7447
7447
  }
7448
- completeJob(jobId, status2, result, error, executionToken) {
7448
+ completeJob(jobId, status2, result, error, executionToken, cleanupVerified = true) {
7449
7449
  return this.request(
7450
- `mutation CompleteJob($jobId: ID!, $status: AgentJobStatus!, $result: JSON, $error: String, $executionToken: String) {
7451
- completeAgentJob(jobId: $jobId, status: $status, result: $result, error: $error, executionToken: $executionToken) { id status }
7450
+ `mutation CompleteJob($jobId: ID!, $status: AgentJobStatus!, $result: JSON, $error: String, $executionToken: String, $cleanupVerified: Boolean!) {
7451
+ completeAgentJob(jobId: $jobId, status: $status, result: $result, error: $error, executionToken: $executionToken, cleanupVerified: $cleanupVerified) { id status }
7452
7452
  }`,
7453
7453
  {
7454
7454
  jobId,
7455
7455
  status: status2,
7456
7456
  result,
7457
7457
  error,
7458
- ...executionToken ? { executionToken } : {}
7458
+ ...executionToken ? { executionToken } : {},
7459
+ cleanupVerified
7459
7460
  }
7460
7461
  );
7461
7462
  }
@@ -11742,7 +11743,7 @@ function tailscaleServeRemovePayload(value) {
11742
11743
  // package.json
11743
11744
  var package_default = {
11744
11745
  name: "@ai-development-environment/control-agent",
11745
- version: "0.0.143",
11746
+ version: "0.0.144",
11746
11747
  description: "Control agent for the AI Development Environment",
11747
11748
  license: "MIT",
11748
11749
  repository: {
@@ -11900,6 +11901,12 @@ import {
11900
11901
  writeFileSync
11901
11902
  } from "node:fs";
11902
11903
  import { dirname as dirname2, join as join2 } from "node:path";
11904
+ var ProcessInspectionTimeoutError = class extends Error {
11905
+ constructor() {
11906
+ super("Process inspection timed out after 30 seconds");
11907
+ this.name = "ProcessInspectionTimeoutError";
11908
+ }
11909
+ };
11903
11910
  var executions = new AsyncLocalStorage();
11904
11911
  function executionEnvironment() {
11905
11912
  const journal = executions.getStore();
@@ -12043,11 +12050,18 @@ var spawnExecutionProcess = ((command3, args = [], options = {}) => {
12043
12050
  });
12044
12051
  function processTable(environment = false) {
12045
12052
  const args = environment ? ["axeww", "-o", "pid=,pgid=,stat=,command="] : ["axww", "-o", "pid=,pgid=,stat=,command="];
12046
- const output = execFileSync("/bin/ps", args, {
12047
- encoding: "utf8",
12048
- timeout: 5e3,
12049
- maxBuffer: 32 * 1024 * 1024
12050
- });
12053
+ let output;
12054
+ try {
12055
+ output = execFileSync("/bin/ps", args, {
12056
+ encoding: "utf8",
12057
+ timeout: 3e4,
12058
+ maxBuffer: 32 * 1024 * 1024
12059
+ });
12060
+ } catch (error) {
12061
+ if (error.code === "ETIMEDOUT")
12062
+ throw new ProcessInspectionTimeoutError();
12063
+ throw error;
12064
+ }
12051
12065
  return output.split("\n").flatMap((line) => {
12052
12066
  const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
12053
12067
  return match ? [
@@ -12086,6 +12100,7 @@ var delay = (ms, signal2) => new Promise((resolve13) => {
12086
12100
  if (signal2?.aborted) finish();
12087
12101
  });
12088
12102
  async function cleanupGroups(journal, signal2) {
12103
+ if (!journal.groups.length) return;
12089
12104
  const groups = new Set(journal.groups);
12090
12105
  const ownGroup = process.platform !== "win32" ? processTable().find((entry) => entry.pid === process.pid)?.group : process.pid;
12091
12106
  if (process.platform !== "win32") {
@@ -12166,12 +12181,40 @@ async function withExecutionJournal(identity, task) {
12166
12181
  try {
12167
12182
  return await executions.run(
12168
12183
  journal,
12169
- () => task({ cleanupProcesses: () => cleanupGroups(journal), settle })
12184
+ () => task({
12185
+ cleanupProcesses: () => cleanupGroups(journal),
12186
+ settle,
12187
+ checkpointCompletion: (completion) => {
12188
+ journal.completion = completion;
12189
+ persist(journal);
12190
+ }
12191
+ })
12170
12192
  );
12171
12193
  } finally {
12172
12194
  await settle();
12173
12195
  }
12174
12196
  }
12197
+ function executionCompletion(identity) {
12198
+ let journal;
12199
+ try {
12200
+ journal = JSON.parse(readFileSync(journalPath(identity), "utf8"));
12201
+ } catch (error) {
12202
+ if (error.code === "ENOENT") return void 0;
12203
+ throw error;
12204
+ }
12205
+ if (journal.version !== 1 || journal.agentId !== identity.agentId || journal.jobId !== identity.jobId || journal.executionToken !== identity.executionToken || journal.marker !== digest(identity.executionToken) || !Array.isArray(journal.groups))
12206
+ throw new Error(
12207
+ "Execution journal identity is invalid; its claim remains protected"
12208
+ );
12209
+ const completion = journal.completion;
12210
+ if (completion && (!["SUCCEEDED", "FAILED", "CANCELLED", "TIMED_OUT"].includes(
12211
+ completion.status
12212
+ ) || !completion.result || typeof completion.result !== "object" || typeof completion.result.cancelled !== "boolean" || typeof completion.result.timedOut !== "boolean" || !(completion.result.exitCode === null || Number.isInteger(completion.result.exitCode))))
12213
+ throw new Error(
12214
+ "Execution completion checkpoint is invalid; its claim remains protected"
12215
+ );
12216
+ return completion;
12217
+ }
12175
12218
  async function recoverExecution(identity, signal2) {
12176
12219
  let journal;
12177
12220
  try {
@@ -50043,7 +50086,7 @@ var runIosBuildWithPaths = async (payload, timeoutMs, signal2, onLog, context) =
50043
50086
  finalSignal
50044
50087
  );
50045
50088
  const codeStateObservedAt = (/* @__PURE__ */ new Date()).toISOString();
50046
- return {
50089
+ const completedResult = {
50047
50090
  exitCode,
50048
50091
  signal: buildResult?.signal ?? preflightResult?.signal ?? null,
50049
50092
  timedOut,
@@ -50058,6 +50101,8 @@ var runIosBuildWithPaths = async (payload, timeoutMs, signal2, onLog, context) =
50058
50101
  finalStateHash,
50059
50102
  codeStateObservedAt
50060
50103
  };
50104
+ await context?.finalizeBuildResult?.(completedResult);
50105
+ return completedResult;
50061
50106
  };
50062
50107
  let enteredProtectedBuild = false;
50063
50108
  try {
@@ -57175,6 +57220,16 @@ async function refreshFetchedCodebase(client, input, timeoutMs, signal2, withIns
57175
57220
  }
57176
57221
 
57177
57222
  // src/job-executor.ts
57223
+ var PROCESS_INSPECTION_WARNING = "Process inspection timed out after 30 seconds. The completed build result and artifacts were preserved; process cleanup will be retried.";
57224
+ var resultStatus = (result) => result.cancelled ? "CANCELLED" : result.timedOut ? "TIMED_OUT" : result.exitCode === 0 ? "SUCCEEDED" : "FAILED";
57225
+ function warnPreservedBuild(completion) {
57226
+ completion.result.warnings = [
57227
+ .../* @__PURE__ */ new Set([
57228
+ ...completion.result.warnings ?? [],
57229
+ PROCESS_INSPECTION_WARNING
57230
+ ])
57231
+ ];
57232
+ }
57178
57233
  function resourceSignature(value) {
57179
57234
  return JSON.stringify(
57180
57235
  parseOperationDescriptor(value).resources.map(
@@ -57198,18 +57253,20 @@ var JobExecutor = class {
57198
57253
  repositoryCoordinator;
57199
57254
  running = /* @__PURE__ */ new Map();
57200
57255
  stopping = false;
57256
+ cleanupPending = /* @__PURE__ */ new Set();
57201
57257
  shutdown = new AbortController();
57202
57258
  execute(job) {
57203
57259
  if (this.stopping || this.running.has(job.id)) return;
57204
57260
  const controller = new AbortController();
57205
- const task = this.run(job, controller).finally(
57206
- () => this.running.delete(job.id)
57207
- );
57261
+ const task = this.run(job, controller).finally(() => {
57262
+ this.running.delete(job.id);
57263
+ this.cleanupPending.delete(job.id);
57264
+ });
57208
57265
  this.running.set(job.id, { controller, task });
57209
57266
  }
57210
57267
  cancel(jobId) {
57211
57268
  const running = this.running.get(jobId);
57212
- running?.controller.abort();
57269
+ if (!this.cleanupPending.has(jobId)) running?.controller.abort();
57213
57270
  return Boolean(running);
57214
57271
  }
57215
57272
  /** Hold source resources until interrupted subprocess cleanup is verified. */
@@ -57233,19 +57290,46 @@ var JobExecutor = class {
57233
57290
  async () => {
57234
57291
  entered.resolve();
57235
57292
  let reported = false;
57293
+ let completion;
57294
+ let checkpointRead = false;
57295
+ let completedStatus = status2;
57236
57296
  while (!this.shutdown.signal.aborted) {
57237
57297
  try {
57298
+ if (!checkpointRead) {
57299
+ completion = job.kind === IOS_BUILD_JOB_KIND ? executionCompletion(identity) : void 0;
57300
+ checkpointRead = true;
57301
+ if (completion && ["RUNNING", "CANCELLING"].includes(job.status))
57302
+ completedStatus = completion.status;
57303
+ }
57238
57304
  await recoverExecution(identity, this.shutdown.signal);
57239
57305
  await this.client.completeJob(
57240
57306
  job.id,
57241
- status2,
57242
- void 0,
57243
- "Agent service restarted; interrupted processes were stopped and verified",
57307
+ completedStatus,
57308
+ completion?.result,
57309
+ completion?.error ?? (completion ? void 0 : "Agent service restarted; interrupted processes were stopped and verified"),
57244
57310
  job.executionToken
57245
57311
  );
57246
57312
  forgetExecution(identity);
57247
57313
  return;
57248
57314
  } catch (error) {
57315
+ if (completion && error instanceof ProcessInspectionTimeoutError) {
57316
+ warnPreservedBuild(completion);
57317
+ try {
57318
+ await this.client.completeJob(
57319
+ job.id,
57320
+ completedStatus,
57321
+ completion.result,
57322
+ completion.error,
57323
+ job.executionToken,
57324
+ false
57325
+ );
57326
+ } catch (reportError) {
57327
+ console.error(
57328
+ `Could not preserve completed build ${job.id}:`,
57329
+ reportError
57330
+ );
57331
+ }
57332
+ }
57249
57333
  if (!reported) {
57250
57334
  console.error(
57251
57335
  `Cleanup verification required for job ${job.id}; execution resources remain protected:`,
@@ -57317,6 +57401,7 @@ var JobExecutor = class {
57317
57401
  const handler = handlers[claimed.kind];
57318
57402
  if (!handler)
57319
57403
  throw new Error(`No local handler is registered for ${claimed.kind}`);
57404
+ let execution;
57320
57405
  const runHandler = () => {
57321
57406
  controller.signal.throwIfAborted();
57322
57407
  return handler(
@@ -57372,6 +57457,58 @@ var JobExecutor = class {
57372
57457
  )
57373
57458
  ),
57374
57459
  reportBuildProgress: (input) => this.client.reportBuildProgress(input),
57460
+ ...resourceAware && claimed.kind === IOS_BUILD_JOB_KIND ? {
57461
+ finalizeBuildResult: async (completedResult) => {
57462
+ if (!execution)
57463
+ throw new Error("Build execution journal is unavailable");
57464
+ result = completedResult;
57465
+ const completion = {
57466
+ status: resultStatus(completedResult),
57467
+ result: completedResult,
57468
+ ...completedResult.exitCode !== 0 && !completedResult.cancelled && !completedResult.timedOut ? {
57469
+ error: `Process exited with code ${completedResult.exitCode}`
57470
+ } : {}
57471
+ };
57472
+ execution.checkpointCompletion(completion);
57473
+ this.cleanupPending.add(claimed.id);
57474
+ try {
57475
+ await execution.settle();
57476
+ return;
57477
+ } catch (error) {
57478
+ if (!(error instanceof ProcessInspectionTimeoutError))
57479
+ throw error;
57480
+ }
57481
+ warnPreservedBuild(completion);
57482
+ execution.checkpointCompletion(completion);
57483
+ let preserved = false;
57484
+ while (!this.shutdown.signal.aborted) {
57485
+ try {
57486
+ if (!preserved) {
57487
+ await this.client.completeJob(
57488
+ claimed.id,
57489
+ completion.status,
57490
+ completion.result,
57491
+ completion.error,
57492
+ claimed.executionToken,
57493
+ false
57494
+ );
57495
+ preserved = true;
57496
+ }
57497
+ await execution.settle();
57498
+ return;
57499
+ } catch (error) {
57500
+ console.error(
57501
+ `Build ${claimed.id} result is preserved; cleanup verification will retry:`,
57502
+ error instanceof Error ? error.message : error
57503
+ );
57504
+ await waitForAgentRetry(1e3, this.shutdown.signal);
57505
+ }
57506
+ }
57507
+ throw new Error(
57508
+ "Build cleanup interrupted by agent shutdown; its result remains protected"
57509
+ );
57510
+ }
57511
+ } : {},
57375
57512
  reportBuildDeploymentLaunched: (deploymentId, launchedAt) => this.client.reportBuildDeploymentLaunched(
57376
57513
  claimed.id,
57377
57514
  deploymentId,
@@ -57396,7 +57533,10 @@ var JobExecutor = class {
57396
57533
  jobId: claimed.id,
57397
57534
  executionToken: claimed.executionToken
57398
57535
  },
57399
- runHandler
57536
+ (journal) => {
57537
+ execution = journal;
57538
+ return runHandler();
57539
+ }
57400
57540
  );
57401
57541
  else if (!codebaseId) result = await runHandler();
57402
57542
  else if (claimed.kind === IOS_BUILD_JOB_KIND) {
@@ -57425,6 +57565,7 @@ var JobExecutor = class {
57425
57565
  status2 = controller.signal.aborted ? "CANCELLED" : "FAILED";
57426
57566
  console.error(`Job ${claimed.id} failed:`, completionError);
57427
57567
  }
57568
+ if (this.stopping && this.cleanupPending.has(claimed.id)) return;
57428
57569
  for (let attempt = 1; ; attempt++) {
57429
57570
  try {
57430
57571
  if (resourceAware && claimed.executionToken)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-development-environment/control-agent",
3
- "version": "0.0.143",
3
+ "version": "0.0.144",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {