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.
- package/README.md +23 -3
- package/dist/ast/nodes.d.ts +8 -0
- package/dist/auth/circuit-breaker.d.ts +11 -0
- package/dist/auth/circuit-breaker.js +83 -12
- package/dist/auth/credentials.d.ts +6 -1
- package/dist/auth/credentials.js +12 -4
- package/dist/auth/oauth2-provider.js +13 -3
- package/dist/auth/rate-limiter.d.ts +8 -1
- package/dist/auth/rate-limiter.js +30 -10
- package/dist/auth/token-store.js +8 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +65 -6
- package/dist/config/constants.d.ts +15 -4
- package/dist/config/constants.js +15 -4
- package/dist/control/server.d.ts +17 -0
- package/dist/control/server.js +82 -5
- package/dist/control/types.d.ts +6 -0
- package/dist/debug/cli-debugger.js +8 -3
- package/dist/execution/store.js +2 -2
- package/dist/execution-log/events.d.ts +125 -0
- package/dist/execution-log/events.js +17 -0
- package/dist/execution-log/fold.d.ts +38 -0
- package/dist/execution-log/fold.js +54 -0
- package/dist/execution-log/index.d.ts +18 -0
- package/dist/execution-log/index.js +6 -0
- package/dist/execution-log/postgres-store.d.ts +36 -0
- package/dist/execution-log/postgres-store.js +108 -0
- package/dist/execution-log/resume.d.ts +11 -0
- package/dist/execution-log/resume.js +5 -0
- package/dist/execution-log/sqlite-store.d.ts +16 -0
- package/dist/execution-log/sqlite-store.js +101 -0
- package/dist/execution-log/store.d.ts +72 -0
- package/dist/execution-log/store.js +182 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/interpreter/context.d.ts +15 -0
- package/dist/interpreter/context.js +3 -0
- package/dist/interpreter/evaluator.js +38 -8
- package/dist/interpreter/executor.d.ts +63 -1
- package/dist/interpreter/executor.js +406 -30
- package/dist/interpreter/fetch-handler.d.ts +39 -1
- package/dist/interpreter/fetch-handler.js +84 -15
- package/dist/interpreter/http.d.ts +31 -2
- package/dist/interpreter/http.js +187 -26
- package/dist/interpreter/index.d.ts +3 -3
- package/dist/interpreter/index.js +3 -3
- package/dist/interpreter/pagination.d.ts +1 -1
- package/dist/interpreter/pagination.js +7 -1
- package/dist/interpreter/step-handlers/for-handler.d.ts +3 -0
- package/dist/interpreter/step-handlers/for-handler.js +18 -3
- package/dist/interpreter/step-handlers/match-handler.js +5 -2
- package/dist/interpreter/step-handlers/store-handler.d.ts +7 -1
- package/dist/interpreter/step-handlers/store-handler.js +25 -16
- package/dist/interpreter/step-handlers/validate-handler.js +4 -1
- package/dist/interpreter/step-handlers/webhook-handler.d.ts +1 -0
- package/dist/interpreter/step-handlers/webhook-handler.js +13 -3
- package/dist/interpreter/store-manager.d.ts +1 -1
- package/dist/interpreter/store-manager.js +5 -1
- package/dist/loader/index.js +5 -8
- package/dist/mcp/sandbox.d.ts +41 -0
- package/dist/mcp/sandbox.js +76 -0
- package/dist/mcp/server.js +62 -9
- package/dist/oas/loader.d.ts +13 -1
- package/dist/oas/loader.js +25 -3
- package/dist/oas/mock-generator.js +13 -4
- package/dist/oas/validator.js +45 -5
- package/dist/observability/events.d.ts +6 -2
- package/dist/observability/events.js +0 -5
- package/dist/observability/logger.js +17 -10
- package/dist/observability/otel.d.ts +8 -0
- package/dist/observability/otel.js +45 -10
- package/dist/parser/action-parser.js +2 -2
- package/dist/parser/base.d.ts +7 -0
- package/dist/parser/base.js +11 -0
- package/dist/parser/expressions.d.ts +1 -0
- package/dist/parser/expressions.js +17 -4
- package/dist/parser/fetch-parser.js +13 -2
- package/dist/pause/index.d.ts +1 -0
- package/dist/pause/index.js +1 -0
- package/dist/pause/log-store.d.ts +33 -0
- package/dist/pause/log-store.js +98 -0
- package/dist/pause/manager.d.ts +12 -0
- package/dist/pause/manager.js +77 -28
- package/dist/pause/store.js +5 -3
- package/dist/scheduler/cron-parser.d.ts +10 -3
- package/dist/scheduler/cron-parser.js +227 -48
- package/dist/scheduler/scheduler.js +56 -22
- package/dist/stores/factory.d.ts +6 -0
- package/dist/stores/factory.js +11 -1
- package/dist/stores/file.js +9 -17
- package/dist/stores/memory.js +3 -12
- package/dist/stores/postgrest.d.ts +28 -0
- package/dist/stores/postgrest.js +84 -37
- package/dist/sync/index.d.ts +3 -2
- package/dist/sync/index.js +2 -1
- package/dist/sync/log-store.d.ts +30 -0
- package/dist/sync/log-store.js +45 -0
- package/dist/sync/store.js +1 -1
- package/dist/trace/index.d.ts +2 -0
- package/dist/trace/index.js +1 -0
- package/dist/trace/log-view.d.ts +57 -0
- package/dist/trace/log-view.js +76 -0
- package/dist/trace/recorder.d.ts +5 -1
- package/dist/trace/recorder.js +19 -6
- package/dist/trace/store.d.ts +6 -0
- package/dist/trace/store.js +47 -22
- package/dist/utils/deep-merge.d.ts +10 -0
- package/dist/utils/deep-merge.js +23 -0
- package/dist/utils/file.d.ts +13 -4
- package/dist/utils/file.js +70 -12
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/long-timeout.d.ts +19 -0
- package/dist/utils/long-timeout.js +33 -0
- package/dist/utils/path.d.ts +22 -1
- package/dist/utils/path.js +46 -1
- package/dist/utils/redact.d.ts +22 -0
- package/dist/utils/redact.js +42 -0
- package/dist/webhook/server.d.ts +9 -0
- package/dist/webhook/server.js +115 -30
- package/dist/webhook/types.d.ts +9 -1
- 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>;
|
package/dist/stores/postgrest.js
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
116
|
+
const response = await this.request(this.baseUrl, {
|
|
52
117
|
method: 'POST',
|
|
53
118
|
headers: {
|
|
54
119
|
...this.headers,
|
|
55
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
199
|
+
const response = await this.request(this.baseUrl, {
|
|
146
200
|
method: 'POST',
|
|
147
201
|
headers: {
|
|
148
202
|
...this.headers,
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
222
|
+
const response = await this.request(url, {
|
|
176
223
|
method: 'GET',
|
|
177
224
|
headers: {
|
|
178
225
|
...this.headers,
|
|
179
|
-
|
|
226
|
+
Prefer: 'count=exact',
|
|
180
227
|
},
|
|
181
228
|
});
|
|
182
229
|
if (!response.ok) {
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type { SyncCheckpoint, SyncState, SinceResolution, SinceDateFormat
|
|
2
|
-
export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH
|
|
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';
|
package/dist/sync/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { generateCheckpointKey, formatSinceDate, parseSinceDate, EPOCH
|
|
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
|
+
}
|
package/dist/sync/store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { EPOCH } from './state.js';
|
|
3
|
-
import { ensureParentDirectory, writeJsonFile, readJsonFile, restoreDates
|
|
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
|
package/dist/trace/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/trace/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/trace/recorder.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/trace/recorder.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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'
|
|
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
|
}
|
package/dist/trace/store.d.ts
CHANGED
|
@@ -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)
|