Skip to content

feat(t3team): durable task journal + transcript search over tool activity - #206

Merged
johnnyelwailer merged 2 commits into
mainfrom
nexi/task-journal
Sep 11, 2026
Merged

johnnyelwailer merged 2 commits into
mainfrom
nexi/task-journal

Conversation

@johnnyelwailer

Copy link
Copy Markdown
Owner

Two changes that share one idea: the things an agent needs to remember must live outside its context window.

Both are grounded in one real 31-hour nexplore orchestration run (thread fbdb583b): 128 turns, 22 compactions, 12 hard mid-conversation truncations, 10 session restarts.

1. Search the whole transcript, newest first (6e51a22)

t3team.thread.search scanned only messages. A thread detail also carries activities — every command and its output, every file read, every tool call. In that run: 1319 searchable messages against 1250 invisible activities, and the invisible half is where the evidence lives.

  • messages and activities interleaved chronologically, so a match ranks by when it happened, not which stream it came from
  • order now defaults to recent. Matches are capped, so transcript order returned the oldest — one real call reported totalMatches: 70, returnedMatches: 5, the five earliest of seventy
  • offset paging, hasMore, and a hint naming the exact next offset
  • scope narrows to one stream; role filters a message role or an activity kind
  • a multi-word query matching nothing verbatim retries requiring every term (matchMode); terms under 3 chars are excluded so "a b" cannot match inside "bash"
  • activity matches report activity_id, not message_id — an activity id can never be resolved by thread.read_message, and offering it under that key invites a lookup that always fails

The messages-only helper and search_source's divergent index result shape are removed rather than kept alongside. Both tools return the same shape now.

2. Durable per-thread task journal (b77b219)

That run made zero task records — no such tool existed — and 75 t3team_children polls, because the orchestrator had no durable plan and re-derived its own state by interrogating its children.

TaskRecord contract, migration 056 (thread_task_records, indexed on (thread_id, position)), a persistence service, and two broker tools. t3team.task.write replaces the whole list rather than patching by id — deliberate, because the primary consumer is a weak local model that handles re-sending a list far more reliably than addressing individual records. note is part of the record so a failure keeps its reason.

Review

Ran a refute-first adversarial pass. It found 4 real defects in the first draft of the search work, all fixed here: stream-ordered instead of time-ordered ranking, unreachable arguments (schemas never declared offset/scope/order), activity ids masquerading as message ids, and 1–2 char terms driving the fallback.

One finding was not fixable here and is worth knowing: the nexplore pack records only the first 500 characters of a tool result, so activity search finds which command ran and its first 500 chars, not full output. Both tool descriptions now say so. A distro-side change raises that budget separately.

Verification

t3team-toolBrokerBindingSearch{Thread,Source}   25 passed
t3team-toolBrokerBindingTaskJournal             11 passed
t3team-ThreadTaskRecords                         5 passed
t3team-toolkit (MCP parity)                      8 passed
t3teamToolCatalog                                5 passed
                                    total 49 + 5 passed, 0 failed

Additive guard: no errors from these files; one warning (t3team-ThreadTaskRecords.ts, 161 lines, under the 200 cap). Remaining guard/lint/typecheck failures reproduce at clean fork/main.

Also fixes three catalog tests that were already red on main: t3team.thread.ask_user is status: "implemented" but was missing from both expected lists and the catalog doc.

Not included: web UI, and the pack-side capability policy that would auto-enable the journal per provider.

🤖 Generated with Claude Code

Phil J and others added 2 commits September 11, 2026 21:51
`t3team.thread.search` scanned only a thread's `messages`. A thread detail
also carries `activities` — the tool lifecycle: every command and its
output, every file read, every tool call. In a real 31-hour run that was
1319 searchable messages against 1250 invisible activities, and the
invisible half is where the evidence lives: what a test printed, which
file held a symbol, why a command failed.

One engine now serves both `thread.search` and `thread.search_source`:

- messages and activities are normalized into one stream and interleaved
  chronologically, so a match is ranked by when it happened rather than
  which stream it came from
- `order` defaults to `recent`. Matches are capped, so filling them in
  transcript order returned the OLDEST; one real call reported
  `totalMatches: 70, returnedMatches: 5` — the five earliest of seventy
- `offset` pages, `hasMore` reports whether more remain, and the hint
  names the exact next offset
- `scope` narrows to one stream; `role` filters a message role or an
  activity kind
- a multi-word query that matches nothing verbatim retries requiring
  every term, reported as `matchMode`. Terms under three characters are
  excluded so "a b" cannot match inside "bash"

An activity id cannot be resolved by `thread.read_message`, so activity
matches report `activity_id` rather than presenting an id under a key
that invites a lookup which always fails.

The messages-only helper and `search_source`'s divergent `index` result
shape are removed rather than kept alongside; both tools now return the
same shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An agent's plan lived only in its context window, so compaction erased
it. Measured over a 31-hour nexplore orchestration run: 128 turns, 22
compactions, 12 hard truncations, zero task records — and 75
`t3team_children` polls, because the orchestrator had no durable plan and
re-derived its own state by interrogating its children instead.

Adds `TaskRecord` (contract), migration 056 with `thread_task_records`
indexed on `(thread_id, position)`, a persistence service, and two broker
tools on the MCP surface:

- `t3team.task.write` replaces the whole list for the thread. Whole-list
  replace rather than id-based patching is deliberate: the primary
  consumer is a weak local model, which handles re-sending a list far
  more reliably than addressing individual records.
- `t3team.task.list` reads it back.

`note` is part of the record so a failure keeps its reason — the detail
compaction otherwise destroys.

Also declares the thread-search arguments added in the previous commit
(`offset`, `scope`, `order`, and a `role` that accepts an activity kind)
on both the MCP tool schema and the tool catalog. Without that the
catalog's `additionalProperties: false` rejected them, so the paging the
search result advertises was unreachable through the published contract.
Both search tool descriptions now also state that an activity records
only the first 500 characters of a tool result.

`t3team.thread.ask_user` is added to the catalog test's expected lists
and to the catalog doc. It is `status: "implemented"` but was missing
from both, which left three catalog tests failing before this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 11, 2026
@johnnyelwailer
johnnyelwailer merged commit 57ce7e5 into main Sep 11, 2026
9 of 17 checks passed
@johnnyelwailer
johnnyelwailer deleted the nexi/task-journal branch September 11, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant