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
12 changes: 10 additions & 2 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,14 +740,22 @@ export async function reportServiceServing(
deps: Parameters<typeof confirmServiceServing>[0] = {},
): Promise<void> {
const healthBudgetMs = deps.timeoutMs ?? serviceInstallHealthMs();
// Timed here rather than reported from the budget. confirmServiceServing knocks once
// more after a grace sleep whenever it waited at all, so the real wait is the budget
// plus that grace — and printing the budget states a number the run did not spend.
// What the reader is deciding is whether the service was still coming up, which is a
// judgement about elapsed time (#3009).
const now = deps.now ?? Date.now;
const startedAt = now();
const serving = await confirmServiceServing({ ...deps, timeoutMs: healthBudgetMs });
const waitedMs = Math.max(0, now() - startedAt);
Comment on lines +748 to +751

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 Measure elapsed service time with a monotonic clock

When the system clock is corrected while the 20–45s probe runs, such as after resume or initial time synchronization, Date.now() can jump. The warning then reports the clock correction rather than elapsed time—a forward jump can produce “after 3600s” almost immediately, while a backward jump can hide an hour actually spent—defeating the purpose of this fix. Measure the displayed duration with a monotonic clock such as performance.now(), while retaining an injectable clock for tests.

Useful? React with 👍 / 👎.

if (serving.ok) {
console.log(`✅ opencodex service ${verb} and serving on port ${serving.port}.`);
return;
}
console.error(
`⚠️ Service ${verb}, but no proxy answered on port ${serving.port} within `
+ `${Math.trunc(healthBudgetMs / 1000)}s.\n`
`⚠️ Service ${verb}, but no proxy answered on port ${serving.port} after `
+ `${Math.round(waitedMs / 1000)}s.\n`
Comment on lines 756 to +758

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 Update the documented service warning

A normal non-Windows failure now renders after 21s because the 20s budget is followed by the 500ms grace and rounded, but the exact CLI transcripts in docs-site/src/content/docs/reference/cli/lifecycle.md and the French, Turkish, and Traditional Chinese translations still show within 20s. Update those snippets so the user-facing reference matches the output introduced here and the translations remain consistent.

AGENTS.md reference: AGENTS.md:L343-L344

Useful? React with 👍 / 👎.

+ ` The manager registered the job; that is not the same as serving.\n`
+ ` Log: ${serviceLogPath()}\n`
+ ` Meanwhile: ocx start (serves in the foreground)`,
Expand Down
39 changes: 36 additions & 3 deletions tests/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3344,7 +3344,13 @@ describe("service serving confirmation", () => {
expect(serviceInstallHealthMs("darwin")).toBe(SERVICE_INSTALL_HEALTH_MS);
});

test("reports the effective Windows failure budget", async () => {
// The failure line reports what the run actually spent, not what it was allowed to.
// With the Windows budget the loop exits at 45s and the post-deadline grace knock
// adds its 500ms sleep, so the real wait is 45.5s. Reporting the budget printed 45s
// for a 45.5s wait -- a small gap here, but the same expression understates every
// future grace the loop grows, and the reader is using this number to judge whether
// the service was still coming up (#3009).
test("reports the wait it actually spent, grace knock included", async () => {
const errors: string[] = [];
const previousError = console.error;
const previousExitCode = process.exitCode;
Expand All @@ -3358,8 +3364,35 @@ describe("service serving confirmation", () => {
now: () => now,
timeoutMs: SERVICE_INSTALL_HEALTH_WINDOWS_MS,
});
expect(errors.join("\n")).toContain("within 45s");
expect(errors.join("\n")).not.toContain("within 20s");
expect(now).toBe(SERVICE_INSTALL_HEALTH_WINDOWS_MS + 500);
expect(errors.join("\n")).toContain("after 46s");
expect(errors.join("\n")).not.toContain("45s");
expect(errors.join("\n")).not.toContain("20s");
} finally {
console.error = previousError;
process.exitCode = previousExitCode ?? 0;
}
});

// A caller that asked not to wait must not be told it waited: with a zero budget
// confirmServiceServing takes its single probe and skips the grace entirely, so the
// reported wait is 0 rather than the budget.
test("reports no wait when the caller asked not to wait", async () => {
const errors: string[] = [];
const previousError = console.error;
const previousExitCode = process.exitCode;
let now = 0;
console.error = (...values: unknown[]) => { errors.push(values.join(" ")); };
try {
await reportServiceServing("started", {
port: 10100,
probe: async () => false,
sleep: async ms => { now += ms; },
now: () => now,
timeoutMs: 0,
});
expect(now).toBe(0);
expect(errors.join("\n")).toContain("after 0s");
} finally {
console.error = previousError;
process.exitCode = previousExitCode ?? 0;
Expand Down
Loading