import: set factory CreatedAt from transcript modtime · Entire

import: set factory CreatedAt from transcript modtime

983cd32→main·

computermode·3w ago·2 files·+19 added/-2 removed

Droid envelopes carry no per-message timestamp (the agent stamps events with time.Now() at hook time), so imported factory turns had a zero CreatedAt. Fall back to the transcript file's modtime, matching the Gemini importer.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Sessions

7897bb6707d0View transcript

Changes

2

2 unmodified lines

3
4
5
6
7
8
9
30 unmodified lines

40
41
42
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
10 unmodified lines

67
68
69
62
70
71
72
73

2 unmodified lines

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
    "time"

30 unmodified lines

// SplitTurns produces one Turn per user-prompt envelope, bounded by the next.
// Token usage (including spawned subagents) is delegated to the Factory agent;
// the model is read once from the session's adjacent settings file.
// the model is read once from the session's adjacent settings file. Droid
// envelopes carry no per-message timestamp (the agent stamps events with
// time.Now() at hook time), so every turn falls back to the transcript file's
// modtime — the same fallback the Gemini importer uses.
func (factoryImporter) SplitTurns(sf SessionFile, full []byte) ([]Turn, error) {
    subagentsDir := filepath.Join(filepath.Dir(sf.Path), sf.SessionID, "subagents")
    model := factoryaidroid.ExtractModelFromTranscript(sf.Path)
    var createdAt time.Time
    if info, statErr := os.Stat(sf.Path); statErr == nil {
        createdAt = info.ModTime()
    }
    ag := &factoryaidroid.FactoryAIDroidAgent{}
    return splitLineTurns(splitRawLines(full),
        func(raw []byte) bool { _, ok := factoryPromptText(raw); return ok },
10 unmodified lines

return nil, nil
        }
        prompt, _ := factoryPromptText(rawLines[start])
        return &Turn{UUID: env.ID, Prompt: prompt, Model: model, Tokens: tokens}, nil
        return &Turn{UUID: env.ID, Prompt: prompt, Model: model, CreatedAt: createdAt, Tokens: tokens}, nil
    })
}

Mcmd/entire/cli/agentimport/factory.go+10/-2

56 unmodified lines

57
58
59
60
61
62
63
64
65
66
67
68
18 unmodified lines

87
88
89
90
91
92
93
94
95

56 unmodified lines

if err := os.WriteFile(sessPath, full, 0o644); err != nil {
        t.Fatal(err)
    }
    // Droid carries no per-message timestamp, so turns fall back to the file
    // modtime; pin it so we can assert CreatedAt is populated from it.
    modTime := time.Date(2026, 6, 24, 9, 30, 0, 0, time.UTC)
    if err := os.Chtimes(sessPath, modTime, modTime); err != nil {
        t.Fatal(err)
    }
    // Model comes from the adjacent <session>.settings.json, not the transcript.
    if err := os.WriteFile(filepath.Join(dir, "sess.settings.json"), []byte(`{"model":"custom:Gemini-2.5-Pro-0"}`), 0o644); err != nil {
        t.Fatal(err)
    }
18 unmodified lines

if turns[0].Model != "Gemini-2.5-Pro-0" {
        t.Errorf("turn0 model = %q, want Gemini-2.5-Pro-0 (cleaned from settings)", turns[0].Model)
    }
    if !turns[0].CreatedAt.Equal(modTime) {
        t.Errorf("turn0 CreatedAt = %v, want file modtime %v", turns[0].CreatedAt, modTime)
    }
    if turns[0].Tokens == nil || turns[0].Tokens.OutputTokens != 5 {
        t.Errorf("turn0 tokens not bounded to its own turn: %+v", turns[0].Tokens)
    }