toolroll 0.9.46 → 0.9.48
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/CHANGELOG.md +27 -0
- package/dist/activity-line.d.ts +26 -0
- package/dist/activity-line.js +47 -0
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +18 -1
- package/dist/browser-workspace.js +9 -3
- package/dist/builder.js +4 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/contracts/cli.d.ts +87 -0
- package/dist/contracts/cli.js +2 -0
- package/dist/contracts/health.d.ts +84 -0
- package/dist/contracts/health.js +11 -0
- package/dist/flow-card-task.d.ts +2 -0
- package/dist/flow-card-task.js +57 -0
- package/dist/flow-live.d.ts +7 -15
- package/dist/flow-live.js +77 -115
- package/dist/flows-ui.js +15 -1
- package/dist/health-cli.d.ts +3 -0
- package/dist/health-cli.js +79 -0
- package/dist/live-bus.d.ts +89 -0
- package/dist/live-bus.js +321 -0
- package/dist/live.d.ts +14 -0
- package/dist/live.js +71 -22
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +53 -1
- package/dist/operate.js +43 -25
- package/dist/planner.js +4 -3
- package/dist/read-executor-worker.d.ts +1 -0
- package/dist/read-executor-worker.js +26 -0
- package/dist/read-executor.d.ts +49 -0
- package/dist/read-executor.js +119 -0
- package/dist/request-budget.d.ts +1 -0
- package/dist/request-budget.js +6 -0
- package/dist/serve.d.ts +7 -0
- package/dist/serve.js +118 -42
- package/dist/server-telemetry.d.ts +86 -0
- package/dist/server-telemetry.js +263 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/sqlite-telemetry.d.ts +9 -0
- package/dist/sqlite-telemetry.js +98 -0
- package/dist/store.d.ts +22 -0
- package/dist/store.js +59 -3
- package/dist/surface.js +2 -1
- package/dist/task-activity.d.ts +27 -0
- package/dist/task-activity.js +53 -0
- package/dist/task-live.d.ts +8 -0
- package/dist/task-live.js +47 -0
- package/dist/work-index.js +12 -1
- package/dist/workspace-revision.d.ts +2 -0
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/** Bounded, in-memory performance measurements. Never retain URLs, SQL, or identities. */
|
|
2
|
+
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
|
3
|
+
export const TELEMETRY_WINDOW_MS = 300_000;
|
|
4
|
+
const SLICE_MS = 10_000;
|
|
5
|
+
/** monitorEventLoopDelay samples by sleeping this long, so every raw sample includes it. */
|
|
6
|
+
const EVENT_LOOP_RESOLUTION_MS = 20;
|
|
7
|
+
export const LATENCY_BUCKETS = [0.0001, 0.00025, 0.0005, 0.001, 0.0025, 0.005, 0.01, 0.015, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 15, 30, 60];
|
|
8
|
+
export const ROUTE_FAMILIES = ['cli', 'mcp', 'team', 'team-stream', 'chat', 'chat-stream', 'flows', 'flow-stream', 'tasks', 'results', 'decisions', 'sessions', 'settings', 'projects', 'assets', 'auth', 'metrics', 'health', 'console', 'other'];
|
|
9
|
+
export const STREAM_FAMILIES = ['team-stream', 'chat-stream', 'flow-stream', 'other'];
|
|
10
|
+
export const QUEUE_BUCKETS = [0, 1024, 4096, 16384, 65536, 262144, 1048576];
|
|
11
|
+
/** Only literals from the allowlist can become labels, including for unknown or malformed paths. */
|
|
12
|
+
export function routeFamily(raw) {
|
|
13
|
+
let path;
|
|
14
|
+
try {
|
|
15
|
+
path = new URL(raw, 'http://localhost').pathname;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return 'other';
|
|
19
|
+
}
|
|
20
|
+
if (path === '/api/cli')
|
|
21
|
+
return 'cli';
|
|
22
|
+
if (path === '/mcp' || path.startsWith('/mcp/'))
|
|
23
|
+
return 'mcp';
|
|
24
|
+
if (path === '/api/team/events')
|
|
25
|
+
return 'team-stream';
|
|
26
|
+
if (path === '/api/team')
|
|
27
|
+
return 'team';
|
|
28
|
+
if (path === '/chat/stream')
|
|
29
|
+
return 'chat-stream';
|
|
30
|
+
if (/^\/flows\/[^/]+\/live$/.test(path))
|
|
31
|
+
return 'flow-stream';
|
|
32
|
+
if (path === '/metrics')
|
|
33
|
+
return 'metrics';
|
|
34
|
+
if (path === '/health')
|
|
35
|
+
return 'health';
|
|
36
|
+
const first = path.split('/')[1];
|
|
37
|
+
switch (first) {
|
|
38
|
+
case 'chat':
|
|
39
|
+
case 'lead': return 'chat';
|
|
40
|
+
case 'flows':
|
|
41
|
+
case 'recipes': return 'flows';
|
|
42
|
+
case 't':
|
|
43
|
+
case 'tasks':
|
|
44
|
+
case 'queue': return 'tasks';
|
|
45
|
+
case 'r':
|
|
46
|
+
case 'runs':
|
|
47
|
+
case 'review': return 'results';
|
|
48
|
+
case 'd':
|
|
49
|
+
case 'i': return 'decisions';
|
|
50
|
+
case 'code':
|
|
51
|
+
case 'session': return 'sessions';
|
|
52
|
+
case 'settings':
|
|
53
|
+
case 'people':
|
|
54
|
+
case 'spend': return 'settings';
|
|
55
|
+
case 'projects': return 'projects';
|
|
56
|
+
case 'assets': return 'assets';
|
|
57
|
+
case 'login':
|
|
58
|
+
case 'logout':
|
|
59
|
+
case 'auth': return 'auth';
|
|
60
|
+
case '':
|
|
61
|
+
case 'work':
|
|
62
|
+
case 'inbox':
|
|
63
|
+
case 'board':
|
|
64
|
+
case 'fleet':
|
|
65
|
+
case 'menu':
|
|
66
|
+
case 'done': return 'console';
|
|
67
|
+
default: return 'other';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const counts = () => ({ bins: Array.from({ length: LATENCY_BUCKETS.length + 1 }, () => 0), count: 0, sum: 0, max: 0 });
|
|
71
|
+
const summary = (value) => {
|
|
72
|
+
const percentile = (fraction) => {
|
|
73
|
+
if (value.count === 0)
|
|
74
|
+
return null;
|
|
75
|
+
const rank = Math.ceil(value.count * fraction);
|
|
76
|
+
let n = 0;
|
|
77
|
+
for (const [i, count] of value.bins.entries()) {
|
|
78
|
+
n += count;
|
|
79
|
+
if (n >= rank)
|
|
80
|
+
return Math.min(LATENCY_BUCKETS[i] ?? value.max, value.max) * 1000;
|
|
81
|
+
}
|
|
82
|
+
return value.max * 1000;
|
|
83
|
+
};
|
|
84
|
+
return { count: value.count, p50Ms: percentile(0.5), p99Ms: percentile(0.99), maxMs: value.count ? value.max * 1000 : null, totalMs: value.sum * 1000 };
|
|
85
|
+
};
|
|
86
|
+
/** Lifetime histogram plus a ring of ten-second slices. Cost and memory do not grow with traffic. */
|
|
87
|
+
export class TimingHistogram {
|
|
88
|
+
clock;
|
|
89
|
+
total = counts();
|
|
90
|
+
slices = new Map();
|
|
91
|
+
constructor(clock) {
|
|
92
|
+
this.clock = clock;
|
|
93
|
+
}
|
|
94
|
+
observe(seconds, weight = 1) {
|
|
95
|
+
if (!Number.isFinite(seconds) || seconds < 0 || weight <= 0)
|
|
96
|
+
return;
|
|
97
|
+
const at = Math.floor(this.clock() / SLICE_MS);
|
|
98
|
+
this.prune(at);
|
|
99
|
+
let slice = this.slices.get(at);
|
|
100
|
+
if (!slice) {
|
|
101
|
+
slice = counts();
|
|
102
|
+
this.slices.set(at, slice);
|
|
103
|
+
}
|
|
104
|
+
let bin = LATENCY_BUCKETS.findIndex(bound => seconds <= bound);
|
|
105
|
+
if (bin < 0)
|
|
106
|
+
bin = LATENCY_BUCKETS.length;
|
|
107
|
+
for (const value of [this.total, slice]) {
|
|
108
|
+
value.bins[bin] += weight;
|
|
109
|
+
value.count += weight;
|
|
110
|
+
value.sum += seconds * weight;
|
|
111
|
+
value.max = Math.max(value.max, seconds);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
prune(at) {
|
|
115
|
+
for (const key of this.slices.keys())
|
|
116
|
+
if (key <= at - TELEMETRY_WINDOW_MS / SLICE_MS)
|
|
117
|
+
this.slices.delete(key);
|
|
118
|
+
}
|
|
119
|
+
recent() {
|
|
120
|
+
this.prune(Math.floor(this.clock() / SLICE_MS));
|
|
121
|
+
const merged = counts();
|
|
122
|
+
for (const value of this.slices.values()) {
|
|
123
|
+
value.bins.forEach((n, i) => { merged.bins[i] += n; });
|
|
124
|
+
merged.count += value.count;
|
|
125
|
+
merged.sum += value.sum;
|
|
126
|
+
merged.max = Math.max(merged.max, value.max);
|
|
127
|
+
}
|
|
128
|
+
return summary(merged);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export class ServerTelemetry {
|
|
132
|
+
clock;
|
|
133
|
+
requests;
|
|
134
|
+
routes = new Map();
|
|
135
|
+
eventLoop;
|
|
136
|
+
writeWait;
|
|
137
|
+
writeHold;
|
|
138
|
+
writeStatement;
|
|
139
|
+
refusals = new Map();
|
|
140
|
+
recentRefusals = new Map();
|
|
141
|
+
streams = new Map();
|
|
142
|
+
monitor;
|
|
143
|
+
timer;
|
|
144
|
+
started;
|
|
145
|
+
constructor(clock = () => performance.now()) {
|
|
146
|
+
this.clock = clock;
|
|
147
|
+
this.started = clock();
|
|
148
|
+
this.requests = new TimingHistogram(clock);
|
|
149
|
+
this.eventLoop = new TimingHistogram(clock);
|
|
150
|
+
this.writeWait = new TimingHistogram(clock);
|
|
151
|
+
this.writeHold = new TimingHistogram(clock);
|
|
152
|
+
this.writeStatement = new TimingHistogram(clock);
|
|
153
|
+
for (const family of ROUTE_FAMILIES)
|
|
154
|
+
this.routes.set(family, new TimingHistogram(clock));
|
|
155
|
+
}
|
|
156
|
+
start() {
|
|
157
|
+
if (this.monitor)
|
|
158
|
+
return;
|
|
159
|
+
this.monitor = monitorEventLoopDelay({ resolution: EVENT_LOOP_RESOLUTION_MS });
|
|
160
|
+
this.monitor.enable();
|
|
161
|
+
this.timer = setInterval(() => this.sampleEventLoop(), 1000);
|
|
162
|
+
this.timer.unref();
|
|
163
|
+
}
|
|
164
|
+
stop() {
|
|
165
|
+
if (this.timer)
|
|
166
|
+
clearInterval(this.timer);
|
|
167
|
+
this.sampleEventLoop();
|
|
168
|
+
this.monitor?.disable();
|
|
169
|
+
this.monitor = undefined;
|
|
170
|
+
this.timer = undefined;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Native nanosecond samples, folded into bounded buckets once a second, never one retained object per tick.
|
|
174
|
+
* Each raw sample includes the sampling timer's own interval, so it is subtracted: an idle loop reads near zero.
|
|
175
|
+
*/
|
|
176
|
+
sampleEventLoop() {
|
|
177
|
+
const h = this.monitor;
|
|
178
|
+
if (!h || !h.count)
|
|
179
|
+
return;
|
|
180
|
+
const count = h.count, steps = Math.min(count, 1000);
|
|
181
|
+
let previous = 0;
|
|
182
|
+
for (let i = 1; i <= steps; i++) {
|
|
183
|
+
const rank = Math.floor(i * count / steps);
|
|
184
|
+
this.eventLoop.observe(Math.max(0, h.percentile(100 * rank / count) / 1e6 - EVENT_LOOP_RESOLUTION_MS) / 1000, rank - previous);
|
|
185
|
+
previous = rank;
|
|
186
|
+
}
|
|
187
|
+
h.reset();
|
|
188
|
+
}
|
|
189
|
+
budgetRefused(route, reason) {
|
|
190
|
+
const key = `${route}:${reason}`;
|
|
191
|
+
this.refusals.set(key, (this.refusals.get(key) ?? 0) + 1);
|
|
192
|
+
const at = Math.floor(this.clock() / SLICE_MS);
|
|
193
|
+
this.pruneRefusals(at);
|
|
194
|
+
this.recentRefusals.set(at, (this.recentRefusals.get(at) ?? 0) + 1);
|
|
195
|
+
}
|
|
196
|
+
pruneRefusals(at) {
|
|
197
|
+
for (const key of this.recentRefusals.keys())
|
|
198
|
+
if (key <= at - TELEMETRY_WINDOW_MS / SLICE_MS)
|
|
199
|
+
this.recentRefusals.delete(key);
|
|
200
|
+
}
|
|
201
|
+
observeRequest(family, milliseconds) {
|
|
202
|
+
this.requests.observe(milliseconds / 1000);
|
|
203
|
+
this.routes.get(family).observe(milliseconds / 1000);
|
|
204
|
+
}
|
|
205
|
+
openStream(response, family) {
|
|
206
|
+
this.streams.set(response, family);
|
|
207
|
+
const closed = () => this.streams.delete(response);
|
|
208
|
+
response.once('close', closed);
|
|
209
|
+
response.once('finish', closed);
|
|
210
|
+
}
|
|
211
|
+
streamQueues(family) {
|
|
212
|
+
return [...this.streams].filter(([response, kind]) => !response.destroyed && !response.writableEnded && (family === undefined || family === kind)).map(([response]) => response.writableLength);
|
|
213
|
+
}
|
|
214
|
+
snapshot() {
|
|
215
|
+
this.sampleEventLoop();
|
|
216
|
+
this.pruneRefusals(Math.floor(this.clock() / SLICE_MS));
|
|
217
|
+
const queues = this.streamQueues();
|
|
218
|
+
return {
|
|
219
|
+
version: 1, windowSeconds: TELEMETRY_WINDOW_MS / 1000, uptimeSeconds: (this.clock() - this.started) / 1000,
|
|
220
|
+
requests: this.requests.recent(), routes: Object.fromEntries([...this.routes].map(([route, histogram]) => [route, histogram.recent()]).filter(([, value]) => value.count > 0)),
|
|
221
|
+
eventLoop: this.eventLoop.recent(), writes: { wait: this.writeWait.recent(), hold: this.writeHold.recent(), statement: this.writeStatement.recent() },
|
|
222
|
+
streams: { open: queues.length, queuedBytes: queues.reduce((sum, bytes) => sum + bytes, 0), maxQueuedBytes: queues.reduce((max, bytes) => Math.max(max, bytes), 0) },
|
|
223
|
+
budgetRefusals: [...this.recentRefusals.values()].reduce((sum, n) => sum + n, 0),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** SSE latency is time to opening headers; a live view's lifetime is not request work. */
|
|
228
|
+
export function instrumentRequest(telemetry, request, response) {
|
|
229
|
+
const family = routeFamily(request.url ?? '/'), started = telemetry.clock();
|
|
230
|
+
let recorded = false;
|
|
231
|
+
const record = () => { if (!recorded) {
|
|
232
|
+
recorded = true;
|
|
233
|
+
telemetry.observeRequest(family, telemetry.clock() - started);
|
|
234
|
+
} };
|
|
235
|
+
const writeHead = response.writeHead;
|
|
236
|
+
response.writeHead = function (...args) {
|
|
237
|
+
const result = Reflect.apply(writeHead, this, args);
|
|
238
|
+
const headers = typeof args[1] === 'object' ? args[1] : args[2];
|
|
239
|
+
const contentType = this.getHeader('content-type') ?? (headers && !Array.isArray(headers) ? Object.entries(headers).find(([name]) => name.toLowerCase() === 'content-type')?.[1] : undefined);
|
|
240
|
+
if (String(contentType).startsWith('text/event-stream') && !recorded) {
|
|
241
|
+
record();
|
|
242
|
+
telemetry.openStream(this, STREAM_FAMILIES.includes(family) ? family : 'other');
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
};
|
|
246
|
+
response.once('finish', record);
|
|
247
|
+
response.once('close', record);
|
|
248
|
+
}
|
|
249
|
+
export function healthWords(health) {
|
|
250
|
+
const ms = (n) => n === null ? 'no samples' : `${n.toFixed(2)} ms`;
|
|
251
|
+
const timing = (value) => value.count ? `p50 ${ms(value.p50Ms)}, p99 ${ms(value.p99Ms)} (${value.count} samples)` : 'no samples yet';
|
|
252
|
+
return [
|
|
253
|
+
`Server health — last 5 minutes (up ${Math.floor(health.uptimeSeconds)} seconds; approximate percentiles)`,
|
|
254
|
+
`Requests: ${timing(health.requests)}.`,
|
|
255
|
+
...Object.entries(health.routes).map(([route, value]) => ` ${route}: ${timing(value)}.`),
|
|
256
|
+
`Event-loop delay: ${timing(health.eventLoop)}; 20 ms sampling interval.`,
|
|
257
|
+
`SQLite write-lock waits: ${timing(health.writes.wait)}.`,
|
|
258
|
+
`SQLite time holding a write transaction: ${timing(health.writes.hold)}.`,
|
|
259
|
+
`SQLite standalone writes, including lock waits: ${timing(health.writes.statement)}.`,
|
|
260
|
+
`Live streams: ${health.streams.open} open; ${health.streams.queuedBytes} bytes waiting to send, largest queue ${health.streams.maxQueuedBytes} bytes.`,
|
|
261
|
+
`Request-budget refusals: ${health.budgetRefusals}.`,
|
|
262
|
+
].join('\n');
|
|
263
|
+
}
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Database } from './store.js';
|
|
2
|
+
import type { ServerTelemetry } from './server-telemetry.js';
|
|
3
|
+
/** One observation for the entire bounded BEGIN retry, including a failed acquisition. */
|
|
4
|
+
export declare function measureWriteWait<T>(db: Database, body: () => T): T;
|
|
5
|
+
/** Preserve SQLite calls, timeouts and transaction semantics. No SQL or parameters leave this wrapper.
|
|
6
|
+
* Native SQLite does not expose busy-handler time for autocommit writes: report their whole duration separately.
|
|
7
|
+
* Runtime explicit IMMEDIATE/EXCLUSIVE transactions report acquisition and hold; startup migrations are excluded.
|
|
8
|
+
*/
|
|
9
|
+
export declare function instrumentDatabase(db: Database, telemetry: ServerTelemetry): Database;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const states = new WeakMap();
|
|
2
|
+
/** One observation for the entire bounded BEGIN retry, including a failed acquisition. */
|
|
3
|
+
export function measureWriteWait(db, body) {
|
|
4
|
+
const state = states.get(db);
|
|
5
|
+
if (!state || state.measuringWait)
|
|
6
|
+
return body();
|
|
7
|
+
const started = state.telemetry.clock();
|
|
8
|
+
state.measuringWait = true;
|
|
9
|
+
try {
|
|
10
|
+
return body();
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
state.measuringWait = false;
|
|
14
|
+
state.telemetry.writeWait.observe((state.telemetry.clock() - started) / 1000);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Preserve SQLite calls, timeouts and transaction semantics. No SQL or parameters leave this wrapper.
|
|
18
|
+
* Native SQLite does not expose busy-handler time for autocommit writes: report their whole duration separately.
|
|
19
|
+
* Runtime explicit IMMEDIATE/EXCLUSIVE transactions report acquisition and hold; startup migrations are excluded.
|
|
20
|
+
*/
|
|
21
|
+
export function instrumentDatabase(db, telemetry) {
|
|
22
|
+
const state = { telemetry, measuringWait: false };
|
|
23
|
+
let heldAt = null;
|
|
24
|
+
const elapsed = (at) => (telemetry.clock() - at) / 1000;
|
|
25
|
+
const operation = (sql) => sql.replace(/^(?:\s|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)+/, '').trim();
|
|
26
|
+
const writing = (sql) => /^(?:INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER)\b/i.test(sql) || /^WITH\b[\s\S]*\b(?:INSERT|UPDATE|DELETE|REPLACE)\b/i.test(sql);
|
|
27
|
+
const standalone = (write, body) => {
|
|
28
|
+
if (!write || heldAt !== null)
|
|
29
|
+
return body();
|
|
30
|
+
const at = telemetry.clock();
|
|
31
|
+
try {
|
|
32
|
+
return body();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
telemetry.writeStatement.observe(elapsed(at));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const finish = () => { if (heldAt !== null) {
|
|
39
|
+
telemetry.writeHold.observe(elapsed(heldAt));
|
|
40
|
+
heldAt = null;
|
|
41
|
+
} };
|
|
42
|
+
const methods = {
|
|
43
|
+
exec(sql) {
|
|
44
|
+
const op = operation(sql);
|
|
45
|
+
if (/^BEGIN\s+(?:IMMEDIATE|EXCLUSIVE)\b/i.test(op)) {
|
|
46
|
+
const at = telemetry.clock();
|
|
47
|
+
try {
|
|
48
|
+
db.exec(sql);
|
|
49
|
+
heldAt = telemetry.clock();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (!state.measuringWait)
|
|
53
|
+
telemetry.writeWait.observe(elapsed(at));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (/^(?:COMMIT|END|ROLLBACK(?!\s+TO))\b/i.test(op)) {
|
|
57
|
+
db.exec(sql);
|
|
58
|
+
finish();
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
standalone(writing(op), () => db.exec(sql));
|
|
62
|
+
},
|
|
63
|
+
prepare(sql) {
|
|
64
|
+
const statement = db.prepare(sql), write = writing(operation(sql));
|
|
65
|
+
const methods = {
|
|
66
|
+
run: (...params) => standalone(write, () => statement.run(...params)),
|
|
67
|
+
get: (...params) => standalone(write, () => statement.get(...params)),
|
|
68
|
+
all: (...params) => standalone(write, () => statement.all(...params)),
|
|
69
|
+
};
|
|
70
|
+
return preservingNative(statement, methods);
|
|
71
|
+
},
|
|
72
|
+
close() { try {
|
|
73
|
+
db.close();
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
finish();
|
|
77
|
+
} },
|
|
78
|
+
};
|
|
79
|
+
// Store.handle also exposes native extensions (for example scalar functions used by work summaries).
|
|
80
|
+
// Preserve their receivers and getters instead of narrowing the live connection to the minimal test interface.
|
|
81
|
+
const wrapped = preservingNative(db, methods);
|
|
82
|
+
states.set(wrapped, state);
|
|
83
|
+
return wrapped;
|
|
84
|
+
}
|
|
85
|
+
function preservingNative(native, methods) {
|
|
86
|
+
// Overrides belong to the facade, so a test spy or caller replacing prepare/run never mutates the
|
|
87
|
+
// native method that the timed implementation calls (and cannot recurse back into itself).
|
|
88
|
+
return new Proxy(methods, {
|
|
89
|
+
get(target, property) {
|
|
90
|
+
if (Object.hasOwn(target, property))
|
|
91
|
+
return Reflect.get(target, property);
|
|
92
|
+
const value = Reflect.get(native, property, native);
|
|
93
|
+
return typeof value === 'function' ? value.bind(native) : value;
|
|
94
|
+
},
|
|
95
|
+
has: (target, property) => Reflect.has(target, property) || Reflect.has(native, property),
|
|
96
|
+
getPrototypeOf: () => Object.getPrototypeOf(native),
|
|
97
|
+
});
|
|
98
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ServerTelemetry } from "./server-telemetry.js";
|
|
1
2
|
import type { DecisionOption } from "./contracts/decision.js";
|
|
2
3
|
import { type SavedToolAction, type SavedToolRule } from "./contracts/store-json.js";
|
|
3
4
|
import type { FlowCardChange, FlowCardFields } from "./contracts/flow-card.js";
|
|
4
5
|
import { type Actor } from "./actor.js";
|
|
6
|
+
import { type LiveActivityKind } from "./live.js";
|
|
5
7
|
import { type TokenPurpose } from "./api-tokens.js";
|
|
6
8
|
import { type CriterionMatrixRow, type CriterionJudgementWord } from "./proof.js";
|
|
7
9
|
import { type ExecutionProfile, type ChainEntry, type UnattendedPermissionMode, type AcceptanceCriterion } from "./scope.js";
|
|
@@ -69,6 +71,10 @@ export declare const FLOW_SEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS flow_send
|
|
|
69
71
|
/** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
|
|
70
72
|
* projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
|
|
71
73
|
* outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
|
|
74
|
+
/** What a running agent did last (live tasks): one row per run, a fixed kind from live.ts's
|
|
75
|
+
* vocabulary and when — never text, tool names or arguments. Display state, not evidence;
|
|
76
|
+
* a finished run's row goes with the live log's own retention (sweepRunActivity). */
|
|
77
|
+
export declare const RUN_ACTIVITY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS run_activity (\n run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('session','message','edit','command','read','search','lookup','plan','delegate','tool')),\n at TEXT NOT NULL\n);\n";
|
|
72
78
|
export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
|
|
73
79
|
/** The second tap on a flow decision in Telegram (telegram-flow.ts): Approve arms Yes and Cancel, bound to the binding,
|
|
74
80
|
* chat, message and exact visit of the card; Yes decides it once. */
|
|
@@ -1934,6 +1940,7 @@ export declare class ReviewBindingError extends Error {
|
|
|
1934
1940
|
}
|
|
1935
1941
|
export declare class Store {
|
|
1936
1942
|
#private;
|
|
1943
|
+
readonly telemetry: ServerTelemetry;
|
|
1937
1944
|
private readonly db;
|
|
1938
1945
|
constructor(db: Database);
|
|
1939
1946
|
/** TESTS ONLY: raw database access for migration fixtures. Production
|
|
@@ -2134,6 +2141,10 @@ export declare class Store {
|
|
|
2134
2141
|
* themselves when called alone.
|
|
2135
2142
|
*/
|
|
2136
2143
|
transact<T>(body: () => T): T;
|
|
2144
|
+
private readonly commitListeners;
|
|
2145
|
+
/** Hear each outer transact() commit (live-bus.ts). Listeners run synchronously
|
|
2146
|
+
* after COMMIT and must not write; the return value stops listening. */
|
|
2147
|
+
onCommit(listener: () => void): () => void;
|
|
2137
2148
|
/**
|
|
2138
2149
|
* A named SAVEPOINT for a multi-statement primitive that must roll back
|
|
2139
2150
|
* WHOLESALE on a mid-body error, EVEN when nested inside an outer
|
|
@@ -5601,6 +5612,17 @@ export declare class Store {
|
|
|
5601
5612
|
* page and focused chat render live, including once the run that wrote
|
|
5602
5613
|
* it has ended. */
|
|
5603
5614
|
latestCheckpointForTask(taskRef: number): RunCheckpoint | null;
|
|
5615
|
+
/** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
|
|
5616
|
+
recordRunActivity(run: number, kind: LiveActivityKind, now: Date): void;
|
|
5617
|
+
/** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
|
|
5618
|
+
runActivity(run: number): {
|
|
5619
|
+
kind: LiveActivityKind | null;
|
|
5620
|
+
at: string | null;
|
|
5621
|
+
progressAt: string | null;
|
|
5622
|
+
};
|
|
5623
|
+
/** The live log's own retention: a finished run's activity goes once older than `retainMs`, and beyond the newest
|
|
5624
|
+
* `bound` finished runs; a live run's always stays. */
|
|
5625
|
+
sweepRunActivity(now: Date, retainMs: number, bound: number): number;
|
|
5604
5626
|
/** One run's full checkpoint history, oldest first. */
|
|
5605
5627
|
checkpointHistory(run: number): RunCheckpoint[];
|
|
5606
5628
|
/** The newest scout run's report for a task (v34), when one exists. */
|
package/dist/store.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { chatSchema, chatTables } from "./contracts/chat-tables.js";
|
|
2
|
+
import { ServerTelemetry } from "./server-telemetry.js";
|
|
3
|
+
import { instrumentDatabase, measureWriteWait } from "./sqlite-telemetry.js";
|
|
2
4
|
import { parseStoreColumn, readStoreColumn, readStoreStringifiedList, readStoreTextList } from "./contracts/store-json.js";
|
|
3
5
|
import { assessmentFromSavedEvidence, verificationEvidence } from "./verification-evidence.js";
|
|
4
6
|
import { LEARNING_SCHEMA, queueLearning } from "./project-learning.js";
|
|
@@ -12,6 +14,7 @@ import { cardOutputsForStore, cardOutputsFromStore } from "./contracts/stage-out
|
|
|
12
14
|
import { chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
13
15
|
import { actorLabel, currentActor, leadSecretMatches, mintLeadToken, parseLeadToken } from "./actor.js";
|
|
14
16
|
import { scanForSecrets } from "./evidence.js";
|
|
17
|
+
import { LIVE_ACTIVITY_KINDS } from "./live.js";
|
|
15
18
|
import { tokenPurpose, tokenRotationProblem } from "./api-tokens.js";
|
|
16
19
|
/**
|
|
17
20
|
* The database: a small task store, and the operational overlay beside it.
|
|
@@ -359,6 +362,16 @@ CREATE INDEX IF NOT EXISTS telegram_flow_choice_visit ON telegram_flow_choice (c
|
|
|
359
362
|
/** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
|
|
360
363
|
* projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
|
|
361
364
|
* outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
|
|
365
|
+
/** What a running agent did last (live tasks): one row per run, a fixed kind from live.ts's
|
|
366
|
+
* vocabulary and when — never text, tool names or arguments. Display state, not evidence;
|
|
367
|
+
* a finished run's row goes with the live log's own retention (sweepRunActivity). */
|
|
368
|
+
export const RUN_ACTIVITY_SCHEMA = `
|
|
369
|
+
CREATE TABLE IF NOT EXISTS run_activity (
|
|
370
|
+
run INTEGER PRIMARY KEY REFERENCES run(id) ON DELETE CASCADE,
|
|
371
|
+
kind TEXT NOT NULL CHECK (kind IN ('session','message','edit','command','read','search','lookup','plan','delegate','tool')),
|
|
372
|
+
at TEXT NOT NULL
|
|
373
|
+
);
|
|
374
|
+
`;
|
|
362
375
|
export const CHAT_APPROVAL_SCHEMA = `
|
|
363
376
|
CREATE TABLE IF NOT EXISTS chat_approval_setting (
|
|
364
377
|
approver TEXT NOT NULL,
|
|
@@ -3838,6 +3851,9 @@ export function isDatabaseBusy(error) {
|
|
|
3838
3851
|
* wait, which is still counted in nominal sleep.
|
|
3839
3852
|
*/
|
|
3840
3853
|
function beginWriteWithin(db, budgetMs) {
|
|
3854
|
+
measureWriteWait(db, () => beginWriteAttempt(db, budgetMs));
|
|
3855
|
+
}
|
|
3856
|
+
function beginWriteAttempt(db, budgetMs) {
|
|
3841
3857
|
const deadline = performance.now() + budgetMs;
|
|
3842
3858
|
db.exec(`PRAGMA busy_timeout = ${WRITE_WAIT_SLICE_MS}`);
|
|
3843
3859
|
try {
|
|
@@ -4090,6 +4106,8 @@ function initializeStore(db, file) {
|
|
|
4090
4106
|
db.exec(CHAT_DECIDE_SCHEMA);
|
|
4091
4107
|
// An owner's lasting chat-approval setting (no version bump: additive only; absent means off).
|
|
4092
4108
|
db.exec(CHAT_APPROVAL_SCHEMA);
|
|
4109
|
+
// What a running agent did last (no version bump: additive only; absent means no word yet).
|
|
4110
|
+
db.exec(RUN_ACTIVITY_SCHEMA);
|
|
4093
4111
|
db.exec(TELEGRAM_FLOW_CONFIRM_SCHEMA);
|
|
4094
4112
|
// A flow's "Send to me" and "Person chooses" zones (no version bump: additive only).
|
|
4095
4113
|
db.exec(FLOW_SEND_SCHEMA);
|
|
@@ -7336,9 +7354,11 @@ export class ReviewBindingError extends Error {
|
|
|
7336
7354
|
constructor(message) { super(message); this.name = "ReviewBindingError"; }
|
|
7337
7355
|
}
|
|
7338
7356
|
export class Store {
|
|
7357
|
+
telemetry;
|
|
7339
7358
|
db;
|
|
7340
7359
|
constructor(db) {
|
|
7341
|
-
this.
|
|
7360
|
+
this.telemetry = new ServerTelemetry();
|
|
7361
|
+
this.db = instrumentDatabase(db, this.telemetry);
|
|
7342
7362
|
}
|
|
7343
7363
|
/** TESTS ONLY: raw database access for migration fixtures. Production
|
|
7344
7364
|
* code never calls this — the typed methods are the API. */
|
|
@@ -7954,10 +7974,10 @@ export class Store {
|
|
|
7954
7974
|
return body();
|
|
7955
7975
|
beginWriteWithin(this.db, CONCURRENT_WRITER_WAIT_MS);
|
|
7956
7976
|
this.transacting = true;
|
|
7977
|
+
let result;
|
|
7957
7978
|
try {
|
|
7958
|
-
|
|
7979
|
+
result = body();
|
|
7959
7980
|
this.db.exec("COMMIT");
|
|
7960
|
-
return result;
|
|
7961
7981
|
}
|
|
7962
7982
|
catch (error) {
|
|
7963
7983
|
this.db.exec("ROLLBACK");
|
|
@@ -7966,6 +7986,21 @@ export class Store {
|
|
|
7966
7986
|
finally {
|
|
7967
7987
|
this.transacting = false;
|
|
7968
7988
|
}
|
|
7989
|
+
// Only after the outer COMMIT: a rolled-back or nested body tells no one.
|
|
7990
|
+
for (const listener of this.commitListeners) {
|
|
7991
|
+
try {
|
|
7992
|
+
listener();
|
|
7993
|
+
}
|
|
7994
|
+
catch { /* a listener never fails the write */ }
|
|
7995
|
+
}
|
|
7996
|
+
return result;
|
|
7997
|
+
}
|
|
7998
|
+
commitListeners = new Set();
|
|
7999
|
+
/** Hear each outer transact() commit (live-bus.ts). Listeners run synchronously
|
|
8000
|
+
* after COMMIT and must not write; the return value stops listening. */
|
|
8001
|
+
onCommit(listener) {
|
|
8002
|
+
this.commitListeners.add(listener);
|
|
8003
|
+
return () => { this.commitListeners.delete(listener); };
|
|
7969
8004
|
}
|
|
7970
8005
|
/**
|
|
7971
8006
|
* A named SAVEPOINT for a multi-statement primitive that must roll back
|
|
@@ -17523,6 +17558,27 @@ export class Store {
|
|
|
17523
17558
|
const row = this.db.prepare("SELECT * FROM run_checkpoint WHERE task_ref = ? ORDER BY id DESC LIMIT 1").get(taskRef);
|
|
17524
17559
|
return row === undefined ? null : readRunCheckpoint(row);
|
|
17525
17560
|
}
|
|
17561
|
+
/** What the agent did last on this run (live tasks): one fixed kind and when, replacing the last. */
|
|
17562
|
+
recordRunActivity(run, kind, now) {
|
|
17563
|
+
this.db.prepare("INSERT INTO run_activity (run, kind, at) VALUES (?, ?, ?) ON CONFLICT (run) DO UPDATE SET kind = excluded.kind, at = excluded.at")
|
|
17564
|
+
.run(run, kind, now.toISOString());
|
|
17565
|
+
}
|
|
17566
|
+
/** A run's last recorded activity and its newest progress checkpoint's time, in one read. */
|
|
17567
|
+
runActivity(run) {
|
|
17568
|
+
const row = this.db.prepare(`SELECT (SELECT kind FROM run_activity WHERE run = ?1) AS kind, (SELECT at FROM run_activity WHERE run = ?1) AS at,
|
|
17569
|
+
(SELECT created_at FROM run_checkpoint WHERE run = ?1 ORDER BY id DESC LIMIT 1) AS progress_at`).get(run);
|
|
17570
|
+
const kind = typeof row["kind"] === "string" && LIVE_ACTIVITY_KINDS.includes(row["kind"]) ? row["kind"] : null;
|
|
17571
|
+
return { kind, at: kind === null ? null : String(row["at"]), progressAt: row["progress_at"] == null ? null : String(row["progress_at"]) };
|
|
17572
|
+
}
|
|
17573
|
+
/** The live log's own retention: a finished run's activity goes once older than `retainMs`, and beyond the newest
|
|
17574
|
+
* `bound` finished runs; a live run's always stays. */
|
|
17575
|
+
sweepRunActivity(now, retainMs, bound) {
|
|
17576
|
+
const finished = "run IN (SELECT id FROM run WHERE outcome IS NOT NULL)";
|
|
17577
|
+
const aged = this.db.prepare(`DELETE FROM run_activity WHERE at < ? AND ${finished}`).run(new Date(now.getTime() - retainMs).toISOString()).changes;
|
|
17578
|
+
const over = this.db.prepare(`DELETE FROM run_activity WHERE ${finished} AND run NOT IN (SELECT run FROM run_activity WHERE ${finished} ORDER BY at DESC, run DESC LIMIT ?)`)
|
|
17579
|
+
.run(Math.max(0, Math.floor(bound))).changes;
|
|
17580
|
+
return Number(aged) + Number(over);
|
|
17581
|
+
}
|
|
17526
17582
|
/** One run's full checkpoint history, oldest first. */
|
|
17527
17583
|
checkpointHistory(run) {
|
|
17528
17584
|
return this.db.prepare("SELECT * FROM run_checkpoint WHERE run = ? ORDER BY id ASC").all(run).map(readRunCheckpoint);
|
package/dist/surface.js
CHANGED
|
@@ -129,6 +129,7 @@ const GUIDE_ROWS = [
|
|
|
129
129
|
...MODELS_DESCRIPTORS.map(spec => ({ invocation: `models ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "target", required: true, meaning: "the CLI to update, or on/off" }] } : {}) })),
|
|
130
130
|
// ---- the queue (agent surface) ----
|
|
131
131
|
{ invocation: "status", synopsis: "running work, queued reasons, results to review, the latest release check and plan windows", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
|
|
132
|
+
{ invocation: 'health', synopsis: 'recent server latency, event-loop delay, SQLite writes and live streams from the saved connection; instance operators only', audience: 'agent', agentMayInvoke: true, mutation: 'none', flags: [jsonFlag, { name: 'profile', takesValue: true, meaning: 'saved server connection' }] },
|
|
132
133
|
{ invocation: "integrations", synopsis: "every integration as Connected, Not set up or Broken, with its last success and error, what uses it and what to do; --saved skips new checks; never a secret", audience: "agent", agentMayInvoke: true, mutation: "none",
|
|
133
134
|
flags: [jsonFlag, dbFlag, { name: "saved", takesValue: false, meaning: "show the last checks without checking again" }] },
|
|
134
135
|
{ invocation: "ready", synopsis: "what could be dispatched right now (rows carry reservedFor)", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
|
|
@@ -446,7 +447,7 @@ const REMOTE_NO = [
|
|
|
446
447
|
"conversation read", "conversation follow", "conversation stop",
|
|
447
448
|
"session capabilities", "session list", "session show", "session changes", "session start", "session send", "session stop", "session resume", "session recover",
|
|
448
449
|
// This machine's checkout, saved profile, credentials and catch-up.
|
|
449
|
-
"project show", "project use", "brief", "lead token", "lead say",
|
|
450
|
+
"project show", "project use", "brief", "lead token", "lead say", "health",
|
|
450
451
|
"assignment show", "assignment updates", "assignment claim", "assignment check", "assignment brief", "assignment inbox", "assignment ack",
|
|
451
452
|
// Workers, the loop and the services that run here.
|
|
452
453
|
"claim", "heartbeat", "release", "reap", "tick", "build", "reconcile",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a running agent did last, recorded and read (live tasks).
|
|
3
|
+
*
|
|
4
|
+
* The recorder sits beside the live log on the agent's stream and keeps one
|
|
5
|
+
* row per run: a fixed kind from live.ts's vocabulary and when. No text, no
|
|
6
|
+
* tool names, no arguments are ever kept, so nothing here can leak a secret.
|
|
7
|
+
* The reader picks the newer of that row and the run's latest progress
|
|
8
|
+
* checkpoint, and asks the runner's heartbeat whether the worker still
|
|
9
|
+
* answers. Neither side reads or stats a live-log file.
|
|
10
|
+
*/
|
|
11
|
+
import { type RunActivity } from "./activity-line.js";
|
|
12
|
+
import type { Store } from "./store.js";
|
|
13
|
+
type Observer = {
|
|
14
|
+
observe(event: Record<string, unknown>): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
};
|
|
17
|
+
/** Watch a run's stream for what the agent does, alongside its live log (which may be null). Fail-soft: a write
|
|
18
|
+
* that fails never touches the run. */
|
|
19
|
+
export declare function withRunActivity(liveLog: Observer | null, store: Pick<Store, "recordRunActivity">, run: number, clock: () => Date): Observer;
|
|
20
|
+
/** A live run's last activity: the newer of its recorded kind and its latest progress checkpoint, else when it
|
|
21
|
+
* started; and "offline" only when its registered worker's heartbeat has lapsed. */
|
|
22
|
+
export declare function runActivityOf(store: Store, run: {
|
|
23
|
+
id: number;
|
|
24
|
+
runner: string;
|
|
25
|
+
startedAt: string;
|
|
26
|
+
}, now: Date): RunActivity;
|
|
27
|
+
export {};
|