11import { useEffect , useState } from "react" ;
2- import { ChatApiError , fetchChatSessions , fetchLoopXMode , fetchLoopXTeamWork , readLoopXTeamWork } from "../../data/chat" ;
3- import { TeamArtifactReport , isMarkdownArtifact , type TeamArtifact } from "./team-artifact-content" ;
2+ import { ChatApiError , fetchChatSessions , fetchLoopXMode , fetchLoopXTeamWork , fetchManagedGoalResults , readLoopXTeamWork , readManagedGoalResult , type ManagedGoalResultRow } from "../../data/chat" ;
3+ import { TeamArtifactReport , isMarkdownArtifact , managedReportArtifact , type TeamArtifact } from "./team-artifact-content" ;
44
55type Readback = { kind : "waiting" | "unavailable" | "multiple" } | {
66 kind : "adopted" ; artifact : TeamArtifact ; agentId : string ;
77} ;
8+ type ManagedReadback = { kind : "waiting" | "unavailable" | "multiple" } | {
9+ kind : "accepted" ; artifact : TeamArtifact ; agentId : string ; title : string ;
10+ } ;
811
912/**
1013 * The Todo identities a Goal conversation itself reports work for, or the
@@ -21,6 +24,11 @@ const WORK_INDEX_WINDOW_MS = 30_000;
2124const RELATED_SESSION_LIMIT = 8 ;
2225const workIndexes = new Map < string , GoalWorkIndex > ( ) ;
2326const workIndexReads = new Map < string , Promise < GoalWorkIndex > > ( ) ;
27+ /** Goal-wide inventory plus the exact Todo ids the server could not verify, so a
28+ * plan scopes its readback to its own ids instead of the whole Goal's health. */
29+ type ManagedGoalIndex = { readAt : number ; rows : ManagedGoalResultRow [ ] ; unavailableTodoIds : Set < string > } ;
30+ const managedIndexes = new Map < string , ManagedGoalIndex > ( ) ;
31+ const managedIndexReads = new Map < string , Promise < ManagedGoalIndex > > ( ) ;
2432
2533/** A 4xx is the server declining this conversation's team readback: without a
2634 * coordinator identity it cannot own delegation work, so it is unrelated rather
@@ -154,11 +162,87 @@ async function readAdoptedResult(goalId: string, todoIds: Set<string>, force: bo
154162 return adopted . values ( ) . next ( ) . value ?? { kind : "waiting" } ;
155163}
156164
165+ async function collectManagedGoalIndex ( goalId : string ) : Promise < ManagedGoalIndex > {
166+ let cursor : string | undefined ;
167+ let total : number | undefined ;
168+ const rows : ManagedGoalResultRow [ ] = [ ] ;
169+ const unavailableTodoIds = new Set < string > ( ) ;
170+ // Page until the snapshot ends. The previous fixed eight-page budget hid a
171+ // matching report that happened to sort later in the Goal's history.
172+ for ( ; ; ) {
173+ const page = await fetchManagedGoalResults ( goalId , cursor ) ;
174+ if ( ! Array . isArray ( page . items ) || ! Number . isInteger ( page . total ) || page . total < 0 ||
175+ ( total !== undefined && page . total !== total ) ||
176+ ! Number . isInteger ( page . unavailable_count ) || page . unavailable_count < 0 ||
177+ ! Array . isArray ( page . unavailable_todo_ids ) ||
178+ page . unavailable_count !== page . unavailable_todo_ids . length ||
179+ ( page . next_cursor !== null && ( ! page . next_cursor || typeof page . next_cursor !== "string" ) ) ) {
180+ throw new Error ( "managed inventory incomplete" ) ;
181+ }
182+ total = page . total ;
183+ rows . push ( ...page . items ) ;
184+ for ( const todoId of page . unavailable_todo_ids ) {
185+ if ( typeof todoId !== "string" || ! todoId ) throw new Error ( "managed inventory incomplete" ) ;
186+ unavailableTodoIds . add ( todoId ) ;
187+ }
188+ if ( ! page . next_cursor ) return { readAt : Date . now ( ) , rows, unavailableTodoIds} ;
189+ if ( page . next_cursor === cursor ) throw new Error ( "managed cursor repeated" ) ;
190+ if ( page . items . length === 0 && page . unavailable_count === 0 ) {
191+ throw new Error ( "managed inventory made no progress" ) ;
192+ }
193+ cursor = page . next_cursor ;
194+ }
195+ }
196+
197+ /** Share the Goal inventory across plan cards; manual refresh always bypasses the short cache. */
198+ async function readManagedGoalIndex ( goalId : string , force : boolean ) : Promise < ManagedGoalIndex > {
199+ const running = managedIndexReads . get ( goalId ) ;
200+ if ( running ) return running ;
201+ const cached = managedIndexes . get ( goalId ) ;
202+ if ( ! force && cached && Date . now ( ) - cached . readAt < WORK_INDEX_WINDOW_MS ) return cached ;
203+ const read = collectManagedGoalIndex ( goalId )
204+ . then ( index => { managedIndexes . set ( goalId , index ) ; return index ; } )
205+ . finally ( ( ) => { managedIndexReads . delete ( goalId ) ; } ) ;
206+ managedIndexReads . set ( goalId , read ) ;
207+ return read ;
208+ }
209+
210+ /** A confirmed plan supplies the only Todo ids that may return to its source conversation. */
211+ async function readManagedPlanResult ( goalId : string , todoIds : Set < string > , force : boolean ) : Promise < ManagedReadback > {
212+ try {
213+ const index = await readManagedGoalIndex ( goalId , force ) ;
214+ // Only a plan-owned unreadable row can hide this plan's report; unrelated
215+ // historical rows stay the server's and the Files view's concern.
216+ for ( const todoId of todoIds ) {
217+ if ( index . unavailableTodoIds . has ( todoId ) ) return { kind : "unavailable" } ;
218+ }
219+ const matched = new Map < string , ManagedGoalResultRow > ( ) ;
220+ for ( const row of index . rows ) {
221+ if ( ! todoIds . has ( row . todo_id ) ) continue ;
222+ if ( ! row . sha256 || ! row . producer_agent_id || ! row . title ) return { kind : "unavailable" } ;
223+ const previous = matched . get ( row . todo_id ) ;
224+ if ( previous && previous . sha256 !== row . sha256 ) return { kind : "unavailable" } ;
225+ matched . set ( row . todo_id , row ) ;
226+ }
227+ if ( matched . size > 1 ) return { kind : "multiple" } ;
228+ const row = matched . values ( ) . next ( ) . value ;
229+ if ( ! row ) return { kind : "waiting" } ;
230+ const read = await readManagedGoalResult ( goalId , row . todo_id ) ;
231+ if ( read . goal_id !== goalId || read . todo_id !== row . todo_id ||
232+ read . result . sha256 !== row . sha256 || read . result . producer_agent_id !== row . producer_agent_id ||
233+ read . result . content_type !== row . content_type || typeof read . text !== "string" ) return { kind : "unavailable" } ;
234+ return { kind : "accepted" , artifact : managedReportArtifact ( row . content_type , row . sha256 , read . text ) ,
235+ agentId : row . producer_agent_id , title : row . title } ;
236+ } catch { /* A failed inventory or exact read cannot keep an earlier report visible. */ }
237+ return { kind : "unavailable" } ;
238+ }
239+
157240/** Return only an accepted, currently adopted report to the manager conversation. */
158241export function ManagerTeamResult ( { goalId, todoIds, zh, onOpenGoalEvidence} : {
159242 goalId : string ; todoIds : string [ ] ; zh : boolean ; onOpenGoalEvidence : ( goalId : string ) => void ;
160243} ) {
161244 const [ result , setResult ] = useState < { key : string ; readback : Readback } | null > ( null ) ;
245+ const [ managed , setManaged ] = useState < { key : string ; readback : ManagedReadback } | null > ( null ) ;
162246 const [ request , setRequest ] = useState ( { count : 0 , force : false } ) ;
163247 const todoKey = [ ...todoIds ] . sort ( ) . join ( "," ) ;
164248 // A new read must withdraw the previous accepted report immediately. The
@@ -170,6 +254,9 @@ export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
170254 void readAdoptedResult ( goalId , new Set ( todoKey . split ( "," ) ) , request . force )
171255 . then ( value => { if ( ! cancelled ) setResult ( { key, readback : value } ) ; } )
172256 . catch ( ( ) => { if ( ! cancelled ) setResult ( { key, readback : { kind : "unavailable" } } ) ; } ) ;
257+ void readManagedPlanResult ( goalId , new Set ( todoKey . split ( "," ) ) , request . force )
258+ . then ( value => { if ( ! cancelled ) setManaged ( { key, readback : value } ) ; } )
259+ . catch ( ( ) => { if ( ! cancelled ) setManaged ( { key, readback : { kind : "unavailable" } } ) ; } ) ;
173260 return ( ) => { cancelled = true ; } ;
174261 } , [ goalId , todoKey , key , request . force ] ) ;
175262 useEffect ( ( ) => {
@@ -179,16 +266,25 @@ export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
179266 return ( ) => window . clearInterval ( timer ) ;
180267 } , [ ] ) ;
181268 const readback = result ?. key === key ? result . readback : null ;
269+ const managedReadback = managed ?. key === key ? managed . readback : null ;
182270 if ( ! goalId || ! todoKey ) return null ;
183- return < section className = { `personal-manager-team-result is-${ readback ?. kind ?? "loading" } ` } aria-label = { zh ? "团队结果回到管家" : "Team result returned to manager" } aria-busy = { ! readback } >
271+ return < section className = { `personal-manager-team-result is-${ readback ?. kind ?? "loading" } ` } aria-label = { zh ? "团队结果回到管家" : "Team result returned to manager" } aria-busy = { ! readback || ! managedReadback } >
184272 { ! readback ? < p role = "status" > { zh ? "正在核验团队结果…" : "Verifying team result…" } </ p > : readback . kind === "adopted" ? < >
185273 < header > < strong > { zh ? "团队验收结果" : "Team result" } </ strong > < small > { goalId } · { readback . agentId } </ small > </ header >
186274 < TeamArtifactReport artifact = { readback . artifact } zh = { zh } heading = { zh ? "依据已采用 · 结果已验收" : "Source adopted · Result accepted" } />
187- </ > : < p role = "status" > { readback . kind === "unavailable"
275+ </ > : readback . kind === "waiting" && managedReadback ?. kind === "accepted" ? null : < p role = "status" > { readback . kind === "unavailable"
188276 ? ( zh ? "团队结果或采用证据无法核验,请到 Goal 查看版本关系。" : "Team result or adoption evidence cannot be verified; inspect versions in the Goal." )
189277 : readback . kind === "multiple"
190278 ? ( zh ? "有多个已验收的下游结果,请到 Goal 选择要采用的结论。" : "Multiple downstream results are accepted; choose the conclusion in the Goal." )
191279 : ( zh ? "团队任务已分配,尚无可核验的已采用结果。" : "Team work is assigned; no verifiable adopted result yet." ) } </ p > }
280+ { managedReadback ?. kind === "accepted" ? < div className = "personal-manager-managed-report" >
281+ < header > < strong > { zh ? "托管团队报告 · 已验收,采用尚未核验" : "Managed report · Accepted, adoption not verified" } </ strong >
282+ < small > { managedReadback . agentId } </ small > </ header >
283+ < TeamArtifactReport artifact = { managedReadback . artifact } zh = { zh } heading = { managedReadback . title } />
284+ </ div > : managedReadback ?. kind === "multiple" ? < p role = "status" > { zh
285+ ? "这次分配已有多份托管报告;请到 Goal 选择结论。" : "This assignment has multiple managed reports; choose a conclusion in the Goal." } </ p >
286+ : managedReadback ?. kind === "unavailable" ? < p role = "status" > { zh
287+ ? "托管报告无法核验;旧内容已撤回。" : "Managed report cannot be verified; previous content was withdrawn." } </ p > : null }
192288 < div className = "personal-manager-team-result-actions" >
193289 < button type = "button" onClick = { ( ) => onOpenGoalEvidence ( goalId ) } > { zh ? "查看证据与任务" : "Inspect evidence and tasks" } </ button >
194290 < button type = "button" disabled = { ! readback } onClick = { ( ) => setRequest ( previous => ( { count : previous . count + 1 , force : true } ) ) } > { zh ? "刷新结果" : "Refresh result" } </ button >
0 commit comments