{"record":{"id":"cd461655eb773bbf","repo":"vxcontrol/pentagi","slug":"failed-to-get-termlog-w","errorCode":null,"errorMessage":"failed to get termlog: %w","messagePattern":"failed to get termlog: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/pkg/controller/termlog.go","lineNumber":87,"sourceCode":"\t\tText:        database.SanitizeUTF8(msg),\n\t\tContainerID: containerID,\n\t\tFlowID:      tlw.flowID,\n\t\tTaskID:      database.Int64ToNullInt64(taskID),\n\t\tSubtaskID:   database.Int64ToNullInt64(subtaskID),\n\t})\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to create termlog: %w\", err)\n\t}\n\n\ttlw.pub.TerminalLogAdded(ctx, termLog)\n\n\treturn termLog.ID, nil\n}\n\nfunc (tlw *flowTermLogWorker) GetMsg(ctx context.Context, msgID int64) (database.Termlog, error) {\n\tmsg, err := tlw.db.GetTermLog(ctx, msgID)\n\tif err != nil {\n\t\treturn database.Termlog{}, fmt.Errorf(\"failed to get termlog: %w\", err)\n\t}\n\n\treturn msg, nil\n}\n\nfunc (tlw *flowTermLogWorker) GetContainers(ctx context.Context) ([]database.Container, error) {\n\tcontainers, err := tlw.db.GetFlowContainers(ctx, tlw.flowID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get containers: %w\", err)\n\t}\n\n\treturn containers, nil\n}\n","sourceCodeStart":69,"sourceCodeEnd":101,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/controller/termlog.go#L69-L101","documentation":"GetMsg fetches a single terminal log row by message ID via db.GetTermLog (no flow scoping in the query itself). Any failure — most often sql.ErrNoRows for a deleted/never-existing message ID — is wrapped with this message. Transient DB errors are wrapped identically.","triggerScenarios":"Requesting a msgID that was deleted by retention/cleanup, an ID from another flow via a mismatched worker handle, or during a DB outage.","commonSituations":"Frontend subscription replays referencing pruned rows; clients caching message IDs across environment resets; pagination cursors pointing past deleted data.","solutions":["Check errors.Is(err, sql.ErrNoRows) and map to 404","Confirm the message row still exists (retention may have purged it)","Use the correct flow's FlowTermLogWorker for authorization before fetching","Validate msgID > 0 and comes from a trusted source","Retry only transient connection errors"],"exampleFix":"// before\nmsg, err := worker.GetMsg(ctx, msgID)\nif err != nil { return err } // 500 for missing rows\n// after\nmsg, err := worker.GetMsg(ctx, msgID)\nif errors.Is(err, sql.ErrNoRows) { return nil, ErrTermlogNotFound }\nif err != nil { return err }","handlingStrategy":"type-guard","validationCode":"func validMsgID(id int64) bool { return id > 0 }","typeGuard":"func termlogNotFound(err error) bool { return errors.Is(err, sql.ErrNoRows) }","tryCatchPattern":"msg, err := w.GetMsg(ctx, msgID)\nif termlogNotFound(err) {\n    return nil, httpErr(http.StatusNotFound, \"termlog %d not found\", msgID)\n}\nif err != nil {\n    return nil, fmt.Errorf(\"get termlog: %w\", err)\n}","preventionTips":["Map sql.ErrNoRows to 404 in handlers instead of a 500","Account for retention policies deleting old termlog rows referenced by clients","Validate message IDs before querying and scope authorization via the flow worker","Treat subscription replays of purged rows as normal and skip them","Retry only transient connection errors, never a no-rows result"],"tags":["database","not-found"],"backgroundTag":"sql-no-rows","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}