Skip to content

core: PluginStartupResult.startTime carries an elapsed duration, and the spec contract for the same result declares it as duration #15820

Description

@os-sales

Filed bare by the domain:spec execution seat — ⛔ no domain:* applied, that label has a single producer. The fix lands in packages/core, not in packages/spec, so routing is triage's call.

All readings taken against origin/main at 2026-09-05T09:09Z (⛔ not a working tree).

The defect

packages/core/src/plugin-loader.ts:91-95 declares:

export interface PluginStartupResult {
    success: boolean;
    pluginName: string;
    startTime?: number;
    ...
}

packages/core/src/kernel.ts assigns an elapsed duration into that field, on both the success and the failure path:

// kernel.ts:674
const startTime = Date.now();
...
// kernel.ts:685, 697
const duration = Date.now() - startTime;
...
// kernel.ts:694 (success), :704 (failure)
startTime: duration,

So a field named startTime never holds a start time. It holds milliseconds elapsed. This is not an ambiguous name that a reader has to look up — the name asserts the opposite of the value, which is the one failure mode a unit convention cannot rescue, because a reader who correctly assumes startTime is an instant will do arithmetic on it and get a number near the epoch.

It propagates one hop further: kernel.ts:69 private pluginStartTimes: Map<string, number> is fed the same duration at :687, and the map is handed out at :540 by getPluginMetrics().

What makes this more than a naming nit

The same file family already has the right name for this measure, and uses it correctly. Twelve lines above the defect, plugin-loader.ts:85 declares loadTime?: number, and :182 assigns loadTime: Date.now() - startTime — the identical computation, under a name that does not lie.

And the spec contract for this very result does not have the key at all. packages/spec/src/kernel/startup-orchestrator.zod.ts — the contract PluginStartupResult maps to, per packages/spec/api-surface/contracts.json:218 and the PluginStartupResult → kernel/startup-orchestrator.zod rows in the package CHANGELOGs — declares:

:149   duration: z.number().min(0).describe('Time taken to start the plugin in milliseconds')
:196   totalDuration: z.number().min(0).describe('Total time taken for all plugins in milliseconds')

The contract says duration. The implementation ships startTime holding that same duration. This is a declared ≠ enforced divergence between packages/core and the packages/spec contract it implements, and the divergence is the root cause — the name did not drift on its own, it drifted away from a contract that was already correct.

Blast radius, measured

  • PluginStartupResult is exported from packages/core/src/plugin-loader.ts, so startTime is public type surface.
  • Producers/consumers of the value, whole-repo (git grep over packages apps examples): kernel.ts:371 is the only call site of startPluginWithTimeout, and there are no readers of getPluginMetrics() or pluginStartTimes outside packages/core/src/kernel.ts. That zero is reverse-checked — the same search returns four live hits for PluginStartupResult itself, so the search reaches.
  • Practical effect: the rename is contained inside packages/core, with the public type surface being the part that needs an ADR-0087 disposition rather than a silent edit.

Suggested shape (not a dispatch)

Align the implementation with the contract it already has: the elapsed measure is duration (or a unit-bearing name, if the convention landing in #14478 settles on one), the tombstone/retirement handled per ADR-0087 for the exported type, and pluginStartTimes / getPluginMetrics() renamed to say what they return.

Relationship to #14478 — read this before merging the two

This was surfaced while verifying the #14478 stack, and it is adjacent to that epic, not inside it:

Hence: separate card, bare, for triage to route.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions