agl 22.0.0 → 22.0.1

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,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
  export class PostDatabaseDocumentMigrationRunner {
7
8
  database;
8
9
  migrations;
@@ -10,6 +11,7 @@ export class PostDatabaseDocumentMigrationRunner {
10
11
  this.database = database;
11
12
  this.migrations = [
12
13
  new RuntimeConfigV23Migration(this.database),
14
+ new LegacyTerminalLayoutV24Migration(this.database),
13
15
  new ProtocolV6DocumentMigration(this.database),
14
16
  new ProtocolV18ResourceLayoutMigration(this.database),
15
17
  ];
@@ -23,4 +25,4 @@ export class PostDatabaseDocumentMigrationRunner {
23
25
  await new ProtocolV22ProjectFilesystemIdentityMigration(this.database, controllerIdArg).run();
24
26
  }
25
27
  }
26
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5kb2N1bWVudG1pZ3JhdGlvbnJ1bm5lci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX21pZ3JhdGlvbi9jbGFzc2VzLmRvY3VtZW50bWlncmF0aW9ucnVubmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sbUNBQW1DLENBQUM7QUFDaEYsT0FBTyxFQUFFLGtDQUFrQyxFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDdEYsT0FBTyxFQUFFLDZDQUE2QyxFQUFFLE1BQU0sNkNBQTZDLENBQUM7QUFDNUcsT0FBTyxFQUFFLHlCQUF5QixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFNbkUsTUFBTSxPQUFPLG1DQUFtQztJQUdqQjtJQUZaLFVBQVUsQ0FBdUI7SUFFbEQsWUFBNkIsUUFBbUQ7UUFBbkQsYUFBUSxHQUFSLFFBQVEsQ0FBMkM7UUFDOUUsSUFBSSxDQUFDLFVBQVUsR0FBRztZQUNoQixJQUFJLHlCQUF5QixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUM7WUFDNUMsSUFBSSwyQkFBMkIsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDO1lBQzlDLElBQUksa0NBQWtDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQztTQUN0RCxDQUFDO0lBQ0osQ0FBQztJQUVNLEtBQUssQ0FBQyxHQUFHO1FBQ2QsS0FBSyxNQUFNLFNBQVMsSUFBSSxJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDeEMsTUFBTSxTQUFTLENBQUMsR0FBRyxFQUFFLENBQUM7UUFDeEIsQ0FBQztJQUNILENBQUM7SUFFTSxLQUFLLENBQUMsNEJBQTRCLENBQUMsZUFBdUI7UUFDL0QsTUFBTSxJQUFJLDZDQUE2QyxDQUNyRCxJQUFJLENBQUMsUUFBUSxFQUNiLGVBQWUsQ0FDaEIsQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUNWLENBQUM7Q0FDRiJ9
28
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5kb2N1bWVudG1pZ3JhdGlvbnJ1bm5lci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX21pZ3JhdGlvbi9jbGFzc2VzLmRvY3VtZW50bWlncmF0aW9ucnVubmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sbUNBQW1DLENBQUM7QUFDaEYsT0FBTyxFQUFFLGtDQUFrQyxFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDdEYsT0FBTyxFQUFFLDZDQUE2QyxFQUFFLE1BQU0sNkNBQTZDLENBQUM7QUFDNUcsT0FBTyxFQUFFLHlCQUF5QixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDbkUsT0FBTyxFQUFFLGdDQUFnQyxFQUFFLE1BQU0sd0NBQXdDLENBQUM7QUFNMUYsTUFBTSxPQUFPLG1DQUFtQztJQUdqQjtJQUZaLFVBQVUsQ0FBdUI7SUFFbEQsWUFBNkIsUUFBbUQ7UUFBbkQsYUFBUSxHQUFSLFFBQVEsQ0FBMkM7UUFDOUUsSUFBSSxDQUFDLFVBQVUsR0FBRztZQUNoQixJQUFJLHlCQUF5QixDQUFDLElBQUksQ0FBQyxRQUFRLENBQUM7WUFDNUMsSUFBSSxnQ0FBZ0MsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDO1lBQ25ELElBQUksMkJBQTJCLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQztZQUM5QyxJQUFJLGtDQUFrQyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUM7U0FDdEQsQ0FBQztJQUNKLENBQUM7SUFFTSxLQUFLLENBQUMsR0FBRztRQUNkLEtBQUssTUFBTSxTQUFTLElBQUksSUFBSSxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQ3hDLE1BQU0sU0FBUyxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3hCLENBQUM7SUFDSCxDQUFDO0lBRU0sS0FBSyxDQUFDLDRCQUE0QixDQUFDLGVBQXVCO1FBQy9ELE1BQU0sSUFBSSw2Q0FBNkMsQ0FDckQsSUFBSSxDQUFDLFFBQVEsRUFDYixlQUFlLENBQ2hCLENBQUMsR0FBRyxFQUFFLENBQUM7SUFDVixDQUFDO0NBQ0YifQ==
@@ -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';
@@ -12,4 +12,6 @@ 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
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19taWdyYXRpb24vaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLGtDQUFrQyxDQUFDO0FBQ2pELGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLGlDQUFpQyxDQUFDO0FBQ2hELGNBQWMsOEJBQThCLENBQUM7QUFDN0MsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYyw2Q0FBNkMsQ0FBQztBQUM1RCxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsNkNBQTZDLENBQUM7QUFDNUQsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLGtCQUFrQixDQUFDIn0=
15
+ export * from './v24_legacyterminallayout.js';
16
+ export * from './v24_legacyterminallayoutmigration.js';
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19taWdyYXRpb24vaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLGtDQUFrQyxDQUFDO0FBQ2pELGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLGlDQUFpQyxDQUFDO0FBQ2hELGNBQWMsOEJBQThCLENBQUM7QUFDN0MsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYyw2Q0FBNkMsQ0FBQztBQUM1RCxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsNkNBQTZDLENBQUM7QUFDNUQsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyx3Q0FBd0MsQ0FBQyJ9
@@ -0,0 +1,18 @@
1
+ export interface IV24LegacySessionGroup {
2
+ id: string;
3
+ name: string;
4
+ sessionIds: unknown[];
5
+ }
6
+ export interface IV24LegacySessionGroupsDocument {
7
+ id: string;
8
+ controllerId: string;
9
+ projectId: string;
10
+ groups: IV24LegacySessionGroup[];
11
+ ungroupedSessionIds?: unknown[];
12
+ revision?: number;
13
+ }
14
+ export interface IV24LayoutMigrationResult {
15
+ document: IV24LegacySessionGroupsDocument;
16
+ migrated: boolean;
17
+ }
18
+ export declare const migrateV24LegacyTerminalLayoutDocument: (valueArg: unknown) => IV24LayoutMigrationResult;
@@ -0,0 +1,79 @@
1
+ const legacyOpenCodePtyIdPattern = /^pty_[0-9a-f]{12}[0-9A-Za-z]{14}$/;
2
+ const legacyControllerTerminalIdPattern = /^terminal_[A-Za-z0-9_-]{22}$/;
3
+ const isPlainObject = (valueArg) => {
4
+ if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg))
5
+ return false;
6
+ const prototype = Object.getPrototypeOf(valueArg);
7
+ return prototype === Object.prototype || prototype === null;
8
+ };
9
+ const hasExactKeys = (valueArg, requiredArg, optionalArg = []) => {
10
+ const allowed = new Set([...requiredArg, ...optionalArg]);
11
+ return requiredArg.every((key) => Object.hasOwn(valueArg, key))
12
+ && Object.keys(valueArg).every((key) => allowed.has(key));
13
+ };
14
+ const isCanonicalControllerTerminalId = (valueArg) => {
15
+ if (!legacyControllerTerminalIdPattern.test(valueArg))
16
+ return false;
17
+ const suffix = valueArg.slice('terminal_'.length);
18
+ const decoded = Buffer.from(suffix, 'base64url');
19
+ return decoded.length === 16 && decoded.toString('base64url') === suffix;
20
+ };
21
+ const isLegacyTerminalNativeId = (valueArg) => (typeof valueArg === 'string'
22
+ && (legacyOpenCodePtyIdPattern.test(valueArg)
23
+ || isCanonicalControllerTerminalId(valueArg)));
24
+ const isLegacyTerminalLayoutReference = (valueArg) => {
25
+ if (typeof valueArg === 'string')
26
+ return isLegacyTerminalNativeId(valueArg);
27
+ if (!isPlainObject(valueArg)
28
+ || Object.keys(valueArg).length !== 2
29
+ || !Object.hasOwn(valueArg, 'harnessId')
30
+ || !Object.hasOwn(valueArg, 'nativeId')
31
+ || typeof valueArg.nativeId !== 'string')
32
+ return false;
33
+ if (valueArg.harnessId === 'controller')
34
+ return true;
35
+ return valueArg.harnessId === 'opencode' && isLegacyTerminalNativeId(valueArg.nativeId);
36
+ };
37
+ export const migrateV24LegacyTerminalLayoutDocument = (valueArg) => {
38
+ if (!isPlainObject(valueArg)
39
+ || !hasExactKeys(valueArg, ['id', 'controllerId', 'projectId', 'groups'], ['ungroupedSessionIds', 'revision'])
40
+ || !Array.isArray(valueArg.groups)) {
41
+ throw new Error('Invalid legacy terminal layout migration document.');
42
+ }
43
+ const sourceGroups = valueArg.groups;
44
+ const groups = sourceGroups.map((groupArg) => {
45
+ if (!isPlainObject(groupArg)
46
+ || !hasExactKeys(groupArg, ['id', 'name', 'sessionIds'])
47
+ || !Array.isArray(groupArg.sessionIds)) {
48
+ throw new Error('Invalid legacy terminal layout group.');
49
+ }
50
+ return {
51
+ ...groupArg,
52
+ sessionIds: groupArg.sessionIds.filter((sessionId) => (!isLegacyTerminalLayoutReference(sessionId))),
53
+ };
54
+ });
55
+ const hasUngroupedSessionIds = Object.hasOwn(valueArg, 'ungroupedSessionIds');
56
+ if (hasUngroupedSessionIds && !Array.isArray(valueArg.ungroupedSessionIds)) {
57
+ throw new Error('Invalid legacy terminal ungrouped layout list.');
58
+ }
59
+ const ungroupedSessionIds = hasUngroupedSessionIds
60
+ ? valueArg.ungroupedSessionIds.filter((sessionId) => (!isLegacyTerminalLayoutReference(sessionId)))
61
+ : undefined;
62
+ const migrated = groups.some((group, index) => (group.sessionIds.length !== sourceGroups[index].sessionIds.length)) || (hasUngroupedSessionIds
63
+ && ungroupedSessionIds.length !== valueArg.ungroupedSessionIds.length);
64
+ if (!migrated) {
65
+ return {
66
+ document: valueArg,
67
+ migrated: false,
68
+ };
69
+ }
70
+ return {
71
+ document: {
72
+ ...valueArg,
73
+ groups,
74
+ ...(hasUngroupedSessionIds ? { ungroupedSessionIds } : {}),
75
+ },
76
+ migrated: true,
77
+ };
78
+ };
79
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidjI0X2xlZ2FjeXRlcm1pbmFsbGF5b3V0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfbWlncmF0aW9uL3YyNF9sZWdhY3l0ZXJtaW5hbGxheW91dC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxNQUFNLDBCQUEwQixHQUFHLG1DQUFtQyxDQUFDO0FBQ3ZFLE1BQU0saUNBQWlDLEdBQUcsOEJBQThCLENBQUM7QUFzQnpFLE1BQU0sYUFBYSxHQUFHLENBQUMsUUFBaUIsRUFBdUIsRUFBRTtJQUMvRCxJQUFJLE9BQU8sUUFBUSxLQUFLLFFBQVEsSUFBSSxRQUFRLEtBQUssSUFBSSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDL0YsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLGNBQWMsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUNsRCxPQUFPLFNBQVMsS0FBSyxNQUFNLENBQUMsU0FBUyxJQUFJLFNBQVMsS0FBSyxJQUFJLENBQUM7QUFDOUQsQ0FBQyxDQUFDO0FBRUYsTUFBTSxZQUFZLEdBQUcsQ0FDbkIsUUFBaUIsRUFDakIsV0FBOEIsRUFDOUIsY0FBaUMsRUFBRSxFQUMxQixFQUFFO0lBQ1gsTUFBTSxPQUFPLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxHQUFHLFdBQVcsRUFBRSxHQUFHLFdBQVcsQ0FBQyxDQUFDLENBQUM7SUFDMUQsT0FBTyxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLFFBQVEsRUFBRSxHQUFHLENBQUMsQ0FBQztXQUMxRCxNQUFNLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0FBQzlELENBQUMsQ0FBQztBQUVGLE1BQU0sK0JB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
@@ -0,0 +1,6 @@
1
+ import * as plugins from '../dist_ts/plugins.js';
2
+ export declare class LegacyTerminalLayoutV24Migration {
3
+ private readonly database;
4
+ constructor(database: plugins.smartdata.SmartdataDb | undefined);
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,67 @@
1
+ import * as plugins from '../dist_ts/plugins.js';
2
+ import { assertControllerSessionGroupsDocument, ControllerSessionGroupsModel, } from '../dist_ts/classes.authmodels.js';
3
+ import { AuthError } from '../dist_ts/interfaces.auth.js';
4
+ import { migrateProtocolV6SessionGroupsDocument } from './v6_protocoldocuments.js';
5
+ import { migrateV24LegacyTerminalLayoutDocument } from './v24_legacyterminallayout.js';
6
+ const migrationPageLimit = 128;
7
+ const persistedBodyFromRawDocument = (valueArg) => {
8
+ const body = { ...valueArg };
9
+ delete body._id;
10
+ delete body._smartdataRevision;
11
+ return body;
12
+ };
13
+ const assertProtocolV6NormalizableLayout = (valueArg) => {
14
+ const normalized = migrateProtocolV6SessionGroupsDocument(valueArg);
15
+ assertControllerSessionGroupsDocument(normalized.document);
16
+ };
17
+ export class LegacyTerminalLayoutV24Migration {
18
+ database;
19
+ constructor(database) {
20
+ this.database = database;
21
+ }
22
+ async run() {
23
+ if (!this.database) {
24
+ throw new AuthError('not_initialized', 'The authentication store database is unavailable.');
25
+ }
26
+ const collection = ControllerSessionGroupsModel.collection.mongoDbCollection;
27
+ let lastId;
28
+ while (true) {
29
+ const cursor = collection.find(lastId ? { _id: { $gt: lastId } } : {})
30
+ .sort({ _id: 1 })
31
+ .limit(migrationPageLimit);
32
+ let documents;
33
+ try {
34
+ documents = await cursor.toArray();
35
+ }
36
+ finally {
37
+ await cursor.close();
38
+ }
39
+ for (const raw of documents) {
40
+ const migration = migrateV24LegacyTerminalLayoutDocument(persistedBodyFromRawDocument(raw));
41
+ if (!migration.migrated)
42
+ continue;
43
+ assertProtocolV6NormalizableLayout(migration.document);
44
+ const selector = raw._smartdataRevision === undefined
45
+ ? { _id: raw._id, _smartdataRevision: { $exists: false } }
46
+ : { _id: raw._id, _smartdataRevision: raw._smartdataRevision };
47
+ const replaced = await collection.findOneAndReplace(selector, { _id: raw._id, ...migration.document, _smartdataRevision: plugins.crypto.randomUUID() }, { returnDocument: 'after', includeResultMetadata: false, upsert: false });
48
+ if (!replaced) {
49
+ const concurrent = await collection.findOne({ _id: raw._id });
50
+ if (!concurrent) {
51
+ throw new AuthError('concurrent_change', 'The legacy terminal layout migration changed concurrently.');
52
+ }
53
+ const remaining = migrateV24LegacyTerminalLayoutDocument(persistedBodyFromRawDocument(concurrent));
54
+ assertProtocolV6NormalizableLayout(remaining.document);
55
+ if (remaining.migrated) {
56
+ throw new AuthError('concurrent_change', 'The legacy terminal layout migration changed concurrently.');
57
+ }
58
+ }
59
+ }
60
+ const last = documents.at(-1);
61
+ if (!last || documents.length < migrationPageLimit)
62
+ break;
63
+ lastId = last._id;
64
+ }
65
+ }
66
+ }
67
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidjI0X2xlZ2FjeXRlcm1pbmFsbGF5b3V0bWlncmF0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfbWlncmF0aW9uL3YyNF9sZWdhY3l0ZXJtaW5hbGxheW91dG1pZ3JhdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFDTCxxQ0FBcUMsRUFDckMsNEJBQTRCLEdBQzdCLE1BQU0sNkJBQTZCLENBQUM7QUFDckMsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBRXJELE9BQU8sRUFBRSxzQ0FBc0MsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQ25GLE9BQU8sRUFBRSxzQ0FBc0MsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBR3ZGLE1BQU0sa0JBQWtCLEdBQUcsR0FBRyxDQUFDO0FBRS9CLE1BQU0sNEJBQTRCLEdBQUcsQ0FDbkMsUUFBaUMsRUFDQSxFQUFFO0lBQ25DLE1BQU0sSUFBSSxHQUFHLEVBQUUsR0FBRyxRQUFRLEVBQUUsQ0FBQztJQUM3QixPQUFPLElBQUksQ0FBQyxHQUFHLENBQUM7SUFDaEIsT0FBTyxJQUFJLENBQUMsa0JBQWtCLENBQUM7SUFDL0IsT0FBTyxJQUFrRCxDQUFDO0FBQzVELENBQUMsQ0FBQztBQUVGLE1BQU0sa0NBQWtDLEdBQUcsQ0FDekMsUUFBeUMsRUFDbkMsRUFBRTtJQUNSLE1BQU0sVUFBVSxHQUFHLHNDQUFzQyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ3BFLHFDQUFxQyxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUM3RCxDQUFDLENBQUM7QUFFRixNQUFNLE9BQU8sZ0NBQWdDO0lBQ2Q7SUFBN0IsWUFBNkIsUUFBbUQ7UUFBbkQsYUFBUSxHQUFSLFFBQVEsQ0FBMkM7SUFBRyxDQUFDO0lBRTdFLEtBQUssQ0FBQyxHQUFHO1FBQ2QsSUFBSSxDQUFDLElBQUksQ0FB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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agl",
3
- "version": "22.0.0",
3
+ "version": "22.0.1",
4
4
  "controllerUpgradeManagementVersion": 2,
5
5
  "private": false,
6
6
  "description": "Agent Gateway Layer for OpenCode and FlexHarness sessions",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '22.0.0',
6
+ version: '22.0.1',
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[],
@@ -1378,6 +1378,13 @@ interface IControllerHarnessSessionEnumeration {
1378
1378
  flex: PromiseSettledResult<IControllerSession[]>;
1379
1379
  }
1380
1380
 
1381
+ interface IControllerHarnessSessionEnumerationTask {
1382
+ task: Promise<IControllerHarnessSessionEnumeration>;
1383
+ invalidated: boolean;
1384
+ settled: boolean;
1385
+ waiterCount: number;
1386
+ }
1387
+
1381
1388
  export class OpenCodeController {
1382
1389
  public readonly options: IControllerStartOptions;
1383
1390
 
@@ -1509,7 +1516,7 @@ export class OpenCodeController {
1509
1516
  private readonly activeOperations = new Set<Promise<unknown>>();
1510
1517
  private readonly harnessSessionEnumerationTasks = new Map<
1511
1518
  string,
1512
- Promise<IControllerHarnessSessionEnumeration>
1519
+ IControllerHarnessSessionEnumerationTask
1513
1520
  >();
1514
1521
  private readonly sessionLayoutOperationTails = new Map<string, Promise<void>>();
1515
1522
  private readonly sessionLayoutOperationDepths = new Map<string, number>();
@@ -2015,12 +2022,6 @@ export class OpenCodeController {
2015
2022
  );
2016
2023
  const startupProjectState = await this.reconcileProjectState(false);
2017
2024
  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
2025
  const startupSettings = await authStore.getSettings();
2025
2026
  this.defaultModels = startupSettings.defaultModels;
2026
2027
  this.autoAcceptPermissions = startupSettings.autoAcceptPermissions === true;
@@ -7360,31 +7361,55 @@ export class OpenCodeController {
7360
7361
  }
7361
7362
  }
7362
7363
 
7363
- private enumerateHarnessSessions(
7364
+ private async enumerateHarnessSessions(
7364
7365
  projectIdArg: string,
7365
7366
  directoryArg: string,
7366
7367
  signalArg: AbortSignal,
7367
7368
  ): 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) {
7369
+ while (true) {
7370
+ signalArg.throwIfAborted();
7371
+ let shared = this.harnessSessionEnumerationTasks.get(projectIdArg);
7372
+ if (!shared || shared.invalidated) {
7373
+ const lifecycleSignal = this.operationAbortController.signal;
7374
+ const operation = Promise.allSettled([
7375
+ Promise.resolve().then(() => (
7376
+ this.requireOpenCodeClient().listSessions(directoryArg, lifecycleSignal)
7377
+ )),
7378
+ Promise.resolve().then(() => this.listFlexSessions(projectIdArg, lifecycleSignal)),
7379
+ ]).then(([openCode, flex]) => ({ openCode, flex }));
7380
+ let taskRecord!: IControllerHarnessSessionEnumerationTask;
7381
+ let trackedTask!: Promise<IControllerHarnessSessionEnumeration>;
7382
+ trackedTask = this.trackOperationPromise(operation, () => {
7383
+ taskRecord.settled = true;
7384
+ const current = this.harnessSessionEnumerationTasks.get(projectIdArg);
7385
+ if (current === taskRecord && taskRecord.waiterCount === 0) {
7386
+ this.harnessSessionEnumerationTasks.delete(projectIdArg);
7387
+ }
7388
+ });
7389
+ taskRecord = {
7390
+ task: trackedTask,
7391
+ invalidated: false,
7392
+ settled: false,
7393
+ waiterCount: 0,
7394
+ };
7395
+ shared = taskRecord;
7396
+ this.harnessSessionEnumerationTasks.set(projectIdArg, shared);
7397
+ }
7398
+ shared.waiterCount += 1;
7399
+ try {
7400
+ const result = await this.waitForPromiseWithSignal(shared.task, signalArg);
7401
+ if (!shared.invalidated) return result;
7402
+ } finally {
7403
+ shared.waiterCount -= 1;
7404
+ if (
7405
+ shared.settled
7406
+ && shared.waiterCount === 0
7407
+ && this.harnessSessionEnumerationTasks.get(projectIdArg) === shared
7408
+ ) {
7381
7409
  this.harnessSessionEnumerationTasks.delete(projectIdArg);
7382
7410
  }
7383
- });
7384
- this.harnessSessionEnumerationTasks.set(projectIdArg, trackedTask);
7385
- sharedTask = trackedTask;
7411
+ }
7386
7412
  }
7387
- return this.waitForPromiseWithSignal(sharedTask, signalArg);
7388
7413
  }
7389
7414
 
7390
7415
  private toFlexSession(
@@ -11720,7 +11745,7 @@ export class OpenCodeController {
11720
11745
  signalArg?: AbortSignal,
11721
11746
  ): Promise<IControllerSession> {
11722
11747
  if (sessionIdArg.harnessId === 'opencode') {
11723
- return this.requireOpenCodeClient().getSession(
11748
+ return this.requireOpenCodeClient().getSessionSnapshot(
11724
11749
  directoryArg,
11725
11750
  sessionIdArg.nativeId,
11726
11751
  signalArg,
@@ -11977,6 +12002,9 @@ export class OpenCodeController {
11977
12002
  projectIdArg: string,
11978
12003
  sessionIdArg: IControllerRuntimeId,
11979
12004
  ): void {
12005
+ const enumeration = this.harnessSessionEnumerationTasks.get(projectIdArg);
12006
+ if (enumeration) enumeration.invalidated = true;
12007
+ this.harnessSessionEnumerationTasks.delete(projectIdArg);
11980
12008
  if (sessionIdArg.harnessId === 'opencode') {
11981
12009
  this.purgeOpenCodeStreamStateForSession(projectIdArg, sessionIdArg);
11982
12010
  } else if (sessionIdArg.harnessId === 'flex') {
@@ -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.1',
7
7
  description: 'Agent Gateway Layer for OpenCode and FlexHarness sessions'
8
8
  }