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
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Worklog — #1398 publish job executions to the durable stream

Implementation lane: Codex · GPT-5.6 Sol · medium (`normal_implementation`).
Branch: `fix/1398-publish-job-executions-to-durable-stream`.
Baseline: `origin/main@01aa12b67`.

The approved `plan.md`, `plan-eval.md`, and `slices/research-1398.md` were read from the local
orchestration branch `chore/release-0.0.6-features-orchestration` because the fresh leaf branch was
created at the shared baseline before those orchestration-only commits existed. The locked plan was
not re-decided.

## Design

- **Public surface:** no new export and no `WorkerExecutionZodSchema` change. The existing
`WorkerExecutionRecord` type acknowledges the already-stored optional W3C trace headers.
- **Runtime state and identity:** every created, updated, and deleted execution mutation retains its
execution id, correlation id, `traceparent`, and optional `tracestate`.
- **Ports:** `WorkersStreamProducer` remains the stream port. The core mutation hook uses
`@netscript/telemetry/context` to make the stored W3C context ambient while that port publishes.
- **Plugin wiring:** the thin workers plugin installs the core-owned mutation hook only in the
worker-owning background entrypoints (`startWorkerProcess` and `startCombinedProcess`). The
scheduler does not own or mutate execution state.
- **Commit slices:** S1 installs and tests the hook/context join; S2 un-defers the two OTEL gates and
updates both registry tests; S3 records the one-pass live `scaffold.runtime` result.
- **Deferred scope:** stream rejection reasons (#1405), undeclared stream-core imports, and worker
execution schema changes remain out of scope.
- **Contributor path:** execution stream policy lives in
`packages/plugin-workers-core/src/streams/producer.ts`; process wiring lives in
`plugins/workers/bin/runtime.ts`; the adjacent stream and runtime tests are the regression map.

## S1 — mutation hook installation and trace-context join

Changed:

- `packages/plugin-workers-core/src/streams/producer.ts` wraps every mutation publication in the
execution's extracted W3C context.
- `plugins/workers/bin/runtime.ts` installs the hook in worker-only and combined background
processes; scheduler-only remains unchanged because its execution-state surface is reserved and
has no mutation methods.
- `packages/plugin-workers-core/tests/streams/workers-streams_test.ts` adds stored-context and
pre-span trace-id guards.
- `plugins/workers/tests/runtime/background-stream-hook_test.ts` spies the real combined entrypoint's
`KvExecutionState.setMutationHook` call while suppressing worker/scheduler loops.

Gate results before commit:

| Gate | Result |
| --- | --- |
| workers scoped check | PASS — 101 files, 0 findings |
| plugin-workers-core scoped check | PASS — 111 files, 0 findings |
| workers scoped lint | PASS — 101 files, 0 findings |
| workers scoped format | PASS — 101 files, 0 findings |
| plugin-workers-core package test | PASS — 27 passed, 0 failed |
| combined-runtime installation test | PASS — 1 passed, 0 failed |
| `quality:gate` | PASS — quality scan clean; doctrine `FAIL=0` on touched roots |
| explicit plugin-workers-core quality scan | PASS — 0 findings, 0 allowances |

Negative guard evidence:

- Removing only D3 made both trace-context tests fail (exit 1): ambient trace id was retained for
the override case and a new random trace id appeared for the pre-span case.
- Removing only the combined-process hook installation made the installation test fail (exit 1):
actual installation count `0`, expected `1`.

Reconcile note: issue #1398 is open with milestone `0.0.6` and already carries `type:fix`,
`area:plugins`, `area:telemetry`, `priority:p1`, and exactly one lifecycle label, `status:impl`.

## S2 — restore the two OTEL runtime gates

Changed in one slice, per PLAN-EVAL F2:

- Added `behavior.otel.stream-consumer` and `behavior.otel.traces` to `RUNTIME_GATES`, which also
carries them into the SQLite runtime tier.
- Emptied `SCAFFOLD_RUNTIME_DEFERRED_GATES` while preserving the shared explicit deferral surface.
- Flipped the main runtime-suite presence assertions to `true`.
- Rewrote the exact-deferral test to assert both runtime tiers have no #1398 deferral and execute
both gates.

First full-package check: RED. An empty `as const satisfies readonly DeferredGate[]` inferred the
constant's element as `never`, so existing runner-test fixture code could not read `issue` or
`reason`. Fixed by declaring the empty value against the explicit `readonly DeferredGate[]`
contract; no runtime behavior or assertion was relaxed.

Second full-package test: RED with one in-scope and one unrelated failure. The in-scope failure was
a third stale #1398 pin in `suite-runner_test.ts`, which still expected two deferred skipped steps;
it now asserts zero deferrals, steps, skipped summary entries, and deferred reporter events. This
test was not named in the approved plan or PLAN-EVAL F2 and is recorded as minor factual drift. The
unrelated `quickstart-command-drift_test.ts` failure came from the package task changing cwd while
the test reads `docs/site/quickstart.vto` relative to the repository root; verify it from root and
do not absorb that task/path defect into #1398.

Final S2 evidence:

| Gate | Result |
| --- | --- |
| suite-runner + registry focused tests | PASS — 27 passed, 0 failed |
| CLI E2E test tree from repository root | PASS — 152 passed, 0 failed |
| CLI E2E scoped check | PASS — 163 files, 0 findings |
| CLI E2E scoped lint | PASS — 163 files, 0 findings |
| CLI E2E scoped format | PASS — 163 files, 0 findings |
| explicit CLI E2E quality scan | PASS — 0 findings, 0 allowances |

Negative guard evidence: temporarily removing both OTEL gates from `RUNTIME_GATES` made the main
runtime presence test and the both-tier execution test fail (exit 1, actual `false`, expected
`true`).

## Final implementation gates

All required non-live gates passed on the S1+S2 head:

| Gate | Result |
| --- | --- |
| workers scoped check | PASS — 101 files, 0 findings |
| plugin-workers-core scoped check | PASS — 111 files, 0 findings |
| workers scoped lint | PASS — 101 files, 0 findings |
| workers scoped format | PASS — 101 files, 0 findings |
| plugin-workers-core package test | PASS — 27 passed, 0 failed |
| `quality:gate` | PASS — quality scan clean; doctrine `FAIL=0` on touched roots |
| explicit plugin-workers-core quality scan | PASS — 0 findings, 0 allowances |
| explicit CLI E2E quality scan | PASS — 0 findings, 0 allowances |

The configured quality scan covers `packages/cli/src` and `plugins`, but not
`packages/plugin-workers-core` or `packages/cli/e2e`; the two explicit scans above close that
coverage gap. No lockfile change, schema change, public export, lint suppression, `any`, unsafe
double cast, or `@ts-ignore` was added.

## S3 — one-pass live runtime evidence

Serialization was confirmed before launch: `/tmp/netscript-e2e-scaffold-runtime.lease` was absent,
no competing `e2e:cli`/`scaffold.runtime` process was running, and Docker reported zero containers.

The mandated single invocation was run once and was not retried:

```text
$ deno task e2e:cli run scaffold.runtime --cleanup --format pretty
> runtime.flow-b-fixture: Wire real Flow-B callback fixture
FAILED 10777ms
Command exited 1; expected 0.
error: Uncaught (in promise) Error: netscript generate plugins failed: Error: fetch failed
> cleanup.aspire-stop: Stop generated Aspire AppHost
PASSED 81ms
Summary: passed=33 failed=1 skipped=0
raw exit code: 1
```

This failure occurred before Aspire launch and before `behavior.otel.stream-consumer` or
`behavior.otel.traces`, so neither restored gate received a live verdict. The gate therefore does
not provide #1398's required end-to-end acceptance evidence.

Diagnostics followed the required order:

- Plugin doctor against the preserved generated fixture reported every configured plugin healthy;
its only warning was that no AppHost was running, expected because the failure preceded launch.
- Aspire OTEL logs/traces were unavailable because the AppHost never started.
- The failure is not the known `Missing plugin reference "streams"` residual; that message did not
occur, and generation failed earlier with Deno's generic network-resolution `fetch failed`.
- A focused rerun of only the failed `generate plugins` command against the preserved fixture then
passed in 0.9 seconds and wrote all three registries. This makes the observed failure consistent
with a transient dependency/registry fetch, but the nested Deno error did not expose a URL, so
the exact endpoint could not be verified. This diagnostic does not alter the authoritative red
E2E verdict and was not a second suite run.
- `agentic:leak-check` reported Aspire and Docker probes `ok` with no survivors; Docker also showed
zero running containers after cleanup.
19 changes: 4 additions & 15 deletions packages/cli/e2e/suites/scaffold/capability-suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,8 @@ export interface ScaffoldCapabilitySuite {
readonly defaults?: Partial<RunOptions>;
}

/** Runtime gates deferred until workers-combined publishes execution mutations (#1398). */
export const SCAFFOLD_RUNTIME_DEFERRED_GATES = [
{
id: GATE.BEHAVIOR_OTEL_STREAM_CONSUMER,
issue: '#1398',
reason: 'workers-combined does not install the stream mutation hook',
},
{
id: GATE.BEHAVIOR_OTEL_TRACES,
issue: '#1398',
reason: 'TC-14 requires the deferred Flow-B stream-consumer record',
},
] as const satisfies readonly DeferredGate[];
/** Runtime gates with explicitly accepted temporary deferrals. */
export const SCAFFOLD_RUNTIME_DEFERRED_GATES: readonly DeferredGate[] = [];

const SERVICE_GATES = [
GATE.PREFLIGHT_DENO,
Expand Down Expand Up @@ -143,6 +132,8 @@ const RUNTIME_GATES = [
GATE.BEHAVIOR_MCP_WIDGET_ROUNDTRIP,
GATE.BEHAVIOR_PLUGINS_HEALTH,
GATE.BEHAVIOR_OTEL_WEBHOOK,
GATE.BEHAVIOR_OTEL_STREAM_CONSUMER,
GATE.BEHAVIOR_OTEL_TRACES,
GATE.BEHAVIOR_STREAMS_PRODUCER_RECONNECT,
GATE.BEHAVIOR_OTEL_TASK_TRACES,
GATE.CLEANUP_ASPIRE_STOP,
Expand Down Expand Up @@ -206,14 +197,12 @@ export const scaffoldCapabilitySuites: readonly ScaffoldCapabilitySuite[] = [
id: SCAFFOLD.RUNTIME,
title: SCAFFOLD_TITLE.RUNTIME,
gates: RUNTIME_GATES,
// Explicit release deferral: #1398 must restore both gates to RUNTIME_GATES.
deferredGates: SCAFFOLD_RUNTIME_DEFERRED_GATES,
},
{
id: SCAFFOLD.RUNTIME_SQLITE,
title: SCAFFOLD_TITLE.RUNTIME_SQLITE,
gates: RUNTIME_SQLITE_GATES,
// Keep both runtime tiers honest about the same #1398 coverage gap.
deferredGates: SCAFFOLD_RUNTIME_DEFERRED_GATES,
defaults: { database: DATABASE.SQLITE, cache: false },
},
Expand Down
27 changes: 6 additions & 21 deletions packages/cli/e2e/tests/application/runner/suite-runner_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
scaffoldCapabilitySuites,
} from '../../../suites/scaffold/capability-suites.ts';

Deno.test('suite runner reports every #1398 deferral as an explicit skipped step', async () => {
Deno.test('suite runner reports no skipped steps after the #1398 deferrals are removed', async () => {
const runtime = createScaffoldRuntimeSuite({
repoRoot: '.',
projectName: 'runner-deferral-test',
Expand All @@ -47,29 +47,14 @@ Deno.test('suite runner reports every #1398 deferral as an explicit skipped step
suiteLeaseManager: new RecordingSuiteLeaseManager(),
}).run(suite, { suiteId: suite.id, options: suite.defaultOptions });

assertEquals(report.steps.map((step) => step.id), [
GATE.BEHAVIOR_OTEL_STREAM_CONSUMER,
GATE.BEHAVIOR_OTEL_TRACES,
]);
assertEquals(report.steps.every((step) => step.verdict === 'skipped' && !step.critical), true);
assertEquals(report.summary, { passed: 0, failed: 0, skipped: 2 });
assertEquals(
report.steps.map((step) => step.evidence[0]),
SCAFFOLD_RUNTIME_DEFERRED_GATES.map((deferred) => ({
kind: 'summary' as const,
label: 'owned suite deferral',
data: {
status: 'deferred',
issue: deferred.issue,
reason: deferred.reason,
},
})),
);
assertEquals(SCAFFOLD_RUNTIME_DEFERRED_GATES, []);
assertEquals(report.steps, []);
assertEquals(report.summary, { passed: 0, failed: 0, skipped: 0 });
assertEquals(
reporter.events
.filter((event) => event.type === 'gate-start')
.map((event) => event.title.startsWith('DEFERRED #1398:')),
[true, true],
.map((event) => event.title),
[],
);
});

Expand Down
28 changes: 6 additions & 22 deletions packages/cli/e2e/tests/presentation/suite-registry_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,35 +209,19 @@ Deno.test('runtime suite includes full scaffold, database, runtime, and behavior
runtime.gates.some((gate) => gate.id === GATE.BEHAVIOR_STREAMS_PRODUCER_RECONNECT),
true,
);
assertEquals(runtime.gates.some((gate) => gate.id === GATE.BEHAVIOR_OTEL_STREAM_CONSUMER), false);
assertEquals(runtime.gates.some((gate) => gate.id === GATE.BEHAVIOR_OTEL_TRACES), false);
assertEquals(runtime.gates.some((gate) => gate.id === GATE.BEHAVIOR_OTEL_STREAM_CONSUMER), true);
assertEquals(runtime.gates.some((gate) => gate.id === GATE.BEHAVIOR_OTEL_TRACES), true);
assertEquals(runtime.gates.some((gate) => gate.id === GATE.BEHAVIOR_OTEL_TASK_TRACES), true);
});

Deno.test('runtime suites pin the exact #1398 OTEL deferral without widening it', () => {
assertEquals(SCAFFOLD_RUNTIME_DEFERRED_GATES, [
{
id: GATE.BEHAVIOR_OTEL_STREAM_CONSUMER,
issue: '#1398',
reason: 'workers-combined does not install the stream mutation hook',
},
{
id: GATE.BEHAVIOR_OTEL_TRACES,
issue: '#1398',
reason: 'TC-14 requires the deferred Flow-B stream-consumer record',
},
]);
Deno.test('runtime suites execute the formerly deferred #1398 OTEL gates', () => {
assertEquals(SCAFFOLD_RUNTIME_DEFERRED_GATES, []);

for (const suiteId of [SCAFFOLD.RUNTIME, SCAFFOLD.RUNTIME_SQLITE]) {
const suite = resolveSuite(suiteId);
assertEquals(suite.deferredGates, SCAFFOLD_RUNTIME_DEFERRED_GATES, suiteId);
assertEquals(
suite.gates.some((gate) =>
SCAFFOLD_RUNTIME_DEFERRED_GATES.some((deferred) => deferred.id === gate.id)
),
false,
`${suiteId} must not execute a deferred gate`,
);
assertEquals(suite.gates.some((gate) => gate.id === GATE.BEHAVIOR_OTEL_STREAM_CONSUMER), true);
assertEquals(suite.gates.some((gate) => gate.id === GATE.BEHAVIOR_OTEL_TRACES), true);
}
});

Expand Down
21 changes: 16 additions & 5 deletions packages/plugin-workers-core/src/streams/producer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createDurableStream } from '@netscript/plugin-streams-core';
import { extractContext, withContext } from '@netscript/telemetry/context';
import { DEFAULT_TOPIC } from '../domain/mod.ts';
import { type WorkerExecution, type WorkerJob, workersStreamSchema } from './schema.ts';

Expand Down Expand Up @@ -37,6 +38,8 @@ export type WorkerExecutionRecord = Readonly<
readonly result?: Record<string, unknown> | null;
readonly workerId?: string | null;
readonly attempt?: number;
readonly traceparent?: string;
readonly tracestate?: string;
}
>;

Expand Down Expand Up @@ -110,11 +113,19 @@ export function createStreamMutationHook(
producer: WorkersStreamProducer,
): ExecutionMutationHook {
return ({ type, execution }) => {
if (type === 'deleted') {
producer.delete('execution', execution.id);
} else {
producer.upsert('execution', toExecutionStreamEntity(execution));
}
withContext(
extractContext({
...(execution.traceparent ? { traceparent: execution.traceparent } : {}),
...(execution.tracestate ? { tracestate: execution.tracestate } : {}),
}),
() => {
if (type === 'deleted') {
producer.delete('execution', execution.id);
return;
}
producer.upsert('execution', toExecutionStreamEntity(execution));
},
);
};
}

Expand Down
Loading
Loading