Chat pre-turn parallelization + real tool duration_ms — implementation plan
Status: Approved
Author: Claude (NOS-212 orchestrator)
Date: 2026-07-23
Repo: north-os
Related: design spec; Linear NOS-212
Step 1 — pre-turn parallelization (packages/chat-runtime/src/run.ts)
- Extract the memory-load block (~:2073) into
const loadMemoryBlock = async (): Promise<string>— keeps its internal try/catch and the fire-and-forgetrecordMemoryUse. - Extract the ledger-load block (~:2107) and the digest-load block (~:2204) into one
loadLedgerThenDigestsclosure returning{ ledgerBlock, ledgerEntries, ledgerCoverageAvailable, attachmentOrigins, contextDigests }. Digest keeps its!isContextCleanerDisabled() && ledgerCoverageAvailablegate and its own try/catch. const [memoryBlock, ledgerResult] = await Promise.all([...]); downstream code (normalizeThreadDocumentCoverage,ledgerCoverage,threadHot, graph config) reads from the result — output-identical modulo the documented digest-freshness window.- New deterministic concurrency test in
run.test.ts: hold the memory load unresolved via a deferred promise, assert the ledger load has started; hold the ledger unresolved, assert the digest load has not started; resolve both, assert the graph config carries the loaded blocks.
Step 2 — ALS tool timing (packages/agent-runtime)
- New
src/audit/tool-timing.ts:AsyncLocalStorage<ToolTimingScope>whereToolTimingScope = { startedAt: number; pendingAudit: PendingAuditTee | null }. ExportsinvokeWithToolTiming<T>(toolName: string, fn: () => Promise<T>): Promise<T>(emits histogram always + deferred product_event on settle; rethrowsGraphInterruptwithout emitting) and a scope accessor forrecordToolAudit. src/audit/tool-audit.ts: inside an ALS scope with no explicitdurationMs→ write the audit row, stashpendingAudit(last wins), skip the immediate tee. Outside a scope or with explicitdurationMs→ emit immediately (mcp-server path), plus a histogram sample when an explicit duration was given. Rewrite the staleagentId/durationMsdoc comments.- Wrap the seven invoke sites:
agents/legal-assistant/graph.ts:1126agents/screen-companion/graph.ts:102agents/desktop-ack/graph.ts:84agents/proactive-glance/graph.ts:77agents/skill-author/graph.ts:124agents/glance-analyst/graph.ts(runToolCalls)workflows/executor.ts:138
- Export
invokeWithToolTimingfrom the package index only if a consumer outside agent-runtime needs it (none known — keep internal).
Step 3 — tests
src/audit/tool-timing.test.ts: full-span emit (audit early, keep working, duration covers the whole invoke); throw → histogram sample withoutcome: "error"and no product_event when nothing was audited;GraphInterrupt→ rethrown, nothing emitted; nesting → innermost scope wins; exactly one event per invocation when a tool audits twice (last outcome wins).src/audit/tool-audit.test.ts: mock@workspace/analytics; assert (a) explicitdurationMspasses through and emits immediately with a histogram sample, (b) wrapped call defers — no immediate emit, wrapper emits real elapsed > 0, (c) unwrapped call emits 0 immediately with no histogram sample. Histogram assertions go through the observability registry (seepackages/observability/src/metrics.test.tspatterns) with a registry reset between tests.- Run
bun --filter=@workspace/agent-runtime testandbun --filter=@workspace/chat-runtime test.
Step 4 — gates + proof
bun run lint+bun run typecheckfrom root.- Scratchpad probe (never committed): time the three pre-turn loads serial vs
Promise.allagainst the dev DB (read-only), 5 runs each, plus a 10-concurrent variant (pool-pressure check — report total latency under concurrency, not just sequential medians) — before/after table for the PR body. - Tool-duration proof: invoke real read-only tools wrapped in
invokeWithToolTimingagainst the HSE org (timing-probe pattern) sorecordToolAuditemits live rows; then run the p50/p95 SQL from the spec (with thecreated_atfilter — historical rows are all 0) againstproduct_event— rows must show real durations.
Worker split
- One omp backend worker: Steps 1–3 (single package-boundary-respecting change set, no file overlap with other NOS issues this wave; NOS-210/211 rebase cleanly — this branch touches only run.ts pre-turn block + agent-runtime audit/graph invoke lines).
- Orchestrator: docs, probes, gates, PR.