agl 22.0.0 → 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.
Files changed (33) hide show
  1. package/changelog.md +20 -0
  2. package/dist_serve/bundle.js +520 -509
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.authstore.d.ts +1 -2
  5. package/dist_ts/classes.authstore.js +1 -6
  6. package/dist_ts/classes.cli.d.ts +4 -2
  7. package/dist_ts/classes.cli.js +161 -68
  8. package/dist_ts/classes.controller.js +68 -30
  9. package/dist_ts/classes.upgradecoordinator.d.ts +39 -1
  10. package/dist_ts/classes.upgradecoordinator.js +740 -74
  11. package/dist_ts/classes.upgradetransaction.d.ts +68 -1
  12. package/dist_ts/classes.upgradetransaction.js +383 -223
  13. package/dist_ts_migration/classes.documentmigrationrunner.js +3 -1
  14. package/dist_ts_migration/index.d.ts +2 -0
  15. package/dist_ts_migration/index.js +3 -1
  16. package/dist_ts_migration/v24_legacyterminallayout.d.ts +18 -0
  17. package/dist_ts_migration/v24_legacyterminallayout.js +79 -0
  18. package/dist_ts_migration/v24_legacyterminallayoutmigration.d.ts +6 -0
  19. package/dist_ts_migration/v24_legacyterminallayoutmigration.js +67 -0
  20. package/package.json +1 -1
  21. package/readme.md +255 -378
  22. package/ts/00_commitinfo_data.ts +1 -1
  23. package/ts/classes.authstore.ts +0 -15
  24. package/ts/classes.cli.ts +184 -83
  25. package/ts/classes.controller.ts +79 -33
  26. package/ts/classes.upgradecoordinator.ts +876 -74
  27. package/ts/classes.upgradetransaction.ts +574 -250
  28. package/ts_migration/classes.documentmigrationrunner.ts +2 -0
  29. package/ts_migration/index.ts +2 -0
  30. package/ts_migration/v24_legacyterminallayout.ts +131 -0
  31. package/ts_migration/v24_legacyterminallayoutmigration.ts +88 -0
  32. package/ts_web/00_commitinfo_data.ts +1 -1
  33. package/ts_web/elements.harnesscontrollerapp.ts +32 -2
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '22.0.0',
6
+ version: '22.0.2',
7
7
  description: 'Agent Gateway Layer for OpenCode and FlexHarness sessions'
8
8
  }
@@ -47,7 +47,6 @@ import type {
47
47
  IControllerRuntimeId,
48
48
  IControllerSessionGroup,
49
49
  IControllerSessionLayout,
50
- TControllerTerminalId,
51
50
  TControllerResourceKind,
52
51
  TControllerSessionId,
53
52
  IControllerIntelligenceExchange,
@@ -1946,20 +1945,6 @@ export class SmartDataAuthStore implements IAuthStore {
1946
1945
  );
1947
1946
  }
1948
1947
 
1949
- public async pruneTerminalLayoutEntries(
1950
- projectIdArg: string,
1951
- liveTerminalIdsArg: readonly TControllerTerminalId[],
1952
- ): Promise<boolean> {
1953
- const liveTerminalKeys = new Set(liveTerminalIdsArg.map(controllerRuntimeIdKey));
1954
- return this.filterSessionLayout(
1955
- projectIdArg,
1956
- (layoutItemId) => (
1957
- layoutItemId.harnessId !== 'controller'
1958
- || liveTerminalKeys.has(controllerRuntimeIdKey(layoutItemId))
1959
- ),
1960
- );
1961
- }
1962
-
1963
1948
  public async pruneArchivedSessionLayoutEntries(
1964
1949
  projectIdArg: string,
1965
1950
  archivedSessionIdsArg: readonly IControllerRuntimeId[],
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,
@@ -1378,6 +1381,13 @@ interface IControllerHarnessSessionEnumeration {
1378
1381
  flex: PromiseSettledResult<IControllerSession[]>;
1379
1382
  }
1380
1383
 
1384
+ interface IControllerHarnessSessionEnumerationTask {
1385
+ task: Promise<IControllerHarnessSessionEnumeration>;
1386
+ invalidated: boolean;
1387
+ settled: boolean;
1388
+ waiterCount: number;
1389
+ }
1390
+
1381
1391
  export class OpenCodeController {
1382
1392
  public readonly options: IControllerStartOptions;
1383
1393
 
@@ -1509,7 +1519,7 @@ export class OpenCodeController {
1509
1519
  private readonly activeOperations = new Set<Promise<unknown>>();
1510
1520
  private readonly harnessSessionEnumerationTasks = new Map<
1511
1521
  string,
1512
- Promise<IControllerHarnessSessionEnumeration>
1522
+ IControllerHarnessSessionEnumerationTask
1513
1523
  >();
1514
1524
  private readonly sessionLayoutOperationTails = new Map<string, Promise<void>>();
1515
1525
  private readonly sessionLayoutOperationDepths = new Map<string, number>();
@@ -2015,12 +2025,6 @@ export class OpenCodeController {
2015
2025
  );
2016
2026
  const startupProjectState = await this.reconcileProjectState(false);
2017
2027
  for (const project of pendingProjectRemovals) this.retiringProjectIds.add(project.id);
2018
- for (const project of startupProjectState.projects) {
2019
- await this.runSessionLayoutOperation(
2020
- project.id,
2021
- () => authStore.pruneTerminalLayoutEntries(project.id, []),
2022
- );
2023
- }
2024
2028
  const startupSettings = await authStore.getSettings();
2025
2029
  this.defaultModels = startupSettings.defaultModels;
2026
2030
  this.autoAcceptPermissions = startupSettings.autoAcceptPermissions === true;
@@ -3367,10 +3371,12 @@ export class OpenCodeController {
3367
3371
  );
3368
3372
  }
3369
3373
  return this.runOperation(peer, async (signalArg) => {
3374
+ let coordinator: UpgradeCoordinator | undefined;
3375
+ let worker: IDetachedUpgradeWorker | undefined;
3370
3376
  try {
3371
3377
  const installation = await resolveCurrentPnpmGlobalInstallation();
3372
3378
  const status = this.getStatus();
3373
- const coordinator = this.coordinatorForUpgradeToken(
3379
+ coordinator = this.coordinatorForUpgradeToken(
3374
3380
  requestArg.token,
3375
3381
  installation.globalRoot,
3376
3382
  );
@@ -3387,7 +3393,7 @@ export class OpenCodeController {
3387
3393
  },
3388
3394
  });
3389
3395
  const transaction = await coordinator.readTransaction(requestArg.token);
3390
- const worker = await launchDetachedUpgradeWorker({
3396
+ worker = await launchDetachedUpgradeWorker({
3391
3397
  installation,
3392
3398
  payload: {
3393
3399
  version: upgradeCoordinationVersion,
@@ -3414,7 +3420,18 @@ export class OpenCodeController {
3414
3420
  logFilePath: worker.logFilePath,
3415
3421
  };
3416
3422
  } catch (errorArg) {
3417
- 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);
3418
3435
  throw new plugins.typedrequest.TypedResponseError(
3419
3436
  `The ${currentCliName} upgrade launch was not authorized or could not be started.`,
3420
3437
  { code: 'upgrade_launch_failed' },
@@ -7360,31 +7377,55 @@ export class OpenCodeController {
7360
7377
  }
7361
7378
  }
7362
7379
 
7363
- private enumerateHarnessSessions(
7380
+ private async enumerateHarnessSessions(
7364
7381
  projectIdArg: string,
7365
7382
  directoryArg: string,
7366
7383
  signalArg: AbortSignal,
7367
7384
  ): Promise<IControllerHarnessSessionEnumeration> {
7368
- signalArg.throwIfAborted();
7369
- let sharedTask = this.harnessSessionEnumerationTasks.get(projectIdArg);
7370
- if (!sharedTask) {
7371
- const lifecycleSignal = this.operationAbortController.signal;
7372
- const operation = Promise.allSettled([
7373
- Promise.resolve().then(() => (
7374
- this.requireOpenCodeClient().listSessions(directoryArg, lifecycleSignal)
7375
- )),
7376
- Promise.resolve().then(() => this.listFlexSessions(projectIdArg, lifecycleSignal)),
7377
- ]).then(([openCode, flex]) => ({ openCode, flex }));
7378
- let trackedTask!: Promise<IControllerHarnessSessionEnumeration>;
7379
- trackedTask = this.trackOperationPromise(operation, () => {
7380
- if (this.harnessSessionEnumerationTasks.get(projectIdArg) === trackedTask) {
7385
+ while (true) {
7386
+ signalArg.throwIfAborted();
7387
+ let shared = this.harnessSessionEnumerationTasks.get(projectIdArg);
7388
+ if (!shared || shared.invalidated) {
7389
+ const lifecycleSignal = this.operationAbortController.signal;
7390
+ const operation = Promise.allSettled([
7391
+ Promise.resolve().then(() => (
7392
+ this.requireOpenCodeClient().listSessions(directoryArg, lifecycleSignal)
7393
+ )),
7394
+ Promise.resolve().then(() => this.listFlexSessions(projectIdArg, lifecycleSignal)),
7395
+ ]).then(([openCode, flex]) => ({ openCode, flex }));
7396
+ let taskRecord!: IControllerHarnessSessionEnumerationTask;
7397
+ let trackedTask!: Promise<IControllerHarnessSessionEnumeration>;
7398
+ trackedTask = this.trackOperationPromise(operation, () => {
7399
+ taskRecord.settled = true;
7400
+ const current = this.harnessSessionEnumerationTasks.get(projectIdArg);
7401
+ if (current === taskRecord && taskRecord.waiterCount === 0) {
7402
+ this.harnessSessionEnumerationTasks.delete(projectIdArg);
7403
+ }
7404
+ });
7405
+ taskRecord = {
7406
+ task: trackedTask,
7407
+ invalidated: false,
7408
+ settled: false,
7409
+ waiterCount: 0,
7410
+ };
7411
+ shared = taskRecord;
7412
+ this.harnessSessionEnumerationTasks.set(projectIdArg, shared);
7413
+ }
7414
+ shared.waiterCount += 1;
7415
+ try {
7416
+ const result = await this.waitForPromiseWithSignal(shared.task, signalArg);
7417
+ if (!shared.invalidated) return result;
7418
+ } finally {
7419
+ shared.waiterCount -= 1;
7420
+ if (
7421
+ shared.settled
7422
+ && shared.waiterCount === 0
7423
+ && this.harnessSessionEnumerationTasks.get(projectIdArg) === shared
7424
+ ) {
7381
7425
  this.harnessSessionEnumerationTasks.delete(projectIdArg);
7382
7426
  }
7383
- });
7384
- this.harnessSessionEnumerationTasks.set(projectIdArg, trackedTask);
7385
- sharedTask = trackedTask;
7427
+ }
7386
7428
  }
7387
- return this.waitForPromiseWithSignal(sharedTask, signalArg);
7388
7429
  }
7389
7430
 
7390
7431
  private toFlexSession(
@@ -11720,7 +11761,7 @@ export class OpenCodeController {
11720
11761
  signalArg?: AbortSignal,
11721
11762
  ): Promise<IControllerSession> {
11722
11763
  if (sessionIdArg.harnessId === 'opencode') {
11723
- return this.requireOpenCodeClient().getSession(
11764
+ return this.requireOpenCodeClient().getSessionSnapshot(
11724
11765
  directoryArg,
11725
11766
  sessionIdArg.nativeId,
11726
11767
  signalArg,
@@ -11977,6 +12018,9 @@ export class OpenCodeController {
11977
12018
  projectIdArg: string,
11978
12019
  sessionIdArg: IControllerRuntimeId,
11979
12020
  ): void {
12021
+ const enumeration = this.harnessSessionEnumerationTasks.get(projectIdArg);
12022
+ if (enumeration) enumeration.invalidated = true;
12023
+ this.harnessSessionEnumerationTasks.delete(projectIdArg);
11980
12024
  if (sessionIdArg.harnessId === 'opencode') {
11981
12025
  this.purgeOpenCodeStreamStateForSession(projectIdArg, sessionIdArg);
11982
12026
  } else if (sessionIdArg.harnessId === 'flex') {
@@ -13703,9 +13747,10 @@ export class OpenCodeController {
13703
13747
  || session.continueState === 'pending'
13704
13748
  || session.continueState === 'failed')
13705
13749
  ));
13706
- this.upgradeStatus = transaction.targetVersion ? {
13750
+ const targetVersion = upgradeTransactionTargetVersion(transaction);
13751
+ this.upgradeStatus = targetVersion ? {
13707
13752
  fromVersion: transaction.sourceVersion,
13708
- toVersion: transaction.targetVersion,
13753
+ toVersion: targetVersion,
13709
13754
  phase: 'continuing',
13710
13755
  } : this.upgradeStatus;
13711
13756
  await coordinatorArg.mutateTransaction(tokenArg, (current) => ({
@@ -13750,9 +13795,10 @@ export class OpenCodeController {
13750
13795
  this.harnessOperationManager.endUpgradeBarrier(this.upgradeBarrier);
13751
13796
  this.upgradeBarrier = undefined;
13752
13797
  }
13753
- this.upgradeStatus = transaction.targetVersion ? {
13798
+ const targetVersion = upgradeTransactionTargetVersion(transaction);
13799
+ this.upgradeStatus = targetVersion ? {
13754
13800
  fromVersion: transaction.sourceVersion,
13755
- toVersion: transaction.targetVersion,
13801
+ toVersion: targetVersion,
13756
13802
  phase: failed > 0 || modeArg === 'compensate' ? 'failed' : 'completed',
13757
13803
  } : this.upgradeStatus;
13758
13804
  if (this.upgradeStatus) {