/**
 * operator-subscription.ts — Phase C C6 agent-as-subscriber primitive.
 *
 * Driver-agent's hot loop subscribes to operator chunks; each new operator
 * chunk feeds the agent's next reasoning iteration as steer input. The
 * substrate IS the orchestration channel — no RPC, no event bus, no
 * "interject" API on the agent runtime: the agent simply reads chunks
 * where `actor_type='operator' AND seq > last_consumed_seq`.
 *
 * This module ships the PURE-FUNCTION pattern for both the driver
 * subscriber and other agent kinds (codex, lookouts). The devbox
 * bootstrap launcher that spawns these processes is C+1 follow-up
 * scope — the pure pattern is what's load-bearing for the seam.
 *
 * Architecture (per pass-7 plan body):
 *   - Driver in devbox container opens a shape subscription via
 *     `host.docker.internal:3112/v1/shape` (the host-cp sidecar from
 *     Phase B B1).
 *   - On each new operator chunk, the driver's NEXT reasoning iteration
 *     receives it as a prompt prefix.
 *   - Codex runs as a SEPARATE process with the same subscription
 *     pattern, but with a CONTENT FILTER (PR-draft regex).
 *   - Lookouts (Scout / PM / Brainstorm from PR #443) follow the
 *     same pattern with persona-specific trigger heuristics; emit
 *     SIGNALS (to plan_sidebar_signals) instead of chunks.
 *
 * Per OQ10 lean (a): driver + codex + lookouts all talk to host-cp's
 * /v1/shape via `host.docker.internal:3112`. No in-container PGlite
 * mirror; no LISTEN/NOTIFY direct.
 *
 * Source: docs/plans/olam-plan-chat-chunks-substrate/phase-c-tasks.md (C6)
 */
/**
 * Pure: derive the consumed-key for an operator pickup. The key is
 * `${messageId}:${seq}` — the (PK) shape Postgres uses for chunks.
 * Callers store these in a Set to dedup the operator stream.
 */
export function pickupConsumedKey(p) {
    return `${p.messageId}:${p.seq}`;
}
/**
 * Pure: filter a chunk stream to operator chunks NOT YET CONSUMED.
 * Used as the load-bearing primitive by the driver's hot loop —
 * each iteration call materialises the next batch of operator pickups
 * to feed into the next reasoning turn.
 *
 * `consumedKeys` is tracked by the caller (driver hot loop's local
 * state). On first call (cold start), pass an empty Set to receive
 * all historical operator chunks for the session.
 *
 * **History note** (Phase D D6 dedup fix): pre-fix this took a
 * `lastConsumedSeq: number` and filtered with `c.seq <= lastConsumedSeq`.
 * That broke in production because each SPA-dispatched operator chunk is
 * a NEW message_id with `seq=0` (the chunks substrate scopes seq per
 * message_id, not per session). Under that model, the second operator
 * dispatch with `seq=0` was rejected because `0 <= 0`, silently dropping
 * every operator chunk after the first one. Switched to message_id+seq
 * Set dedup which matches the substrate's PK shape exactly.
 */
export function filterOperatorPickups(chunks, consumedKeys) {
    const out = [];
    for (const c of chunks) {
        if (c.actor_type !== 'operator')
            continue;
        const key = `${c.message_id}:${c.seq}`;
        if (consumedKeys.has(key))
            continue;
        out.push({
            messageId: c.message_id,
            seq: c.seq,
            content: c.chunk,
            createdAt: c.created_at,
        });
    }
    return out;
}
/**
 * Pure: compose a prompt-prefix string from operator pickups. Each
 * pickup becomes a `[operator]: ...` line; multiple pickups stack
 * in seq order. The driver's next-iteration prompt prepends this
 * prefix so the agent sees the operator's interjects without
 * needing a custom message-injection hook in the SDK.
 *
 * Returns null when there are no pickups (skip the prefix; vanilla
 * iteration).
 */
export function pickupsToPromptPrefix(pickups) {
    if (pickups.length === 0)
        return null;
    const sorted = [...pickups].sort((a, b) => a.seq - b.seq);
    const lines = sorted.map((p) => `[operator]: ${p.content}`);
    return lines.join('\n');
}
/**
 * Pure: derive the next-iteration `consumedKeys` Set after processing
 * a batch of pickups. The driver hot loop calls this after each
 * iteration to update its local state. Returns a NEW Set (caller
 * keeps the prior set immutable; cheap on the small operator volumes
 * a single session produces).
 */
export function advanceConsumedKeys(pickups, current) {
    const next = new Set(current);
    for (const p of pickups) {
        next.add(pickupConsumedKey(p));
    }
    return next;
}
/**
 * Pure: detect PR-draft patterns in driver chunks for codex pickup
 * (Phase C C6 codex-runner trigger).
 *
 * Codex runs as a parallel process subscribing to driver chunks; on
 * detecting "PR draft" / "draft PR" / "opening PR" / "PR title"
 * patterns, codex generates an approval/rejection chunk. This pure
 * helper isolates the trigger heuristic for unit-test coverage
 * separate from the codex process's stateful execution loop.
 */
export function detectCodexTrigger(chunk) {
    if (chunk.actor_type !== 'agent')
        return { matched: false };
    const text = chunk.chunk;
    const patterns = [
        /\bdraft\s+pr\b/i,
        /\bpr\s+draft\b/i,
        /\bpr\s+title\b/i,
        /\bopening\s+pr\b/i,
        /\bcreate\s+(a\s+)?pull\s+request\b/i,
    ];
    for (const re of patterns) {
        const m = re.exec(text);
        if (m !== null) {
            return { matched: true, matchedPattern: m[0] };
        }
    }
    return { matched: false };
}
/**
 * Pure: generate a codex approval/rejection chunk content for a
 * matched PR-draft trigger. The codex runner uses this to compose
 * the chunk it writes back via host-cp POST /v1/chunks (with
 * actor_type='codex').
 *
 * The actual reasoning runs through Claude Agent SDK with a codex
 * system prompt; this helper covers the deterministic boilerplate
 * (decision + reasoning summary). Real codex output flows through
 * streamSdkToChunks (Phase C C1's adapter).
 */
export function codexApprovalDraft(opts) {
    const verdict = opts.approved ? 'APPROVED' : 'BLOCKED';
    return {
        content: [
            `[codex] ${verdict}`,
            `Trigger: chunk ${opts.triggerChunk.message_id}#${opts.triggerChunk.seq}`,
            `Reason: ${opts.reason}`,
        ].join('\n'),
        actorType: 'codex',
    };
}
/**
 * Pure: minimal v1 trigger heuristic for Scout (lookout persona). Fires
 * on agent chunks containing "TODO" or "FIXME" or "XXX" markers as a
 * conservative starting point. Real Scout uses LLM-based judgment;
 * this v1 heuristic is the deterministic fallback for tests + the
 * pattern other personas extend.
 */
export const scoutTrigger = (chunk) => {
    if (chunk.actor_type !== 'agent')
        return { triggered: false };
    const markers = /\b(TODO|FIXME|XXX|HACK)\b/.exec(chunk.chunk);
    if (markers === null)
        return { triggered: false };
    return {
        triggered: true,
        urgency: 'p2',
        reason: `marker ${markers[0]} surfaced`,
        content: `Scout noted: ${markers[0]} in chunk ${chunk.message_id}#${chunk.seq}.`,
    };
};
//# sourceMappingURL=operator-subscription.js.map