agl 22.0.1 → 22.0.2

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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '22.0.1',
6
+ version: '22.0.2',
7
7
  description: 'Agent Gateway Layer for OpenCode and FlexHarness sessions'
8
8
  }
package/ts/classes.cli.ts CHANGED
@@ -39,20 +39,30 @@ import {
39
39
  } from './classes.processinspection.js';
40
40
  import {
41
41
  createUpgradeToken,
42
+ normalizeUpgradeRegistryUrl,
42
43
  parseUpgradeWorkerPayload,
43
44
  UpgradeCoordinator,
44
45
  upgradePackageTransitionSource,
45
46
  upgradePackageTransitionTarget,
47
+ upgradeTransactionTargetVersion,
46
48
  type UpgradeStartLease,
47
49
  } from './classes.upgradecoordinator.js';
48
50
  import { upgradeTokenEnvironmentVariable } from './constants.upgradeenvironment.js';
49
51
  import {
52
+ adoptOrphanedUpgradeUnderLock,
50
53
  createUpgradeWorkerPayload,
54
+ handoffLaunchedUpgradeWorkerUnderLock,
55
+ handoffStalledUpgradeRecoveryUnderLock,
56
+ type IUpgradeWorkerLaunchCandidate,
57
+ inspectOrphanedUpgradeForAdoption,
51
58
  launchDetachedUpgradeWorker,
52
59
  resolveCurrentCliPath,
53
60
  resolveCurrentPnpmGlobalInstallation,
54
61
  runUpgradeWorker,
62
+ terminalizeUpgradeWorkerHandoffFailure,
55
63
  tryResolveCurrentPnpmGlobalInstallation,
64
+ UpgradeWorkerCandidateDrainageError,
65
+ UpgradeWorkerHandoffError,
56
66
  } from './classes.upgradetransaction.js';
57
67
  import { followUpgradeTransaction } from './functions.upgradefollower.js';
58
68
 
@@ -64,6 +74,10 @@ const currentCliName = commitinfo.name === upgradePackageTransitionSource.packag
64
74
  const currentRuntimeName = currentCliName === upgradePackageTransitionTarget.cliName
65
75
  ? 'AGL'
66
76
  : currentCliName;
77
+ const upgradeRegistryUsage = currentRuntimeName === 'AGL' ? ' [--registry <url>]' : '';
78
+ const upgradeRegistryHelp = currentRuntimeName === 'AGL'
79
+ ? ' --registry <url> Override pnpm\'s configured registry for this upgrade\n'
80
+ : '';
67
81
 
68
82
  const helpText = `
69
83
  ${currentRuntimeName === 'AGL' ? 'AGL - Agent Gateway Layer' : currentRuntimeName}
@@ -72,7 +86,7 @@ Usage:
72
86
  ${currentCliName} start [options]
73
87
  ${currentCliName} status [--port 4097] [--json]
74
88
  ${currentCliName} stop [--port 4097]
75
- ${currentCliName} upgrade [--port 4097] [--grace-period-seconds 300] [--continue-sessions] [--json]
89
+ ${currentCliName} upgrade [--port 4097]${upgradeRegistryUsage} [--grace-period-seconds 300] [--continue-sessions] [--json]
76
90
  ${currentCliName} foreground [options]
77
91
  ${currentCliName} temp-password [--port 4097] [--ttl-hours 24]
78
92
 
@@ -100,7 +114,7 @@ Startup options:
100
114
  characters; short codes weaken brute-force resistance)
101
115
 
102
116
  Upgrade options:
103
- --grace-period-seconds <n> Maximum graceful pause time (default: 300)
117
+ ${upgradeRegistryHelp} --grace-period-seconds <n> Maximum graceful pause time (default: 300)
104
118
  --continue-sessions Continue sessions paused for the upgrade
105
119
 
106
120
  Temp password options:
@@ -956,46 +970,70 @@ const stopController = async (argvArg: TParsedArguments): Promise<void> => {
956
970
  process.stdout.write(`${currentRuntimeName} stopped.\n`);
957
971
  };
958
972
 
973
+ export interface ICreateCurrentPackageUpgradeTransactionOptions {
974
+ token: string;
975
+ port: number;
976
+ sourceVersion: string;
977
+ registryUrl?: string;
978
+ controllerWasRunning: boolean;
979
+ continueSessions: boolean;
980
+ gracePeriodMs: number;
981
+ }
982
+
959
983
  export const createCurrentPackageUpgradeTransaction = async (
960
984
  coordinatorArg: UpgradeCoordinator,
961
- optionsArg: {
962
- token: string;
963
- port: number;
964
- sourceVersion: string;
965
- controllerWasRunning: boolean;
966
- continueSessions: boolean;
967
- gracePeriodMs: number;
968
- },
985
+ optionsArg: ICreateCurrentPackageUpgradeTransactionOptions,
969
986
  ) => {
970
987
  if (
971
988
  commitinfo.name === upgradePackageTransitionSource.packageName
972
989
  && optionsArg.sourceVersion === upgradePackageTransitionSource.version
973
990
  ) {
991
+ if (optionsArg.registryUrl !== undefined) {
992
+ throw new Error(
993
+ 'The legacy hcon package transition uses pnpm registry configuration and cannot retain --registry.',
994
+ );
995
+ }
974
996
  return await coordinatorArg.createPackageTransitionTransaction(optionsArg);
975
997
  }
976
998
  return await coordinatorArg.createTransaction(optionsArg);
977
999
  };
978
1000
 
979
1001
  const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
980
- await preflightAGLHomeMigration();
981
1002
  if (process.platform !== 'linux' && process.platform !== 'darwin') {
982
1003
  throw new Error(`${currentCliName} upgrade is currently supported on Linux and macOS only.`);
983
1004
  }
984
1005
  const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!;
1006
+ const registryValue = readStringOption(argvArg, 'registry', 'registry');
1007
+ const registryUrl = registryValue === undefined
1008
+ ? undefined
1009
+ : normalizeUpgradeRegistryUrl(registryValue);
985
1010
  const gracePeriodMs = readUpgradeGracePeriodMs(argvArg);
986
1011
  const continueSessions = readBooleanOption(argvArg, 'continueSessions', 'continue-sessions');
987
1012
  const installation = await resolveCurrentPnpmGlobalInstallation();
1013
+ const coordinator = new UpgradeCoordinator(installation.globalRoot);
1014
+ await coordinator.initializeExistingCanonicalState();
1015
+ const orphaned = await inspectOrphanedUpgradeForAdoption({
1016
+ coordinator,
1017
+ installation,
1018
+ port,
1019
+ ...(registryUrl === undefined ? {} : { registryUrl }),
1020
+ });
1021
+ if (!orphaned) await preflightAGLHomeMigration();
988
1022
  const status = await tryQueryControllerStatus(port);
989
1023
  if (!status && await isLoopbackPortListening(port)) {
990
1024
  throw new Error(
991
1025
  `Port ${port} is occupied by an incompatible service or controller version; refusing to upgrade.`,
992
1026
  );
993
1027
  }
994
- if (!status) await ensureControllerDataRoot();
1028
+ if (orphaned && status) {
1029
+ throw new Error('An orphaned upgrade cannot be adopted while a controller is running.');
1030
+ }
1031
+ if (!status && !orphaned) await ensureControllerDataRoot();
1032
+ const retainedTransaction = orphaned?.transaction;
995
1033
  const payload = createUpgradeWorkerPayload({
996
1034
  port,
997
- gracePeriodMs,
998
- continueSessions,
1035
+ gracePeriodMs: retainedTransaction?.gracePeriodMs ?? gracePeriodMs,
1036
+ continueSessions: retainedTransaction?.continueSessions ?? continueSessions,
999
1037
  ...(status
1000
1038
  ? {
1001
1039
  expectedController: {
@@ -1006,88 +1044,147 @@ const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
1006
1044
  }
1007
1045
  : {}),
1008
1046
  });
1009
- const coordinator = new UpgradeCoordinator(installation.globalRoot);
1010
1047
  const invocationToken = createUpgradeToken();
1011
1048
  const invocationLease = await coordinator.acquireUpgradeLock({
1012
1049
  token: invocationToken,
1013
1050
  cliPath: installation.cliPath,
1014
1051
  command: 'upgrade',
1015
1052
  });
1016
- let handoffStarted = false;
1053
+ let invocationLeaseHandled = false;
1017
1054
  let invocationError: unknown;
1018
1055
  try {
1019
- await createCurrentPackageUpgradeTransaction(coordinator, {
1020
- token: payload.token,
1021
- port,
1022
- sourceVersion: installation.packageVersion,
1023
- controllerWasRunning: status !== undefined,
1024
- continueSessions,
1025
- gracePeriodMs: payload.gracePeriodMs,
1026
- });
1027
- let worker: { workerPid: number; logFilePath: string };
1028
- try {
1029
- if (status) {
1030
- await coordinator.createLaunchGrant({
1056
+ let worker: IUpgradeWorkerLaunchCandidate;
1057
+ let controllerWasRunning = status !== undefined;
1058
+ if (orphaned) {
1059
+ const adopted = await adoptOrphanedUpgradeUnderLock({
1060
+ coordinator,
1061
+ lock: invocationLease,
1062
+ token: payload.token,
1063
+ expected: orphaned,
1064
+ installation,
1065
+ port,
1066
+ ...(registryUrl === undefined ? {} : { registryUrl }),
1067
+ });
1068
+ controllerWasRunning = adopted.controllerWasRunning;
1069
+ let launched: Awaited<ReturnType<typeof handoffStalledUpgradeRecoveryUnderLock>>;
1070
+ try {
1071
+ launched = await handoffStalledUpgradeRecoveryUnderLock({
1072
+ coordinator,
1031
1073
  token: payload.token,
1032
- port,
1033
- packageName: controllerPackageName,
1034
- packageVersion: commitinfo.version,
1035
- cliPath: installation.cliPath,
1036
- controller: payload.expectedController!,
1074
+ installation,
1075
+ command: 'upgrade',
1076
+ lock: invocationLease,
1037
1077
  });
1038
- try {
1039
- worker = await requestControllerUpgradeLaunch(port, payload.token);
1040
- } finally {
1041
- await coordinator.removeLaunchGrant(payload.token);
1078
+ } finally {
1079
+ invocationLeaseHandled = true;
1080
+ }
1081
+ if (!launched) throw new Error('The adopted upgrade became terminal before recovery launch.');
1082
+ worker = launched;
1083
+ } else {
1084
+ const racedOrphan = await inspectOrphanedUpgradeForAdoption({
1085
+ coordinator,
1086
+ installation,
1087
+ port,
1088
+ ...(registryUrl === undefined ? {} : { registryUrl }),
1089
+ });
1090
+ if (racedOrphan) {
1091
+ throw new Error('An orphaned upgrade appeared after normal upgrade preflight.');
1092
+ }
1093
+ await createCurrentPackageUpgradeTransaction(coordinator, {
1094
+ token: payload.token,
1095
+ port,
1096
+ sourceVersion: installation.packageVersion,
1097
+ ...(registryUrl === undefined ? {} : { registryUrl }),
1098
+ controllerWasRunning,
1099
+ continueSessions,
1100
+ gracePeriodMs: payload.gracePeriodMs,
1101
+ });
1102
+ try {
1103
+ if (status) {
1104
+ await coordinator.createLaunchGrant({
1105
+ token: payload.token,
1106
+ port,
1107
+ packageName: controllerPackageName,
1108
+ packageVersion: commitinfo.version,
1109
+ cliPath: installation.cliPath,
1110
+ controller: payload.expectedController!,
1111
+ });
1112
+ try {
1113
+ const launched = await requestControllerUpgradeLaunch(port, payload.token);
1114
+ worker = {
1115
+ pid: launched.workerPid,
1116
+ cliPath: installation.cliPath,
1117
+ logFilePath: launched.logFilePath,
1118
+ };
1119
+ } finally {
1120
+ await coordinator.removeLaunchGrant(payload.token);
1121
+ }
1122
+ } else {
1123
+ const launched = await launchDetachedUpgradeWorker({ installation, payload });
1124
+ worker = launched;
1125
+ }
1126
+ } catch (errorArg) {
1127
+ if (errorArg instanceof UpgradeWorkerCandidateDrainageError || status) {
1128
+ invocationLeaseHandled = true;
1129
+ throw errorArg instanceof UpgradeWorkerCandidateDrainageError
1130
+ ? errorArg
1131
+ : new UpgradeWorkerCandidateDrainageError(
1132
+ 'The controller upgrade launch failed and candidate drainage cannot be proven by the CLI.',
1133
+ errorArg,
1134
+ );
1135
+ }
1136
+ const transaction = await coordinator.readTransaction(payload.token);
1137
+ if (transaction.worker?.logFilePath) {
1138
+ worker = {
1139
+ pid: transaction.worker.pid,
1140
+ processGroupId: transaction.worker.processGroupId,
1141
+ fingerprint: transaction.worker.fingerprint,
1142
+ cliPath: transaction.worker.cliPath,
1143
+ logFilePath: transaction.worker.logFilePath,
1144
+ };
1145
+ } else {
1146
+ await coordinator.finishTransaction(
1147
+ payload.token,
1148
+ false,
1149
+ 'The upgrade worker could not be launched.',
1150
+ errorArg instanceof Error ? errorArg.message : String(errorArg),
1151
+ );
1152
+ throw errorArg;
1042
1153
  }
1043
- } else {
1044
- const launched = await launchDetachedUpgradeWorker({ installation, payload });
1045
- worker = { workerPid: launched.pid, logFilePath: launched.logFilePath };
1046
1154
  }
1047
- } catch (errorArg) {
1048
- const transaction = await coordinator.readTransaction(payload.token);
1049
- if (transaction.worker?.logFilePath) {
1050
- worker = {
1051
- workerPid: transaction.worker.pid,
1052
- logFilePath: transaction.worker.logFilePath,
1053
- };
1054
- } else {
1055
- await coordinator.finishTransaction(
1056
- payload.token,
1057
- false,
1058
- 'The upgrade worker could not be launched.',
1059
- errorArg instanceof Error ? errorArg.message : String(errorArg),
1060
- );
1061
- throw errorArg;
1155
+ try {
1156
+ try {
1157
+ worker = await handoffLaunchedUpgradeWorkerUnderLock({
1158
+ coordinator,
1159
+ token: payload.token,
1160
+ installation,
1161
+ command: 'upgrade',
1162
+ lock: invocationLease,
1163
+ candidate: worker,
1164
+ retainLockOnFailure: true,
1165
+ });
1166
+ } catch (errorArg) {
1167
+ if (!(errorArg instanceof UpgradeWorkerHandoffError)) throw errorArg;
1168
+ await terminalizeUpgradeWorkerHandoffFailure({
1169
+ coordinator,
1170
+ token: payload.token,
1171
+ error: errorArg,
1172
+ });
1173
+ }
1174
+ } finally {
1175
+ invocationLeaseHandled = true;
1062
1176
  }
1063
1177
  }
1064
- await coordinator.recordWorkerLaunch(
1065
- payload.token,
1066
- worker.workerPid,
1067
- installation.cliPath,
1068
- worker.logFilePath,
1069
- );
1070
1178
  const json = readBooleanOption(argvArg, 'json');
1071
1179
  if (!json) {
1072
1180
  process.stdout.write([
1073
- `Upgrade worker started (PID ${worker.workerPid}).`,
1181
+ `Upgrade worker started (PID ${worker.pid}).`,
1074
1182
  `Logs: ${worker.logFilePath}`,
1075
- status
1183
+ controllerWasRunning
1076
1184
  ? `Controller on port ${port} will restart after a successful upgrade.`
1077
1185
  : 'No running controller was found; it will remain stopped.',
1078
1186
  ].join('\n') + '\n');
1079
1187
  }
1080
- const launchedTransaction = await coordinator.readTransaction(payload.token);
1081
- if (!launchedTransaction.worker) {
1082
- throw new Error('The launched upgrade worker identity was not retained.');
1083
- }
1084
- await coordinator.transferUpgradeLockToWorker({
1085
- lock: invocationLease,
1086
- token: payload.token,
1087
- worker: launchedTransaction.worker,
1088
- });
1089
- handoffStarted = true;
1090
- await coordinator.acknowledgeWorker(payload.token);
1091
1188
  let finalTransaction: Awaited<ReturnType<UpgradeCoordinator['readTransaction']>>;
1092
1189
  let followError: unknown;
1093
1190
  try {
@@ -1105,16 +1202,17 @@ const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
1105
1202
  tokenHash: '',
1106
1203
  revision: 0,
1107
1204
  port,
1108
- sourceVersion: installation.packageVersion,
1109
- controllerWasRunning: status !== undefined,
1110
- continueSessions,
1205
+ sourceVersion: retainedTransaction?.sourceVersion ?? installation.packageVersion,
1206
+ ...(registryUrl === undefined ? {} : { registryUrl }),
1207
+ controllerWasRunning,
1208
+ continueSessions: payload.continueSessions,
1111
1209
  gracePeriodMs: payload.gracePeriodMs,
1112
1210
  phase: 'failed' as const,
1113
1211
  message: errorArg instanceof Error ? errorArg.message : String(errorArg),
1114
1212
  createdAt: Date.now(),
1115
1213
  phaseStartedAt: Date.now(),
1116
1214
  updatedAt: Date.now(),
1117
- sessions: [],
1215
+ sessions: retainedTransaction?.sessions ?? [],
1118
1216
  terminal: {
1119
1217
  success: false,
1120
1218
  error: errorArg instanceof Error ? errorArg.message : String(errorArg),
@@ -1127,7 +1225,7 @@ const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
1127
1225
  workerPid: finalTransaction.worker.pid,
1128
1226
  logFilePath: finalTransaction.worker.logFilePath,
1129
1227
  }
1130
- : worker;
1228
+ : { workerPid: worker.pid, logFilePath: worker.logFilePath };
1131
1229
  const sessionSummary = finalTransaction.sessions.reduce((summary, session) => {
1132
1230
  summary.pause[session.pauseState] = (summary.pause[session.pauseState] ?? 0) + 1;
1133
1231
  if (session.continueState) {
@@ -1142,9 +1240,9 @@ const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
1142
1240
  process.stdout.write(JSON.stringify({
1143
1241
  accepted: true,
1144
1242
  ...reportedWorker,
1145
- controllerWasRunning: status !== undefined,
1243
+ controllerWasRunning,
1146
1244
  fromVersion: finalTransaction.sourceVersion,
1147
- toVersion: finalTransaction.targetVersion,
1245
+ toVersion: upgradeTransactionTargetVersion(finalTransaction),
1148
1246
  phase: finalTransaction.phase,
1149
1247
  success: finalTransaction.terminal?.success === true && followError === undefined,
1150
1248
  error: finalTransaction.terminal?.error
@@ -1156,13 +1254,16 @@ const upgradeController = async (argvArg: TParsedArguments): Promise<void> => {
1156
1254
  }
1157
1255
  if (followError) throw followError;
1158
1256
  if (!finalTransaction.terminal?.success) {
1159
- throw new Error(finalTransaction.terminal?.error ?? `${currentCliName} upgrade failed.`);
1257
+ const terminalError = new Error(
1258
+ finalTransaction.terminal?.error ?? `${currentCliName} upgrade failed.`,
1259
+ );
1260
+ throw terminalError;
1160
1261
  }
1161
1262
  } catch (errorArg) {
1162
1263
  invocationError = errorArg;
1163
1264
  }
1164
1265
  let releaseError: unknown;
1165
- if (!handoffStarted) {
1266
+ if (!invocationLeaseHandled) {
1166
1267
  try {
1167
1268
  await invocationLease.release();
1168
1269
  } catch (errorArg) {
@@ -291,6 +291,7 @@ import {
291
291
  upgradePackageTransitionTarget,
292
292
  upgradePackageTransitionTransactionVersion,
293
293
  upgradeTransactionIsStalled,
294
+ upgradeTransactionTargetVersion,
294
295
  type UpgradeInstallationLock,
295
296
  type IUpgradeSessionState,
296
297
  type TUpgradeTransaction,
@@ -301,6 +302,8 @@ import {
301
302
  launchDetachedUpgradeWorker,
302
303
  resolveCurrentPnpmGlobalInstallation,
303
304
  resolveInheritedUpgradeTargetInstallation,
305
+ terminateUpgradeWorkerCandidateUntilDrained,
306
+ type IDetachedUpgradeWorker,
304
307
  } from './classes.upgradetransaction.js';
305
308
  import {
306
309
  upgradeCoordinationRootEnvironmentVariable,
@@ -3368,10 +3371,12 @@ export class OpenCodeController {
3368
3371
  );
3369
3372
  }
3370
3373
  return this.runOperation(peer, async (signalArg) => {
3374
+ let coordinator: UpgradeCoordinator | undefined;
3375
+ let worker: IDetachedUpgradeWorker | undefined;
3371
3376
  try {
3372
3377
  const installation = await resolveCurrentPnpmGlobalInstallation();
3373
3378
  const status = this.getStatus();
3374
- const coordinator = this.coordinatorForUpgradeToken(
3379
+ coordinator = this.coordinatorForUpgradeToken(
3375
3380
  requestArg.token,
3376
3381
  installation.globalRoot,
3377
3382
  );
@@ -3388,7 +3393,7 @@ export class OpenCodeController {
3388
3393
  },
3389
3394
  });
3390
3395
  const transaction = await coordinator.readTransaction(requestArg.token);
3391
- const worker = await launchDetachedUpgradeWorker({
3396
+ worker = await launchDetachedUpgradeWorker({
3392
3397
  installation,
3393
3398
  payload: {
3394
3399
  version: upgradeCoordinationVersion,
@@ -3415,7 +3420,18 @@ export class OpenCodeController {
3415
3420
  logFilePath: worker.logFilePath,
3416
3421
  };
3417
3422
  } catch (errorArg) {
3418
- console.error(`${currentCliName} upgrade worker launch failed:`, errorArg);
3423
+ let reportedError: unknown = errorArg;
3424
+ if (worker && coordinator) {
3425
+ try {
3426
+ await terminateUpgradeWorkerCandidateUntilDrained(coordinator, worker);
3427
+ } catch (cleanupErrorArg) {
3428
+ reportedError = new AggregateError(
3429
+ [errorArg, cleanupErrorArg],
3430
+ `${currentCliName} upgrade worker launch failed and its candidate did not drain.`,
3431
+ );
3432
+ }
3433
+ }
3434
+ console.error(`${currentCliName} upgrade worker launch failed:`, reportedError);
3419
3435
  throw new plugins.typedrequest.TypedResponseError(
3420
3436
  `The ${currentCliName} upgrade launch was not authorized or could not be started.`,
3421
3437
  { code: 'upgrade_launch_failed' },
@@ -13731,9 +13747,10 @@ export class OpenCodeController {
13731
13747
  || session.continueState === 'pending'
13732
13748
  || session.continueState === 'failed')
13733
13749
  ));
13734
- this.upgradeStatus = transaction.targetVersion ? {
13750
+ const targetVersion = upgradeTransactionTargetVersion(transaction);
13751
+ this.upgradeStatus = targetVersion ? {
13735
13752
  fromVersion: transaction.sourceVersion,
13736
- toVersion: transaction.targetVersion,
13753
+ toVersion: targetVersion,
13737
13754
  phase: 'continuing',
13738
13755
  } : this.upgradeStatus;
13739
13756
  await coordinatorArg.mutateTransaction(tokenArg, (current) => ({
@@ -13778,9 +13795,10 @@ export class OpenCodeController {
13778
13795
  this.harnessOperationManager.endUpgradeBarrier(this.upgradeBarrier);
13779
13796
  this.upgradeBarrier = undefined;
13780
13797
  }
13781
- this.upgradeStatus = transaction.targetVersion ? {
13798
+ const targetVersion = upgradeTransactionTargetVersion(transaction);
13799
+ this.upgradeStatus = targetVersion ? {
13782
13800
  fromVersion: transaction.sourceVersion,
13783
- toVersion: transaction.targetVersion,
13801
+ toVersion: targetVersion,
13784
13802
  phase: failed > 0 || modeArg === 'compensate' ? 'failed' : 'completed',
13785
13803
  } : this.upgradeStatus;
13786
13804
  if (this.upgradeStatus) {