33
44const fs = require ( 'fs' )
55const path = require ( 'path' )
6+ const crypto = require ( 'crypto' )
67const { spawnSync } = require ( 'child_process' )
78
89const CLAUDE_EVENT_MAP = Object . freeze ( {
@@ -89,13 +90,81 @@ function adapterTracePath(env = process.env) {
8990 return target
9091}
9192
93+ function lifecycleStateTraceSummary ( env = process . env ) {
94+ const workspaceRoot = String ( env . DEVCODEX_WORKSPACE_ROOT || '' ) . trim ( )
95+ if ( ! workspaceRoot ) return null
96+ const namespaceRoot = path . join ( path . resolve ( workspaceRoot ) , '.devcodex' )
97+ if ( ! fs . existsSync ( namespaceRoot ) ) return null
98+ const candidates = [ ]
99+ for ( const projectEntry of fs . readdirSync ( namespaceRoot , { withFileTypes : true } ) . slice ( 0 , 32 ) ) {
100+ if ( ! projectEntry . isDirectory ( ) ) continue
101+ const hooksRoot = path . join ( namespaceRoot , projectEntry . name , '.memory' , 'hooks' )
102+ if ( ! fs . existsSync ( hooksRoot ) ) continue
103+ for ( const hookEntry of fs . readdirSync ( hooksRoot , { withFileTypes : true } ) . slice ( 0 , 32 ) ) {
104+ if ( ! hookEntry . isDirectory ( ) ) continue
105+ const file = path . join ( hooksRoot , hookEntry . name , 'lifecycle-state.json' )
106+ if ( fs . existsSync ( file ) ) {
107+ candidates . push ( {
108+ file,
109+ workspaceMeta : projectEntry . name . toLowerCase ( ) === 'workspace'
110+ } )
111+ }
112+ }
113+ }
114+ if ( ! candidates . length ) return null
115+ const expectedEpoch = String ( env . DEVCODEX_CONTEXT_EPOCH || '' ) . trim ( )
116+ const parsedCandidates = candidates . map ( candidate => ( {
117+ ...candidate ,
118+ state : JSON . parse ( fs . readFileSync ( candidate . file , 'utf8' ) ) ,
119+ mtimeMs : fs . statSync ( candidate . file ) . mtimeMs
120+ } ) )
121+ const selected = parsedCandidates . sort ( ( left , right ) => {
122+ const leftEpochMatch = left . state . contextAcquisition ?. contextEpoch === expectedEpoch ? 1 : 0
123+ const rightEpochMatch = right . state . contextAcquisition ?. contextEpoch === expectedEpoch ? 1 : 0
124+ if ( leftEpochMatch !== rightEpochMatch ) return rightEpochMatch - leftEpochMatch
125+ if ( left . workspaceMeta !== right . workspaceMeta ) return left . workspaceMeta ? 1 : - 1
126+ return right . mtimeMs - left . mtimeMs
127+ } ) [ 0 ]
128+ const file = selected . file
129+ const state = selected . state
130+ const acquisition = state . contextAcquisition || { }
131+ const receipt = acquisition . receipt || { }
132+ return {
133+ file : path . relative ( path . resolve ( workspaceRoot ) , file ) . replace ( / \\ / g, '/' ) ,
134+ activeProject : state . activeProject || null ,
135+ project : acquisition . project || state . activeProject || null ,
136+ contextEpoch : acquisition . contextEpoch || null ,
137+ planId : acquisition . plan ?. planId || null ,
138+ receiptStatus : receipt . status || null ,
139+ satisfiedSourceIds : receipt . satisfiedSourceIds || [ ] ,
140+ missingSourceIds : receipt . missingSourceIds || [ ] ,
141+ inFlight : ( acquisition . inFlight || [ ] ) . map ( item => ( {
142+ canonical : item . canonical ,
143+ toolCallId : item . toolCallId
144+ } ) ) ,
145+ postHistory : ( acquisition . postHistory || [ ] ) . slice ( - 8 ) . map ( item => ( {
146+ canonical : item . canonical ,
147+ toolCallId : item . toolCallId ,
148+ outcome : item . outcome
149+ } ) ) ,
150+ lastError : acquisition . lastError || receipt . lastError || null
151+ }
152+ }
153+
92154function appendAdapterTrace ( host , phase , input , normalized , result , env = process . env ) {
93155 const target = adapterTracePath ( env )
94156 if ( ! target ) return
95157 try {
96158 const payload = input && typeof input === 'object' && ! Array . isArray ( input ) ? input : { }
159+ const normalizedPayload = normalized ?. payload &&
160+ typeof normalized . payload === 'object' &&
161+ ! Array . isArray ( normalized . payload )
162+ ? normalized . payload
163+ : { }
97164 const toolInput = payload . toolInput || payload . tool_input || payload . toolArgs || { }
98165 const toolResult = payload . toolResult || payload . tool_result || payload . toolResponse || payload . tool_response || { }
166+ const normalizedToolInput = normalizedPayload . tool_input || normalizedPayload . toolInput || { }
167+ const normalizedToolResult = normalizedPayload . tool_result || normalizedPayload . toolResult || { }
99168 const entry = {
100169 schemaVersion : 'LifecycleHostAdapterTraceV1' ,
101170 observedAt : new Date ( ) . toISOString ( ) ,
@@ -104,11 +173,23 @@ function appendAdapterTrace(host, phase, input, normalized, result, env = proces
104173 originalEvent : normalized ?. originalEvent || null ,
105174 mappedEvent : normalized ?. mappedEvent || null ,
106175 payloadKeys : Object . keys ( payload ) . sort ( ) . slice ( 0 , 64 ) ,
176+ adapterCwd : process . cwd ( ) ,
177+ payloadCwd : String ( payload . cwd || payload . workingDirectory || '' ) ,
178+ promptPreview : String (
179+ payload . prompt || payload . userPrompt || payload . user_prompt || ''
180+ ) . slice ( 0 , 512 ) ,
107181 toolName : String ( payload . toolName || payload . tool_name || '' ) ,
108182 toolUseId : String ( payload . toolUseId || payload . tool_use_id || '' ) ,
183+ normalizedToolCallId : String (
184+ normalizedPayload . tool_call_id || normalizedPayload . toolCallId || ''
185+ ) ,
109186 toolInputKeys : toolInput && typeof toolInput === 'object'
110187 ? Object . keys ( toolInput ) . sort ( ) . slice ( 0 , 64 )
111188 : [ ] ,
189+ normalizedToolInputDigest : crypto . createHash ( 'sha256' )
190+ . update ( JSON . stringify ( stableJsonValue ( normalizedToolInput ) ) )
191+ . digest ( 'hex' ) ,
192+ normalizedToolInputPreview : JSON . stringify ( stableJsonValue ( normalizedToolInput ) ) . slice ( 0 , 1024 ) ,
112193 dispatchedServer : String (
113194 toolInput ?. server || toolInput ?. serverName || toolInput ?. server_name || ''
114195 ) ,
@@ -118,10 +199,17 @@ function appendAdapterTrace(host, phase, input, normalized, result, env = proces
118199 toolResultKeys : toolResult && typeof toolResult === 'object'
119200 ? Object . keys ( toolResult ) . sort ( ) . slice ( 0 , 64 )
120201 : [ ] ,
202+ normalizedToolResultPreview : ( ( ) => {
203+ const value = typeof normalizedToolResult === 'string'
204+ ? normalizedToolResult
205+ : JSON . stringify ( normalizedToolResult )
206+ return String ( value || '' ) . slice ( 0 , 512 )
207+ } ) ( ) ,
121208 status : result ?. status ?? null ,
122209 outputKeys : result ?. output && typeof result . output === 'object'
123210 ? Object . keys ( result . output ) . sort ( ) . slice ( 0 , 32 )
124- : [ ]
211+ : [ ] ,
212+ lifecycleState : phase === 'output' ? lifecycleStateTraceSummary ( env ) : null
125213 }
126214 fs . mkdirSync ( path . dirname ( target ) , { recursive : true } )
127215 const prior = fs . existsSync ( target )
@@ -171,6 +259,23 @@ function parseToolInputEnvelope(value) {
171259 }
172260}
173261
262+ function stableJsonValue ( value ) {
263+ if ( Array . isArray ( value ) ) return value . map ( stableJsonValue )
264+ if ( ! value || typeof value !== 'object' ) return value
265+ return Object . fromEntries (
266+ Object . keys ( value ) . sort ( ) . map ( key => [ key , stableJsonValue ( value [ key ] ) ] )
267+ )
268+ }
269+
270+ function copilotToolCallId ( payload ) {
271+ const material = JSON . stringify ( stableJsonValue ( {
272+ sessionId : payload . session_id || payload . sessionId || '' ,
273+ toolName : payload . tool_name || payload . toolName || '' ,
274+ toolInput : payload . tool_input || payload . toolInput || { }
275+ } ) )
276+ return `copilot-${ crypto . createHash ( 'sha256' ) . update ( material ) . digest ( 'hex' ) . slice ( 0 , 32 ) } `
277+ }
278+
174279function normalizeGrokToolResult ( value ) {
175280 const output = value && typeof value === 'object' && ! Array . isArray ( value ) &&
176281 Object . prototype . hasOwnProperty . call ( value , 'output' )
@@ -232,11 +337,40 @@ function normalizeHostPayload(host, payload) {
232337 if ( normalized . sessionId && ! normalized . session_id ) normalized . session_id = normalized . sessionId
233338 if ( normalized . transcriptPath && ! normalized . transcript_path ) normalized . transcript_path = normalized . transcriptPath
234339 if ( normalized . stopReason && ! normalized . stop_reason ) normalized . stop_reason = normalized . stopReason
235- if ( normalized . toolResult && ! normalized . tool_result ) normalized . tool_result = normalized . toolResult
236- if ( originalEvent === 'userPromptTransformed' &&
237- / ^ P r o g r e s s i v e S k i l l r o u t e i s i n c o m p l e t e : / i. test ( String ( normalized . prompt || '' ) . trim ( ) ) ) {
340+ if ( typeof normalized . tool_input === 'string' ) {
341+ normalized . tool_input = parseToolInputEnvelope ( normalized . tool_input ) || normalized . tool_input
342+ }
343+ if ( ! normalized . tool_call_id && ! normalized . toolCallId ) {
344+ normalized . tool_call_id = copilotToolCallId ( normalized )
345+ normalized . toolCallId = normalized . tool_call_id
346+ }
347+ if ( normalized . toolResult && ! normalized . tool_result ) {
348+ const result = normalized . toolResult
349+ normalized . tool_result = typeof result . textResultForLlm === 'string'
350+ ? {
351+ success : result . resultType === 'success' ,
352+ result : result . textResultForLlm ,
353+ ...( result . resultType === 'success'
354+ ? { }
355+ : { error : result . textResultForLlm || 'Copilot tool call failed.' } )
356+ }
357+ : result
358+ }
359+ const continuationPrompts = [
360+ normalized . prompt ,
361+ normalized . transformedPrompt ,
362+ normalized . transformed_prompt
363+ ]
364+ if ( ( originalEvent === 'userPromptSubmitted' ||
365+ originalEvent === 'userPromptTransformed' ) &&
366+ continuationPrompts . some ( value =>
367+ / ^ P r o g r e s s i v e S k i l l r o u t e i s i n c o m p l e t e : / i. test ( String ( value || '' ) . trim ( ) )
368+ ) ) {
238369 normalized . devcodex_host_continuation = true
239370 }
371+ if ( originalEvent === 'userPromptTransformed' ) {
372+ normalized . devcodex_host_transform_only = true
373+ }
240374 }
241375 if ( host === 'gemini' && typeof payload ?. prompt_response === 'string' ) normalized . response = payload . prompt_response
242376 return { originalEvent, mappedEvent, payload : normalized }
@@ -471,10 +605,20 @@ function runHostAdapter(host, input, options = {}) {
471605 }
472606 const lifecycle = options . lifecycle || path . join ( __dirname , 'lifecycle.cjs' )
473607 const spawn = options . spawnSync || spawnSync
608+ const payloadCwd = String ( input ?. cwd || input ?. workingDirectory || '' ) . trim ( )
609+ const lifecycleCwd = ( ( ) => {
610+ if ( ! payloadCwd ) return undefined
611+ try {
612+ return fs . statSync ( payloadCwd ) . isDirectory ( ) ? path . resolve ( payloadCwd ) : undefined
613+ } catch {
614+ return undefined
615+ }
616+ } ) ( )
474617 const child = spawn ( process . execPath , [ lifecycle ] , {
475618 input : JSON . stringify ( normalized . payload ) ,
476619 encoding : 'utf8' ,
477620 maxBuffer : 8 * 1024 * 1024 ,
621+ ...( lifecycleCwd ? { cwd : lifecycleCwd } : { } ) ,
478622 env : { ...( options . env || process . env ) , DEVCODEX_HOST_PLATFORM : host }
479623 } )
480624 if ( child . error ) {
@@ -547,6 +691,11 @@ function applyCliEnvironmentOverrides (argv, env = process.env) {
547691 if ( workspaceRootIndex >= 0 && argv [ workspaceRootIndex + 1 ] ) {
548692 env . DEVCODEX_WORKSPACE_ROOT = path . resolve ( argv [ workspaceRootIndex + 1 ] )
549693 }
694+ const contextEpochIndex = argv . indexOf ( '--context-epoch' )
695+ if ( contextEpochIndex >= 0 && argv [ contextEpochIndex + 1 ] ) {
696+ env . DEVCODEX_CONTEXT_EPOCH = String ( argv [ contextEpochIndex + 1 ] ) . trim ( )
697+ env . DEVCODEX_CONTEXT_EPOCH_SOURCE = 'host-adapter-cli'
698+ }
550699 const eventIndex = argv . indexOf ( '--event' )
551700 if ( eventIndex >= 0 && argv [ eventIndex + 1 ] ) {
552701 env . DEVCODEX_HOST_EVENT = String ( argv [ eventIndex + 1 ] ) . trim ( )
0 commit comments