@openchambery/web 2.0.0-beta.22 → 2.0.0-beta.24

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 (58) hide show
  1. package/dist/assets/{AssistantView-BkjDwtaQ.js → AssistantView-qn9Hipja.js} +3 -3
  2. package/dist/assets/{DiagramView-DRuf0R6u.js → DiagramView-BJxfEzpH.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-DalDLpzv.js → MarkdownRenderer-DHo-HJTP.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-DBwCHT5d.js → MarkdownRendererImpl-Cyx24H_y.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-B9BVC6Vb.js → MarkstreamRendererImpl-DKT8cFlu.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-y40xCSR7.js → MobileDetailNavigation-C1Pfx1ab.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-Cbq4bKs6.js → MobileShareBridge-yzsL3AUy.js} +1 -1
  8. package/dist/assets/{MobileSurface-D-F3eHiN.js → MobileSurface-DOn29tPs.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-EJuLTLUQ.js → MultiRunWindow-CZPkrJUM.js} +1 -1
  10. package/dist/assets/{SettingsView-CVqmT8RN.js → SettingsView-BMqMEWMN.js} +1 -1
  11. package/dist/assets/{SettingsWindow-BNDmIW89.js → SettingsWindow-CekCsYBw.js} +1 -1
  12. package/dist/assets/{TerminalView-gZgQfbuO.js → TerminalView-k9o96CEf.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-QoCh-2gJ.js → ToolOutputDialog-CEtY95oc.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-kguN_Kk3.js → appThemeRegistry-DbhqYhdg.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-ygAGnsZQ.js → fileMentionTouchSelection-iDGupHSi.js} +1 -1
  16. package/dist/assets/{gitApi-CHNElSO9.js → gitApi-BnEbIrdK.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-CAGUdmJD.js → insertionBoundaries-By_qiwC0.js} +1 -1
  18. package/dist/assets/{main-CMC55kzu.js → main-CaXU4OGJ.js} +4 -4
  19. package/dist/assets/{main-BbcLOyi3.js → main-D8lh8zYp.js} +2 -2
  20. package/dist/assets/{mcpQueries-B9Je_SGv.js → mcpQueries-BmqKC8WC.js} +1 -1
  21. package/dist/assets/{miniChat-CcSfMnJH.js → miniChat-zZJBQLvR.js} +2 -2
  22. package/dist/assets/{mobile-B9WA1Y98.js → mobile-DeaWxGOe.js} +2 -2
  23. package/dist/assets/{multirun-CsyU-7QN.js → multirun-CmuIahrn.js} +1 -1
  24. package/dist/assets/{projectMeta-BDEHdXJ9.js → projectMeta-bhpIcoPU.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-Dx8MwWwO.js → renderElectronMiniChatApp-t8FY3CLa.js} +2 -2
  26. package/dist/assets/{renderMobileApp-BFXRsHXI.js → renderMobileApp-BYbN4f39.js} +4 -4
  27. package/dist/assets/{runtimeConfig-DoIlK5gp.js → runtimeConfig-D0osnvg3.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-CAYxvd4H.js → runtimeEndpointReset-WWmoRWPS.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-D9uXfzZ_.js → sessionChangeSummary-BvA1TWzS.js} +1 -1
  30. package/dist/assets/{sessionLookup-DSuGYE36.js → sessionLookup-BJFtStrg.js} +1 -1
  31. package/dist/assets/src-OzbnJsph.css +2 -0
  32. package/dist/assets/{useAppFontEffects-Cakdb23p.js → useAppFontEffects-Dc7ZKwKO.js} +6 -6
  33. package/dist/assets/{useDesktopWindowControlsLayout-DaReS2qH.js → useDesktopWindowControlsLayout-iq6hANF2.js} +1 -1
  34. package/dist/assets/{useEffectiveDirectory-CbfHKmnU.js → useEffectiveDirectory-DwBnQQpk.js} +1 -1
  35. package/dist/assets/{useMobileNavigationStore-BLy93WtU.js → useMobileNavigationStore-CNtROYYh.js} +7 -7
  36. package/dist/assets/{useSessionAutoCleanup-BiTaPK87.js → useSessionAutoCleanup-DFLjs5K8.js} +1 -1
  37. package/dist/assets/{useWorkerHighlightedLines-DXEA8HEy.js → useWorkerHighlightedLines-CMBw7PAo.js} +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/mini-chat.html +3 -3
  40. package/dist/mobile.html +3 -3
  41. package/package.json +1 -1
  42. package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
  43. package/server/lib/question-auto-delegate/core.d.ts +5 -1
  44. package/server/lib/question-auto-delegate/core.js +21 -13
  45. package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
  46. package/server/lib/question-auto-delegate/form-io.js +39 -0
  47. package/server/lib/question-auto-delegate/runtime.js +4 -24
  48. package/server/lib/question-auto-delegate/runtime.test.js +138 -0
  49. package/server/lib/relay/DOCUMENTATION.md +1 -0
  50. package/server/lib/relay/service.js +1 -0
  51. package/server/lib/relay/service.test.js +13 -0
  52. package/server/lib/session-goal/DOCUMENTATION.md +11 -0
  53. package/server/lib/session-goal/runtime.js +4 -4
  54. package/server/lib/session-goal/runtime.test.js +32 -3
  55. package/server/lib/session-title/DOCUMENTATION.md +16 -6
  56. package/server/lib/session-title/native-runtime.test.js +17 -5
  57. package/server/lib/session-title/runtime.js +45 -9
  58. package/dist/assets/src-qYyYBKwA.css +0 -2
@@ -722,20 +722,20 @@ export const createSessionGoalRuntime = ({
722
722
  const client = openCodeClient();
723
723
  if (typeof client?.message?.list === 'function') {
724
724
  const listed = await client.message.list(
725
- { sessionID: sessionId, limit: MESSAGE_FETCH_LIMIT, order: 'asc' },
725
+ { sessionID: sessionId, limit: MESSAGE_FETCH_LIMIT, order: 'desc' },
726
726
  directoryRequestOptions(directory),
727
727
  );
728
728
  const projected = projectGoalMessages(listed, sessionId);
729
- if (projected) return projected;
729
+ if (projected) return projected.reverse();
730
730
  }
731
731
  } catch {
732
732
  // Fall through to controlled HTTP seam for tests / older fixtures.
733
733
  }
734
734
  const raw = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}/message`, {
735
735
  directory,
736
- query: { limit: String(MESSAGE_FETCH_LIMIT) },
736
+ query: { limit: String(MESSAGE_FETCH_LIMIT), order: 'desc' },
737
737
  }).catch(() => null);
738
- return projectGoalMessages(raw, sessionId);
738
+ return projectGoalMessages(raw, sessionId)?.reverse() ?? null;
739
739
  };
740
740
 
741
741
  /**
@@ -136,7 +136,7 @@ const makeFetch = ({ messages, liveStatus }) => {
136
136
  }
137
137
  if (pathEndsWith(reqPath, '/session/ses-goal/message')) {
138
138
  // Official v2 SessionMessagesResponse shape.
139
- return jsonResponse({ data: messages, cursor: {} });
139
+ return jsonResponse({ data: new URL(url).searchParams.get('order') === 'desc' ? [...messages].reverse() : messages, cursor: {} });
140
140
  }
141
141
  if (pathEndsWith(reqPath, '/session/ses-goal') && !reqPath.includes('/message')) {
142
142
  if (init.method === 'PATCH') {
@@ -263,6 +263,33 @@ describe('session-goal v2 message projection helpers', () => {
263
263
  });
264
264
 
265
265
  describe('session-goal continuation delivery', () => {
266
+ it.each([false, true])('continues after a completed compaction beyond the first message page (HTTP fallback: %s)', async (fallback) => {
267
+ const messages = Array.from({ length: 42 }, (_, index) => ({
268
+ id: `msg_${String(index).padStart(3, '0')}`,
269
+ type: index === 41 ? 'compaction' : index % 2 ? 'user' : 'assistant',
270
+ time: index === 41 ? { created: index + 1 } : { created: index + 1, completed: index + 2 },
271
+ model: { providerID: 'p', id: 'm' },
272
+ agent: 'build',
273
+ ...(index === 41 ? { status: 'completed', summary: 'Work remains' } : { text: 'Working' }),
274
+ }));
275
+ const fixture = makeFetch({ messages, liveStatus: 'idle' });
276
+ const runtime = makeRuntime();
277
+ try {
278
+ await withFetch(async (input, init) => {
279
+ const url = new URL(String(input));
280
+ if (pathEndsWith(url.pathname, '/session/ses-goal/message')) {
281
+ if (fallback && url.pathname.startsWith('/api/')) return jsonResponse({ error: 'unavailable' }, 503);
282
+ const ordered = url.searchParams.get('order') === 'desc' ? [...messages].reverse() : messages;
283
+ return jsonResponse({ data: ordered.slice(0, Number(url.searchParams.get('limit') || 40)), cursor: {} });
284
+ }
285
+ return fixture.fetch(input, init);
286
+ }, async () => runtime.runTick('ses-goal', '/repo'));
287
+ expect(fixture.calls.prompt).toBe(1);
288
+ } finally {
289
+ runtime.stop();
290
+ }
291
+ });
292
+
266
293
  it('dispatches v2 session.prompt with stable continuation id and metadata', async () => {
267
294
  const promptBodies = [];
268
295
  const messages = [
@@ -291,7 +318,7 @@ describe('session-goal continuation delivery', () => {
291
318
  return jsonResponse({ data: {} });
292
319
  }
293
320
  if (pathEndsWith(reqPath, '/session/ses-goal/message')) {
294
- return jsonResponse({ data: messages, cursor: {} });
321
+ return jsonResponse({ data: new URL(url).searchParams.get('order') === 'desc' ? [...messages].reverse() : messages, cursor: {} });
295
322
  }
296
323
  if (pathEndsWith(reqPath, '/session/ses-goal') && !reqPath.includes('/message')) {
297
324
  if (init.method === 'PATCH' && init.body) {
@@ -984,7 +1011,9 @@ describe('session-goal runtime — pause / audit / dispatch boundary', () => {
984
1011
  const body = Array.isArray(messages)
985
1012
  ? { data: messages, cursor: {} }
986
1013
  : messages;
987
- return jsonResponse(body);
1014
+ return jsonResponse(new URL(String(input)).searchParams.get('order') === 'desc'
1015
+ ? { ...body, data: [...body.data].reverse() }
1016
+ : body);
988
1017
  }
989
1018
  if (pathEndsWith(reqPath, '/session/ses-goal')) {
990
1019
  if (init.method === 'PATCH') {
@@ -50,18 +50,21 @@ extension-only VS Code retains the limitation below.
50
50
  assistant reply run concurrently. Generation publishes `isGenerating` via
51
51
  the existing Host metadata path, then the final title through `session.updated`.
52
52
  Later user admissions/idle events do not regenerate the initial title.
53
- The legacy first-idle edge remains a fallback when no admission was observed.
54
- A fork title (`(fork #n)`) waits for its first
55
- newly-created user message; the matching assistant completion triggers an
56
- immediate title refresh that bypasses inherited title metadata and throttle.
53
+ If `session.created` was missed, the admission's session read recovers
54
+ an empty/default title with no previous auto-title metadata and starts
55
+ immediately. The legacy first-idle edge remains a fallback when no
56
+ admission was observed.
57
+ A fork title (`(fork #n)`) refreshes on its first newly-admitted user
58
+ message, bypassing inherited title metadata and throttle without waiting
59
+ for the assistant. Legacy message-only events retain the idle fallback.
57
60
  If the fork's `session.created` was lost (SSE reconnect gap, server or
58
61
  OpenCode restart), the first newly-created user message lazily re-registers
59
62
  the pending fork from the session read inside `recordUserActivity` (fork
60
63
  title + message created after the fork + activity timestamp not yet past
61
- the fork time), so the first-reply refresh still fires.
64
+ the fork time), so the first-send refresh still fires.
62
65
  Ordinary later idle transitions do **not** arm another refresh. Any
63
66
  `busy`/`retry` status or a fresh user `message.updated` still clears an
64
- ordinary already-armed timer; an admitted first-title timer is exempt.
67
+ ordinary already-armed timer; admitted initial/fork-title timers are exempt.
65
68
  A sidebar smart-title request sets `titleRefresh.requestedAt`; its
66
69
  `session.updated` event arms the same flow immediately (forced / manual
67
70
  refresh is unaffected by the background gate).
@@ -141,6 +144,13 @@ auto updates for that session.
141
144
  Title language follows the user's real message text. Assistant responses, tool
142
145
  output, and transcript labels provide context without selecting the title language.
143
146
 
147
+ Attachments do not gate title generation. Admission retains user text plus at
148
+ most eight bounded filename/MIME descriptors, never attachment bytes or URLs.
149
+ Attachment-only messages also start generation; the text-only summary model can
150
+ name the attachment subject from those descriptors, but is explicitly told that
151
+ file contents are not included. Transcript-based/manual generation uses the
152
+ same file descriptors. This does not perform image recognition or file parsing.
153
+
144
154
  - Lives in the web server, so VS Code (extension-only) does not generate
145
155
  refreshes; it still receives `session.updated` title changes produced by a
146
156
  web/desktop instance of the same OpenCode server.
@@ -110,10 +110,12 @@ it('uses the real v2 client and publishes the generated title without another se
110
110
  }
111
111
  });
112
112
 
113
- it('starts the first title from admitted user text while the assistant is still running', async () => {
113
+ it.each(['new', 'missed-create', 'fork', 'missed-fork-create'].flatMap((scenario) =>
114
+ ['text', 'text-and-file', 'file-only'].map((content) => ({ scenario, content })),
115
+ ))('starts the first title before assistant completion ($scenario, $content)', async ({ scenario, content }) => {
114
116
  vi.useFakeTimers();
115
117
  const session = {
116
- id: 'ses_first', title: '', time: { created: 1, updated: 1 },
118
+ id: 'ses_first', title: scenario.includes('fork') ? 'Original (fork #1)' : '', time: { created: 1, updated: 1 },
117
119
  metadata: {},
118
120
  };
119
121
  let finishTitle;
@@ -135,26 +137,36 @@ it('starts the first title from admitted user text while the assistant is still
135
137
  });
136
138
  const emit = (type, data) => runtime.processPayload({ type, data, created: 1000, location: { directory: '/repo' } });
137
139
  try {
138
- emit('session.created', { sessionID: session.id, location: { directory: '/repo' } });
140
+ if (!scenario.startsWith('missed')) {
141
+ emit('session.created', { sessionID: session.id, title: session.title, time: session.time, location: { directory: '/repo' } });
142
+ }
139
143
  emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_synthetic', item: {
140
144
  type: 'synthetic', delivery: 'steer', payload: { text: 'Internal instructions' },
141
145
  } });
142
146
  await vi.runAllTimersAsync();
143
147
  expect(generate).not.toHaveBeenCalled();
144
148
  emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_first', item: {
145
- type: 'user', delivery: 'steer', payload: { text: 'Implement a session title generator' },
149
+ type: 'user', delivery: 'steer', payload: {
150
+ text: content === 'file-only' ? '' : 'Implement a session title generator',
151
+ ...(content !== 'text' ? { files: [{ name: 'title-bug.png', mime: 'image/png', source: { type: 'inline' }, data: 'YQ=='.repeat(100_000) }] } : {}),
152
+ },
146
153
  } });
147
154
  emit('session.execution.started', { sessionID: session.id });
148
155
  emit('session.status', { sessionID: session.id, status: { type: 'busy' } });
149
156
  await vi.runAllTimersAsync();
150
157
  expect(generate).toHaveBeenCalledTimes(1);
151
- expect(generate.mock.calls[0][0].prompt).toContain('Implement a session title generator');
158
+ const prompt = generate.mock.calls[0][0].prompt;
159
+ if (content !== 'file-only') expect(prompt).toContain('Implement a session title generator');
160
+ if (content !== 'text') expect(prompt).toContain('title-bug.png');
161
+ expect(prompt).not.toContain('YQ==');
162
+ expect(prompt.length).toBeLessThan(4000);
152
163
  expect(session.metadata.openchamber?.titleRefresh?.isGenerating).toBe(true);
153
164
  expect(access.messages).not.toHaveBeenCalled();
154
165
  finishTitle({ text: 'Session titles', providerID: 'test', modelID: 'test' });
155
166
  await vi.runAllTimersAsync();
156
167
  expect(session.title).toBe('Session titles');
157
168
  expect(session.metadata.openchamber.titleRefresh.isGenerating).toBeUndefined();
169
+ expect(access.messages).not.toHaveBeenCalled();
158
170
  emit('session.execution.succeeded', { sessionID: session.id });
159
171
  emit('session.inbox.enqueued', { sessionID: session.id, inboxID: 'msg_second', item: {
160
172
  type: 'user', delivery: 'steer', payload: { text: 'Follow up on the implementation' },
@@ -1,7 +1,7 @@
1
1
  // Session title refresh: regenerate the sidebar title from the conversation's
2
2
  // MAIN SUBJECT (overall feature / goal), not just the last wrap-up utterance.
3
3
  // OpenCode only titles once from the first user message. Auto refresh is sparse
4
- // for title stability: first idle of a new session, first newly-sent reply on a
4
+ // for title stability: first user admission of a new session, first send on a
5
5
  // fork, plus explicit smart-title / forced refresh. Continuing the same work
6
6
  // should keep naming the thing being done — not "commit and push". This
7
7
  // watcher throttles to at most one refresh per session every TITLE_THROTTLE_MS
@@ -201,13 +201,22 @@ const extractUserMessage = (payload) => {
201
201
  const item = properties.item;
202
202
  if (typeof properties.sessionID !== 'string' || item?.type !== 'user') return null;
203
203
  const text = typeof item.payload?.text === 'string' ? item.payload.text.slice(0, TRANSCRIPT_USER_CHAR_LIMIT) : '';
204
+ const files = Array.isArray(item.payload?.files) ? item.payload.files.slice(0, 8) : [];
205
+ const parts = [
206
+ ...(text.trim() ? [{ type: 'text', text }] : []),
207
+ ...files.filter((file) => file && typeof file === 'object').map((file) => ({
208
+ type: 'file',
209
+ filename: typeof file.name === 'string' ? file.name.slice(0, 200) : '',
210
+ mime: typeof file.mime === 'string' ? file.mime.slice(0, 100) : '',
211
+ })),
212
+ ];
204
213
  return {
205
214
  sessionId: properties.sessionID,
206
215
  createdAt: payload.created ?? 0,
207
216
  directory: payload.location?.directory ?? '',
208
- initialRecord: text.trim() ? {
217
+ initialRecord: parts.length ? {
209
218
  info: { id: properties.inboxID, role: 'user' },
210
- parts: [{ type: 'text', text }],
219
+ parts,
211
220
  } : null,
212
221
  };
213
222
  }
@@ -230,7 +239,13 @@ const messagePartsToText = (message) => {
230
239
  ? TRANSCRIPT_ASSISTANT_CHAR_LIMIT
231
240
  : TRANSCRIPT_USER_CHAR_LIMIT;
232
241
  return parts
233
- .map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : ''))
242
+ .map((part) => {
243
+ if (part?.type === 'text' && typeof part.text === 'string') return part.text;
244
+ if (part?.type !== 'file') return '';
245
+ const filename = typeof part.filename === 'string' ? part.filename.slice(0, 200) : '';
246
+ const mime = typeof part.mime === 'string' ? part.mime.slice(0, 100) : '';
247
+ return `[Attachment: ${filename || mime || 'file'}${filename && mime ? ` (${mime})` : ''}; contents not included]`;
248
+ })
234
249
  .filter(Boolean)
235
250
  .join('\n')
236
251
  .slice(0, charLimit);
@@ -495,7 +510,7 @@ export const createSessionTitleRuntime = ({
495
510
  });
496
511
  };
497
512
 
498
- const recordUserActivity = async (sessionId, directory, createdAt) => {
513
+ const recordUserActivity = async (sessionId, directory, createdAt, initialRecord) => {
499
514
  if (!Number.isFinite(createdAt) || createdAt <= 0) return;
500
515
  const session = await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, { directory })
501
516
  .catch(() => null);
@@ -508,7 +523,7 @@ export const createSessionTitleRuntime = ({
508
523
  // never learned about the fork. The first newly-created user message on a
509
524
  // fork-titled session (message created after the fork, before any activity
510
525
  // timestamp advanced past the fork) rebuilds that pending entry so the
511
- // matching idle still triggers the first-refresh. Idempotent: the normal
526
+ // first admission can trigger the refresh. Idempotent: the normal
512
527
  // session.created path already holds a pending entry, and once activity
513
528
  // advances past the fork time later messages never re-register.
514
529
  const forkCreated = Number(session?.time?.created);
@@ -526,6 +541,26 @@ export const createSessionTitleRuntime = ({
526
541
  });
527
542
  }
528
543
 
544
+ // Recover first-send admission even when session.created was missed.
545
+ // Use the admitted text: the message projection may not exist yet.
546
+ if (initialRecord && !stopped && !inflight.has(sessionId) && !initialRefreshes.has(sessionId)
547
+ && !forkFirstRefreshes.has(sessionId) && !session.parentID && !isSystemOwnedSession(session)) {
548
+ const pendingFork = forkFirstSendPending.get(sessionId);
549
+ const firstForkSend = pendingFork && createdAt > pendingFork.createdAt;
550
+ const untitled = !session.title || isDefaultSessionTitle(session.title);
551
+ if (firstForkSend || (untitled && !meta.generatedAt && !meta.lastAutoTitle)) {
552
+ newSessions.delete(sessionId);
553
+ if (firstForkSend) {
554
+ forkFirstSendPending.delete(sessionId);
555
+ forkFirstRefreshes.add(sessionId);
556
+ } else {
557
+ initialRefreshes.add(sessionId);
558
+ }
559
+ initialUserMessages.set(sessionId, initialRecord);
560
+ armTimer(sessionId, directory, 0);
561
+ }
562
+ }
563
+
529
564
  const previous = Number(meta.titleRefresh.activityUpdatedAt);
530
565
  const activityUpdatedAt = Number.isFinite(previous) ? Math.max(previous, createdAt) : createdAt;
531
566
  if (activityUpdatedAt === previous) return;
@@ -683,7 +718,7 @@ export const createSessionTitleRuntime = ({
683
718
  return;
684
719
  }
685
720
 
686
- const latestMessages = await fetchRecentMessages(sessionId, directory);
721
+ const latestMessages = initialUserMessage ? null : await fetchRecentMessages(sessionId, directory);
687
722
  const latestAssistantId = (() => {
688
723
  if (!latestMessages) return null;
689
724
  for (let i = latestMessages.length - 1; i >= 0; i -= 1) {
@@ -777,7 +812,7 @@ export const createSessionTitleRuntime = ({
777
812
  initialRefreshes.add(status.sessionId);
778
813
  armTimer(status.sessionId, status.directory || directoryHint, 0);
779
814
  }
780
- } else if (!initialRefreshes.has(status.sessionId)) {
815
+ } else if (!initialRefreshes.has(status.sessionId) && !forkFirstRefreshes.has(status.sessionId)) {
781
816
  clearTimer(status.sessionId);
782
817
  }
783
818
  return;
@@ -797,6 +832,7 @@ export const createSessionTitleRuntime = ({
797
832
  userMessage.sessionId,
798
833
  userMessage.directory || directoryHint,
799
834
  userMessage.createdAt,
835
+ userMessage.initialRecord,
800
836
  ).catch((error) => {
801
837
  console.warn('[session-title] failed to record user activity:', error?.message || error);
802
838
  });
@@ -804,7 +840,7 @@ export const createSessionTitleRuntime = ({
804
840
  // session settles. Only a message created after the timer was armed
805
841
  // means the user actually moved on.
806
842
  const armed = timers.get(userMessage.sessionId);
807
- if (armed && !initialRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
843
+ if (armed && !initialRefreshes.has(userMessage.sessionId) && !forkFirstRefreshes.has(userMessage.sessionId) && userMessage.createdAt >= armed.armedAt) {
808
844
  clearTimer(userMessage.sessionId);
809
845
  }
810
846
  }