Skip to content

Commit 3e74489

Browse files
brownjuly2003-codeJuliaEdomclaude
authored
feat(ops): exception inbox — dead-letter/webhook overlay + R1/R2 reconciliation (D4) (#146)
GET /v1/ops/exceptions, GET /v1/ops/exceptions/stats, POST .../acknowledge, POST .../resolve (ops-surfaces-spec.md §4): one triage feed aggregating dead-letter events (native, read-only mirror of the existing store), dead webhook deliveries and R1 journal_vs_store / R2 stuck_replay reconciliation findings (both overlay-backed via the new ops_exception_triage table, control- plane state class 7, both adapters). - store.py: TriageState + ensure/list/upsert/auto-resolve/set-state/count port methods, plus dead-letter/webhook reads for the inbox and stuck_replay_threshold_seconds() (reuses AGENTFLOW_CONTROLPLANE_LEASE_SECONDS). - embedded.py/postgres.py: ops_exception_triage schema + full method set; the resolved-row reopen decision runs in SQL (not Python) so DuckDB's naive-local timestamp coercion and PostgreSQL's TIMESTAMPTZ both compare consistently regardless of whether the caller's seen_at is aware or naive. - reconciliation.py: pure R1 (serving store behind its journal stage) / R2 (dead-letter stuck in replay_pending) checks, read-only over the QueryEngine/ControlPlaneStore ports (I10). - duckdb_backend.py: dead_letter_events store counterparts for the two already-seeded evt-004/evt-009 journal rows, so the demo inbox is non-empty (I7) — control-plane state on the shared connection, seeded once regardless of SERVING_BACKEND. - manual_resolutions (§4.5) counts only operator acknowledge/resolve actions, not system auto-resolves (AUTO_RESOLVE_NOTE sentinel). Tests: 16 new unit cases for the store/overlay methods, a new test_exceptions_inbox.py integration suite (demo pins I7, stable ids I5, dl: 409 guard I6, R1/R2 detection, idempotent reads I10, auto-resolve, ack/resolve lifecycle, manual_resolutions re-pin I9), plus a tenant-scoping case in test_tenant_isolation.py (I8). Full unit 1620 passed, integration 240 passed/52 skipped (docker unavailable on this host, pre-existing), ruff+mypy strict clean, openapi/agent-tools regenerated. Co-authored-by: JuliaEdom <uedomskikh@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 330c056 commit 3e74489

13 files changed

Lines changed: 2612 additions & 9 deletions

File tree

‎docs/agent-tools/claude-tools.json‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,151 @@
882882
}
883883
}
884884
},
885+
{
886+
"name": "list_exceptions_v1_ops_exceptions_get",
887+
"description": "List Exceptions",
888+
"input_schema": {
889+
"type": "object",
890+
"properties": {
891+
"source": {
892+
"anyOf": [
893+
{
894+
"enum": [
895+
"deadletter",
896+
"webhook_delivery",
897+
"reconciliation"
898+
],
899+
"type": "string"
900+
},
901+
{
902+
"type": "null"
903+
}
904+
],
905+
"title": "Source"
906+
},
907+
"status": {
908+
"anyOf": [
909+
{
910+
"enum": [
911+
"open",
912+
"in_progress",
913+
"acknowledged",
914+
"resolved"
915+
],
916+
"type": "string"
917+
},
918+
{
919+
"type": "null"
920+
}
921+
],
922+
"title": "Status"
923+
},
924+
"page": {
925+
"type": "integer",
926+
"minimum": 1,
927+
"default": 1,
928+
"title": "Page"
929+
},
930+
"page_size": {
931+
"type": "integer",
932+
"maximum": 100,
933+
"minimum": 1,
934+
"default": 50,
935+
"title": "Page Size"
936+
}
937+
}
938+
}
939+
},
940+
{
941+
"name": "exceptions_stats_v1_ops_exceptions_stats_get",
942+
"description": "Exceptions Stats",
943+
"input_schema": {
944+
"type": "object",
945+
"properties": {}
946+
}
947+
},
948+
{
949+
"name": "acknowledge_exception_v1_ops_exceptions__item_id__acknowledge_post",
950+
"description": "Acknowledge Exception",
951+
"input_schema": {
952+
"type": "object",
953+
"properties": {
954+
"item_id": {
955+
"type": "string",
956+
"title": "Item Id"
957+
},
958+
"body": {
959+
"anyOf": [
960+
{
961+
"properties": {
962+
"note": {
963+
"anyOf": [
964+
{
965+
"type": "string"
966+
},
967+
{
968+
"type": "null"
969+
}
970+
],
971+
"title": "Note"
972+
}
973+
},
974+
"type": "object",
975+
"title": "TriageActionRequest"
976+
},
977+
{
978+
"type": "null"
979+
}
980+
],
981+
"title": "Payload"
982+
}
983+
},
984+
"required": [
985+
"item_id"
986+
]
987+
}
988+
},
989+
{
990+
"name": "resolve_exception_v1_ops_exceptions__item_id__resolve_post",
991+
"description": "Resolve Exception",
992+
"input_schema": {
993+
"type": "object",
994+
"properties": {
995+
"item_id": {
996+
"type": "string",
997+
"title": "Item Id"
998+
},
999+
"body": {
1000+
"anyOf": [
1001+
{
1002+
"properties": {
1003+
"note": {
1004+
"anyOf": [
1005+
{
1006+
"type": "string"
1007+
},
1008+
{
1009+
"type": "null"
1010+
}
1011+
],
1012+
"title": "Note"
1013+
}
1014+
},
1015+
"type": "object",
1016+
"title": "TriageActionRequest"
1017+
},
1018+
{
1019+
"type": "null"
1020+
}
1021+
],
1022+
"title": "Payload"
1023+
}
1024+
},
1025+
"required": [
1026+
"item_id"
1027+
]
1028+
}
1029+
},
8851030
{
8861031
"name": "search_v1_search_get",
8871032
"description": "Search",

‎docs/agent-tools/openai-tools.json‎

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,163 @@
981981
}
982982
}
983983
},
984+
{
985+
"type": "function",
986+
"function": {
987+
"name": "list_exceptions_v1_ops_exceptions_get",
988+
"description": "List Exceptions",
989+
"parameters": {
990+
"type": "object",
991+
"properties": {
992+
"source": {
993+
"anyOf": [
994+
{
995+
"enum": [
996+
"deadletter",
997+
"webhook_delivery",
998+
"reconciliation"
999+
],
1000+
"type": "string"
1001+
},
1002+
{
1003+
"type": "null"
1004+
}
1005+
],
1006+
"title": "Source"
1007+
},
1008+
"status": {
1009+
"anyOf": [
1010+
{
1011+
"enum": [
1012+
"open",
1013+
"in_progress",
1014+
"acknowledged",
1015+
"resolved"
1016+
],
1017+
"type": "string"
1018+
},
1019+
{
1020+
"type": "null"
1021+
}
1022+
],
1023+
"title": "Status"
1024+
},
1025+
"page": {
1026+
"type": "integer",
1027+
"minimum": 1,
1028+
"default": 1,
1029+
"title": "Page"
1030+
},
1031+
"page_size": {
1032+
"type": "integer",
1033+
"maximum": 100,
1034+
"minimum": 1,
1035+
"default": 50,
1036+
"title": "Page Size"
1037+
}
1038+
}
1039+
}
1040+
}
1041+
},
1042+
{
1043+
"type": "function",
1044+
"function": {
1045+
"name": "exceptions_stats_v1_ops_exceptions_stats_get",
1046+
"description": "Exceptions Stats",
1047+
"parameters": {
1048+
"type": "object",
1049+
"properties": {}
1050+
}
1051+
}
1052+
},
1053+
{
1054+
"type": "function",
1055+
"function": {
1056+
"name": "acknowledge_exception_v1_ops_exceptions__item_id__acknowledge_post",
1057+
"description": "Acknowledge Exception",
1058+
"parameters": {
1059+
"type": "object",
1060+
"properties": {
1061+
"item_id": {
1062+
"type": "string",
1063+
"title": "Item Id"
1064+
},
1065+
"body": {
1066+
"anyOf": [
1067+
{
1068+
"properties": {
1069+
"note": {
1070+
"anyOf": [
1071+
{
1072+
"type": "string"
1073+
},
1074+
{
1075+
"type": "null"
1076+
}
1077+
],
1078+
"title": "Note"
1079+
}
1080+
},
1081+
"type": "object",
1082+
"title": "TriageActionRequest"
1083+
},
1084+
{
1085+
"type": "null"
1086+
}
1087+
],
1088+
"title": "Payload"
1089+
}
1090+
},
1091+
"required": [
1092+
"item_id"
1093+
]
1094+
}
1095+
}
1096+
},
1097+
{
1098+
"type": "function",
1099+
"function": {
1100+
"name": "resolve_exception_v1_ops_exceptions__item_id__resolve_post",
1101+
"description": "Resolve Exception",
1102+
"parameters": {
1103+
"type": "object",
1104+
"properties": {
1105+
"item_id": {
1106+
"type": "string",
1107+
"title": "Item Id"
1108+
},
1109+
"body": {
1110+
"anyOf": [
1111+
{
1112+
"properties": {
1113+
"note": {
1114+
"anyOf": [
1115+
{
1116+
"type": "string"
1117+
},
1118+
{
1119+
"type": "null"
1120+
}
1121+
],
1122+
"title": "Note"
1123+
}
1124+
},
1125+
"type": "object",
1126+
"title": "TriageActionRequest"
1127+
},
1128+
{
1129+
"type": "null"
1130+
}
1131+
],
1132+
"title": "Payload"
1133+
}
1134+
},
1135+
"required": [
1136+
"item_id"
1137+
]
1138+
}
1139+
}
1140+
},
9841141
{
9851142
"type": "function",
9861143
"function": {

0 commit comments

Comments
 (0)