@@ -20,7 +20,6 @@ import {
2020 resolveBillingAttribution ,
2121 resolveOrganizationBillingAttribution ,
2222} from '@/lib/billing/core/attribution'
23- import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
2423import { type AtomicClaimResult , chatSendIdempotency } from '@/lib/core/idempotency'
2524import { asOrchestrationError , statusForOrchestrationError } from '@/lib/core/orchestration/types'
2625import { listPersonalCredentials } from '@/lib/credentials/application/personal-credentials'
@@ -30,7 +29,6 @@ import {
3029} from '@/lib/knowledge/access/availability'
3130import { loadCopilotSearchIntegrations } from '@/lib/mothership/application/load-search-integrations'
3231import { chatOperations } from '@/lib/mothership/application/operations'
33- import { resolveInvocationWorkspace } from '@/lib/mothership/application/workspace-target'
3432import {
3533 authoringInvocation ,
3634 createCopilotResourceAdmission ,
@@ -51,18 +49,15 @@ import {
5149 DESKTOP_TERMINAL_HINT_ID_MAX_LENGTH ,
5250 DESKTOP_TERMINAL_HINT_TEXT_MAX_LENGTH ,
5351} from '@/lib/mothership/chat/desktop-capabilities'
54- import { resolveFileAttachment } from '@/lib/mothership/chat/file-attachment'
5552import {
5653 type ChatLoadResult ,
5754 loadChatMcpServerIds ,
5855 resolveOrCreateChat ,
5956} from '@/lib/mothership/chat/lifecycle'
6057import { authorizeOrganizationChat } from '@/lib/mothership/chat/organization-chats'
6158import { buildCopilotRequestPayload } from '@/lib/mothership/chat/payload'
62- import {
63- processContextsServer ,
64- resolveActiveResourceContext ,
65- } from '@/lib/mothership/chat/process-contents'
59+ import { processContextsServer } from '@/lib/mothership/chat/process-contents'
60+ import { resolveResourceAttachmentContexts } from '@/lib/mothership/chat/resource-attachments'
6661import {
6762 MAX_FILE_SELECTION_TEXT_LENGTH ,
6863 MAX_TABLE_SELECTION_COLUMNS ,
@@ -100,13 +95,7 @@ import {
10095} from '@/lib/mothership/request/session'
10196import { getLocalChatStreamLease } from '@/lib/mothership/request/session/abort'
10297import type { ExecutionContext } from '@/lib/mothership/request/types'
103- import { persistChatResources } from '@/lib/mothership/resources/persistence'
104- import { searchResourceMatchesOwner } from '@/lib/mothership/resources/search'
105- import {
106- hasAddressableId ,
107- isEphemeralResource ,
108- sanitizeChatResources ,
109- } from '@/lib/mothership/resources/types'
98+ import { hasAddressableId } from '@/lib/mothership/resources/types'
11099import { prepareExecutionContext } from '@/lib/mothership/tools/handlers/context'
111100import { isWorkspaceCapabilityWithheld } from '@/lib/permission-groups/capability-assertions'
112101import { capabilityRefusalResponse } from '@/lib/permission-groups/capability-response'
@@ -135,38 +124,6 @@ const FileAttachmentSchema = z.object({
135124 path : z . string ( ) . optional ( ) ,
136125} )
137126
138- const GENERIC_RESOURCE_TITLE : Record < z . infer < typeof ResourceAttachmentSchema > [ 'type' ] , string > = {
139- search : 'Search results' ,
140- sources : 'Sources' ,
141- workflow : 'Workflow' ,
142- table : 'Table' ,
143- integration : 'Integration' ,
144- file : 'File' ,
145- dashboard : 'Dashboard' ,
146- test : 'Test' ,
147- knowledgebase : 'Knowledge Base' ,
148- folder : 'Folder' ,
149- filefolder : 'File Folder' ,
150- task : 'Task' ,
151- log : 'Log' ,
152- generic : 'Resource' ,
153- browser : 'Browser' ,
154- terminal : 'Terminal' ,
155- }
156-
157- /**
158- * Synthetic client-side panels are context-only: never persisted to the chat.
159- * Browser tabs are among them — the desktop app restores its own pages — so
160- * their page title and URL remain request context only.
161- */
162- function isPersistableAttachment ( resource : z . infer < typeof ResourceAttachmentSchema > ) : boolean {
163- return ! isEphemeralResource ( {
164- type : resource . type ,
165- id : resource . id ,
166- title : resource . title ?? '' ,
167- } )
168- }
169-
170127/**
171128 * Drops open tabs the client cannot address, so one unusable tab does not fail
172129 * the whole message — clients on a stale bundle still send them. A non-string
@@ -622,133 +579,19 @@ async function resolveAgentContexts(params: {
622579 }
623580 }
624581
625- const authorizedResources : z . infer < typeof mothershipResourceSchema > [ ] = [ ]
626- if (
627- Array . isArray ( resourceAttachments ) &&
628- resourceAttachments . length > 0 &&
629- ( workspaceId || organizationId )
630- ) {
631- const results = await Promise . allSettled (
632- resourceAttachments . map ( async ( resource ) => {
633- if ( resource . type === 'sources' ) return null
634- // The live browser panel resolves from the attachment itself: its
635- // page state is client-held (the desktop app's embedded browser),
636- // not a workspace entity the server could look up.
637- if ( resource . type === 'browser' ) {
638- if ( ! resource . url ) return null
639- const title = resource . title ?. trim ( )
640- return {
641- type : 'active_resource' ,
642- tag : resource . active ? '@active_tab' : '@open_tab' ,
643- content : `The user's ${
644- resource . active ? 'currently visible browser tab' : 'other open browser tab'
645- } is open on: ${
646- title ? `"${ title } " — ` : ''
647- } ${ resource . url } . ${ browserAvailable ? 'Browser tools are available for inspecting and interacting with this tab.' : 'This attachment supplies only the title and URL; browser tools are unavailable.' } `,
648- }
649- }
650- if ( resource . type === 'search' ) {
651- if (
652- ! resource . search ||
653- ! searchResourceMatchesOwner ( resource . search , { organizationId, workspaceId } )
654- )
655- return null
656- if ( persistResources )
657- authorizedResources . push (
658- mothershipResourceSchema . parse ( { ...resource , title : 'Search results' } )
659- )
660- return {
661- type : 'active_resource' ,
662- tag : resource . active ? '@active_tab' : '@open_tab' ,
663- content : `The user's Search results tab has this retrieval address: ${ JSON . stringify ( resource . search ) } . This is query context, not retrieved evidence; use the search tools for current authorized results.` ,
664- }
665- }
666- if ( resource . type === 'file' ) {
667- const { context, tab } = await resolveFileAttachment ( resource , {
668- userId,
669- workspaceId,
670- organizationId,
671- chatId,
672- requestMode,
673- } )
674- if ( persistResources && tab ) authorizedResources . push ( tab )
675- return context
676- }
677- const target =
678- organizationId || resource . workspaceId
679- ? await resolveInvocationWorkspace (
680- { userId, workspaceId, organizationId, chatId } ,
681- resource . workspaceId
682- )
683- : workspaceId
684- ? { workspaceId }
685- : null
686- if ( ! target ) return null
687- if (
688- resource . owner &&
689- ( resource . owner . entityType !== 'workspace' ||
690- resource . owner . entityId !== target . workspaceId )
691- )
692- throw new Error ( 'File owner does not match the selected workspace' )
693- const ctx = await withWorkspaceInvocationScope (
694- { workspaceId : target . workspaceId , organizationId } ,
695- ( ) =>
696- resolveActiveResourceContext (
697- resource . type ,
698- resource . id ,
699- target . workspaceId ,
700- userId ,
701- chatId ,
702- resource . viewId ,
703- resource . currentView
704- )
705- )
706- if ( ! ctx ) return null
707- if ( persistResources && isPersistableAttachment ( resource ) )
708- authorizedResources . push (
709- mothershipResourceSchema . parse ( {
710- ...resource ,
711- title : resource . title ?? GENERIC_RESOURCE_TITLE [ resource . type ] ,
712- ...( organizationId
713- ? { workspaceId : target . workspaceId }
714- : { workspaceId : undefined , workspaceName : undefined } ) ,
715- } )
716- )
717- return {
718- ...ctx ,
719- ...( organizationId
720- ? { content : `Workspace ${ target . workspaceId } :\n${ ctx . content } ` }
721- : { } ) ,
722- tag : resource . active ? '@active_tab' : '@open_tab' ,
723- }
724- } )
725- )
726-
727- results . forEach ( ( result , index ) => {
728- if ( result . status === 'fulfilled' ) {
729- if ( result . value ) agentContexts . push ( result . value )
730- return
731- }
732- // A tab can outlive its resource or the viewer's access to it; the turn
733- // goes without that context.
734- const resourceType = resourceAttachments [ index ] ?. type
735- const code = asOrchestrationError ( result . reason ) ?. code
736- if ( code === 'not_found' ) {
737- logger . info ( `[${ requestId } ] Skipped resource attachment that no longer exists` , {
738- resourceType,
739- } )
740- } else if ( code === 'forbidden' ) {
741- logger . warn ( `[${ requestId } ] Skipped resource attachment the user can no longer access` , {
742- resourceType,
743- } )
744- } else {
745- logger . error ( `[${ requestId } ] Failed to resolve resource attachment` , result . reason )
746- }
747- } )
748- }
749-
750- if ( chatId && authorizedResources . length )
751- await persistChatResources ( chatId , sanitizeChatResources ( authorizedResources ) )
582+ agentContexts . push (
583+ ...( await resolveResourceAttachmentContexts ( {
584+ resourceAttachments,
585+ userId,
586+ workspaceId,
587+ organizationId,
588+ chatId,
589+ requestMode,
590+ persistResources,
591+ browserAvailable,
592+ requestId,
593+ } ) )
594+ )
752595 return agentContexts
753596}
754597
0 commit comments