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
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ npm run dev:ui:package # Vite dev server resolving shared UI from
- Use `??` for nullish defaults (not `||`, which also triggers on `0`/`''`/`false`) and `?.` for safe property access
- Prefer optional properties (`foo?: T`) over `foo: T | undefined` when a key may legitimately be absent

**Comments / JSDoc**:
- Doc comments for functions and methods start with a third-person singular present-tense verb (`Creates a channel`, `Returns the driver`, `Appends an event`), not the imperative (`Create`, `Return`, `Append`). This applies to method/function descriptions; type, property, and parameter docs are not constrained this way.
Comment on lines +39 to +40
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.

praise: Strongly agree with formalising the "Linear IDs / milestone labels don't belong in code" guidance — these always rot. The carve-out for TODO(ENG-1234) against a tracked follow-up is exactly the right shape: it points at the issue tracker as the source of truth instead of trying to mirror it inside the comment.

The TODO(ENG-3034) markers added in curate-prompt-builder.ts and i-curate-executor.ts model the exception clause nicely; cleaning up the pre-existing M0-1 / Q2 / M5+ violations in src/shared/types/channel.ts (which is touched by this PR) would model the rule clause in the same change — see the comment on that file.

- Don't embed ephemeral project-planning references in code/doc comments — milestone labels (`M1`, `M5+`), open-question numbers (`Q7`), or Linear issue IDs. They rot as plans change and mislead later readers. State the durable *why* (e.g. "a local subprocess and a remote peer implement the same interface"), not the *when* (which milestone). Exception: a `TODO(ENG-1234)` pointing at a tracked follow-up issue is the correct way to mark known debt. Milestone/issue-linked rationale belongs in Linear and the byterover context tree, not in code.

**Testing (Strict TDD — MANDATORY)**:
- You MUST follow Test-Driven Development. This is non-negotiable.
- **Step 1 — Write failing tests FIRST**: Before writing ANY implementation code, write or update tests that describe the expected behavior. Do NOT write implementation and tests together or in reverse order.
Expand Down
29 changes: 29 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,33 @@ export default [
],
},
},
// Architecture boundary: src/server/core may depend only on abstractions.
// NOTE: infra is a SIBLING of core under src/server/, so a core→infra relative import
// (e.g. ../../../infra/foo.js) has NO "server" segment — the sibling-relative ../infra
// variants below are REQUIRED in addition to the **/server/infra/** form.
{
files: ['src/server/core/**/*.ts'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/server/infra/**', '../infra/**', '../../infra/**', '../../../infra/**', '../../../../infra/**'],
message:
'core must not import from server/infra. Depend on an interface in core/interfaces and let infra implement it (dependency inversion).',
},
Comment on lines +102 to +106
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.

nit (defensive): The sibling-relative patterns top out at ../../../../infra/** (4 ups). Current max depth in src/server/core/ is interfaces/<a>/<b>/file.ts../../../infra/ to reach src/server/infra/, so today there's one level of headroom. The day someone adds src/server/core/interfaces/channel/subdir/another/file.ts, the sibling-relative escape route reopens: a 5-up relative import like ../../../../../infra/foo.js won't match **/server/infra/** (no literal server/infra/ substring) and won't match ../../../../infra/** (one .. short).

Cheap fixes (either is fine):

  • Add ../../../../../infra/** and ../../../../../../infra/** to the list for headroom.
  • Or, since the rule message says "core must not import from server/infra", prefer a single pattern based on import/no-restricted-paths if available, which resolves the import to an absolute file path before matching — that closes the relative-import escape hatch by construction.

Not blocking — the rule does what it advertises at the present directory depth. Worth a follow-up before the channel subsystem nests deeper.

{
group: ['**/oclif/**', '../oclif/**', '../../oclif/**', '../../../oclif/**', '../../../../oclif/**'],
message: 'core must not import from oclif. Keep CLI wiring out of the domain/application core.',
},
{
group: ['**/tui/**', '../tui/**', '../../tui/**', '../../../tui/**', '../../../../tui/**'],
message: 'core must not import from tui. Keep UI out of the domain/application core.',
},
],
},
],
},
},
]
4 changes: 4 additions & 0 deletions src/server/core/domain/render/curate-prompt-builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// TODO(ENG-3034): relocate the HtmlWriteError type into core so this stops importing infra.
// eslint-disable-next-line no-restricted-imports
import type {HtmlWriteError} from '../../../infra/render/writer/html-writer.js'

// TODO(ENG-3034): inject ELEMENT_REGISTRY (or move it into core) so this stops importing infra.
// eslint-disable-next-line no-restricted-imports
import {ELEMENT_REGISTRY} from '../../../infra/render/elements/registry.js'
import {ELEMENT_NAMES} from './element-types.js'

Expand Down
84 changes: 84 additions & 0 deletions src/server/core/interfaces/channel/i-agent-driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type {ContentBlock, TurnEvent} from '../../../../shared/types/index.js'

/**
* Payload-only `TurnEvent`: a variant's fields WITHOUT the base coordination
* metadata (`channelId`, `turnId`, `deliveryId`, `memberHandle`, `emittedAt`,
* `seq`). A driver is oblivious to channel-side coordinates — the orchestrator
* stamps the base fields (including the gap-free monotonic `seq`) as it relays
* each payload into the transcript, so correct ordering can only be assigned by
* that single writer.
*
* Derived structurally from {@link TurnEvent} via a distributive conditional so
* the two can never drift: adding a new `TurnEvent` variant updates this type
* automatically. The omitted keys MUST mirror the `TurnEvent` base shape.
*/
export type TurnEventPayload = TurnEvent extends infer T
? T extends TurnEvent
? Omit<T, 'channelId' | 'deliveryId' | 'emittedAt' | 'memberHandle' | 'seq' | 'turnId'>
: never
: never
Comment on lines +3 to +19
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.

suggestion (robustness, non-blocking): TurnEventPayload is a brilliant derived type via distributive conditional, but the Omit list is a hand-maintained literal mirror of the TurnEventBaseShape keys defined in src/shared/types/channel.ts:241-248. The two cannot drift today, but nothing prevents them from drifting tomorrow — adding a base field to TurnEventBaseShape silently leaks it through every payload yielded by every IAgentDriver, and TypeScript will not flag it.

The doc comment acknowledges this ("The omitted keys MUST mirror the TurnEvent base shape") — that's an invariant a type check could enforce. Consider exporting a TurnEventBaseFields shape (or just the key-union type TurnEventBaseKey = 'channelId' | 'turnId' | ... derived from the schema) from shared/types/channel.ts and using Omit<T, TurnEventBaseKey> here. The cost is one shared key-union; the gain is "MUST mirror" becomes structurally enforced instead of comment-policed.

Not blocking for this PR, but worth a follow-up before there's more than one driver implementing the interface.


/** Arguments for a single prompt/turn dispatched to a driver. */
export type AgentDriverPromptArgs = {
/** Opaque per-turn metadata forwarded to the underlying agent (driver-defined). */
readonly meta?: Record<string, unknown>
/** Prompt content blocks for this turn. */
readonly prompt: ContentBlock[]
/** Channel turn this dispatch belongs to (correlation only; not echoed in payloads). */
readonly turnId: string
}

/** Lifecycle status of a single driver instance. */
export type AgentDriverStatus = 'errored' | 'idle' | 'stopped' | 'streaming'

/**
* Transport-agnostic contract for driving one agent and streaming its turn — the
* single most important seam in the channel subsystem. A local ACP subprocess
* and a remote A2A peer implement THIS SAME interface, so the orchestrator never
* knows or cares whether an agent is local or networked.
*
* Deliberately free of ACP vocabulary: no protocol version, no ACP capability
* snapshot, no ACP `initialize` handshake — those belong to the concrete ACP
* implementation, not to this contract.
*
* One instance serves one channel member. Spawn / teardown is the caller's
* concern via {@link IAgentDriver.start} / {@link IAgentDriver.stop}.
*/
export interface IAgentDriver {
/**
* Cancels in-flight work. With a `turnId`, cancels just that turn; without, it
* cancels whatever is currently streaming. Idempotent; later prompts still work.
*/
cancel(turnId?: string): Promise<void>

/** Stable channel-member handle this driver serves (e.g. `@claude`). */
readonly handle: string

/**
* Dispatches a prompt and stream the turn as it unfolds. Each yielded
* {@link TurnEventPayload} is a base-field-free slice; the orchestrator stamps
* `channelId` / `turnId` / `deliveryId` / `memberHandle` / `seq` / `emittedAt`
* before persisting and broadcasting. The iterator completes when the turn
* reaches a terminal state and may throw to signal a driver-level failure.
*/
prompt(args: AgentDriverPromptArgs): AsyncIterableIterator<TurnEventPayload>
Comment on lines +57 to +64
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.

nit (style): Per the new CLAUDE.md "third-person singular present-tense" rule, the leading verb is conjugated correctly but the coordinated second verb is not:

"Dispatches a prompt and stream the turn as it unfolds."

Should be "streams" to match "dispatches".

Suggested change
/**
* Dispatches a prompt and stream the turn as it unfolds. Each yielded
* {@link TurnEventPayload} is a base-field-free slice; the orchestrator stamps
* `channelId` / `turnId` / `deliveryId` / `memberHandle` / `seq` / `emittedAt`
* before persisting and broadcasting. The iterator completes when the turn
* reaches a terminal state and may throw to signal a driver-level failure.
*/
prompt(args: AgentDriverPromptArgs): AsyncIterableIterator<TurnEventPayload>
/**
* Dispatches a prompt and streams the turn as it unfolds. Each yielded
* {@link TurnEventPayload} is a base-field-free slice; the orchestrator stamps
* `channelId` / `turnId` / `deliveryId` / `memberHandle` / `seq` / `emittedAt`
* before persisting and broadcasting. The iterator completes when the turn
* reaches a terminal state and may throw to signal a driver-level failure.
*/
prompt(args: AgentDriverPromptArgs): AsyncIterableIterator<TurnEventPayload>


/**
* Resolves a pending permission request the driver surfaced (via a
* `permission_request` payload). `response` is opaque here; the concrete driver
* interprets it.
*
* @param permissionRequestId - Id from the `permission_request` payload.
* @param response - Driver-defined decision payload.
*/
respondToPermission(permissionRequestId: string, response: unknown): Promise<void>

/** Brings the underlying session up (spawn / connect / handshake). Idempotent. */
start(): Promise<void>

/** Current lifecycle status. */
readonly status: AgentDriverStatus

/** Tears the session down and release resources. Idempotent. */
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.

nit (style): Same coordinated-verb mismatch as on prompt:

"Tears the session down and release resources."

Should be "releases" to match "tears".

Suggested change
/** Tears the session down and release resources. Idempotent. */
/** Tears the session down and releases resources. Idempotent. */

stop(): Promise<void>
}
22 changes: 22 additions & 0 deletions src/server/core/interfaces/channel/i-channel-broadcaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Outbound fan-out port used by the channel orchestrator.
*
* The orchestrator lives in the core layer and MUST NOT depend on the transport
* server directly — the transport may later be swapped for a cross-machine
* relay. The infra adapter binds this port to the real transport
* server, delegating to `broadcastTo('channel:<channelId>', event, data)`.
*
* Fire-and-forget: there is no awaitable delivery guarantee. Subscribers are the
* clients (TUI / webui / cli) joined to the `channel:<channelId>` room.
*/
export interface IChannelBroadcaster {
/**
* Emits `event` (with payload `data`) to every client subscribed to
* `channel:<channelId>`.
*
* @param channelId - Channel whose subscribers receive the event.
* @param event - Transport event name (e.g. `channel:turn-event`).
* @param data - Event payload; shape is event-specific.
*/
broadcastToChannel<T>(channelId: string, event: string, data: T): void
}
58 changes: 58 additions & 0 deletions src/server/core/interfaces/channel/i-channel-orchestrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {Channel, ContentBlock, Turn} from '../../../../shared/types/index.js'

/** Cancel an in-flight turn. */
export type CancelTurnArgs = {
readonly channelId: string
readonly turnId: string
}

/** Create a new channel. */
export type CreateChannelArgs = {
readonly channelId: string
readonly title?: string
}

/** Post a new turn (a user or local-agent prompt) into a channel. */
export type PostTurnArgs = {
readonly channelId: string
/** Optional client-supplied idempotency key for safe retries. */
readonly idempotencyKey?: string
/** Handles explicitly mentioned in the prompt; drives dispatch. */
readonly mentions?: string[]
readonly promptBlocks: ContentBlock[]
}

/**
* Thin application-facing coordinator for the channel subsystem. Deliberately
* smaller than the POC's god-object: it exposes only the read + lifecycle
* operations the foundation needs. The interface is EXTENDED (never replaced) as
* the subsystem grows — member management, streaming dispatch, quorum fan-out,
* and permission decisions land additively once their domain types and consumers
* exist.
*
* Implementations validate inputs against the transport request schemas before
* these methods run (the handler does this), so orchestrator methods can trust
* their arguments.
*/
export interface IChannelOrchestrator {
/** Cancels an in-flight turn and its deliveries. */
cancelTurn(args: CancelTurnArgs): Promise<void>

/** Creates a new channel and returns its record. */
createChannel(args: CreateChannelArgs): Promise<Channel>

/** Reads one channel, or `undefined` when it does not exist. */
getChannel(channelId: string): Promise<Channel | undefined>

/** Reads one turn within a channel, or `undefined` when it does not exist. */
getTurn(channelId: string, turnId: string): Promise<Turn | undefined>

/** Lists all channels. */
listChannels(): Promise<Channel[]>

/** Lists the turns of a channel; ordering is defined by the adapter. */
listTurns(channelId: string): Promise<Turn[]>

/** Posts a new turn into a channel and returns the created turn record. */
postTurn(args: PostTurnArgs): Promise<Turn>
}
63 changes: 63 additions & 0 deletions src/server/core/interfaces/channel/i-channel-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type {Channel, ChannelMember, ChannelSettings} from '../../../../shared/types/index.js'

/** Add a full member record to a channel. */
export type ChannelStoreAddMemberArgs = {
readonly channelId: string
readonly member: ChannelMember
}

/** Create a new channel record. */
export type ChannelStoreCreateArgs = {
readonly channelId: string
readonly settings?: ChannelSettings
readonly title?: string
}

/** Remove a member from a channel by handle. */
export type ChannelStoreRemoveMemberArgs = {
readonly channelId: string
readonly memberHandle: string
}

/** Apply a metadata patch to an existing channel (title / settings / archive). */
export type ChannelStoreUpdateArgs = {
readonly archivedAt?: string
readonly channelId: string
readonly settings?: ChannelSettings
readonly title?: string
}

/**
* Persistence port for channel + member METADATA only. It owns the durable
* {@link Channel} record and the full {@link ChannelMember} records behind it;
* `Channel.members` remains the summarised projection the adapter derives from
* those records.
*
* It deliberately does NOT store transcripts (turns / events) — that is
* `ITranscriptStore`'s responsibility. Splitting the two lets transcript
* retention / GC evolve independently, without touching channel metadata. Member
* CRUD (`addMember` / `removeMember` / `listMembers`) is the seam invite /
* uninvite drives.
*/
export interface IChannelStore {
/** Persists a new member record under a channel. */
addMember(args: ChannelStoreAddMemberArgs): Promise<void>
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.

question (spec gap): This contract is silent on collision semantics. createChannel explicitly states "rejects when channelId already exists" (line 46) and removeMember explicitly states "No-op when absent" (line 58) — both excellent. But addMember does not say what happens if a member with the same handle already exists under that channelId. Three behaviors are all defensible:

  1. Reject (invariant: handles are unique per channel; caller must check)
  2. Overwrite (last-write-wins; idempotent re-invite)
  3. No-op (first-write-wins; idempotent re-invite)

Each implies a different invite/re-invite flow. Picking one and documenting it here is cheaper than discovering the divergence between two infra adapters later.

Same kind of gap on IChannelOrchestrator.cancelTurn (i-channel-orchestrator.ts:39) — what's the behavior when turnId doesn't exist or is already in a terminal state?


/** Creates and persists a new channel; rejects when `channelId` already exists. */
createChannel(args: ChannelStoreCreateArgs): Promise<Channel>

/** Lists all channels (summary view). */
listChannels(): Promise<Channel[]>

/** Lists the full member records of a channel. */
listMembers(channelId: string): Promise<ChannelMember[]>

/** Reads one channel record, or `undefined` when it does not exist. */
readChannel(channelId: string): Promise<Channel | undefined>

/** Removes a member record from a channel by handle. No-op when absent. */
removeMember(args: ChannelStoreRemoveMemberArgs): Promise<void>

/** Applies a metadata patch to an existing channel; returns the updated record. */
updateChannel(args: ChannelStoreUpdateArgs): Promise<Channel>
}
46 changes: 46 additions & 0 deletions src/server/core/interfaces/channel/i-driver-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type {IAgentDriver} from './i-agent-driver.js'

/** Composite key identifying one member's driver slot within a channel. */
export type DriverPoolKey = {
readonly channelId: string
readonly memberHandle: string
}

/** Look up the driver registered for a `{channelId, memberHandle}`. */
export type DriverPoolAcquireArgs = DriverPoolKey

/** Register an already-started driver under its `{channelId, memberHandle}` key. */
export type DriverPoolRegisterArgs = DriverPoolKey & {
readonly driver: IAgentDriver
}

/** Release (stop + evict) the driver for a `{channelId, memberHandle}`. */
export type DriverPoolReleaseArgs = DriverPoolKey

/**
* Keyed registry of live {@link IAgentDriver} instances — one slot per
* `{channelId, memberHandle}`. The pool is pure lifecycle bookkeeping: it does
* NOT spawn drivers. The orchestrator constructs + starts a driver and hands it
* over via {@link IDriverPool.register}; `acquire` is a non-blocking lookup; the
* `release*` methods call `driver.stop()` so subprocess agents never leak.
*
* Pre-warming is intentionally absent: it has no consumer at this layer yet, and
* the pool's "never constructs drivers" invariant means warming belongs to the
* orchestrator (which owns driver construction) when a consumer needs it.
*/
export interface IDriverPool {
/** Returns the registered driver for the key, or `undefined` if none. Never spawns. */
acquire(args: DriverPoolAcquireArgs): IAgentDriver | undefined

/** Stores an already-started driver under its key, replacing any prior slot. */
register(args: DriverPoolRegisterArgs): void

/** Stops and evict the driver for a single key. No-op when absent. */
release(args: DriverPoolReleaseArgs): Promise<void>

/** Stops and evict every driver in the pool (daemon shutdown). */
releaseAll(): Promise<void>

/** Stops and evict all drivers belonging to a channel (channel close / archive). */
releaseChannel(channelId: string): Promise<void>
Comment on lines +38 to +45
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.

nit (style): Per the new CLAUDE.md rule, doc comments on functions/methods should start with a third-person singular present-tense verb. These three docs start with a third-person verb but then coordinate with a bare imperative (evict) where the conjugated form (evicts) is needed:

  • line 38: "Stops and evict the driver for a single key.""Stops and evicts the driver for a single key."
  • line 41: "Stops and evict every driver in the pool""Stops and evicts every driver in the pool"
  • line 44: "Stops and evict all drivers belonging to a channel""Stops and evicts all drivers belonging to a channel"
Suggested change
/** Stops and evict the driver for a single key. No-op when absent. */
release(args: DriverPoolReleaseArgs): Promise<void>
/** Stops and evict every driver in the pool (daemon shutdown). */
releaseAll(): Promise<void>
/** Stops and evict all drivers belonging to a channel (channel close / archive). */
releaseChannel(channelId: string): Promise<void>
/** Stops and evicts the driver for a single key. No-op when absent. */
release(args: DriverPoolReleaseArgs): Promise<void>
/** Stops and evicts every driver in the pool (daemon shutdown). */
releaseAll(): Promise<void>
/** Stops and evicts all drivers belonging to a channel (channel close / archive). */
releaseChannel(channelId: string): Promise<void>

}
38 changes: 38 additions & 0 deletions src/server/core/interfaces/channel/i-transcript-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {TurnEvent} from '../../../../shared/types/index.js'

/**
* Append one fully-stamped transcript event. Location-agnostic by design: the
* caller supplies `projectRoot` + `channelId` + `turnId`; the contract says
* nothing about files, NDJSON, per-turn indexes, or whether storage is
* per-project vs global. The path / retention policy is the adapter's concern.
*/
export type AppendTurnEventArgs = {
readonly channelId: string
/** Fully-stamped event — base fields already populated by the orchestrator. */
readonly event: TurnEvent
/** Project the channel lives under; the adapter resolves storage location from it. */
readonly projectRoot: string
readonly turnId: string
}

/** Read back every persisted event for one turn, in `seq` order. */
export type ReadTurnEventsArgs = {
readonly channelId: string
readonly projectRoot: string
readonly turnId: string
}

/**
* Append-and-read port for a turn's event log. Deliberately minimal: it does NOT
* model turn / channel metadata (`IChannelStore` owns that), nor does it leak any
* storage mechanism (no file handles, NDJSON, index, or GC in the contract).
* Split out from the POC's combined store so transcript retention can evolve
* without touching channel metadata.
*/
export interface ITranscriptStore {
/** Appends one stamped `TurnEvent` to a turn's log. */
appendTurnEvent(args: AppendTurnEventArgs): Promise<void>
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.

question (spec gap): The contract doesn't say what appendTurnEvent does when an event with an already-persisted seq arrives, or when events are appended in non-monotonic seq order. Given the architectural note in i-agent-driver.ts:3-9 that the orchestrator is the sole writer of seq and treats it as gap-free monotonic, two defensible behaviors:

  1. Trust the orchestrator — append as-given; out-of-order is the caller's bug.
  2. Defensive — reject on gap / duplicate / regression so a buggy orchestrator can't poison the transcript.

Either is fine, but stating it here is cheap and pins down what an adapter must implement. Same kind of clarity that createChannel already provides on IChannelStore.


/** Reads all persisted events for a turn, ordered by `seq` ascending. */
readTurnEvents(args: ReadTurnEventsArgs): Promise<TurnEvent[]>
}
2 changes: 2 additions & 0 deletions src/server/core/interfaces/executor/i-curate-executor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type {ICipherAgent} from '../../../../agent/core/interfaces/i-cipher-agent.js'
// TODO(ENG-3034): relocate the HtmlWriteError type into core so this stops importing infra.
// eslint-disable-next-line no-restricted-imports
import type {HtmlWriteError} from '../../../infra/render/writer/html-writer.js'
import type {CurateUsageRecord} from '../../domain/entities/curate-log-entry.js'
import type {IUsageAggregator} from '../telemetry/i-usage-aggregator.js'
Expand Down
Loading
Loading