// call_monitor: telemetria de invocaciones del agente al fn_registry. // Issue 0085. Persiste eventos en operations.db local. Hook PostToolUse (0085b) // y wrapper Python (0085c) escriben aqui. registry_dashboard lee via sqlite_api. package main import ( "flag" "fmt" "os" "path/filepath" "text/tabwriter" ) const defaultDBName = "operations.db" func main() { if len(os.Args) < 2 { usage() os.Exit(2) } sub := os.Args[1] fs := flag.NewFlagSet(sub, flag.ExitOnError) dbPath := fs.String("db", "", "Path to operations.db (default: ./operations.db relative to this binary's directory).") switch sub { case "init": fs.Parse(os.Args[2:]) runInit(resolveDB(*dbPath)) case "status": topN := fs.Int("top", 10, "Top N functions to list.") fs.Parse(os.Args[2:]) runStatus(resolveDB(*dbPath), *topN) case "snapshot": registry := fs.String("registry", "", "Path to registry.db (default: walk up from cwd until found).") fs.Parse(os.Args[2:]) runSnapshot(resolveDB(*dbPath), *registry) case "copied-code": root := fs.String("root", "", "Path to fn_registry root (default: walk up from cwd until registry.db found).") fs.Parse(os.Args[2:]) runCopiedCode(resolveDB(*dbPath), *root) case "propose": root := fs.String("root", "", "Path to fn_registry root (default: walk up from cwd).") dry := fs.Bool("dry-run", false, "Generate drafts without persisting to registry.db.proposals.") fs.Parse(os.Args[2:]) runPropose(*root, *dry) case "sequences": detect := fs.Bool("detect", false, "Run sequence detection (required).") report := fs.Bool("report", false, "Print human-readable report (implied when --propose is not set).") propose := fs.Bool("propose", false, "Write new_pipeline proposals to registry.db for uncovered candidates.") formatJSON := fs.Bool("format", false, "Output JSON instead of text (use with --report or --detect).") root := fs.String("root", "", "Path to fn_registry root (default: walk up from cwd).") minOcc := fs.Int("min-occurrences", 5, "Minimum occurrences to be a candidate.") windowSecs := fs.Int("window-secs", 30, "Max gap in seconds between consecutive calls in a sequence.") lookbackDays := fs.Int("lookback-days", 30, "How many days of calls to scan.") fs.Parse(os.Args[2:]) cfg := SequenceConfig{ WindowSecs: *windowSecs, LookbackDays: *lookbackDays, MinOccurrences: *minOcc, MinSessions: 2, MinSuccessRate: 0.9, } runSequences(resolveDB(*dbPath), *detect, *report, *propose, *formatJSON, *root, cfg) case "-h", "--help", "help": usage() default: fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n\n", sub) usage() os.Exit(2) } } func usage() { fmt.Fprintln(os.Stderr, `call_monitor — telemetria de invocaciones del agente al fn_registry USO: call_monitor [flags] SUBCOMANDOS: init Crea/abre operations.db y aplica migraciones (idempotente). status Resumen: conteo de filas por tabla + top funciones por calls_total. snapshot Lee registry.db.functions y snapshotea (function_id, content_hash) en function_versions con source='index'. Idempotente: solo inserta nuevas tuplas. sequences Detecta secuencias A→B(→C) repetidas candidatas a pipeline one-shot. --detect Ejecutar deteccion (obligatorio). --report Imprimir reporte human-readable (default cuando no se usa --propose). --propose Escribir proposals new_pipeline a registry.db. --format Output JSON en vez de texto. --min-occurrences N Minimo de ocurrencias para ser candidata (default 5). --window-secs N Max gap en segundos entre calls consecutivas (default 30). --lookback-days N Dias de historial a escanear (default 30). FLAGS GLOBALES: --db PATH Ruta a operations.db (default: ./operations.db junto al binario). --registry PATH (subcomando snapshot) Ruta a registry.db. Default: walk up. EJEMPLOS: call_monitor init call_monitor status --top 20 call_monitor snapshot call_monitor snapshot --registry /home/lucas/fn_registry/registry.db call_monitor sequences --detect --report call_monitor sequences --detect --propose call_monitor sequences --detect --report --format --min-occurrences 3`) } func resolveRegistryDB(override string) string { if override != "" { return override } exe, err := os.Executable() if err != nil { return "registry.db" } dir := filepath.Dir(exe) for { candidate := filepath.Join(dir, "registry.db") if _, err := os.Stat(candidate); err == nil { return candidate } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return "registry.db" } func resolveRegistryRoot(override string) string { if override != "" { return override } exe, err := os.Executable() if err != nil { return "." } dir := filepath.Dir(exe) for { if _, err := os.Stat(filepath.Join(dir, "registry.db")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return "." } func runCopiedCode(callDBPath, rootOverride string) { db, err := openDB(callDBPath) if err != nil { fmt.Fprintf(os.Stderr, "error: open call_monitor db: %v\n", err) os.Exit(1) } defer db.Close() root := resolveRegistryRoot(rootOverride) if _, err := os.Stat(filepath.Join(root, "registry.db")); err != nil { fmt.Fprintf(os.Stderr, "error: registry.db not found under %s\n", root) os.Exit(1) } inserted, total, err := persistCopiedCode(db, root) if err != nil { fmt.Fprintf(os.Stderr, "error: copied-code: %v\n", err) os.Exit(1) } fmt.Printf("copied-code: %d total match(es) detected, %d newly inserted into copied_code.\n", total, inserted) } func runSnapshot(callDBPath, registryOverride string) { db, err := openDB(callDBPath) if err != nil { fmt.Fprintf(os.Stderr, "error: open call_monitor db: %v\n", err) os.Exit(1) } defer db.Close() registryPath := resolveRegistryDB(registryOverride) if _, err := os.Stat(registryPath); err != nil { fmt.Fprintf(os.Stderr, "error: registry.db not found at %s\n", registryPath) os.Exit(1) } inserted, seen, err := snapshotFromRegistry(db, registryPath) if err != nil { fmt.Fprintf(os.Stderr, "error: snapshot: %v\n", err) os.Exit(1) } fmt.Printf("snapshot: %d new versions inserted (out of %d functions seen with content_hash)\n", inserted, seen) } func resolveDB(override string) string { if override != "" { return override } exe, err := os.Executable() if err == nil { return filepath.Join(filepath.Dir(exe), defaultDBName) } return defaultDBName } func runInit(path string) { db, err := openDB(path) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } defer db.Close() abs, _ := filepath.Abs(path) fmt.Printf("call_monitor.operations.db ready: %s\n", abs) } func runStatus(path string, topN int) { db, err := openDB(path) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } defer db.Close() counts, err := db.tableCounts() if err != nil { fmt.Fprintf(os.Stderr, "table counts: %v\n", err) os.Exit(1) } tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, "=== Tables ===") fmt.Fprintln(tw, "TABLE\tROWS") for _, c := range counts { fmt.Fprintf(tw, "%s\t%d\n", c.Name, c.Rows) } tw.Flush() top, err := db.topFunctions(topN) if err != nil { fmt.Fprintf(os.Stderr, "top functions: %v\n", err) os.Exit(1) } fmt.Println() tw = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintf(tw, "=== Top %d functions (by calls_total) ===\n", topN) if len(top) == 0 { fmt.Fprintln(tw, "(no calls recorded yet)") tw.Flush() return } fmt.Fprintln(tw, "FUNCTION_ID\tCALLS\tCALLS_7D\tERRORS\tERROR_RATE\tMEAN_MS\tLAST_USED_TS") for _, s := range top { last := "" if s.LastUsedAt.Valid { last = fmt.Sprintf("%d", s.LastUsedAt.Int64) } fmt.Fprintf(tw, "%s\t%d\t%d\t%d\t%.2f\t%.0f\t%s\n", s.FunctionID, s.CallsTotal, s.Calls7d, s.ErrorsTotal, s.ErrorRate, s.MeanDurationMs, last) } tw.Flush() }