Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,12 @@ macOS Keychain and keeps all model/search provider credentials in Cortex Worker.
Live execution requires the authenticated catalog's durable protocol v2: the
client submits one idempotent request, records the server-owned run ID, and
polls the tenant-bound result. A dropped response or rolling deployment is
retried with the same request ID instead of starting a second paid run. Preserve
the printed request ID; `--request-id <id>` explicitly reattaches after a local
timeout or interruption.
retried with the same request ID instead of starting a second paid run. Before
the first submission, the client atomically writes a chmod-0600 checkpoint to
`--metadata` and prints its generated request ID to stderr. After Cortex accepts
the run, that checkpoint also records the durable run ID; terminal success
replaces it with final metadata. Preserve the checkpoint and use
`--request-id <id>` to reattach after a local timeout or interruption.
`run-research.mjs` remains available as an explicit local-provider fallback; it
supports allowlisted interactive-shell environment loading and build-freshness
checks. Exact lane preflight requires an as-of date:
Expand Down Expand Up @@ -204,7 +207,7 @@ npm run pack:release # → releases/braintied-research-<version>.tgz

```jsonc
// consumer package.json
"@braintied/research": "file:vendor/braintied-research-0.8.1.tgz"
"@braintied/research": "file:vendor/braintied-research-0.8.2.tgz"
```

## Development
Expand Down
50 changes: 32 additions & 18 deletions docs/internal-agent-research-release-2026-07-23.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,41 @@

## Outcome

`@braintied/research` 0.8.1 replaces the long-lived HTTP execution contract
with resumable, idempotent durable polling. It is the client-side half of Cortex
internal-tools protocol v2 and is required for paid agent research.
`@braintied/research` 0.8.2 makes Cortex protocol-v2 research recoverable even
when the local CLI is killed before a terminal response. The request ID now
exists in a private durable checkpoint and a flushed diagnostic before the
first paid submission instead of living only in process memory.

## Root cause

Version 0.8.0 kept a deep research request attached to one Fly Machine. JSON
whitespace heartbeats prevented idle proxy timeouts, but a rolling Machine
replacement still severed the live request after provider work had begun. The
client could not determine whether work or billing continued and a retry risked
starting a second attempt.
Version 0.8.1 replaced the long-lived 0.8.0 request with resumable durable
polling, but a generated request ID was written to stdout and metadata only
after terminal success. A SIGINT, process death, or local machine failure after
submission could therefore lose the only reattachment key even though Cortex
still owned a queued or running paid attempt.

## Durable contract

- The authenticated catalog must advertise protocol v2 and exact same-origin
submission/status paths.
- Submission uses one caller request ID as its idempotency key. The client may
retry a dropped submission without creating another durable record.
- Before the first submission, the client fsyncs a mode-0600 temporary
checkpoint, atomically renames it onto `--metadata`, fsyncs the parent
directory, and flushes a compact request-ID recovery diagnostic to stderr.
The checkpoint contains no brief, Agent Auth token, provider credential, or
provider result.
- The server returns a run UUID; the client polls its authenticated status and
validates the final result before writing any artifact.
validates the final result. Immediately after acceptance, the checkpoint is
atomically replaced with one that also records the durable run ID and status.
- Terminal success atomically replaces the checkpoint with final run metadata.
- `--request-id` reattaches a later CLI process to the same attempt.
- Temporary transport, `404`, `408`, `425`, `429`, `502`, `503`, and `504`
responses use bounded retry delays within the caller's deadline.
- Metadata and stdout expose the request ID and durable run ID, never the Agent
Auth token, provider credentials, raw trusted recall, or provider error body.
- Private checkpoint metadata and pre-submission stderr expose the request ID;
accepted/final metadata and final stdout also expose the durable run ID.
None expose the Agent Auth token, provider credentials, raw trusted recall,
brief, or provider error body.
- A protocol-v1/stream-only deployment is rejected before a paid call.

## Deployment exclusion
Expand All @@ -40,27 +50,31 @@ bounded so a dead deploy process cannot create an indefinite outage.
## Operator recovery

When a client deadline or local process interruption occurs, do not infer zero
cost and do not issue a new request ID. Repeat the identical command with the
reported `--request-id`; the client will retrieve or continue polling the
existing durable record. A fresh ID is a deliberately separate paid run.
cost and do not issue a new request ID. Read `request_id` from `--metadata` or
copy it from the pre-submission stderr line, then repeat the identical command
with `--request-id <id>`. A `submission_pending` checkpoint does not prove
whether Cortex accepted the request; resubmitting its same ID is safe because
server admission is idempotent. A fresh ID is a deliberately separate paid
run.

## Acceptance gates

Package acceptance passed:

- unit and contract tests: 56/56;
- unit and contract tests: 57/57, including forced SIGINT before terminal
status plus a second-process `--request-id` reattachment proof;
- Instagram/Bright Data boundary tests: 8/8;
- TypeScript and production build;
- CommonJS, ESM, packaged CLI, and exact version import from a clean tarball
consumer;
- clean production dependency audit: zero vulnerabilities;
- exact 14-file tarball audit plus Gitleaks and high-confidence provider,
Agent Auth, Swishh, GitHub, Slack, and private-key signatures: no leaks;
- archive: `releases/braintied-research-0.8.1.tgz`;
- archive: `releases/braintied-research-0.8.2.tgz`;
- SHA-256:
`901a1734195d060e47fe5f34bb9b5fb540f7afa3a81e374fed9aebff7e0c05ea`;
`cbb5fa5d255914728a213085dc3b0e379e7da678901538d9576e46b99f47e155`;
- npm integrity:
`sha512-w/GrdkCn56vmlQ+gExVPE4F/NHDbttS2xAkgE33N7fKBJcbwZ8eBEacYKXJbfwF0FQaTxBVNUmFuoTw2gd9zPw==`.
`sha512-67nLLm7tme3GaUDYrZFi7Eap+B0qIVnA4NKaVYdFMi63SNJGODUDvU6jXibTDXiw807y95Lwvv5OcdloCqSqvA==`.

Production acceptance additionally requires a protocol-v2 catalog probe plus a
transport-loss canary that destroys the first submission socket after
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@braintied/research",
"version": "0.8.1",
"version": "0.8.2",
"description": "Deep-research engine — multi-provider search, quote extraction, synthesis, critique, grounded report assembly, and research-driven document generation (PRDs, briefs, specs)",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
19 changes: 13 additions & 6 deletions skills/run-braintied-research/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ model providers; treat every brief as outbound data.
```

The server persists the authenticated input before dispatch and returns a
durable run ID. Submission and status requests retry bounded transport and
temporary-service failures with the same request ID. If the local deadline
or process interrupts the client, rerun the identical command with the
durable run ID. Before the first submission, the client atomically writes a
chmod-0600 checkpoint to `--metadata` and prints the generated request ID to
stderr. Once Cortex accepts the run, the same checkpoint also records the
durable run ID; terminal success replaces it with final metadata. Submission
and status requests retry bounded transport and temporary-service failures
with the same request ID. If the local deadline or process interrupts the
client, preserve that checkpoint and rerun the identical command with its
printed `--request-id <id>` to reattach; never choose a fresh ID for the same
paid attempt. Cortex deploys atomically exclude queued/running research, so a
normal rolling release cannot cut a paid run in half.
Expand Down Expand Up @@ -197,9 +201,12 @@ grounding were **not produced**; do not describe either as `null`. If standalone
provider probes were made, report their known quota/credit cost separately.

For an interrupted internal run, distinguish client transport from server
execution. Preserve its request ID and use `--request-id` to retrieve the same
durable record. Do not report `$0.00`, infer provider cost, or start a replacement
run until the durable status proves the original attempt terminal.
execution. Read the request ID from the private `--metadata` checkpoint or the
pre-submission stderr diagnostic and use `--request-id` to retrieve the same
durable record. A `submission_pending` checkpoint does not prove the server did
or did not accept the request; resubmitting that same ID is the safe recovery
operation. Do not report `$0.00`, infer provider cost, or start a replacement run
until the durable status proves the original attempt terminal.

Run `node skills/run-braintied-research/scripts/run-internal-research.mjs --help`
for the default internal surface and `run-research.mjs --help` for the local
Expand Down
24 changes: 16 additions & 8 deletions skills/run-braintied-research/references/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ node skills/run-braintied-research/scripts/run-internal-research.mjs \
--allow-external
```

The Markdown file is always the public-provider report. The metadata file adds
`program_status`, `source_coverage`, `profile_coverage`, a pointer to the
trusted artifact, and a separately labeled reference-only `private_manifest`.
The Markdown file is always the public-provider report. Before submission, the
metadata path contains a minimal chmod-0600 recovery checkpoint with the
request ID and no brief, token, or provider content. Cortex acceptance adds the
durable run ID; terminal success atomically replaces that checkpoint with final
metadata. Final metadata adds `program_status`, `source_coverage`,
`profile_coverage`, a pointer to the trusted artifact, and a separately labeled
reference-only `private_manifest`.
The third file is a chmod-0600 trusted-local appendix for the authenticated
agent: up to 20 findings balanced across Cortex and Telegram, each with
credential-scrubbed excerpts capped at 1,200 Unicode characters, bounded
Expand All @@ -69,11 +73,15 @@ reattaches instead of paying for a second run.

Deep requests default to a 3,600-second local client deadline; other kinds
default to 1,200 seconds. The deadline stops polling, not the server-owned run.
Preserve the request ID from the diagnostic and repeat the same invocation with
`--request-id <id>` to resume. Metadata records both `request_id` and
`durable_run_id`. A successful HTTP response is not itself success: the client
requires the strict completed result envelope and a non-empty report before it
writes artifacts.
The client fsyncs and atomically renames a private checkpoint, then flushes a
compact request-ID diagnostic to stderr, before it sends the first paid
submission. Preserve the request ID from that checkpoint or diagnostic and
repeat the same invocation with `--request-id <id>` to resume. A
`submission_pending` checkpoint is intentionally ambiguous about remote
acceptance; resubmission with its same ID is idempotent. Accepted and final
metadata record both `request_id` and `durable_run_id`. A successful HTTP
response is not itself success: the client requires the strict completed
result envelope and a non-empty report before it writes final artifacts.

Cortex deployments acquire an atomic database exclusion lease. Lease
acquisition fails while any unexpired research run is queued/running, and new
Expand Down
110 changes: 104 additions & 6 deletions skills/run-braintied-research/scripts/run-internal-research.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { execFile } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { mkdir, open, readFile, rename, rm } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
Expand All @@ -15,6 +15,7 @@ const PIPELINE_KINDS = new Set(['quick', 'standard', 'deep', 'social']);
const DEFAULT_ENDPOINT = 'https://ora-cortex-worker.fly.dev/internal/tools/execute';
const DEFAULT_KEYCHAIN_SERVICE = 'braintied-agent-auth';
const DEFAULT_KEYCHAIN_ACCOUNT = 'codex';
const CHECKPOINT_SCHEMA_VERSION = 1;
const PACKAGE_ROOT = fileURLToPath(new URL('../../../', import.meta.url));
const PACKAGE_JSON = path.join(PACKAGE_ROOT, 'package.json');
const execFileAsync = promisify(execFile);
Expand All @@ -41,7 +42,7 @@ Options:
--keychain-service <name> macOS Keychain service (default: braintied-agent-auth)
--keychain-account <name> macOS Keychain account (default: codex)
--output <path> Required Markdown report output
--metadata <path> Required JSON run metadata output
--metadata <path> Required private JSON checkpoint/final metadata output
--trusted-output <path> Required for live profile runs; chmod-0600 JSON
trusted-local appendix (never public Markdown)
--allow-external Acknowledge that the brief goes to external services
Expand Down Expand Up @@ -289,17 +290,77 @@ async function readBrief(values) {

async function atomicWrite(targetPath, contents) {
const absolute = path.resolve(targetPath);
await mkdir(path.dirname(absolute), { recursive: true });
const temporary = `${absolute}.tmp-${process.pid}`;
const directory = path.dirname(absolute);
await mkdir(directory, { recursive: true });
const temporary = `${absolute}.tmp-${process.pid}-${randomUUID()}`;
let handle;
try {
await writeFile(temporary, contents, { encoding: 'utf8', mode: 0o600 });
handle = await open(temporary, 'wx', 0o600);
await handle.writeFile(contents, { encoding: 'utf8' });
await handle.sync();
await handle.close();
handle = undefined;
await rename(temporary, absolute);
const directoryHandle = await open(directory, 'r');
try {
await directoryHandle.sync();
} finally {
await directoryHandle.close();
}
} finally {
await handle?.close();
await rm(temporary, { force: true });
}
return absolute;
}

async function writeStderrLine(value) {
await new Promise((resolve, reject) => {
process.stderr.write(`${value}\n`, (error) => {
if (error) reject(error);
else resolve();
});
});
}

function createResearchCheckpoint({
packageVersion: currentPackageVersion,
requestId,
run,
protocolVersion,
startedAt,
kind,
maxCostUsd,
recencyDays,
profileRef,
profileMode,
asOf,
timeoutSeconds,
}) {
return {
artifact_type: 'braintied_internal_research_checkpoint',
schema_version: CHECKPOINT_SCHEMA_VERSION,
mode: 'internal',
package_version: currentPackageVersion,
request_id: requestId,
durable_run_id: run?.id ?? null,
execution_protocol: protocolVersion,
checkpoint_status: run === null
? 'submission_pending'
: 'awaiting_terminal_result',
durable_run_status: run?.status ?? null,
started_at: startedAt.toISOString(),
updated_at: new Date().toISOString(),
kind,
requested_max_cost_usd: maxCostUsd ?? null,
requested_recency_days: recencyDays ?? null,
profile_ref: profileRef ?? null,
profile_mode: profileMode ?? null,
as_of: asOf ?? null,
timeout_seconds: timeoutSeconds,
};
}

function validateResult(payload) {
if (payload === null || typeof payload !== 'object' || payload.ok !== true) {
throw new Error('Internal tool returned an invalid success envelope.');
Expand Down Expand Up @@ -794,7 +855,15 @@ async function pollDurableResearch({
);
}

async function executeResearch({ endpoint, token, timeoutSeconds, requestId, input, probe }) {
async function executeResearch({
endpoint,
token,
timeoutSeconds,
requestId,
input,
probe,
onSubmitted,
}) {
const durable = resolveDurableEndpoints(endpoint, probe);
const deadlineMs = Date.now() + timeoutSeconds * 1000;
const run = await submitDurableResearch({
Expand All @@ -804,6 +873,7 @@ async function executeResearch({ endpoint, token, timeoutSeconds, requestId, inp
input,
deadlineMs,
});
await onSubmitted(run);
const result = await pollDurableResearch({
endpoint: durable.status(run.id),
token,
Expand Down Expand Up @@ -935,12 +1005,40 @@ async function main() {
const requestId = requestedRequestId ?? randomUUID();
const startedAt = new Date();
const startedMonotonic = process.hrtime.bigint();
const checkpointContext = {
packageVersion: check.package_version,
requestId,
protocolVersion: liveProbe.protocol_version,
startedAt,
kind,
maxCostUsd,
recencyDays,
profileRef,
profileMode,
asOf,
timeoutSeconds,
};
const persistCheckpoint = async (run) => atomicWrite(
values.metadata,
`${JSON.stringify(createResearchCheckpoint({
...checkpointContext,
run,
}), null, 2)}\n`,
);
const checkpointPath = await persistCheckpoint(null);
await writeStderrLine(JSON.stringify({
event: 'braintied_internal_research_checkpoint',
request_id: requestId,
metadata: checkpointPath,
resume_with: `--request-id ${requestId}`,
}));
const execution = await executeResearch({
endpoint,
token: auth.token,
timeoutSeconds,
requestId,
probe: liveProbe,
onSubmitted: persistCheckpoint,
input: {
brief,
kind,
Expand Down
Loading