Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
24019f6
docs: define incremental thread index design
seonghobae Aug 4, 2026
21aa7ba
docs: plan incremental thread index implementation
seonghobae Aug 4, 2026
b0623b5
test: define incremental index public contract
seonghobae Aug 4, 2026
9d68967
test: define incremental component parity
seonghobae Aug 4, 2026
238f271
test: define incremental RFC parity
seonghobae Aug 4, 2026
a1d2b91
test: define incremental snapshot contract
seonghobae Aug 4, 2026
29a639a
feat: implement atomic incremental thread index
seonghobae Aug 5, 2026
dee120b
feat: export incremental mailbox API
seonghobae Aug 5, 2026
75249b8
test: cover incremental validation branches
seonghobae Aug 5, 2026
b0bf3e6
test: cover incremental graph defenses
seonghobae Aug 5, 2026
7d75421
test: require incremental API docstrings
seonghobae Aug 5, 2026
1a88c68
fix: enforce RFC 8474 ObjectID contracts
seonghobae Aug 5, 2026
9259123
test: enforce RFC 8474 ObjectID contracts
seonghobae Aug 5, 2026
0a2e128
docs: explain incremental mailbox threading
seonghobae Aug 5, 2026
b96ee32
docs: record incremental mailbox changes
seonghobae Aug 5, 2026
b813fd8
docs: document incremental mailbox API
seonghobae Aug 5, 2026
459da07
ci: smoke-test installed incremental API
seonghobae Aug 5, 2026
459a5bd
docs: add incremental threading invariants
seonghobae Aug 5, 2026
668d292
ci: stage verified incremental update bundle
seonghobae Aug 5, 2026
c942f33
ci: trigger sealed PR 20 update on synchronize
seonghobae Aug 5, 2026
ff1a6c7
ci: apply sealed incremental update from required PR workflow
seonghobae Aug 5, 2026
9d8c7de
ci: verify sealed update by committed Git blob identity
seonghobae Aug 5, 2026
8ab207a
ci: decode and verify the sealed PR 20 update
seonghobae Aug 5, 2026
6f420b3
ci: stage verified PR 20 patch part 1
seonghobae Aug 5, 2026
96e0d7b
ci: stage verified PR 20 patch part 2
seonghobae Aug 5, 2026
f4ea764
ci: stage verified PR 20 patch part 3
seonghobae Aug 5, 2026
9b2c42d
ci: stage verified PR 20 patch part 4
seonghobae Aug 5, 2026
872acb0
ci: stage verified PR 20 patch part 5
seonghobae Aug 5, 2026
bd2e132
ci: stage verified PR 20 patch part 6
seonghobae Aug 5, 2026
838ac37
ci: stage verified PR 20 patch part 7
seonghobae Aug 5, 2026
7911032
ci: stage verified PR 20 patch part 8
seonghobae Aug 5, 2026
bbee122
ci: stage verified PR 20 patch part 9
seonghobae Aug 5, 2026
76ced79
ci: stage verified PR 20 patch part 10
seonghobae Aug 5, 2026
9326ce6
ci: apply the verified PR 20 text patch
seonghobae Aug 5, 2026
6d6e343
ci: repair verified PR 20 patch lint failures
seonghobae Aug 5, 2026
2e9f0a2
ci: publish verified PR 20 product changes without workflow mutation
seonghobae Aug 5, 2026
f4e44ab
perf: harden incremental mailbox updates
github-actions[bot] Aug 5, 2026
10a02a3
ci: schedule reproducible incremental mailbox benchmarks
seonghobae Aug 5, 2026
f9728b3
ci: remove the completed PR 20 bootstrap job
seonghobae Aug 5, 2026
49fbe0b
ci: remove the completed push bootstrap workflow
seonghobae Aug 5, 2026
6cbfa46
ci: remove the completed pull-request bootstrap workflow
seonghobae Aug 5, 2026
6f00b1a
test: stage verified incremental concurrency fix
seonghobae Aug 5, 2026
c6b52a3
ci: verify and apply incremental concurrency fix
seonghobae Aug 5, 2026
bd9b620
ci: trigger verified concurrency fix from PR synchronization
seonghobae Aug 5, 2026
15c2c9c
test: update verified incremental concurrency fix
seonghobae Aug 5, 2026
d8d9592
ci: verify the corrected concurrency patch
seonghobae Aug 5, 2026
54e4434
fix: serialize incremental index state access
github-actions[bot] Aug 5, 2026
19ca855
ci: remove completed concurrency-fix bootstrap
seonghobae Aug 5, 2026
b62ba55
ci: verify incremental snapshot hardening test-first
seonghobae Aug 5, 2026
5d12348
ci: make snapshot hardening verifier anchor-safe
seonghobae Aug 5, 2026
4520933
fix: harden incremental snapshot validation
github-actions[bot] Aug 5, 2026
fb48d5e
ci: remove completed snapshot hardening verifier
seonghobae Aug 5, 2026
6a7f75e
test: stage plain-container snapshot hardening
seonghobae Aug 5, 2026
5e7e8c4
ci: verify and apply plain-container snapshot hardening
seonghobae Aug 5, 2026
4115e89
fix: reject executable snapshot container subclasses
github-actions[bot] Aug 5, 2026
635e14c
ci: remove completed plain-JSON hardening verifier
seonghobae Aug 5, 2026
344d21f
test: stage cyclic snapshot regression
seonghobae Aug 5, 2026
a189871
fix: stage iterative cyclic snapshot detection
seonghobae Aug 5, 2026
83141bb
ci: prove and apply cyclic snapshot hardening
seonghobae Aug 5, 2026
812cacf
ci: verify cyclic hardening patches by Git blob identity
seonghobae Aug 5, 2026
e0aced3
ci: run the cyclic snapshot fix through an exact-state TDD verifier
seonghobae Aug 5, 2026
57219ee
fix: reject cyclic incremental snapshots iteratively
github-actions[bot] Aug 5, 2026
cf1cecb
ci: remove completed cyclic snapshot verifier
seonghobae Aug 5, 2026
e77e146
test: stage bounded snapshot expansion hardening
seonghobae Aug 6, 2026
504d6f2
ci: verify bounded incremental snapshot expansion
seonghobae Aug 6, 2026
f3b3fb8
ci: correct snapshot hardening red proof
seonghobae Aug 6, 2026
6b69d0a
fix: bound incremental snapshot expansion
github-actions[bot] Aug 6, 2026
aa5c199
ci: remove completed snapshot expansion verifier
seonghobae Aug 6, 2026
eb301a8
ci: apply PR 20 ordering review fix
seonghobae Aug 6, 2026
0062681
test: complete hostile ordering protocol
github-actions[bot] Aug 6, 2026
7a0d2f0
ci: remove completed ordering review verifier
seonghobae Aug 6, 2026
856498b
ci: verify allocation-free snapshot UTF-8 sizing
seonghobae Aug 6, 2026
b38cd84
perf: avoid snapshot UTF-8 bytes duplication
github-actions[bot] Aug 6, 2026
1f159c4
ci: remove completed UTF-8 sizing verifier
seonghobae Aug 6, 2026
00b0422
ci: verify snapshot restore preflight
seonghobae Aug 6, 2026
68d3c34
fix: preflight incremental snapshot bounds
github-actions[bot] Aug 6, 2026
a38532c
ci: remove completed snapshot preflight verifier
seonghobae Aug 6, 2026
1144842
perf: stage bounded incremental state overlays
seonghobae Aug 6, 2026
acbdc3c
ci: verify bounded incremental state overlays
seonghobae Aug 6, 2026
fc78fe6
ci: repair bounded overlay patch metadata
seonghobae Aug 6, 2026
0855822
ci: repair all bounded overlay hunk counts
seonghobae Aug 6, 2026
1593bd7
ci: apply bounded overlays without workflow mutation
seonghobae Aug 6, 2026
db66b40
perf: bound default incremental state updates
github-actions[bot] Aug 6, 2026
5a46216
ci: remove completed bounded-overlay repair verifier
seonghobae Aug 6, 2026
46e3c5d
ci: remove completed bounded-overlay verifier
seonghobae Aug 6, 2026
6e1b289
ci: diagnose hourly product workflow validation
seonghobae Aug 6, 2026
9ec4786
ci: correct actionlint diagnostic invocation
seonghobae Aug 6, 2026
15f362e
ci: diagnose every repository workflow
seonghobae Aug 6, 2026
7165a4f
ci: diagnose workflows after known YAML correction
seonghobae Aug 6, 2026
db85f2b
ci: diagnose workflows after nested-shell correction
seonghobae Aug 6, 2026
cc68649
ci: move nested-shell actionlint suppression
seonghobae Aug 6, 2026
d35b1f4
ci: correct nested-shell actionlint directive
seonghobae Aug 6, 2026
ea0e54a
ci: finalize bounded incremental state overlays
seonghobae Aug 6, 2026
a2daf91
ci: finalize bounded incremental state overlays
github-actions[bot] Aug 6, 2026
fc626fa
ci: ensure incremental delta memory evidence
seonghobae Aug 6, 2026
da4b859
perf: retain incremental delta allocation evidence
github-actions[bot] Aug 6, 2026
871b0e5
docs: restore benchmark bullet indentation
seonghobae Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- name: Install the reviewed CI lock
run: python -m pip install --require-hashes -r requirements/ci.lock
- run: ruff check .
- run: python -m compileall -q src tests scripts
- run: python -m compileall -q src tests scripts benchmarks
- name: Run production doctests
run: |
python -m doctest \
Expand Down Expand Up @@ -143,6 +143,9 @@ jobs:
cd "$temp_dir"
python - <<'PY'
from threadweave import (
IncrementalThreadIndex,
IndexedMessage,
MailboxChangeSet,
Message,
__version__,
serialize_thread_response,
Expand Down Expand Up @@ -170,6 +173,39 @@ jobs:
"later@example.com",
]
assert serialize_thread_response(roots) == "* THREAD (1)(2)\r\n"

index = IncrementalThreadIndex()
delta = index.apply(
MailboxChangeSet(
expected_version=0,
additions=(
IndexedMessage(
"root-key",
Message(message_id="root", sequence_number=1, uid=101),
email_id="Email_101",
thread_id="Thread_7",
),
IndexedMessage(
"reply-key",
Message(
message_id="reply",
references=["root"],
sequence_number=2,
uid=102,
),
email_id="Email_102",
thread_id="Thread_7",
),
),
)
)
assert delta.version == 1
assert index.projections[0].message_keys == ("root-key", "reply-key")
assert serialize_thread_response(index.roots, identifier="uid") == (
"* THREAD (101 102)\r\n"
)
restored = IncrementalThreadIndex.restore(index.snapshot())
assert restored.projections == index.projections
PY
- run: python -m pip check
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/diagnose-hourly-product-workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Diagnose repository workflows

on:
push:
branches:
- feature/incremental-thread-index
paths:
- .github/workflows/diagnose-hourly-product-workflow.yml

permissions:
contents: read

jobs:
actionlint:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out the exact branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: feature/incremental-thread-index
persist-credentials: false

- name: Install pinned actionlint
shell: bash
run: |
set -euo pipefail
archive="${RUNNER_TEMP}/actionlint.tar.gz"
curl -fsSL \
-o "$archive" \
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz
printf '%s %s\n' \
8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 \
"$archive" | sha256sum -c -
tar -xzf "$archive" -C "$RUNNER_TEMP" actionlint

- name: Apply known corrections only in the diagnostic workspace
shell: bash
run: |
set -euo pipefail
python - <<'PY'
from pathlib import Path

path = Path('.github/workflows/hourly-product-development.yml')
text = path.read_text(encoding='utf-8')
indentation_old = " printf 'threadweave @ file://%s \\\n --hash=sha256:%s\\n' \\\n"
indentation_new = " printf 'threadweave @ file://%s \\\n --hash=sha256:%s\\n' \\\n"
if text.count(indentation_old) != 1:
raise SystemExit('unexpected hourly workflow indentation state')
text = text.replace(indentation_old, indentation_new)

shellcheck_old = " if timeout --kill-after=30s \"${OPENCODE_RUN_TIMEOUT_SECONDS}s\" \\\n"
shellcheck_new = (
" # shellcheck disable=SC2016\n"
" if timeout --kill-after=30s \"${OPENCODE_RUN_TIMEOUT_SECONDS}s\" \\\n"
)
if text.count(shellcheck_old) != 1:
raise SystemExit('unexpected nested-shell state')
path.write_text(text.replace(shellcheck_old, shellcheck_new), encoding='utf-8')
PY

- name: Diagnose every repository workflow after known corrections
shell: bash
run: |
set -o pipefail
"${RUNNER_TEMP}/actionlint" \
-color=false \
.github/workflows/*.yml \
2>&1 | tee "${RUNNER_TEMP}/actionlint-output.txt"
96 changes: 96 additions & 0 deletions .github/workflows/incremental-benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Incremental Mailbox Benchmark

on:
workflow_dispatch:
inputs:
messages:
description: Number of existing mailbox messages
required: false
default: "100000"
type: string
schedule:
- cron: "17 3 * * 1"

permissions:
contents: read

concurrency:
group: incremental-mailbox-benchmark-${{ github.repository }}
cancel-in-progress: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
PYTHONPATH: src

jobs:
benchmark:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Harden runner and block undeclared egress
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: block
disable-telemetry: true
allowed-endpoints: |
codeload.github.com:443
files.pythonhosted.org:443
github.com:443
objects.githubusercontent.com:443
pypi.org:443
release-assets.githubusercontent.com:443
results-receiver.actions.githubusercontent.com:443
*.actions.githubusercontent.com:443
*.blob.core.windows.net:443

- name: Check out the reviewed source without persisted credentials
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
cache: pip
cache-dependency-path: requirements/ci.lock

- name: Install the reviewed benchmark toolchain
run: python -m pip install --require-hashes -r requirements/ci.lock

- name: Run the isolated mailbox benchmark
env:
BENCHMARK_MESSAGES: ${{ inputs.messages || '100000' }}
run: |
set -euo pipefail
python benchmarks/incremental_mailbox.py \
--messages "$BENCHMARK_MESSAGES" \
--thread-size 10 \
--output incremental-benchmark.json

- name: Enforce parity and the mailbox-scale delta target
run: |
python - <<'PY'
import json
from pathlib import Path

result = json.loads(
Path("incremental-benchmark.json").read_text(encoding="utf-8")
)
incremental = result["incremental"]
full_rebuild = result["full_rebuild"]
assert incremental["projection_sha256"] == full_rebuild["projection_sha256"]
assert incremental["affected_message_count"] == 21
if result["message_count"] >= 100_001:
assert incremental["delta_apply_seconds"] < full_rebuild[
"full_rebuild_seconds"
]
PY

- name: Upload benchmark evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: incremental-mailbox-benchmark-${{ github.run_id }}-${{ github.run_attempt }}
path: incremental-benchmark.json
if-no-files-found: error
retention-days: 90
35 changes: 26 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ Operating guide for automated agents working on this repository.
`threadweave` implements the JWZ container model with RFC 5256 `REFERENCES`
threading semantics, RFC 5322 identification-field parsing, RFC 2047 encoded-word
decoding, RFC 5256 base-subject extraction, RFC 5051 Unicode casemap comparison,
optional RFC 5256 sent-date ordering, and RFC 5256 IMAP `THREAD` response
serialization. Its value is correctness: mail clients and ingestion systems rely
on threading being deterministic, standards-grounded, and impossible to hang on
malformed input. Treat changes to `threading.py`, `container.py`, `subject.py`,
`collation.py`, `dates.py`, `headers.py`, and `imap.py` as behavior-sensitive.
optional RFC 5256 sent-date ordering, RFC 5256 IMAP `THREAD` response
serialization, and atomic incremental mailbox indexing. Its value is correctness:
mail clients and ingestion systems rely on threading being deterministic,
standards-grounded, and impossible to hang on malformed input. Treat changes to
`threading.py`, `incremental.py`, `container.py`, `subject.py`, `collation.py`,
`dates.py`, `headers.py`, and `imap.py` as behavior-sensitive.

## Invariants that must not regress

Expand Down Expand Up @@ -55,6 +56,17 @@ malformed input. Treat changes to `threading.py`, `container.py`, `subject.py`,
dummy-root grouping after search projection, reject cyclic or shared graphs,
and leave the source `Container` tree unchanged. Rendering stays iterative,
and response framing accepts only CRLF or a caller-owned empty suffix.
13. **Incremental updates remain batch-equivalent and atomic.** Immutable caller
message keys—not sequence numbers—identify indexed records. Recompute every
affected old/new reference or subject component through `thread_messages`,
but do not pass unrelated components to the batch delegate. Validation or
recomputation failure must leave records, version, roots, and projections
unchanged. Snapshot state excludes arbitrary payloads and graph pointers.
14. **External identities follow RFC 8474.** `EMAILID` and `THREADID` use exact
1–255 character ObjectID grammar, are case-sensitive, and use disjoint
namespaces. Equal EMAILIDs require equal THREADIDs. Once a non-null value is
reported, replacement cannot remove or change it; merges and splits remain
explicit transitions rather than silent identity rewrites.

## Architecture and dependency rules

Expand All @@ -63,10 +75,15 @@ malformed input. Treat changes to `threading.py`, `container.py`, `subject.py`,
justified.
- Preserve the standalone package API and its use as a naruon module. The header
primitives originated in naruon; port behavioral fixes in both directions.
- Keep IMAP response serialization separate from the transport-neutral tree and
date layers so non-IMAP callers do not inherit protocol-specific state.
- Public behavior, compatibility aliases, and typing markers are release
contracts. Record changes in `CHANGELOG.md` and update user/research docs.
- Keep batch threading authoritative. The incremental layer owns caller keys,
component bookkeeping, deltas, and payload-free snapshots; it must delegate
every recomputed component to the existing batch engine rather than fork the
threading algorithm.
- Keep IMAP response serialization separate from the transport-neutral batch,
incremental, and date layers so non-IMAP callers do not inherit protocol state.
- Public behavior, compatibility aliases, typing markers, snapshot schemas, and
external-ID handoff are release contracts. Record changes in `CHANGELOG.md`
and update user/research docs.
- Unicode collation results depend on the Unicode Character Database bundled
with the supported Python runtime. Tests must cover stable RFC examples and
security-sensitive non-equivalences rather than version-specific new codepoints.
Expand Down
88 changes: 88 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# ThreadWeave Architecture

## Decision status

This document is the repository-level architecture decision record. `AGENTS.md` is
the canonical operating policy; this file explains component boundaries and data
flow for human reviewers and embedding services such as naruon.

## Architectural goal

ThreadWeave provides one standards-grounded threading kernel that works both as a
zero-runtime-dependency Python package and as a module inside a larger mail or
knowledge platform. Protocol, incremental state, automation, and release concerns
remain separate from the canonical batch algorithm.

## Modules

| Boundary | Responsibility | Must not own |
|---|---|---|
| `headers`, `encoded_words`, `subject`, `collation`, `dates` | RFC parsing, normalization, and comparison primitives | graph state, sockets, databases |
| `threading`, `container` | authoritative JWZ/RFC 5256 batch forest | mailbox sessions, persistence, IMAP framing |
| `incremental` | caller keys, atomic change sets, component indexes, deltas, payload-free snapshots | a second threading algorithm, database/network state |
| `imap` | non-mutating RFC 5256 `THREAD`/`UID THREAD` presentation | authentication, command parsing, mailbox storage |
| stdlib adapters | conversion from Python `email` messages | transport sessions or durable state |
| GitHub workflows and `scripts/ci` | review-first automation, NIM isolation, release evidence | runtime package behavior |

## Authoritative data flow

```text
caller message metadata
-> RFC normalization
-> canonical batch thread_messages(component)
-> Container forest
-> optional incremental component cache and ThreadDelta
-> optional IMAP response projection
```

The incremental layer over-approximates connectivity with normalized message IDs,
effective reference IDs, and optional RFC 5051 subject keys. Every affected
component is still evaluated by `thread_messages`; no copy of the threading rules
is maintained in incremental code.

## State and mutation policy

- `IndexedMessage.message_key` is caller-owned and immutable across revisions.
- Public reads, snapshots, and `apply` calls on one index are serialized by an
in-process reentrant lock. Two transactions targeting one version cannot both
commit; the later transaction observes the new version and fails explicitly.
- The process-local lock is not a distributed lock. Naruon or another host must
serialize durable writes across processes and persist the optimistic version.
- `apply` validates and computes on isolated transaction overlays, then commits once.
- Default-mode updates retain the existing state-map objects and publish only touched
record, position, token, component, EMAILID, and THREADID entries.
- Reverse connectivity buckets use copy-on-write mutation; RFC 8474 indexes retain
compact association/count state rather than one set object per message.
- RFC sent-date ordering may still scan the mailbox to derive global ranks and reject
effective sequence-number collisions; the default first-appearance mode does not.
- Complete root/projection views are lazy caches invalidated by a successful change.
- `roots` returns a defensive structural copy; payload references stay caller-owned.
- Snapshot schema version 1 contains structural metadata only, never payload objects
or graph pointers.
- RFC 8474 `EMAILID` and `THREADID` remain external identities. Structural merges
and splits are explicit transitions rather than silent identifier rewrites.

## Ordering and protocol policy

Implicit input positions may be used internally for RFC 5256 sent-date ordering,
but they are not mailbox sequence numbers and are cleared before public roots are
returned. IMAP serialization therefore fails closed unless callers supply valid
sequence numbers or UIDs.

## Scale evidence

The deterministic benchmark runs incremental and full-rebuild workers in separate
processes. It compares projection SHA-256 values and records initial-build time,
delta-application time, full-view materialization time, full-rebuild time, affected
message count, root count, delta retained/transient traced bytes, and peak RSS.
Scheduled evidence defaults to 100,000 existing messages. Focused performance
contracts additionally reject default-mode
small-delta implementations that iterate or replace unrelated state maps.

## Integration policy

Naruon and other services should own persistence, tenancy, authentication,
mailbox synchronization, distributed write serialization, and external stable-ID
policy. They pass immutable caller keys and `Message` metadata into ThreadWeave and
consume `ThreadDelta`, snapshots, or IMAP presentation output through public
interfaces.
Loading
Loading