Files
PaperClipAI/tests/runner-e2e/everyday-observations.ts
T
DottaandClaude Opus 5.5 d87f6fa4c2 test(runner-e2e): add staging-soak reliability cases to everyday workflows (#19455)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The paid runner E2E suite (`tests/runner-e2e`) drives the real
browser, server, native runner, provider and Daytona
> - A manual staging soak found about nine reliability bugs (S47–S55,
H2, U9). Each fix got unit or integration tests
> - The end-to-end journeys that exposed the bugs existed only as ad-hoc
browser scripts, so a regression could return without a signal
> - These journeys are expensive, so they must not run per PR or in
`--all`. They must run on demand before a release
> - This pull request adds the journeys to the existing
`everyday-workflows` suite as an explicit `reliability` group
> - The benefit is one command that runs the whole reliability set
before a release or after a risky runner change

## Linked Issues or Issue Description

**What is the problem?**
The staging-soak scenarios that found S47–S55, H2 and U9 are not
repeatable. They were ad-hoc browser scripts on staging.

**Why does it matter?**
Without a repeatable end-to-end case, a reliability regression (stalled
follow-ups, duplicate execution, dead Retry, hangs after silent
commands) can return without a signal.

**What is the proposed solution?**
Add the scenarios to the paid `everyday-workflows` suite as
explicit-only cases with independent graders and a selectable
`reliability` group.

Refs #19370, #19355, #19354, #19407, #19419, #19394, #19421, #19431 (the
related fixes).

## What Changed

- `everyday-reliability-cases.ts`: seven new cases
(`interrupt-redirect`, `reassign-midrun`, `three-turn-revise`,
`long-silent-command`, `tree-pause-release`, `queue-steer`,
`queued-across-restart`), their soak signatures, prompts, bounds and the
explicit matrix.
- `everyday-reliability-flow.ts`: browser and public-API drivers for
each journey. A shared watcher records `executionBlocker`,
`interruption` and execution projections while it waits. It uses an
offered Retry at most once (never while an automatic retry is
scheduled).
- `everyday-reliability.ts`: pure graders. They encode the product rule:
interrupted work resumes automatically or offers a Retry that works on
first use. A send-a-message blocker, a dead or repeated Retry, a silent
stall, or duplicate execution fails.
- `types.ts` / `selectors.ts`: new optional `explicitGroup` on a task.
Such a task is selected only by its group, `--case`, or an exact `--id`.
`--all`, the plain suite and generic selectors skip it. `--list` still
shows it.
- `catalog.ts`: the suite gets the reliability tasks, the released
Grok/Kimi/Pi facades (only for `long-silent-command`), a per-turn
Daytona override for `three-turn-revise`, the `reliability` and `slow`
groups, definition version 11, and a computed `expectedMatrixSize` (52 →
62).
- `everyday-flow.ts`: a narrow hook calls the scenario driver after task
creation. Riley is created for the reassign and tree cases. The
reliability checks are added after settlement.
- `everyday-observations.ts`: an allowed interrupted run may end
`operator_interrupted` or `issue_reassigned`. Lifecycle checks accept a
declared reassigned parent owner.
- `live-fixtures.ts`: reliability cells get the existing 1,000-cent
company and lead-agent hard stop.
- Tests: new `everyday-reliability.test.ts` (selection exclusion,
matrix, credentials, and calibrated pass/fail cases for every grader).
Updated counts in `catalog.test.ts`, `everyday.test.ts` and
`installed-cli.test.ts`.
- Docs: new "Staging-soak reliability group" section in
`EVERYDAY-WORKFLOWS.md` (cases, signatures, run commands, cadence,
coverage boundary) and a link from `doc/evals.md`.

`everyday-flow.ts` was already over 1,000 lines. This PR adds only about
20 lines of wiring there. The scenario logic is in the new modules.

Matrix (10 cells, all Daytona, single attempt): one `runner-codex`
(gpt-5.6-sol) cell per case, plus `runner-acpx-grok`, `runner-acpx-kimi`
and `runner-acpx-pi` for `long-silent-command`.

Run command:

```sh
pnpm test:e2e:runner -- --suite everyday-workflows --group reliability --max-parallel 2
```

## Verification

- `pnpm test:e2e:runner:typecheck` passes.
- `node node_modules/vitest/vitest.mjs run --config
tests/runner-e2e/vitest.config.ts everyday catalog installed-cli
selectors support.test`: 15 files, 362 tests pass.
- The full runner-e2e Vitest run has the same 14 failures with and
without this change (`controller-outage`, `operating-receipts`,
`pi-controls-evidence`, `pi-file-evidence`, `workflow-security`). These
fail on the base commit in this local environment too.
- `pnpm test:e2e:runner -- --list --suite everyday-workflows --group
reliability` lists exactly the ten cells.
- `--all`, `--suite everyday-workflows` (still 52 cells),
profile/environment selectors and the daily/weekly scheduled selections
contain no reliability cell. A test asserts this.
- Not run: the paid cells. They need provider and Daytona credentials.
The first paid run of the group is the acceptance step.

## Risks

- The drivers have not run against a live stack. A first paid run can
expose harness timing or selector issues (for example, how fast the
queued Interrupt control appears). The failure then names the stalled
step and keeps `reliability-observations.json`.
- `long-silent-command` does not inject a runner connection loss. The
named-cause contract is checked only if a loss happens naturally.
- The Grok/Kimi/Pi facades run as ordinary released profiles. If one
returns to pending qualification, it drops out of the group and the
matrix size follows.
- No product code changes. Existing everyday cells keep their selection
and checks. The suite definition hash changes because the suite
definition changed.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edit, code execution). Standard context window.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:37:22 -06:00

669 lines
24 KiB
TypeScript

import type { IssueComment } from "../../packages/shared/src/types/issue.js";
import { gradeRemoteRestart, type RemoteRestartEvidence } from "./everyday-remote-restart.js";
export interface StoryCheck {
id: string;
passed: boolean;
detail: string;
}
export interface StoryIssue {
id: string;
companyId: string;
title: string;
status: string;
identifier?: string | null;
description?: string | null;
createdAt?: string;
updatedAt?: string;
assigneeAgentId?: string | null;
parentId?: string | null;
projectId?: string | null;
executionRunId?: string | null;
scheduledRetry?: unknown;
activeRecoveryAction?: unknown;
comments?: StoryComment[];
interactions?: StoryInteraction[];
activity?: StoryActivityRecord[];
wakeDiagnostics?: StoryWakeDiagnostics;
blockedTransitionAt?: string | null;
}
export interface StoryComment extends Partial<Pick<IssueComment, "authorAgentId" | "createdByRunId">> {
id?: string;
body?: unknown;
createdAt?: string;
}
export interface StoryActivityRecord {
action?: string;
details?: unknown;
createdAt?: string;
}
export interface StoryInteraction {
sourceRunId?: string | null;
createdByAgentId?: string | null;
id?: string;
kind?: string;
issueId?: string | null;
status?: string;
continuationPolicy?: string | null;
resolverPolicy?: string | null;
effectiveResolverPolicy?: string | null;
addresseeAgentId?: string | null;
resolvedByAgentId?: string | null;
resolvedByRunId?: string | null;
resolvedAt?: string | null;
result?: Record<string, unknown> | null;
payload?: Record<string, unknown> | null;
createdAt?: string;
}
/** Select the accepted review card for the same child and lead after the initial handoff boundary. */
export function storyAcceptedAgentReview(
child: StoryIssue | undefined,
initialInteractionId: string | undefined,
leadId: string,
runs: StoryRun[],
): StoryInteraction | undefined {
if (!child?.interactions?.length || !initialInteractionId) return undefined;
const initial = child.interactions.find(
(candidate) => candidate.id === initialInteractionId,
);
if (!initial) return undefined;
const initialCreatedAt = Date.parse(initial.createdAt ?? "");
return child.interactions
.filter((candidate) => {
const result = candidate.result;
const target = candidate.payload?.target as
| Record<string, unknown>
| undefined;
const revisionId = String(target?.revisionId ?? "");
const reviewRun = runs.find(
(run) => run.id === candidate.resolvedByRunId,
);
return (
candidate.kind === "request_confirmation" &&
target?.type === "custom" &&
target?.key === "native_completion_review" &&
candidate.issueId === child.id &&
candidate.addresseeAgentId === leadId &&
candidate.resolvedByAgentId === leadId &&
candidate.status === "accepted" &&
Number.isFinite(initialCreatedAt) &&
Number.isFinite(Date.parse(candidate.createdAt ?? "")) &&
Date.parse(candidate.createdAt ?? "") >= initialCreatedAt &&
result?.version === 1 &&
result?.outcome === "accepted" &&
reviewRun?.status === "succeeded" &&
reviewRun.agentId === leadId &&
reviewRun.contextSnapshot?.nativeReviewInteractionId === candidate.id &&
reviewRun.contextSnapshot?.nativeReviewDecisionId === revisionId &&
revisionId.length > 0
);
})
.sort((a, b) =>
Date.parse(a.resolvedAt ?? "") - Date.parse(b.resolvedAt ?? ""),
)[0];
}
export interface StoryWakeDiagnosticEvent {
kind?: string;
agentId?: string | null;
reason?: string | null;
status?: string;
payload?: Record<string, unknown> | null;
}
export interface StoryWakeDiagnostics {
events?: StoryWakeDiagnosticEvent[];
blockerDiagnostics?: {
readiness?: { unresolvedBlockerCount?: number | null } | null;
blockers?: unknown[];
};
}
export interface StoryRun {
id: string;
companyId: string;
agentId: string;
status: string;
runtimeMode?: string;
runnerInstanceId?: string | null;
processPid?: number | null;
processStartedAt?: string | null;
nativeIssueId?: string | null;
nativeSessionId?: string | null;
contextSnapshot?: Record<string, unknown> | null;
resultJson?: Record<string, unknown> | null;
usageJson?: Record<string, unknown> | null;
runnerProfileJson?: Record<string, unknown> | null;
sessionIdAfter?: string | null;
startedAt?: string | null;
finishedAt?: string | null;
retryOfRunId?: string | null;
scheduledRetryReason?: string | null;
runtimeModeResolvedAt?: string | null;
lastOutputSeq?: number | null;
errorCode?: string | null;
error?: string | null;
nativePhase?: string | null;
}
export type StoryArtifactGradeMode = "base" | "separator" | "max-length";
/** Select the artifact oracle variant required by each workflow's actual case. */
export function artifactGradeModeForPhase(
phase:
| "hired-delivery"
| "reused-delivery"
| "delegated-delivery"
| "reviewed-delivery"
| "recovered-delivery",
): StoryArtifactGradeMode {
switch (phase) {
case "hired-delivery":
case "reviewed-delivery":
return "base";
case "reused-delivery":
return "separator";
case "delegated-delivery":
case "recovered-delivery":
return "max-length";
}
}
/** A review run is valid only after the lead run has completed. */
export function storyParentCompletionPrecedesReview(
parentFinishedAt: string | null | undefined,
reviewStartedAt: string | null | undefined,
parentBlocked: boolean,
): boolean {
if (reviewStartedAt) {
return Boolean(
parentFinishedAt &&
Date.parse(parentFinishedAt) <= Date.parse(reviewStartedAt),
);
}
return parentBlocked;
}
export const isActiveStoryRun = (run: StoryRun) =>
["queued", "running", "scheduled_retry"].includes(run.status);
export function isStoryWorkspaceDeferral(run: StoryRun) {
const recovery = run.resultJson?.executionRecovery as
Record<string, unknown> | undefined;
const startup = run.resultJson?.startupCancellation as
Record<string, unknown> | undefined;
const neverStartedRetry =
run.errorCode === "cancelled" &&
run.scheduledRetryReason === "workspace_busy" &&
typeof run.retryOfRunId === "string" &&
run.retryOfRunId.length > 0 &&
run.startedAt === null &&
run.runtimeModeResolvedAt === null &&
run.lastOutputSeq === 0 &&
!run.processStartedAt &&
!run.sessionIdAfter &&
!run.usageJson &&
!run.runnerProfileJson?.nativeExecutionInput &&
typeof startup?.requestedAt === "string" &&
Number.isFinite(Date.parse(startup.requestedAt));
return (
run.status === "cancelled" &&
((run.errorCode === "workspace_busy" &&
recovery?.providerWorkStarted === false) ||
neverStartedRetry) &&
!run.runnerInstanceId &&
!run.nativeSessionId &&
!run.processPid
);
}
/** Exempt the injected stop itself, never a later recovery/provider failure on that run. */
export function isExpectedStoryInterruption(
run: StoryRun,
allowedRunIds: readonly string[],
): boolean {
if (!allowedRunIds.includes(run.id)) return false;
return (
// Operator interrupts and reassignment are deliberate reliability-scenario stops.
(run.status === "cancelled" && (!run.errorCode || ["cancelled", "operator_interrupted", "issue_reassigned"].includes(run.errorCode))) ||
(run.status === "interrupted" && run.errorCode === "server_shutdown_interrupted") ||
(run.status === "failed" && run.errorCode === "process_lost")
);
}
/** `recoveredRunIds`: failures whose work a later run on the same task verifiably resumed. */
export function storyUnexpectedRunFailure(runs: StoryRun[], allowedRunIds: readonly string[], recoveredRunIds: readonly string[] = []) {
return runs.find((run) =>
["failed", "timed_out", "cancelled", "interrupted"].includes(run.status) &&
!isStoryWorkspaceDeferral(run) && !recoveredRunIds.includes(run.id) &&
!isExpectedStoryInterruption(run, allowedRunIds),
);
}
/** Called after the boundary matcher; reject only with durable proof of the wrong ordering. */
export function storyUnexercisedReviewBoundary(
issues: StoryIssue[],
runs: StoryRun[],
parentId: string,
leadId: string,
): string | undefined {
if (issues.length === 0 || runs.length === 0) return;
if (!issues.every((issue) =>
issue.status === "done" && !issue.scheduledRetry && !issue.activeRecoveryAction,
)) return;
if (!runs.every((run) => run.status === "succeeded")) return;
const child = issues.find((issue) => issue.parentId === parentId);
const accepted = child?.interactions?.flatMap((interaction) => {
const review = storyAcceptedAgentReview(child, interaction.id, leadId, runs);
return review ? [review] : [];
})[0];
const reviewRun = accepted && runs.find((run) => run.id === accepted.resolvedByRunId);
const reviewStartedAt = Date.parse(reviewRun?.startedAt ?? "");
const parentRuns = runs.filter((run) =>
run.nativeIssueId === parentId ||
run.contextSnapshot?.issueId === parentId ||
run.contextSnapshot?.taskId === parentId,
);
// Snapshots are fetched separately. Missing review evidence is not proof that it
// never happened; wait unless persisted timestamps rule out the required order.
if (!Number.isFinite(reviewStartedAt) || parentRuns.length === 0) return;
if (!parentRuns.every((run) => {
const finishedAt = Date.parse(run.finishedAt ?? "");
return Number.isFinite(finishedAt) && finishedAt > reviewStartedAt;
})) return;
return "Review handoff boundary not exercised: the accepted review started before any parent run finished, so the blocked-parent-before-review ordering was not tested.";
}
export function storyLifecycleChecks(input: {
issues: StoryIssue[];
runs: StoryRun[];
parentId: string;
leadId: string;
allowedInterruptedRuns?: string[];
/** Failed runs whose work a later run on the same task verifiably resumed (reliability scenarios). */
recoveredRuns?: readonly string[];
/** Agents that may execute on the parent; defaults to the lead (reassignment adds the new owner). */
parentAgentIds?: readonly string[];
requireRemoteRestart?: boolean;
remoteRestart?: RemoteRestartEvidence;
}): StoryCheck[] {
const executed = input.runs.filter((r) => !isStoryWorkspaceDeferral(r));
const allowed = input.allowedInterruptedRuns ?? [];
const check = (id: string, passed: boolean, detail: string): StoryCheck => ({
id,
passed,
detail,
});
return [
...(input.requireRemoteRestart ? gradeRemoteRestart(input.remoteRestart) : []),
check(
"tasks-done",
input.issues.length > 0 && input.issues.every((i) => i.status === "done"),
"Every story task must reach Done.",
),
check(
"settled",
!input.runs.some(isActiveStoryRun) &&
!input.issues.some((i) => i.scheduledRetry || i.activeRecoveryAction),
"No active runs or scheduled recovery remain.",
),
check(
"native-runtime",
executed.length > 0 &&
executed.every(
(r) => r.runtimeMode === "native" && Boolean(r.runnerInstanceId),
),
"All executions must prove native runtime and runner identity.",
),
check(
"successful-runs",
executed.every((r) => r.status === "succeeded" || isExpectedStoryInterruption(r, allowed)
|| (input.recoveredRuns ?? []).includes(r.id)),
"Only the expected stop or process-loss outcome of an injected interruption is exempt.",
),
check(
"bounded-work",
input.runs.length <= 12,
"No more than twelve executions, including child and recovery turns.",
),
check(
"parent-owned-by-lead",
!executed.some(
(r) =>
(r.contextSnapshot?.issueId === input.parentId ||
r.contextSnapshot?.taskId === input.parentId) &&
!(input.parentAgentIds ?? [input.leadId]).includes(r.agentId),
),
"A mentioned worker must not execute on the parent.",
),
];
}
/** A terminal first turn cannot satisfy a later queued request. */
export function storyRepliesConsumed(
runs: StoryRun[],
commentIds: string[],
): boolean {
return commentIds.every((id) =>
runs.some((run) => {
if (run.status !== "succeeded" || isStoryWorkspaceDeferral(run))
return false;
const input = run.runnerProfileJson?.nativeExecutionInput as
{ task?: { prompt?: string } } | undefined;
return input?.task?.prompt?.includes(id) === true;
}),
);
}
export type StoryReplyExpectation = string | ((body: string) => boolean);
export function storyAgentReplyMatches(
comment: StoryComment | undefined,
agentId: string,
expected: StoryReplyExpectation,
): boolean {
if (!comment || comment.authorAgentId !== agentId) return false;
const body = String(comment.body ?? "");
return typeof expected === "string" ? body.includes(expected) : expected(body);
}
/** The requested output is one exact token after this continuation's marker. */
export function retainedControllerReplyMatcher(marker: string): (body: string) => boolean {
const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`(?:^|\\s)${escaped}\\s+queue_r(?=\\s|$)`);
return body => pattern.test(body);
}
/** A terminal task is only ready for response assertions after its agent reply is visible. */
export function storyHasAgentReply(
issue: StoryIssue | undefined,
agentId: string,
expectedText: StoryReplyExpectation,
): boolean {
return (
issue?.comments?.some(
(comment) => storyAgentReplyMatches(comment, agentId, expectedText),
) ?? false
);
}
const RUNNABLE_WAKE_STATUSES = new Set([
"queued",
"deferred_issue_execution",
"claimed",
]);
/** A durable wake means an agent still has a recorded next action. */
export function storyHasPendingAgentWake(
issue: StoryIssue | undefined,
agentIds: string | readonly string[],
): boolean {
const ids = new Set(typeof agentIds === "string" ? [agentIds] : agentIds);
return (
issue?.wakeDiagnostics?.events?.some(
(event) =>
event.kind === "wake_request" &&
event.agentId != null &&
ids.has(event.agentId) &&
RUNNABLE_WAKE_STATUSES.has(String(event.status)),
) ?? false
);
}
/** Any issue-scoped wake keeps the workflow actionable while it is being dispatched. */
export function storyHasPendingAgentWork(
issues: StoryIssue[],
agentIds: string | readonly string[],
): boolean {
return issues.some((issue) => storyHasPendingAgentWake(issue, agentIds));
}
/** A blocked story is stranded only when no issue has a durable continuation wake. */
export function storyHasStrandedBlockedLeaf(
issues: StoryIssue[],
agentIds: string | readonly string[],
): boolean {
return (
issues.some((issue) => issue.status === "blocked") &&
!storyHasPendingAgentWork(issues, agentIds)
);
}
/**
* A completed native review is a durable continuation trigger even when the
* parent projection has not yet exposed the claimed wake. Keep the existing
* workflow deadline; its timeout diagnostic identifies a missing continuation.
*/
export function storyHasDurableAgentReviewContinuation(
issues: StoryIssue[],
parentId: string,
leadId: string,
runs: StoryRun[],
): boolean {
const parent = issues.find((issue) => issue.id === parentId);
if (parent?.status !== "blocked") return false;
const blockedAt = Date.parse(parent.blockedTransitionAt ?? "");
const terminalWake = parent.wakeDiagnostics?.events?.some(
(event) =>
event.kind === "wake_request" &&
event.agentId === leadId &&
event.reason === "issue_blockers_resolved" &&
["completed", "failed", "cancelled"].includes(String(event.status)),
);
if (terminalWake) return false;
return issues.some((child) =>
child.parentId === parentId &&
child.status === "done" &&
(child.interactions?.some((interaction) => {
const target = interaction.payload?.target as
| Record<string, unknown>
| undefined;
const reviewRun = runs.find((run) => run.id === interaction.resolvedByRunId);
return (
interaction.kind === "request_confirmation" &&
interaction.status === "accepted" &&
interaction.issueId === child.id &&
interaction.addresseeAgentId === leadId &&
interaction.resolvedByAgentId === leadId &&
interaction.result?.version === 1 &&
interaction.result?.outcome === "accepted" &&
Number.isFinite(blockedAt) &&
Date.parse(interaction.resolvedAt ?? "") >= blockedAt &&
target?.type === "custom" &&
target?.key === "native_completion_review" &&
reviewRun?.status === "succeeded" &&
reviewRun.agentId === leadId
);
}) ?? false),
);
}
/** An executed approval or recorded rejection may enqueue its wake just after run finalization.
* Wait within the original deadline only for this task/run's recorded response.
* Once a follow-up run has consumed it, a new blocker is a real failure.
*/
export function storyHasDurableServiceContinuation(issues: StoryIssue[], parentId: string, agentId: string, runs: StoryRun[]): boolean {
const issue = issues.find(i => i.id === parentId);
if (issue?.status !== "blocked" || issue.assigneeAgentId !== agentId) return false;
return issue.interactions?.some(interaction => {
const source = runs.find(run => run.id === interaction.sourceRunId);
const action = interaction.payload?.toolAction as Record<string, unknown> | undefined;
const result = interaction.result?.toolAction as Record<string, unknown> | undefined;
const resolved = (interaction.status === "accepted" && interaction.result?.outcome === "accepted" && result?.status === "executed") ||
(interaction.status === "rejected" && interaction.result?.outcome === "rejected" && result === undefined);
if (interaction.issueId !== parentId || interaction.createdByAgentId !== agentId ||
interaction.kind !== "request_confirmation" || interaction.continuationPolicy !== "wake_assignee" ||
!resolved ||
action?.version !== 1 || typeof action.actionRequestId !== "string" || !action.actionRequestId.trim() ||
source?.agentId !== agentId || source.companyId !== issue.companyId || source.nativeIssueId !== parentId || source.status !== "succeeded" ||
(!Number.isFinite(Date.parse(source.finishedAt ?? "")) || !Number.isFinite(Date.parse(interaction.resolvedAt ?? "")))) return false;
return !runs.some(run => {
const input = run.runnerProfileJson?.nativeExecutionInput as
{ interactionResponses?: Array<{ interactionId?: string }> } | undefined;
return run.id !== source.id && run.agentId === agentId && run.companyId === issue.companyId &&
input?.interactionResponses?.some(response => response.interactionId === interaction.id);
});
}) ?? false;
}
/** Timeout evidence requires a stranded leaf with durable accepted-review evidence. */
export function storyReviewContinuationTimeoutDetail(
issues: StoryIssue[],
parentId: string,
leadId: string,
runs: StoryRun[],
observableAgentIds: string | readonly string[],
): string | undefined {
return (
storyHasStrandedBlockedLeaf(issues, observableAgentIds) &&
storyHasDurableAgentReviewContinuation(issues, parentId, leadId, runs)
)
? "task is Blocked without an active continuation after accepted review"
: undefined;
}
/** A pending interaction is agent-owned when its wake policy and wake request agree. */
export function storyHasPendingAgentReview(
issue: StoryIssue | undefined,
agentIds: string | readonly string[],
): boolean {
return Boolean(
issue?.interactions?.some((interaction) =>
storyHasPendingAgentReviewForInteraction(issue, interaction, agentIds),
),
);
}
/** Pending review is human-owned only when no matching agent review wake is runnable. */
export function storyHasPendingHumanInteraction(
issue: StoryIssue | undefined,
agentIds: string | readonly string[],
): boolean {
const pendingInteraction = issue?.interactions?.some(
(interaction) =>
interaction.status === "pending" &&
!storyHasPendingAgentReviewForInteraction(issue, interaction, agentIds),
);
return Boolean(
pendingInteraction,
);
}
function storyHasPendingAgentReviewForInteraction(
issue: StoryIssue | undefined,
interaction: StoryInteraction,
agentIds: string | readonly string[],
): boolean {
const ids = new Set(typeof agentIds === "string" ? [agentIds] : agentIds);
if (
interaction.status !== "pending" ||
interaction.continuationPolicy !== "wake_assignee" ||
interaction.resolverPolicy === "human_only" ||
interaction.effectiveResolverPolicy === "human_only" ||
!interaction.addresseeAgentId ||
!ids.has(interaction.addresseeAgentId)
)
return false;
const wakeEvents =
issue?.wakeDiagnostics?.events?.filter(
(event) =>
event.kind === "wake_request" &&
event.agentId === interaction.addresseeAgentId &&
RUNNABLE_WAKE_STATUSES.has(String(event.status)),
) ?? [];
const explicitlyBound = wakeEvents.some(
(event) =>
event.payload?.nativeReviewInteractionId === interaction.id,
);
if (explicitlyBound) return true;
if (
wakeEvents.some(
(event) => event.payload?.nativeReviewInteractionId != null,
)
)
return false;
const sameAgentPendingCards =
issue?.interactions?.filter(
(candidate) =>
candidate.status === "pending" &&
candidate.continuationPolicy === "wake_assignee" &&
candidate.resolverPolicy !== "human_only" &&
candidate.effectiveResolverPolicy !== "human_only" &&
candidate.addresseeAgentId === interaction.addresseeAgentId,
).length ?? 0;
// The public v1 wake projection has no binding payload. With multiple cards
// for one agent, conservatively refuse to attribute an unbound wake.
return wakeEvents.length > 0 && sameAgentPendingCards === 1;
}
/** The public blocker projection is durable evidence of a dependency hold. */
export function storyIssueHasUnresolvedDependency(issue: StoryIssue): boolean {
const readiness = issue.wakeDiagnostics?.blockerDiagnostics?.readiness;
return (
issue.status === "blocked" &&
((readiness?.unresolvedBlockerCount ?? 0) > 0 ||
(issue.wakeDiagnostics?.blockerDiagnostics?.blockers?.length ?? 0) > 0)
);
}
/** Status history remains observable after the scheduler clears the blocker. */
export function storyIssueHasBlockedTimeline(issue: StoryIssue): boolean {
return storyIssueHasBlockedTimelineBefore(issue);
}
/** A blocker timeline entry is useful only when it predates the review card. */
export function storyIssueHasBlockedTimelineBefore(
issue: StoryIssue,
before?: string,
): boolean {
return Boolean(
issue.activity?.some((record) => {
const action = String(record.action ?? "").toLowerCase();
const details = (record.details ?? {}) as Record<string, unknown>;
const changes = (details.changes ?? {}) as Record<string, unknown>;
const statusChange = (changes.status ?? {}) as Record<string, unknown>;
const recordAt = Date.parse(String(record.createdAt ?? ""));
const beforeAt = before ? Date.parse(before) : Number.NaN;
return (
action === "issue.updated" &&
Number.isFinite(recordAt) &&
(!Number.isFinite(beforeAt) || recordAt <= beforeAt) &&
(details.status === "blocked" ||
details.toStatus === "blocked" ||
statusChange.to === "blocked")
);
}),
);
}
/** Native result evidence survives a fast transition past the blocked projection. */
export function storyRunReportsDependencyBlock(run: StoryRun): boolean {
const result = run.resultJson?.nativeResult as
| Record<string, unknown>
| undefined;
if (result?.reportedWorkDisposition !== "blocked") return false;
const blocker = result.blocker as Record<string, unknown> | undefined;
const reason = String(blocker?.reasonCode ?? "").toLowerCase();
return reason.includes("depend");
}
export function storyParentFinishedAfterChildren(
runs: StoryRun[],
parentId: string,
leadId: string,
childIds: string[],
): boolean {
const scope = (r: StoryRun) =>
r.nativeIssueId ?? r.contextSnapshot?.issueId ?? r.contextSnapshot?.taskId;
const completed = runs.filter(
(r) => r.status === "succeeded" && !isStoryWorkspaceDeferral(r),
);
const children = completed.filter((r) => childIds.includes(String(scope(r))));
if (!children.length) return false;
const lastChildFinish = Math.max(
...children.map((r) => Date.parse(r.finishedAt ?? "")),
);
return completed.some(
(r) =>
r.agentId === leadId &&
scope(r) === parentId &&
Date.parse(r.finishedAt ?? "") >= lastChildFinish,
);
}