@link-assistant/agent 0.26.1 → 0.26.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -12,7 +12,9 @@ import {
12
12
  getDefaultModelParts,
13
13
  getDefaultModelSource,
14
14
  getDefaultCompactionModel,
15
+ getDefaultCompactionModelSource,
15
16
  getDefaultCompactionModels,
17
+ getDefaultCompactionModelsSource,
16
18
  getDefaultCompactionSafetyMarginPercent,
17
19
  } from './defaults.ts';
18
20
  import { outputModelResolved } from './model-resolution.ts';
@@ -356,6 +358,64 @@ async function resolveCompactionModelEntry(
356
358
  };
357
359
  }
358
360
 
361
+ /**
362
+ * Narrow a built-in compaction default to the provider the run was pointed at.
363
+ *
364
+ * The shipped cascade names OpenCode models, because the shipped base model is
365
+ * an OpenCode one. A run launched with `--model formalai/formal-ai` inherited
366
+ * that cascade unchanged, so compaction — and the session summary, which reuses
367
+ * the compaction model — called a provider the operator may hold no credentials
368
+ * for. Every one of those calls fails, and the failures read as the run having
369
+ * failed.
370
+ *
371
+ * Only the *built-in* defaults are narrowed, and only for a run pointed away
372
+ * from the built-in default provider. A cascade the operator named
373
+ * (`--compaction-model`, `--compaction-models`, or the
374
+ * `LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODEL*` env vars) is an explicit
375
+ * choice and is left alone, cross-provider or not; and a run on the shipped
376
+ * provider keeps the shipped cascade, whose extra entries are the rate-limit
377
+ * fallbacks it was written for.
378
+ *
379
+ * @param {{providerID: string, modelID: string, useSameModel: boolean}[]} entries
380
+ * @param {string} baseProviderID Provider of the model that runs the turns
381
+ * @param {string} baseModelID Model that runs the turns
382
+ * @returns {{providerID: string, modelID: string, useSameModel: boolean}[]}
383
+ * @see https://github.com/link-assistant/agent/issues/307
384
+ */
385
+ function inheritBaseProvider(entries, baseProviderID, baseModelID) {
386
+ const kept = entries.filter(
387
+ (entry) => entry.useSameModel || entry.providerID === baseProviderID
388
+ );
389
+ const dropped = entries.filter((entry) => !kept.includes(entry));
390
+ if (dropped.length === 0) {
391
+ return entries;
392
+ }
393
+
394
+ const inherited =
395
+ kept.length > 0
396
+ ? kept
397
+ : [
398
+ {
399
+ providerID: baseProviderID,
400
+ modelID: baseModelID,
401
+ useSameModel: true,
402
+ },
403
+ ];
404
+
405
+ Log.Default.info(() => ({
406
+ message: 'default compaction models narrowed to the configured provider',
407
+ hint: 'Secondary calls (compaction, session summary) inherit --model unless a compaction model is named explicitly',
408
+ baseProviderID,
409
+ baseModelID,
410
+ dropped: dropped.map((entry) => `${entry.providerID}/${entry.modelID}`),
411
+ kept: inherited.map((entry) =>
412
+ entry.useSameModel ? 'same' : `${entry.providerID}/${entry.modelID}`
413
+ ),
414
+ }));
415
+
416
+ return inherited;
417
+ }
418
+
359
419
  /**
360
420
  * Parse compaction model config from argv.
361
421
  * Supports both --compaction-model (single) and --compaction-models (cascade).
@@ -363,6 +423,7 @@ async function resolveCompactionModelEntry(
363
423
  * The special value "same" means use the base model for compaction.
364
424
  * @see https://github.com/link-assistant/agent/issues/219
365
425
  * @see https://github.com/link-assistant/agent/issues/232
426
+ * @see https://github.com/link-assistant/agent/issues/307
366
427
  */
367
428
  async function parseCompactionModelConfig(
368
429
  argv,
@@ -425,9 +486,18 @@ async function parseCompactionModelConfig(
425
486
  }
426
487
  }
427
488
 
489
+ // A cascade nobody asked for must not reach across providers (#307).
490
+ const usingBuiltInCascade =
491
+ compactionModelsSource === 'default' &&
492
+ getDefaultCompactionModelsSource(defaultOptions) === 'default' &&
493
+ baseProviderID !== DEFAULT_PROVIDER_ID;
494
+ const cascade = usingBuiltInCascade
495
+ ? inheritBaseProvider(compactionModels, baseProviderID, baseModelID)
496
+ : compactionModels;
497
+
428
498
  Log.Default.info(() => ({
429
499
  message: 'compaction models cascade configured',
430
- models: compactionModels.map((m) =>
500
+ models: cascade.map((m) =>
431
501
  m.useSameModel ? 'same' : `${m.providerID}/${m.modelID}`
432
502
  ),
433
503
  source: compactionModelsSource,
@@ -435,7 +505,7 @@ async function parseCompactionModelConfig(
435
505
 
436
506
  // Use the first model as the primary compaction model (for backward compatibility)
437
507
  // The full cascade is stored in compactionModels array
438
- const primary = compactionModels[0] || {
508
+ const primary = cascade[0] || {
439
509
  providerID: baseProviderID,
440
510
  modelID: baseModelID,
441
511
  useSameModel: true,
@@ -446,16 +516,21 @@ async function parseCompactionModelConfig(
446
516
  modelID: primary.modelID,
447
517
  useSameModel: primary.useSameModel,
448
518
  compactionSafetyMarginPercent,
449
- compactionModels,
519
+ compactionModels: cascade,
450
520
  };
451
521
  }
452
522
 
453
523
  // Fallback to single --compaction-model
454
524
  const cliCompactionModelArg = getCompactionModelFromProcessArgv();
525
+ const defaultCompactionModel = getDefaultCompactionModel(defaultOptions);
526
+ const compactionModelSource =
527
+ cliCompactionModelArg ||
528
+ (argv['compaction-model'] &&
529
+ argv['compaction-model'] !== defaultCompactionModel)
530
+ ? 'cli'
531
+ : 'default';
455
532
  const compactionModelArg =
456
- cliCompactionModelArg ??
457
- argv['compaction-model'] ??
458
- getDefaultCompactionModel(defaultOptions);
533
+ cliCompactionModelArg ?? argv['compaction-model'] ?? defaultCompactionModel;
459
534
 
460
535
  const resolved = await resolveCompactionModelEntry(
461
536
  compactionModelArg,
@@ -463,23 +538,33 @@ async function parseCompactionModelConfig(
463
538
  baseModelID
464
539
  );
465
540
 
541
+ // Same rule as the cascade: an unrequested default follows --model (#307).
542
+ const usingBuiltInCompactionModel =
543
+ compactionModelSource === 'default' &&
544
+ getDefaultCompactionModelSource(defaultOptions) === 'default' &&
545
+ baseProviderID !== DEFAULT_PROVIDER_ID;
546
+ const [entry] = usingBuiltInCompactionModel
547
+ ? inheritBaseProvider([resolved], baseProviderID, baseModelID)
548
+ : [resolved];
549
+
466
550
  Log.Default.info(() => ({
467
551
  message: 'using single compaction model',
468
- compactionProviderID: resolved.providerID,
469
- compactionModelID: resolved.modelID,
470
- useSameModel: resolved.useSameModel,
552
+ compactionProviderID: entry.providerID,
553
+ compactionModelID: entry.modelID,
554
+ useSameModel: entry.useSameModel,
555
+ source: compactionModelSource,
471
556
  }));
472
557
 
473
558
  return {
474
- providerID: resolved.providerID,
475
- modelID: resolved.modelID,
476
- useSameModel: resolved.useSameModel,
559
+ providerID: entry.providerID,
560
+ modelID: entry.modelID,
561
+ useSameModel: entry.useSameModel,
477
562
  compactionSafetyMarginPercent,
478
563
  compactionModels: [
479
564
  {
480
- providerID: resolved.providerID,
481
- modelID: resolved.modelID,
482
- useSameModel: resolved.useSameModel,
565
+ providerID: entry.providerID,
566
+ modelID: entry.modelID,
567
+ useSameModel: entry.useSameModel,
483
568
  },
484
569
  ],
485
570
  };
@@ -184,13 +184,13 @@ export function buildRunOptions(yargs, defaultOptions = {}) {
184
184
  .option('summarize-session', {
185
185
  type: 'boolean',
186
186
  description:
187
- 'Generate AI session summaries (default: true). Use --no-summarize-session to disable.',
187
+ 'Generate AI session summaries (default: true). Use --no-summarize-session to disable. Automatically off for a model declared as having no context limit (`unlimited: true` or `limit.context: null` in the provider config).',
188
188
  default: true,
189
189
  })
190
190
  .option('compaction-model', {
191
191
  type: 'string',
192
192
  description:
193
- 'Model to use for context compaction in format providerID/modelID. Use "same" to use the base model. Default: opencode/gpt-5-nano (free, 400K context). Overridden by --compaction-models if both are specified.',
193
+ 'Model to use for context compaction and session summaries, in format providerID/modelID. Use "same" to use the base model. Default: opencode/gpt-5-nano (free, 400K context), which falls back to the --model model when --model points at another provider. Overridden by --compaction-models if both are specified.',
194
194
  default: defaultCompactionModel,
195
195
  })
196
196
  .option('compaction-models', {
@@ -198,7 +198,8 @@ export function buildRunOptions(yargs, defaultOptions = {}) {
198
198
  description:
199
199
  'Ordered cascade of compaction models in links notation sequence format: "(model1 model2 ... same)". ' +
200
200
  "Models are tried from smallest/cheapest context to largest. If used context exceeds a model's limit or its rate limit is reached, the next model is tried. " +
201
- 'The special value "same" uses the base model. Overrides --compaction-model when specified.',
201
+ 'The special value "same" uses the base model. Overrides --compaction-model when specified. ' +
202
+ 'The default cascade is OpenCode-specific: when --model points at another provider, its entries from other providers are dropped so secondary calls inherit --model. A cascade given explicitly is used as written.',
202
203
  default: defaultCompactionModels,
203
204
  })
204
205
  .option('compaction-safety-margin', {
@@ -142,6 +142,24 @@ export function getDefaultCompactionModel(
142
142
  );
143
143
  }
144
144
 
145
+ /**
146
+ * Report where the effective default compaction model came from.
147
+ *
148
+ * Same contract as `getDefaultModelSource`: `'config'` when an operator set
149
+ * `defaultOptions.defaultCompactionModel` or
150
+ * `LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODEL`, `'default'` when the
151
+ * built-in `DEFAULT_COMPACTION_MODEL` applies. Only a built-in default may be
152
+ * narrowed to the configured model's provider (#307).
153
+ */
154
+ export function getDefaultCompactionModelSource(
155
+ options: DefaultConfigOptions = {}
156
+ ): 'config' | 'default' {
157
+ const env = options.env ?? process.env;
158
+ if (optionString(options.defaultCompactionModel)) return 'config';
159
+ if (envString(env, DEFAULT_COMPACTION_MODEL_ENV)) return 'config';
160
+ return 'default';
161
+ }
162
+
145
163
  export function getDefaultCompactionModels(
146
164
  options: DefaultConfigOptions = {}
147
165
  ): string {
@@ -153,6 +171,20 @@ export function getDefaultCompactionModels(
153
171
  );
154
172
  }
155
173
 
174
+ /**
175
+ * Report where the effective default compaction cascade came from.
176
+ *
177
+ * @see getDefaultCompactionModelSource
178
+ */
179
+ export function getDefaultCompactionModelsSource(
180
+ options: DefaultConfigOptions = {}
181
+ ): 'config' | 'default' {
182
+ const env = options.env ?? process.env;
183
+ if (optionString(options.defaultCompactionModels)) return 'config';
184
+ if (envString(env, DEFAULT_COMPACTION_MODELS_ENV)) return 'config';
185
+ return 'default';
186
+ }
187
+
156
188
  export function getDefaultCompactionSafetyMarginPercent(
157
189
  options: DefaultConfigOptions = {}
158
190
  ): number {
@@ -834,7 +834,22 @@ export namespace Config {
834
834
  ModelsDev.Provider.partial()
835
835
  .extend({
836
836
  models: z
837
- .record(z.string(), ModelsDev.Model.partial())
837
+ .record(
838
+ z.string(),
839
+ ModelsDev.Model.partial().extend({
840
+ // `context: null` states that the model has no context
841
+ // window to run out of, which turns off compaction and
842
+ // session summarization for it (#307). `output` is
843
+ // optional here so `{ "context": null }` alone is a valid
844
+ // override.
845
+ limit: z
846
+ .object({
847
+ context: z.number().nullable().optional(),
848
+ output: z.number().optional(),
849
+ })
850
+ .optional(),
851
+ })
852
+ )
838
853
  .optional(),
839
854
  options: z
840
855
  .object({
package/src/index.js CHANGED
@@ -971,4 +971,9 @@ async function main() {
971
971
  }
972
972
  }
973
973
 
974
- main();
974
+ // Awaited on purpose: everything the CLI does happens inside this promise, and
975
+ // on Windows nothing else holds the process open while startup waits on async
976
+ // filesystem I/O — the event loop drains and Bun exits 0 mid-run, so the CLI
977
+ // prints its startup logs, never contacts the provider and reports success.
978
+ // See: https://github.com/link-assistant/agent/issues/306
979
+ await main();
@@ -42,6 +42,12 @@ export namespace ModelsDev {
42
42
  context: z.number(),
43
43
  output: z.number(),
44
44
  }),
45
+ unlimited: z
46
+ .boolean()
47
+ .optional()
48
+ .describe(
49
+ 'Model has no context limit. Compaction and session summarization are skipped for it. Also set by writing `limit.context: null` in a provider model override.'
50
+ ),
45
51
  modalities: z
46
52
  .object({
47
53
  input: z.array(z.enum(['text', 'audio', 'image', 'video', 'pdf'])),
@@ -59,6 +65,26 @@ export namespace ModelsDev {
59
65
  });
60
66
  export type Model = z.infer<typeof Model>;
61
67
 
68
+ /**
69
+ * Whether a model declares that it has no context window to run out of.
70
+ *
71
+ * An operator states this per model in the config, either as
72
+ * `"unlimited": true` or as `"limit": { "context": null }` — the two are
73
+ * normalized to the same flag when the config is merged into the model
74
+ * database.
75
+ *
76
+ * Compaction and session summarization both exist to keep a conversation
77
+ * inside a context window. For a model without one they have no work to do,
78
+ * so they are skipped instead of spending a secondary API call per turn.
79
+ *
80
+ * @see https://github.com/link-assistant/agent/issues/307
81
+ */
82
+ export function hasUnlimitedContext(
83
+ model: { unlimited?: boolean } | undefined | null
84
+ ): boolean {
85
+ return model?.unlimited === true;
86
+ }
87
+
62
88
  export const Provider = z
63
89
  .object({
64
90
  api: z.string().optional(),
@@ -1138,11 +1138,21 @@ export namespace Provider {
1138
1138
  ...existing?.options,
1139
1139
  ...model.options,
1140
1140
  },
1141
- limit: model.limit ??
1142
- existing?.limit ?? {
1143
- context: 0,
1144
- output: 0,
1145
- },
1141
+ limit: iife(() => {
1142
+ const limit = model.limit ?? existing?.limit;
1143
+ if (!limit) return { context: 0, output: 0 };
1144
+ // `context: null` is the config spelling of "no context limit";
1145
+ // the arithmetic downstream keeps reading 0 as "no window to
1146
+ // check", and the capability itself travels in `unlimited` (#307).
1147
+ return {
1148
+ context: limit.context ?? 0,
1149
+ output: limit.output ?? existing?.limit?.output ?? 0,
1150
+ };
1151
+ }),
1152
+ unlimited:
1153
+ model.unlimited ??
1154
+ (model.limit && model.limit.context === null ? true : undefined) ??
1155
+ existing?.unlimited,
1146
1156
  modalities: model.modalities ??
1147
1157
  existing?.modalities ?? {
1148
1158
  input: ['text'],
@@ -7,7 +7,7 @@ import { MessageV2 } from './message-v2';
7
7
  import { SystemPrompt } from './system';
8
8
  import { Bus } from '../bus';
9
9
  import z from 'zod';
10
- import type { ModelsDev } from '../provider/models';
10
+ import { ModelsDev } from '../provider/models';
11
11
  import { SessionPrompt } from './prompt';
12
12
  import { config, isVerbose } from '../config/config';
13
13
  import { Token } from '../util/token';
@@ -135,6 +135,8 @@ export namespace SessionCompaction {
135
135
  estimatedInputTokens?: number;
136
136
  }) {
137
137
  if (config.disableAutocompact) return false;
138
+ // A model without a context window can never overflow it (#307).
139
+ if (ModelsDev.hasUnlimitedContext(input.model)) return false;
138
140
  const baseModelContextLimit = input.model.limit.context;
139
141
  if (baseModelContextLimit === 0) return false;
140
142
  const providerCount =
@@ -193,6 +195,7 @@ export namespace SessionCompaction {
193
195
  compactionModel?: CompactionModelConfig;
194
196
  compactionModelContextLimit?: number;
195
197
  }): MessageV2.ContextDiagnostics | undefined {
198
+ if (ModelsDev.hasUnlimitedContext(input.model)) return undefined;
196
199
  const contextLimit = input.model.limit.context;
197
200
  if (contextLimit === 0) return undefined;
198
201
  const outputLimit =
@@ -508,9 +508,20 @@ export namespace SessionProcessor {
508
508
  }
509
509
  snapshot = undefined;
510
510
  }
511
- SessionSummary.summarize({
511
+ // Fire-and-forget on purpose, and guarded: a summarization
512
+ // failure must not become an unhandled rejection that exits
513
+ // the process while this stream is still being processed.
514
+ // See: https://github.com/link-assistant/agent/issues/304
515
+ void SessionSummary.summarize({
512
516
  sessionID: input.sessionID,
513
517
  messageID: input.assistantMessage.parentID,
518
+ }).catch((error) => {
519
+ log.warn(() => ({
520
+ message: 'session summarization failed',
521
+ sessionID: input.sessionID,
522
+ error:
523
+ error instanceof Error ? error.message : String(error),
524
+ }));
514
525
  });
515
526
  break;
516
527
 
@@ -833,9 +833,19 @@ export namespace SessionPrompt {
833
833
  };
834
834
 
835
835
  if (step === 1) {
836
- SessionSummary.summarize({
836
+ // Fire-and-forget on purpose: the summary runs alongside the turn. The
837
+ // `void` marks that, and the `.catch` keeps a summarization failure
838
+ // from becoming an unhandled rejection that would exit the process and
839
+ // abort this turn. See: https://github.com/link-assistant/agent/issues/304
840
+ void SessionSummary.summarize({
837
841
  sessionID: sessionID,
838
842
  messageID: lastUser.id,
843
+ }).catch((error) => {
844
+ log.warn(() => ({
845
+ message: 'session summarization failed',
846
+ sessionID,
847
+ error: error instanceof Error ? error.message : String(error),
848
+ }));
839
849
  });
840
850
  }
841
851
 
@@ -14,6 +14,7 @@ import { Instance } from '../project/instance';
14
14
  import { Storage } from '../storage/storage';
15
15
  import { Bus } from '../bus';
16
16
  import { config, isVerbose } from '../config/config';
17
+ import { ModelsDev } from '../provider/models';
17
18
  import { Token } from '../util/token';
18
19
 
19
20
  export namespace SessionSummary {
@@ -25,11 +26,29 @@ export namespace SessionSummary {
25
26
  messageID: z.string(),
26
27
  }),
27
28
  async (input) => {
28
- const all = await Session.messages({ sessionID: input.sessionID });
29
- await Promise.all([
30
- summarizeSession({ sessionID: input.sessionID, messages: all }),
31
- summarizeMessage({ messageID: input.messageID, messages: all }),
32
- ]);
29
+ // Summarization is a side quest: its whole product is a session title, a
30
+ // body and a diff stat, and it runs concurrently with the turn it
31
+ // describes. Neither caller awaits it, so a rejection escaping this
32
+ // function has no handler anywhere and becomes an `unhandledRejection`,
33
+ // which exits the process with status 1 and aborts the streaming turn.
34
+ // A failed title must never be a failed run.
35
+ // See: https://github.com/link-assistant/agent/issues/304
36
+ try {
37
+ const all = await Session.messages({ sessionID: input.sessionID });
38
+ await Promise.all([
39
+ summarizeSession({ sessionID: input.sessionID, messages: all }),
40
+ summarizeMessage({ messageID: input.messageID, messages: all }),
41
+ ]);
42
+ } catch (error) {
43
+ log.warn(() => ({
44
+ message: 'session summarization failed',
45
+ hint: 'The turn is unaffected; only the session summary is missing',
46
+ sessionID: input.sessionID,
47
+ messageID: input.messageID,
48
+ error: error instanceof Error ? error.message : String(error),
49
+ stack: error instanceof Error ? error.stack : undefined,
50
+ }));
51
+ }
33
52
  }
34
53
  );
35
54
 
@@ -94,6 +113,25 @@ export namespace SessionSummary {
94
113
  const assistantMsg = messages.find((m) => m.info.role === 'assistant')!
95
114
  .info as MessageV2.Assistant;
96
115
 
116
+ // Summarization keeps a conversation inside a context window. A model that
117
+ // declares it has none has nothing to summarize for, so the secondary call
118
+ // is skipped rather than spent — an integrator running such a model no
119
+ // longer has to pass --no-summarize-session.
120
+ // See: https://github.com/link-assistant/agent/issues/307
121
+ const baseModel = await Provider.getModel(
122
+ assistantMsg.providerID,
123
+ assistantMsg.modelID
124
+ ).catch(() => null);
125
+ if (baseModel && ModelsDev.hasUnlimitedContext(baseModel.info)) {
126
+ log.info(() => ({
127
+ message: 'session summarization skipped for an unlimited-context model',
128
+ hint: 'The model declares `unlimited: true` (or `limit.context: null`); the diff stat is still recorded',
129
+ providerID: assistantMsg.providerID,
130
+ modelID: assistantMsg.modelID,
131
+ }));
132
+ return;
133
+ }
134
+
97
135
  // Use the compaction model (--compaction-model, e.g. gpt-5-nano) for summarization
98
136
  // to avoid doubling rate-limit pressure on the main model.
99
137
  // If the compaction model is unavailable, fall back to the main model.
@@ -127,10 +165,7 @@ export namespace SessionSummary {
127
165
  ? 'Compaction model unavailable, falling back to main model'
128
166
  : 'Using same model as --model (no compaction model configured)',
129
167
  }));
130
- model = await Provider.getModel(
131
- assistantMsg.providerID,
132
- assistantMsg.modelID
133
- ).catch(() => null);
168
+ model = baseModel;
134
169
  }
135
170
  if (!model) {
136
171
  log.info(() => ({
@@ -202,20 +237,34 @@ export namespace SessionSummary {
202
237
  ],
203
238
  headers: model.info.headers,
204
239
  model: model.language,
205
- });
206
-
207
- if (isVerbose()) {
208
- log.info(() => ({
209
- message: 'title API response received',
240
+ }).catch((err) => {
241
+ // A provider that resolves a model but errors on the request must cost
242
+ // the run its title, not its exit status.
243
+ // See: https://github.com/link-assistant/agent/issues/304
244
+ log.warn(() => ({
245
+ message: 'title API call failed',
210
246
  providerID: model.providerID,
211
247
  modelID: model.modelID,
212
- titleLength: result.text.length,
213
- usage: result.usage,
248
+ error: err instanceof Error ? err.message : String(err),
249
+ stack: err instanceof Error ? err.stack : undefined,
214
250
  }));
251
+ return undefined;
252
+ });
253
+
254
+ if (result) {
255
+ if (isVerbose()) {
256
+ log.info(() => ({
257
+ message: 'title API response received',
258
+ providerID: model.providerID,
259
+ modelID: model.modelID,
260
+ titleLength: result.text.length,
261
+ usage: result.usage,
262
+ }));
263
+ }
264
+ log.info(() => ({ message: 'title', title: result.text }));
265
+ userMsg.summary.title = result.text;
266
+ await Session.updateMessage(userMsg);
215
267
  }
216
- log.info(() => ({ message: 'title', title: result.text }));
217
- userMsg.summary.title = result.text;
218
- await Session.updateMessage(userMsg);
219
268
  }
220
269
 
221
270
  if (