Agent history Python API¶
The readers and the schema contract of habemus-papadum-agent-history. The
TypeScript side is documented in the TypeScript API reference.
The DuckDB schema shared by every ingester and by the TypeScript query package.
Every table carries source (codex-fs, claude-fs, codex-api, live) and
origin (the concrete input such as state_5, rollout or thread/items/list).
Timestamps are naive UTC. Blobs live in their own tables so a later MotherDuck migration can
move them to object storage without touching the fact tables. fork_edges records which thread
inherited context from which (continued sessions, forks, forked sub-agents) and threads.lineage_id
names the root of that chain, which is what a person means by "a session". thread_links holds the
per-thread facts those are derived from and ingest_ledger what each source's files looked like when
they were last read, so a run can replace only the threads whose files changed. :func:describe renders
the contract as JSON; the TypeScript package keeps a copy and a parity test fails when they drift.
SCHEMA_VERSION = 5
module-attribute
¶
Bump when a table, column or view changes shape; the TypeScript parity test reads it.
VIEWS = {'usage_events': "\nSELECT source_id, thread_id, turn_id, ts, rollout_ordinal, model, input_tokens, cached_input_tokens,\n cache_write_input_tokens, cache_write_1h_input_tokens, output_tokens, reasoning_output_tokens,\n wasted_output_tokens, total_tokens, 'usage_record' AS basis, source\nFROM (SELECT * FROM responses WHERE origin <> 'copied-context'\n QUALIFY row_number() OVER (PARTITION BY source, response_id ORDER BY ts, rollout_ordinal) = 1)\nUNION ALL\nSELECT source_id, thread_id, turn_id, ts, rollout_ordinal, NULL AS model,\n total_input_tokens - prev_input, total_cached_input_tokens - prev_cached,\n total_cache_write_input_tokens - prev_cache_write, NULL AS cache_write_1h_input_tokens,\n total_output_tokens - prev_output, total_reasoning_output_tokens - prev_reasoning, NULL AS wasted_output_tokens,\n total_tokens - prev_total, 'counter_delta' AS basis, source\nFROM (SELECT *,\n lag(total_input_tokens, 1, 0) OVER w AS prev_input,\n lag(total_cached_input_tokens, 1, 0) OVER w AS prev_cached,\n lag(COALESCE(total_cache_write_input_tokens, 0), 1, 0) OVER w AS prev_cache_write,\n lag(total_output_tokens, 1, 0) OVER w AS prev_output,\n lag(total_reasoning_output_tokens, 1, 0) OVER w AS prev_reasoning,\n lag(total_tokens, 1, 0) OVER w AS prev_total\n FROM token_counts\n WHERE total_tokens IS NOT NULL AND thread_id NOT IN (SELECT DISTINCT thread_id FROM responses)\n WINDOW w AS (PARTITION BY thread_id ORDER BY rollout_ordinal))\nWHERE total_tokens - prev_total > 0\n"}
module-attribute
¶
Per-response usage where recorded, else positive deltas of cumulative counters.
Threads with usage records never enter the delta branch, so nothing is counted twice, and responses a
file copied from another session or parent (origin = 'copied-context') are left out. The
first counter value of a thread counts as an increment from zero; a fork inherits its parent's
counters, so that first increment can include inherited history.
create(connection)
¶
Create every table and view when absent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
DuckDBPyConnection
|
Open connection to the target database. |
required |
Source code in packages/agent-history/src/pdum/agent_history/schema.py
ddl()
¶
Return the CREATE statements for every table and view, in dependency order.
Source code in packages/agent-history/src/pdum/agent_history/schema.py
describe()
¶
Render the schema contract as JSON-compatible data.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in packages/agent-history/src/pdum/agent_history/schema.py
describe_json()
¶
from_iso(value)
¶
Convert ISO-8601 text such as 2026-10-06T13:30:29.834Z to naive UTC.
Source code in packages/agent-history/src/pdum/agent_history/schema.py
from_ms(value)
¶
Convert epoch milliseconds, or seconds when the number is small, to naive UTC.
Source code in packages/agent-history/src/pdum/agent_history/schema.py
insert(connection, table, rows)
¶
Insert rows as one Arrow batch when pyarrow is present, else statement by statement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
DuckDBPyConnection
|
Open connection. |
required |
table
|
str
|
Target table. |
required |
rows
|
list of tuple
|
Rows built with :func: |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of rows inserted. |
Source code in packages/agent-history/src/pdum/agent_history/schema.py
row(table, **values)
¶
Return a tuple in column order; unnamed columns are NULL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
table
|
str
|
Table name in :data: |
required |
**values
|
Any
|
Column values by name. |
{}
|
Source code in packages/agent-history/src/pdum/agent_history/schema.py
Read a Codex home into the shared DuckDB schema without writing to the home.
Inputs are the thread catalogue (state_5.sqlite), the history projection
(thread_history_1.sqlite), the log database (logs_2.sqlite) and every rollout file under
sessions/ and archived_sessions/. Rollouts are the record: they alone hold per-response
usage, rate limits, turn settings and fork lineage. Threads older than the support window are
listed in coverage with in_window = false and otherwise skipped.
A unit of incremental work is one thread: its rollout file plus its catalogue row, whose
updated_at and archived flag are the change fingerprint. The catalogue, projection and logs
are small databases that every run reads, but rows are written only for the units being replaced;
logs are appended from the target's last timestamp.
DEFAULT_WINDOW_DAYS = 183
module-attribute
¶
Roughly six months; older history is counted, not modelled.
ROLLOUT_NAME = re.compile('^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-(.+)$')
module-attribute
¶
rollout-<timestamp>-<thread id>; the id after the timestamp names the thread.
CodexIngest
¶
Bases: BaseIngest
One ingest of a Codex home into a target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
home
|
Path
|
Codex home to read. |
required |
target
|
Target or Path or str
|
Local DuckDB file or |
required |
window_days
|
int or None
|
Threads created before |
DEFAULT_WINDOW_DAYS
|
blobs
|
bool
|
Store full item JSON in |
True
|
now
|
datetime or None
|
Reference time for the window; defaults to the current UTC time. |
None
|
mode
|
str
|
|
'rebuild'
|
Source code in packages/agent-history/src/pdum/agent_history/codex.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 | |
rollout_items = {}
instance-attribute
¶
item_completed payloads per thread, the fallback when the projection never materialized.
ingest(units)
¶
Read the catalogue, the given units' rollouts, their projection rows and new logs.
Source code in packages/agent-history/src/pdum/agent_history/codex.py
items_from_rollouts()
¶
Write items from rollout item_completed events for threads the projection does not cover.
A home that only ran codex exec has rollouts but no materialized projection, so its tool
calls would otherwise be lost. The core's item shape is normalized to the v2 shape first.
Returns the number of items written.
Source code in packages/agent-history/src/pdum/agent_history/codex.py
prepare(target)
¶
Remember the newest log already published, so a sync appends only newer log rows.
Source code in packages/agent-history/src/pdum/agent_history/codex.py
rollout(path, unit_id)
¶
Read one rollout; return its line count, or None when outside the window.
Source code in packages/agent-history/src/pdum/agent_history/codex.py
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 | |
scan()
¶
List every thread as a unit: its rollout file, if any, plus its catalogue fingerprint.
Source code in packages/agent-history/src/pdum/agent_history/codex.py
session_meta(thread_id, payload, path, fmt)
¶
Merge a rollout's first line into the thread record; return the window anchor.
Source code in packages/agent-history/src/pdum/agent_history/codex.py
thread_in_window(thread_id, fallback_ms)
¶
Decide membership for a projection row: only this run's units, by their window, else by timestamp.
Source code in packages/agent-history/src/pdum/agent_history/codex.py
IngestSummary
dataclass
¶
What one ingest run read and wrote.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run_id
|
str
|
Identifier shared by the run's |
required |
source
|
str
|
The |
required |
source_id
|
str
|
Provenance key written to every row; stable for one agent, machine and root. |
required |
root
|
str
|
Directory that was read. |
required |
target
|
str
|
Where rows were published: a file path or |
required |
mode
|
str
|
|
required |
window_cutoff
|
str or None
|
ISO timestamp before which threads are only counted, or None when unlimited. |
required |
Source code in packages/agent-history/src/pdum/agent_history/base.py
db_path
property
¶
The target spec, kept under its old name for callers that printed it.
compact(db_path)
¶
Rewrite a freshly written DuckDB file without the free blocks bulk inserts leave behind.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_path
|
Path
|
Database to compact in place; a sibling temporary file is written and renamed over it. |
required |
Source code in packages/agent-history/src/pdum/agent_history/base.py
default_codex_home()
¶
ingest_codex_home(home=None, db_path=None, *, window_days=DEFAULT_WINDOW_DAYS, blobs=True, now=None, mode='rebuild')
¶
Ingest a Codex home into a DuckDB file or MotherDuck database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
home
|
Path or None
|
Codex home; defaults to |
None
|
db_path
|
Target or Path or str or None
|
Target DuckDB file or |
None
|
window_days
|
int or None
|
Support window in days; None ingests everything. |
DEFAULT_WINDOW_DAYS
|
blobs
|
bool
|
Store full item JSON in |
True
|
now
|
datetime or None
|
Reference time for the window, for tests. |
None
|
mode
|
str
|
|
'rebuild'
|
Returns:
| Type | Description |
|---|---|
IngestSummary
|
Phase timings, row counts and notes. |
Source code in packages/agent-history/src/pdum/agent_history/codex.py
Read a Claude Code configuration directory into the shared DuckDB schema.
Claude Code keeps one JSONL file per session under projects/<cwd-slug>/<session>.jsonl and
one file per sub-agent under projects/<cwd-slug>/<session>/subagents/agent-<id>.jsonl.
Lines form a tree through uuid and parentUuid; assistant lines carry the model, effort
and a usage block per API response; tool calls and results are content blocks paired by id.
There is no turn record, so turns are derived from user prompts. Sub-agents become their own
threads with the session as parent. Older storage formats are counted, not modelled.
A continued or forked session copies its inherited transcript into the new file, ids and
timestamps included. Since Claude Code 2.1.199 every copied message keeps the id of the session
that produced it in session_id while sessionId names the file, so the marker decides what a
file owns and names the direct predecessor (the last foreign producer before the file's own
messages). Unmarked messages before a file's first own message are copies too; unmarked files
are taken at face value. Sub-agents since 2.1.220 reference the parent's context
(fork-context-ref) instead of copying it. Each file is one unit of incremental work: parsing
it needs no other file, and what it says about other threads goes to thread_links for
:mod:pdum.agent_history.derive to turn into fork edges and lineage after publishing.
METADATA_TYPES = {'agent-setting', 'mode', 'permission-mode', 'atis-latch', 'last-prompt', 'agent-name', 'relocated', 'worktree-state', 'file-history-snapshot', 'file-history-delta', 'api-request-blob', 'api-request-shape', 'artifact-autoreact-ledger', 'artifact-comment-monitor', 'bridge-session', 'frame-link', 'queue-operation'}
module-attribute
¶
Line types that carry no session facts we model; they are counted as ignored.
TOTAL_INDEX = [name for name, _ in schema.COLUMNS['responses']].index('total_tokens')
module-attribute
¶
Origin of lines a file copies from another session or from a parent before its own first prompt.
WORKFLOW_TYPES = {'launched', 'started', 'result'}
module-attribute
¶
Workflow-tool orchestration records inside subagents/workflows/ files.
ClaudeIngest
¶
Bases: BaseIngest
One ingest of a Claude Code configuration directory into a fresh DuckDB file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
claude_dir
|
Path
|
Claude configuration directory holding |
required |
target
|
Target or Path or str
|
Local DuckDB file or |
required |
window_days
|
int or None
|
Sessions first seen before |
DEFAULT_WINDOW_DAYS
|
blobs
|
bool
|
Store tool calls with their results and message blocks in |
True
|
now
|
datetime or None
|
Reference time for the window; defaults to the current UTC time. |
None
|
mode
|
str
|
|
'rebuild'
|
Source code in packages/agent-history/src/pdum/agent_history/claude.py
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 | |
anchor(path, session_id)
¶
Return when the session itself started, for the window decision.
That is the timestamp of the first message the file did not inherit (no marker, or a marker naming this session), else the first timestamp in the file, else the modification time. A continued session carries its predecessor's old messages first, so the first timestamp alone would date it to its ancestor.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
close_turn(thread)
¶
Finalize the open turn's status and timing.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
ingest(units)
¶
Read the given session and sub-agent files.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
links(thread)
¶
What this file says about other threads: producers it copied, its successor, its fork reference.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
own_start(thread)
¶
Return when the thread produced its first own message, from markers or the boundary rule.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
scan()
¶
List every session and sub-agent file under projects/ as a unit, from metadata alone.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
session(path, parent)
¶
Read one session or sub-agent file; return its line count, or None when outside the window.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
tool_result(thread, block, record, ts)
¶
Complete the item a tool_result block answers.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
tool_use(block, record)
¶
Map one Claude tool_use block onto item columns.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
usage_rows(thread)
¶
Build responses and synthesized token_counts rows with running totals.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 | |
default_claude_dir()
¶
ingest_claude_dir(claude_dir=None, db_path=None, *, window_days=DEFAULT_WINDOW_DAYS, blobs=True, now=None, mode='rebuild')
¶
Ingest a Claude Code configuration directory into a DuckDB file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
claude_dir
|
Path or None
|
Configuration directory; defaults to |
None
|
db_path
|
Target or Path or str or None
|
Target DuckDB file or |
None
|
window_days
|
int or None
|
Support window in days; None ingests everything. |
DEFAULT_WINDOW_DAYS
|
blobs
|
bool
|
Store tool calls with results and message blocks in |
True
|
now
|
datetime or None
|
Reference time for the window, for tests. |
None
|
mode
|
str
|
|
'rebuild'
|
Returns:
| Type | Description |
|---|---|
IngestSummary
|
Phase timings, row counts and notes. |
Source code in packages/agent-history/src/pdum/agent_history/claude.py
patch_line_counts(patch)
¶
Count added and deleted lines in a Claude structuredPatch list of hunks.
Source code in packages/agent-history/src/pdum/agent_history/claude.py
Scaffolding shared by the ingesters: provenance, run summary, staging, publishing, compaction.
A run scans its source for units (one transcript file each), asks the target's ledger which units
changed, parses only those into a staging database, and publishes them into the target by replacing
each changed thread's rows inside one transaction. The target is a local DuckDB file or a
MotherDuck database; the same path serves both. Lineage is then recomputed from the store, see
:mod:pdum.agent_history.derive.
CHILD_KEYED_TABLES = ('spawn_edges', 'fork_edges')
module-attribute
¶
Tables keyed by the child thread; the child's rows are replaced with it.
DEFAULT_WINDOW_DAYS = 183
module-attribute
¶
Roughly six months; older history is counted, not modelled.
THREAD_KEYED_TABLES = ('threads', 'turns', 'responses', 'token_counts', 'items', 'item_blobs', 'rollout_events', 'response_items', 'coverage', 'thread_links')
module-attribute
¶
Tables whose rows belong to one thread and are replaced with it.
BaseIngest
¶
Common run scaffolding for one source into one target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str
|
The |
required |
agent
|
str
|
Agent kind recorded in provenance, |
required |
root
|
Path
|
Directory to read. |
required |
target
|
Target or Path or str
|
Where to publish: a local DuckDB file or |
required |
window_days
|
int or None
|
Threads anchored before |
DEFAULT_WINDOW_DAYS
|
blobs
|
bool
|
Store full item JSON in |
True
|
now
|
datetime or None
|
Reference time for the window; defaults to the current UTC time. |
None
|
host
|
dict or None
|
Host identity; defaults to :func: |
None
|
mode
|
str
|
|
'rebuild'
|
Source code in packages/agent-history/src/pdum/agent_history/base.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 | |
db_path
property
¶
The local target file; raises for MotherDuck targets.
unit_threads = {}
instance-attribute
¶
Thread ids each unit produced this run, for the ledger and for later replacement.
cover(thread_id)
¶
Return the mutable coverage record for a thread, creating it on first use.
Source code in packages/agent-history/src/pdum/agent_history/base.py
in_window(moment)
¶
Return whether a thread anchored at moment is modelled.
ingest(units)
¶
Parse the given units and write their rows.
phase(name, started, rows, detail='')
¶
Record one phase's timing in the summary and in ingest_runs.
Source code in packages/agent-history/src/pdum/agent_history/base.py
prepare(target)
¶
Read anything a sync needs from the target before parsing, such as a log watermark.
publish(staging, plan, stored)
¶
Replace the changed threads' rows in the target with the staged rows, then derive lineage.
Source code in packages/agent-history/src/pdum/agent_history/base.py
run()
¶
Scan, plan, ingest the changed units, publish, derive lineage and return the summary.
Source code in packages/agent-history/src/pdum/agent_history/base.py
scan()
¶
Return every unit the source holds, keyed by unit id, from file metadata alone.
write(table, rows)
¶
Insert rows, stamping this run's source_id where the row left it empty.
Source code in packages/agent-history/src/pdum/agent_history/base.py
write_coverage_and_meta(extra_meta=None)
¶
Write coverage, provenance and meta rows; return the number of coverage rows.
Source code in packages/agent-history/src/pdum/agent_history/base.py
IngestSummary
dataclass
¶
What one ingest run read and wrote.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run_id
|
str
|
Identifier shared by the run's |
required |
source
|
str
|
The |
required |
source_id
|
str
|
Provenance key written to every row; stable for one agent, machine and root. |
required |
root
|
str
|
Directory that was read. |
required |
target
|
str
|
Where rows were published: a file path or |
required |
mode
|
str
|
|
required |
window_cutoff
|
str or None
|
ISO timestamp before which threads are only counted, or None when unlimited. |
required |
Source code in packages/agent-history/src/pdum/agent_history/base.py
db_path
property
¶
The target spec, kept under its old name for callers that printed it.
assign_lineage(threads)
¶
Fill lineage_id and lineage_depth on every thread record from its predecessor chain.
A thread's predecessor is forked_from_id (the session it continued or forked from) or, for
a sub-agent, parent_thread_id. The root of the chain is the lineage id and the hop count is
the depth. A chain that leaves the corpus ends at the missing predecessor, which still names the
lineage so siblings of a deleted session stay together; a cycle breaks at its first repeat.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threads
|
dict
|
Thread records by id, updated in place. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in packages/agent-history/src/pdum/agent_history/base.py
compact(db_path)
¶
Rewrite a freshly written DuckDB file without the free blocks bulk inserts leave behind.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_path
|
Path
|
Database to compact in place; a sibling temporary file is written and renamed over it. |
required |
Source code in packages/agent-history/src/pdum/agent_history/base.py
host_identity(state_dir=None)
¶
Return this machine's stable identity, minting host_id on first use.
A hostname is a label, never a key: machines get renamed and a rename would fork one
machine's history into two. The id is minted once and kept under the launcher's own data
directory, never inside an agent's home, and the hostname rides along as a label that may
change. PDUM_AGENT_HISTORY_HOST_ID overrides the stored id, for tests and containers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_dir
|
Path or None
|
Directory holding |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in packages/agent-history/src/pdum/agent_history/base.py
ingester_version()
¶
Return the installed distribution version, or dev from a source checkout.
Source code in packages/agent-history/src/pdum/agent_history/base.py
repo_from_origin(origin)
¶
Normalize a git remote URL to owner/name when it has that shape.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origin
|
str or None
|
|
required |
Returns:
| Type | Description |
|---|---|
str or None
|
The last two path segments without |
Source code in packages/agent-history/src/pdum/agent_history/base.py
One command for every agent on the machine: sync each configured source into each target.
build(source, target, config, *, mode, now=None, host=None)
¶
Construct the ingester for one source and target.
Source code in packages/agent-history/src/pdum/agent_history/sync.py
sync(config, *, targets=None, full=False, now=None, host=None)
¶
Sync every source into every selected target and return one summary per pair.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
Config
|
Parsed configuration. |
required |
targets
|
iterable of str or None
|
Target names or specs to use; all when None. |
None
|
full
|
bool
|
Re-read every unit instead of only the changed ones. |
False
|
now
|
datetime or None
|
Reference time for the window, for tests. |
None
|
host
|
dict or None
|
Host identity override, for tests. |
None
|
Source code in packages/agent-history/src/pdum/agent_history/sync.py
The per-machine description of what to ingest and where to publish it.
One YAML file lists sources (Codex homes, Claude directories, or every profile of a launcher) and
targets (local DuckDB files, MotherDuck databases). pdum-history sync reads it so one command
covers every agent on the machine. The default location is the user configuration directory,
overridable with --config or PDUM_AGENT_HISTORY_CONFIG.
.. code-block:: yaml
window_days: 183
blobs: true
sources:
- agent: codex
home: ~/.codex
- agent: codex
profiles: all # every Codex launcher profile on this machine
- agent: claude
dir: ~/.claude
- agent: claude
profiles: all # every Claude launcher profile's claude/ directory
targets:
- name: local
duckdb: ~/history/agents.duckdb
- name: cloud
motherduck: agent_history # md:agent_history, token from MOTHERDUCK_TOKEN
token_env: MOTHERDUCK_BROWSER_TOKEN
Config
dataclass
¶
Parsed configuration.
Source code in packages/agent-history/src/pdum/agent_history/config.py
ConfigError
¶
Source
dataclass
¶
One directory to read.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent
|
str
|
|
required |
root
|
Path
|
The Codex home or Claude configuration directory. |
required |
label
|
str
|
Where it came from, for output: |
required |
Source code in packages/agent-history/src/pdum/agent_history/config.py
default_config_path()
¶
Return $PDUM_AGENT_HISTORY_CONFIG or history.yaml in the user configuration directory.
Source code in packages/agent-history/src/pdum/agent_history/config.py
load(path=None)
¶
Read and validate the configuration file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path or None
|
File to read; defaults to :func: |
None
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
When the file is missing, not a mapping, or names an unknown agent or an empty target. |
Source code in packages/agent-history/src/pdum/agent_history/config.py
profile_roots(agent, root)
¶
List (name, directory) for every launcher profile under root.
A Codex profile directory is itself the Codex home. A Claude profile keeps its native
configuration in a claude/ subdirectory.
Source code in packages/agent-history/src/pdum/agent_history/config.py
profiles_root(agent)
¶
Return the launcher's default profiles directory for an agent.
Where a run's rows end up: a local DuckDB file or a MotherDuck database.
A target is named by a string: a filesystem path, or md:<database> for MotherDuck. The
MotherDuck token comes from the environment (MOTHERDUCK_TOKEN, else motherduck_token, else
the variable a target names), never from a file this package writes. Every run first writes into a
staging database and then publishes into the target through one connection that sees both, so the
same code path serves both kinds of target.
Target
dataclass
¶
One place rows are published to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
str
|
A filesystem path or |
required |
name
|
str
|
Short label used in output; defaults to the spec. |
''
|
token_env
|
str or None
|
Environment variable holding the MotherDuck token when it is not one of the defaults. |
None
|
Source code in packages/agent-history/src/pdum/agent_history/targets.py
path
property
¶
The local file, for filesystem targets.
connect()
¶
Open a read-write connection to the target, creating a local file when absent.
Source code in packages/agent-history/src/pdum/agent_history/targets.py
token()
¶
Return the MotherDuck token from the environment, or None when unset.
Source code in packages/agent-history/src/pdum/agent_history/targets.py
TargetError
¶
Bases: RuntimeError
The target cannot be used as asked, for example because its schema version differs.
ensure_schema(connection, *, rebuild)
¶
Create the tables and views, or replace them when the version differs and a rebuild is allowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
DuckDBPyConnection
|
Open connection to the target database. |
required |
rebuild
|
bool
|
Drop and recreate every table of this schema when the stored version differs. Tables that
are not part of the schema, such as a |
required |
Raises:
| Type | Description |
|---|---|
TargetError
|
When the stored version differs and |
Source code in packages/agent-history/src/pdum/agent_history/targets.py
schema_version(connection)
¶
Return the schema version a database declares in meta, or None when it has none.
Source code in packages/agent-history/src/pdum/agent_history/targets.py
What a source's files looked like when they were last read, so a run can do only the new work.
The unit of incremental work is one transcript file: a Claude session or sub-agent file, a Codex
rollout with its catalogue row. Files are append-only, so size, modification time and a small
fingerprint (the catalogue's updated_at for Codex) decide whether a unit changed. A unit whose
file has vanished is marked gone and its rows stay: the agents' own stores forget, this one does
not. Removing history is an explicit command, never a side effect of a run.
LedgerRow
dataclass
¶
One stored ledger entry.
Source code in packages/agent-history/src/pdum/agent_history/ledger.py
Plan
dataclass
¶
Which units a run reads, leaves alone or marks gone.
Source code in packages/agent-history/src/pdum/agent_history/ledger.py
Unit
dataclass
¶
One file as found on disk during a scan.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_id
|
str
|
Stable id, the thread id the file is about ( |
required |
path
|
Path or None
|
The file; None for a catalogue-only Codex thread without a rollout. |
required |
bytes
|
int
|
File size. |
0
|
mtime
|
datetime or None
|
Modification time as naive UTC. |
None
|
fingerprint
|
str or None
|
Extra change signal, such as the catalogue's |
None
|
extra
|
dict
|
Anything the ingester needs later, such as a sub-agent file's parent session. |
dict()
|
Source code in packages/agent-history/src/pdum/agent_history/ledger.py
plan(scan, ledger, *, full)
¶
Compare a scan with the ledger.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scan
|
dict
|
Units found on disk, by id. |
required |
ledger
|
dict
|
Stored entries, by id. |
required |
full
|
bool
|
Treat every scanned unit as changed. |
required |
Source code in packages/agent-history/src/pdum/agent_history/ledger.py
read(connection, source_id)
¶
Return the stored ledger for one source, empty when the table is absent.
Source code in packages/agent-history/src/pdum/agent_history/ledger.py
rows(source_id, run_id, plan_, ledger, thread_ids, now)
¶
Build the ledger rows a run writes: changed units as present, vanished units as gone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
thread_ids
|
dict
|
Thread ids each changed unit produced, so a later replacement can delete exactly those. |
required |
Source code in packages/agent-history/src/pdum/agent_history/ledger.py
Facts that look across threads, computed from the store after a source's threads are written.
Parsing one file decides what that file owns. Which file continued which, and which root a thread
belongs to, need every thread of a source, so the ingesters record per-thread links while parsing
(thread_links) and this module turns them into fork_edges and the forked_from_id,
lineage_id and lineage_depth columns of threads. It runs on the target after every
publish, over metadata only, so an incremental run that replaced three files recomputes a source's
lineage in milliseconds.
Link kinds
producer
A Claude file copied records messages produced by target_thread_id; ordinal is the
order of first appearance, ts the last copied message. The last producer is the parent.
continued-in
A Claude session's own record naming the session that continued it.
fork-context-ref
A Claude sub-agent referencing its session's context; detail holds the parent's last uuid.
forked-from
A Codex rollout's session_meta naming the thread it was forked from.
dedupe_responses(connection, source_id)
¶
Mark responses that several unmarked files of one source claim, keeping the earliest-started thread's.
Parallel sub-agents launched from one call can begin with the same responses, and files
written without markers cannot say so themselves. After publishing, any response id that more
than one thread of a source holds as its own stays with the thread that started first (ties
broken by thread id) and the other copies get origin = 'copied-context', so usage counts
them once and the duplicate check stays at zero. One pass settles every group.
Returns:
| Type | Description |
|---|---|
int
|
Number of response rows marked as copies. |
Source code in packages/agent-history/src/pdum/agent_history/derive.py
derive_lineage(connection, source_id, source)
¶
Rebuild fork_edges and the lineage columns of threads for one source.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
DuckDBPyConnection
|
Connection to the database holding the source's threads and links. |
required |
source_id
|
str
|
Which source to recompute. |
required |
source
|
str
|
The |
required |
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in packages/agent-history/src/pdum/agent_history/derive.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | |