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
456 changes: 456 additions & 0 deletions hypaware-core/smoke/flows/backfill_openclaw_fixture.js

Large diffs are not rendered by default.

58 changes: 37 additions & 21 deletions llp/0172-openclaw-two-lane-capture.design.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,16 +402,19 @@ already enforces.

### 4.3 Narrowing `runProvider`'s context type, not widening the daemon's

`runProvider()` and `resolveOwnersForRun()` (both in
`src/core/commands/backfill.js`) only ever read `ctx.backfills`,
`ctx.backfillMaterializers`, `ctx.env`, `ctx.storage`, and (via
`resolveOwnersForRun`) `ctx.config` for plugin-configured resolution. None
of `CommandRunContext`'s other fields (`stdout`, `commands`, `verbs`,
`skills`, `agents`, `sources`, `sinks`, `initPresets`, `capabilities`,
`plugins`, `cwd`) are touched anywhere in this call path. Rather than force
the daemon to assemble a full, mostly-unused `CommandRunContext` just to
call `runBackfillProvider`, this design narrows the type both functions
declare their `ctx` parameter as, to a new, smaller type:
`runProvider()`, `resolveOwnersForRun()`, and the materialize/write/flush
helpers they call (all in `src/core/commands/backfill.js`) only ever read
`ctx.backfills`, `ctx.backfillMaterializers`, `ctx.env`, `ctx.storage`,
`ctx.query` (`writeRows`/`flushDataset` resolve a dataset's registered
table path through it before a row can be committed or a partition
flushed), and (via `resolveOwnersForRun`) `ctx.config` for
plugin-configured resolution. None of `CommandRunContext`'s other fields
(`stdout`, `commands`, `verbs`, `skills`, `agents`, `sources`, `sinks`,
`initPresets`, `capabilities`, `plugins`, `cwd`) are touched anywhere in
this call path. Rather than force the daemon to assemble a full,
mostly-unused `CommandRunContext` just to call `runBackfillProvider`, this
design narrows the type both functions declare their `ctx` parameter as,
to a new, smaller type:

```ts
// A structural subset of CommandRunContext; every existing
Expand All @@ -422,34 +425,47 @@ interface BackfillRunnerContext {
env: NodeJS.ProcessEnv
config: HypAwareV2Config
storage: QueryStorageService
query: QueryRegistry
backfills: BackfillRegistry
backfillMaterializers: BackfillMaterializerRegistry
}
```

`runBackfillProvider`, `runProvider`, and `resolveOwnersForRun`'s `ctx`
parameters change from `CommandRunContext` to `BackfillRunnerContext`. This
is a pure narrowing: `CommandRunContext` is structurally a superset, so no
existing caller's argument stops satisfying the (now smaller) parameter
type. The daemon can now build a `BackfillRunnerContext` object out of
fields `boot.runtime` already carries (`env`, `config`, `storage`,
`backfills`, `backfillMaterializers`, the latter two already referenced at
`src/core/runtime/activation.js` lines 96 and 152) without touching
`CommandRunContext` or constructing stub versions of fields it doesn't need.
`runBackfillProvider`, `runProvider`, `resolveOwnersForRun`, and the
materialize/write/flush helpers' `ctx` parameters change from
`CommandRunContext` to `BackfillRunnerContext`. This is a pure narrowing:
`CommandRunContext` is structurally a superset, so no existing caller's
argument stops satisfying the (now smaller) parameter type. The daemon can
now build a `BackfillRunnerContext` object out of fields `boot.runtime`
already carries (`env`, `config`, `storage`, `query`, `backfills`,
`backfillMaterializers`, all already referenced at
`src/core/runtime/activation.js`) without touching `CommandRunContext` or
constructing stub versions of fields it doesn't need.

`query` was not part of this list until LLP 0173 T12's hermetic smoke (the
first caller to drive a real, non-dry-run write through the sweep
driver rather than a mocked `runBackfill` seam) found `writeRows` and
`flushDataset` crash on `ctx.query.getDataset` when the daemon-built
`BackfillRunnerContext` reached them: the field really is on this call
path, this design's original field enumeration just missed it because
T9's own tests never exercised a real write. `BackfillSweepDriverOptions`
(Section 4.4) and the daemon's `createBackfillSweepDriver(...)` call
(`src/core/daemon/runtime.js`) both require `query` now for the same
reason.

### 4.4 Wiring the tick

New file: `src/core/daemon/backfill_sweep.js`, exporting
`createBackfillSweepDriver({backfills, backfillMaterializers, env, config,
storage})` with one method, `tick({now})`:
storage, query})` with one method, `tick({now})`:

```js
function tick({ now }) {
for (const provider of backfills.list()) {
if (!provider.sweep) continue
if (!cronMatches(provider.sweep.cron, now)) continue
void runBackfillProvider({
ctx: { env, config, storage, backfills, backfillMaterializers },
ctx: { env, config, storage, query, backfills, backfillMaterializers },
provider: provider.name,
dryRun: false,
devRunId: `sweep-${provider.name}-${now.getTime()}`,
Expand Down
22 changes: 6 additions & 16 deletions src/core/commands/backfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,17 +476,7 @@ async function runProvider(args) {
const rows = await materializeItem({
materializer,
item: yielded,
// materializeItem/writeRows/flushDataset are outside T7's
// narrowing (they read ctx.query, which BackfillRunnerContext
// does not carry); today's only real callers (hyp backfill's
// CLI path, the onboarding finale) still hand runProvider a
// full CommandRunContext, so this is a type-level widening
// back to the shape those three helpers already require, not
// a behavior change.
// @ref LLP 0172#lane-b-sweep [constrained-by]: runProvider's
// own ctx param narrows to BackfillRunnerContext; its
// still-CommandRunContext-typed callees need this back-cast
ctx: /** @type {CommandRunContext} */ (ctx),
ctx,
devRunId,
provider: provider.name,
log,
Expand All @@ -501,7 +491,7 @@ async function runProvider(args) {
dataset: yielded.dataset,
provider: provider.name,
devRunId,
ctx: /** @type {CommandRunContext} */ (ctx),
ctx,
log,
})
result.rows_written += written.rowsWritten
Expand All @@ -516,7 +506,7 @@ async function runProvider(args) {
dataset,
provider: provider.name,
devRunId,
ctx: /** @type {CommandRunContext} */ (ctx),
ctx,
log,
})
}
Expand Down Expand Up @@ -562,7 +552,7 @@ async function runProvider(args) {
* @param {{
* materializer: BackfillMaterializerContribution,
* item: BackfillItem,
* ctx: CommandRunContext,
* ctx: BackfillRunnerContext,
* devRunId: string,
* provider: string,
* log: PluginLogger,
Expand Down Expand Up @@ -607,7 +597,7 @@ async function materializeItem(args) {
* dataset: string,
* provider: string,
* devRunId: string,
* ctx: CommandRunContext,
* ctx: BackfillRunnerContext,
* log: PluginLogger,
* }} args
* @returns {Promise<{ rowsWritten: number, status: 'ok' | 'failed', error?: string }>}
Expand Down Expand Up @@ -662,7 +652,7 @@ async function writeRows(args) {
* dataset: string,
* provider: string,
* devRunId: string,
* ctx: CommandRunContext,
* ctx: BackfillRunnerContext,
* log: PluginLogger,
* }} args
*/
Expand Down
12 changes: 9 additions & 3 deletions src/core/commands/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
BackfillMaterializerRegistry,
BackfillRegistry,
HypAwareV2Config,
QueryRegistry,
QueryStorageService,
} from '../../../hypaware-plugin-kernel-types.js'

Expand Down Expand Up @@ -33,18 +34,23 @@ export interface PolicyHumanVocabulary {
}

// A structural subset of `CommandRunContext`: exactly the fields
// `runBackfillProvider`, `runProvider`, and `resolveOwnersForRun`
// `runBackfillProvider`, `runProvider`, `resolveOwnersForRun`, and the
// materialize/write/flush helpers they call
// (`src/core/commands/backfill.js`) read off `ctx`. Every existing
// `CommandRunContext` already satisfies it, so `hyp backfill`'s CLI path
// and the onboarding finale's call keep typechecking unchanged; the
// daemon sweep driver (LLP 0173 T9) can build one directly out of
// `boot.runtime` fields without assembling a full, mostly-unused
// `CommandRunContext`.
// @ref LLP 0172#lane-b-sweep [implements]: the narrowed context type `runBackfillProvider`, `runProvider`, and `resolveOwnersForRun` declare, so the daemon sweep driver can build one without a full `CommandRunContext`
// `CommandRunContext`. `query` was missing from this list until LLP 0173
// T12's smoke (the first caller to drive a real, non-mocked write through
// the sweep driver) found `writeRows`/`flushDataset` crash on
// `ctx.query.getDataset` when a sweep-built `ctx` reached them.
// @ref LLP 0172#lane-b-sweep [implements]: the narrowed context type `runBackfillProvider`, `runProvider`, `resolveOwnersForRun`, and the materialize/write/flush helpers declare, so the daemon sweep driver can build one without a full `CommandRunContext`
export interface BackfillRunnerContext {
env: NodeJS.ProcessEnv
config: HypAwareV2Config
storage: QueryStorageService
query: QueryRegistry
backfills: BackfillRegistry
backfillMaterializers: BackfillMaterializerRegistry
}
Expand Down
5 changes: 3 additions & 2 deletions src/core/daemon/backfill_sweep.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ const SWEEP_OPERATION = 'backfill.sweep'
* @returns {BackfillSweepDriver}
*/
export function createBackfillSweepDriver(opts) {
const { backfills, backfillMaterializers, env, config, storage } = opts
const { backfills, backfillMaterializers, env, config, storage, query } = opts
if (!backfills) throw new Error('createBackfillSweepDriver: backfills required')
if (!backfillMaterializers) throw new Error('createBackfillSweepDriver: backfillMaterializers required')
if (!storage) throw new Error('createBackfillSweepDriver: storage required')
if (!query) throw new Error('createBackfillSweepDriver: query required')
const runBackfill = opts.runBackfill ?? runBackfillProvider
const log = getLogger('backfill-sweep')

Expand Down Expand Up @@ -80,7 +81,7 @@ export function createBackfillSweepDriver(opts) {
// Fire-and-forget, with both settlements handled: `void` here means "not
// awaited", never "not observed".
void runBackfill({
ctx: { env, config: config ?? { version: 2 }, storage, backfills, backfillMaterializers },
ctx: { env, config: config ?? { version: 2 }, storage, query, backfills, backfillMaterializers },
provider: provider.name,
dryRun: false,
devRunId,
Expand Down
1 change: 1 addition & 0 deletions src/core/daemon/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ export async function runDaemon(opts = {}) {
backfillMaterializers: boot.runtime.backfillMaterializers,
env,
storage: boot.runtime.storage,
query: boot.runtime.query,
config: boot.config ?? undefined,
})

Expand Down
8 changes: 8 additions & 0 deletions src/core/daemon/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,14 @@ export interface BackfillSweepDriverOptions {
backfillMaterializers: BackfillMaterializerRegistry
env: NodeJS.ProcessEnv
storage: QueryStorageService
/**
* Dataset registry. `runBackfillProvider`'s write/flush path
* (`writeRows`/`flushDataset` in `src/core/commands/backfill.js`) resolves
* a dataset's registered table path through this before it can commit a
* row, so a fired sweep run needs it on `BackfillRunnerContext` exactly
* like `hyp backfill`'s CLI path already gets it from `CommandRunContext`.
*/
query: QueryRegistry
/** The daemon's effective config; absent on a host with no readable document. */
config?: HypAwareV2Config
/** Test seam: defaults to `runBackfillProvider`. */
Expand Down
9 changes: 9 additions & 0 deletions test/core/daemon-backfill-sweep.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function driverFor(args) {
backfillMaterializers: /** @type {any} */ ({ register() {}, get: () => undefined, list: () => [] }),
env: /** @type {any} */ ({ HYP_HOME: '/nonexistent-home' }),
storage: /** @type {any} */ ({ cacheRoot: '/nonexistent-cache' }),
query: /** @type {any} */ ({ getDataset: () => undefined }),
config: args.config,
runBackfill: args.runBackfill,
})
Expand Down Expand Up @@ -117,11 +118,13 @@ test('the fired run gets the narrowed runner context, built from the daemon runt
const backfillMaterializers = /** @type {any} */ ({ register() {}, get: () => undefined, list: () => [] })
const env = /** @type {any} */ ({ HYP_HOME: '/nonexistent-home' })
const storage = /** @type {any} */ ({ cacheRoot: '/nonexistent-cache' })
const query = /** @type {any} */ ({ getDataset: () => undefined })
const driver = createBackfillSweepDriver({
backfills,
backfillMaterializers,
env,
storage,
query,
config: /** @type {any} */ (config),
runBackfill: async (args) => { seen = args.ctx; return OK },
})
Expand All @@ -130,6 +133,7 @@ test('the fired run gets the narrowed runner context, built from the daemon runt

assert.equal(seen.env, env)
assert.equal(seen.storage, storage)
assert.equal(seen.query, query)
assert.equal(seen.config, config)
assert.equal(seen.backfills, backfills)
assert.equal(seen.backfillMaterializers, backfillMaterializers)
Expand Down Expand Up @@ -206,6 +210,7 @@ test('createBackfillSweepDriver refuses to build without the registries it fires
backfillMaterializers: /** @type {any} */ ({ register() {}, get: () => undefined, list: () => [] }),
env: /** @type {any} */ ({}),
storage: /** @type {any} */ ({ cacheRoot: '/nonexistent-cache' }),
query: /** @type {any} */ ({ getDataset: () => undefined }),
}
assert.throws(
() => createBackfillSweepDriver(/** @type {any} */ ({ ...ok, backfills: undefined })),
Expand All @@ -219,4 +224,8 @@ test('createBackfillSweepDriver refuses to build without the registries it fires
() => createBackfillSweepDriver(/** @type {any} */ ({ ...ok, storage: undefined })),
/storage required/
)
assert.throws(
() => createBackfillSweepDriver(/** @type {any} */ ({ ...ok, query: undefined })),
/query required/
)
})
Loading