vouchington-tooling 0.1.0 → 0.1.2
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 +7 -1
- package/dist/browser-session-runner/attempt.mjs +38 -37
- package/dist/browser-session-runner/index.d.mts +2 -2
- package/dist/browser-session-runner/index.mjs +10 -3
- package/dist/browser-session-runner/output.d.mts +8 -0
- package/dist/browser-session-runner/output.mjs +24 -0
- package/dist/browser-session-runner/process-group.mjs +19 -0
- package/dist/browser-session-runner/types.d.mts +18 -1
- package/dist/browser-session-runner/watchdog.d.mts +10 -0
- package/dist/browser-session-runner/watchdog.mjs +38 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/retrospective-transcript/codex-segment.d.mts +2 -0
- package/dist/retrospective-transcript/codex-segment.mjs +73 -0
- package/dist/retrospective-transcript/codex.d.mts +0 -1
- package/dist/retrospective-transcript/codex.mjs +0 -4
- package/dist/retrospective-transcript/index.mjs +13 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,11 @@ import {
|
|
|
90
90
|
import { createArtifactClassifier, runCleanup } from 'vouchington-tooling/gha-artifacts-cleanup'
|
|
91
91
|
import { validateOptionalHttpOrigin } from 'vouchington-tooling/http-origin'
|
|
92
92
|
import { boundPendingLine, splitCompleteLines } from 'vouchington-tooling/process-line-buffer'
|
|
93
|
-
import {
|
|
93
|
+
import {
|
|
94
|
+
isProcessGroupAlive,
|
|
95
|
+
runBrowserSession,
|
|
96
|
+
waitForProcessGroupExit,
|
|
97
|
+
} from 'vouchington-tooling/browser-session-runner'
|
|
94
98
|
import {
|
|
95
99
|
generateSchemaSnapshot,
|
|
96
100
|
renderSchemaMarkdown,
|
|
@@ -144,3 +148,5 @@ are terminal. Output is decoded independently per stream; unfinished lines are c
|
|
|
144
148
|
`diagnosticTailBytes` retains a UTF-8-safe tail no larger than its byte budget.
|
|
145
149
|
The runner uses monotonic elapsed time, rejects timer budgets above Node's maximum delay, and waits for
|
|
146
150
|
the dedicated process group to exit after the direct child closes so descendants cannot overlap a retry.
|
|
151
|
+
`isProcessGroupAlive` and `waitForProcessGroupExit` are also available when callers need the same
|
|
152
|
+
process-group probe and bounded-drain semantics outside a browser session.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { boundPendingLine, splitCompleteLines } from '../process-line-buffer/index.mjs';
|
|
1
|
+
import { createOutputConsumer } from './output.mjs';
|
|
3
2
|
import { tailText } from './tail.mjs';
|
|
4
3
|
import { TailQueue } from './tail-queue.mjs';
|
|
4
|
+
import { createWatchdogLifecycle, registerWatchdogSetup } from './watchdog.mjs';
|
|
5
5
|
export function runAttempt(options, deps, deadline, attempt) {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
const startedAt = deps.now(), process = options.start(attempt);
|
|
@@ -20,7 +20,9 @@ export function runAttempt(options, deps, deadline, attempt) {
|
|
|
20
20
|
let startupProgress = false, semanticProgress = false;
|
|
21
21
|
let failure, hasFailure = false, childExited = false, closeExit, draining = false, drainDone = false, reason = 'exit', complete = false, terminating = false;
|
|
22
22
|
let killTimer, stallTimer;
|
|
23
|
+
const watchdog = createWatchdogLifecycle();
|
|
23
24
|
const listeners = [];
|
|
25
|
+
const captureFailure = (error) => !hasFailure && ((failure = error), (hasFailure = true));
|
|
24
26
|
const finish = (exit) => {
|
|
25
27
|
if (complete)
|
|
26
28
|
return;
|
|
@@ -29,6 +31,7 @@ export function runAttempt(options, deps, deadline, attempt) {
|
|
|
29
31
|
if (killTimer)
|
|
30
32
|
deps.clearTimeout(killTimer);
|
|
31
33
|
deps.clearTimeout(stallTimer);
|
|
34
|
+
watchdog.complete(captureFailure);
|
|
32
35
|
for (const remove of listeners)
|
|
33
36
|
remove();
|
|
34
37
|
const result = {
|
|
@@ -78,10 +81,9 @@ export function runAttempt(options, deps, deadline, attempt) {
|
|
|
78
81
|
signal('SIGTERM');
|
|
79
82
|
};
|
|
80
83
|
const fail = (error) => {
|
|
81
|
-
if (hasFailure)
|
|
84
|
+
if (hasFailure || complete)
|
|
82
85
|
return;
|
|
83
|
-
|
|
84
|
-
hasFailure = true;
|
|
86
|
+
captureFailure(error);
|
|
85
87
|
terminate('exit');
|
|
86
88
|
};
|
|
87
89
|
const drain = () => {
|
|
@@ -102,39 +104,22 @@ export function runAttempt(options, deps, deadline, attempt) {
|
|
|
102
104
|
deps.clearTimeout(stallTimer);
|
|
103
105
|
stallTimer = deps.setTimeout(() => terminate(nextReason), ms);
|
|
104
106
|
};
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (startupProgress && !childExited)
|
|
118
|
-
armStall(options.semanticStallMs, 'semantic-stall');
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
const appendLines = (text) => {
|
|
122
|
-
tail.append(text);
|
|
123
|
-
const split = splitCompleteLines(pending + text);
|
|
124
|
-
pending = boundPendingLine(split.pending);
|
|
125
|
-
for (const value of split.complete)
|
|
126
|
-
line(value);
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
flush: () => {
|
|
130
|
-
appendLines(decoder.end());
|
|
131
|
-
if (pending)
|
|
132
|
-
line(pending);
|
|
133
|
-
},
|
|
134
|
-
write: (chunk) => appendLines(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))),
|
|
135
|
-
};
|
|
107
|
+
const line = (value) => {
|
|
108
|
+
const event = options.onLine(value);
|
|
109
|
+
if (event === 'startup' && !startupProgress) {
|
|
110
|
+
startupProgress = true;
|
|
111
|
+
if (!childExited)
|
|
112
|
+
armStall(options.semanticStallMs, 'semantic-stall');
|
|
113
|
+
}
|
|
114
|
+
if (event === 'semantic') {
|
|
115
|
+
semanticProgress = true;
|
|
116
|
+
if (startupProgress && !childExited)
|
|
117
|
+
armStall(options.semanticStallMs, 'semantic-stall');
|
|
118
|
+
}
|
|
136
119
|
};
|
|
137
|
-
const streams = [process.stdout, process.stderr].map((stream) => stream
|
|
120
|
+
const streams = [process.stdout, process.stderr].map((stream, index) => stream
|
|
121
|
+
? createOutputConsumer(options, tail, index === 0 ? 'stdout' : 'stderr', line)
|
|
122
|
+
: undefined);
|
|
138
123
|
for (const [index, stream] of [process.stdout, process.stderr].entries())
|
|
139
124
|
stream?.on('data', (chunk) => {
|
|
140
125
|
try {
|
|
@@ -191,5 +176,21 @@ export function runAttempt(options, deps, deadline, attempt) {
|
|
|
191
176
|
if (drainDone || hasFailure)
|
|
192
177
|
finish(closeExit);
|
|
193
178
|
});
|
|
179
|
+
try {
|
|
180
|
+
const setup = options.watchdog?.({
|
|
181
|
+
attempt,
|
|
182
|
+
deadline,
|
|
183
|
+
now: () => deps.now(),
|
|
184
|
+
process,
|
|
185
|
+
terminate: () => {
|
|
186
|
+
if (!childExited)
|
|
187
|
+
terminate('provider-watchdog');
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
registerWatchdogSetup(setup, watchdog, fail);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
fail(error);
|
|
194
|
+
}
|
|
194
195
|
});
|
|
195
196
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { BrowserSessionDeps, BrowserSessionOptions, BrowserSessionResult } from './types.mts';
|
|
2
|
-
export type { BrowserSessionDeps, BrowserSessionEvent, BrowserSessionExit, BrowserSessionOptions, BrowserSessionProcess, BrowserSessionResult, } from './types.mts';
|
|
3
|
-
export { ProcessGroupDrainTimeoutError } from './process-group.mts';
|
|
2
|
+
export type { BrowserSessionDeps, BrowserSessionEvent, BrowserSessionExit, BrowserSessionOptions, BrowserSessionOutput, BrowserSessionProcess, BrowserSessionResult, BrowserSessionTerminationReason, BrowserSessionWatchdog, BrowserSessionWatchdogCleanup, BrowserSessionWatchdogController, } from './types.mts';
|
|
3
|
+
export { isProcessGroupAlive, ProcessGroupDrainTimeoutError, waitForProcessGroupExit, } from './process-group.mts';
|
|
4
4
|
export declare function runBrowserSession(options: BrowserSessionOptions, deps?: BrowserSessionDeps): Promise<BrowserSessionResult>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runAttempt } from './attempt.mjs';
|
|
2
2
|
import { expiredResult } from './result.mjs';
|
|
3
3
|
import { isProcessGroupAlive, waitForProcessGroupExit } from './process-group.mjs';
|
|
4
|
-
export { ProcessGroupDrainTimeoutError } from './process-group.mjs';
|
|
4
|
+
export { isProcessGroupAlive, ProcessGroupDrainTimeoutError, waitForProcessGroupExit, } from './process-group.mjs';
|
|
5
5
|
const defaultDeps = {
|
|
6
6
|
clearInterval,
|
|
7
7
|
clearTimeout,
|
|
@@ -31,11 +31,12 @@ export async function runBrowserSession(options, deps = defaultDeps) {
|
|
|
31
31
|
let attempts = 0;
|
|
32
32
|
for (let attempt = 1; attempt <= options.attempts && deps.now() < deadline; attempt += 1) {
|
|
33
33
|
attempts = attempt;
|
|
34
|
-
last = await runAttempt(options, deps, deadline, attempt);
|
|
34
|
+
last = { ...(await runAttempt(options, deps, deadline, attempt)), attempts };
|
|
35
|
+
options.onAttemptComplete?.(snapshotResult(last));
|
|
35
36
|
if (last.reason === 'parent-signal' ||
|
|
36
37
|
last.reason === 'deadline' ||
|
|
37
38
|
options.classifyExit(last.exit, omitAttempts(last)) !== 'retry')
|
|
38
|
-
return
|
|
39
|
+
return last;
|
|
39
40
|
}
|
|
40
41
|
if (attempts === options.attempts)
|
|
41
42
|
return { ...last, attempts };
|
|
@@ -43,6 +44,7 @@ export async function runBrowserSession(options, deps = defaultDeps) {
|
|
|
43
44
|
}
|
|
44
45
|
function validateOptions(options) {
|
|
45
46
|
const maxTimerDelay = 2_147_483_647;
|
|
47
|
+
const drainTimeoutMs = options.graceMs + (options.processGroupDrainMs ?? options.graceMs);
|
|
46
48
|
for (const [name, value] of [
|
|
47
49
|
['attempts', options.attempts],
|
|
48
50
|
['deadlineMs', options.deadlineMs],
|
|
@@ -58,7 +60,12 @@ function validateOptions(options) {
|
|
|
58
60
|
const tailBytes = options.diagnosticTailBytes ?? 4096;
|
|
59
61
|
if (!Number.isSafeInteger(tailBytes) || tailBytes <= 0)
|
|
60
62
|
throw new RangeError('diagnosticTailBytes must be positive');
|
|
63
|
+
if (!Number.isSafeInteger(drainTimeoutMs) || drainTimeoutMs > maxTimerDelay)
|
|
64
|
+
throw new RangeError('graceMs + processGroupDrainMs must be a positive Node timer delay');
|
|
61
65
|
}
|
|
62
66
|
function omitAttempts({ attempts: _attempts, ...result }) {
|
|
63
67
|
return result;
|
|
64
68
|
}
|
|
69
|
+
function snapshotResult(result) {
|
|
70
|
+
return { ...result, exit: { ...result.exit } };
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BrowserSessionOptions, BrowserSessionOutput } from './types.mts';
|
|
2
|
+
import type { TailQueue } from './tail-queue.mts';
|
|
3
|
+
type OutputConsumer = {
|
|
4
|
+
flush(): void;
|
|
5
|
+
write(chunk: string | Buffer): void;
|
|
6
|
+
};
|
|
7
|
+
export declare function createOutputConsumer(options: BrowserSessionOptions, tail: TailQueue, source: BrowserSessionOutput['source'], onLine: (line: string) => void): OutputConsumer;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
+
import { boundPendingLine, splitCompleteLines } from '../process-line-buffer/index.mjs';
|
|
3
|
+
export function createOutputConsumer(options, tail, source, onLine) {
|
|
4
|
+
const decoder = new StringDecoder('utf8');
|
|
5
|
+
let pending = '';
|
|
6
|
+
const appendLines = (text) => {
|
|
7
|
+
tail.append(text);
|
|
8
|
+
const split = splitCompleteLines(pending + text);
|
|
9
|
+
pending = boundPendingLine(split.pending);
|
|
10
|
+
for (const value of split.complete)
|
|
11
|
+
onLine(value);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
flush: () => {
|
|
15
|
+
appendLines(decoder.end());
|
|
16
|
+
if (pending)
|
|
17
|
+
onLine(pending);
|
|
18
|
+
},
|
|
19
|
+
write: (chunk) => {
|
|
20
|
+
options.onOutput?.({ chunk, source });
|
|
21
|
+
appendLines(decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -5,6 +5,8 @@ export class ProcessGroupDrainTimeoutError extends Error {
|
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
7
|
export function isProcessGroupAlive(processGroupId) {
|
|
8
|
+
validateProcessGroupId(processGroupId);
|
|
9
|
+
assertSupportedPlatform();
|
|
8
10
|
try {
|
|
9
11
|
process.kill(-processGroupId, 0);
|
|
10
12
|
return true;
|
|
@@ -14,6 +16,9 @@ export function isProcessGroupAlive(processGroupId) {
|
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
export async function waitForProcessGroupExit(processGroupId, timeoutMs) {
|
|
19
|
+
validateProcessGroupId(processGroupId);
|
|
20
|
+
validateTimerDelay(timeoutMs);
|
|
21
|
+
assertSupportedPlatform();
|
|
17
22
|
const deadline = performance.now() + timeoutMs;
|
|
18
23
|
while (isProcessGroupAlive(processGroupId)) {
|
|
19
24
|
if (performance.now() >= deadline)
|
|
@@ -21,3 +26,17 @@ export async function waitForProcessGroupExit(processGroupId, timeoutMs) {
|
|
|
21
26
|
await new Promise((resolve) => setTimeout(resolve, Math.min(10, deadline - performance.now())));
|
|
22
27
|
}
|
|
23
28
|
}
|
|
29
|
+
function assertSupportedPlatform() {
|
|
30
|
+
if (process.platform === 'win32')
|
|
31
|
+
throw new Error('process-group control is unsupported on Windows');
|
|
32
|
+
}
|
|
33
|
+
function validateProcessGroupId(processGroupId) {
|
|
34
|
+
if (!Number.isSafeInteger(processGroupId) ||
|
|
35
|
+
processGroupId <= 1 ||
|
|
36
|
+
processGroupId > 2_147_483_647)
|
|
37
|
+
throw new RangeError('processGroupId must be a positive supported PID');
|
|
38
|
+
}
|
|
39
|
+
function validateTimerDelay(timeoutMs) {
|
|
40
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
|
|
41
|
+
throw new RangeError('timeoutMs must be a positive Node timer delay');
|
|
42
|
+
}
|
|
@@ -4,6 +4,11 @@ export type BrowserSessionExit = {
|
|
|
4
4
|
code: number | null;
|
|
5
5
|
signal: NodeJS.Signals | null;
|
|
6
6
|
};
|
|
7
|
+
export type BrowserSessionOutput = {
|
|
8
|
+
chunk: string | Buffer;
|
|
9
|
+
source: 'stderr' | 'stdout';
|
|
10
|
+
};
|
|
11
|
+
export type BrowserSessionTerminationReason = 'deadline' | 'parent-signal' | 'provider-watchdog' | 'semantic-stall' | 'startup-stall';
|
|
7
12
|
type BrowserSessionStream = {
|
|
8
13
|
on(event: 'data', listener: (chunk: string | Buffer) => void): unknown;
|
|
9
14
|
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
@@ -13,7 +18,7 @@ export type BrowserSessionResult = {
|
|
|
13
18
|
deadlineExceeded: boolean;
|
|
14
19
|
diagnosticTail: string;
|
|
15
20
|
exit: BrowserSessionExit;
|
|
16
|
-
reason: 'exit' |
|
|
21
|
+
reason: 'exit' | BrowserSessionTerminationReason;
|
|
17
22
|
startupProgress: boolean;
|
|
18
23
|
semanticProgress: boolean;
|
|
19
24
|
};
|
|
@@ -37,6 +42,15 @@ export type BrowserSessionDeps = {
|
|
|
37
42
|
setTimeout(callback: () => void, ms: number): unknown;
|
|
38
43
|
waitForProcessGroupExit(processGroupId: number, timeoutMs: number): Promise<void>;
|
|
39
44
|
};
|
|
45
|
+
export type BrowserSessionWatchdogController = {
|
|
46
|
+
attempt: number;
|
|
47
|
+
deadline: number;
|
|
48
|
+
now(): number;
|
|
49
|
+
process: BrowserSessionProcess;
|
|
50
|
+
terminate(reason?: 'provider-watchdog'): void;
|
|
51
|
+
};
|
|
52
|
+
export type BrowserSessionWatchdogCleanup = () => void;
|
|
53
|
+
export type BrowserSessionWatchdog = (controller: BrowserSessionWatchdogController) => void | BrowserSessionWatchdogCleanup | Promise<void | BrowserSessionWatchdogCleanup>;
|
|
40
54
|
export type BrowserSessionOptions = {
|
|
41
55
|
attempts: number;
|
|
42
56
|
classifyExit(exit: BrowserSessionExit, result: Omit<BrowserSessionResult, 'attempts'>): 'retry' | 'return';
|
|
@@ -44,10 +58,13 @@ export type BrowserSessionOptions = {
|
|
|
44
58
|
diagnosticTailBytes?: number;
|
|
45
59
|
graceMs: number;
|
|
46
60
|
onLine(line: string): BrowserSessionEvent | undefined;
|
|
61
|
+
onOutput?(output: BrowserSessionOutput): void;
|
|
62
|
+
onAttemptComplete?(result: BrowserSessionResult): void;
|
|
47
63
|
processGroupDrainMs?: number;
|
|
48
64
|
semanticStallMs: number;
|
|
49
65
|
start(attempt: number): BrowserSessionProcess;
|
|
50
66
|
startupStallMs: number;
|
|
51
67
|
watchdogIntervalMs?: number;
|
|
68
|
+
watchdog?: BrowserSessionWatchdog;
|
|
52
69
|
};
|
|
53
70
|
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type WatchdogCleanup = () => void;
|
|
2
|
+
type WatchdogSetup = void | WatchdogCleanup | Promise<void | WatchdogCleanup>;
|
|
3
|
+
type WatchdogLifecycle = ReturnType<typeof createWatchdogLifecycle>;
|
|
4
|
+
export declare function createWatchdogLifecycle(): {
|
|
5
|
+
complete: (onError: (error: unknown) => void) => void;
|
|
6
|
+
isComplete: () => boolean;
|
|
7
|
+
register: (next: void | WatchdogCleanup, onError: (error: unknown) => void) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerWatchdogSetup(setup: WatchdogSetup, lifecycle: WatchdogLifecycle, onError: (error: unknown) => void): void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function createWatchdogLifecycle() {
|
|
2
|
+
let complete = false;
|
|
3
|
+
let cleanup;
|
|
4
|
+
const run = (next, onError) => {
|
|
5
|
+
try {
|
|
6
|
+
next();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
onError(error);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
complete: (onError) => {
|
|
14
|
+
complete = true;
|
|
15
|
+
if (!cleanup)
|
|
16
|
+
return;
|
|
17
|
+
const next = cleanup;
|
|
18
|
+
cleanup = undefined;
|
|
19
|
+
run(next, onError);
|
|
20
|
+
},
|
|
21
|
+
isComplete: () => complete,
|
|
22
|
+
register: (next, onError) => {
|
|
23
|
+
if (!next)
|
|
24
|
+
return;
|
|
25
|
+
if (complete)
|
|
26
|
+
return run(next, onError);
|
|
27
|
+
cleanup = next;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function registerWatchdogSetup(setup, lifecycle, onError) {
|
|
32
|
+
if (!(setup instanceof Promise))
|
|
33
|
+
return lifecycle.register(setup, onError);
|
|
34
|
+
void setup.then((cleanup) => lifecycle.register(cleanup, onError), (error) => {
|
|
35
|
+
if (!lifecycle.isComplete())
|
|
36
|
+
onError(error);
|
|
37
|
+
});
|
|
38
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -22,8 +22,8 @@ export { createArtifactClassifier, parseArtifactPatternsJson, planRunDeletions,
|
|
|
22
22
|
export type { ArtifactClassification, ArtifactClassifier, ArtifactPatterns, CleanupRequest, DeletionSummary, } from './gha-artifacts-cleanup/index.mts';
|
|
23
23
|
export { validateOptionalHttpOrigin } from './http-origin/index.mts';
|
|
24
24
|
export { boundPendingLine, DEFAULT_MAX_PENDING_LINE_LENGTH, DEFAULT_TRUNCATED_LINE_MARKER, splitCompleteLines, } from './process-line-buffer/index.mts';
|
|
25
|
-
export { ProcessGroupDrainTimeoutError, runBrowserSession, } from './browser-session-runner/index.mts';
|
|
26
|
-
export type { BrowserSessionDeps, BrowserSessionEvent, BrowserSessionExit, BrowserSessionOptions, BrowserSessionProcess, BrowserSessionResult, } from './browser-session-runner/index.mts';
|
|
25
|
+
export { isProcessGroupAlive, ProcessGroupDrainTimeoutError, runBrowserSession, waitForProcessGroupExit, } from './browser-session-runner/index.mts';
|
|
26
|
+
export type { BrowserSessionDeps, BrowserSessionEvent, BrowserSessionExit, BrowserSessionOptions, BrowserSessionOutput, BrowserSessionProcess, BrowserSessionResult, BrowserSessionTerminationReason, BrowserSessionWatchdog, BrowserSessionWatchdogCleanup, BrowserSessionWatchdogController, } from './browser-session-runner/index.mts';
|
|
27
27
|
export { buildSchemaSnapshot, detectRenamedIndexes, generateSchemaSnapshot, indexShapeKey, readSchemaCatalog, renderSchemaMarkdown, stableStringify, writeSchemaSnapshot, } from './pg-schema-snapshot/index.mts';
|
|
28
28
|
export type { CatalogQuery, PartitionPolicy, SchemaCatalog, SchemaGrowthMaps, SchemaSnapshot, SchemaTableSnapshot, } from './pg-schema-snapshot/index.mts';
|
|
29
29
|
export { buildOpenApiDocument, hashContractSchema, nodeToOpenApi, writeOpenApi, } from './openapi-document/index.mts';
|
package/dist/index.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export { decodeSelectedFiles, encodeSelectedFiles, formatMultilineOutput, SELECT
|
|
|
13
13
|
export { createArtifactClassifier, parseArtifactPatternsJson, planRunDeletions, runCleanup, sweepCleanup, } from './gha-artifacts-cleanup/index.mjs';
|
|
14
14
|
export { validateOptionalHttpOrigin } from './http-origin/index.mjs';
|
|
15
15
|
export { boundPendingLine, DEFAULT_MAX_PENDING_LINE_LENGTH, DEFAULT_TRUNCATED_LINE_MARKER, splitCompleteLines, } from './process-line-buffer/index.mjs';
|
|
16
|
-
export { ProcessGroupDrainTimeoutError, runBrowserSession, } from './browser-session-runner/index.mjs';
|
|
16
|
+
export { isProcessGroupAlive, ProcessGroupDrainTimeoutError, runBrowserSession, waitForProcessGroupExit, } from './browser-session-runner/index.mjs';
|
|
17
17
|
export { buildSchemaSnapshot, detectRenamedIndexes, generateSchemaSnapshot, indexShapeKey, readSchemaCatalog, renderSchemaMarkdown, stableStringify, writeSchemaSnapshot, } from './pg-schema-snapshot/index.mjs';
|
|
18
18
|
export { buildOpenApiDocument, hashContractSchema, nodeToOpenApi, writeOpenApi, } from './openapi-document/index.mjs';
|
|
19
19
|
export { decide, deriveRetryAttempt } from './transient-retry/index.mjs';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { asNumber, asRecord, emptyTokens, parseLines, } from './shared.mjs';
|
|
2
|
+
function hasInheritedParent(sessionMeta) {
|
|
3
|
+
const payload = asRecord(sessionMeta.payload);
|
|
4
|
+
return (asRecord(asRecord(payload?.source)?.subagent) !== undefined ||
|
|
5
|
+
typeof payload?.forked_from_id === 'string' ||
|
|
6
|
+
typeof payload?.parent_thread_id === 'string' ||
|
|
7
|
+
(typeof payload?.id === 'string' &&
|
|
8
|
+
typeof payload.session_id === 'string' &&
|
|
9
|
+
payload.id.toLowerCase() !== payload.session_id.toLowerCase()));
|
|
10
|
+
}
|
|
11
|
+
function isOwnedTaskStart(record, timestampMs) {
|
|
12
|
+
if (record.type !== 'event_msg')
|
|
13
|
+
return false;
|
|
14
|
+
const payload = asRecord(record.payload);
|
|
15
|
+
if (payload?.type !== 'task_started' || typeof payload.started_at !== 'number')
|
|
16
|
+
return false;
|
|
17
|
+
return payload.started_at >= 1_000_000_000_000
|
|
18
|
+
? payload.started_at >= timestampMs
|
|
19
|
+
: payload.started_at >= Math.floor(timestampMs / 1000);
|
|
20
|
+
}
|
|
21
|
+
function usage(record) {
|
|
22
|
+
if (record.type !== 'event_msg')
|
|
23
|
+
return undefined;
|
|
24
|
+
const payload = asRecord(record.payload);
|
|
25
|
+
if (payload?.type !== 'token_count')
|
|
26
|
+
return undefined;
|
|
27
|
+
const totals = asRecord(asRecord(payload?.info)?.total_token_usage);
|
|
28
|
+
if (!totals)
|
|
29
|
+
return undefined;
|
|
30
|
+
return {
|
|
31
|
+
input: asNumber(totals.input_tokens),
|
|
32
|
+
output: asNumber(totals.output_tokens),
|
|
33
|
+
cacheRead: asNumber(totals.cached_input_tokens),
|
|
34
|
+
cacheCreation: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function retainHighWater(previous, current) {
|
|
38
|
+
return {
|
|
39
|
+
input: Math.max(previous.input, current.input),
|
|
40
|
+
output: Math.max(previous.output, current.output),
|
|
41
|
+
cacheRead: Math.max(previous.cacheRead, current.cacheRead),
|
|
42
|
+
cacheCreation: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function segmentCodex(lines) {
|
|
46
|
+
const content = lines.filter(Boolean);
|
|
47
|
+
const sessionMeta = parseLines(content.slice(0, 1))[0];
|
|
48
|
+
if (sessionMeta?.type !== 'session_meta')
|
|
49
|
+
return { lines: content, baseline: emptyTokens() };
|
|
50
|
+
if (!hasInheritedParent(sessionMeta))
|
|
51
|
+
return { lines: content.slice(1), baseline: emptyTokens() };
|
|
52
|
+
const timestamp = asRecord(sessionMeta.payload)?.timestamp;
|
|
53
|
+
const timestampMs = typeof timestamp === 'string' ? Date.parse(timestamp) : Number.NaN;
|
|
54
|
+
if (!Number.isFinite(timestampMs))
|
|
55
|
+
return undefined;
|
|
56
|
+
const ownedIndex = content.findIndex((line, index) => {
|
|
57
|
+
if (index === 0)
|
|
58
|
+
return false;
|
|
59
|
+
const record = parseLines([line])[0];
|
|
60
|
+
return record ? isOwnedTaskStart(record, timestampMs) : false;
|
|
61
|
+
});
|
|
62
|
+
if (ownedIndex === -1)
|
|
63
|
+
return undefined;
|
|
64
|
+
let baseline;
|
|
65
|
+
for (const record of parseLines(content.slice(1, ownedIndex))) {
|
|
66
|
+
const current = usage(record);
|
|
67
|
+
if (current)
|
|
68
|
+
baseline = retainHighWater(baseline ?? emptyTokens(), current);
|
|
69
|
+
}
|
|
70
|
+
if (!baseline)
|
|
71
|
+
return undefined;
|
|
72
|
+
return { lines: content.slice(ownedIndex), baseline };
|
|
73
|
+
}
|
|
@@ -7,5 +7,4 @@ export declare function codexIdentity(lines: string[]): {
|
|
|
7
7
|
threadId?: string;
|
|
8
8
|
agentPath: string;
|
|
9
9
|
};
|
|
10
|
-
export declare function withoutLeadingSessionMetadata(lines: string[]): string[];
|
|
11
10
|
export declare function computeCodex(lines: string[], subagents?: CodexSegment[], baseline?: TokenTotals): TranscriptFacts;
|
|
@@ -161,10 +161,6 @@ export function codexIdentity(lines) {
|
|
|
161
161
|
agentPath: typeof payload?.agent_path === 'string' ? payload.agent_path : '/root',
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
|
-
export function withoutLeadingSessionMetadata(lines) {
|
|
165
|
-
const content = lines.filter(Boolean);
|
|
166
|
-
return parseLines(content.slice(0, 1))[0]?.type === 'session_meta' ? content.slice(1) : content;
|
|
167
|
-
}
|
|
168
164
|
export function computeCodex(lines, subagents = [], baseline) {
|
|
169
165
|
const facts = emptyFacts();
|
|
170
166
|
applyRecords(parseLines(lines), facts, false, baseline);
|
|
@@ -2,10 +2,11 @@ import { existsSync, globSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, dirname, join } from 'node:path';
|
|
5
|
-
import { codexChildren, codexIdentity, computeCodex
|
|
5
|
+
import { codexChildren, codexIdentity, computeCodex } from './codex.mjs';
|
|
6
|
+
import { segmentCodex } from './codex-segment.mjs';
|
|
6
7
|
import { computeClaude } from './claude.mjs';
|
|
7
8
|
import { formatTranscriptFacts, formatUnavailable, sessionLabel } from './format.mjs';
|
|
8
|
-
import { emptyFacts,
|
|
9
|
+
import { emptyFacts, hasMalformedInteriorRecord, parseLines, } from './shared.mjs';
|
|
9
10
|
export { codexChildren, codexIdentity } from './codex.mjs';
|
|
10
11
|
export { formatTranscriptFacts, formatUnavailable } from './format.mjs';
|
|
11
12
|
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -114,9 +115,11 @@ async function codexSubagents(lines, sessionsDir, ownerPath, visited = new Set()
|
|
|
114
115
|
hasMalformedInteriorRecord(child) ||
|
|
115
116
|
!matchesChildIdentity(child, edge.threadId, edge.agentPath))
|
|
116
117
|
return undefined;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const segment = segmentCodex(child);
|
|
119
|
+
if (!segment)
|
|
120
|
+
return undefined;
|
|
121
|
+
result.push(segment);
|
|
122
|
+
const nested = await codexSubagents(segment.lines, sessionsDir, edge.agentPath, visited);
|
|
120
123
|
if (!nested)
|
|
121
124
|
return undefined;
|
|
122
125
|
result.push(...nested);
|
|
@@ -141,11 +144,14 @@ export async function runRetrospectiveTranscript(options) {
|
|
|
141
144
|
return formatTranscriptFacts(resolved.sessionId, computeTranscriptFacts(lines, subagents.filter((value) => value !== undefined && schema(value) === 'claude' && !hasMalformedInteriorRecord(value))));
|
|
142
145
|
}
|
|
143
146
|
const identity = codexIdentity(lines);
|
|
147
|
+
const root = segmentCodex(lines);
|
|
148
|
+
if (!root)
|
|
149
|
+
return formatUnavailable('could not segment Codex transcript');
|
|
144
150
|
const codexHome = (options.env ?? process.env).CODEX_HOME;
|
|
145
|
-
const subagents = await codexSubagents(lines, options.codexSessionsDir ??
|
|
151
|
+
const subagents = await codexSubagents(root.lines, options.codexSessionsDir ??
|
|
146
152
|
(options.jsonlPath ? dirname(resolved.path) : undefined) ??
|
|
147
153
|
join(codexHome || join(homedir(), '.codex'), 'sessions'), identity.agentPath, new Set([identity.threadId ?? resolved.sessionId].filter((threadId) => SESSION_ID.test(threadId))));
|
|
148
154
|
if (!subagents)
|
|
149
155
|
return formatUnavailable('could not resolve a referenced Codex child transcript');
|
|
150
|
-
return formatTranscriptFacts(resolved.sessionId,
|
|
156
|
+
return formatTranscriptFacts(resolved.sessionId, computeCodex(root.lines, subagents, root.baseline));
|
|
151
157
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|