reqon-dsl 0.3.0 → 0.4.0

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 (122) hide show
  1. package/README.md +23 -3
  2. package/dist/ast/nodes.d.ts +8 -0
  3. package/dist/auth/circuit-breaker.d.ts +11 -0
  4. package/dist/auth/circuit-breaker.js +83 -12
  5. package/dist/auth/credentials.d.ts +6 -1
  6. package/dist/auth/credentials.js +12 -4
  7. package/dist/auth/oauth2-provider.js +13 -3
  8. package/dist/auth/rate-limiter.d.ts +8 -1
  9. package/dist/auth/rate-limiter.js +30 -10
  10. package/dist/auth/token-store.js +8 -1
  11. package/dist/cli.d.ts +11 -1
  12. package/dist/cli.js +65 -6
  13. package/dist/config/constants.d.ts +15 -4
  14. package/dist/config/constants.js +15 -4
  15. package/dist/control/server.d.ts +17 -0
  16. package/dist/control/server.js +82 -5
  17. package/dist/control/types.d.ts +6 -0
  18. package/dist/debug/cli-debugger.js +8 -3
  19. package/dist/execution/store.js +2 -2
  20. package/dist/execution-log/events.d.ts +125 -0
  21. package/dist/execution-log/events.js +17 -0
  22. package/dist/execution-log/fold.d.ts +38 -0
  23. package/dist/execution-log/fold.js +54 -0
  24. package/dist/execution-log/index.d.ts +18 -0
  25. package/dist/execution-log/index.js +6 -0
  26. package/dist/execution-log/postgres-store.d.ts +36 -0
  27. package/dist/execution-log/postgres-store.js +108 -0
  28. package/dist/execution-log/resume.d.ts +11 -0
  29. package/dist/execution-log/resume.js +5 -0
  30. package/dist/execution-log/sqlite-store.d.ts +16 -0
  31. package/dist/execution-log/sqlite-store.js +101 -0
  32. package/dist/execution-log/store.d.ts +72 -0
  33. package/dist/execution-log/store.js +182 -0
  34. package/dist/index.d.ts +4 -3
  35. package/dist/index.js +4 -3
  36. package/dist/interpreter/context.d.ts +15 -0
  37. package/dist/interpreter/context.js +3 -0
  38. package/dist/interpreter/evaluator.js +38 -8
  39. package/dist/interpreter/executor.d.ts +63 -1
  40. package/dist/interpreter/executor.js +406 -30
  41. package/dist/interpreter/fetch-handler.d.ts +39 -1
  42. package/dist/interpreter/fetch-handler.js +84 -15
  43. package/dist/interpreter/http.d.ts +31 -2
  44. package/dist/interpreter/http.js +187 -26
  45. package/dist/interpreter/index.d.ts +3 -3
  46. package/dist/interpreter/index.js +3 -3
  47. package/dist/interpreter/pagination.d.ts +1 -1
  48. package/dist/interpreter/pagination.js +7 -1
  49. package/dist/interpreter/step-handlers/for-handler.d.ts +3 -0
  50. package/dist/interpreter/step-handlers/for-handler.js +18 -3
  51. package/dist/interpreter/step-handlers/match-handler.js +5 -2
  52. package/dist/interpreter/step-handlers/store-handler.d.ts +7 -1
  53. package/dist/interpreter/step-handlers/store-handler.js +25 -16
  54. package/dist/interpreter/step-handlers/validate-handler.js +4 -1
  55. package/dist/interpreter/step-handlers/webhook-handler.d.ts +1 -0
  56. package/dist/interpreter/step-handlers/webhook-handler.js +13 -3
  57. package/dist/interpreter/store-manager.d.ts +1 -1
  58. package/dist/interpreter/store-manager.js +5 -1
  59. package/dist/loader/index.js +5 -8
  60. package/dist/mcp/sandbox.d.ts +41 -0
  61. package/dist/mcp/sandbox.js +76 -0
  62. package/dist/mcp/server.js +62 -9
  63. package/dist/oas/loader.d.ts +13 -1
  64. package/dist/oas/loader.js +25 -3
  65. package/dist/oas/mock-generator.js +13 -4
  66. package/dist/oas/validator.js +45 -5
  67. package/dist/observability/events.d.ts +6 -2
  68. package/dist/observability/events.js +0 -5
  69. package/dist/observability/logger.js +17 -10
  70. package/dist/observability/otel.d.ts +8 -0
  71. package/dist/observability/otel.js +45 -10
  72. package/dist/parser/action-parser.js +2 -2
  73. package/dist/parser/base.d.ts +7 -0
  74. package/dist/parser/base.js +11 -0
  75. package/dist/parser/expressions.d.ts +1 -0
  76. package/dist/parser/expressions.js +17 -4
  77. package/dist/parser/fetch-parser.js +13 -2
  78. package/dist/pause/index.d.ts +1 -0
  79. package/dist/pause/index.js +1 -0
  80. package/dist/pause/log-store.d.ts +33 -0
  81. package/dist/pause/log-store.js +98 -0
  82. package/dist/pause/manager.d.ts +12 -0
  83. package/dist/pause/manager.js +77 -28
  84. package/dist/pause/store.js +5 -3
  85. package/dist/scheduler/cron-parser.d.ts +10 -3
  86. package/dist/scheduler/cron-parser.js +227 -48
  87. package/dist/scheduler/scheduler.js +56 -22
  88. package/dist/stores/factory.d.ts +6 -0
  89. package/dist/stores/factory.js +11 -1
  90. package/dist/stores/file.js +9 -17
  91. package/dist/stores/memory.js +3 -12
  92. package/dist/stores/postgrest.d.ts +28 -0
  93. package/dist/stores/postgrest.js +84 -37
  94. package/dist/sync/index.d.ts +3 -2
  95. package/dist/sync/index.js +2 -1
  96. package/dist/sync/log-store.d.ts +30 -0
  97. package/dist/sync/log-store.js +45 -0
  98. package/dist/sync/store.js +1 -1
  99. package/dist/trace/index.d.ts +2 -0
  100. package/dist/trace/index.js +1 -0
  101. package/dist/trace/log-view.d.ts +57 -0
  102. package/dist/trace/log-view.js +76 -0
  103. package/dist/trace/recorder.d.ts +5 -1
  104. package/dist/trace/recorder.js +19 -6
  105. package/dist/trace/store.d.ts +6 -0
  106. package/dist/trace/store.js +47 -22
  107. package/dist/utils/deep-merge.d.ts +10 -0
  108. package/dist/utils/deep-merge.js +23 -0
  109. package/dist/utils/file.d.ts +13 -4
  110. package/dist/utils/file.js +70 -12
  111. package/dist/utils/index.d.ts +1 -1
  112. package/dist/utils/index.js +1 -1
  113. package/dist/utils/long-timeout.d.ts +19 -0
  114. package/dist/utils/long-timeout.js +33 -0
  115. package/dist/utils/path.d.ts +22 -1
  116. package/dist/utils/path.js +46 -1
  117. package/dist/utils/redact.d.ts +22 -0
  118. package/dist/utils/redact.js +42 -0
  119. package/dist/webhook/server.d.ts +9 -0
  120. package/dist/webhook/server.js +115 -30
  121. package/dist/webhook/types.d.ts +9 -1
  122. package/package.json +22 -4
@@ -10,6 +10,17 @@ export interface PostgRESTOptions {
10
10
  primaryKey?: string;
11
11
  /** Optional schema (for Supabase, typically 'public') */
12
12
  schema?: string;
13
+ /**
14
+ * Per-request timeout in milliseconds (default: 30000). Every request is
15
+ * aborted after this elapses so a hung connection can't stall a mission.
16
+ */
17
+ timeoutMs?: number;
18
+ /**
19
+ * Opt-in guard for {@link PostgRESTStore.clear}, which issues a full-table
20
+ * DELETE. Defaults to false so a misconfigured store (e.g. a `sql` type
21
+ * resolving here by mistake) can't wipe a table.
22
+ */
23
+ allowFullTableClear?: boolean;
13
24
  }
14
25
  /**
15
26
  * PostgREST-compatible store adapter.
@@ -29,7 +40,24 @@ export declare class PostgRESTStore implements StoreAdapter {
29
40
  private baseUrl;
30
41
  private headers;
31
42
  private primaryKey;
43
+ private timeoutMs;
32
44
  constructor(options: PostgRESTOptions);
45
+ /** fetch wrapper that aborts the request once {@link timeoutMs} elapses. */
46
+ private request;
47
+ /**
48
+ * Reject a where-clause field unless it is a plain column identifier. This
49
+ * stops a key such as `or` or `id,or=(...)` from being concatenated into the
50
+ * query string and reinterpreted as a PostgREST operator.
51
+ */
52
+ private validateField;
53
+ /**
54
+ * Render a where-clause value as a PostgREST operand. Strings containing
55
+ * reserved characters are wrapped in a quoted operand (with `"` and `\`
56
+ * escaped) so they can't be parsed as list/group syntax.
57
+ */
58
+ private formatFilterValue;
59
+ /** Append a validated, escaped where clause to the given params. */
60
+ private applyWhere;
33
61
  get(key: string): Promise<Record<string, unknown> | null>;
34
62
  set(key: string, value: Record<string, unknown>): Promise<void>;
35
63
  update(key: string, value: Partial<Record<string, unknown>>): Promise<void>;
@@ -1,3 +1,17 @@
1
+ /** PostgREST query parameters that are operators/modifiers, not columns. */
2
+ const RESERVED_QUERY_KEYS = new Set([
3
+ 'or',
4
+ 'and',
5
+ 'not',
6
+ 'select',
7
+ 'order',
8
+ 'limit',
9
+ 'offset',
10
+ 'on_conflict',
11
+ 'columns',
12
+ ]);
13
+ /** A bare, safe column identifier: no operators, separators, or grouping. */
14
+ const SAFE_FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
1
15
  /**
2
16
  * PostgREST-compatible store adapter.
3
17
  * Works with Supabase, standalone PostgREST, or any PostgREST-compatible API.
@@ -16,26 +30,77 @@ export class PostgRESTStore {
16
30
  baseUrl;
17
31
  headers;
18
32
  primaryKey;
33
+ timeoutMs;
19
34
  constructor(options) {
20
35
  this.options = options;
21
36
  // Normalize URL (remove trailing slash)
22
37
  const url = options.url.replace(/\/$/, '');
23
38
  this.baseUrl = `${url}/${options.table}`;
24
39
  this.primaryKey = options.primaryKey ?? 'id';
40
+ this.timeoutMs = options.timeoutMs ?? 30000;
25
41
  this.headers = {
26
42
  'Content-Type': 'application/json',
27
- 'apikey': options.apiKey,
28
- 'Authorization': `Bearer ${options.apiKey}`,
29
- 'Prefer': 'return=representation',
43
+ apikey: options.apiKey,
44
+ Authorization: `Bearer ${options.apiKey}`,
45
+ Prefer: 'return=representation',
30
46
  };
31
47
  if (options.schema) {
32
48
  this.headers['Accept-Profile'] = options.schema;
33
49
  this.headers['Content-Profile'] = options.schema;
34
50
  }
35
51
  }
52
+ /** fetch wrapper that aborts the request once {@link timeoutMs} elapses. */
53
+ async request(url, init) {
54
+ try {
55
+ return await fetch(url, { ...init, signal: AbortSignal.timeout(this.timeoutMs) });
56
+ }
57
+ catch (error) {
58
+ if (error instanceof DOMException && error.name === 'TimeoutError') {
59
+ throw new PostgRESTError(`Request timed out after ${this.timeoutMs}ms`, 0);
60
+ }
61
+ throw error;
62
+ }
63
+ }
64
+ /**
65
+ * Reject a where-clause field unless it is a plain column identifier. This
66
+ * stops a key such as `or` or `id,or=(...)` from being concatenated into the
67
+ * query string and reinterpreted as a PostgREST operator.
68
+ */
69
+ validateField(field) {
70
+ if (!SAFE_FIELD.test(field) || RESERVED_QUERY_KEYS.has(field.toLowerCase())) {
71
+ throw new PostgRESTError(`Unsafe filter field name: ${JSON.stringify(field)}`, 0);
72
+ }
73
+ }
74
+ /**
75
+ * Render a where-clause value as a PostgREST operand. Strings containing
76
+ * reserved characters are wrapped in a quoted operand (with `"` and `\`
77
+ * escaped) so they can't be parsed as list/group syntax.
78
+ */
79
+ formatFilterValue(value) {
80
+ if (value === null)
81
+ return 'is.null';
82
+ if (typeof value === 'number' || typeof value === 'boolean')
83
+ return `eq.${value}`;
84
+ if (typeof value === 'string') {
85
+ if (/[,.()"\\:]|\s/.test(value)) {
86
+ return `eq."${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
87
+ }
88
+ return `eq.${value}`;
89
+ }
90
+ // Complex values: JSON-encode, then quote as a string operand.
91
+ const json = JSON.stringify(value);
92
+ return `eq."${json.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
93
+ }
94
+ /** Append a validated, escaped where clause to the given params. */
95
+ applyWhere(params, where) {
96
+ for (const [field, value] of Object.entries(where)) {
97
+ this.validateField(field);
98
+ params.append(field, this.formatFilterValue(value));
99
+ }
100
+ }
36
101
  async get(key) {
37
102
  const url = `${this.baseUrl}?${this.primaryKey}=eq.${encodeURIComponent(key)}&limit=1`;
38
- const response = await fetch(url, {
103
+ const response = await this.request(url, {
39
104
  method: 'GET',
40
105
  headers: this.headers,
41
106
  });
@@ -48,11 +113,11 @@ export class PostgRESTStore {
48
113
  async set(key, value) {
49
114
  // Upsert using PostgREST's on_conflict resolution
50
115
  const record = { ...value, [this.primaryKey]: key };
51
- const response = await fetch(this.baseUrl, {
116
+ const response = await this.request(this.baseUrl, {
52
117
  method: 'POST',
53
118
  headers: {
54
119
  ...this.headers,
55
- 'Prefer': 'resolution=merge-duplicates,return=representation',
120
+ Prefer: 'resolution=merge-duplicates,return=representation',
56
121
  },
57
122
  body: JSON.stringify(record),
58
123
  });
@@ -63,7 +128,7 @@ export class PostgRESTStore {
63
128
  }
64
129
  async update(key, value) {
65
130
  const url = `${this.baseUrl}?${this.primaryKey}=eq.${encodeURIComponent(key)}`;
66
- const response = await fetch(url, {
131
+ const response = await this.request(url, {
67
132
  method: 'PATCH',
68
133
  headers: this.headers,
69
134
  body: JSON.stringify(value),
@@ -75,7 +140,7 @@ export class PostgRESTStore {
75
140
  }
76
141
  async delete(key) {
77
142
  const url = `${this.baseUrl}?${this.primaryKey}=eq.${encodeURIComponent(key)}`;
78
- const response = await fetch(url, {
143
+ const response = await this.request(url, {
79
144
  method: 'DELETE',
80
145
  headers: this.headers,
81
146
  });
@@ -88,21 +153,7 @@ export class PostgRESTStore {
88
153
  const params = new URLSearchParams();
89
154
  // Apply where clause
90
155
  if (filter?.where) {
91
- for (const [field, value] of Object.entries(filter.where)) {
92
- if (value === null) {
93
- params.append(field, 'is.null');
94
- }
95
- else if (typeof value === 'string') {
96
- params.append(field, `eq.${value}`);
97
- }
98
- else if (typeof value === 'number' || typeof value === 'boolean') {
99
- params.append(field, `eq.${value}`);
100
- }
101
- else {
102
- // For complex values, try JSON
103
- params.append(field, `eq.${JSON.stringify(value)}`);
104
- }
105
- }
156
+ this.applyWhere(params, filter.where);
106
157
  }
107
158
  // Apply pagination
108
159
  if (filter?.limit) {
@@ -113,7 +164,7 @@ export class PostgRESTStore {
113
164
  }
114
165
  const queryString = params.toString();
115
166
  const url = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl;
116
- const response = await fetch(url, {
167
+ const response = await this.request(url, {
117
168
  method: 'GET',
118
169
  headers: this.headers,
119
170
  });
@@ -124,10 +175,13 @@ export class PostgRESTStore {
124
175
  return response.json();
125
176
  }
126
177
  async clear() {
178
+ if (!this.options.allowFullTableClear) {
179
+ throw new PostgRESTError('Refusing full-table delete: set allowFullTableClear to enable clear()', 0);
180
+ }
127
181
  // Delete all records - PostgREST requires a filter, so we use a always-true condition
128
182
  // This deletes where primary key is not null (i.e., all records)
129
183
  const url = `${this.baseUrl}?${this.primaryKey}=not.is.null`;
130
- const response = await fetch(url, {
184
+ const response = await this.request(url, {
131
185
  method: 'DELETE',
132
186
  headers: this.headers,
133
187
  });
@@ -142,11 +196,11 @@ export class PostgRESTStore {
142
196
  async bulkInsert(records) {
143
197
  if (records.length === 0)
144
198
  return;
145
- const response = await fetch(this.baseUrl, {
199
+ const response = await this.request(this.baseUrl, {
146
200
  method: 'POST',
147
201
  headers: {
148
202
  ...this.headers,
149
- 'Prefer': 'resolution=merge-duplicates',
203
+ Prefer: 'resolution=merge-duplicates',
150
204
  },
151
205
  body: JSON.stringify(records),
152
206
  });
@@ -162,21 +216,14 @@ export class PostgRESTStore {
162
216
  const params = new URLSearchParams();
163
217
  params.append('select', 'count');
164
218
  if (filter?.where) {
165
- for (const [field, value] of Object.entries(filter.where)) {
166
- if (value === null) {
167
- params.append(field, 'is.null');
168
- }
169
- else {
170
- params.append(field, `eq.${value}`);
171
- }
172
- }
219
+ this.applyWhere(params, filter.where);
173
220
  }
174
221
  const url = `${this.baseUrl}?${params.toString()}`;
175
- const response = await fetch(url, {
222
+ const response = await this.request(url, {
176
223
  method: 'GET',
177
224
  headers: {
178
225
  ...this.headers,
179
- 'Prefer': 'count=exact',
226
+ Prefer: 'count=exact',
180
227
  },
181
228
  });
182
229
  if (!response.ok) {
@@ -1,4 +1,5 @@
1
- export type { SyncCheckpoint, SyncState, SinceResolution, SinceDateFormat, } from './state.js';
2
- export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH, } from './state.js';
1
+ export type { SyncCheckpoint, SyncState, SinceResolution, SinceDateFormat } from './state.js';
2
+ export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH } from './state.js';
3
3
  export type { SyncStore } from './store.js';
4
4
  export { FileSyncStore, MemorySyncStore } from './store.js';
5
+ export { LogBackedSyncStore } from './log-store.js';
@@ -1,2 +1,3 @@
1
- export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH, } from './state.js';
1
+ export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH } from './state.js';
2
2
  export { FileSyncStore, MemorySyncStore } from './store.js';
3
+ export { LogBackedSyncStore } from './log-store.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Log-backed sync store: incremental-sync checkpoints as a view over the
3
+ * execution log.
4
+ *
5
+ * In durable mode the execution log is the single source of truth — a sync's
6
+ * progress is recorded as a `checkpoint.advanced` event alongside everything
7
+ * else, so it survives a crash and resumes atomically with the run. This
8
+ * adapter reads `getLastSync`/checkpoint state back out of those events
9
+ * (newest-per-key, monotonic by `syncedAt`) instead of a separate sync file.
10
+ *
11
+ * Writes go through the log, not here: the executor appends `checkpoint.advanced`
12
+ * when a sync's fetched data is durably stored, so {@link recordSync} is a no-op.
13
+ */
14
+ import type { ExecutionLogStore } from '../execution-log/index.js';
15
+ import type { SyncStore } from './store.js';
16
+ import type { SyncCheckpoint } from './state.js';
17
+ export declare class LogBackedSyncStore implements SyncStore {
18
+ private log;
19
+ private mission?;
20
+ constructor(log: ExecutionLogStore, mission?: string | undefined);
21
+ private checkpoint;
22
+ getLastSync(key: string): Promise<Date>;
23
+ getCheckpoint(key: string): Promise<SyncCheckpoint | null>;
24
+ list(): Promise<SyncCheckpoint[]>;
25
+ /** No-op: the log (via `checkpoint.advanced`) is the write path. */
26
+ recordSync(_checkpoint: SyncCheckpoint): Promise<void>;
27
+ /** Clearing a log-derived checkpoint isn't supported (the log is append-only). */
28
+ clear(): Promise<void>;
29
+ clearAll(): Promise<void>;
30
+ }
@@ -0,0 +1,45 @@
1
+ import { EPOCH } from './state.js';
2
+ export class LogBackedSyncStore {
3
+ log;
4
+ mission;
5
+ constructor(log, mission) {
6
+ this.log = log;
7
+ this.mission = mission;
8
+ }
9
+ async checkpoint(key) {
10
+ const records = await this.log.listCheckpoints(this.mission);
11
+ const record = records.find((r) => r.key === key);
12
+ if (!record)
13
+ return null;
14
+ return {
15
+ key: record.key,
16
+ syncedAt: new Date(record.syncedAt),
17
+ recordCount: record.recordCount,
18
+ cursor: record.cursor,
19
+ mission: record.mission,
20
+ executionId: record.executionId,
21
+ };
22
+ }
23
+ async getLastSync(key) {
24
+ return (await this.checkpoint(key))?.syncedAt ?? EPOCH;
25
+ }
26
+ async getCheckpoint(key) {
27
+ return this.checkpoint(key);
28
+ }
29
+ async list() {
30
+ const records = await this.log.listCheckpoints(this.mission);
31
+ return records.map((r) => ({
32
+ key: r.key,
33
+ syncedAt: new Date(r.syncedAt),
34
+ recordCount: r.recordCount,
35
+ cursor: r.cursor,
36
+ mission: r.mission,
37
+ executionId: r.executionId,
38
+ }));
39
+ }
40
+ /** No-op: the log (via `checkpoint.advanced`) is the write path. */
41
+ async recordSync(_checkpoint) { }
42
+ /** Clearing a log-derived checkpoint isn't supported (the log is append-only). */
43
+ async clear() { }
44
+ async clearAll() { }
45
+ }
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
2
  import { EPOCH } from './state.js';
3
- import { ensureParentDirectory, writeJsonFile, readJsonFile, restoreDates, } from '../utils/file.js';
3
+ import { ensureParentDirectory, writeJsonFile, readJsonFile, restoreDates } from '../utils/file.js';
4
4
  /**
5
5
  * File-based sync store
6
6
  * Stores sync state in .reqon-data/sync/{mission}.json
@@ -14,3 +14,5 @@ export type { TraceRecorderConfig } from './recorder.js';
14
14
  export { TraceRecorder, createTraceRecorder } from './recorder.js';
15
15
  export type { ReplaySession, ReplayStepResult, TimelineEvent, VariableChange, SnapshotDiff, } from './replay.js';
16
16
  export { TraceReplayer, createTraceReplayer } from './replay.js';
17
+ export type { LogTimelineEntry, LogTraceSummary } from './log-view.js';
18
+ export { LogTraceView, traceTimelineFromLog } from './log-view.js';
@@ -10,3 +10,4 @@ export { generateSnapshotId, createExecutionTrace, safeClone, truncateForTrace }
10
10
  export { FileTraceStore, MemoryTraceStore } from './store.js';
11
11
  export { TraceRecorder, createTraceRecorder } from './recorder.js';
12
12
  export { TraceReplayer, createTraceReplayer } from './replay.js';
13
+ export { LogTraceView, traceTimelineFromLog } from './log-view.js';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Trace as a view over the execution log.
3
+ *
4
+ * Time-travel debugging and the audit trail fall straight out of the
5
+ * append-only log: the ordered sequence of events *is* the trace. This module
6
+ * derives a navigable timeline and a folded summary from `StoredEvent[]`, so a
7
+ * run's history is reconstructable from the log with no separate trace store.
8
+ *
9
+ * Scope: the log records the structural timeline (steps, effects, checkpoints,
10
+ * pauses, errors), which is what time-travel navigation and audit need. The
11
+ * richer variable-level snapshots remain the job of the opt-in {@link TraceRecorder}.
12
+ */
13
+ import type { ExecutionLogStore, StoredEvent, ExecutionEventType } from '../execution-log/index.js';
14
+ import { foldLog } from '../execution-log/index.js';
15
+ /** One step in the timeline view, derived from a single log event. */
16
+ export interface LogTimelineEntry {
17
+ /** Sequence within the execution (the time-travel cursor). */
18
+ seq: number;
19
+ /** Recorded wall-clock time (ISO 8601). */
20
+ at: string;
21
+ /** The underlying event type. */
22
+ type: ExecutionEventType;
23
+ /** Action name, for step events. */
24
+ action?: string;
25
+ /** Step identity, for step/effect events. */
26
+ step?: string;
27
+ /** Step kind (fetch, store, …), for step events. */
28
+ stepType?: string;
29
+ /** Attempt number, for step/effect events. */
30
+ attempt?: number;
31
+ /** Human-readable summary of this entry. */
32
+ detail?: string;
33
+ }
34
+ /** A folded summary of a run, for the trace overview. */
35
+ export interface LogTraceSummary {
36
+ status: ReturnType<typeof foldLog>['status'];
37
+ totalEvents: number;
38
+ stepsCompleted: number;
39
+ effectsApplied: number;
40
+ checkpoints: number;
41
+ pendingPauseId?: string;
42
+ error?: string;
43
+ }
44
+ /** Build an ordered, navigable timeline from a run's log events. */
45
+ export declare function traceTimelineFromLog(events: StoredEvent[]): LogTimelineEntry[];
46
+ /**
47
+ * Navigable, log-backed trace for one execution: the timeline and a folded
48
+ * summary, read straight from the execution log.
49
+ */
50
+ export declare class LogTraceView {
51
+ private log;
52
+ private executionId;
53
+ constructor(log: ExecutionLogStore, executionId: string);
54
+ events(): Promise<StoredEvent[]>;
55
+ timeline(): Promise<LogTimelineEntry[]>;
56
+ summary(): Promise<LogTraceSummary>;
57
+ }
@@ -0,0 +1,76 @@
1
+ import { foldLog } from '../execution-log/index.js';
2
+ function describe(event) {
3
+ switch (event.type) {
4
+ case 'mission.started':
5
+ return `mission ${event.mission} started`;
6
+ case 'step.started':
7
+ return `${event.action}: ${event.stepType} step started`;
8
+ case 'step.completed':
9
+ return `step ${event.stepId} completed`;
10
+ case 'effect.applied':
11
+ return `${event.effectType} effect applied`;
12
+ case 'checkpoint.advanced':
13
+ return `checkpoint ${event.key} -> ${event.syncedAt}`;
14
+ case 'pause.created':
15
+ return `paused (${event.pauseId})`;
16
+ case 'pause.resumed':
17
+ return `resumed by ${event.resumedBy}`;
18
+ case 'mission.completed':
19
+ return 'mission completed';
20
+ case 'mission.failed':
21
+ return `mission failed: ${event.error}`;
22
+ default:
23
+ return undefined;
24
+ }
25
+ }
26
+ /** Build an ordered, navigable timeline from a run's log events. */
27
+ export function traceTimelineFromLog(events) {
28
+ return events.map((event) => {
29
+ const entry = {
30
+ seq: event.seq,
31
+ at: event.at,
32
+ type: event.type,
33
+ detail: describe(event),
34
+ };
35
+ if ('action' in event)
36
+ entry.action = event.action;
37
+ if ('stepId' in event)
38
+ entry.step = event.stepId;
39
+ if ('stepType' in event)
40
+ entry.stepType = event.stepType;
41
+ if ('attempt' in event)
42
+ entry.attempt = event.attempt;
43
+ return entry;
44
+ });
45
+ }
46
+ /**
47
+ * Navigable, log-backed trace for one execution: the timeline and a folded
48
+ * summary, read straight from the execution log.
49
+ */
50
+ export class LogTraceView {
51
+ log;
52
+ executionId;
53
+ constructor(log, executionId) {
54
+ this.log = log;
55
+ this.executionId = executionId;
56
+ }
57
+ async events() {
58
+ return this.log.read(this.executionId);
59
+ }
60
+ async timeline() {
61
+ return traceTimelineFromLog(await this.events());
62
+ }
63
+ async summary() {
64
+ const events = await this.events();
65
+ const folded = foldLog(events);
66
+ return {
67
+ status: folded.status,
68
+ totalEvents: events.length,
69
+ stepsCompleted: folded.completedSteps.size,
70
+ effectsApplied: folded.appliedEffects.size,
71
+ checkpoints: folded.checkpoints.size,
72
+ pendingPauseId: folded.pendingPauseId,
73
+ error: folded.error,
74
+ };
75
+ }
76
+ }
@@ -57,12 +57,16 @@ export declare class TraceRecorder {
57
57
  */
58
58
  getSnapshot(index: number): TraceSnapshot | undefined;
59
59
  /**
60
- * Get the total number of snapshots recorded
60
+ * Get the total number of snapshots recorded. In streaming mode the in-memory
61
+ * array is bounded (older snapshots live only on disk), so this reflects the
62
+ * true total rather than the retained window.
61
63
  */
62
64
  getSnapshotCount(): number;
63
65
  private createSnapshot;
64
66
  private captureVariables;
65
67
  private captureStores;
68
+ /** Cap on snapshots kept in memory while streaming (each is already on disk). */
69
+ private static readonly MAX_STREAMED_IN_MEMORY;
66
70
  private addSnapshot;
67
71
  }
68
72
  /**
@@ -5,6 +5,7 @@
5
5
  * building a complete trace that can be replayed later.
6
6
  */
7
7
  import { createExecutionTrace, generateSnapshotId, safeClone, truncateForTrace, } from './state.js';
8
+ import { redactSecrets } from '../utils/redact.js';
8
9
  /**
9
10
  * Records execution state for time-travel debugging
10
11
  */
@@ -65,19 +66,24 @@ export class TraceRecorder {
65
66
  return this.trace.snapshots[index];
66
67
  }
67
68
  /**
68
- * Get the total number of snapshots recorded
69
+ * Get the total number of snapshots recorded. In streaming mode the in-memory
70
+ * array is bounded (older snapshots live only on disk), so this reflects the
71
+ * true total rather than the retained window.
69
72
  */
70
73
  getSnapshotCount() {
71
- return this.trace.snapshots.length;
74
+ return this.snapshotIndex;
72
75
  }
73
76
  createSnapshot(action, stepIndex, stepType, phase, ctx) {
74
77
  const index = this.snapshotIndex++;
75
- // Capture variables from context chain
76
- const variables = this.captureVariables(ctx);
78
+ // Capture variables from context chain. Trace files are diagnostic and
79
+ // persisted to disk, so redact credential-looking fields.
80
+ const variables = redactSecrets(this.captureVariables(ctx));
77
81
  // Capture store states
78
82
  const stores = this.captureStores(ctx);
79
83
  // Capture response (truncated for large responses)
80
- const response = this.config.mode === 'full' ? truncateForTrace(safeClone(ctx.response)) : undefined;
84
+ const response = this.config.mode === 'full'
85
+ ? redactSecrets(truncateForTrace(safeClone(ctx.response)))
86
+ : undefined;
81
87
  return {
82
88
  id: generateSnapshotId(this.trace.id, index),
83
89
  index,
@@ -128,11 +134,18 @@ export class TraceRecorder {
128
134
  }
129
135
  return stores;
130
136
  }
137
+ /** Cap on snapshots kept in memory while streaming (each is already on disk). */
138
+ static MAX_STREAMED_IN_MEMORY = 200;
131
139
  async addSnapshot(snapshot) {
132
140
  this.trace.snapshots.push(snapshot);
133
- // If streaming mode, persist immediately
134
141
  if (this.config.streaming) {
142
+ // Persisted immediately, so the in-memory copy is only a recent window for
143
+ // live inspection — bound it so a long run doesn't grow without limit.
135
144
  await this.config.store.appendSnapshot(this.trace.id, snapshot);
145
+ const cap = TraceRecorder.MAX_STREAMED_IN_MEMORY;
146
+ if (this.trace.snapshots.length > cap) {
147
+ this.trace.snapshots.splice(0, this.trace.snapshots.length - cap);
148
+ }
136
149
  }
137
150
  }
138
151
  }
@@ -35,6 +35,8 @@ export interface TraceStore {
35
35
  export declare class FileTraceStore implements TraceStore {
36
36
  private baseDir;
37
37
  private initialized;
38
+ /** Per-trace append serialization to prevent concurrent index collisions. */
39
+ private appendChain;
38
40
  constructor(baseDir?: string);
39
41
  private getTraceDir;
40
42
  private getMetadataPath;
@@ -43,6 +45,7 @@ export declare class FileTraceStore implements TraceStore {
43
45
  private deserializeSnapshot;
44
46
  save(trace: ExecutionTrace): Promise<void>;
45
47
  appendSnapshot(traceId: string, snapshot: TraceSnapshot): Promise<void>;
48
+ private doAppendSnapshot;
46
49
  load(id: string): Promise<ExecutionTrace | null>;
47
50
  loadSnapshot(traceId: string, snapshotIndex: number): Promise<TraceSnapshot | null>;
48
51
  listByMission(mission: string, limit?: number): Promise<ExecutionTrace[]>;
@@ -50,6 +53,9 @@ export declare class FileTraceStore implements TraceStore {
50
53
  delete(id: string): Promise<void>;
51
54
  findLatest(mission: string): Promise<ExecutionTrace | null>;
52
55
  getMetadata(id: string): Promise<Omit<ExecutionTrace, 'snapshots'> | null>;
56
+ /** Read the metadata file, returning the committed snapshot count and the
57
+ * trace metadata (snapshotCount stripped). */
58
+ private readMetadataRaw;
53
59
  }
54
60
  /**
55
61
  * In-memory trace store (for testing)