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,7 @@ import { ProtocolV6DocumentMigration } from './v6_protocoldocumentmigration.js';
3
3
  import { ProtocolV18ResourceLayoutMigration } from './v18_resourcelayoutmigration.js';
4
4
  import { ProtocolV22ProjectFilesystemIdentityMigration } from './v22_projectfilesystemidentitymigration.js';
5
5
  import { RuntimeConfigV23Migration } from './v23_runtimeconfig.js';
6
+ import { LegacyTerminalLayoutV24Migration } from './v24_legacyterminallayoutmigration.js';
6
7
 
7
8
  interface IDocumentMigration {
8
9
  run(): Promise<void>;
@@ -14,6 +15,7 @@ export class PostDatabaseDocumentMigrationRunner {
14
15
  constructor(private readonly database: plugins.smartdata.SmartdataDb | undefined) {
15
16
  this.migrations = [
16
17
  new RuntimeConfigV23Migration(this.database),
18
+ new LegacyTerminalLayoutV24Migration(this.database),
17
19
  new ProtocolV6DocumentMigration(this.database),
18
20
  new ProtocolV18ResourceLayoutMigration(this.database),
19
21
  ];
@@ -12,3 +12,5 @@ export * from './v19_flexprojectmanagement.js';
12
12
  export * from './v22_projectfilesystemidentitymigration.js';
13
13
  export * from './v23_runtimeconfig.js';
14
14
  export * from './v23_aglhome.js';
15
+ export * from './v24_legacyterminallayout.js';
16
+ export * from './v24_legacyterminallayoutmigration.js';
@@ -0,0 +1,131 @@
1
+ type TObject = Record<string, unknown>;
2
+
3
+ const legacyOpenCodePtyIdPattern = /^pty_[0-9a-f]{12}[0-9A-Za-z]{14}$/;
4
+ const legacyControllerTerminalIdPattern = /^terminal_[A-Za-z0-9_-]{22}$/;
5
+
6
+ export interface IV24LegacySessionGroup {
7
+ id: string;
8
+ name: string;
9
+ sessionIds: unknown[];
10
+ }
11
+
12
+ export interface IV24LegacySessionGroupsDocument {
13
+ id: string;
14
+ controllerId: string;
15
+ projectId: string;
16
+ groups: IV24LegacySessionGroup[];
17
+ ungroupedSessionIds?: unknown[];
18
+ revision?: number;
19
+ }
20
+
21
+ export interface IV24LayoutMigrationResult {
22
+ document: IV24LegacySessionGroupsDocument;
23
+ migrated: boolean;
24
+ }
25
+
26
+ const isPlainObject = (valueArg: unknown): valueArg is TObject => {
27
+ if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg)) return false;
28
+ const prototype = Object.getPrototypeOf(valueArg);
29
+ return prototype === Object.prototype || prototype === null;
30
+ };
31
+
32
+ const hasExactKeys = (
33
+ valueArg: TObject,
34
+ requiredArg: readonly string[],
35
+ optionalArg: readonly string[] = [],
36
+ ): boolean => {
37
+ const allowed = new Set([...requiredArg, ...optionalArg]);
38
+ return requiredArg.every((key) => Object.hasOwn(valueArg, key))
39
+ && Object.keys(valueArg).every((key) => allowed.has(key));
40
+ };
41
+
42
+ const isCanonicalControllerTerminalId = (valueArg: string): boolean => {
43
+ if (!legacyControllerTerminalIdPattern.test(valueArg)) return false;
44
+ const suffix = valueArg.slice('terminal_'.length);
45
+ const decoded = Buffer.from(suffix, 'base64url');
46
+ return decoded.length === 16 && decoded.toString('base64url') === suffix;
47
+ };
48
+
49
+ const isLegacyTerminalNativeId = (valueArg: unknown): valueArg is string => (
50
+ typeof valueArg === 'string'
51
+ && (
52
+ legacyOpenCodePtyIdPattern.test(valueArg)
53
+ || isCanonicalControllerTerminalId(valueArg)
54
+ )
55
+ );
56
+
57
+ const isLegacyTerminalLayoutReference = (valueArg: unknown): boolean => {
58
+ if (typeof valueArg === 'string') return isLegacyTerminalNativeId(valueArg);
59
+ if (
60
+ !isPlainObject(valueArg)
61
+ || Object.keys(valueArg).length !== 2
62
+ || !Object.hasOwn(valueArg, 'harnessId')
63
+ || !Object.hasOwn(valueArg, 'nativeId')
64
+ || typeof valueArg.nativeId !== 'string'
65
+ ) return false;
66
+ if (valueArg.harnessId === 'controller') return true;
67
+ return valueArg.harnessId === 'opencode' && isLegacyTerminalNativeId(valueArg.nativeId);
68
+ };
69
+
70
+ export const migrateV24LegacyTerminalLayoutDocument = (
71
+ valueArg: unknown,
72
+ ): IV24LayoutMigrationResult => {
73
+ if (
74
+ !isPlainObject(valueArg)
75
+ || !hasExactKeys(
76
+ valueArg,
77
+ ['id', 'controllerId', 'projectId', 'groups'],
78
+ ['ungroupedSessionIds', 'revision'],
79
+ )
80
+ || !Array.isArray(valueArg.groups)
81
+ ) {
82
+ throw new Error('Invalid legacy terminal layout migration document.');
83
+ }
84
+
85
+ const sourceGroups = valueArg.groups;
86
+ const groups = sourceGroups.map((groupArg) => {
87
+ if (
88
+ !isPlainObject(groupArg)
89
+ || !hasExactKeys(groupArg, ['id', 'name', 'sessionIds'])
90
+ || !Array.isArray(groupArg.sessionIds)
91
+ ) {
92
+ throw new Error('Invalid legacy terminal layout group.');
93
+ }
94
+ return {
95
+ ...groupArg,
96
+ sessionIds: groupArg.sessionIds.filter((sessionId) => (
97
+ !isLegacyTerminalLayoutReference(sessionId)
98
+ )),
99
+ } as unknown as IV24LegacySessionGroup;
100
+ });
101
+
102
+ const hasUngroupedSessionIds = Object.hasOwn(valueArg, 'ungroupedSessionIds');
103
+ if (hasUngroupedSessionIds && !Array.isArray(valueArg.ungroupedSessionIds)) {
104
+ throw new Error('Invalid legacy terminal ungrouped layout list.');
105
+ }
106
+ const ungroupedSessionIds = hasUngroupedSessionIds
107
+ ? (valueArg.ungroupedSessionIds as unknown[]).filter((sessionId) => (
108
+ !isLegacyTerminalLayoutReference(sessionId)
109
+ ))
110
+ : undefined;
111
+ const migrated = groups.some((group, index) => (
112
+ group.sessionIds.length !== (sourceGroups[index] as TObject & { sessionIds: unknown[] }).sessionIds.length
113
+ )) || (
114
+ hasUngroupedSessionIds
115
+ && ungroupedSessionIds!.length !== (valueArg.ungroupedSessionIds as unknown[]).length
116
+ );
117
+ if (!migrated) {
118
+ return {
119
+ document: valueArg as unknown as IV24LegacySessionGroupsDocument,
120
+ migrated: false,
121
+ };
122
+ }
123
+ return {
124
+ document: {
125
+ ...valueArg,
126
+ groups,
127
+ ...(hasUngroupedSessionIds ? { ungroupedSessionIds } : {}),
128
+ } as unknown as IV24LegacySessionGroupsDocument,
129
+ migrated: true,
130
+ };
131
+ };
@@ -0,0 +1,88 @@
1
+ import * as plugins from '../ts/plugins.js';
2
+ import {
3
+ assertControllerSessionGroupsDocument,
4
+ ControllerSessionGroupsModel,
5
+ } from '../ts/classes.authmodels.js';
6
+ import { AuthError } from '../ts/interfaces.auth.js';
7
+ import type { IControllerSessionGroupsDocument } from '../ts/interfaces.projects.js';
8
+ import { migrateProtocolV6SessionGroupsDocument } from './v6_protocoldocuments.js';
9
+ import { migrateV24LegacyTerminalLayoutDocument } from './v24_legacyterminallayout.js';
10
+ import type { IV24LegacySessionGroupsDocument } from './v24_legacyterminallayout.js';
11
+
12
+ const migrationPageLimit = 128;
13
+
14
+ const persistedBodyFromRawDocument = (
15
+ valueArg: Record<string, unknown>,
16
+ ): IV24LegacySessionGroupsDocument => {
17
+ const body = { ...valueArg };
18
+ delete body._id;
19
+ delete body._smartdataRevision;
20
+ return body as unknown as IV24LegacySessionGroupsDocument;
21
+ };
22
+
23
+ const assertProtocolV6NormalizableLayout = (
24
+ valueArg: IV24LegacySessionGroupsDocument,
25
+ ): void => {
26
+ const normalized = migrateProtocolV6SessionGroupsDocument(valueArg);
27
+ assertControllerSessionGroupsDocument(normalized.document);
28
+ };
29
+
30
+ export class LegacyTerminalLayoutV24Migration {
31
+ constructor(private readonly database: plugins.smartdata.SmartdataDb | undefined) {}
32
+
33
+ public async run(): Promise<void> {
34
+ if (!this.database) {
35
+ throw new AuthError('not_initialized', 'The authentication store database is unavailable.');
36
+ }
37
+ const collection = ControllerSessionGroupsModel.collection.mongoDbCollection;
38
+ let lastId: plugins.smartdata.TStoredDocument<IControllerSessionGroupsDocument>['_id'] | undefined;
39
+ while (true) {
40
+ const cursor = collection.find(lastId ? { _id: { $gt: lastId } } : {})
41
+ .sort({ _id: 1 })
42
+ .limit(migrationPageLimit);
43
+ let documents: Awaited<ReturnType<typeof cursor.toArray>>;
44
+ try {
45
+ documents = await cursor.toArray();
46
+ } finally {
47
+ await cursor.close();
48
+ }
49
+ for (const raw of documents) {
50
+ const migration = migrateV24LegacyTerminalLayoutDocument(
51
+ persistedBodyFromRawDocument(raw),
52
+ );
53
+ if (!migration.migrated) continue;
54
+ assertProtocolV6NormalizableLayout(migration.document);
55
+ const selector = raw._smartdataRevision === undefined
56
+ ? { _id: raw._id, _smartdataRevision: { $exists: false } }
57
+ : { _id: raw._id, _smartdataRevision: raw._smartdataRevision };
58
+ const replaced = await collection.findOneAndReplace(
59
+ selector,
60
+ { _id: raw._id, ...migration.document, _smartdataRevision: plugins.crypto.randomUUID() },
61
+ { returnDocument: 'after', includeResultMetadata: false, upsert: false },
62
+ );
63
+ if (!replaced) {
64
+ const concurrent = await collection.findOne({ _id: raw._id });
65
+ if (!concurrent) {
66
+ throw new AuthError(
67
+ 'concurrent_change',
68
+ 'The legacy terminal layout migration changed concurrently.',
69
+ );
70
+ }
71
+ const remaining = migrateV24LegacyTerminalLayoutDocument(
72
+ persistedBodyFromRawDocument(concurrent),
73
+ );
74
+ assertProtocolV6NormalizableLayout(remaining.document);
75
+ if (remaining.migrated) {
76
+ throw new AuthError(
77
+ 'concurrent_change',
78
+ 'The legacy terminal layout migration changed concurrently.',
79
+ );
80
+ }
81
+ }
82
+ }
83
+ const last = documents.at(-1);
84
+ if (!last || documents.length < migrationPageLimit) break;
85
+ lastId = last._id;
86
+ }
87
+ }
88
+ }
@@ -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
  }
@@ -796,6 +796,9 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
796
796
  @plugins.deesElement.state()
797
797
  accessor workspaceError = '';
798
798
 
799
+ @plugins.deesElement.state()
800
+ accessor sessionLayoutError = '';
801
+
799
802
  @plugins.deesElement.state()
800
803
  accessor workspaceNotice = '';
801
804
 
@@ -1403,6 +1406,11 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
1403
1406
  line-height: 1.4;
1404
1407
  }
1405
1408
 
1409
+ .sessionLayoutError {
1410
+ flex: 0 0 auto;
1411
+ margin: 0 var(--dees-spacing-sm) var(--dees-spacing-xs);
1412
+ }
1413
+
1406
1414
  .sidebarHeader {
1407
1415
  display: flex;
1408
1416
  align-items: center;
@@ -2042,6 +2050,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2042
2050
  this.draftSessionActive = false;
2043
2051
  this.activeMutationId = undefined;
2044
2052
  this.workspaceError = '';
2053
+ this.sessionLayoutError = '';
2045
2054
  this.workspaceNotice = '';
2046
2055
  this.clearAnsweredCardCaches();
2047
2056
  this.clearSubagentModalState();
@@ -2565,6 +2574,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
2565
2574
  this.invalidateDetailLoad();
2566
2575
  this.invalidateSlashCatalog();
2567
2576
  this.sessionDetail = undefined;
2577
+ this.sessionLayoutError = '';
2568
2578
  }
2569
2579
  } catch (error) {
2570
2580
  if (requestId === this.projectsRequestId && this.isCurrentConnection(generation)) {
@@ -7241,8 +7251,17 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
7241
7251
  this.confirmedSessionLayouts.set(projectId, layout);
7242
7252
  this.applySessionLayout(layout);
7243
7253
  this.groupsLoadedForProjectId = projectId;
7244
- } catch {
7245
- // Groups are sidebar decoration; the chat list works without them.
7254
+ this.sessionLayoutError = '';
7255
+ } catch (error) {
7256
+ if (
7257
+ requestId === this.layoutRequestId
7258
+ && this.authenticated
7259
+ && this.isCurrentConnection(generation)
7260
+ && this.selectedProjectId === projectId
7261
+ && this.projects.some((project) => project.id === projectId)
7262
+ ) {
7263
+ this.sessionLayoutError = `Conversation groups could not be loaded: ${errorMessage(error)}`;
7264
+ }
7246
7265
  }
7247
7266
  }
7248
7267
 
@@ -8183,6 +8202,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
8183
8202
  this.sessionGroups = [];
8184
8203
  this.ungroupedSessionIds = [];
8185
8204
  this.groupsLoadedForProjectId = '';
8205
+ this.sessionLayoutError = '';
8186
8206
  this.terminals = [];
8187
8207
  this.resources = [];
8188
8208
  this.selectedResourceId = '';
@@ -8264,6 +8284,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
8264
8284
  this.draftSessionDetailReadyId = undefined;
8265
8285
  this.draftSessionActive = false;
8266
8286
  this.selectedProjectId = '';
8287
+ this.sessionLayoutError = '';
8267
8288
  this.selectedSessionId = undefined;
8268
8289
  this.invalidateDetailLoad();
8269
8290
  this.invalidateSlashCatalog();
@@ -8372,6 +8393,7 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
8372
8393
  this.draftSessionDetailReadyId = undefined;
8373
8394
  this.draftSessionActive = false;
8374
8395
  this.selectedProjectId = response.project.id;
8396
+ this.sessionLayoutError = '';
8375
8397
  this.selectedSessionId = undefined;
8376
8398
  this.invalidateDetailLoad();
8377
8399
  this.invalidateSlashCatalog();
@@ -10545,6 +10567,14 @@ export class HarnessControllerApp extends plugins.deesElement.DeesElement {
10545
10567
  New
10546
10568
  </dees-button>
10547
10569
  </div>
10570
+ ${this.sessionLayoutError
10571
+ ? plugins.deesElement.html`
10572
+ <div class="projectCreateError sessionLayoutError" role="alert">
10573
+ <dees-icon icon="lucide:TriangleAlert" iconSize="13"></dees-icon>
10574
+ <span>${this.sessionLayoutError}</span>
10575
+ </div>
10576
+ `
10577
+ : ''}
10548
10578
  <dees-harness-session-list
10549
10579
  .sessions=${sessionMetas}
10550
10580
  .selectedSessionId=${selectedUiKey}