@steve31415/baselib 3.39.0 → 3.40.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.
@@ -3,12 +3,14 @@
3
3
  // ~/migration/research/deploy-procedure-design.md). All logic lives in
4
4
  // ../deploy/deploy.ts; configuration is the app's deploy.config.json.
5
5
  //
6
- // process.exit (not exitCode) so an in-flight Cloud Build child cannot keep
7
- // a failed deploy's process alive; the server-side build finishes on its own
8
- // and is reused by the next attempt.
6
+ // An explicit exit (not exitCode) so an in-flight Cloud Build child cannot
7
+ // keep a failed deploy's process alive; the server-side build finishes on its
8
+ // own and is reused by the next attempt. exitWhenFlushed lets the output
9
+ // already written reach a piped reader (Coder captures it) first.
10
+ import { exitWhenFlushed } from '../ops/cli.js';
9
11
  import { realRunner } from '../deploy/exec.js';
10
12
  import { runDeploy } from '../deploy/deploy.js';
11
- process.exit(await runDeploy({
13
+ await exitWhenFlushed(await runDeploy({
12
14
  repoRoot: process.cwd(),
13
15
  runner: realRunner,
14
16
  argv: process.argv.slice(2),
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // `pw-logs` — Axiom log query (OPERATIONS.md "Investigating production
3
3
  // state"). All logic lives in ../ops/logs.ts.
4
+ import { exitWhenFlushed } from '../ops/cli.js';
4
5
  import { runLogs } from '../ops/logs.js';
5
- process.exit(await runLogs({ argv: process.argv.slice(2), cwd: process.cwd() }));
6
+ await exitWhenFlushed(await runLogs({ argv: process.argv.slice(2), cwd: process.cwd() }));
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // `pw-rollback` — one-command traffic move to a previously released build.
3
3
  // All logic lives in ../deploy/rollback.ts.
4
+ import { exitWhenFlushed } from '../ops/cli.js';
4
5
  import { realRunner } from '../deploy/exec.js';
5
6
  import { runRollback } from '../deploy/rollback.js';
6
- process.exit(await runRollback({
7
+ await exitWhenFlushed(await runRollback({
7
8
  repoRoot: process.cwd(),
8
9
  runner: realRunner,
9
10
  argv: process.argv.slice(2),
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // `pw-sql` — read-only production SQL (OPERATIONS.md "Investigating
3
3
  // production state"). All logic lives in ../ops/sql.ts.
4
+ import { exitWhenFlushed } from '../ops/cli.js';
4
5
  import { runSql } from '../ops/sql.js';
5
- process.exit(await runSql({ argv: process.argv.slice(2), cwd: process.cwd() }));
6
+ await exitWhenFlushed(await runSql({ argv: process.argv.slice(2), cwd: process.cwd() }));
@@ -2,6 +2,7 @@
2
2
  // baselib's `npm run deploy` — release the checked-out version (tag push +
3
3
  // wait for the registry; the tag-triggered workflow does the actual
4
4
  // publish). All logic lives in ../release.ts.
5
+ import { exitWhenFlushed } from '../ops/cli.js';
5
6
  import { realRunner } from '../deploy/exec.js';
6
7
  import { release } from '../release.js';
7
8
  try {
@@ -12,5 +13,5 @@ try {
12
13
  }
13
14
  catch (e) {
14
15
  console.error(e instanceof Error ? e.message : String(e));
15
- process.exit(1);
16
+ await exitWhenFlushed(1);
16
17
  }
@@ -83,6 +83,7 @@ export async function llmComplete(request) {
83
83
  continue;
84
84
  }
85
85
  const provider = providers[modelEntry.provider];
86
+ const attemptStartedAt = Date.now();
86
87
  try {
87
88
  const result = await provider.complete({
88
89
  apiModelId: modelEntry.apiModelId,
@@ -124,6 +125,7 @@ export async function llmComplete(request) {
124
125
  fallbackModel: modelName,
125
126
  fallbackProvider: modelEntry.provider,
126
127
  attemptsBeforeSuccess: failedAttempts.length,
128
+ ...(request.callSite ? { callSite: request.callSite } : {}),
127
129
  });
128
130
  }
129
131
  return {
@@ -162,6 +164,12 @@ export async function llmComplete(request) {
162
164
  declined: err instanceof LLMError && err.declined === true,
163
165
  attemptNumber: failedAttempts.length,
164
166
  nextModel: modelsToTry[failedAttempts.length] ?? 'none',
167
+ // Which caller, and whether it ran out its own deadline: a timeout tail is a
168
+ // headroom question for that call site, an HTTP error is not (reader task 58b65ec9).
169
+ ...(request.callSite ? { callSite: request.callSite } : {}),
170
+ timedOut: err instanceof LLMError && err.timedOut === true,
171
+ timeoutMs,
172
+ attemptMs: Date.now() - attemptStartedAt,
165
173
  });
166
174
  }
167
175
  }
@@ -140,6 +140,7 @@ export const anthropicProvider = {
140
140
  provider: 'anthropic',
141
141
  model: request.apiModelId,
142
142
  isRetryable: true,
143
+ timedOut: err instanceof Error && err.name === 'AbortError',
143
144
  });
144
145
  }
145
146
  finally {
@@ -125,6 +125,7 @@ export const geminiProvider = {
125
125
  provider: 'gemini',
126
126
  model: request.apiModelId,
127
127
  isRetryable: true,
128
+ timedOut: err instanceof Error && err.name === 'AbortError',
128
129
  });
129
130
  }
130
131
  finally {
@@ -149,6 +149,7 @@ export const openaiProvider = {
149
149
  provider: 'openai',
150
150
  model: request.apiModelId,
151
151
  isRetryable: true,
152
+ timedOut: err instanceof Error && err.name === 'AbortError',
152
153
  });
153
154
  }
154
155
  finally {
@@ -155,6 +155,8 @@ export declare class LLMError extends Error {
155
155
  * not a defect.
156
156
  */
157
157
  declined: boolean;
158
+ /** True when the attempt hit its per-provider `timeoutMs` deadline. */
159
+ timedOut: boolean;
158
160
  constructor(opts: {
159
161
  message: string;
160
162
  provider: ProviderName;
@@ -162,6 +164,7 @@ export declare class LLMError extends Error {
162
164
  statusCode?: number;
163
165
  isRetryable: boolean;
164
166
  declined?: boolean;
167
+ timedOut?: boolean;
165
168
  });
166
169
  }
167
170
  export interface ProviderAdapter {
package/dist/llm/types.js CHANGED
@@ -20,6 +20,8 @@ export class LLMError extends Error {
20
20
  * not a defect.
21
21
  */
22
22
  declined;
23
+ /** True when the attempt hit its per-provider `timeoutMs` deadline. */
24
+ timedOut;
23
25
  constructor(opts) {
24
26
  super(opts.message);
25
27
  this.name = 'LLMError';
@@ -28,5 +30,6 @@ export class LLMError extends Error {
28
30
  this.statusCode = opts.statusCode;
29
31
  this.isRetryable = opts.isRetryable;
30
32
  this.declined = opts.declined ?? false;
33
+ this.timedOut = opts.timedOut ?? false;
31
34
  }
32
35
  }
package/dist/ops/cli.d.ts CHANGED
@@ -13,4 +13,16 @@ type Parsed<T extends Options> = ReturnType<typeof parseArgs<{
13
13
  export declare function parseCli<T extends Options>(argv: string[], options: T, usage: string, io: CliIo): Parsed<T> | {
14
14
  code: number;
15
15
  };
16
+ /**
17
+ * `process.exit(code)` once stdout and stderr have handed everything written
18
+ * to them to the OS. A plain process.exit discards what a pipe has not yet
19
+ * taken: Node writes to a pipe asynchronously once its buffer (64 KiB on
20
+ * Linux) is full, so `pw-logs … | jq` lost all but the first 64 KiB, with
21
+ * exit status 0 (2026-10-26). Files and terminals are written synchronously,
22
+ * which is why redirecting to a file never showed it. Still an explicit exit,
23
+ * not `exitCode`: nothing else the bin started (a connection pool, a Cloud
24
+ * Build child) may keep it alive. A reader that goes away (`| head`) ends the
25
+ * wait instead of crashing the bin on EPIPE.
26
+ */
27
+ export declare function exitWhenFlushed(code: number, streams?: NodeJS.WritableStream[], exit?: (code: number) => void): Promise<void>;
16
28
  export {};
package/dist/ops/cli.js CHANGED
@@ -25,3 +25,29 @@ export function parseCli(argv, options, usage, io) {
25
25
  }
26
26
  return parsed;
27
27
  }
28
+ /**
29
+ * `process.exit(code)` once stdout and stderr have handed everything written
30
+ * to them to the OS. A plain process.exit discards what a pipe has not yet
31
+ * taken: Node writes to a pipe asynchronously once its buffer (64 KiB on
32
+ * Linux) is full, so `pw-logs … | jq` lost all but the first 64 KiB, with
33
+ * exit status 0 (2026-10-26). Files and terminals are written synchronously,
34
+ * which is why redirecting to a file never showed it. Still an explicit exit,
35
+ * not `exitCode`: nothing else the bin started (a connection pool, a Cloud
36
+ * Build child) may keep it alive. A reader that goes away (`| head`) ends the
37
+ * wait instead of crashing the bin on EPIPE.
38
+ */
39
+ export async function exitWhenFlushed(code, streams = [process.stdout, process.stderr], exit = (c) => process.exit(c)) {
40
+ await Promise.all(streams.map(flushed));
41
+ exit(code);
42
+ }
43
+ function flushed(stream) {
44
+ return new Promise((resolve) => {
45
+ // A broken pipe is reported to the write callback and then, a tick
46
+ // later, as an 'error' event, which would crash the process if nothing
47
+ // listened — so the listener stays until the exit.
48
+ stream.on('error', () => resolve());
49
+ // Writes complete in order, so an empty write's callback runs only after
50
+ // everything written before it has gone out.
51
+ stream.write('', () => resolve());
52
+ });
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.39.0",
3
+ "version": "3.40.0",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",