Skip to content

software-factory: run observability — timing instrumentation + QUnit test timeout#5245

Merged
jurgenwerk merged 9 commits into
mainfrom
cs-11572-factory-run-observability
Jun 19, 2026
Merged

software-factory: run observability — timing instrumentation + QUnit test timeout#5245
jurgenwerk merged 9 commits into
mainfrom
cs-11572-factory-run-observability

Conversation

@jurgenwerk

@jurgenwerk jurgenwerk commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Adds detailed timing benchmark log output for the factory when --debug is provided. It's used for seeing where the time went (AI agent / syncing / validations) that helps with making decisions where to optimize the time needed for a software factory run.

jurgenwerk and others added 5 commits June 16, 2026 13:06
A factory:go run otherwise emits a flat wall of text with no timing,
so there's no way to attribute where the wall-clock went. Wrap logger()
to prepend [HH:MM:SS.mmm +Δms] to each line; the delta is the gap to the
previous line on any channel. Disable with FACTORY_LOG_TIMESTAMPS=0.

CS-11572

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Time the three buckets that dominate a run — agent turns, the validation
pipeline, and workspace syncs — per issue, and log a per-issue line plus a
grand-total table at the end. The unattributed remainder (total minus the
three buckets) is scheduler + index-poll overhead, surfaced explicitly.

Also introduces SyncHaltError, exported for the --halt-on-sync-error flag:
the loop's per-step try/catch blocks rethrow it so a halt isn't swallowed.

CS-11572, CS-11575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A hung in-browser test page (boot error, hanging test, never-resolving
promise) used to block the full 300s with only Playwright's opaque
"Timeout 300000ms exceeded". Default to 60s (override via
FACTORY_TEST_TIMEOUT_MS), replace the timeout error with a diagnostic
that names the likely cause, and log the run duration at info level.

CS-11573

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a failed workspace sync the loop normally recovers via a pull on the
next iteration — but that pull mutates local state and can mask the
realm/index inconsistency that the failure caused. The opt-in flag logs
the failure loudly and throws (SyncHaltError) before any recovery pull,
freezing realm + index state at the point of failure for inspection.

CS-11575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The +Δms delta advanced its baseline even for lines the logger suppressed
at the active level, so a hidden debug call between two info lines made the
next visible line's delta measure from an invisible point. Gate the
baseline update on whether the line will actually emit, mirroring
@cardstack/logger's own level check (instance _level vs severity order).

Verified the library's native timestamp feature is unwired in 0.2.1
(config.timestamps is never passed to the Logger constructor), so this
wrapper is the only source of timestamps — no double-stamping.

CS-11572

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jurgenwerk and others added 2 commits June 17, 2026 11:55
The timing instrumentation (per-line timestamps, per-phase durations, and
the end-of-run summary table) is now off by default so normal runs emit
clean output. The factory CLI turns it on under --debug:
- logger timestamps become a runtime flag (default off, FACTORY_LOG_TIMESTAMPS
  still pins it either way) flipped via setLogTimestampsEnabled;
- the issue loop only prints per-phase durations / the summary when
  config.debug is set, threaded from the CLI's --debug.

CS-11572

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the halt-on-sync-error debug flag (and SyncHaltError). It was
speculative forensics for an intermittent atomic-upload 500 that hasn't
recurred, and it can't really do its job: a flag for a rare failure only
helps if it's already on when the failure hits, and leaving it on trades
away the resilient pull-recovery that lets runs self-heal. Sync-failure
forensics are better served by richer always-on logging (follow-up),
without sacrificing resilience.

CS-11575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds debug-gated observability to software-factory runs so slow/opaque factory:go executions can be attributed to concrete phases (agent, validation, sync), with optional per-line timestamp/delta prefixes.

Changes:

  • Add logger support for per-line [HH:MM:SS.mmm +Δms] prefixes, enabled via --debug (or FACTORY_LOG_TIMESTAMPS override).
  • Collect and (under debug) emit per-issue phase timings and an end-of-run attribution summary in the issue loop.
  • Add a configurable QUnit completion timeout (default 60s) with a more actionable timeout error message.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/software-factory/src/test-run-execution.ts Adds configurable QUnit timeout + improved diagnostics and duration formatting.
packages/software-factory/src/logger.ts Wraps loggers to optionally prefix emitted lines with wall-clock + delta timestamps; adds runtime toggle.
packages/software-factory/src/issue-loop.ts Tracks agent/validation/sync wall time per issue and prints per-issue + grand-total timing summary under debug.
packages/software-factory/src/factory-issue-loop-wiring.ts Wires debug flag into the issue loop configuration.
packages/software-factory/src/cli/factory-entrypoint.ts Enables timestamp prefixes and debug-level logging when --debug is passed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +569 to +587
let timeoutMs = qunitTimeoutMs();
try {
await page.waitForFunction(
() => (window as any).__qunitResults?.runEnd !== null,
null,
{ timeout: timeoutMs },
);
} catch {
// Playwright's native message ("Timeout 60000ms exceeded") doesn't say
// what timed out or how long we actually waited. Replace it with a
// diagnostic that names the likely cause and is greppable.
let waited = Date.now() - start;
throw new Error(
`QUnit suite did not reach runEnd within ${timeoutMs}ms (waited ${waited}ms). ` +
`The page never set __qunitResults.runEnd — likely an Ember boot error, ` +
`a hanging test, or a never-resolving promise. Re-run with --debug to see ` +
`browser console output, or raise FACTORY_TEST_TIMEOUT_MS for a heavier suite.`,
);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(Written by Claude on Matic's behalf.)

Fixed in 30b8430. The catch now only rewrites Playwright's TimeoutError (err.name === 'TimeoutError'); any other error (execution context destroyed, page crash, etc.) is rethrown unchanged so it surfaces as-is. I also moved the waited baseline to a waitStart taken immediately before page.waitForFunction, so it measures the wait alone rather than the function's full wall clock (which included server startup, browser launch, and navigation).

Comment on lines 594 to 596
log.info(
`QUnit completed in ${fmtMs(durationMs)}: ${qunitResults.runEnd?.testCounts?.total ?? 0} test(s)`,
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(Written by Claude on Matic's behalf.)

Good catch — reverted to debug in 30b8430. The QUnit completed in … line stays gated like the rest of the observability output; its duration still surfaces under --debug via the timing summary, so a normal run stays clean. Updated the PR description bullet to match.

@jurgenwerk jurgenwerk changed the title software-factory: run observability (timing, test timeout, halt-on-sync-error) software-factory: run observability — timing instrumentation + QUnit test timeout Jun 17, 2026
…n log debug-gated

Per review on #5245:
- Only rewrite Playwright TimeoutError into the diagnostic; rethrow any
  other error (execution context destroyed, page crash) unchanged so it
  isn't masked as a timeout.
- Measure `waited` from the start of the wait, not function entry (which
  included server/browser startup + navigation).
- Revert the "QUnit completed" line info → debug so normal runs stay clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jurgenwerk
jurgenwerk marked this pull request as ready for review June 17, 2026 12:06

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

Copy link
Copy Markdown

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: 30b843019c

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

// Run the agent — it calls tools during its turn
let agentStartMs = Date.now();
let result = await agent.run(context, tools);
let agentMs = Date.now() - agentStartMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Attribute tool-triggered syncs to sync timing

When the agent self-validates with a realm-touching run_* tool, those tools call syncWorkspace before running (via syncWorkspaceForToolRun in factory-tool-builder.ts), and that happens inside agent.run(...). Measuring the whole call here records those tool syncs as agentMs, while syncMs only captures the loop-owned syncs routed through timedSync, so debug timing summaries can tell operators the model was slow when sync/index work was actually the cost.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(Written by Claude on Matic's behalf.)

Good catch — fixed in 1c0297d. Every syncWorkspace() call (the loop's own syncs and the realm-touching run_* tool syncs that fire inside agent.run via syncWorkspaceForToolRun) now goes through one shared stopwatch in the wiring, exposed to the loop as getSyncElapsedMs(). The loop reads sync time from deltas of that counter and subtracts the tool-sync delta elapsed during agent.run from agentMs — so tool-triggered sync time lands in syncMs, not agentMs. Added a regression test asserting a 500ms tool-triggered sync shows up in syncMs while agentMs stays 0.

…ent time

Per review on #5245: realm-touching run_* tools sync the workspace before
executing (syncWorkspaceForToolRun), inside agent.run. The old timing measured
the whole agent.run as agentMs and only counted loop-owned syncs (via timedSync)
as syncMs — so the debug summary could blame the model for sync/index work.

Time every syncWorkspace() call through one shared stopwatch in the wiring and
expose getSyncElapsedMs() to the loop. The loop now reads sync time from deltas
of that counter (per-issue and grand total) and subtracts the tool-sync delta
that elapses during agent.run from agentMs. Drops the loop-local timedSync.

Adds a regression test asserting a tool-triggered sync lands in syncMs and not
agentMs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jurgenwerk
jurgenwerk requested a review from a team June 18, 2026 08:49
@jurgenwerk
jurgenwerk merged commit 13518f7 into main Jun 19, 2026
27 checks passed
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