@atolis-hq/wake 0.4.55 → 0.4.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +7 -1
  2. package/dist/src/bootstrap/analytics-reports.d.ts +27 -0
  3. package/dist/src/bootstrap/analytics-reports.js +38 -0
  4. package/dist/src/bootstrap/surface-api-applications.js +2 -0
  5. package/dist/src/bootstrap/version.js +1 -1
  6. package/dist/src/execution/application/run-reports.d.ts +29 -0
  7. package/dist/src/execution/application/run-reports.js +116 -0
  8. package/dist/src/execution/contracts/runner.js +1 -1
  9. package/dist/src/execution/domain/run.d.ts +2 -0
  10. package/dist/src/execution/domain/run.js +10 -0
  11. package/dist/src/execution/index.d.ts +1 -0
  12. package/dist/src/execution/index.js +1 -0
  13. package/dist/src/kernel/contracts/reports.d.ts +39 -0
  14. package/dist/src/kernel/contracts/reports.js +53 -0
  15. package/dist/src/kernel/index.d.ts +1 -0
  16. package/dist/src/kernel/index.js +1 -0
  17. package/dist/src/node_modules/@atolis-hq/eventing/dist/index.d.ts +1 -0
  18. package/dist/src/node_modules/@atolis-hq/eventing/dist/index.d.ts.map +1 -1
  19. package/dist/src/node_modules/@atolis-hq/eventing/dist/index.js +1 -0
  20. package/dist/src/node_modules/@atolis-hq/eventing/dist/store/event-journal.d.ts +2 -0
  21. package/dist/src/node_modules/@atolis-hq/eventing/dist/store/event-journal.d.ts.map +1 -1
  22. package/dist/src/node_modules/@atolis-hq/eventing/dist/store/scan-journal.d.ts +5 -0
  23. package/dist/src/node_modules/@atolis-hq/eventing/dist/store/scan-journal.d.ts.map +1 -0
  24. package/dist/src/node_modules/@atolis-hq/eventing/dist/store/scan-journal.js +34 -0
  25. package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-event-journal.d.ts +4 -0
  26. package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-event-journal.d.ts.map +1 -1
  27. package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-event-journal.js +60 -2
  28. package/dist/src/orchestration/application/work-completion-summary.d.ts +6 -0
  29. package/dist/src/orchestration/application/work-completion-summary.js +49 -0
  30. package/dist/src/orchestration/index.d.ts +1 -0
  31. package/dist/src/orchestration/index.js +1 -0
  32. package/dist/src/surfaces/api/contracts/observability.d.ts +15 -0
  33. package/dist/src/surfaces/api/contracts/observability.js +1 -1
  34. package/dist/src/surfaces/api/http-server.d.ts +1 -1
  35. package/dist/src/surfaces/api/http-server.js +23 -1
  36. package/dist/src/surfaces/api/routes/applications.d.ts +7 -0
  37. package/dist/src/surfaces/api/routes/index.js +2 -2
  38. package/dist/src/surfaces/api/routes/observability.d.ts +1 -1
  39. package/dist/src/surfaces/api/routes/observability.js +5 -1
  40. package/dist/src/surfaces/api/routes/read.d.ts +1 -1
  41. package/dist/src/surfaces/api/routes/read.js +28 -1
  42. package/dist/src/surfaces/web-assets/assets/{index-Be7GB8fP.css → index-B5-ZKu_w.css} +1 -1
  43. package/dist/src/surfaces/web-assets/assets/index-CNG6RzFJ.js +1131 -0
  44. package/dist/src/surfaces/web-assets/index.html +2 -2
  45. package/package.json +1 -1
  46. package/dist/src/surfaces/web-assets/assets/index-8cbjygY-.js +0 -1118
package/README.md CHANGED
@@ -111,7 +111,9 @@ For more detail, see [docs/vision.md](docs/vision.md) and
111
111
  projections, runs, logs, sandbox auth) nested under a hidden `.wake/`.
112
112
  - **Durable handoffs.** Agent stages can publish work-item artifacts outside
113
113
  Git workspaces. Wake passes only a scoped manifest to later agents, which
114
- fetch content on demand through a run-bound MCP capability.
114
+ fetch content on demand through a run-bound MCP capability. The web work
115
+ item Artifacts tab offers inline viewing for `.md` files (case insensitive),
116
+ with raw HTML disabled, a download link, and Back to artifacts navigation.
115
117
  - **Sandbox-oriented execution.** Wake can run from a persistent Docker sandbox
116
118
  with durable auth state and mounted Wake home data.
117
119
  - **Runner agnostic.** Claude Code, Codex, Cursor, and fake runners sit behind
@@ -148,6 +150,10 @@ enter it in the UI. Successful logins persist for two years across browser and
148
150
  Wake-container restarts. Use `wake ui token set <key>` to replace the durable
149
151
  access key; replacement signs out existing sessions.
150
152
 
153
+ Analytics provides live reports with selectable windows, run outcome buckets,
154
+ and summary metrics alongside today's system activity counters. See
155
+ [Analytics API](docs/analytics-api.md) and [web UI](docs/web-ui.md) for definitions.
156
+
151
157
  ## Supported Agent CLIs
152
158
 
153
159
  Wake wraps existing coding-agent CLIs rather than replacing them. Runner
@@ -0,0 +1,27 @@
1
+ import type { EventJournal } from '@atolis-hq/eventing';
2
+ import { type ReportWindowId } from '../kernel/index.js';
3
+ export declare function createAnalyticsReports(journal: EventJournal, now: () => string): {
4
+ catalogue: () => {
5
+ id: string;
6
+ title: string;
7
+ }[];
8
+ report(id: string, windowId: ReportWindowId, signal?: AbortSignal): Promise<{
9
+ data: import("../kernel/index.js").TimeSeriesReport;
10
+ meta: import("../surfaces/index.js").ResponseMeta;
11
+ }>;
12
+ summary(windowId: ReportWindowId, signal?: AbortSignal): Promise<{
13
+ data: {
14
+ workItemsDone: number;
15
+ medianEndToEndDurationMs: number | null;
16
+ runs: number;
17
+ completed: number;
18
+ blockedOrAwaitingApproval: number;
19
+ failed: number;
20
+ totalTokens: number;
21
+ totalCostUsd: number;
22
+ medianRunDurationMs: number | null;
23
+ window: import("../kernel/index.js").ReportWindow;
24
+ };
25
+ meta: import("../surfaces/index.js").ResponseMeta;
26
+ }>;
27
+ };
@@ -0,0 +1,38 @@
1
+ import { createRunOutcomesReport, queryRunAnalytics } from '../execution/index.js';
2
+ import { reportWindow } from '../kernel/index.js';
3
+ import { queryWorkCompletionSummary } from '../orchestration/index.js';
4
+ import { sampledMeta } from './surface-api-metadata.js';
5
+ export function createAnalyticsReports(journal, now) {
6
+ const definitions = [createRunOutcomesReport(journal)];
7
+ return {
8
+ catalogue: () => definitions.map(({ id, title }) => ({ id, title })),
9
+ async report(id, windowId, signal) {
10
+ const definition = definitions.find((report) => report.id === id);
11
+ if (definition === undefined)
12
+ throw new Error('Unknown report');
13
+ const collectedAt = now();
14
+ return {
15
+ data: await definition.query({
16
+ window: reportWindow(windowId, collectedAt),
17
+ ...(signal === undefined ? {} : { signal }),
18
+ }),
19
+ meta: sampledMeta(collectedAt),
20
+ };
21
+ },
22
+ async summary(windowId, signal) {
23
+ const collectedAt = now();
24
+ const query = {
25
+ window: reportWindow(windowId, collectedAt),
26
+ ...(signal === undefined ? {} : { signal }),
27
+ };
28
+ const [runs, work] = await Promise.all([
29
+ queryRunAnalytics(journal, query),
30
+ queryWorkCompletionSummary(journal, query),
31
+ ]);
32
+ return {
33
+ data: { window: query.window, ...runs.summary, ...work },
34
+ meta: sampledMeta(collectedAt),
35
+ };
36
+ },
37
+ };
38
+ }
@@ -1,6 +1,7 @@
1
1
  import { ActivationSchedulerSubscriptionStatus, ControlStreamKind, } from '../control-plane/index.js';
2
2
  import { ApiCommandStatus, fromWorkItemKey, presentBoardCard, presentResource, presentStatus, presentWorkflowInstance, redactConfiguration, } from '../surfaces/index.js';
3
3
  import { analyticsProjection } from './analytics-projection.js';
4
+ import { createAnalyticsReports } from './analytics-reports.js';
4
5
  import { boardConditionCounts, boardProjection, normalizeBoardActiveRunPhase, } from './board-projection.js';
5
6
  import { primaryExternalRef } from './external-ref.js';
6
7
  import { createSelfUpdateFailureLog, describeSelfUpdateFailure, } from './self-update-failure-log.js';
@@ -187,6 +188,7 @@ async function recentEvents(journal, beforeGlobalPosition, limit, include) {
187
188
  }
188
189
  function createObservabilityApplications(root, now) {
189
190
  return {
191
+ reports: createAnalyticsReports(root.journal, now),
190
192
  async metrics(query = { days: 7 }) {
191
193
  const collectedAt = now();
192
194
  const stored = await root.projections.read(analyticsProjection.name, 'global');
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
124
124
  return `g${headHash.slice(0, 7)}`;
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "v0.4.55";
127
+ export const wakeVersion = "v0.4.57";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -0,0 +1,29 @@
1
+ import { type EventJournal } from '@atolis-hq/eventing';
2
+ import { type ReportDefinition, type ReportQuery, type TimeSeriesReport, type ValueOf } from '../../kernel/index.js';
3
+ import type { RunView } from '../contracts/views.js';
4
+ export declare const RunReportId: {
5
+ readonly Outcomes: "run-outcomes-over-time";
6
+ };
7
+ export declare const RunReportOutcome: {
8
+ readonly Completed: "completed";
9
+ readonly Blocked: "blocked";
10
+ readonly AwaitingApproval: "awaiting-approval";
11
+ readonly Failed: "failed";
12
+ readonly Other: "other";
13
+ };
14
+ export type RunReportOutcome = ValueOf<typeof RunReportOutcome>;
15
+ export interface RunSummary {
16
+ readonly runs: number;
17
+ readonly completed: number;
18
+ readonly blockedOrAwaitingApproval: number;
19
+ readonly failed: number;
20
+ readonly totalTokens: number;
21
+ readonly totalCostUsd: number;
22
+ readonly medianRunDurationMs: number | null;
23
+ }
24
+ export declare function runReportOutcome(run: RunView): RunReportOutcome;
25
+ export declare function createRunOutcomesReport(journal: EventJournal): ReportDefinition;
26
+ export declare function queryRunAnalytics(journal: EventJournal, query: ReportQuery): Promise<{
27
+ report: TimeSeriesReport;
28
+ summary: RunSummary;
29
+ }>;
@@ -0,0 +1,116 @@
1
+ import { scanJournal, yieldScan } from '@atolis-hq/eventing';
2
+ import { ActivityOutcomeKind } from '../../activities/index.js';
3
+ import { defineClosedVocabulary, median, } from '../../kernel/index.js';
4
+ import { selectRunExecutionEvent } from '../contracts/events.js';
5
+ import { agentTokenUsage } from '../contracts/runner.js';
6
+ import { RunStatus } from '../contracts/vocabulary.js';
7
+ import { foldRunEvent } from '../domain/run.js';
8
+ export const RunReportId = defineClosedVocabulary({ Outcomes: 'run-outcomes-over-time' });
9
+ export const RunReportOutcome = defineClosedVocabulary({
10
+ Completed: 'completed',
11
+ Blocked: 'blocked',
12
+ AwaitingApproval: 'awaiting-approval',
13
+ Failed: 'failed',
14
+ Other: 'other',
15
+ });
16
+ const series = [
17
+ { id: RunReportOutcome.Completed, title: 'Completed' },
18
+ { id: RunReportOutcome.Blocked, title: 'Blocked' },
19
+ { id: RunReportOutcome.AwaitingApproval, title: 'Awaiting approval' },
20
+ { id: RunReportOutcome.Failed, title: 'Failed' },
21
+ { id: RunReportOutcome.Other, title: 'Other' },
22
+ ];
23
+ export function runReportOutcome(run) {
24
+ if (run.status === RunStatus.Failed)
25
+ return RunReportOutcome.Failed;
26
+ if (run.status !== RunStatus.Succeeded)
27
+ return RunReportOutcome.Other;
28
+ switch (run.outcome?.kind) {
29
+ case ActivityOutcomeKind.Done:
30
+ return RunReportOutcome.Completed;
31
+ case ActivityOutcomeKind.Blocked:
32
+ return RunReportOutcome.Blocked;
33
+ case ActivityOutcomeKind.Waiting:
34
+ return RunReportOutcome.AwaitingApproval;
35
+ case ActivityOutcomeKind.Failed:
36
+ case ActivityOutcomeKind.Rejected:
37
+ return RunReportOutcome.Failed;
38
+ default:
39
+ return RunReportOutcome.Other;
40
+ }
41
+ }
42
+ export function createRunOutcomesReport(journal) {
43
+ return {
44
+ id: RunReportId.Outcomes,
45
+ title: 'Run outcomes over time',
46
+ query: async (query) => (await queryRunAnalytics(journal, query)).report,
47
+ };
48
+ }
49
+ export async function queryRunAnalytics(journal, query) {
50
+ const grouped = new Map();
51
+ for await (const event of scanJournal(journal, query.signal)) {
52
+ const owned = selectRunExecutionEvent(event);
53
+ if (owned === null)
54
+ continue;
55
+ grouped.set(owned.stream.id, foldRunEvent(grouped.get(owned.stream.id) ?? null, owned));
56
+ }
57
+ const from = Date.parse(query.window.from);
58
+ const to = Date.parse(query.window.to);
59
+ const first = Math.floor(from / query.window.bucketMs) * query.window.bucketMs;
60
+ const zero = () => Object.fromEntries(series.map(({ id }) => [id, 0]));
61
+ const buckets = [];
62
+ for (let start = first; start < to; start += query.window.bucketMs)
63
+ buckets.push({
64
+ from: new Date(start).toISOString(),
65
+ to: new Date(start + query.window.bucketMs).toISOString(),
66
+ values: zero(),
67
+ });
68
+ const totals = zero();
69
+ const durations = [];
70
+ let runs = 0, totalTokens = 0, totalCostUsd = 0, processed = 0;
71
+ for (const run of grouped.values()) {
72
+ if (processed++ % 100 === 0)
73
+ await yieldScan(query.signal);
74
+ const start = Date.parse(run.startedAt);
75
+ if (!(start >= from && start < to))
76
+ continue;
77
+ const outcome = runReportOutcome(run);
78
+ totals[outcome]++;
79
+ buckets[Math.floor((start - first) / query.window.bucketMs)].values[outcome]++;
80
+ runs++;
81
+ const usage = agentTokenUsage(run.agent?.metadata);
82
+ totalTokens += finiteOrZero(usage.tokens);
83
+ totalCostUsd += finiteOrZero(usage.costUsd);
84
+ const duration = runDuration(run);
85
+ if (duration !== null)
86
+ durations.push(duration);
87
+ }
88
+ query.signal?.throwIfAborted();
89
+ return {
90
+ report: {
91
+ report: { id: RunReportId.Outcomes, title: 'Run outcomes over time' },
92
+ window: query.window,
93
+ series,
94
+ totals,
95
+ buckets,
96
+ },
97
+ summary: {
98
+ runs,
99
+ completed: totals[RunReportOutcome.Completed],
100
+ blockedOrAwaitingApproval: totals[RunReportOutcome.Blocked] + totals[RunReportOutcome.AwaitingApproval],
101
+ failed: totals[RunReportOutcome.Failed],
102
+ totalTokens,
103
+ totalCostUsd,
104
+ medianRunDurationMs: await median(durations, () => yieldScan(query.signal)),
105
+ },
106
+ };
107
+ }
108
+ function finiteOrZero(value) {
109
+ return Number.isFinite(value) ? value : 0;
110
+ }
111
+ function runDuration(run) {
112
+ if (run.finishedAt === undefined)
113
+ return null;
114
+ const duration = Date.parse(run.finishedAt) - Date.parse(run.startedAt);
115
+ return Number.isFinite(duration) && duration >= 0 ? duration : null;
116
+ }
@@ -5,7 +5,7 @@ export const WakeMcpServerName = 'wake';
5
5
  export function agentTokenUsage(metadata) {
6
6
  const numeric = (key) => {
7
7
  const value = metadata?.[key];
8
- return typeof value === 'number' ? value : 0;
8
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
9
9
  };
10
10
  return {
11
11
  tokens: numeric('inputTokens') +
@@ -1,3 +1,5 @@
1
1
  import { type RunExecutionEvent } from '../contracts/events.js';
2
2
  import type { RunView } from '../contracts/views.js';
3
3
  export declare function foldRun(events: readonly RunExecutionEvent[]): RunView | null;
4
+ /** Incremental replay for bounded live readers; persisted events must already be decoded. */
5
+ export declare function foldRunEvent(previous: RunView | null, event: RunExecutionEvent): RunView;
@@ -33,6 +33,16 @@ export function foldRun(events) {
33
33
  applyRunEvent(state, event.event);
34
34
  return state;
35
35
  }
36
+ /** Incremental replay for bounded live readers; persisted events must already be decoded. */
37
+ export function foldRunEvent(previous, event) {
38
+ if (previous === null)
39
+ return foldRun([event]);
40
+ if (event.event.eventType === ExecutionEventType.RunPreparationStarted)
41
+ throw invalidRunStream(event.stream.id, 'RunPreparationStarted must be the first event');
42
+ const next = { ...previous };
43
+ applyRunEvent(next, event.event);
44
+ return next;
45
+ }
36
46
  function creationWorkspace(event) {
37
47
  if (event.eventType !== ExecutionEventType.RunStarted || event.payload.workspace === undefined)
38
48
  return {};
@@ -32,3 +32,4 @@ export * from './infrastructure/transcripts.js';
32
32
  export * from './infrastructure/transcript-store.js';
33
33
  export * from './infrastructure/workspace/fake-workspace.js';
34
34
  export * from './infrastructure/workspace/git-workspace.js';
35
+ export * from './application/run-reports.js';
@@ -32,3 +32,4 @@ export * from './infrastructure/transcripts.js';
32
32
  export * from './infrastructure/transcript-store.js';
33
33
  export * from './infrastructure/workspace/fake-workspace.js';
34
34
  export * from './infrastructure/workspace/git-workspace.js';
35
+ export * from './application/run-reports.js';
@@ -0,0 +1,39 @@
1
+ import { type ValueOf } from './vocabulary.js';
2
+ export declare const ReportWindowId: {
3
+ readonly Day: "1d";
4
+ readonly ThreeDays: "3d";
5
+ readonly FiveDays: "5d";
6
+ readonly Week: "7d";
7
+ };
8
+ export type ReportWindowId = ValueOf<typeof ReportWindowId>;
9
+ export interface ReportWindow {
10
+ readonly id: ReportWindowId;
11
+ readonly from: string;
12
+ readonly to: string;
13
+ readonly bucketMs: number;
14
+ }
15
+ export interface ReportQuery {
16
+ readonly window: ReportWindow;
17
+ readonly signal?: AbortSignal;
18
+ }
19
+ export interface ReportDescriptor {
20
+ readonly id: string;
21
+ readonly title: string;
22
+ }
23
+ export interface TimeSeriesReport {
24
+ readonly report: ReportDescriptor;
25
+ readonly window: ReportWindow;
26
+ readonly series: readonly ReportDescriptor[];
27
+ readonly totals: Readonly<Record<string, number>>;
28
+ readonly buckets: readonly {
29
+ readonly from: string;
30
+ readonly to: string;
31
+ readonly values: Readonly<Record<string, number>>;
32
+ }[];
33
+ }
34
+ export interface ReportDefinition extends ReportDescriptor {
35
+ query(query: ReportQuery): Promise<TimeSeriesReport>;
36
+ }
37
+ export declare function reportWindow(id: ReportWindowId, now: string): ReportWindow;
38
+ /** Exact median with bounded sorting/merging work between caller-supplied pauses. */
39
+ export declare function median(values: readonly number[], pause: () => Promise<void>): Promise<number | null>;
@@ -0,0 +1,53 @@
1
+ import { defineClosedVocabulary } from './vocabulary.js';
2
+ export const ReportWindowId = defineClosedVocabulary({
3
+ Day: '1d',
4
+ ThreeDays: '3d',
5
+ FiveDays: '5d',
6
+ Week: '7d',
7
+ });
8
+ export function reportWindow(id, now) {
9
+ const day = 24 * 60 * 60 * 1000;
10
+ const days = {
11
+ [ReportWindowId.Day]: 1,
12
+ [ReportWindowId.ThreeDays]: 3,
13
+ [ReportWindowId.FiveDays]: 5,
14
+ [ReportWindowId.Week]: 7,
15
+ }[id];
16
+ const rolling = days <= 3;
17
+ const to = rolling ? Date.parse(now) : Math.floor(Date.parse(now) / day) * day;
18
+ return {
19
+ id,
20
+ from: new Date(to - days * day).toISOString(),
21
+ to: new Date(to).toISOString(),
22
+ bucketMs: days === 1 ? day / 24 : days === 3 ? day / 4 : day,
23
+ };
24
+ }
25
+ /** Exact median with bounded sorting/merging work between caller-supplied pauses. */
26
+ export async function median(values, pause) {
27
+ if (values.length === 0)
28
+ return null;
29
+ const batchSize = 1024;
30
+ let sorted = [];
31
+ for (let start = 0; start < values.length; start += batchSize) {
32
+ await pause();
33
+ sorted.push(...values.slice(start, start + batchSize).sort((a, b) => a - b));
34
+ }
35
+ for (let width = batchSize; width < values.length; width *= 2) {
36
+ const merged = [];
37
+ for (let start = 0; start < values.length; start += width * 2) {
38
+ const middle = Math.min(start + width, values.length), end = Math.min(start + width * 2, values.length);
39
+ let left = start, right = middle;
40
+ while (left < middle || right < end) {
41
+ if (merged.length % batchSize === 0)
42
+ await pause();
43
+ if (right === end || (left < middle && sorted[left] <= sorted[right]))
44
+ merged.push(sorted[left++]);
45
+ else
46
+ merged.push(sorted[right++]);
47
+ }
48
+ }
49
+ sorted = merged;
50
+ }
51
+ const middle = Math.floor(sorted.length / 2);
52
+ return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
53
+ }
@@ -8,3 +8,4 @@ export * from './domain/match-mode.js';
8
8
  export * from './domain/relation.js';
9
9
  export * from './infrastructure/system-clock.js';
10
10
  export * from './infrastructure/ulid-id-generator.js';
11
+ export * from './contracts/reports.js';
@@ -7,3 +7,4 @@ export * from './domain/match-mode.js';
7
7
  export * from './domain/relation.js';
8
8
  export * from './infrastructure/system-clock.js';
9
9
  export * from './infrastructure/ulid-id-generator.js';
10
+ export * from './contracts/reports.js';
@@ -16,4 +16,5 @@ export * from './store/checkpoint-store.js';
16
16
  export * from './store/event-journal.js';
17
17
  export * from './subscriptions/event-processor.js';
18
18
  export * from './subscriptions/journal-change-signal.js';
19
+ export * from './store/scan-journal.js';
19
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AAExC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,6BAA6B,CAAC;AAE5C,cAAc,uBAAuB,CAAC;AAEtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEjF,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEtF,cAAc,sCAAsC,CAAC;AAErD,cAAc,uCAAuC,CAAC;AAEtD,cAAc,mCAAmC,CAAC;AAElD,cAAc,kCAAkC,CAAC;AAEjD,cAAc,oBAAoB,CAAC;AAEnC,cAAc,mCAAmC,CAAC;AAElD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,uCAAuC,CAAC;AAEtD,cAAc,6BAA6B,CAAC;AAE5C,cAAc,0BAA0B,CAAC;AAEzC,cAAc,oCAAoC,CAAC;AAEnD,cAAc,0CAA0C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AAExC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,6BAA6B,CAAC;AAE5C,cAAc,uBAAuB,CAAC;AAEtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEjF,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAEtF,cAAc,sCAAsC,CAAC;AAErD,cAAc,uCAAuC,CAAC;AAEtD,cAAc,mCAAmC,CAAC;AAElD,cAAc,kCAAkC,CAAC;AAEjD,cAAc,oBAAoB,CAAC;AAEnC,cAAc,mCAAmC,CAAC;AAElD,cAAc,+BAA+B,CAAC;AAE9C,cAAc,uCAAuC,CAAC;AAEtD,cAAc,6BAA6B,CAAC;AAE5C,cAAc,0BAA0B,CAAC;AAEzC,cAAc,oCAAoC,CAAC;AAEnD,cAAc,0CAA0C,CAAC;AAEzD,cAAc,yBAAyB,CAAC"}
@@ -15,3 +15,4 @@ export * from './store/checkpoint-store.js';
15
15
  export * from './store/event-journal.js';
16
16
  export * from './subscriptions/event-processor.js';
17
17
  export * from './subscriptions/journal-change-signal.js';
18
+ export * from './store/scan-journal.js';
@@ -5,6 +5,8 @@ export declare class WrongExpectedSequenceError extends Error {
5
5
  export interface EventJournal {
6
6
  appendToStream(stream: StreamRef, expectedSequence: number, events: readonly EventData[]): Promise<readonly EventEnvelope[]>;
7
7
  readStream(stream: StreamRef): Promise<readonly EventEnvelope[]>;
8
+ /** Optional bounded, cancellable snapshot reader for adapters with a streaming source. */
9
+ scanAll?(signal?: AbortSignal): AsyncIterable<EventEnvelope>;
8
10
  readAll(afterGlobalPosition: number, limit?: number): Promise<readonly EventEnvelope[]>;
9
11
  readLatest?(beforeGlobalPosition?: number, limit?: number): Promise<readonly EventEnvelope[]>;
10
12
  latestGlobalPosition(): Promise<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"event-journal.d.ts","sourceRoot":"","sources":["../../src/store/event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAErF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAExD,MAAM,WAAW,YAAY;IAC3B,cAAc,CACZ,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACjE,OAAO,CAAC,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACxF,UAAU,CAAC,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAI9F,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,kBAAkB,CAChB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAAC;IAGjB,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAC5C"}
1
+ {"version":3,"file":"event-journal.d.ts","sourceRoot":"","sources":["../../src/store/event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2CAA2C,CAAC;AAErF,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAExD,MAAM,WAAW,YAAY;IAC3B,cAAc,CACZ,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACrC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACjE,0FAA0F;IAC1F,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IAC7D,OAAO,CAAC,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IACxF,UAAU,CAAC,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAI9F,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,kBAAkB,CAChB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAAC;IAGjB,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAC5C"}
@@ -0,0 +1,5 @@
1
+ import type { EventJournal } from './event-journal.js';
2
+ /** A bounded snapshot scan that gives hosts time to serve other work between batches. */
3
+ export declare function scanJournal(journal: EventJournal, signal?: AbortSignal): AsyncGenerator<import("../index.js").EventEnvelope<import("../index.js").EventData<string, unknown>, import("../index.js").StreamRef<string, string>>, void, unknown>;
4
+ export declare function yieldScan(signal?: AbortSignal): Promise<void>;
5
+ //# sourceMappingURL=scan-journal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-journal.d.ts","sourceRoot":"","sources":["../../src/store/scan-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,yFAAyF;AACzF,wBAAuB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,WAAW,yKAyB7E;AAED,wBAAsB,SAAS,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAInE"}
@@ -0,0 +1,34 @@
1
+ /** A bounded snapshot scan that gives hosts time to serve other work between batches. */
2
+ export async function* scanJournal(journal, signal) {
3
+ signal?.throwIfAborted();
4
+ if (journal.scanAll !== undefined) {
5
+ for await (const event of journal.scanAll(signal)) {
6
+ signal?.throwIfAborted();
7
+ yield event;
8
+ }
9
+ signal?.throwIfAborted();
10
+ return;
11
+ }
12
+ const end = await journal.latestGlobalPosition();
13
+ let position = 0;
14
+ while (position < end) {
15
+ await yieldScan(signal);
16
+ const batch = await journal.readAll(position, 100);
17
+ signal?.throwIfAborted();
18
+ if (batch.length === 0)
19
+ return;
20
+ for (const event of batch) {
21
+ signal?.throwIfAborted();
22
+ if (event.globalPosition > end)
23
+ return;
24
+ yield event;
25
+ position = event.globalPosition;
26
+ }
27
+ }
28
+ signal?.throwIfAborted();
29
+ }
30
+ export async function yieldScan(signal) {
31
+ signal?.throwIfAborted();
32
+ await new Promise((resolve) => setTimeout(resolve, 0));
33
+ signal?.throwIfAborted();
34
+ }
@@ -28,6 +28,10 @@ export declare class FileEventJournal implements EventJournal {
28
28
  appendToStream(stream: StreamRef, expectedSequence: number, drafts: readonly EventData[]): Promise<readonly EventEnvelope[]>;
29
29
  private nextSegmentFile;
30
30
  readStream(stream: StreamRef): Promise<readonly EventEnvelope<EventData<string, unknown>, StreamRef<string, string>>[]>;
31
+ /** Live readers bypass the full-history cache and stop at captured segment sizes. */
32
+ scanAll(signal?: AbortSignal): AsyncIterable<EventEnvelope>;
33
+ private scanSegment;
34
+ private decodeScanRecord;
31
35
  readAll(after: number, limit?: number): Promise<EventEnvelope<EventData<string, unknown>, StreamRef<string, string>>[]>;
32
36
  readLatest(beforeGlobalPosition?: number, limit?: number): Promise<EventEnvelope<EventData<string, unknown>, StreamRef<string, string>>[]>;
33
37
  latestGlobalPosition(): Promise<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"file-event-journal.d.ts","sourceRoot":"","sources":["../src/file-event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,SAAS,EACV,MAAM,qBAAqB,CAAC;AAW7B,MAAM,WAAW,uBAAuB;IACtC,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CACrD;AAED,MAAM,MAAM,8BAA8B,GAAG,CAC3C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,IAAI,KACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEtC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,8BAA8B,CAAC;IACzD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;CACvC;AAOD,qBAAa,gBAAiB,YAAW,YAAY;IAIjD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;gBAG1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,EACpB,OAAO,GAAE,uBAA4B;IAKxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsC;IACzE,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,qBAAqB,CAA4B;IAEzD,IAAI,YAAY,IAAI,mBAAmB,CAEtC;IAED,OAAO,CAAC,MAAM,CAOA;IAWd,OAAO,CAAC,YAAY,CAC4E;IAEhG,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,gBAAgB,CAC4D;IAEpF,OAAO,CAAC,oBAAoB,CAA2C;IACvE,OAAO,CAAC,yBAAyB,CAAK;IAEhC,cAAc,CAClB,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;YAwCtB,eAAe;IAsBvB,UAAU,CAAC,MAAM,EAAE,SAAS;IA4B5B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAA2B;IAyBvD,UAAU,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,KAAK,SAA2B;IAW1E,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC;IAKvC,kBAAkB,CACtB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAWhB,OAAO,CAAC,mBAAmB;YAUb,kBAAkB;YA4BlB,WAAW;IAkDzB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,qBAAqB;YAQf,eAAe;YAMf,YAAY;YAYZ,IAAI;YAYJ,kBAAkB;YAmBlB,kBAAkB;IAkBhC,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,0BAA0B;YAQpB,mBAAmB;IA0BjC,OAAO,CAAC,mBAAmB;YAwBb,eAAe;CAkD9B"}
1
+ {"version":3,"file":"file-event-journal.d.ts","sourceRoot":"","sources":["../src/file-event-journal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,SAAS,EACV,MAAM,qBAAqB,CAAC;AAW7B,MAAM,WAAW,uBAAuB;IACtC,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CACrD;AAED,MAAM,MAAM,8BAA8B,GAAG,CAC3C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,IAAI,KACf,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEtC,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,8BAA8B,CAAC;IACzD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;CACvC;AAOD,qBAAa,gBAAiB,YAAW,YAAY;IAIjD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;gBAG1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,EACpB,OAAO,GAAE,uBAA4B;IAKxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsC;IACzE,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,qBAAqB,CAA4B;IAEzD,IAAI,YAAY,IAAI,mBAAmB,CAEtC;IAED,OAAO,CAAC,MAAM,CAOA;IAWd,OAAO,CAAC,YAAY,CAC4E;IAEhG,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,gBAAgB,CAC4D;IAEpF,OAAO,CAAC,oBAAoB,CAA2C;IACvE,OAAO,CAAC,yBAAyB,CAAK;IAEhC,cAAc,CAClB,MAAM,EAAE,SAAS,EACjB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC;YAwCtB,eAAe;IAsBvB,UAAU,CAAC,MAAM,EAAE,SAAS;IA4BlC,qFAAqF;IAC9E,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,aAAa,CAAC;YAcnD,WAAW;IAgC1B,OAAO,CAAC,gBAAgB;IAoBlB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAA2B;IAyBvD,UAAU,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,KAAK,SAA2B;IAW1E,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC;IAKvC,kBAAkB,CACtB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAWhB,OAAO,CAAC,mBAAmB;YAUb,kBAAkB;YA4BlB,WAAW;IAkDzB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,qBAAqB;YAQf,eAAe;YAMf,YAAY;YAYZ,IAAI;YAYJ,kBAAkB;YAmBlB,kBAAkB;IAkBhC,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,0BAA0B;YAQpB,mBAAmB;IA0BjC,OAAO,CAAC,mBAAmB;YAwBb,eAAe;CAkD9B"}
@@ -1,6 +1,6 @@
1
- import { decodeEventEnvelope, WrongExpectedSequenceError } from '@atolis-hq/eventing';
1
+ import { decodeEventEnvelope, WrongExpectedSequenceError, yieldScan } from '@atolis-hq/eventing';
2
2
  import { InProcessJournalChangeSignal } from '@atolis-hq/eventing/memory';
3
- import { watch } from 'node:fs';
3
+ import { createReadStream, watch } from 'node:fs';
4
4
  import { mkdir, open, readdir, readFile, rm, stat } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
  import { performance } from 'node:perf_hooks';
@@ -115,6 +115,64 @@ export class FileEventJournal {
115
115
  }
116
116
  return (await this.scan(entries)).filter((event) => key(event.stream) === streamKey);
117
117
  }
118
+ /** Live readers bypass the full-history cache and stop at captured segment sizes. */
119
+ async *scanAll(signal) {
120
+ signal?.throwIfAborted();
121
+ const entries = await this.readCurrentEntries();
122
+ let position = 0;
123
+ for (const entry of entries) {
124
+ let lineNumber = 0;
125
+ for await (const line of this.scanSegment(entry, signal)) {
126
+ if (position % 100 === 0)
127
+ await yieldScan(signal);
128
+ signal?.throwIfAborted();
129
+ yield this.decodeScanRecord(entry.file, ++lineNumber, line, ++position);
130
+ }
131
+ }
132
+ }
133
+ async *scanSegment(entry, signal) {
134
+ if (entry.size === 0)
135
+ return;
136
+ const source = createReadStream(join(this.root, 'events', entry.file), {
137
+ encoding: 'utf8',
138
+ end: entry.size - 1,
139
+ ...(signal === undefined ? {} : { signal }),
140
+ });
141
+ let pending = '', bytes = 0;
142
+ try {
143
+ for await (const chunk of source) {
144
+ signal?.throwIfAborted();
145
+ const text = String(chunk);
146
+ bytes += Buffer.byteLength(text);
147
+ pending += text;
148
+ let offset = 0, newline = pending.indexOf('\n');
149
+ while (newline !== -1) {
150
+ yield pending.slice(offset, newline);
151
+ offset = newline + 1;
152
+ newline = pending.indexOf('\n', offset);
153
+ }
154
+ pending = pending.slice(offset);
155
+ }
156
+ signal?.throwIfAborted();
157
+ if (pending !== '' || bytes !== entry.size)
158
+ throw new Error(`Incomplete trailing line in ${entry.file}`);
159
+ }
160
+ finally {
161
+ source.destroy();
162
+ }
163
+ }
164
+ decodeScanRecord(file, lineNumber, line, position) {
165
+ let input;
166
+ try {
167
+ input = JSON.parse(line);
168
+ const event = decodeEventRecord(input);
169
+ validateEnvelope(event, position);
170
+ return event;
171
+ }
172
+ catch (error) {
173
+ throw new Error(`Corrupt event at ${file}:${lineNumber}${eventContext(input)}: ${error.message}`, { cause: error });
174
+ }
175
+ }
118
176
  async readAll(after, limit = Number.POSITIVE_INFINITY) {
119
177
  const entries = await this.readEntriesForRead();
120
178
  if (this.cached !== undefined && sameEntries(this.cached.entries, entries))
@@ -0,0 +1,6 @@
1
+ import { type EventJournal } from '@atolis-hq/eventing';
2
+ import { type ReportQuery } from '../../kernel/index.js';
3
+ export declare function queryWorkCompletionSummary(journal: EventJournal, query: ReportQuery): Promise<{
4
+ workItemsDone: number;
5
+ medianEndToEndDurationMs: number | null;
6
+ }>;