@@ -5,7 +5,7 @@ import type { AppContext } from "../core/context.js";
55import {
66 ManagedAgentsClient , MANAGED_AGENT_ID , managedAgentError , probeManagedReadiness ,
77 type ManagedReadiness , type ReadinessGate ,
8- type ManagedAgent , type ManagedAgentConfig , type AgentMessage ,
8+ type ManagedAgent , type ManagedAgentConfig , type AgentMessage , type MessageAdmission ,
99} from "../core/managed_agents.js" ;
1010import { theme } from "../ui/theme.js" ;
1111import { sanitizeTerm , sliceVisible , visibleWidth , wrapVisible } from "../ui/text.js" ;
@@ -17,6 +17,7 @@ import { managedChatInput } from "../ui/managed_chat_input.js";
1717export interface ManagedChatContext {
1818 chat : "connecting" | "synced" | "paused" ;
1919 checkedAt ?: number ;
20+ profileCheckedAt ?: number ;
2021 mode ?: string ;
2122 memory ?: string ;
2223 strategies ?: string ;
@@ -99,15 +100,30 @@ export function renderManagedAgents(agents: ManagedAgent[], columns = process.st
99100 return rows . flatMap ( row => wrapVisible ( row , width ) ) . join ( "\n" ) + "\n" ;
100101}
101102
102- /** A compact status strip above the composer; no sidebar steals narrow widths. */
103+ function checkedTime ( value : number | undefined ) : string {
104+ return value !== undefined && Number . isFinite ( value ) && ! Number . isNaN ( new Date ( value ) . getTime ( ) )
105+ ? new Date ( value ) . toISOString ( ) : "not checked" ;
106+ }
107+
108+ function isAts ( agent : ManagedAgent ) : boolean {
109+ return agent . config . profile ?. schema_version === "aether.managed-agent.profile/1" && agent . config . profile . kind === "ats" ;
110+ }
111+
112+ /** Read-only status from Cloud and, for typed ATS agents, checked local setup. */
103113export function renderManagedContext ( agent : ManagedAgent , state : ManagedChatContext , columns : number ) : string {
104114 const width = Math . max ( 20 , Math . floor ( columns ) ) ;
105115 const clean = ( value : string ) : string => sanitizeTerm ( value ) . replace ( / [ \r \n \t ] / g, " " ) ;
106- const lines = [
107- `${ clean ( agent . config . identity . display_name ) } · ${ clean ( agent . runtime . tile_state ) } · chat ${ state . chat } ` ,
108- `mode ${ state . mode ?? "unconfirmed" } · memory ${ state . memory ?? "unverified" } · strategies ${ state . strategies ?? "unverified" } ` ,
109- `browser ${ state . browser ?? "unverified" } · data ${ state . data ?? "unverified" } ` ,
110- ] ;
116+ const name = sliceVisible ( clean ( agent . config . identity . display_name ) , Math . min ( 24 , width - 16 ) ) ;
117+ const dm = `DM ${ state . chat } ` ;
118+ const dmChecked = `checked ${ checkedTime ( state . checkedAt ) } ` ;
119+ const lines = [ `${ name } · ${ sliceVisible ( clean ( agent . runtime . tile_state ) , 16 ) } ` ] ;
120+ lines . push ( ...( visibleWidth ( `${ dm } · ${ dmChecked } ` ) <= width ? [ `${ dm } · ${ dmChecked } ` ] : [ dm , dmChecked ] ) ) ;
121+ if ( isAts ( agent ) ) {
122+ const details = checkedTime ( state . profileCheckedAt ) === "not checked" ? [ ] : ( [ "mode" , "memory" , "strategies" , "browser" , "data" ] as const )
123+ . flatMap ( key => state [ key ] && ! / ^ ( u n v e r i f i e d | u n c o n f i r m e d ) $ / i. test ( state [ key ] ) ? [ `${ key } ${ clean ( state [ key ] ) } ` ] : [ ] ) ;
124+ if ( details . length ) lines . push ( "ATS local" , `checked ${ checkedTime ( state . profileCheckedAt ) } ` , ...details ) ;
125+ else lines . push ( "ATS local · not checked; local setup does not verify live orders" ) ;
126+ }
111127 return lines . flatMap ( line => wrapVisible ( theme . dim ( clean ( line ) ) , width ) ) . join ( "\n" ) ;
112128}
113129
@@ -220,10 +236,45 @@ async function pickManagedAgent(agents: ManagedAgent[], out: Writable, signal?:
220236 } ) ;
221237}
222238
223- function renderMessage ( message : AgentMessage , agent : ManagedAgent ) : string {
239+ function admissionLabel ( admission : MessageAdmission | undefined ) : string {
240+ if ( ! admission ) return "saved · admission unconfirmed" ;
241+ switch ( admission . state ) {
242+ case "admitted" : return "admitted" ;
243+ case "replied" : return "replied" ;
244+ case "blocked_budget" : return "saved · blocked by budget" ;
245+ case "blocked_policy" : return "saved · blocked by policy" ;
246+ case "needs_review" : return "saved · needs review" ;
247+ case "saved" : return "saved · admission pending" ;
248+ }
249+ }
250+
251+ export function renderManagedMessage ( message : AgentMessage , agent : ManagedAgent , columns = 80 ) : string {
224252 const fromAgent = message . sender_type === "agent" || Boolean ( message . sender_agent_id ) ;
225253 const label = fromAgent ? agent . config . identity . display_name : "You" ;
226- return `\n${ theme . cyan ( sanitizeTerm ( label ) ) } \n${ sanitizeTerm ( message . body ) } \n` ;
254+ const parsed = message . created_at ? Date . parse ( message . created_at ) : NaN ;
255+ const time = Number . isFinite ( parsed ) ? new Date ( parsed ) . toISOString ( ) : "time unknown" ;
256+ const admission = ! fromAgent ? ` · ${ admissionLabel ( message . admission ) } ` : "" ;
257+ const width = Math . max ( 20 , Math . floor ( columns ) ) ;
258+ const cleanLabel = sliceVisible ( sanitizeTerm ( label ) . replace ( / [ \r \n \t ] / g, " " ) , Math . min ( 32 , width - 16 ) ) ;
259+ const speakerTime = `${ cleanLabel } · ${ time } ` ;
260+ const header = [
261+ ...( visibleWidth ( speakerTime ) <= width ? [ speakerTime ] : [ cleanLabel , time ] ) ,
262+ ...( admission ? wrapVisible ( admission . slice ( 3 ) , width ) : [ ] ) ,
263+ ] . join ( "\n" ) ;
264+ const body = sanitizeTerm ( message . body ) . split ( "\n" ) . flatMap ( line => wrapVisible ( line , width ) ) . join ( "\n" ) ;
265+ return `\n${ header } \n${ body } \n` ;
266+ }
267+
268+ export function renderManagedAdmission ( admission : MessageAdmission | undefined ) : string {
269+ const state = admission ?. state ;
270+ const next : Record < string , string > = {
271+ blocked_budget : "Next: configure this agent's UVT limits, then send a new message." ,
272+ blocked_policy : "Next: review this agent's permissions in Online before trying again." ,
273+ needs_review : "Next: review this message in Online." ,
274+ saved : "Next: use /refresh to check whether Cloud admitted this message." ,
275+ unreported : "Next: check the shared conversation before sending again." ,
276+ } ;
277+ return `${ admissionLabel ( admission ) } ${ next [ state ?? "unreported" ] ? ` · ${ next [ state ?? "unreported" ] } ` : "" } ` ;
227278}
228279
229280/** Scoped managed-chat hotkey. Repeated key events cannot queue mode changes
@@ -274,18 +325,17 @@ export async function cmdManagedAgentChat(ctx: AppContext, id: string | undefine
274325 let timer : ReturnType < typeof setInterval > | undefined ;
275326 let closeSession : void | ( ( ) => Promise < void > ) = undefined ;
276327 let reader : Interface | undefined ;
328+ let promptReady = false ;
277329 let surfaceClosed = false ;
278- let promptDrawn = false ;
279330 let contextState : ManagedChatContext = { chat : "connecting" } ;
280- let refreshPrompt : ( ( ) => void ) | undefined ;
281331 const surface : ManagedChatSurface = {
282332 signal,
283333 connection : ( ) => ( { chat : contextState . chat , checkedAt : contextState . checkedAt } ) ,
284- setContext ( state ) { contextState = { ...contextState , ...state } ; refreshPrompt ?. ( ) ; } ,
334+ setContext ( state ) { contextState = { ...contextState , ...state } ; } ,
285335 write ( text ) {
286336 if ( surfaceClosed ) return ;
287337 if ( ctx . flags . json ) out . write ( JSON . stringify ( { type : "agent_status" , text : sanitizeTerm ( text ) } ) + "\n" ) ;
288- else if ( reader && terminal ) { writeManagedChatEvent ( out , reader , text ) ; promptDrawn = true ; }
338+ else if ( reader && terminal && promptReady ) writeManagedChatEvent ( out , reader , text ) ;
289339 else out . write ( text ) ;
290340 } ,
291341 } ;
@@ -296,13 +346,14 @@ export async function cmdManagedAgentChat(ctx: AppContext, id: string | undefine
296346 if ( readiness . registry . state !== "enabled" ) { writeGateError ( readiness . registry , ctx , deps . err ?? process . stderr ) ; return 1 ; }
297347 if ( readiness . dm . state !== "enabled" ) { writeGateError ( readiness . dm , ctx , deps . err ?? process . stderr ) ; return 1 ; }
298348 if ( ctx . flags . json ) out . write ( JSON . stringify ( { type : "readiness" , readiness } ) + "\n" ) ;
299- else out . write ( readinessLine ( readiness ) ) ;
349+ else if ( readiness . model_uvt . state !== "enabled" ) out . write ( `Model admission: ${ readiness . model_uvt . code } . ${ readiness . model_uvt . remedy } \n` ) ;
300350 let agent = id ? await client . get ( id , signal ) : await pickManagedAgent ( await client . list ( signal ) , out , signal ) ;
301351 if ( ! agent ) return 0 ;
302352 closeSession = await deps . hooks ?. beforeChat ?.( ctx , agent , surface ) ;
303353 const thread = await client . thread ( agent . agent_id , signal ) ;
304354 if ( typeof thread [ "id" ] !== "string" ) throw new Error ( "Cloud did not return a conversation ID." ) ;
305355 const conversationId = thread [ "id" ] ;
356+ const admissionReceipts = new Map < string , MessageAdmission > ( ) ;
306357 const send = async ( body : string ) : Promise < boolean > => {
307358 const nonce = randomUUID ( ) ;
308359 let receipt ;
@@ -312,41 +363,33 @@ export async function cmdManagedAgentChat(ctx: AppContext, id: string | undefine
312363 throw new Error ( `${ managedAgentError ( error ) } Delivery is unconfirmed; check the shared conversation before sending again.` ) ;
313364 }
314365 const state = receipt . admission ?. state ?? "unreported" ;
366+ if ( receipt . admission ) {
367+ admissionReceipts . set ( receipt . id , receipt . admission ) ;
368+ while ( admissionReceipts . size > 1000 ) admissionReceipts . delete ( admissionReceipts . keys ( ) . next ( ) . value ! ) ;
369+ }
315370 const accepted = state === "admitted" || state === "replied" ;
316371 const code = accepted ? "ADMITTED" : "SEND_NOT_ADMITTED" ;
317372 if ( ctx . flags . json ) out . write ( JSON . stringify ( { type : "message_admission" , code, state } ) + "\n" ) ;
318- else surface . write ( theme . dim ( ` ${ accepted ? "Message admitted" : "Message saved; execution not accepted" } · ${ sanitizeTerm ( state ) } ` ) + "\n" ) ;
373+ else surface . write ( theme . dim ( renderManagedAdmission ( receipt . admission ) ) + "\n" ) ;
319374 return accepted ;
320375 } ;
321376 if ( prompt . trim ( ) ) return await send ( prompt ) ? 0 : 1 ;
322377 if ( ! ctx . flags . json ) {
323- out . write ( renderAgent ( agent ) + theme . dim ( "Shared Online DM · /refresh · /exit" ) + "\n" ) ;
324- const help = deps . hooks ?. help ?.( agent ) ;
325- if ( help ) out . write ( theme . dim ( sanitizeTerm ( help ) ) + "\n" ) ;
378+ out . write ( "Shared Online DM · /help · /refresh · /exit\n" ) ;
326379 }
327380 const seen = new Map < string , string > ( ) ;
328381 let polling = false ;
329382 let syncFailed = false ;
383+ let showedEmpty = false ;
330384 let closed = false ;
331385 const ownedInput = terminal ? managedChatInput ( inputStream ) : undefined ;
332386 const rl = createInterface ( { input : ownedInput ?. input ?? inputStream , output : out , terminal } ) ;
333387 reader = rl ;
334388 const lines = rl [ Symbol . asyncIterator ] ( ) ;
335389 let inputClosed = false ;
336390 rl . once ( "close" , ( ) => { inputClosed = true ; reader = undefined ; } ) ;
337- refreshPrompt = ( ) : void => {
338- const columns = ( out as Writable & { columns ?: number } ) . columns ?? 80 ;
339- const next = ctx . flags . json ? "" : `${ renderManagedContext ( agent ! , contextState , columns ) } \n${ theme . cyan ( "you › " ) } ` ;
340- if ( next === rl . getPrompt ( ) ) return ;
341- // Measure the OLD prompt before changing its wrapped height.
342- if ( promptDrawn && terminal && ! inputClosed && ! closed ) {
343- const { rows } = rl . getCursorPos ( ) ;
344- out . write ( `\r${ rows > 0 ? `\x1b[${ rows } A` : "" } \x1b[0J` ) ;
345- rl . setPrompt ( next ) ; rl . prompt ( true ) ;
346- } else rl . setPrompt ( next ) ;
347- } ;
348- refreshPrompt ( ) ;
349- const onResize = ( ) : void => { refreshPrompt ?.( ) ; if ( terminal && ! inputClosed && ! closed ) rl . prompt ( true ) ; } ;
391+ rl . setPrompt ( ctx . flags . json ? "" : theme . cyan ( "you › " ) ) ;
392+ const onResize = ( ) : void => { if ( terminal && ! inputClosed && ! closed ) rl . prompt ( true ) ; } ;
350393 out . on ( "resize" , onResize ) ;
351394 const refresh = async ( redraw = false ) : Promise < void > => {
352395 if ( polling || closed ) return ;
@@ -356,28 +399,32 @@ export async function cmdManagedAgentChat(ctx: AppContext, id: string | undefine
356399 if ( closed ) return ;
357400 agent = latest ;
358401 contextState = { ...contextState , chat : "synced" , checkedAt : Date . now ( ) } ;
359- refreshPrompt ?.( ) ;
360- for ( const message of messages ) {
361- if ( seen . get ( message . id ) === message . body ) continue ;
362- const rendered = ctx . flags . json ? JSON . stringify ( { type : "message" , message } ) + "\n" : renderMessage ( message , agent ! ) ;
363- if ( terminal && reader ) { writeManagedChatEvent ( out , rl , rendered ) ; promptDrawn = true ; }
402+ if ( redraw && ! ctx . flags . json ) surface . write ( renderManagedContext ( agent ! , contextState , ( out as Writable & { columns ?: number } ) . columns ?? 80 ) + "\n" ) ;
403+ if ( messages . length === 0 && ! ctx . flags . json && ! showedEmpty ) {
404+ surface . write ( "No messages yet. Write to this agent, or use /help for commands.\n" ) ;
405+ showedEmpty = true ;
406+ }
407+ for ( const rawMessage of messages ) {
408+ const message = rawMessage . admission ? rawMessage : { ...rawMessage , admission : admissionReceipts . get ( rawMessage . id ) } ;
409+ const fingerprint = JSON . stringify ( [ message . body , message . admission ?. state , message . admission ?. run_id ] ) ;
410+ if ( seen . get ( message . id ) === fingerprint ) continue ;
411+ const rendered = ctx . flags . json ? JSON . stringify ( { type : "message" , message } ) + "\n" : renderManagedMessage ( message , agent ! , ( out as Writable & { columns ?: number } ) . columns ?? 80 ) ;
412+ if ( terminal && reader && promptReady ) writeManagedChatEvent ( out , rl , rendered ) ;
364413 else out . write ( rendered ) ;
365- seen . set ( message . id , message . body ) ;
414+ seen . set ( message . id , fingerprint ) ;
366415 while ( seen . size > 1000 ) seen . delete ( seen . keys ( ) . next ( ) . value ! ) ;
367416 }
368417 if ( syncFailed && ! ctx . flags . json ) surface . write ( theme . dim ( "Conversation sync restored.\n" ) ) ;
369418 syncFailed = false ;
370419 } catch ( error ) {
371420 contextState = { ...contextState , chat : "paused" } ;
372- refreshPrompt ?.( ) ;
373421 if ( ! closed && ! syncFailed ) {
374422 if ( ctx . flags . json ) out . write ( JSON . stringify ( { type : "sync_error" , message : managedAgentError ( error ) } ) + "\n" ) ;
375- else surface . write ( theme . yellow ( "Conversation sync paused. " + sanitizeTerm ( managedAgentError ( error ) ) ) + "\n" ) ;
423+ else surface . write ( theme . yellow ( "DM sync paused. " + sanitizeTerm ( managedAgentError ( error ) ) + " Next: /refresh to retry." ) + "\n" ) ;
376424 }
377425 syncFailed = true ;
378426 } finally {
379427 polling = false ;
380- if ( redraw && ! closed && ! inputClosed && terminal ) rl . prompt ( true ) ;
381428 }
382429 } ;
383430 let operations = Promise . resolve ( ) ;
@@ -396,18 +443,24 @@ export async function cmdManagedAgentChat(ctx: AppContext, id: string | undefine
396443 rl . on ( "SIGINT" , stop ) ;
397444 signal . addEventListener ( "abort" , stop , { once : true } ) ;
398445 try {
399- await refresh ( ) ;
400- if ( terminal ) timer = setInterval ( ( ) => { void refresh ( true ) ; } , 3000 ) ;
401- if ( terminal && ! inputClosed ) { rl . prompt ( ) ; promptDrawn = true ; }
446+ await refresh ( true ) ;
447+ if ( terminal ) timer = setInterval ( ( ) => { void refresh ( ) ; } , 3000 ) ;
448+ if ( terminal && ! inputClosed ) { rl . prompt ( ) ; promptReady = true ; }
402449 for await ( const line of { [ Symbol . asyncIterator ] : ( ) => lines } ) {
403450 const input = line . trim ( ) ;
404451 if ( input === "/exit" || input === "/quit" ) break ;
405452 try {
406453 await enqueue ( async ( ) => {
407- if ( input === "/refresh" ) await refresh ( ) ;
454+ if ( input === "/refresh" ) await refresh ( true ) ;
455+ else if ( input === "/help" ) {
456+ surface . write ( "Shared Online DM · /refresh checks messages and status · /exit closes chat\n" ) ;
457+ const help = deps . hooks ?. help ?.( agent ! ) ;
458+ if ( help ) surface . write ( sanitizeTerm ( help ) + "\n" ) ;
459+ if ( isAts ( agent ! ) ) surface . write ( "ATS local preferences do not authorize live orders. RC observation is separate from this DM.\n" ) ;
460+ }
408461 else if ( input . startsWith ( "/" ) ) {
409462 if ( ! ( await deps . hooks ?. onChatCommand ?.( ctx , agent ! , input , surface ) ) ) {
410- surface . write ( "Use /refresh or /exit. Configure this agent with `aether agent configure` .\n" ) ;
463+ surface . write ( "Unknown command. Use /help for this agent's commands .\n" ) ;
411464 }
412465 } else if ( input ) { await send ( input ) ; await refresh ( ) ; }
413466 } ) ;
@@ -416,17 +469,17 @@ export async function cmdManagedAgentChat(ctx: AppContext, id: string | undefine
416469 if ( ctx . flags . json ) out . write ( JSON . stringify ( { type : "command_error" , message } ) + "\n" ) ;
417470 else surface . write ( theme . yellow ( message ) + "\n" ) ;
418471 }
419- if ( terminal && ! inputClosed ) { rl . prompt ( ) ; promptDrawn = true ; }
472+ if ( terminal && ! inputClosed ) { rl . prompt ( ) ; promptReady = true ; }
420473 }
421474 } finally {
422475 closed = true ;
423476 reader = undefined ;
477+ promptReady = false ;
424478 removeKeys ( ) ;
425479 await operations ;
426480 signal . removeEventListener ( "abort" , stop ) ;
427481 rl . removeListener ( "SIGINT" , stop ) ;
428482 out . removeListener ( "resize" , onResize ) ;
429- refreshPrompt = undefined ;
430483 rl . close ( ) ;
431484 ownedInput ?. dispose ( ) ;
432485 }
0 commit comments