pi-harness-runtime 1.1.382 → 1.1.384
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/harness/job-state-machine.ts +97 -10
- package/harness/langchain/surge.test.ts +57 -25
- package/index.ts +124 -4
- package/package.json +8 -4
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/dist/minimax-browser-auth.d.ts +1 -0
- package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -1
- package/packages/auth/dist/minimax-browser-auth.js +2 -2
- package/packages/auth/dist/minimax-browser-auth.js.map +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/auth/src/minimax-browser-auth.ts +15 -3
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/dist/checkpoint-manager.d.ts.map +1 -1
- package/packages/checkpoint/dist/checkpoint-manager.js +3 -0
- package/packages/checkpoint/dist/checkpoint-manager.js.map +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/checkpoint/src/checkpoint-manager.ts +8 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/dist/continuation-controller.d.ts +174 -9
- package/packages/jev-judge/dist/continuation-controller.d.ts.map +1 -1
- package/packages/jev-judge/dist/continuation-controller.js +530 -77
- package/packages/jev-judge/dist/continuation-controller.js.map +1 -1
- package/packages/jev-judge/dist/typesafe-provider.d.ts.map +1 -1
- package/packages/jev-judge/dist/typesafe-provider.js +29 -2
- package/packages/jev-judge/dist/typesafe-provider.js.map +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/jev-judge/src/continuation-controller.ts +636 -83
- package/packages/jev-judge/src/typesafe-provider.ts +31 -2
- package/packages/jev-judge/tests/continuation-controller.test.ts +1229 -0
- package/packages/jev-judge/tests/typesafe-provider.test.ts +180 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/dist/client.d.ts +36 -1
- package/packages/relay/dist/client.js +195 -25
- package/packages/relay/dist/server.d.ts +5 -1
- package/packages/relay/dist/server.js +74 -7
- package/packages/relay/dist/types.d.ts +7 -0
- package/packages/relay/package.json +1 -1
- package/packages/relay/src/client.ts +206 -42
- package/packages/relay/src/server.ts +78 -10
- package/packages/relay/src/types.ts +7 -0
- package/packages/relay/test/client.test.ts +312 -0
- package/packages/relay/test/relay-e2e.test.ts +263 -0
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/dist/harness-footer.d.ts +131 -0
- package/packages/tui/dist/harness-footer.d.ts.map +1 -0
- package/packages/tui/dist/harness-footer.js +463 -0
- package/packages/tui/dist/harness-footer.js.map +1 -0
- package/packages/tui/dist/index.d.ts +1 -0
- package/packages/tui/dist/index.d.ts.map +1 -1
- package/packages/tui/dist/index.js +1 -0
- package/packages/tui/dist/index.js.map +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/tui/src/harness-footer.ts +580 -0
- package/packages/tui/src/index.ts +17 -0
- package/packages/tui/test/harness-footer.test.ts +440 -0
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/run-e2e-scripts.mjs +74 -0
- package/scripts/run-langchain-tests.mjs +169 -0
- package/scripts/typecheck-packages.mjs +167 -0
- package/scripts/unified-webhook-server.ts +70 -13
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* reviewing -> repairing -> paused_quota -> waiting_human -> ready_for_client -> archived
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
15
19
|
import type {
|
|
16
20
|
JobStatus,
|
|
17
21
|
RuntimeEvent,
|
|
@@ -98,8 +102,14 @@ export interface CheckpointManager {
|
|
|
98
102
|
export class JobStateMachine {
|
|
99
103
|
private currentCheckpoint: RuntimeCheckpoint | null = null;
|
|
100
104
|
private readonly eventLog: RuntimeEvent[] = [];
|
|
105
|
+
// NOTE: an explicit field (not a constructor parameter property) so the
|
|
106
|
+
// module can be loaded by Node's type-stripping loader, which rejects
|
|
107
|
+
// parameter properties ("ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX").
|
|
108
|
+
private readonly options: JobStateMachineOptions;
|
|
101
109
|
|
|
102
|
-
constructor(
|
|
110
|
+
constructor(options: JobStateMachineOptions) {
|
|
111
|
+
this.options = options;
|
|
112
|
+
}
|
|
103
113
|
|
|
104
114
|
/**
|
|
105
115
|
* Initialize a new job with a requirement
|
|
@@ -337,27 +347,104 @@ export class JobStateMachine {
|
|
|
337
347
|
}
|
|
338
348
|
|
|
339
349
|
/**
|
|
340
|
-
*
|
|
350
|
+
* File-backed checkpoint manager.
|
|
351
|
+
*
|
|
352
|
+
* The previous implementation wrapped the deprecated JsonCheckpointManager,
|
|
353
|
+
* whose save()/load() ALWAYS throw ("Legacy JsonCheckpointManager is
|
|
354
|
+
* deprecated. Use CheckpointEngine instead."), so every state transition that
|
|
355
|
+
* persisted failed and the runtime event log was silently dropped. This
|
|
356
|
+
* manager persists a real checkpoint file plus an append-only event log per
|
|
357
|
+
* job, so the state machine actually works.
|
|
358
|
+
*/
|
|
359
|
+
/**
|
|
360
|
+
* The on-disk directory name for a job id.
|
|
361
|
+
*
|
|
362
|
+
* The id is sanitised so it can never contain a path separator, and a hash
|
|
363
|
+
* of the ORIGINAL id is appended. The hash makes the mapping injective:
|
|
364
|
+
* without it two different job ids ("a/b" and "a_b") would share one
|
|
365
|
+
* checkpoint directory and could overwrite each other's checkpoint.
|
|
366
|
+
*/
|
|
367
|
+
export function checkpointDirName(jobId: string): string {
|
|
368
|
+
const sanitized = jobId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
369
|
+
const digest = createHash("sha256").update(jobId).digest("hex").slice(0, 8);
|
|
370
|
+
if (sanitized === "" || sanitized === "." || sanitized === "..") {
|
|
371
|
+
return `_${sanitized}${digest}`;
|
|
372
|
+
}
|
|
373
|
+
return `${sanitized}-${digest}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export class FileCheckpointManager implements CheckpointManager {
|
|
377
|
+
private readonly rootDir: string;
|
|
378
|
+
|
|
379
|
+
constructor(rootDir: string) {
|
|
380
|
+
this.rootDir = rootDir;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Job ids are used as directory names, so strip path separators and
|
|
385
|
+
* other characters that are not safe in a file name. "." and ".." would
|
|
386
|
+
* still escape the checkpoints directory, so they are escaped as well.
|
|
387
|
+
*/
|
|
388
|
+
private jobDir(jobId: string): string {
|
|
389
|
+
return join(this.rootDir, "checkpoints", checkpointDirName(jobId));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async save(checkpoint: RuntimeCheckpoint): Promise<void> {
|
|
393
|
+
const dir = this.jobDir(checkpoint.jobId);
|
|
394
|
+
await mkdir(dir, { recursive: true });
|
|
395
|
+
await writeFile(
|
|
396
|
+
join(dir, "checkpoint.json"),
|
|
397
|
+
`${JSON.stringify(checkpoint, null, 2)}\n`,
|
|
398
|
+
"utf8",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async load(jobId: string): Promise<RuntimeCheckpoint | null> {
|
|
403
|
+
let raw: string;
|
|
404
|
+
try {
|
|
405
|
+
raw = await readFile(join(this.jobDir(jobId), "checkpoint.json"), "utf8");
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
let parsed: RuntimeCheckpoint | null;
|
|
413
|
+
try {
|
|
414
|
+
parsed = JSON.parse(raw) as RuntimeCheckpoint | null;
|
|
415
|
+
} catch {
|
|
416
|
+
// A corrupted checkpoint file must not crash the resume path:
|
|
417
|
+
// it is reported as "no checkpoint to resume".
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async appendEvent(jobId: string, event: RuntimeEvent): Promise<void> {
|
|
424
|
+
const dir = this.jobDir(jobId);
|
|
425
|
+
await mkdir(dir, { recursive: true });
|
|
426
|
+
await appendFile(join(dir, "events.jsonl"), `${JSON.stringify(event)}\n`, "utf8");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Factory to create a state machine with a file-backed checkpoint manager
|
|
341
432
|
*/
|
|
342
433
|
export async function createJobStateMachine(
|
|
343
434
|
rootDir: string,
|
|
344
435
|
jobId?: string,
|
|
345
436
|
): Promise<{ machine: JobStateMachine; checkpoint: RuntimeCheckpoint | null }> {
|
|
346
|
-
const
|
|
347
|
-
"../packages/checkpoint/src/checkpoint-manager.ts"
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
const manager = new JsonCheckpointManager(rootDir);
|
|
437
|
+
const checkpointManager = new FileCheckpointManager(rootDir);
|
|
351
438
|
|
|
352
439
|
if (jobId) {
|
|
353
|
-
const checkpoint = await
|
|
354
|
-
const machine = new JobStateMachine({ checkpointManager
|
|
440
|
+
const checkpoint = await checkpointManager.load(jobId);
|
|
441
|
+
const machine = new JobStateMachine({ checkpointManager });
|
|
355
442
|
if (checkpoint) {
|
|
356
443
|
machine.resumeJob(jobId);
|
|
357
444
|
}
|
|
358
445
|
return { machine, checkpoint };
|
|
359
446
|
}
|
|
360
447
|
|
|
361
|
-
const machine = new JobStateMachine({ checkpointManager
|
|
448
|
+
const machine = new JobStateMachine({ checkpointManager });
|
|
362
449
|
return { machine, checkpoint: null };
|
|
363
450
|
}
|
|
@@ -260,49 +260,63 @@ describe("T8 — loop deps surviving a mid-graph 529", () => {
|
|
|
260
260
|
import { classifyGLMQuota, invokeWithGLMRetry } from "./surge.js";
|
|
261
261
|
|
|
262
262
|
describe("classifyGLMQuota", () => {
|
|
263
|
-
// The
|
|
264
|
-
//
|
|
265
|
-
//
|
|
263
|
+
// The reset timestamps are computed RELATIVE to now. A hardcoded date
|
|
264
|
+
// is a time bomb: it was "in the past by < 1 day" when this test was
|
|
265
|
+
// written and is now far in the past, which makes the +1 day shift
|
|
266
|
+
// land in the past as well.
|
|
267
|
+
const stamp = (date: Date): string => date.toISOString().slice(0, 19);
|
|
268
|
+
const inDays = (days: number): Date =>
|
|
269
|
+
new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
|
270
|
+
|
|
266
271
|
it("parses GLM 1308 error with reset timestamp", () => {
|
|
272
|
+
const reset = inDays(2);
|
|
267
273
|
const err = new Error(
|
|
268
|
-
|
|
274
|
+
`429: {"code":"1308","message":"Usage limit reached for 5 hour. Your limit will reset at ${stamp(reset)}"}`,
|
|
269
275
|
);
|
|
270
276
|
const sig = classifyGLMQuota(err);
|
|
271
277
|
assert.ok(sig, "should be classified as GLM quota");
|
|
272
|
-
//
|
|
278
|
+
// A reset time in the future is used verbatim.
|
|
273
279
|
assert.ok(sig!.resetAtEpoch > Date.now(), "resetAtEpoch is in the future");
|
|
274
280
|
assert.ok(
|
|
275
|
-
sig!.resetAt.includes(
|
|
276
|
-
"date
|
|
281
|
+
sig!.resetAt.includes(stamp(reset).slice(0, 10)),
|
|
282
|
+
"date preserved verbatim",
|
|
283
|
+
);
|
|
284
|
+
assert.ok(
|
|
285
|
+
sig!.resetAt.includes(stamp(reset).slice(11)),
|
|
286
|
+
"time preserved verbatim",
|
|
277
287
|
);
|
|
278
|
-
assert.ok(sig!.resetAt.includes("20:29:24"), "time preserved verbatim");
|
|
279
288
|
});
|
|
280
289
|
|
|
281
290
|
it("parses 1308 from plain string error", () => {
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
// A reset time in the past is shifted to the NEXT occurrence (+1 day).
|
|
292
|
+
const reset = inDays(-0.5);
|
|
293
|
+
const err = `{"code":"1308","message":"Usage limit reached. Your limit will reset at ${stamp(reset)}"}`;
|
|
284
294
|
const sig = classifyGLMQuota(err);
|
|
285
295
|
assert.ok(sig, "should be classified as GLM quota");
|
|
286
|
-
|
|
296
|
+
const shifted = stamp(new Date(reset.getTime() + 24 * 60 * 60 * 1000));
|
|
287
297
|
assert.ok(
|
|
288
|
-
sig!.resetAt.includes(
|
|
289
|
-
"space normalised and date shifted",
|
|
298
|
+
sig!.resetAt.includes(shifted.slice(0, 10)),
|
|
299
|
+
"space normalised and date shifted to the next occurrence",
|
|
290
300
|
);
|
|
301
|
+
assert.ok(sig!.resetAtEpoch > Date.now(), "shifted epoch is in the future");
|
|
291
302
|
});
|
|
292
303
|
|
|
293
304
|
it("parses reset time with T separator", () => {
|
|
305
|
+
const reset = inDays(2);
|
|
294
306
|
const err = new Error(
|
|
295
|
-
|
|
307
|
+
`{"code":"1308","message":"Usage limit reached. Your limit will reset at ${stamp(reset)}"}`,
|
|
296
308
|
);
|
|
297
309
|
const sig = classifyGLMQuota(err);
|
|
298
310
|
assert.ok(sig, "should be classified as GLM quota");
|
|
299
|
-
// past date → next occurrence (2026-09-05); T separator preserved verbatim
|
|
300
311
|
assert.ok(
|
|
301
|
-
sig!.resetAt.includes(
|
|
302
|
-
"date
|
|
312
|
+
sig!.resetAt.includes(stamp(reset).slice(0, 10)),
|
|
313
|
+
"date preserved verbatim",
|
|
303
314
|
);
|
|
304
315
|
assert.ok(sig!.resetAt.includes("T"), "T separator preserved verbatim");
|
|
305
|
-
assert.ok(
|
|
316
|
+
assert.ok(
|
|
317
|
+
sig!.resetAt.includes(stamp(reset).slice(11)),
|
|
318
|
+
"time preserved verbatim",
|
|
319
|
+
);
|
|
306
320
|
});
|
|
307
321
|
|
|
308
322
|
it("returns null for non-GLM errors", () => {
|
|
@@ -343,9 +357,18 @@ describe("invokeWithGLMRetry", () => {
|
|
|
343
357
|
|
|
344
358
|
it("waits until reset time before retry", async () => {
|
|
345
359
|
const sleeps: number[] = [];
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
360
|
+
// The fake sleep must yield to the event loop: the countdown loop
|
|
361
|
+
// waits for the WALL CLOCK, so a sleep that returns immediately
|
|
362
|
+
// turns the countdown into a busy loop.
|
|
363
|
+
const sleep = async (ms: number) => {
|
|
364
|
+
sleeps.push(ms);
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
366
|
+
};
|
|
367
|
+
// Use a reset time in the future. The reset time is parsed with
|
|
368
|
+
// SECOND precision, so it has to be far enough in the future that
|
|
369
|
+
// the truncation cannot push it into the past (a past reset time is
|
|
370
|
+
// shifted by a whole day).
|
|
371
|
+
const resetIn = 1500;
|
|
349
372
|
const resetAt = new Date(Date.now() + resetIn).toISOString();
|
|
350
373
|
|
|
351
374
|
let attempts = 0;
|
|
@@ -373,8 +396,14 @@ describe("invokeWithGLMRetry", () => {
|
|
|
373
396
|
|
|
374
397
|
it("fires onGLMQuota callback during countdown", async () => {
|
|
375
398
|
const ticks: string[] = [];
|
|
376
|
-
const sleep = async (ms: number) =>
|
|
377
|
-
|
|
399
|
+
const sleep = async (ms: number) => {
|
|
400
|
+
ticks.push(`sleep:${ms}`);
|
|
401
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
402
|
+
};
|
|
403
|
+
// The reset time is parsed with SECOND precision, so it has to be
|
|
404
|
+
// far enough in the future that the truncation cannot push it into
|
|
405
|
+
// the past (a past reset time is shifted by a whole day).
|
|
406
|
+
const resetAt = new Date(Date.now() + 1500).toISOString();
|
|
378
407
|
|
|
379
408
|
let attempts = 0;
|
|
380
409
|
await invokeWithGLMRetry(
|
|
@@ -402,8 +431,11 @@ describe("invokeWithGLMRetry", () => {
|
|
|
402
431
|
|
|
403
432
|
it("fires onGLMRetry callback after countdown expires", async () => {
|
|
404
433
|
const events: string[] = [];
|
|
405
|
-
const sleep = async (_ms: number) =>
|
|
406
|
-
|
|
434
|
+
const sleep = async (_ms: number) => {
|
|
435
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
436
|
+
};
|
|
437
|
+
// Same as above: second precision, so stay safely in the future.
|
|
438
|
+
const resetAt = new Date(Date.now() + 1200).toISOString();
|
|
407
439
|
|
|
408
440
|
let attempts = 0;
|
|
409
441
|
await invokeWithGLMRetry(
|
package/index.ts
CHANGED
|
@@ -660,6 +660,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
660
660
|
|
|
661
661
|
// Clean up controller on session shutdown
|
|
662
662
|
pi.on("session_shutdown", () => {
|
|
663
|
+
// Release the guard timer and the queue: a stopped session must not
|
|
664
|
+
// keep a timer alive nor block a later continuation.
|
|
665
|
+
todoContinuationController?.dispose?.();
|
|
663
666
|
todoContinuationController = null;
|
|
664
667
|
});
|
|
665
668
|
|
|
@@ -830,6 +833,12 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
830
833
|
|
|
831
834
|
// --- agent_settled: Check todos and continue work ---------------
|
|
832
835
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
836
|
+
// The turn that a previous continuation triggered has now settled:
|
|
837
|
+
// release the dispatch guard so the next decision can dispatch again.
|
|
838
|
+
// Keeping it set would block every later dispatch while still
|
|
839
|
+
// consuming the retry budget.
|
|
840
|
+
todoContinuationController?.releaseContinuationGuard?.("agent_settled");
|
|
841
|
+
|
|
833
842
|
// Don't continue if user is waiting for response
|
|
834
843
|
if (waitingForUserSince) {
|
|
835
844
|
todoContinuationController?.clearQueue();
|
|
@@ -857,8 +866,19 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
|
|
|
857
866
|
|
|
858
867
|
if (result.action === "continue" && result.nextItemId) {
|
|
859
868
|
updateAutoContinueStatus("CONTINUING");
|
|
860
|
-
await todoContinuationController.steerToContinue(result.nextItemId);
|
|
861
|
-
|
|
869
|
+
const steer = await todoContinuationController.steerToContinue(result.nextItemId);
|
|
870
|
+
if (!steer?.dispatched) {
|
|
871
|
+
// Surface an explicit stall reason instead of silently doing nothing
|
|
872
|
+
logToFile(
|
|
873
|
+
"[auto-continue]",
|
|
874
|
+
"Continuation not dispatched:",
|
|
875
|
+
steer?.reason ?? "unknown reason",
|
|
876
|
+
`(attempts=${steer?.attempts ?? 0}, blocked=${steer?.blockedDispatches ?? 0})`,
|
|
877
|
+
);
|
|
878
|
+
updateAutoContinueStatus("IDLE");
|
|
879
|
+
}
|
|
880
|
+
// Guard stays set while the agent processes the continuation and is
|
|
881
|
+
// released by releaseContinuationGuard() on the next agent_settled.
|
|
862
882
|
} else {
|
|
863
883
|
// Not continuing - clear queue to allow future decisions
|
|
864
884
|
todoContinuationController.clearQueue();
|
|
@@ -1547,6 +1567,9 @@ async function initRelayStream(): Promise<void> {
|
|
|
1547
1567
|
{
|
|
1548
1568
|
serverUrl: relayWsUrl,
|
|
1549
1569
|
token: relayToken,
|
|
1570
|
+
// Keep retrying in the background: the relay server may not be up
|
|
1571
|
+
// yet when the harness starts.
|
|
1572
|
+
retryOnConnectFailure: true,
|
|
1550
1573
|
},
|
|
1551
1574
|
{
|
|
1552
1575
|
onConnected: (sessionId) => {
|
|
@@ -3664,6 +3687,49 @@ Run \`bd ready\` to see current bd issues.
|
|
|
3664
3687
|
// Store only the setStatus function, NOT the ctx itself.
|
|
3665
3688
|
// Storing ctx causes stale-context errors after pi reload/restart.
|
|
3666
3689
|
let footerSetStatus: ((key: string, value: string) => void) | null = null;
|
|
3690
|
+
// The custom footer needs live session data (cwd, model, usage, context).
|
|
3691
|
+
// The ctx is stored here as well, but the footer guards every access and
|
|
3692
|
+
// keeps the last known values when the ctx has gone stale, so a replaced
|
|
3693
|
+
// session can never break the rendering.
|
|
3694
|
+
let latestFooterCtx: ExtensionContext | null = null;
|
|
3695
|
+
let harnessFooterEnabled = true;
|
|
3696
|
+
|
|
3697
|
+
/**
|
|
3698
|
+
* Install the harness footer.
|
|
3699
|
+
*
|
|
3700
|
+
* Pi's built-in footer renders ALL `setStatus()` values on ONE shared line,
|
|
3701
|
+
* so the quota and the workspace environment summary used to be appended to
|
|
3702
|
+
* each other (and the workspace summary was truncated away on narrow
|
|
3703
|
+
* terminals). The harness footer gives the workspace summary its own row
|
|
3704
|
+
* while keeping Pi's footer information (cwd/branch, token stats, cost,
|
|
3705
|
+
* context usage, model/thinking state, other extensions' statuses).
|
|
3706
|
+
*
|
|
3707
|
+
* This function never rejects: every access to the (possibly stale) ctx is
|
|
3708
|
+
* guarded and failures are logged.
|
|
3709
|
+
*/
|
|
3710
|
+
async function installHarnessFooter(ctx: ExtensionContext): Promise<void> {
|
|
3711
|
+
try {
|
|
3712
|
+
if (!harnessFooterEnabled) return;
|
|
3713
|
+
// A stale ctx throws on property access - read it defensively.
|
|
3714
|
+
let mode: string | undefined;
|
|
3715
|
+
try {
|
|
3716
|
+
mode = ctx.mode;
|
|
3717
|
+
} catch {
|
|
3718
|
+
mode = undefined;
|
|
3719
|
+
}
|
|
3720
|
+
if (mode !== "tui") return;
|
|
3721
|
+
const { createHarnessFooter } = await import(
|
|
3722
|
+
"./packages/tui/src/harness-footer.js"
|
|
3723
|
+
);
|
|
3724
|
+
ctx.ui.setFooter((tui, theme, footerData) =>
|
|
3725
|
+
createHarnessFooter(tui, theme, footerData, {
|
|
3726
|
+
getContext: () => latestFooterCtx,
|
|
3727
|
+
}),
|
|
3728
|
+
);
|
|
3729
|
+
} catch (e) {
|
|
3730
|
+
logToFile("[pi-harness] harness footer install failed:", String(e));
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3667
3733
|
let quotaAutoFetchInFlight = false;
|
|
3668
3734
|
let proactiveCompactInFlight = false;
|
|
3669
3735
|
let lastProactiveCompactAt = 0;
|
|
@@ -4577,6 +4643,49 @@ Run \`bd ready\` to see current bd issues.
|
|
|
4577
4643
|
},
|
|
4578
4644
|
});
|
|
4579
4645
|
|
|
4646
|
+
// --- /harness-footer — Toggle harness custom footer vs built-in Pi footer ----
|
|
4647
|
+
pi.registerCommand("harness-footer", {
|
|
4648
|
+
description: "Toggle or restore footer: /harness-footer [harness|builtin]",
|
|
4649
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
4650
|
+
// A stale ctx throws on property access - read it defensively.
|
|
4651
|
+
let ctxMode: string | undefined;
|
|
4652
|
+
try {
|
|
4653
|
+
ctxMode = ctx.mode;
|
|
4654
|
+
} catch {
|
|
4655
|
+
ctxMode = undefined;
|
|
4656
|
+
}
|
|
4657
|
+
if (ctxMode !== "tui") {
|
|
4658
|
+
ctx.ui.notify("Footer control is only available in TUI mode.", "info");
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
const mode = args.trim().toLowerCase();
|
|
4663
|
+
if (mode === "builtin" || mode === "pi") {
|
|
4664
|
+
harnessFooterEnabled = false;
|
|
4665
|
+
ctx.ui.setFooter(undefined);
|
|
4666
|
+
ctx.ui.notify(
|
|
4667
|
+
"Built-in Pi footer restored. The workspace summary shares the status line again.",
|
|
4668
|
+
"info",
|
|
4669
|
+
);
|
|
4670
|
+
} else if (mode === "harness" || mode === "") {
|
|
4671
|
+
harnessFooterEnabled = true;
|
|
4672
|
+
latestFooterCtx = ctx;
|
|
4673
|
+
await installHarnessFooter(ctx);
|
|
4674
|
+
ctx.ui.notify(
|
|
4675
|
+
"Harness custom footer installed. The workspace summary has its own row.",
|
|
4676
|
+
"info",
|
|
4677
|
+
);
|
|
4678
|
+
} else {
|
|
4679
|
+
ctx.ui.notify(
|
|
4680
|
+
"Usage: /harness-footer [harness|builtin]\n" +
|
|
4681
|
+
" harness — install the Harness footer (workspace summary on its own row)\n" +
|
|
4682
|
+
" builtin — restore Pi's built-in footer",
|
|
4683
|
+
"info",
|
|
4684
|
+
);
|
|
4685
|
+
}
|
|
4686
|
+
},
|
|
4687
|
+
});
|
|
4688
|
+
|
|
4580
4689
|
// --- Harness widget state (like Todos extension) -----------------------
|
|
4581
4690
|
// Learn from Todos extension:
|
|
4582
4691
|
// - setWidget with placement: "aboveEditor" (fixed above input)
|
|
@@ -4652,6 +4761,7 @@ Run \`bd ready\` to see current bd issues.
|
|
|
4652
4761
|
|
|
4653
4762
|
// Capture only the setStatus function, not the ctx
|
|
4654
4763
|
footerSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
4764
|
+
latestFooterCtx = ctx;
|
|
4655
4765
|
refreshFooterStatus(
|
|
4656
4766
|
footerSetStatus,
|
|
4657
4767
|
tracker,
|
|
@@ -4660,6 +4770,9 @@ Run \`bd ready\` to see current bd issues.
|
|
|
4660
4770
|
() => lastActiveProvider,
|
|
4661
4771
|
);
|
|
4662
4772
|
|
|
4773
|
+
// Give the workspace environment summary its own footer row
|
|
4774
|
+
void installHarnessFooter(ctx);
|
|
4775
|
+
|
|
4663
4776
|
// Increment session generation to fence late callbacks
|
|
4664
4777
|
sessionGeneration++;
|
|
4665
4778
|
const mySession = sessionGeneration;
|
|
@@ -4722,6 +4835,7 @@ Run \`bd ready\` to see current bd issues.
|
|
|
4722
4835
|
noteActiveProvider(ctx.model?.id ?? null);
|
|
4723
4836
|
// Capture only the setStatus function, not the ctx
|
|
4724
4837
|
footerSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
4838
|
+
latestFooterCtx = ctx;
|
|
4725
4839
|
refreshFooterStatus(
|
|
4726
4840
|
footerSetStatus,
|
|
4727
4841
|
tracker,
|
|
@@ -4751,6 +4865,7 @@ Run \`bd ready\` to see current bd issues.
|
|
|
4751
4865
|
pi.on("session_compact", async (event, ctx) => {
|
|
4752
4866
|
// Capture only the setStatus function, not the ctx
|
|
4753
4867
|
footerSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
4868
|
+
latestFooterCtx = ctx;
|
|
4754
4869
|
proactiveCompactInFlight = false;
|
|
4755
4870
|
lastProactiveCompactAt = Date.now();
|
|
4756
4871
|
consecutiveCompactFailures = 0;
|
|
@@ -4931,13 +5046,18 @@ function refreshFooterStatus(
|
|
|
4931
5046
|
statusValue,
|
|
4932
5047
|
);
|
|
4933
5048
|
|
|
4934
|
-
//
|
|
5049
|
+
// The workspace environment summary is published under its own status key.
|
|
5050
|
+
// Pi renders every status value on ONE shared line, so the harness footer
|
|
5051
|
+
// (see installHarnessFooter) renders it on a dedicated row instead.
|
|
4935
5052
|
setWorkspaceEnvStatus(setStatus);
|
|
4936
5053
|
}
|
|
4937
5054
|
|
|
4938
5055
|
/**
|
|
4939
5056
|
* Set workspace environment status line
|
|
4940
|
-
*
|
|
5057
|
+
*
|
|
5058
|
+
* Published under the `harness-workspace` status key. The harness footer
|
|
5059
|
+
* renders it on its own row (Pi's built-in footer joins all status values
|
|
5060
|
+
* into a single line).
|
|
4941
5061
|
*/
|
|
4942
5062
|
async function setWorkspaceEnvStatus(setStatus: (key: string, value: string) => void) {
|
|
4943
5063
|
try {
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-harness-runtime",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.384",
|
|
4
4
|
"description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --test",
|
|
7
|
+
"test": "node --test \"test/**/*.test.mjs\" \"packages/clipboard/test/*.test.mjs\"",
|
|
8
|
+
"typecheck": "node scripts/typecheck-packages.mjs",
|
|
8
9
|
"postinstall": "mkdir -p ~/.pi-harness-runtime/cookies && node scripts/copy-plugins.js && ([ -f ~/.config/herdr/config.toml ] || mkdir -p ~/.config/herdr && cp config.toml.example ~/.config/herdr/config.toml)",
|
|
9
10
|
"release": "bun scripts/release-all.ts",
|
|
10
11
|
"release:patch": "bun scripts/release-all.ts --release-as patch",
|
|
@@ -18,8 +19,11 @@
|
|
|
18
19
|
"skills:sync:check": "bun scripts/skills-sync.ts --check-only",
|
|
19
20
|
"build": "for pkg in packages/*/; do [ -f \"${pkg}tsconfig.json\" ] && [ \"$pkg\" != \"packages/provider-router/\" ] && node_modules/.bin/tsc -p \"${pkg}tsconfig.json\" --skipLibCheck > /dev/null 2>&1 || true; done",
|
|
20
21
|
"prepublishOnly": "bun run build",
|
|
21
|
-
"test:harness": "
|
|
22
|
-
"test:
|
|
22
|
+
"test:harness": "node scripts/run-langchain-tests.mjs",
|
|
23
|
+
"test:packages": "bun test packages/",
|
|
24
|
+
"test:e2e": "PI_HARNESS_E2E=1 bun test e2e/",
|
|
25
|
+
"test:e2e:scripts": "node scripts/run-e2e-scripts.mjs",
|
|
26
|
+
"test:all": "npm run test && npm run test:harness",
|
|
23
27
|
"prepublish": "bun run build"
|
|
24
28
|
},
|
|
25
29
|
"bin": {
|
|
@@ -50,6 +50,7 @@ export declare function detectUsagePage(bodyText: string): {
|
|
|
50
50
|
detected: boolean;
|
|
51
51
|
sample: string | null;
|
|
52
52
|
};
|
|
53
|
+
export declare function getChromeExecutablePath(config?: MinimaxBrowserAuthConfig): string;
|
|
53
54
|
/**
|
|
54
55
|
* Manual authentication flow: launches real Chrome so Google login works,
|
|
55
56
|
* waits for user to sign in, then confirms at the TTY.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"minimax-browser-auth.d.ts","sourceRoot":"","sources":["../src/minimax-browser-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsBH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAiBD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,MAAM,WAAW,yBAAyB;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAWD,wBAAgB,cAAc,CAC7B,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,wBAAwB,GAC/B,IAAI,CAIN;AA2BD,wBAAgB,sBAAsB,CACrC,eAAe,CAAC,EAAE,MAAM,GACtB,yBAAyB,GAAG,IAAI,CAUlC;AASD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAClD,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CASA;
|
|
1
|
+
{"version":3,"file":"minimax-browser-auth.d.ts","sourceRoot":"","sources":["../src/minimax-browser-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsBH,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAiBD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,MAAM,WAAW,yBAAyB;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAWD,wBAAgB,cAAc,CAC7B,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,wBAAwB,GAC/B,IAAI,CAIN;AA2BD,wBAAgB,sBAAsB,CACrC,eAAe,CAAC,EAAE,MAAM,GACtB,yBAAyB,GAAG,IAAI,CAUlC;AASD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAClD,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CASA;AA6BD,wBAAgB,uBAAuB,CACtC,MAAM,GAAE,wBAA6B,GACnC,MAAM,CAsDR;AAuID;;;GAGG;AACH,wBAAsB,iCAAiC,CACtD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAqK5B;AA+CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAiC5D;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAChD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAwI5B;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CACjD,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,yBAAyB,CAAC,CA0CpC;AAED;;GAEG;AACH,wBAAsB,2BAA2B,CAChD,OAAO,GAAE,wBAA6B,GACpC,OAAO,CAAC,OAAO,CAAC,CA+ClB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,IAAI,yBAAyB,GAAG,IAAI,CAE9E;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAuJ5B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACpC,MAAM,GAAE,wBAA6B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAoD5B"}
|
|
@@ -131,7 +131,7 @@ function hasProfileData(profileDir) {
|
|
|
131
131
|
return false;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
function getChromeExecutablePath(config = {}) {
|
|
134
|
+
export function getChromeExecutablePath(config = {}) {
|
|
135
135
|
const configuredPath = config.chromeExecutablePath ??
|
|
136
136
|
process.env.PI_HARNESS_CHROME_PATH ??
|
|
137
137
|
process.env.GOOGLE_CHROME_BIN ??
|
|
@@ -142,7 +142,7 @@ function getChromeExecutablePath(config = {}) {
|
|
|
142
142
|
candidates.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta");
|
|
143
143
|
break;
|
|
144
144
|
case "win32":
|
|
145
|
-
candidates.push("C
|
|
145
|
+
candidates.push(path.join(process.env["ProgramFiles"] ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"), path.join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe"), path.join(process.env.LOCALAPPDATA ?? "", "Google", "Chrome", "Application", "chrome.exe"));
|
|
146
146
|
break;
|
|
147
147
|
default:
|
|
148
148
|
candidates.push("/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/snap/bin/google-chrome");
|