agl 40.6.0 → 41.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.
Files changed (90) hide show
  1. package/changelog.md +40 -0
  2. package/dist_serve/bundle.js +3514 -3357
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_serve/bundle.js.third-party-notices.json +58 -32
  5. package/dist_serve/bundle.js.third-party-notices.txt +108 -33
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/aglapi/aglapisurface.document.js +64 -37
  8. package/dist_ts/classes.controller.d.ts +8 -0
  9. package/dist_ts/classes.controller.js +110 -22
  10. package/dist_ts/classes.controllereventdelivery.d.ts +72 -0
  11. package/dist_ts/classes.controllereventdelivery.js +233 -0
  12. package/dist_ts/classes.flexmodels.public.js +4 -1
  13. package/dist_ts/classes.flexprojectionreader.d.ts +6 -0
  14. package/dist_ts/classes.flexprojectionreader.js +29 -5
  15. package/dist_ts/classes.flexservice.d.ts +2 -1
  16. package/dist_ts/classes.flexservice.js +34 -6
  17. package/dist_ts/classes.flexstore.d.ts +41 -4
  18. package/dist_ts/classes.flexstore.js +266 -203
  19. package/dist_ts/classes.mcpreexec.js +4 -4
  20. package/dist_ts/classes.ticketnotifier.js +4 -2
  21. package/dist_ts/classes.ticketstore.d.ts +50 -2
  22. package/dist_ts/classes.ticketstore.js +195 -78
  23. package/dist_ts/classes.workercallers.d.ts +2 -0
  24. package/dist_ts/classes.workercallers.js +11 -8
  25. package/dist_ts/driver/classes.drivereventrelay.d.ts +13 -1
  26. package/dist_ts/driver/classes.drivereventrelay.js +99 -6
  27. package/dist_ts/driver/functions.driverinstructions.d.ts +1 -1
  28. package/dist_ts/driver/functions.driverinstructions.js +1 -1
  29. package/dist_ts/driver/functions.drivertools.d.ts +11 -2
  30. package/dist_ts/driver/functions.drivertools.js +19 -9
  31. package/dist_ts/fleet/classes.boardpump.d.ts +8 -2
  32. package/dist_ts/fleet/classes.boardpump.js +38 -20
  33. package/dist_ts/fleet/classes.ticketeffects.d.ts +17 -2
  34. package/dist_ts/fleet/classes.ticketeffects.js +16 -10
  35. package/dist_ts/fleet/classes.workers.d.ts +4 -0
  36. package/dist_ts/fleet/classes.workers.js +37 -17
  37. package/dist_ts/fleet/functions.workervalidation.js +2 -2
  38. package/dist_ts/functions.flexprivatesnapshot.d.ts +102 -0
  39. package/dist_ts/functions.flexprivatesnapshot.js +461 -0
  40. package/dist_ts/functions.mcptickettools.d.ts +3 -0
  41. package/dist_ts/functions.mcptickettools.js +2 -2
  42. package/dist_ts/functions.ticketcli.d.ts +1 -1
  43. package/dist_ts/functions.ticketcli.js +8 -4
  44. package/dist_ts/functions.ticketvalidation.d.ts +3 -0
  45. package/dist_ts/functions.ticketvalidation.js +7 -4
  46. package/dist_ts/functions.workerprocessproof.d.ts +12 -3
  47. package/dist_ts/functions.workerprocessproof.js +17 -6
  48. package/dist_ts/interfaces.flexipc.d.ts +21 -2
  49. package/dist_ts/interfaces.flexipc.js +33 -16
  50. package/dist_ts_interfaces/fleetworkers.d.ts +6 -0
  51. package/dist_ts_interfaces/interfaces.d.ts +1 -1
  52. package/dist_ts_interfaces/interfaces.js +1 -1
  53. package/dist_ts_interfaces/requests.tickets.d.ts +8 -12
  54. package/dist_ts_interfaces/tickets.d.ts +37 -9
  55. package/dist_ts_interfaces/tickets.js +7 -1
  56. package/package.json +7 -7
  57. package/readme.md +62 -15
  58. package/ts/00_commitinfo_data.ts +1 -1
  59. package/ts/aglapi/aglapisurface.document.ts +63 -36
  60. package/ts/classes.controller.ts +132 -32
  61. package/ts/classes.controllereventdelivery.ts +289 -0
  62. package/ts/classes.flexmodels.public.ts +10 -0
  63. package/ts/classes.flexprojectionreader.ts +30 -3
  64. package/ts/classes.flexservice.ts +33 -5
  65. package/ts/classes.flexstore.ts +381 -246
  66. package/ts/classes.mcpreexec.ts +3 -3
  67. package/ts/classes.ticketnotifier.ts +3 -1
  68. package/ts/classes.ticketstore.ts +201 -75
  69. package/ts/classes.workercallers.ts +12 -7
  70. package/ts/driver/classes.drivereventrelay.ts +89 -4
  71. package/ts/driver/functions.driverinstructions.ts +1 -1
  72. package/ts/driver/functions.drivertools.ts +22 -8
  73. package/ts/fleet/classes.boardpump.ts +51 -19
  74. package/ts/fleet/classes.ticketeffects.ts +25 -11
  75. package/ts/fleet/classes.workers.ts +28 -16
  76. package/ts/fleet/functions.workervalidation.ts +1 -1
  77. package/ts/functions.flexprivatesnapshot.ts +601 -0
  78. package/ts/functions.mcptickettools.ts +1 -1
  79. package/ts/functions.ticketcli.ts +7 -3
  80. package/ts/functions.ticketvalidation.ts +7 -3
  81. package/ts/functions.workerprocessproof.ts +17 -3
  82. package/ts/interfaces.flexipc.ts +50 -21
  83. package/ts_interfaces/fleetworkers.ts +6 -0
  84. package/ts_interfaces/interfaces.ts +1 -1
  85. package/ts_interfaces/requests.tickets.ts +9 -6
  86. package/ts_interfaces/tickets.ts +32 -1
  87. package/ts_web/00_commitinfo_data.ts +1 -1
  88. package/ts_web/elements.harnesscontrollerapp.ts +19 -8
  89. package/ts_web/elements.ticketboard.ts +16 -4
  90. package/ts_web/settings.general.ts +10 -4
@@ -142,15 +142,26 @@ const processOwnsLoopbackConnection = async (connectionArg: IWorkerCallerConnect
142
142
  return matches.length === 1 && await processOwnsInode(pidArg, matches[0][9]);
143
143
  };
144
144
 
145
+ /**
146
+ * The arguments after its `cli.js` entry that an upgrade re-executes an `agl mcp` bridge of the controller on
147
+ * `controllerPortArg` with (`AglMcpReexecCoordinator`): the port pins the next image to its handover record's.
148
+ * A bridge proof accepts exactly these besides the approved command's own.
149
+ */
150
+ export const aglMcpReexecArguments = (controllerPortArg: number): string[] => ['mcp', '--port', String(controllerPortArg)];
151
+
145
152
  /**
146
153
  * The bridge process `bridgePidArg`, proved to hold the caller's end of the direct loopback connection and to run the
147
154
  * approved command, whatever its parent. The command is the one recorded when the launch was prepared: an upgrade
148
- * installs AGL under another path, and a command whose files are gone is refused as such.
155
+ * installs AGL under another path, and a command whose files are gone is refused as such. Its arguments are the
156
+ * approved ones or, once an upgrade re-executed the bridge in place, `aglMcpReexecArguments` for `controllerPortArg`,
157
+ * the port this controller is configured with, never one the bridge names: the connection proof already shows that
158
+ * the bridge talks to this controller.
149
159
  */
150
160
  export const readWorkerBridge = async (
151
161
  connectionArg: IWorkerCallerConnection | undefined,
152
162
  bridgePidArg: number,
153
163
  commandArg: IWorkerBridgeCommand,
164
+ controllerPortArg: number,
154
165
  ): Promise<IWorkerProcessSnapshot> => {
155
166
  for (const file of [commandArg.executable, commandArg.entry]) {
156
167
  let resolved: string;
@@ -165,8 +176,10 @@ export const readWorkerBridge = async (
165
176
  }
166
177
  const bridge = await readWorkerProcessSnapshot(bridgePidArg);
167
178
  const entry = await plugins.fs.promises.realpath(bridge.arguments[1] ?? '');
179
+ const argumentsTail = bridge.arguments.slice(2);
168
180
  if (bridge.executable !== commandArg.executable || entry !== commandArg.entry
169
- || !plugins.util.isDeepStrictEqual(bridge.arguments.slice(2), commandArg.arguments)) {
181
+ || (!plugins.util.isDeepStrictEqual(argumentsTail, commandArg.arguments)
182
+ && !plugins.util.isDeepStrictEqual(argumentsTail, aglMcpReexecArguments(controllerPortArg)))) {
170
183
  throw new WorkerCallerProofError('Worker bridge does not match the approved launch.');
171
184
  }
172
185
  return bridge;
@@ -177,8 +190,9 @@ export const verifyWorkerBridge = async (
177
190
  bridgePidArg: number,
178
191
  commandArg: IWorkerBridgeCommand,
179
192
  parentArg: IWorkerProcessSnapshot,
193
+ controllerPortArg: number,
180
194
  ): Promise<IWorkerProcessSnapshot> => {
181
- const bridge = await readWorkerBridge(connectionArg, bridgePidArg, commandArg);
195
+ const bridge = await readWorkerBridge(connectionArg, bridgePidArg, commandArg, controllerPortArg);
182
196
  if (bridge.parentPid !== parentArg.pid) throw new WorkerCallerProofError('Worker bridge does not match the approved launch.');
183
197
  if (!workerSnapshotsEqual(parentArg, await readWorkerProcessSnapshot(parentArg.pid))
184
198
  || !workerSnapshotsEqual(bridge, await readWorkerProcessSnapshot(bridge.pid))) {
@@ -23,6 +23,7 @@ import {
23
23
  type TFlexCreateSessionOptions = plugins.flexharness.IFlexCreateSessionOptions;
24
24
  type TFlexMessage = plugins.flexharness.IFlexMessage;
25
25
  type TFlexMessagePage = plugins.flexharness.IFlexMessagePage;
26
+ type TFlexMessageMetadataPage = plugins.flexharness.IFlexMessageMetadataPage;
26
27
  type TFlexPermissionRequest = plugins.flexharness.IFlexPermissionRequest;
27
28
  export type TFlexPromptQueueEntry = plugins.flexharness.IFlexPromptQueueEntry;
28
29
  type TFlexPromptOptions = plugins.flexharness.IFlexPromptOptions;
@@ -67,7 +68,7 @@ export type TFlexTurnReversionCaptureInspectionV2 =
67
68
  export type TFlexTurnReversionApplyInspection =
68
69
  plugins.flexharness.IFlexTurnReversionApplyInspection;
69
70
 
70
- export const flexIpcProtocolVersion = 38 as const;
71
+ export const flexIpcProtocolVersion = 39 as const;
71
72
  /**
72
73
  * The installation's own Flex runs, which belong to no project: the configuration projection's adapting run, and
73
74
  * Session Intelligence over a session outside every project.
@@ -307,6 +308,8 @@ export interface IFlexDriverRequest {
307
308
  export interface IFlexDriverResolution {
308
309
  instructions: string;
309
310
  notes: string;
311
+ /** The revision of the session scratchpad that holds the notes; names them as the driver's pinned context. */
312
+ notesRevision: number;
310
313
  }
311
314
  export const flexDriverToolNamePattern = /^[a-z][a-z0-9_]{0,63}$/u;
312
315
  export const flexDriverToolInputMaxBytes = 2 * 1024 * 1024;
@@ -363,7 +366,7 @@ export interface IFlexHostRequestMap {
363
366
  request: IFlexTurnReversionReleaseRequest;
364
367
  response: { released: true };
365
368
  };
366
- /** The controller-owned driver session's instructions and notes; null for every other session. */
369
+ /** The controller-owned driver session's instructions and notes with their revision; null for every other session. */
367
370
  'driver.resolve': {
368
371
  request: IFlexDriverRequest;
369
372
  response: { driver: IFlexDriverResolution | null };
@@ -836,6 +839,14 @@ export interface IFlexRequestMap {
836
839
  request: { scopeId: string; sessionId: string; limit?: number; before?: string; includeUndone?: boolean };
837
840
  response: TFlexMessagePage;
838
841
  };
842
+ /**
843
+ * The same pages with each message's identity and state only (no parts, model, usage or error):
844
+ * a whole transcript is scanned without reading its text or tool output.
845
+ */
846
+ 'message.metadata.page': {
847
+ request: { scopeId: string; sessionId: string; limit?: number; before?: string; includeUndone?: boolean };
848
+ response: TFlexMessageMetadataPage;
849
+ };
839
850
  'message.get': {
840
851
  request: { scopeId: string; sessionId: string; messageId: string; includeUndone?: boolean };
841
852
  response: TFlexMessage;
@@ -1358,6 +1369,7 @@ export const readOnlyFlexRequestMethods: ReadonlySet<TFlexRequestMethod> = new S
1358
1369
  'session.reversion.info',
1359
1370
  'slash.list',
1360
1371
  'message.page',
1372
+ 'message.metadata.page',
1361
1373
  'message.get',
1362
1374
  'prompt.get',
1363
1375
  'prompt.list',
@@ -1387,6 +1399,7 @@ const requestMethods: ReadonlySet<string> = new Set<TFlexRequestMethod>([
1387
1399
  'slash.list',
1388
1400
  'slash.execute',
1389
1401
  'message.page',
1402
+ 'message.metadata.page',
1390
1403
  'message.get',
1391
1404
  'prompt.start',
1392
1405
  'prompt.continue',
@@ -1919,28 +1932,22 @@ const isFlexModelLimit = (valueArg: unknown): valueArg is TFlexModelLimit => (
1919
1932
  isFlexPlainObject(valueArg) && plugins.flexModels.isModelLimitInfo(valueArg)
1920
1933
  );
1921
1934
 
1922
- export const isFlexPublicMessage = (valueArg: unknown): valueArg is TFlexMessage =>
1923
- isFlexPlainObject(valueArg)
1924
- && hasFlexExactKeys(
1925
- valueArg,
1926
- ['messageId', 'sessionId', 'runId', 'role', 'status', 'createdAt', 'parts'],
1927
- ['completedAt', 'model', 'usage', 'error', 'limit', 'rejectedPermissionId', 'steerId', 'cacheWarm', 'promptId', 'keptInContext', 'undone'],
1928
- )
1929
- && isIdentifier(valueArg.messageId)
1935
+ const flexMessageIdentityKeys = ['messageId', 'sessionId', 'runId', 'role', 'status', 'createdAt'] as const;
1936
+ const flexMessageOptionalIdentityKeys = ['completedAt', 'steerId', 'promptId', 'keptInContext', 'undone', 'cacheWarm'] as const;
1937
+ const flexMessageKeys = [...flexMessageIdentityKeys, 'parts'] as const;
1938
+ const flexMessageOptionalKeys = [
1939
+ ...flexMessageOptionalIdentityKeys, 'model', 'usage', 'error', 'limit', 'rejectedPermissionId',
1940
+ ] as const;
1941
+
1942
+ /** The identity and state a transcript message and its metadata share (`TFlexMessageMetadata`). */
1943
+ const hasFlexMessageIdentity = (valueArg: Record<string, unknown>): boolean =>
1944
+ isIdentifier(valueArg.messageId)
1930
1945
  && isIdentifier(valueArg.sessionId)
1931
1946
  && isIdentifier(valueArg.runId)
1932
1947
  && ['user', 'assistant'].includes(String(valueArg.role))
1933
1948
  && ['streaming', 'completed', 'failed', 'cancelled'].includes(String(valueArg.status))
1934
1949
  && isIsoDate(valueArg.createdAt)
1935
1950
  && (valueArg.completedAt === undefined || isIsoDate(valueArg.completedAt))
1936
- && Array.isArray(valueArg.parts)
1937
- && valueArg.parts.length <= 2000
1938
- && valueArg.parts.every((partArg) => isMessagePart(partArg))
1939
- && (valueArg.model === undefined || isModelIdentity(valueArg.model))
1940
- && (valueArg.usage === undefined || isUsage(valueArg.usage))
1941
- && (valueArg.error === undefined || isBoundedString(valueArg.error, 2048))
1942
- && (valueArg.limit === undefined || isFlexModelLimit(valueArg.limit))
1943
- && (valueArg.rejectedPermissionId === undefined || isIdentifier(valueArg.rejectedPermissionId))
1944
1951
  && (valueArg.steerId === undefined || (valueArg.role === 'user' && isIdentifier(valueArg.steerId)))
1945
1952
  && (valueArg.promptId === undefined || (valueArg.role === 'user' && isBoundedString(valueArg.promptId, 512)))
1946
1953
  && !(valueArg.promptId !== undefined && valueArg.steerId !== undefined)
@@ -1949,6 +1956,25 @@ export const isFlexPublicMessage = (valueArg: unknown): valueArg is TFlexMessage
1949
1956
  && (valueArg.undone === undefined || valueArg.undone === true)
1950
1957
  && (valueArg.cacheWarm === undefined || valueArg.cacheWarm === true);
1951
1958
 
1959
+ export const isFlexPublicMessage = (valueArg: unknown): valueArg is TFlexMessage =>
1960
+ isFlexPlainObject(valueArg)
1961
+ && hasFlexExactKeys(valueArg, flexMessageKeys, flexMessageOptionalKeys)
1962
+ && hasFlexMessageIdentity(valueArg)
1963
+ && Array.isArray(valueArg.parts)
1964
+ && valueArg.parts.length <= 2000
1965
+ && valueArg.parts.every((partArg) => isMessagePart(partArg))
1966
+ && (valueArg.model === undefined || isModelIdentity(valueArg.model))
1967
+ && (valueArg.usage === undefined || isUsage(valueArg.usage))
1968
+ && (valueArg.error === undefined || isBoundedString(valueArg.error, 2048))
1969
+ && (valueArg.limit === undefined || isFlexModelLimit(valueArg.limit))
1970
+ && (valueArg.rejectedPermissionId === undefined || isIdentifier(valueArg.rejectedPermissionId));
1971
+
1972
+ /** A transcript message's identity and state as `message.metadata.page` carries it: no content. */
1973
+ export const isFlexMessageMetadata = (valueArg: unknown): valueArg is plugins.flexharness.TFlexMessageMetadata =>
1974
+ isFlexPlainObject(valueArg)
1975
+ && hasFlexExactKeys(valueArg, flexMessageIdentityKeys, flexMessageOptionalIdentityKeys)
1976
+ && hasFlexMessageIdentity(valueArg);
1977
+
1952
1978
  export const isFlexPublicSession = (valueArg: unknown): valueArg is TFlexSession => {
1953
1979
  if (
1954
1980
  !isFlexPlainObject(valueArg)
@@ -2573,6 +2599,7 @@ export const isFlexRequestPayload = <TMethod extends TFlexRequestMethod>(
2573
2599
  && isScopeAndSession(valueArg)
2574
2600
  && isUpdateSessionOptions(valueArg.options);
2575
2601
  case 'message.page':
2602
+ case 'message.metadata.page':
2576
2603
  return hasFlexExactKeys(valueArg, ['scopeId', 'sessionId'], ['limit', 'before', 'includeUndone'])
2577
2604
  && isScopeAndSession(valueArg)
2578
2605
  && (valueArg.limit === undefined
@@ -3118,10 +3145,11 @@ export const isFlexHostResponseResult = <TMethod extends TFlexHostRequestMethod>
3118
3145
  if (!hasFlexExactKeys(valueArg, ['driver'])) return false;
3119
3146
  const driver = valueArg.driver;
3120
3147
  return driver === null || (isFlexPlainObject(driver)
3121
- && hasFlexExactKeys(driver, ['instructions', 'notes'])
3148
+ && hasFlexExactKeys(driver, ['instructions', 'notes', 'notesRevision'])
3122
3149
  && isBoundedString(driver.instructions, 64 * 1024)
3123
3150
  && isBoundedString(driver.notes, 128 * 1024, true)
3124
- && driver.notes.length <= 32_768);
3151
+ && driver.notes.length <= 32_768
3152
+ && isNonNegativeInteger(driver.notesRevision));
3125
3153
  }
3126
3154
  if (methodArg === 'driver.tool') {
3127
3155
  if (Object.hasOwn(valueArg, 'error')) {
@@ -3441,11 +3469,12 @@ export const isFlexResponseResult = <TMethod extends TFlexRequestMethod>(
3441
3469
  case 'slash.execute':
3442
3470
  return isSlashExecutionResult(valueArg);
3443
3471
  case 'message.page':
3472
+ case 'message.metadata.page':
3444
3473
  return isFlexPlainObject(valueArg)
3445
3474
  && hasFlexExactKeys(valueArg, ['messages', 'startIndex'], ['nextCursor'])
3446
3475
  && Array.isArray(valueArg.messages) && valueArg.messages.length <= 50
3447
3476
  && Number.isSafeInteger(valueArg.startIndex) && (valueArg.startIndex as number) >= 0
3448
- && valueArg.messages.every(isFlexPublicMessage)
3477
+ && valueArg.messages.every(methodArg === 'message.page' ? isFlexPublicMessage : isFlexMessageMetadata)
3449
3478
  && (valueArg.nextCursor === undefined || isBoundedString(valueArg.nextCursor, 4096));
3450
3479
  case 'message.get':
3451
3480
  return isFlexPublicMessage(valueArg);
@@ -228,6 +228,12 @@ export interface IFleetEvent {
228
228
  /** The native session (`harness:nativeId`) a ticket notice addressed. */
229
229
  sessionId?: string;
230
230
  reportId?: string;
231
+ /**
232
+ * The comment on `ticketKey` that holds this event's whole text, by the id the ticket board returned: a worker's report
233
+ * the fleet commented (`worker.report.mid_turn`, `worker.turn.ended`), the board pump's stall notice, a commented
234
+ * `ticket.changed`. Read it with `ticket_get` and `commentId`. Absent when no comment holds the text.
235
+ */
236
+ commentId?: string;
231
237
  turnId?: string;
232
238
  status?: string;
233
239
  text?: string;
@@ -5,7 +5,7 @@ import type {
5
5
  TControllerBrowserPaintAvailability,
6
6
  TControllerBrowserPaintTermination,
7
7
  } from './browserpaint.js';
8
- export const controllerProtocolVersion = 45 as const;
8
+ export const controllerProtocolVersion = 46 as const;
9
9
  export const controllerUpgradeManagementVersion = 2 as const;
10
10
  export const controllerPackageName = 'agl' as const;
11
11
  /** The systemd user unit `agl service install` writes; a controller it runs reports it in its status. */
@@ -1,7 +1,7 @@
1
1
  import type * as plugins from './plugins.js';
2
2
  import type {
3
- ITicket, ITicketActivity, ITicketAnswer, ITicketAsk, ITicketBoard, ITicketChanges, ITicketComment, ITicketCreate,
4
- ITicketFilters, ITicketMove, ITicketQuestion, TTicketLink, TTicketOperation, TTicketReferenceContent, TTicketRequest,
3
+ ITicket, ITicketAnswer, ITicketAsk, ITicketBoard, ITicketChanges, ITicketComment, ITicketCreate,
4
+ ITicketDetail, ITicketFilters, ITicketGet, ITicketMove, ITicketQuestion, TTicketLink, TTicketOperation, TTicketReferenceContent, TTicketRequest,
5
5
  TTicketResponse,
6
6
  } from './tickets.js';
7
7
 
@@ -61,15 +61,18 @@ export interface IReq_ControllerMcpTicketSearch extends plugins.typedrequestInte
61
61
  response: { tickets: ITicket[] };
62
62
  }
63
63
  /**
64
- * Reads a ticket by its `AGL-<n>` key, with its Markdown comments, source, links, questions and
65
- * their answers, and its complete activity log.
64
+ * Reads a ticket by its `AGL-<n>` key with its source, links, all its questions and their answers,
65
+ * and the newest page of its history (up to `limit` entries, default 50, at most 500, within 256 KiB), oldest first: the
66
+ * activity entries and the Markdown comments they name, a `commented` entry naming its comment by
67
+ * id. `historyBefore` is present while older history exists; pass it as `before` for the next page.
68
+ * `commentId` reads that one comment instead (`comments: [it]`, `activity: []`), without `before` or `limit`.
66
69
  */
67
70
  export interface IReq_ControllerMcpTicketGet extends plugins.typedrequestInterfaces.implementsTR<
68
71
  TControllerMcpTicketRequest<'get'>, IReq_ControllerMcpTicketGet
69
72
  > {
70
73
  method: 'agl.mcp.ticket.get';
71
- request: { ticketId: string } & { ownerToken?: string };
72
- response: { ticket: ITicket; comments: ITicketComment[]; activity: ITicketActivity[]; questions: ITicketQuestion[] };
74
+ request: ITicketGet & { ownerToken?: string };
75
+ response: ITicketDetail;
73
76
  }
74
77
  /** Reads a ticket's comments. */
75
78
  export interface IReq_ControllerMcpTicketComments extends plugins.typedrequestInterfaces.implementsTR<
@@ -200,6 +200,36 @@ export interface ITicketCreate {
200
200
  attachments?: ITicketAttachment[];
201
201
  }
202
202
  export interface ITicketMove { ticketId: string; columnId: TTicketColumn; beforeTicketId?: string; }
203
+ /**
204
+ * How many history entries one `get` returns unless it names `limit`, the most it may name, and the bytes a page holds
205
+ * at most (its entries plus the comments they name, serialized; a page holds at least one entry). The budget wins over `limit`.
206
+ */
207
+ export const ticketHistoryPage = { default: 50, maximum: 500, bytes: 256 * 1024 } as const;
208
+ /** A ticket and one page of its history: the newest entries, or those older than the entry `before` names, within `ticketHistoryPage`. */
209
+ export interface ITicketGet {
210
+ ticketId: string;
211
+ /** An activity id, as `historyBefore` returned it: the page holds only older entries. */
212
+ before?: string;
213
+ /** Entries on the page, 1 to 500 (`ticketHistoryPage.maximum`); 50 (`ticketHistoryPage.default`) when absent. */
214
+ limit?: number;
215
+ /**
216
+ * One comment of this ticket, by the id an activity entry or a fleet event names: the response holds the ticket, its
217
+ * questions and that comment alone (`comments: [it]`, `activity: []`, no `historyBefore`). Excludes `before` and `limit`.
218
+ */
219
+ commentId?: string;
220
+ }
221
+ /**
222
+ * A ticket, all its questions and one page of its history, oldest first. `activity` is the page; `comments` are the
223
+ * comments its `commented` entries name, whose `changes.comment` holds only `{ after: { id } }` here (the stored
224
+ * entry keeps the whole comment). `historyBefore` is present while older entries exist: pass it as `before`.
225
+ */
226
+ export interface ITicketDetail {
227
+ ticket: ITicket;
228
+ comments: ITicketComment[];
229
+ activity: ITicketActivity[];
230
+ questions: ITicketQuestion[];
231
+ historyBefore?: string;
232
+ }
203
233
  export interface ITicketBoard {
204
234
  revision: number;
205
235
  columns: { id: TTicketColumn; title: string; done?: boolean }[];
@@ -211,7 +241,7 @@ export interface ITicketOperations {
211
241
  list: { request: ITicketFilters; response: { tickets: ITicket[] } };
212
242
  board: { request: ITicketFilters; response: ITicketBoard };
213
243
  search: { request: { query: string; label?: string }; response: { tickets: ITicket[] } };
214
- get: { request: { ticketId: string }; response: { ticket: ITicket; comments: ITicketComment[]; activity: ITicketActivity[]; questions: ITicketQuestion[] } };
244
+ get: { request: ITicketGet; response: ITicketDetail };
215
245
  comments: { request: { ticketId: string }; response: { comments: ITicketComment[] } };
216
246
  create: { request: ITicketCreate; response: { ticket: ITicket } };
217
247
  update: { request: { ticketId: string; expectedRevision: number; changes: ITicketChanges }; response: { ticket: ITicket } };
@@ -259,6 +289,7 @@ export const ticketErrorMessages = {
259
289
  ticket_ask_forbidden: 'Workers may ask questions only on tickets assigned to their verified session.',
260
290
  ticket_answer_forbidden: 'Only the owner or the driver may answer ticket questions.',
261
291
  ticket_question_not_found: 'The requested question does not exist on this ticket.',
292
+ ticket_comment_not_found: 'The requested comment does not exist on this ticket.',
262
293
  ticket_question_answered: 'The question already has a different answer; a second answer is refused. Ask a new question instead.',
263
294
  ticket_answer_invalid: 'An answer chooses existing option ids (one unless multiSelect) and/or gives free text where the question allows it.',
264
295
  } as const;
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '40.6.0',
6
+ version: '41.0.1',
7
7
  description: 'Agent Gateway Layer for OpenCode, FlexHarness, and Codex sessions'
8
8
  }
@@ -941,10 +941,13 @@ const modelHarnessId = (choiceArg: interfaces.TControllerModelChoice): TBrowserS
941
941
  /** How long after asking for a restart this page expects its connection to drop; still connected, AGL did not restart. */
942
942
  const restartCheckMs = 20_000;
943
943
 
944
+ /** A model as a harness's own row names it, without the harness: "openai/gpt-5.6-sol". */
945
+ const modelChoiceName = (choiceArg: interfaces.TControllerModelChoice): string => `${choiceArg.providerID}/${choiceArg.modelID}`;
946
+
944
947
  const modelOptionLabel = (choiceArg: interfaces.TControllerModelChoice): string => {
945
948
  return [
946
949
  sessionHarnessLabel(modelHarnessId(choiceArg)),
947
- `${choiceArg.providerID}/${choiceArg.modelID}`,
950
+ modelChoiceName(choiceArg),
948
951
  ].join(' · ');
949
952
  };
950
953
 
@@ -18816,16 +18819,23 @@ ${entry.detail.stack}`}</pre>`}
18816
18819
 
18817
18820
  /** General as the controller's confirmed settings and this browser's own choices have it. */
18818
18821
  private generalSettingsView(): IGeneralSettingsView {
18822
+ // Each row belongs to one harness, so it names a model without the harness.
18823
+ const modelName = (modelStringArg: string): string => {
18824
+ const choice = this.modelChoicesByString.get(modelStringArg);
18825
+ return choice ? modelChoiceName(choice) : modelStringArg;
18826
+ };
18819
18827
  return {
18820
18828
  defaults: (['opencode', 'flex', 'codex', 'claude'] as const).map((harnessIdArg) => {
18821
18829
  const name = sessionHarnessLabel(harnessIdArg);
18822
18830
  const model = this.defaultModelStrings[harnessIdArg];
18823
18831
  const unavailable = this.unavailableDefaultModels[harnessIdArg];
18824
18832
  // Flex has no default of its own: without one AGL can name, its composer asks for a model.
18825
- const runningDefault = this.composerDefaultModelStrings[harnessIdArg]
18826
- || (harnessIdArg === 'flex' ? '' : `${name}'s own default`);
18833
+ const composerDefault = this.composerDefaultModelStrings[harnessIdArg];
18834
+ const runningDefault = composerDefault
18835
+ ? modelName(composerDefault)
18836
+ : harnessIdArg === 'flex' ? '' : `${name}'s own default`;
18827
18837
  const note = unavailable
18828
- ? `The saved default ${modelOptionLabel(unavailable)} is no longer offered. ${runningDefault
18838
+ ? `The saved default ${modelChoiceName(unavailable)} is no longer offered. ${runningDefault
18829
18839
  ? `New conversations run on ${runningDefault}.`
18830
18840
  : 'New Flex conversations need an account and model chosen in the composer.'}`
18831
18841
  : harnessIdArg === 'flex' && !model ? 'Without a default, Flex\'s composer asks for an account and a model.' : '';
@@ -18834,7 +18844,7 @@ ${entry.detail.stack}`}</pre>`}
18834
18844
  name,
18835
18845
  models: [
18836
18846
  { key: '', label: harnessIdArg === 'flex' ? 'Ask in the composer' : `${name}'s own default` },
18837
- ...this.modelOptionsForHarness(harnessIdArg).map((optionArg) => ({ key: optionArg, label: optionArg })),
18847
+ ...this.modelOptionsForHarness(harnessIdArg).map((optionArg) => ({ key: optionArg, label: modelName(optionArg) })),
18838
18848
  ],
18839
18849
  model,
18840
18850
  efforts: this.settingsEffortDropdownOptions(model)
@@ -18854,9 +18864,10 @@ ${entry.detail.stack}`}</pre>`}
18854
18864
  gpu: this.browserGpu,
18855
18865
  activeGpu: this.activeBrowserGpu,
18856
18866
  presentation: this.browserPresentation,
18857
- paintState: this.activeBrowserPaint
18858
- ? describeBrowserPaintAvailability(this.activeBrowserPaint)
18859
- : 'No browser runtime is running.',
18867
+ // Said only while Paint cannot be used: an available Paint needs no sentence.
18868
+ paintState: !this.activeBrowserPaint
18869
+ ? 'No browser runtime is running.'
18870
+ : this.activeBrowserPaint.state === 'available' ? '' : describeBrowserPaintAvailability(this.activeBrowserPaint),
18860
18871
  paintRearm: this.activeBrowserPaint?.state === 'unavailable'
18861
18872
  && this.activeBrowserPaint.reason !== 'descriptor-missing'
18862
18873
  && this.activeBrowserPaint.reason !== 'unsupported-platform'
@@ -250,6 +250,20 @@ export class AglTicketBoard extends plugins.deesElement.DeesElement {
250
250
  ?? { id: personArg.id, name: personArg.name };
251
251
  }
252
252
 
253
+ /** A ticket's whole history: `get` returns it a page at a time, newest first, so this reads back to the first entry. */
254
+ private async history(ticketIdArg: string, signalArg?: AbortSignal): Promise<Omit<interfaces.ITicketDetail, 'historyBefore'>> {
255
+ const pages: interfaces.ITicketDetail[] = [];
256
+ let before: string | undefined;
257
+ do {
258
+ const page = await this.fire('get', { ticketId: ticketIdArg, limit: interfaces.ticketHistoryPage.maximum,
259
+ ...(before === undefined ? {} : { before }) }, signalArg);
260
+ pages.unshift(page);
261
+ before = page.historyBefore;
262
+ } while (before !== undefined);
263
+ const { ticket, questions } = pages.at(-1)!;
264
+ return { ticket, questions, comments: pages.flatMap((pageArg) => pageArg.comments), activity: pages.flatMap((pageArg) => pageArg.activity) };
265
+ }
266
+
253
267
  /** Tickets as the kanban shows them, remembering whom they name. */
254
268
  private shown(ticketsArg: interfaces.ITicket[]): IKanbanTicket[] {
255
269
  for (const ticket of ticketsArg) {
@@ -274,12 +288,10 @@ export class AglTicketBoard extends plugins.deesElement.DeesElement {
274
288
  searchTickets: async (queryArg, signalArg) => this.shown((await this.fire('search', { query: queryArg }, signalArg)).tickets),
275
289
  // The discussion is the comments and the questions; the kanban orders them by when they were written.
276
290
  loadComments: async (ticketIdArg, signalArg) => {
277
- const { comments, questions } = await this.fire('get', { ticketId: ticketIdArg }, signalArg);
291
+ const { comments, questions } = await this.history(ticketIdArg, signalArg);
278
292
  return [...comments.map(kanbanComment), ...questions.map(kanbanQuestion)];
279
293
  },
280
- loadActivity: async (ticketIdArg, signalArg) => (
281
- (await this.fire('get', { ticketId: ticketIdArg }, signalArg)).activity.map(kanbanActivity)
282
- ),
294
+ loadActivity: async (ticketIdArg, signalArg) => (await this.history(ticketIdArg, signalArg)).activity.map(kanbanActivity),
283
295
  addComment: async (inputArg) => this.change(async () => (
284
296
  kanbanComment((await this.fire('comment', { ticketId: inputArg.ticketId, body: inputArg.body })).comment)
285
297
  )),
@@ -36,7 +36,7 @@ export interface IGeneralSettingsView {
36
36
  gpu: 'enabled' | 'disabled';
37
37
  activeGpu: 'enabled' | 'disabled';
38
38
  presentation: interfaces.TControllerBrowserPresentation;
39
- /** What the runtime says of Paint, or that no browser runtime runs. */
39
+ /** Why Paint cannot be used now, or that no browser runtime runs; empty while Paint is available. */
40
40
  paintState: string;
41
41
  /** Paint's integrity retries stopped and can be started again. */
42
42
  paintRearm: boolean;
@@ -90,10 +90,15 @@ export const renderGeneralSettings = (
90
90
  leading: html`<mp-harness-mark .harness=${rowArg.harnessId}></mp-harness-mark>`,
91
91
  label: rowArg.name,
92
92
  ...(rowArg.note ? { description: rowArg.note } : {}),
93
- control: html`<mp-model-picker class="settingsDefault" data-harness=${rowArg.harnessId} style="width: min(340px, 100%);"
93
+ // One folded button per row (model · effort), as wide as it says and at most a share of a narrow screen;
94
+ // it opens the model and the effort in a speech bubble. A saved default no longer offered gives it the
95
+ // warning tone.
96
+ control: html`<mp-model-picker class="settingsDefault" data-harness=${rowArg.harnessId} fold="always"
97
+ style="max-width: min(360px, 45vw);" .label=${`Default model for ${rowArg.name}`}
98
+ .warning=${rowArg.noteIsWarning ? rowArg.note : ''}
94
99
  .models=${rowArg.models} .model=${rowArg.model} .efforts=${rowArg.efforts} .effort=${rowArg.effort}
95
100
  .effortDisabledReason=${rowArg.effortDisabledReason} .modelPlaceholder=${'Choose a model'}
96
- .modelLabel=${`${rowArg.name} default model`} .effortLabel=${`${rowArg.name} reasoning effort`}
101
+ .modelLabel=${'Model'} .effortLabel=${'Reasoning effort'}
97
102
  @mp-model-change=${(eventArg: CustomEvent<TMpModelChoice>) => actionsArg.setDefault(rowArg.harnessId, eventArg.detail)}
98
103
  ></mp-model-picker>`,
99
104
  })),
@@ -161,7 +166,8 @@ export const renderGeneralSettings = (
161
166
  {
162
167
  key: 'presentation',
163
168
  label: 'New browser views show',
164
- description: `Paint draws the page as vector scenes in this tab; Video streams it. Each view can switch on its own. ${viewArg.paintState}`,
169
+ description: `Paint draws the page as vector scenes in this tab; Video streams it. Each view can switch on its own.${
170
+ viewArg.paintState ? ` ${viewArg.paintState}` : ''}`,
165
171
  control: html`
166
172
  ${viewArg.paintRearm
167
173
  ? html`<dees-button class="settingsBrowserPaintRearm" size="sm" text="Retry Paint" @clicked=${() => actionsArg.rearmPaint()}></dees-button>`