2026-07-17 10:30:45 +03:00
const FETCH_TIMEOUT_MS = 15_000 ;
const MESSAGE_FETCH_LIMIT = 20 ;
const isRecord = ( value ) => Boolean ( value && typeof value === 'object' && ! Array . isArray ( value ));
const readContextState = ( session ) => {
const metadata = isRecord ( session ? . metadata ) ? session . metadata : {};
const openchamber = isRecord ( metadata . openchamber ) ? metadata . openchamber : {};
const messages = Array . isArray ( openchamber . context_obligatory_messages )
? openchamber . context_obligatory_messages . filter (( item ) =>
isRecord ( item )
&& typeof item . id === 'string'
&& typeof item . createdAt === 'number'
&& ( item . role === 'user' || item . role === 'assistant' ))
: [];
return { metadata , openchamber , messages };
};
const buildContextPrompt = ( entries ) => {
const timeline = entries . map (({ pinned , text }) => {
const timestamp = new Date ( pinned . createdAt ). toISOString ();
return `## ${ pinned . role } — ${ timestamp } \n\n ${ text } ` ;
}). join ( '\n\n---\n\n' );
return [
'The following messages are from the compacted conversation. The user explicitly marked them as important and required in your context. Pay close attention to them; they may have been sent by either the user or you before compaction.' ,
'Use them while continuing the pre-compaction work. Do not treat this context restoration as a new standalone task.' ,
'If any tasks or next steps remain, do not acknowledge, summarize, or mention this restored context in a separate response. Simply continue the work and use it silently as background context. Do not append a recap of it after completing those tasks. Only if no tasks or next steps remain, give the user a very brief summary of the important restored context in no more than one short paragraph, without lists or a detailed recap.' ,
'' ,
timeline ,
]. join ( '\n' );
};
export const createContextObligatoryRuntime = ({
buildOpenCodeUrl ,
getOpenCodeAuthHeaders ,
2026-08-18 02:59:04 +03:00
sessionKnowledgeRuntime = null ,
2026-07-17 10:30:45 +03:00
}) => {
const inflight = new Set ();
let stopped = false ;
const openCodeFetch = async ( fetchPath , { directory , method = 'GET' , body , query } = {}) => {
const params = new URLSearchParams ( query || {});
if ( directory ) params . set ( 'directory' , directory );
const search = params . toString ();
const response = await fetch ( ` ${ buildOpenCodeUrl ( fetchPath , '' ) }${ search ? `? ${ search } ` : '' } ` , {
method ,
headers : {
Accept : 'application/json' ,
...( body ? { 'Content-Type' : 'application/json' } : {}),
... getOpenCodeAuthHeaders (),
},
...( body ? { body : JSON . stringify ( body ) } : {}),
signal : AbortSignal . timeout ( FETCH_TIMEOUT_MS ),
});
if ( ! response . ok ) throw new Error ( `OpenCode ${ method } ${ fetchPath } failed with ${ response . status } ` );
return response . json (). catch (() => null );
};
const tick = async ( sessionId , directory ) => {
const session = await openCodeFetch ( `/session/ ${ encodeURIComponent ( sessionId ) } ` , { directory });
if ( session ? . parentID ) return ;
const state = readContextState ( session );
2026-08-18 02:59:04 +03:00
/**
* Project knowledge rides along with the pinned messages. Compaction takes
* both away, and both are restored for the same reason, so they travel as
* one message: two synthetic turns back to back would read as the agent
* being interrupted twice.
*/
const knowledge = sessionKnowledgeRuntime
? await sessionKnowledgeRuntime
2026-08-19 00:07:57 +03:00
. resolvePending (
directory ,
// Compaction removed the previously delivered block, so its stored
// signature is no longer evidence that the session still carries it.
'' ,
sessionKnowledgeRuntime . readPins ( session ),
)
2026-08-18 02:59:04 +03:00
. catch (() => ({ text : '' , signature : '' }))
: { text : '' , signature : '' };
if ( state . messages . length === 0 && ! knowledge . text ) return ;
2026-07-17 10:30:45 +03:00
const recent = await openCodeFetch ( `/session/ ${ encodeURIComponent ( sessionId ) } /message` , {
directory ,
query : { limit : String ( MESSAGE_FETCH_LIMIT ) },
});
if ( ! Array . isArray ( recent ) || recent . length === 0 ) return ;
const summary = recent . toReversed (). find (( message ) =>
message ? . info ? . role === 'assistant' && message . info . summary === true ) ? . info ;
if ( ! summary ? . id || ! summary ? . time ? . completed ) return ;
if ( state . openchamber . context_obligatory_last_compaction_message_id === summary . id ) return ;
const fetched = await Promise . allSettled ( state . messages . map ( async ( pinned ) => {
const message = await openCodeFetch (
`/session/ ${ encodeURIComponent ( sessionId ) } /message/ ${ encodeURIComponent ( pinned . id ) } ` ,
{ directory },
);
const text = Array . isArray ( message ? . parts )
? message . parts . filter (( part ) => part ? . type === 'text' && typeof part . text === 'string' )
. map (( part ) => part . text . trim ()). filter ( Boolean ). join ( '\n\n' )
: '' ;
return { pinned , text };
}));
const entries = fetched
. filter (( result ) => result . status === 'fulfilled' && result . value . text )
. map (( result ) => result . value )
. sort (( left , right ) => left . pinned . createdAt - right . pinned . createdAt );
2026-08-18 02:59:04 +03:00
if ( entries . length === 0 && ! knowledge . text ) return ;
2026-07-17 10:30:45 +03:00
const executionInfo = recent . toReversed (). find (( message ) =>
message ? . info ? . role === 'assistant' && message . info . summary !== true ) ? . info ;
const providerID = typeof executionInfo ? . providerID === 'string' ? executionInfo . providerID : '' ;
const modelID = typeof executionInfo ? . modelID === 'string' ? executionInfo . modelID : '' ;
if ( ! providerID || ! modelID ) throw new Error ( 'no pre-compaction assistant provider/model' );
const agent = typeof executionInfo . agent === 'string' ? executionInfo . agent : executionInfo . mode ;
await openCodeFetch ( `/session/ ${ encodeURIComponent ( sessionId ) } /prompt_async` , {
directory ,
method : 'POST' ,
body : {
model : { providerID , modelID },
...( typeof agent === 'string' && agent ? { agent } : {}),
2026-08-18 02:59:04 +03:00
parts : [{
type : 'text' ,
text : [ knowledge . text , entries . length > 0 ? buildContextPrompt ( entries ) : '' ]
. filter ( Boolean )
. join ( '\n\n---\n\n' ),
synthetic : true ,
}],
2026-07-17 10:30:45 +03:00
},
});
const fresh = await openCodeFetch ( `/session/ ${ encodeURIComponent ( sessionId ) } ` , { directory });
const freshState = readContextState ( fresh );
await openCodeFetch ( `/session/ ${ encodeURIComponent ( sessionId ) } ` , {
directory ,
method : 'PATCH' ,
body : {
metadata : {
... freshState . metadata ,
openchamber : {
... freshState . openchamber ,
context_obligatory_last_compaction_message_id : summary . id ,
2026-08-18 02:59:04 +03:00
// Recorded together with the cursor: the session now carries this
// knowledge again, so the next send must not repeat it.
...( knowledge . signature
? { [ sessionKnowledgeRuntime . metadataKey ] : knowledge . signature }
: {}),
2026-07-17 10:30:45 +03:00
},
},
},
});
};
const processPayload = ( payload , directoryHint = '' ) => {
if ( stopped || payload ? . type !== 'session.compacted' ) return ;
const sessionId = payload ? . properties ? . sessionID ;
if ( typeof sessionId !== 'string' || inflight . has ( sessionId )) return ;
const directory = payload ? . properties ? . directory || directoryHint ;
inflight . add ( sessionId );
return tick ( sessionId , directory )
. catch (( error ) => console . warn ( '[context-obligatory] injection failed:' , error ? . message || error ))
. finally (() => inflight . delete ( sessionId ));
};
const stop = () => {
stopped = true ;
};
return { processPayload , stop };
};