agent: dedupe transcript scanning in opencode and copilot-cli · Entire

agent: dedupe transcript scanning in opencode and copilot-cli

06f522b·

Soph·2w ago·2 files·+39 added/-75 removed

opencode's ExtractModifiedFilesFromOffset and ExtractModifiedFiles repeated the assistant-message/tool-part file collection loop — extract modifiedFilesFromMessages and call it from both. copilot-cli's summary and model extraction repeated the scan-newest-first/unmarshal/return pattern three times — extract lastEventField[T] and express the model-change-then-tool-telemetry fallback as two calls. Covered by the existing transcript extraction tests in both packages.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

11253d4586b3View transcript

Changes

2

168 unmodified lines

// lastEventField scans events newest-first for entries of eventType,
// returning the first non-empty value extract produces (skipping entries
// whose data doesn't unmarshal).
func lastEventField[T any](events []copilotEvent, eventType string, extract func(T) string) string {
    for i := len(events) - 1; i >= 0; i-- {
        if events[i].Type != eventType {
            continue
        }

var data T
        if err := json.Unmarshal(events[i].Data, &data); err != nil {
            continue
        }

if v := extract(data); v != "" {
            return v
        }
    }

return ""
}

// extractSummaryFromEvents returns the content of the last assistant.message event.
func extractSummaryFromEvents(events []copilotEvent) string {
    for i := len(events) - 1; i >= 0; i-- {
        if events[i].Type != eventTypeAssistantMsg {
            continue
        }

var data assistantMessageData
        if err := json.Unmarshal(events[i].Data, &data); err != nil {
            continue
        }

if data.Content != "" {
            return data.Content
        }
    }

return ""
}

// extractModelFromEvents returns the model from transcript events.
// First checks session.model_change events, then falls back to the model field
// in tool.execution_complete events (Copilot CLI includes model per tool call).
func extractModelFromEvents(events []copilotEvent) string {
    // Primary: session.model_change (explicit model declaration)
    for i := len(events) - 1; i >= 0; i-- {
        if events[i].Type != eventTypeModelChange {
            continue
        }

var data modelChangeData
        if err := json.Unmarshal(events[i].Data, &data); err != nil {
            continue
        }

if data.NewModel != "" {
            return data.NewModel
        }
    }
    if model := lastEventField(events, eventTypeModelChange,
        func(d modelChangeData) string { return d.NewModel }); model != "" {
        return model
    }

// Fallback: tool.execution_complete events include a model field
    for i := len(events) - 1; i >= 0; i-- {
        if events[i].Type != eventTypeToolExecDone {
            continue
        }

var data toolExecCompleteData
        if err := json.Unmarshal(events[i].Data, &data); err != nil {
            continue
        }

if data.Model != "" {
            return data.Model
        }
    }

return ""
}

// sessionShutdownData is the data payload for session.shutdown events.

Mcmd/entire/cli/agent/copilotcli/transcript.go+29/-48

102 unmodified lines

// modifiedFilesFromMessages collects unique file paths touched by
// file-modification tool calls in msgs[startOffset:].
func modifiedFilesFromMessages(msgs []ExportMessage, startOffset int) []string {
    seen := make(map[string]bool)
    var files []string

for i := startOffset; i < len(session.Messages); i++ {
        msg := session.Messages[i]

}
    return files
}

// ExtractModifiedFiles extracts modified file paths from raw export JSON transcript bytes.