Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

959 lines
24 KiB
Go

package parser
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/tidwall/gjson"
)
const ZCodeDBName = "db.sqlite"
const zcodeDBName = ZCodeDBName
type zcodeProviderFactory struct {
def AgentDef
}
func newZcodeProviderFactory(def AgentDef) ProviderFactory {
return zcodeProviderFactory{def: cloneAgentDef(def)}
}
func (f zcodeProviderFactory) Definition() AgentDef {
return cloneAgentDef(f.def)
}
func (f zcodeProviderFactory) Capabilities() Capabilities {
return zcodeProviderCapabilities()
}
func (f zcodeProviderFactory) NewProvider(cfg ProviderConfig) Provider {
cfg = cfg.Clone()
cfg.Roots = normalizeZCodeRoots(cfg.Roots)
spec := zcodeProviderSpec()
return &dbBackedProvider{
ProviderBase: ProviderBase{
Def: cloneAgentDef(f.def),
Caps: spec.caps,
Config: cfg,
},
spec: spec,
sources: newDBBackedSourceSet(spec, cfg.Roots),
}
}
func zcodeProviderCapabilities() Capabilities {
return Capabilities{
Source: dbBackedSourceCapabilities(CapabilityNotApplicable),
Content: ContentCapabilities{
FirstMessage: CapabilitySupported,
SessionName: CapabilitySupported,
Cwd: CapabilitySupported,
Thinking: CapabilitySupported,
ToolCalls: CapabilitySupported,
ToolResults: CapabilitySupported,
AggregateUsageEvents: CapabilitySupported,
Model: CapabilitySupported,
},
}
}
func zcodeProviderSpec() dbBackedProviderSpec {
return dbBackedProviderSpec{
agent: AgentZCode,
dbName: zcodeDBName,
findDB: zcodeDBPath,
listMeta: func(dbPath string) ([]dbBackedSessionMeta, error) {
return listZCodeSessionMeta(dbPath)
},
parse: func(dbPath, sessionID, machine string) ([]ParseResult, error) {
result, err := parseZCodeSession(dbPath, sessionID, machine)
if err != nil || result == nil {
return nil, err
}
return []ParseResult{*result}, nil
},
caps: zcodeProviderCapabilities(),
}
}
func normalizeZCodeRoots(roots []string) []string {
cleaned := cleanJSONLRoots(roots)
out := make([]string, 0, len(cleaned))
seen := make(map[string]struct{}, len(cleaned))
for _, root := range cleaned {
normalized := normalizeZCodeRoot(root)
if normalized == "" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
out = append(out, normalized)
}
return out
}
func normalizeZCodeRoot(root string) string {
root = filepath.Clean(root)
if root == "" || root == "." {
return ""
}
if filepath.Base(root) == "db" {
return root
}
return filepath.Join(root, "db")
}
func zcodeDBPath(dir string) string {
if dir == "" {
return ""
}
path := filepath.Join(dir, zcodeDBName)
info, err := os.Stat(path)
if err != nil || info.IsDir() {
return ""
}
return path
}
func zcodeVirtualPathParts(path string) (string, string, bool) {
return ParseVirtualSourcePathForBase(path, zcodeDBName)
}
func ZCodeSQLiteVirtualPath(dbPath, sessionID string) string {
return VirtualSourcePath(dbPath, sessionID)
}
func ZCodeSQLiteSourceMtime(path string) (int64, error) {
dbPath, sessionID, ok := zcodeVirtualPathParts(path)
if !ok {
return 0, fmt.Errorf("not a zcode sqlite virtual path: %s", path)
}
db, err := openZCodeDB(dbPath)
if err != nil {
return 0, err
}
defer db.Close()
row, err := loadZCodeSessionRow(db, sessionID)
if err != nil {
return 0, err
}
return zcodeSessionFileMtime(dbPath, db, row), nil
}
func listZCodeSessionMeta(dbPath string) ([]dbBackedSessionMeta, error) {
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil, nil
}
db, err := openZCodeDB(dbPath)
if err != nil {
return nil, err
}
defer db.Close()
rows, err := db.Query(`
SELECT id,
project_id,
workspace_id,
COALESCE(directory, ''),
COALESCE(title, ''),
COALESCE(time_created, ''),
COALESCE(time_updated, '')
FROM session
ORDER BY COALESCE(time_updated, time_created), id
`)
if err != nil {
return nil, fmt.Errorf("listing zcode sessions: %w", err)
}
defer rows.Close()
var metas []dbBackedSessionMeta
for rows.Next() {
row, err := scanZCodeSessionRow(rows)
if err != nil {
return nil, err
}
if row.id == "" {
continue
}
metas = append(metas, dbBackedSessionMeta{
SessionID: row.id,
VirtualPath: ZCodeSQLiteVirtualPath(dbPath, row.id),
FileMtime: zcodeSessionFileMtime(dbPath, db, row),
})
}
if err := rows.Err(); err != nil {
return nil, err
}
sort.Slice(metas, func(i, j int) bool {
return metas[i].SessionID < metas[j].SessionID
})
return metas, nil
}
func parseZCodeSession(dbPath, sessionID, machine string) (*ParseResult, error) {
db, err := openZCodeDB(dbPath)
if err != nil {
return nil, err
}
defer db.Close()
row, err := loadZCodeSessionRow(db, sessionID)
if err != nil {
return nil, err
}
result, err := buildZCodeParseResult(dbPath, machine, row, db)
if err != nil {
return nil, err
}
return &result, nil
}
func openZCodeDB(dbPath string) (*sql.DB, error) {
dsn := "file:" + sqliteURIPath(dbPath) + "?mode=ro&immutable=0&_busy_timeout=3000"
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("opening zcode db %s: %w", dbPath, err)
}
return db, nil
}
type zcodeSessionRow struct {
id string
projectID sql.NullString
workspaceID sql.NullString
directory sql.NullString
title sql.NullString
timeCreated sql.NullString
timeUpdated sql.NullString
}
type zcodeUsageRow struct {
sessionID string
turnID sql.NullString
providerID sql.NullString
modelID sql.NullString
status sql.NullString
inputTokens sql.NullInt64
outputTokens sql.NullInt64
reasoningTokens sql.NullInt64
cacheCreationInputTokens sql.NullInt64
cacheReadInputTokens sql.NullInt64
computedTotalTokens sql.NullInt64
startedAt sql.NullString
completedAt sql.NullString
durationMS sql.NullInt64
toolCallCount sql.NullInt64
}
type zcodeMessageRow struct {
id string
data string
timeCreated string
}
type zcodePartRow struct {
id string
messageID string
timeCreated string
data string
}
func loadZCodeSessionRow(db *sql.DB, sessionID string) (zcodeSessionRow, error) {
row := db.QueryRow(`
SELECT id,
project_id,
workspace_id,
COALESCE(directory, ''),
COALESCE(title, ''),
COALESCE(time_created, ''),
COALESCE(time_updated, '')
FROM session
WHERE id = ?
`, sessionID)
var out zcodeSessionRow
err := row.Scan(
&out.id, &out.projectID, &out.workspaceID,
&out.directory, &out.title, &out.timeCreated, &out.timeUpdated,
)
if err != nil {
return zcodeSessionRow{}, fmt.Errorf("loading zcode session %s: %w", sessionID, err)
}
return out, nil
}
func scanZCodeSessionRow(rows *sql.Rows) (zcodeSessionRow, error) {
var out zcodeSessionRow
if err := rows.Scan(
&out.id, &out.projectID, &out.workspaceID,
&out.directory, &out.title, &out.timeCreated, &out.timeUpdated,
); err != nil {
return zcodeSessionRow{}, fmt.Errorf("scanning zcode session meta: %w", err)
}
return out, nil
}
func buildZCodeParseResult(
dbPath, machine string,
row zcodeSessionRow,
db *sql.DB,
) (ParseResult, error) {
startedAt := zcodeParseTime(row.timeCreated.String)
endedAt := zcodeParseTime(row.timeUpdated.String)
if startedAt.IsZero() {
startedAt = endedAt
}
if endedAt.IsZero() {
endedAt = startedAt
}
directory := row.directory.String
project := ExtractProjectFromCwd(directory)
if project == "" {
switch {
case row.projectID.Valid:
project = "project-" + strings.TrimSpace(row.projectID.String)
case row.workspaceID.Valid:
project = "workspace-" + strings.TrimSpace(row.workspaceID.String)
default:
project = "unknown"
}
}
title := strings.TrimSpace(row.title.String)
firstMessage := title
if firstMessage != "" {
firstMessage = truncate(strings.ReplaceAll(firstMessage, "\n", " "), 300)
}
msgs, err := loadZCodeMessages(db, row.id)
if err != nil {
return ParseResult{}, err
}
parts, err := loadZCodeParts(db, row.id)
if err != nil {
return ParseResult{}, err
}
parsedMessages := buildZCodeMessages(msgs, parts)
userMessageCount := 0
for _, msg := range parsedMessages {
if msg.Role == RoleUser {
userMessageCount++
}
}
sess := ParsedSession{
ID: "zcode:" + row.id,
Project: project,
Machine: machine,
Agent: AgentZCode,
Cwd: directory,
FirstMessage: firstMessage,
SessionName: title,
StartedAt: startedAt,
EndedAt: endedAt,
MessageCount: len(parsedMessages),
UserMessageCount: userMessageCount,
File: FileInfo{
Path: ZCodeSQLiteVirtualPath(dbPath, row.id),
},
}
if info, err := os.Stat(dbPath); err == nil {
sess.File.Size = info.Size()
}
sess.File.Mtime = zcodeSessionFileMtime(dbPath, db, row)
usageEvents, err := listZCodeUsageEvents(db, row.id, startedAt, endedAt)
if err != nil {
return ParseResult{}, err
}
applyUsageEventTokenTotals(&sess, usageEvents)
return ParseResult{
Session: sess,
Messages: parsedMessages,
UsageEvents: usageEvents,
}, nil
}
func loadZCodeMessages(
db *sql.DB, sessionID string,
) ([]zcodeMessageRow, error) {
rows, err := db.Query(`
SELECT id,
COALESCE(data, '{}'),
CAST(COALESCE(time_created, '') AS TEXT)
FROM message
WHERE session_id = ?
ORDER BY CAST(COALESCE(time_created, '') AS TEXT), id
`, sessionID)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "no such table: message") {
return nil, nil
}
return nil, fmt.Errorf("listing zcode messages for %s: %w", sessionID, err)
}
defer rows.Close()
msgs := make([]zcodeMessageRow, 0)
for rows.Next() {
var row zcodeMessageRow
if err := rows.Scan(&row.id, &row.data, &row.timeCreated); err != nil {
return nil, fmt.Errorf("scanning zcode message row: %w", err)
}
if row.id == "" {
continue
}
msgs = append(msgs, row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return msgs, nil
}
func loadZCodeParts(
db *sql.DB, sessionID string,
) (map[string][]zcodePartRow, error) {
selectTime := `''`
orderBy := `id`
if has, err := zcodeTableHasColumn(db, "part", "time_created"); err != nil {
return nil, err
} else if has {
selectTime = `CAST(COALESCE(time_created, '') AS TEXT)`
orderBy = selectTime + `, id`
}
rows, err := db.Query(`
SELECT id,
message_id,
`+selectTime+`,
COALESCE(data, '{}')
FROM part
WHERE session_id = ?
ORDER BY `+orderBy, sessionID)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "no such table: part") {
return nil, nil
}
return nil, fmt.Errorf("listing zcode parts for %s: %w", sessionID, err)
}
defer rows.Close()
parts := make(map[string][]zcodePartRow)
for rows.Next() {
var row zcodePartRow
if err := rows.Scan(&row.id, &row.messageID, &row.timeCreated, &row.data); err != nil {
return nil, fmt.Errorf("scanning zcode part row: %w", err)
}
if row.messageID == "" {
continue
}
parts[row.messageID] = append(parts[row.messageID], row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return parts, nil
}
func buildZCodeMessages(
msgs []zcodeMessageRow,
parts map[string][]zcodePartRow,
) []ParsedMessage {
if msgs == nil || parts == nil {
return nil
}
parsed := make([]ParsedMessage, 0, len(msgs))
for _, row := range msgs {
msg, ok := buildZCodeMessage(len(parsed), row, parts[row.id])
if !ok {
continue
}
parsed = append(parsed, msg)
}
return parsed
}
func buildZCodeMessage(
ordinal int,
row zcodeMessageRow,
parts []zcodePartRow,
) (ParsedMessage, bool) {
role, ok := normalizeZCodeRole(gjson.Get(row.data, "role").Str)
if !ok {
return ParsedMessage{}, false
}
msg := ParsedMessage{
Ordinal: ordinal,
Role: role,
Timestamp: zcodeParseTime(row.timeCreated),
Model: zcodeMessageModel(row.data),
IsSystem: role == RoleSystem,
}
var texts []string
var thinking []string
for _, part := range parts {
block := gjson.Parse(part.data)
switch block.Get("type").Str {
case "text":
if text := zcodeBlockText(block); text != "" {
texts = append(texts, text)
}
case "thinking", "reasoning":
text := zcodeThinkingText(block)
if text == "" {
continue
}
msg.HasThinking = true
thinking = append(thinking, text)
texts = append(texts, "[Thinking]\n"+text+"\n[/Thinking]")
case "tool_use", "tool":
msg.HasToolUse = true
if tc, ok := zcodeParseToolCall(block); ok {
msg.ToolCalls = append(msg.ToolCalls, tc)
}
if tr, ok := zcodeParseToolResult(block); ok {
msg.ToolResults = append(msg.ToolResults, tr)
}
case "tool_result":
if tr, ok := zcodeParseToolResult(block); ok {
msg.ToolResults = append(msg.ToolResults, tr)
}
}
}
msg.Content = strings.Join(texts, "\n")
msg.ThinkingText = strings.Join(thinking, "\n\n")
msg.ContentLength = len(msg.Content)
return msg, true
}
func normalizeZCodeRole(role string) (RoleType, bool) {
switch strings.ToLower(strings.TrimSpace(role)) {
case "user":
return RoleUser, true
case "assistant", "model":
return RoleAssistant, true
case "system":
return RoleSystem, true
default:
return "", false
}
}
func zcodeMessageModel(raw string) string {
for _, path := range []string{
"modelID",
"model_id",
"model.modelID",
"model.id",
"model.name",
"model",
} {
if model := strings.TrimSpace(gjson.Get(raw, path).Str); model != "" {
return model
}
}
return ""
}
func zcodeBlockText(block gjson.Result) string {
for _, path := range []string{"text", "content", "value"} {
value := block.Get(path)
if !value.Exists() {
continue
}
if text := decodeContent(value); text != "" {
return text
}
if value.Type == gjson.String && value.Str != "" {
return value.Str
}
}
return ""
}
func zcodeThinkingText(block gjson.Result) string {
for _, path := range []string{"thinking", "text", "content"} {
value := block.Get(path)
if !value.Exists() {
continue
}
if text := decodeContent(value); text != "" {
return text
}
if value.Type == gjson.String && value.Str != "" {
return value.Str
}
}
return ""
}
func zcodeParseToolCall(block gjson.Result) (ParsedToolCall, bool) {
name := block.Get("name").Str
if name == "" {
name = block.Get("tool_name").Str
}
if name == "" {
name = block.Get("toolName").Str
}
if name == "" {
name = block.Get("tool").Str
}
if name == "" {
return ParsedToolCall{}, false
}
input := toolCallInput(block)
if input.Raw == "" || input.Raw == "{}" {
if stateInput := block.Get("state.input"); stateInput.Exists() {
input = stateInput
}
}
inputJSON := input.Raw
if inputJSON == "" {
inputJSON = "{}"
}
toolUseID := block.Get("id").Str
if toolUseID == "" {
toolUseID = block.Get("tool_use_id").Str
}
if toolUseID == "" {
toolUseID = block.Get("toolUseID").Str
}
if toolUseID == "" {
toolUseID = block.Get("callID").Str
}
if toolUseID == "" {
toolUseID = block.Get("callId").Str
}
call := ParsedToolCall{
ToolUseID: toolUseID,
ToolName: name,
Category: NormalizeToolCategory(name),
InputJSON: inputJSON,
}
switch name {
case "Skill", "skill":
call.SkillName = input.Get("skill").Str
if call.SkillName == "" {
call.SkillName = input.Get("name").Str
}
default:
call.SkillName = inferToolSkillName(name, inputJSON)
}
return call, true
}
func zcodeParseToolResult(block gjson.Result) (ParsedToolResult, bool) {
toolUseID := block.Get("tool_use_id").Str
if toolUseID == "" {
toolUseID = block.Get("toolUseID").Str
}
if toolUseID == "" {
toolUseID = block.Get("tool_call_id").Str
}
if toolUseID == "" {
toolUseID = block.Get("callID").Str
}
if toolUseID == "" {
toolUseID = block.Get("callId").Str
}
if toolUseID == "" {
return ParsedToolResult{}, false
}
content := block.Get("content")
if !content.Exists() || content.Type == gjson.Null {
content = block.Get("result")
}
if !content.Exists() || content.Type == gjson.Null {
content = block.Get("output")
}
if !content.Exists() || content.Type == gjson.Null {
content = block.Get("state.output")
}
if !content.Exists() || content.Type == gjson.Null {
content = block.Get("state.error")
}
if content.Exists() && content.Type != gjson.Null {
return ParsedToolResult{
ToolUseID: toolUseID,
ContentLength: toolResultContentLength(content),
ContentRaw: content.Raw,
}, true
}
for _, key := range []string{"text", "error", "value"} {
if text := block.Get(key).Str; text != "" {
return ParsedToolResult{
ToolUseID: toolUseID,
ContentLength: len(text),
ContentRaw: strconv.Quote(text),
}, true
}
}
return ParsedToolResult{}, false
}
func zcodeTableHasColumn(db *sql.DB, table, column string) (bool, error) {
rows, err := db.Query(`PRAGMA table_info(` + table + `)`)
if err != nil {
return false, fmt.Errorf("listing zcode table info for %s: %w", table, err)
}
defer rows.Close()
for rows.Next() {
var (
cid int
name string
typeName string
notNull int
defaultV sql.NullString
primaryKey int
)
if err := rows.Scan(
&cid, &name, &typeName, &notNull, &defaultV, &primaryKey,
); err != nil {
return false, fmt.Errorf("scanning zcode table info for %s: %w", table, err)
}
if strings.EqualFold(name, column) {
return true, nil
}
}
if err := rows.Err(); err != nil {
return false, err
}
return false, nil
}
func listZCodeUsageEvents(
db *sql.DB,
sessionID string,
startedAt, endedAt time.Time,
) ([]ParsedUsageEvent, error) {
rows, err := db.Query(`
SELECT session_id,
CAST(turn_id AS TEXT),
provider_id,
COALESCE(model_id, ''),
COALESCE(status, ''),
COALESCE(input_tokens, 0),
COALESCE(output_tokens, 0),
COALESCE(reasoning_tokens, 0),
COALESCE(cache_creation_input_tokens, 0),
COALESCE(cache_read_input_tokens, 0),
COALESCE(computed_total_tokens, 0),
COALESCE(started_at, ''),
COALESCE(completed_at, ''),
COALESCE(duration_ms, 0),
COALESCE(tool_call_count, 0)
FROM model_usage
WHERE session_id = ?
ORDER BY
COALESCE(turn_id, -1),
COALESCE(started_at, ''),
COALESCE(completed_at, ''),
COALESCE(provider_id, -1),
COALESCE(model_id, ''),
COALESCE(status, ''),
COALESCE(input_tokens, 0),
COALESCE(output_tokens, 0),
COALESCE(reasoning_tokens, 0),
COALESCE(cache_creation_input_tokens, 0),
COALESCE(cache_read_input_tokens, 0),
COALESCE(computed_total_tokens, 0),
COALESCE(duration_ms, 0),
COALESCE(tool_call_count, 0)
`, sessionID)
if err != nil {
if strings.Contains(err.Error(), "no such table: model_usage") {
return nil, nil
}
return nil, fmt.Errorf("listing zcode usage rows for %s: %w", sessionID, err)
}
defer rows.Close()
var events []ParsedUsageEvent
for rows.Next() {
row, err := scanZCodeUsageRow(rows)
if err != nil {
return nil, err
}
if row.sessionID == "" {
continue
}
fullSessionID := "zcode:" + row.sessionID
ev := ParsedUsageEvent{
SessionID: fullSessionID,
Source: "session",
Model: row.modelID.String,
InputTokens: int(row.inputTokens.Int64),
OutputTokens: int(row.outputTokens.Int64),
CacheCreationInputTokens: int(row.cacheCreationInputTokens.Int64),
CacheReadInputTokens: int(row.cacheReadInputTokens.Int64),
ReasoningTokens: int(row.reasoningTokens.Int64),
OccurredAt: timeString(zcodeUsageTime(row.completedAt.String), zcodeUsageTime(row.startedAt.String)),
DedupKey: zcodeUsageDedupKey(fullSessionID, row),
}
if row.turnID.Valid {
if parsed, err := strconv.Atoi(strings.TrimSpace(row.turnID.String)); err == nil {
ev.MessageOrdinal = &parsed
}
}
if ev.OccurredAt == "" {
ev.OccurredAt = timeString(endedAt, startedAt)
}
events = append(events, ev)
}
if err := rows.Err(); err != nil {
return nil, err
}
return events, nil
}
func scanZCodeUsageRow(rows *sql.Rows) (zcodeUsageRow, error) {
var out zcodeUsageRow
if err := rows.Scan(
&out.sessionID, &out.turnID, &out.providerID,
&out.modelID, &out.status, &out.inputTokens, &out.outputTokens,
&out.reasoningTokens, &out.cacheCreationInputTokens,
&out.cacheReadInputTokens, &out.computedTotalTokens,
&out.startedAt, &out.completedAt, &out.durationMS,
&out.toolCallCount,
); err != nil {
return zcodeUsageRow{}, fmt.Errorf("scanning zcode usage row: %w", err)
}
return out, nil
}
func zcodeUsageDedupKey(fullSessionID string, row zcodeUsageRow) string {
parts := []string{
"session:" + fullSessionID,
"turn=" + row.turnID.String,
"provider=" + row.providerID.String,
"model=" + row.modelID.String,
"status=" + row.status.String,
"started_at=" + row.startedAt.String,
"completed_at=" + row.completedAt.String,
"duration_ms=" + zcodeNullInt64String(row.durationMS),
"tool_call_count=" + zcodeNullInt64String(row.toolCallCount),
"input_tokens=" + zcodeNullInt64String(row.inputTokens),
"output_tokens=" + zcodeNullInt64String(row.outputTokens),
"reasoning_tokens=" + zcodeNullInt64String(row.reasoningTokens),
"cache_creation_input_tokens=" + zcodeNullInt64String(row.cacheCreationInputTokens),
"cache_read_input_tokens=" + zcodeNullInt64String(row.cacheReadInputTokens),
"computed_total_tokens=" + zcodeNullInt64String(row.computedTotalTokens),
}
return strings.Join(parts, "|")
}
func zcodeNullInt64String(v sql.NullInt64) string {
if !v.Valid {
return ""
}
return strconv.FormatInt(v.Int64, 10)
}
func zcodeParseTime(raw string) time.Time {
raw = strings.TrimSpace(raw)
if raw == "" {
return time.Time{}
}
if ts := parseTimestamp(raw); !ts.IsZero() {
return ts
}
if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
switch {
case n >= 1_000_000_000_000_000_000:
return time.Unix(0, n).UTC()
case n >= 1_000_000_000_000:
return time.UnixMilli(n).UTC()
case n >= 1_000_000_000:
return time.Unix(n, 0).UTC()
default:
return time.Unix(n, 0).UTC()
}
}
return time.Time{}
}
func zcodeUsageTime(raw string) time.Time {
return zcodeParseTime(raw)
}
func zcodeSessionFileMtime(dbPath string, db *sql.DB, row zcodeSessionRow) int64 {
maxMtime := zcodeTimeUnixNano(zcodeRowUpdatedAt(row))
if usageMtime, err := zcodeMaxUsageMtime(db, row.id); err == nil {
maxMtime = max(maxMtime, usageMtime)
}
for _, path := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} {
if info, err := os.Stat(path); err == nil {
maxMtime = max(maxMtime, info.ModTime().UnixNano())
}
}
return maxMtime
}
func zcodeMaxUsageMtime(db *sql.DB, sessionID string) (int64, error) {
rows, err := db.Query(`
SELECT CAST(COALESCE(completed_at, '') AS TEXT),
CAST(COALESCE(started_at, '') AS TEXT)
FROM model_usage
WHERE session_id = ?
`, sessionID)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "no such table: model_usage") {
return 0, nil
}
return 0, fmt.Errorf("listing zcode usage mtimes: %w", err)
}
defer rows.Close()
var maxMtime int64
for rows.Next() {
var completedAt, startedAt string
if err := rows.Scan(&completedAt, &startedAt); err != nil {
return 0, fmt.Errorf("scanning zcode usage mtime: %w", err)
}
maxMtime = max(maxMtime, zcodeTimeUnixNano(zcodeUsageTime(completedAt)))
maxMtime = max(maxMtime, zcodeTimeUnixNano(zcodeUsageTime(startedAt)))
}
if err := rows.Err(); err != nil {
return 0, err
}
return maxMtime, nil
}
func zcodeTimeUnixNano(ts time.Time) int64 {
if ts.IsZero() {
return 0
}
return ts.UnixNano()
}
func zcodeRowUpdatedAt(row zcodeSessionRow) time.Time {
if ts := zcodeParseTime(row.timeUpdated.String); !ts.IsZero() {
return ts
}
if ts := zcodeParseTime(row.timeCreated.String); !ts.IsZero() {
return ts
}
return time.Time{}
}