@ai-development-environment/control-agent 0.0.142 → 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 +437 -135
  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
  }
@@ -7617,22 +7618,31 @@ var AgentGraphQLClient = class {
7617
7618
  health() {
7618
7619
  return this.request(`query Health { health }`);
7619
7620
  }
7621
+ async requestDefaultBranchConfiguration(query) {
7622
+ try {
7623
+ return await this.request(query);
7624
+ } catch (error) {
7625
+ if (!(error instanceof Error) || !/Cannot query field "defaultBranchPatterns"/.test(error.message))
7626
+ throw error;
7627
+ return this.request(query.replace(/\sdefaultBranchPatterns\b/g, ""));
7628
+ }
7629
+ }
7620
7630
  async agentCodebases() {
7621
- const data = await this.request(`query AgentCodebases {
7631
+ const data = await this.requestDefaultBranchConfiguration(`query AgentCodebases {
7622
7632
  agentCodebases {
7623
- id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate lastFetchedAt lastFetchAttemptAt
7633
+ id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate defaultBranchPatterns lastFetchedAt lastFetchAttemptAt
7624
7634
  worktrees { id folder branch gitDirectory baseBranchOverride }
7625
7635
  }
7626
7636
  }`);
7627
7637
  return data.agentCodebases;
7628
7638
  }
7629
7639
  async agentCodebaseConfiguration() {
7630
- const data = await this.request(`query AgentCodebaseConfiguration {
7640
+ const data = await this.requestDefaultBranchConfiguration(`query AgentCodebaseConfiguration {
7631
7641
  agentCodebaseConfiguration {
7632
7642
  refreshIntervalSeconds
7633
7643
  fetchIntervalSeconds
7634
7644
  codebases {
7635
- id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate lastFetchedAt lastFetchAttemptAt
7645
+ id folder canonicalOrigin defaultBranch keepBaseBranchUpToDate defaultBranchPatterns lastFetchedAt lastFetchAttemptAt
7636
7646
  worktrees { id folder branch gitDirectory baseBranchOverride }
7637
7647
  }
7638
7648
  }
@@ -8134,6 +8144,50 @@ var CODEBASE_GIT_INSPECT_JOB_KIND = "codebase.git.inspect";
8134
8144
  var CODEBASE_GIT_OPERATION_JOB_KIND = "codebase.git.operation";
8135
8145
  var CODEBASE_BRANCHES_DELETE_JOB_KIND = "codebase.branches.delete";
8136
8146
  var CODEBASE_RECONCILE_EVENT_CAPABILITY = "codebase.reconcile.requested";
8147
+ var CODEBASE_DEFAULT_BRANCHES_CAPABILITY = "codebase.default-branches.v1";
8148
+ function normalizeDefaultBranchPatterns(value) {
8149
+ if (!Array.isArray(value) || value.length > 100)
8150
+ throw new Error(
8151
+ "Default branch patterns must be a list of at most 100 entries"
8152
+ );
8153
+ const patterns = value.map((entry) => {
8154
+ if (typeof entry !== "string" || entry.trim().length > 255)
8155
+ throw new Error(
8156
+ "Default branch patterns must be strings of at most 255 characters"
8157
+ );
8158
+ const pattern = entry.trim();
8159
+ if (pattern && /[\s\x00-\x1f\x7f]/.test(pattern))
8160
+ throw new Error(
8161
+ "Default branch patterns cannot contain whitespace or control characters"
8162
+ );
8163
+ return pattern;
8164
+ });
8165
+ return [...new Set(patterns.filter(Boolean))];
8166
+ }
8167
+ function isDefaultBranch(branch, remoteDefaultBranch, patterns = []) {
8168
+ if (!branch) return false;
8169
+ if (branch === remoteDefaultBranch) return true;
8170
+ return patterns.some((pattern) => {
8171
+ let position = 0;
8172
+ let patternPosition = 0;
8173
+ let star = -1;
8174
+ let resume = 0;
8175
+ while (position < branch.length) {
8176
+ if (pattern[patternPosition] === "*") {
8177
+ star = patternPosition++;
8178
+ resume = position;
8179
+ } else if (pattern[patternPosition] === branch[position]) {
8180
+ patternPosition++;
8181
+ position++;
8182
+ } else if (star >= 0) {
8183
+ patternPosition = star + 1;
8184
+ position = ++resume;
8185
+ } else return false;
8186
+ }
8187
+ while (pattern[patternPosition] === "*") patternPosition++;
8188
+ return patternPosition === pattern.length;
8189
+ });
8190
+ }
8137
8191
  var MAX_CODEBASE_GIT_BRANCHES = 1e3;
8138
8192
  var MAX_CODEBASE_STASHES = 200;
8139
8193
  var MAX_CODEBASE_STASH_PATCH_BYTES = 256 * 1024;
@@ -8439,7 +8493,8 @@ function codebaseJobPayload(value) {
8439
8493
  "codebaseId",
8440
8494
  "expectedOrigin",
8441
8495
  "baseBranch",
8442
- "keepBaseBranchUpToDate"
8496
+ "keepBaseBranchUpToDate",
8497
+ "defaultBranchPatterns"
8443
8498
  ]);
8444
8499
  const unexpected = Object.keys(payload).find((key) => !allowed.has(key));
8445
8500
  if (unexpected)
@@ -8461,6 +8516,11 @@ function codebaseJobPayload(value) {
8461
8516
  payload.keepBaseBranchUpToDate,
8462
8517
  "payload.keepBaseBranchUpToDate"
8463
8518
  )
8519
+ },
8520
+ ...payload.defaultBranchPatterns === void 0 ? {} : {
8521
+ defaultBranchPatterns: normalizeDefaultBranchPatterns(
8522
+ payload.defaultBranchPatterns
8523
+ )
8464
8524
  }
8465
8525
  };
8466
8526
  }
@@ -11683,7 +11743,7 @@ function tailscaleServeRemovePayload(value) {
11683
11743
  // package.json
11684
11744
  var package_default = {
11685
11745
  name: "@ai-development-environment/control-agent",
11686
- version: "0.0.142",
11746
+ version: "0.0.144",
11687
11747
  description: "Control agent for the AI Development Environment",
11688
11748
  license: "MIT",
11689
11749
  repository: {
@@ -11753,6 +11813,7 @@ var PORTABLE_AGENT_CAPABILITIES = [
11753
11813
  CCUSAGE_REPORT_JOB_KIND,
11754
11814
  ...CODEBASE_JOB_KINDS,
11755
11815
  CODEBASE_RECONCILE_EVENT_CAPABILITY,
11816
+ CODEBASE_DEFAULT_BRANCHES_CAPABILITY,
11756
11817
  ...WORKTREE_JOB_KINDS,
11757
11818
  WORKTREE_REVIEWED_SYNC_CAPABILITY,
11758
11819
  ...SKILL_JOB_KINDS,
@@ -11840,6 +11901,12 @@ import {
11840
11901
  writeFileSync
11841
11902
  } from "node:fs";
11842
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
+ };
11843
11910
  var executions = new AsyncLocalStorage();
11844
11911
  function executionEnvironment() {
11845
11912
  const journal = executions.getStore();
@@ -11983,11 +12050,18 @@ var spawnExecutionProcess = ((command3, args = [], options = {}) => {
11983
12050
  });
11984
12051
  function processTable(environment = false) {
11985
12052
  const args = environment ? ["axeww", "-o", "pid=,pgid=,stat=,command="] : ["axww", "-o", "pid=,pgid=,stat=,command="];
11986
- const output = execFileSync("/bin/ps", args, {
11987
- encoding: "utf8",
11988
- timeout: 5e3,
11989
- maxBuffer: 32 * 1024 * 1024
11990
- });
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
+ }
11991
12065
  return output.split("\n").flatMap((line) => {
11992
12066
  const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
11993
12067
  return match ? [
@@ -12026,6 +12100,7 @@ var delay = (ms, signal2) => new Promise((resolve13) => {
12026
12100
  if (signal2?.aborted) finish();
12027
12101
  });
12028
12102
  async function cleanupGroups(journal, signal2) {
12103
+ if (!journal.groups.length) return;
12029
12104
  const groups = new Set(journal.groups);
12030
12105
  const ownGroup = process.platform !== "win32" ? processTable().find((entry) => entry.pid === process.pid)?.group : process.pid;
12031
12106
  if (process.platform !== "win32") {
@@ -12106,12 +12181,40 @@ async function withExecutionJournal(identity, task) {
12106
12181
  try {
12107
12182
  return await executions.run(
12108
12183
  journal,
12109
- () => 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
+ })
12110
12192
  );
12111
12193
  } finally {
12112
12194
  await settle();
12113
12195
  }
12114
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
+ }
12115
12218
  async function recoverExecution(identity, signal2) {
12116
12219
  let journal;
12117
12220
  try {
@@ -45725,76 +45828,165 @@ var refreshCodebase = async (payload, timeoutMs, signal2) => {
45725
45828
  )
45726
45829
  };
45727
45830
  };
45728
- async function updateBaseBranchAfterFetch(folder, baseBranch, timeoutMs, signal2) {
45729
- const localRef = `refs/heads/${baseBranch}`;
45730
- const remoteRef = `refs/remotes/origin/${baseBranch}`;
45831
+ async function updateBaseBranchAfterFetch(folder, baseBranch, timeoutMs, signal2, expectedOrigin, expectedGitDirectory) {
45731
45832
  try {
45732
- const validRef = await git2(
45833
+ return await withRepositoryMetadata(
45733
45834
  folder,
45734
- ["check-ref-format", localRef],
45735
- timeoutMs,
45736
- signal2
45737
- );
45738
- if (validRef.exitCode !== 0) return false;
45739
- const staged = await git2(
45740
- folder,
45741
- ["diff", "--cached", "--quiet"],
45742
- timeoutMs,
45743
- signal2
45744
- );
45745
- if (staged.exitCode !== 0) return false;
45746
- const remoteExists = await git2(
45747
- folder,
45748
- ["show-ref", "--verify", "--quiet", remoteRef],
45749
- timeoutMs,
45750
- signal2
45751
- );
45752
- if (remoteExists.exitCode !== 0) return false;
45753
- const remoteTip = requireSuccess(
45754
- await git2(folder, ["rev-parse", remoteRef], timeoutMs, signal2),
45755
- "Could not pin the fetched base branch"
45756
- ).stdout.trim();
45757
- const currentBranch2 = await git2(
45758
- folder,
45759
- ["symbolic-ref", "--short", "-q", "HEAD"],
45760
- timeoutMs,
45835
+ async () => {
45836
+ if (expectedGitDirectory) {
45837
+ const directory = await git2(
45838
+ folder,
45839
+ ["rev-parse", "--path-format=absolute", "--git-dir"],
45840
+ timeoutMs,
45841
+ signal2
45842
+ );
45843
+ if (directory.exitCode !== 0 || await realpath3(directory.stdout.trim()) !== expectedGitDirectory)
45844
+ return false;
45845
+ }
45846
+ const current = await git2(
45847
+ folder,
45848
+ ["symbolic-ref", "--short", "-q", "HEAD"],
45849
+ timeoutMs,
45850
+ signal2
45851
+ );
45852
+ if (current.exitCode !== 0 || current.stdout.trim() !== baseBranch)
45853
+ return false;
45854
+ if (expectedOrigin) {
45855
+ const remote = await git2(
45856
+ folder,
45857
+ ["remote", "get-url", "origin"],
45858
+ timeoutMs,
45859
+ signal2
45860
+ );
45861
+ if (remote.exitCode !== 0 || normalizeGitOrigin(remote.stdout.trim()).canonicalOrigin !== expectedOrigin)
45862
+ return false;
45863
+ }
45864
+ const status2 = await git2(
45865
+ folder,
45866
+ [
45867
+ "status",
45868
+ "--porcelain=v1",
45869
+ "-z",
45870
+ "--untracked-files=all",
45871
+ "--ignore-submodules=none"
45872
+ ],
45873
+ timeoutMs,
45874
+ signal2
45875
+ );
45876
+ if (status2.exitCode !== 0 || status2.outputTruncated || status2.stdout)
45877
+ return false;
45878
+ for (const name of [
45879
+ "MERGE_HEAD",
45880
+ "rebase-merge",
45881
+ "rebase-apply",
45882
+ "CHERRY_PICK_HEAD",
45883
+ "REVERT_HEAD"
45884
+ ]) {
45885
+ const path = await git2(
45886
+ folder,
45887
+ ["rev-parse", "--path-format=absolute", "--git-path", name],
45888
+ timeoutMs,
45889
+ signal2
45890
+ );
45891
+ if (path.exitCode !== 0) return false;
45892
+ try {
45893
+ await stat4(path.stdout.trim());
45894
+ return false;
45895
+ } catch (error) {
45896
+ if (error.code !== "ENOENT") throw error;
45897
+ }
45898
+ }
45899
+ const remoteRef = `refs/remotes/origin/${baseBranch}`;
45900
+ const valid = await git2(
45901
+ folder,
45902
+ ["check-ref-format", remoteRef],
45903
+ timeoutMs,
45904
+ signal2
45905
+ );
45906
+ if (valid.exitCode !== 0) return false;
45907
+ const tip = await git2(
45908
+ folder,
45909
+ ["rev-parse", "--verify", remoteRef],
45910
+ timeoutMs,
45911
+ signal2
45912
+ );
45913
+ if (tip.exitCode !== 0) return false;
45914
+ const ancestor = await git2(
45915
+ folder,
45916
+ ["merge-base", "--is-ancestor", "HEAD", tip.stdout.trim()],
45917
+ timeoutMs,
45918
+ signal2
45919
+ );
45920
+ if (ancestor.exitCode !== 0) return false;
45921
+ const branch = await git2(
45922
+ folder,
45923
+ ["symbolic-ref", "--short", "-q", "HEAD"],
45924
+ timeoutMs,
45925
+ signal2
45926
+ );
45927
+ if (branch.exitCode !== 0 || branch.stdout.trim() !== baseBranch)
45928
+ return false;
45929
+ return (await git2(
45930
+ folder,
45931
+ ["merge", "--ff-only", tip.stdout.trim()],
45932
+ timeoutMs,
45933
+ signal2
45934
+ )).exitCode === 0;
45935
+ },
45761
45936
  signal2
45762
45937
  );
45763
- if (currentBranch2.exitCode === 0 && currentBranch2.stdout.trim() === baseBranch) {
45764
- const merged = await git2(
45765
- folder,
45766
- ["merge", "--ff-only", remoteTip],
45767
- timeoutMs,
45768
- signal2
45769
- );
45770
- return merged.exitCode === 0;
45771
- }
45772
- const localExists = await git2(
45938
+ } catch (error) {
45939
+ if (signal2.aborted) throw error;
45940
+ return false;
45941
+ }
45942
+ }
45943
+ async function updateDefaultBranchesAfterFetch(folder, defaultBranch, patterns, timeoutMs, signal2, withCheckout, expectedOrigin) {
45944
+ const listed = requireSuccess(
45945
+ await git2(
45773
45946
  folder,
45774
- ["show-ref", "--verify", "--quiet", localRef],
45947
+ ["worktree", "list", "--porcelain", "-z"],
45775
45948
  timeoutMs,
45776
45949
  signal2
45777
- );
45778
- if (localExists.exitCode === 0) {
45779
- const fastForward = await git2(
45780
- folder,
45781
- ["merge-base", "--is-ancestor", localRef, remoteTip],
45782
- timeoutMs,
45783
- signal2
45950
+ ),
45951
+ "Could not list worktrees"
45952
+ );
45953
+ if (listed.outputTruncated) return;
45954
+ for (const block of listed.stdout.split("\0\0")) {
45955
+ signal2.throwIfAborted();
45956
+ const fields = block.split("\0");
45957
+ if (fields.some((field) => field.startsWith("prunable")) || fields.includes("bare"))
45958
+ continue;
45959
+ const checkout = fields.find((field) => field.startsWith("worktree "))?.slice(9);
45960
+ const branch = fields.find((field) => field.startsWith("branch refs/heads/"))?.slice(18);
45961
+ if (!checkout || !isDefaultBranch(branch, defaultBranch, patterns))
45962
+ continue;
45963
+ try {
45964
+ const path = await realpath3(checkout);
45965
+ const directory = requireSuccess(
45966
+ await git2(
45967
+ path,
45968
+ ["rev-parse", "--path-format=absolute", "--git-dir"],
45969
+ timeoutMs,
45970
+ signal2
45971
+ ),
45972
+ "Could not resolve worktree"
45973
+ ).stdout.trim();
45974
+ const expectedGitDirectory = await realpath3(directory);
45975
+ await withCheckout(
45976
+ path,
45977
+ expectedGitDirectory,
45978
+ () => updateBaseBranchAfterFetch(
45979
+ path,
45980
+ branch,
45981
+ timeoutMs,
45982
+ signal2,
45983
+ expectedOrigin,
45984
+ expectedGitDirectory
45985
+ )
45784
45986
  );
45785
- if (fastForward.exitCode !== 0) return false;
45786
- } else if (localExists.exitCode !== 1) {
45787
- return false;
45987
+ } catch (error) {
45988
+ if (signal2.aborted) throw error;
45788
45989
  }
45789
- const updated = await git2(
45790
- folder,
45791
- ["branch", "--force", baseBranch, remoteTip],
45792
- timeoutMs,
45793
- signal2
45794
- );
45795
- return updated.exitCode === 0;
45796
- } catch {
45797
- return false;
45798
45990
  }
45799
45991
  }
45800
45992
  var fetchCodebase = async (payload, timeoutMs, signal2, onLog, context) => {
@@ -45833,46 +46025,37 @@ var fetchCodebase = async (payload, timeoutMs, signal2, onLog, context) => {
45833
46025
  if (error instanceof InterruptedGitInspection) return error.result;
45834
46026
  throw error;
45835
46027
  }
45836
- if (result.exitCode === 0 && input.keepBaseBranchUpToDate && input.baseBranch) {
45837
- await log2("Updating the base branch where it can be fast-forwarded");
45838
- const update = () => updateBaseBranchAfterFetch(
46028
+ if (result.exitCode === 0 && input.keepBaseBranchUpToDate) {
46029
+ await log2(
46030
+ "Updating clean default branch worktrees from their matching origin branches"
46031
+ );
46032
+ await updateDefaultBranchesAfterFetch(
45839
46033
  input.folder,
45840
- input.baseBranch,
46034
+ input.baseBranch ?? null,
46035
+ input.defaultBranchPatterns ?? [],
45841
46036
  timeoutMs,
45842
- signal2
45843
- );
45844
- const gitDirectory2 = requireSuccess(
45845
- await git2(
45846
- input.folder,
45847
- ["rev-parse", "--path-format=absolute", "--git-dir"],
45848
- timeoutMs,
45849
- signal2
45850
- ),
45851
- "Could not resolve base worktree"
45852
- ).stdout.trim();
45853
- const grant = context?.tryWithOperationResources ? await context.tryWithOperationResources(
45854
- [
45855
- {
45856
- kind: "WORKTREE",
45857
- key: `${context.agentId}:worktree:${await realpath3(gitDirectory2)}`,
45858
- agentId: context.agentId,
45859
- codebaseId: input.codebaseId,
45860
- mode: "WRITE"
45861
- },
45862
- {
45863
- kind: "PATH",
45864
- key: input.folder,
45865
- path: input.folder,
45866
- agentId: context.agentId,
45867
- codebaseId: input.codebaseId,
45868
- mode: "WRITE"
45869
- }
45870
- ],
45871
- update
45872
- ) : { ran: true, result: await update() };
45873
- const updated = grant.ran && grant.result;
45874
- await log2(
45875
- updated ? "Base branch updated" : "Base branch left unchanged because it could not be safely fast-forwarded"
46037
+ signal2,
46038
+ (folder, gitDirectory2, update) => context?.tryWithOperationResources ? context.tryWithOperationResources(
46039
+ [
46040
+ {
46041
+ kind: "WORKTREE",
46042
+ key: `${context.agentId}:worktree:${gitDirectory2}`,
46043
+ agentId: context.agentId,
46044
+ codebaseId: input.codebaseId,
46045
+ mode: "WRITE"
46046
+ },
46047
+ {
46048
+ kind: "PATH",
46049
+ key: folder,
46050
+ path: folder,
46051
+ agentId: context.agentId,
46052
+ codebaseId: input.codebaseId,
46053
+ mode: "WRITE"
46054
+ }
46055
+ ],
46056
+ update
46057
+ ) : update(),
46058
+ input.expectedOrigin
45876
46059
  );
45877
46060
  }
45878
46061
  const afterResult = await inspectCodebaseProcess(
@@ -49903,7 +50086,7 @@ var runIosBuildWithPaths = async (payload, timeoutMs, signal2, onLog, context) =
49903
50086
  finalSignal
49904
50087
  );
49905
50088
  const codeStateObservedAt = (/* @__PURE__ */ new Date()).toISOString();
49906
- return {
50089
+ const completedResult = {
49907
50090
  exitCode,
49908
50091
  signal: buildResult?.signal ?? preflightResult?.signal ?? null,
49909
50092
  timedOut,
@@ -49918,6 +50101,8 @@ var runIosBuildWithPaths = async (payload, timeoutMs, signal2, onLog, context) =
49918
50101
  finalStateHash,
49919
50102
  codeStateObservedAt
49920
50103
  };
50104
+ await context?.finalizeBuildResult?.(completedResult);
50105
+ return completedResult;
49921
50106
  };
49922
50107
  let enteredProtectedBuild = false;
49923
50108
  try {
@@ -57035,6 +57220,16 @@ async function refreshFetchedCodebase(client, input, timeoutMs, signal2, withIns
57035
57220
  }
57036
57221
 
57037
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
+ }
57038
57233
  function resourceSignature(value) {
57039
57234
  return JSON.stringify(
57040
57235
  parseOperationDescriptor(value).resources.map(
@@ -57058,18 +57253,20 @@ var JobExecutor = class {
57058
57253
  repositoryCoordinator;
57059
57254
  running = /* @__PURE__ */ new Map();
57060
57255
  stopping = false;
57256
+ cleanupPending = /* @__PURE__ */ new Set();
57061
57257
  shutdown = new AbortController();
57062
57258
  execute(job) {
57063
57259
  if (this.stopping || this.running.has(job.id)) return;
57064
57260
  const controller = new AbortController();
57065
- const task = this.run(job, controller).finally(
57066
- () => this.running.delete(job.id)
57067
- );
57261
+ const task = this.run(job, controller).finally(() => {
57262
+ this.running.delete(job.id);
57263
+ this.cleanupPending.delete(job.id);
57264
+ });
57068
57265
  this.running.set(job.id, { controller, task });
57069
57266
  }
57070
57267
  cancel(jobId) {
57071
57268
  const running = this.running.get(jobId);
57072
- running?.controller.abort();
57269
+ if (!this.cleanupPending.has(jobId)) running?.controller.abort();
57073
57270
  return Boolean(running);
57074
57271
  }
57075
57272
  /** Hold source resources until interrupted subprocess cleanup is verified. */
@@ -57093,19 +57290,46 @@ var JobExecutor = class {
57093
57290
  async () => {
57094
57291
  entered.resolve();
57095
57292
  let reported = false;
57293
+ let completion;
57294
+ let checkpointRead = false;
57295
+ let completedStatus = status2;
57096
57296
  while (!this.shutdown.signal.aborted) {
57097
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
+ }
57098
57304
  await recoverExecution(identity, this.shutdown.signal);
57099
57305
  await this.client.completeJob(
57100
57306
  job.id,
57101
- status2,
57102
- void 0,
57103
- "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"),
57104
57310
  job.executionToken
57105
57311
  );
57106
57312
  forgetExecution(identity);
57107
57313
  return;
57108
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
+ }
57109
57333
  if (!reported) {
57110
57334
  console.error(
57111
57335
  `Cleanup verification required for job ${job.id}; execution resources remain protected:`,
@@ -57177,6 +57401,7 @@ var JobExecutor = class {
57177
57401
  const handler = handlers[claimed.kind];
57178
57402
  if (!handler)
57179
57403
  throw new Error(`No local handler is registered for ${claimed.kind}`);
57404
+ let execution;
57180
57405
  const runHandler = () => {
57181
57406
  controller.signal.throwIfAborted();
57182
57407
  return handler(
@@ -57232,6 +57457,58 @@ var JobExecutor = class {
57232
57457
  )
57233
57458
  ),
57234
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
+ } : {},
57235
57512
  reportBuildDeploymentLaunched: (deploymentId, launchedAt) => this.client.reportBuildDeploymentLaunched(
57236
57513
  claimed.id,
57237
57514
  deploymentId,
@@ -57256,7 +57533,10 @@ var JobExecutor = class {
57256
57533
  jobId: claimed.id,
57257
57534
  executionToken: claimed.executionToken
57258
57535
  },
57259
- runHandler
57536
+ (journal) => {
57537
+ execution = journal;
57538
+ return runHandler();
57539
+ }
57260
57540
  );
57261
57541
  else if (!codebaseId) result = await runHandler();
57262
57542
  else if (claimed.kind === IOS_BUILD_JOB_KIND) {
@@ -57285,6 +57565,7 @@ var JobExecutor = class {
57285
57565
  status2 = controller.signal.aborted ? "CANCELLED" : "FAILED";
57286
57566
  console.error(`Job ${claimed.id} failed:`, completionError);
57287
57567
  }
57568
+ if (this.stopping && this.cleanupPending.has(claimed.id)) return;
57288
57569
  for (let attempt = 1; ; attempt++) {
57289
57570
  try {
57290
57571
  if (resourceAware && claimed.executionToken)
@@ -57465,22 +57746,43 @@ var CodebaseMonitor = class {
57465
57746
  signal2
57466
57747
  );
57467
57748
  if (fetchResult.exitCode === 0) {
57468
- if (codebase.keepBaseBranchUpToDate && codebase.defaultBranch) {
57469
- const baseBranch = codebase.defaultBranch;
57470
- await runExclusive(
57471
- () => updateBaseBranchAfterFetch(
57472
- codebase.folder,
57473
- baseBranch,
57474
- INSPECTION_TIMEOUT_MS,
57475
- signal2
57476
- )
57477
- );
57478
- }
57479
57749
  snapshot = await this.inspectRoot(codebase, signal2);
57480
57750
  } else {
57481
57751
  fetchError = (fetchResult.stderr || "Git fetch failed").replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1").slice(0, 2e3);
57482
57752
  }
57483
57753
  }
57754
+ if (snapshot.availability === "AVAILABLE" && codebase.keepBaseBranchUpToDate && !signal2.aborted) {
57755
+ await updateDefaultBranchesAfterFetch(
57756
+ codebase.folder,
57757
+ codebase.defaultBranch,
57758
+ codebase.defaultBranchPatterns ?? [],
57759
+ INSPECTION_TIMEOUT_MS,
57760
+ signal2,
57761
+ (folder, gitDirectory2, update) => this.agentId ? this.repositoryCoordinator.tryWithResources(
57762
+ [
57763
+ {
57764
+ kind: "WORKTREE",
57765
+ key: `${this.agentId}:worktree:${gitDirectory2}`,
57766
+ agentId: this.agentId,
57767
+ codebaseId: codebase.id,
57768
+ mode: "WRITE"
57769
+ },
57770
+ {
57771
+ kind: "PATH",
57772
+ key: folder,
57773
+ path: folder,
57774
+ agentId: this.agentId,
57775
+ codebaseId: codebase.id,
57776
+ mode: "WRITE"
57777
+ }
57778
+ ],
57779
+ update,
57780
+ signal2
57781
+ ) : runExclusive(update),
57782
+ codebase.canonicalOrigin
57783
+ );
57784
+ snapshot = await this.inspectRoot(codebase, signal2);
57785
+ }
57484
57786
  let inventory;
57485
57787
  try {
57486
57788
  inventory = await discoverWorktrees(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-development-environment/control-agent",
3
- "version": "0.0.142",
3
+ "version": "0.0.144",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {