Skip to content

feat(proactive): runFollowUpBatch + decidePostingPolicy + block-counter interface#82

Merged
khaliqgant merged 2 commits intomainfrom
feat/proactive-run-follow-up-batch
May 5, 2026
Merged

feat(proactive): runFollowUpBatch + decidePostingPolicy + block-counter interface#82
khaliqgant merged 2 commits intomainfrom
feat/proactive-run-follow-up-batch

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

Three primitives extracted from sage so future AA proactive consumers inherit the same safety properties without reimplementing them. Spec: sage docs/specs/proactive-batch-runner.md §5.1.

  • runFollowUpBatch<C, R> — bounded-parallel batch runner over candidates. Layered on boundedParallel from @agent-assistant/coordination ^0.4.25 (PR feat(coordination): boundedParallel batch runner with timeout + abort #81). Adds per-run cap + overflow tracking + structured stats.
  • decidePostingPolicy + readFailOpenFromEnv — fail-open posting verification policy extracted from sage. Env var renames SAGE_PROACTIVE_FAIL_OPEN_VERIFICATIONAA_PROACTIVE_FAIL_OPEN_VERIFICATION with deprecation alias.
  • ProactiveBlockCounter / ProactiveBlockCounterReader interfaces — minimal contract for /api/proactive/health operational counters. Adapter implementations stay in the consuming app.

Surface

// runFollowUpBatch
export interface RunFollowUpBatchInput<C, R> {
  candidates: ReadonlyArray<C>;
  runOne: (candidate: C, signal: AbortSignal) => Promise<R>;
  concurrency?: number;             // default 5
  perCandidateTimeoutMs?: number;   // default 8000
  maxCandidatesPerRun?: number;     // default Infinity
  signal?: AbortSignal;
}
export interface RunFollowUpBatchResult<C, R> {
  results: BoundedParallelResult<R>[];   // per-item, in input order
  processed: ReadonlyArray<C>;           // post-cap prefix
  dropped: ReadonlyArray<C>;             // overflow tail (caller handles)
  stats: { processed; fulfilled; rejected; timedOut; aborted; dropped };
}

// decidePostingPolicy
export type VerifyDraftOutcome<E = unknown> =
  | { kind: 'verified'; text; evidence: E[] }
  | { kind: 'bridge_unavailable'; draftedText }
  | { kind: 'verification_degraded'; draftedText; reason }
  | { kind: 'no_supporting_evidence' };
export type PolicyDecision<E = unknown> =
  | { action: 'post'; text; evidence: E[] }
  | { action: 'post_unverified'; text; reason }
  | { action: 'block'; reason };

Design notes

  • runFollowUpBatch is intentionally thin. The per-candidate pipeline (evidence → LLM verify → policy → post → persist) stays in caller code via runOne. Each AA surface owns its pipeline; AA owns the orchestration safety properties (concurrency, timeout, cap, abort).
  • Generic over evidence type. VerifyDraftOutcome<E> means consumers aren't locked into sage's ClaimEvidence. Sage will pass <ClaimEvidence>; another surface could pass its own.
  • Conservative fail-open read. Setting EITHER AA_* or legacy SAGE_* env var to "false" wins. An operator running the legacy var to intentionally fail-closed during an incident must NOT silently start failing-open after the migration.
  • EvidenceCollector deliberately stays in sage. Its scoreClosure has hardcoded sage-domain rules (pr-merge → close, affirmative-reply + reaction → close) that don't generalize across AA consumers. Each surface picks its own scoring policy.

Test plan

  • cd packages/proactive && npm test — 141/141 pass (118 existing + 23 new)
    • 8 cases for runFollowUpBatch: empty, no-cap, FIFO cap, cap-of-zero, cap-larger-than-input, rejected/timedOut classified, external abort threading, input-order preservation
    • 15 cases for decidePostingPolicy: every outcome × failOpen branch, idempotent prefix, AA env var, legacy SAGE env var, conservative-read precedence, non-"false" values don't trigger fail-closed
  • npm run build clean across the build chain (surfaces → coordination → proactive)
  • All four primitives + types exported in dist/index.d.ts

Out of scope (follow-up)

  • Version bump — handled by separate chore(release) commit per the existing convention
  • Sage adoption — checkFollowUps collapses ~200 LOC of sequential loop into ~30 LOC of glue calling runFollowUpBatch; staged at sage workflows/proactive-batch-runner/04-sage-adopt.ts. Sage drops its local verification-policy.ts, runCandidatePool, runCandidateWithTimeout, and the ProactiveBlockCounter interface (re-imports from AA)
  • AA feat(surfaces): add SlackThreadGate for agent-in-Slack gating #3createSupermemoryClient move from sage to @agent-assistant/memory. Independent of this PR

Sequence status

PR Repo Status
sage #200 sage MERGED — in-place runner + spec + workflow scaffolds
AA #81 agent-assistant MERGEDboundedParallel published as @agent-assistant/coordination@0.4.25
This PR agent-assistant OPEN — depends on coordination@0.4.25
AA #3 agent-assistant Not started — createSupermemoryClient extraction
sage #4 sage Not started — adoption; deletes in-place runner + verification-policy

🤖 Generated with Claude Code

…er interface

Three primitives extracted from sage so future AA proactive consumers
inherit the same safety properties without reimplementing them.

1. `runFollowUpBatch<C, R>` — bounded-parallel batch runner over candidates.
   Layered on `boundedParallel` from @agent-assistant/coordination ^0.4.25.
   Adds a per-run cap with overflow tracking (caller gets `dropped` for
   provenance — sage uses this to seed `deferredSessionIds` so dropped
   signal-inbox candidates aren't silently cleared) and aggregated
   structured stats (processed / fulfilled / rejected / timedOut /
   aborted / dropped) for /health-style surfaces. Deliberately thin:
   the per-candidate pipeline (evidence collection, LLM verification,
   policy gate, post, persistence) stays in caller code via the `runOne`
   callback — keeping each surface's pipeline free to compose without
   dragging the whole follow-up loop into AA.

2. `decidePostingPolicy` + `readFailOpenFromEnv` — fail-open posting
   verification policy. Direct extraction from sage's
   `verification-policy.ts` (originally PR #186). The kill-switch env
   var renames from `SAGE_PROACTIVE_FAIL_OPEN_VERIFICATION` to
   `AA_PROACTIVE_FAIL_OPEN_VERIFICATION`; the legacy SAGE-prefixed name
   is read as a deprecation alias for one minor cycle so the rename can
   land without flipping prod behavior on the dependency bump.
   Conservative read: setting EITHER env var to "false" wins. Generic
   over evidence type (`<E>` parameter) so callers aren't locked into
   sage's `ClaimEvidence` shape.

3. `ProactiveBlockCounter` / `ProactiveBlockCounterReader` interfaces —
   minimal contract for the best-effort operational counters that power
   /api/proactive/health. Adapter implementations stay in the consuming
   app (sage's KV adapter, future Redis or D1 adapters elsewhere). Errors
   thrown by adapters are documented as caller-swallowed so a flaky
   counter never blocks a proactive decision.

Tests:
  - 8 cases for runFollowUpBatch: empty input, no-cap full batch, FIFO
    cap with dropped tail, cap-of-zero, cap-larger-than-input,
    rejected/timedOut counted separately, external abort threading,
    input-order preservation under parallel completion.
  - 15 cases for decidePostingPolicy: every outcome × failOpen branch,
    idempotent prefix application, AA env var, legacy SAGE env var,
    conservative-read precedence, non-"false" values don't trigger
    fail-closed.

Existing 118 proactive tests still pass; 23 new = 141/141. tsc clean
across the build chain (surfaces → coordination → proactive).

Sage adoption is the next PR in the sequence. EvidenceCollector
intentionally stays in sage — its `scoreClosure` rules are domain-
specific (pr-merge → close, affirmative-reply + reaction → close, etc.)
and don't generalize across AA consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cda80f35-63df-411e-9a65-fe73046f6b68

📥 Commits

Reviewing files that changed from the base of the PR and between c75a79c and af58b0f.

📒 Files selected for processing (3)
  • packages/proactive/src/decide-posting-policy.test.ts
  • packages/proactive/src/decide-posting-policy.ts
  • packages/proactive/src/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/proactive/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/proactive/src/decide-posting-policy.test.ts
  • packages/proactive/src/decide-posting-policy.ts

📝 Walkthrough

Walkthrough

Adds a proactive posting policy module, a FIFO-capped parallel batch runner, operational block-counter interfaces, tests for both features, a coordination dependency, and re-exports the new APIs from the package entry point.

Changes

Proactive Posting Policy & Batch Infrastructure

Layer / File(s) Summary
Dependency
packages/proactive/package.json
Adds @agent-assistant/coordination@^0.4.25.
Data Shape / Types
packages/proactive/src/decide-posting-policy.ts, packages/proactive/src/proactive-block-counter.ts, packages/proactive/src/run-follow-up-batch.ts
Adds VerifyDraftOutcome, PolicyDecision, PolicyEnv, EnvSource, UNVERIFIED_PREFIX; declares ProactiveBlockCounter, ProactiveBlockCounterReader; defines RunFollowUpBatchInput, RunFollowUpBatchStats, RunFollowUpBatchResult shapes.
Core Implementation
packages/proactive/src/decide-posting-policy.ts, packages/proactive/src/run-follow-up-batch.ts
Implements decidePostingPolicy, applyUnverifiedPrefix, readFailOpenFromEnv (worker-safe env reader); implements runFollowUpBatch with FIFO maxCandidatesPerRun, bounded parallel execution, per-item timeout, external AbortSignal propagation, ordered results, and aggregated stats.
Public API / Wiring
packages/proactive/src/index.ts
Re-exports new policy functions/types, batch runner and its types, and block-counter interfaces.
Tests
packages/proactive/src/decide-posting-policy.test.ts, packages/proactive/src/run-follow-up-batch.test.ts
Adds comprehensive tests for policy mapping and fail-open behavior, env var precedence, unverified prefix idempotency, batch FIFO capping, concurrency/timeout behavior, abort propagation, and result ordering.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant PolicyModule as Posting Policy Module
    participant BatchRunner as Batch Runner
    participant Worker as Per-Item Worker
    participant Counter as Block Counter

    Caller->>PolicyModule: provide VerifyDraftOutcome + env.failOpen
    PolicyModule->>PolicyModule: decidePostingPolicy / applyUnverifiedPrefix
    PolicyModule-->>Caller: PolicyDecision (post / post_unverified / block)

    Caller->>BatchRunner: runFollowUpBatch(candidates, options)
    BatchRunner->>BatchRunner: slice FIFO prefix -> processed, dropped
    BatchRunner->>BatchRunner: boundedParallel(processed, concurrency, perItemTimeout, signal)
    par Parallel Execution
        BatchRunner->>Worker: run candidate[i] (with per-item timeout + signal)
        Worker->>Counter: increment(event) (fire-and-forget)
        Worker-->>BatchRunner: result (fulfilled | rejected | timedOut | aborted)
    end
    BatchRunner->>BatchRunner: aggregate stats, preserve input order
    BatchRunner-->>Caller: RunFollowUpBatchResult(results, processed, dropped, stats)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • AgentWorkforce/agent-assistant#81: Adds/uses a runFollowUpBatch relying on the coordination package’s boundedParallel primitive and adds the same coordination dependency.

Poem

🐰
I hop through tests and tidy types,
Cap the queue and mind the bytes,
When bridges wobble, I prefix true,
Count the hops the whole day through,
A proactive rabbit, here for you!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly reflects the three main primitives introduced: runFollowUpBatch, decidePostingPolicy, and the block-counter interfaces.
Description check ✅ Passed The description provides comprehensive context: purpose (extracting from sage), detailed API surface, design rationale, test coverage, and out-of-scope follow-up work—all clearly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/proactive-run-follow-up-batch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c75a79cd8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* legacy env var to intentionally fail-closed during an incident must not
* silently start failing-open after the migration.
*/
export function readFailOpenFromEnv(env: NodeJS.ProcessEnv = process.env): boolean {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid Node-only default in readFailOpenFromEnv

Using process.env as the default and NodeJS.ProcessEnv in the public signature makes this helper unsafe for Cloudflare Worker consumers: in Worker runtime (without Node compatibility), calling readFailOpenFromEnv() throws because process is undefined, and Worker-only TypeScript projects can also fail on the emitted NodeJS type in declarations. This package is shared across non-Node environments, so the function should accept a plain env map (or require an explicit env argument) instead of hard-binding to Node globals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/proactive/src/decide-posting-policy.ts`:
- Line 53: The exported function readFailOpenFromEnv currently types its
parameter as NodeJS.ProcessEnv and defaults to process.env, which leaks Node
typings and crashes in non-Node runtimes; change the parameter type to a generic
env map (e.g., Record<string, string | undefined> or
Partial<Record<string,string>>) and make the default safe by using a runtime
guard (check typeof process !== "undefined" && process.env ? process.env : {})
so the function no longer references the process global or forces Node types on
consumers; update any internal accesses in readFailOpenFromEnv to work with the
new env map type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3edfe47e-9fb7-4dfe-b08f-8f49947c9483

📥 Commits

Reviewing files that changed from the base of the PR and between 941a840 and c75a79c.

📒 Files selected for processing (7)
  • packages/proactive/package.json
  • packages/proactive/src/decide-posting-policy.test.ts
  • packages/proactive/src/decide-posting-policy.ts
  • packages/proactive/src/index.ts
  • packages/proactive/src/proactive-block-counter.ts
  • packages/proactive/src/run-follow-up-batch.test.ts
  • packages/proactive/src/run-follow-up-batch.ts

Comment thread packages/proactive/src/decide-posting-policy.ts Outdated
…review)

Both reviewers flagged the same P1: `readFailOpenFromEnv(env: NodeJS.ProcessEnv = process.env)`
made the helper unsafe for Cloudflare Worker consumers without
`nodejs_compat`. Two failure modes:
  1. Runtime: calling `readFailOpenFromEnv()` throws ReferenceError
     because `process` is undefined.
  2. Build: `NodeJS.ProcessEnv` in the emitted .d.ts means Worker-only
     TypeScript projects fail to consume the package types.

Fix:
  - Public signature now takes `EnvSource = Readonly<Record<string,
    string | undefined>>` — a structural shape that matches `process.env`
    at runtime so existing Node callers pass it without changes, but
    doesn't drag the `NodeJS` global type into the package's surface.
  - No-arg default falls back to `globalThis.process?.env ?? {}` via a
    private `defaultEnvSource()` helper. On hosts where `process` is
    undefined (pure Workers, browsers) the empty map cleanly falls
    through to the policy default (fail-open) instead of throwing.
  - Export `EnvSource` from index.ts so callers can use the type
    explicitly when wiring their own env source.

New test case verifies the no-arg path doesn't throw when
`globalThis.process` is deleted (simulating a Worker without
nodejs_compat). 142/142 proactive tests pass; build clean; dist .d.ts
no longer references `NodeJS` in public surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@khaliqgant khaliqgant merged commit 2b557a4 into main May 5, 2026
2 checks passed
@khaliqgant khaliqgant deleted the feat/proactive-run-follow-up-batch branch May 5, 2026 10:53
khaliqgant pushed a commit that referenced this pull request May 5, 2026
`npm ci` failed on PR #83 because adding `supermemory@^4.21.1` to
packages/memory/package.json without refreshing package-lock.json puts
the lockfile out of sync. Unlike PR #81 (no new deps) and PR #82
(internal workspace dep), this PR is the first in the series to add a
real npm-registry dependency, so the lock must update.

Diff is in two parts, both unavoidable:
  1. New `node_modules/supermemory@4.21.1` entry — what this PR needs.
  2. Workspace package versions sync from 0.4.23 → 0.4.25 — the recent
     release commits (2740310, 941a840) bumped the workspace
     package.json files but never refreshed the lockfile. `npm ci`
     refuses any out-of-sync state, so the sync has to land here even
     though it's incidental to this feature.

Verified locally with the same install path CI runs:
  - rm -rf node_modules
  - npm install (regenerates lock)
  - cd packages/memory && npm test (84/84 pass)
  - npm run build (clean)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant