@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* like a small validator at runtime.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { areJsonValuesEqual } from "@oh-my-pi/pi-ai/utils/schema";
|
|
20
|
+
import { areJsonValuesEqual, upgradeJsonSchemaTo202012, validateJsonSchemaValue } from "@oh-my-pi/pi-ai/utils/schema";
|
|
21
21
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
// Type aliases — exported so `import type { Static, TSchema } from "..."`
|
|
@@ -41,6 +41,8 @@ export type TOptional<_E extends ArkSchema> = ArkSchema;
|
|
|
41
41
|
export type TUnion<_T extends readonly ArkSchema[] = readonly ArkSchema[]> = ArkSchema;
|
|
42
42
|
export type TEnum<_T extends readonly (string | number)[] = readonly (string | number)[]> = ArkSchema;
|
|
43
43
|
export type TRecord<_K extends ArkSchema, _V extends ArkSchema> = ArkSchema;
|
|
44
|
+
/** TypeBox-compatible wrapper for raw JSON Schema documents. */
|
|
45
|
+
export type TUnsafe<_T = unknown> = ArkSchema;
|
|
44
46
|
|
|
45
47
|
// ---------------------------------------------------------------------------
|
|
46
48
|
// ArkSchema wrapper — JSON Schema object with hidden validator metadata
|
|
@@ -560,18 +562,14 @@ function tNull(opts?: Meta): ArkSchema {
|
|
|
560
562
|
return applyMeta(createArkSchema(validator, { type: "null" }), opts);
|
|
561
563
|
}
|
|
562
564
|
|
|
565
|
+
const identityValidator = (data: unknown): unknown => data;
|
|
566
|
+
|
|
563
567
|
function tAny(opts?: Meta): ArkSchema {
|
|
564
|
-
return applyMeta(
|
|
565
|
-
createArkSchema((data: unknown) => data, {}),
|
|
566
|
-
opts,
|
|
567
|
-
);
|
|
568
|
+
return applyMeta(createArkSchema(identityValidator, {}), opts);
|
|
568
569
|
}
|
|
569
570
|
|
|
570
571
|
function tUnknown(opts?: Meta): ArkSchema {
|
|
571
|
-
return applyMeta(
|
|
572
|
-
createArkSchema((data: unknown) => data, {}),
|
|
573
|
-
opts,
|
|
574
|
-
);
|
|
572
|
+
return applyMeta(createArkSchema(identityValidator, {}), opts);
|
|
575
573
|
}
|
|
576
574
|
|
|
577
575
|
function tNever(opts?: Meta): ArkSchema {
|
|
@@ -920,6 +918,21 @@ export const Type = {
|
|
|
920
918
|
Null: tNull,
|
|
921
919
|
Any: tAny,
|
|
922
920
|
Unknown: tUnknown,
|
|
921
|
+
Unsafe<T = unknown>(jsonSchema: Record<string, unknown> = {}): TUnsafe<T> {
|
|
922
|
+
// Validate against the same draft-2020-12 upgrade the wire/tool-call
|
|
923
|
+
// path applies, so legacy draft-07 documents (tuple `items`, etc.)
|
|
924
|
+
// behave identically in `safeParse` and `validateToolArguments`.
|
|
925
|
+
const upgradedSchema = upgradeJsonSchemaTo202012(jsonSchema);
|
|
926
|
+
const validator = (data: unknown): unknown => {
|
|
927
|
+
const result = validateJsonSchemaValue(upgradedSchema, data);
|
|
928
|
+
if (result.success) return data;
|
|
929
|
+
const messages = result.issues.map(issue =>
|
|
930
|
+
issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message,
|
|
931
|
+
);
|
|
932
|
+
return validationFailure(messages.join("; ") || "Invalid value");
|
|
933
|
+
};
|
|
934
|
+
return createArkSchema(validator, jsonSchema);
|
|
935
|
+
},
|
|
923
936
|
Never: tNever,
|
|
924
937
|
Literal: tLiteral,
|
|
925
938
|
Union: tUnion,
|
|
@@ -31,3 +31,45 @@ describe("HindsightApi fetch cancellation", () => {
|
|
|
31
31
|
expect(requestSignal?.reason).toBe(caller.signal.reason);
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
describe("HindsightApi per-op timeouts", () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reports the effective per-op deadline in the timeout error", async () => {
|
|
41
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
42
|
+
Object.assign(
|
|
43
|
+
async (): Promise<Response> => {
|
|
44
|
+
throw new DOMException("The operation timed out.", "TimeoutError");
|
|
45
|
+
},
|
|
46
|
+
{ preconnect: globalThis.fetch.preconnect },
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const client = new HindsightApi({
|
|
51
|
+
baseUrl: "https://hindsight.example",
|
|
52
|
+
timeouts: { reflect: 90_000, recall: 15_000 },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await expect(client.reflect("bank", "q")).rejects.toThrow("reflect request timed out after 90s");
|
|
56
|
+
await expect(client.recall("bank", "q")).rejects.toThrow("recall request timed out after 15s");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("falls back to the client request default for ops without an override", async () => {
|
|
60
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
61
|
+
Object.assign(
|
|
62
|
+
async (): Promise<Response> => {
|
|
63
|
+
throw new DOMException("The operation timed out.", "TimeoutError");
|
|
64
|
+
},
|
|
65
|
+
{ preconnect: globalThis.fetch.preconnect },
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
const client = new HindsightApi({
|
|
69
|
+
baseUrl: "https://hindsight.example",
|
|
70
|
+
timeouts: { request: 45_000 },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await expect(client.createBank("bank")).rejects.toThrow("createBank request timed out after 45s");
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/hindsight/client.ts
CHANGED
|
@@ -13,17 +13,32 @@ import type { HindsightConfig } from "./config";
|
|
|
13
13
|
|
|
14
14
|
const USER_AGENT = "oh-my-pi-coding-agent";
|
|
15
15
|
const DEFAULT_USER_AGENT = USER_AGENT;
|
|
16
|
-
|
|
16
|
+
/** Fallback deadlines (ms) applied when the caller supplies no override. */
|
|
17
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
18
|
+
const DEFAULT_REFLECT_TIMEOUT_MS = 120_000;
|
|
19
|
+
const DEFAULT_RECALL_TIMEOUT_MS = 30_000;
|
|
20
|
+
const DEFAULT_RETAIN_TIMEOUT_MS = 60_000;
|
|
17
21
|
|
|
18
22
|
export type Budget = "low" | "mid" | "high" | string;
|
|
19
23
|
export type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
|
|
20
24
|
export type UpdateMode = "replace" | "append";
|
|
21
25
|
export type ConsolidationState = "failed" | "pending" | "done";
|
|
22
26
|
|
|
27
|
+
/** Per-operation request deadlines (ms). Each falls back to a built-in default. */
|
|
28
|
+
export interface HindsightTimeouts {
|
|
29
|
+
/** Default deadline for ops without a specific override. */
|
|
30
|
+
request?: number;
|
|
31
|
+
reflect?: number;
|
|
32
|
+
recall?: number;
|
|
33
|
+
retain?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
export interface HindsightApiOptions {
|
|
24
37
|
baseUrl: string;
|
|
25
38
|
apiKey?: string;
|
|
26
39
|
userAgent?: string;
|
|
40
|
+
/** Per-op deadlines; unset entries fall back to built-in defaults. */
|
|
41
|
+
timeouts?: HindsightTimeouts;
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
/** Caller cancellation shared by Hindsight request option bags. */
|
|
@@ -216,11 +231,17 @@ interface RequestOptions {
|
|
|
216
231
|
/** Return null instead of throwing on a 404 response. */
|
|
217
232
|
allow404?: boolean;
|
|
218
233
|
signal?: AbortSignal;
|
|
234
|
+
/** Op deadline (ms); defaults to the client's request timeout. */
|
|
235
|
+
timeoutMs?: number;
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
export class HindsightApi {
|
|
222
239
|
#baseUrl: string;
|
|
223
240
|
#headers: Record<string, string>;
|
|
241
|
+
#reflectTimeoutMs: number;
|
|
242
|
+
#recallTimeoutMs: number;
|
|
243
|
+
#retainTimeoutMs: number;
|
|
244
|
+
#requestTimeoutMs: number;
|
|
224
245
|
|
|
225
246
|
constructor(options: HindsightApiOptions) {
|
|
226
247
|
this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
@@ -231,6 +252,11 @@ export class HindsightApi {
|
|
|
231
252
|
if (options.apiKey) {
|
|
232
253
|
this.#headers.Authorization = `Bearer ${options.apiKey}`;
|
|
233
254
|
}
|
|
255
|
+
const timeouts = options.timeouts;
|
|
256
|
+
this.#requestTimeoutMs = timeouts?.request ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
257
|
+
this.#reflectTimeoutMs = timeouts?.reflect ?? DEFAULT_REFLECT_TIMEOUT_MS;
|
|
258
|
+
this.#recallTimeoutMs = timeouts?.recall ?? DEFAULT_RECALL_TIMEOUT_MS;
|
|
259
|
+
this.#retainTimeoutMs = timeouts?.retain ?? DEFAULT_RETAIN_TIMEOUT_MS;
|
|
234
260
|
}
|
|
235
261
|
|
|
236
262
|
async retain(bankId: string, content: string, options?: RetainOptions): Promise<RetainResponse> {
|
|
@@ -251,6 +277,7 @@ export class HindsightApi {
|
|
|
251
277
|
{
|
|
252
278
|
body: { items: [item], async: options?.async },
|
|
253
279
|
signal: options?.signal,
|
|
280
|
+
timeoutMs: this.#retainTimeoutMs,
|
|
254
281
|
},
|
|
255
282
|
);
|
|
256
283
|
}
|
|
@@ -282,6 +309,7 @@ export class HindsightApi {
|
|
|
282
309
|
async: options?.async,
|
|
283
310
|
},
|
|
284
311
|
signal: options?.signal,
|
|
312
|
+
timeoutMs: this.#retainTimeoutMs,
|
|
285
313
|
},
|
|
286
314
|
);
|
|
287
315
|
}
|
|
@@ -301,6 +329,7 @@ export class HindsightApi {
|
|
|
301
329
|
tags_match: options?.tagsMatch,
|
|
302
330
|
},
|
|
303
331
|
signal: options?.signal,
|
|
332
|
+
timeoutMs: this.#recallTimeoutMs,
|
|
304
333
|
},
|
|
305
334
|
);
|
|
306
335
|
}
|
|
@@ -319,6 +348,7 @@ export class HindsightApi {
|
|
|
319
348
|
tags_match: options?.tagsMatch,
|
|
320
349
|
},
|
|
321
350
|
signal: options?.signal,
|
|
351
|
+
timeoutMs: this.#reflectTimeoutMs,
|
|
322
352
|
},
|
|
323
353
|
);
|
|
324
354
|
}
|
|
@@ -506,10 +536,11 @@ export class HindsightApi {
|
|
|
506
536
|
if (qs) url += `?${qs}`;
|
|
507
537
|
}
|
|
508
538
|
|
|
539
|
+
const timeoutMs = opts?.timeoutMs ?? this.#requestTimeoutMs;
|
|
509
540
|
const init: RequestInit = {
|
|
510
541
|
method,
|
|
511
542
|
headers: this.#headers,
|
|
512
|
-
signal: withTimeoutSignal(
|
|
543
|
+
signal: withTimeoutSignal(timeoutMs, opts?.signal),
|
|
513
544
|
};
|
|
514
545
|
if (opts?.body !== undefined) {
|
|
515
546
|
init.body = JSON.stringify(pruneUndefined(opts.body));
|
|
@@ -520,7 +551,7 @@ export class HindsightApi {
|
|
|
520
551
|
response = await fetch(url, init);
|
|
521
552
|
} catch (err) {
|
|
522
553
|
const message = isTimeoutError(err)
|
|
523
|
-
? `${operation} request timed out after
|
|
554
|
+
? `${operation} request timed out after ${Math.round(timeoutMs / 1000)}s`
|
|
524
555
|
: `${operation} request failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
525
556
|
throw new HindsightError(message, undefined, err);
|
|
526
557
|
}
|
|
@@ -639,5 +670,11 @@ export function createHindsightClient(config: HindsightConfig & { hindsightApiUr
|
|
|
639
670
|
baseUrl: config.hindsightApiUrl,
|
|
640
671
|
apiKey: config.hindsightApiToken ?? undefined,
|
|
641
672
|
userAgent: USER_AGENT,
|
|
673
|
+
timeouts: {
|
|
674
|
+
request: config.requestTimeoutMs,
|
|
675
|
+
reflect: config.reflectTimeoutMs,
|
|
676
|
+
recall: config.recallTimeoutMs,
|
|
677
|
+
retain: config.retainTimeoutMs,
|
|
678
|
+
},
|
|
642
679
|
});
|
|
643
680
|
}
|
package/src/hindsight/config.ts
CHANGED
|
@@ -42,6 +42,15 @@ export interface HindsightConfig {
|
|
|
42
42
|
|
|
43
43
|
debug: boolean;
|
|
44
44
|
|
|
45
|
+
/** Default per-request client deadline (ms) for ops without a specific override. */
|
|
46
|
+
requestTimeoutMs: number;
|
|
47
|
+
/** Client deadline (ms) for reflect (agentic synthesis; costlier than a metadata fetch). */
|
|
48
|
+
reflectTimeoutMs: number;
|
|
49
|
+
/** Client deadline (ms) for recall. */
|
|
50
|
+
recallTimeoutMs: number;
|
|
51
|
+
/** Client deadline (ms) for retain / retainBatch. */
|
|
52
|
+
retainTimeoutMs: number;
|
|
53
|
+
|
|
45
54
|
mentalModelsEnabled: boolean;
|
|
46
55
|
mentalModelAutoSeed: boolean;
|
|
47
56
|
mentalModelRefreshIntervalMs: number;
|
|
@@ -115,6 +124,10 @@ export function loadHindsightConfig(settings: Settings, env: NodeJS.ProcessEnv =
|
|
|
115
124
|
const recallContextTurnsEnv = envInt(env.HINDSIGHT_RECALL_CONTEXT_TURNS);
|
|
116
125
|
const recallMaxQueryCharsEnv = envInt(env.HINDSIGHT_RECALL_MAX_QUERY_CHARS);
|
|
117
126
|
const retainEveryNTurnsEnv = envInt(env.HINDSIGHT_RETAIN_EVERY_N_TURNS);
|
|
127
|
+
const requestTimeoutMsEnv = envInt(env.HINDSIGHT_REQUEST_TIMEOUT_MS);
|
|
128
|
+
const reflectTimeoutMsEnv = envInt(env.HINDSIGHT_REFLECT_TIMEOUT_MS);
|
|
129
|
+
const recallTimeoutMsEnv = envInt(env.HINDSIGHT_RECALL_TIMEOUT_MS);
|
|
130
|
+
const retainTimeoutMsEnv = envInt(env.HINDSIGHT_RETAIN_TIMEOUT_MS);
|
|
118
131
|
|
|
119
132
|
// Read from settings (each falls back to its schema default).
|
|
120
133
|
const settingsRetainMode = pickRetainMode(settings.get("hindsight.retainMode"));
|
|
@@ -158,6 +171,11 @@ export function loadHindsightConfig(settings: Settings, env: NodeJS.ProcessEnv =
|
|
|
158
171
|
|
|
159
172
|
debug: debugEnv ?? settings.get("hindsight.debug"),
|
|
160
173
|
|
|
174
|
+
requestTimeoutMs: requestTimeoutMsEnv ?? settings.get("hindsight.requestTimeoutMs"),
|
|
175
|
+
reflectTimeoutMs: reflectTimeoutMsEnv ?? settings.get("hindsight.reflectTimeoutMs"),
|
|
176
|
+
recallTimeoutMs: recallTimeoutMsEnv ?? settings.get("hindsight.recallTimeoutMs"),
|
|
177
|
+
retainTimeoutMs: retainTimeoutMsEnv ?? settings.get("hindsight.retainTimeoutMs"),
|
|
178
|
+
|
|
161
179
|
mentalModelsEnabled: settings.get("hindsight.mentalModelsEnabled"),
|
|
162
180
|
mentalModelAutoSeed: settings.get("hindsight.mentalModelAutoSeed"),
|
|
163
181
|
mentalModelRefreshIntervalMs: settings.get("hindsight.mentalModelRefreshIntervalMs"),
|
|
@@ -90,3 +90,43 @@ export async function sessionFilesFromDisk(): Promise<Map<string, string>> {
|
|
|
90
90
|
for (const dir of artifactsDirsFromRegistry()) await scan(dir, 0);
|
|
91
91
|
return found;
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Availability half of the `history://` resolution semantics: true when a
|
|
96
|
+
* transcript for `agentId` can be served from a registered ref's live session
|
|
97
|
+
* or retained session file, or from an on-disk `.jsonl` under a known
|
|
98
|
+
* artifacts dir. Hint surfaces use this so they only advertise
|
|
99
|
+
* `history://<agentId>` links that `HistoryProtocolHandler` can actually
|
|
100
|
+
* resolve. A retained sessionFile path is verified on disk before it counts,
|
|
101
|
+
* and probing never throws: a stale path or unreadable artifacts subtree
|
|
102
|
+
* reads as unavailable instead of disturbing the caller's delivery path.
|
|
103
|
+
*/
|
|
104
|
+
export async function hasResolvableTranscript(agentId: string): Promise<boolean> {
|
|
105
|
+
try {
|
|
106
|
+
const registry = AgentRegistry.global();
|
|
107
|
+
const lower = agentId.toLowerCase();
|
|
108
|
+
let ref = registry.get(agentId);
|
|
109
|
+
if (ref?.kind === "advisor") ref = undefined;
|
|
110
|
+
ref ??= registry.list().find(candidate => candidate.kind !== "advisor" && candidate.id.toLowerCase() === lower);
|
|
111
|
+
if (ref?.session) return true;
|
|
112
|
+
if (ref?.sessionFile && (await isReadableFile(ref.sessionFile))) return true;
|
|
113
|
+
const files = await sessionFilesFromDisk();
|
|
114
|
+
for (const id of files.keys()) {
|
|
115
|
+
if (id.toLowerCase() === lower) return true;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Availability probing is advisory; any filesystem failure means the
|
|
119
|
+
// transcript cannot be promised, never that delivery should fail.
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** True when `file` exists and is a regular file; never throws. */
|
|
125
|
+
async function isReadableFile(file: string): Promise<boolean> {
|
|
126
|
+
try {
|
|
127
|
+
const stat = await fs.stat(file);
|
|
128
|
+
return stat.isFile();
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/launch/broker.ts
CHANGED
|
@@ -491,8 +491,17 @@ class DaemonBroker {
|
|
|
491
491
|
await this.#launch(record);
|
|
492
492
|
let readyTimedOut = false;
|
|
493
493
|
if (spec.ready && !terminalState(record.snapshot.state)) {
|
|
494
|
-
|
|
495
|
-
|
|
494
|
+
// Wake on the sticky readyAt marker or any terminal state, not the live
|
|
495
|
+
// state: a fast process flips starting→ready→exited within one poll
|
|
496
|
+
// interval, so sampling `state === "ready"` never observes readiness even
|
|
497
|
+
// though #markReady durably recorded readyAt. A pre-ready exit must also
|
|
498
|
+
// wake the wait rather than block for the full timeout.
|
|
499
|
+
const ready = await this.#waitUntil(
|
|
500
|
+
record,
|
|
501
|
+
() => record.snapshot.readyAt !== undefined || terminalState(record.snapshot.state),
|
|
502
|
+
spec.ready.timeoutMs,
|
|
503
|
+
);
|
|
504
|
+
readyTimedOut = !ready;
|
|
496
505
|
}
|
|
497
506
|
await record.persistQueue;
|
|
498
507
|
return { op: "start", daemon: record.snapshot, readyTimedOut };
|
|
@@ -748,6 +757,11 @@ class DaemonBroker {
|
|
|
748
757
|
const uptime = Date.now() - record.snapshot.startedAt;
|
|
749
758
|
record.consecutiveFailures = uptime >= 30_000 ? 0 : record.consecutiveFailures + 1;
|
|
750
759
|
record.snapshot.restartCount++;
|
|
760
|
+
// Readiness belongs to the exited generation; clear it before the backoff
|
|
761
|
+
// so start / for:"ready" waits don't treat a dead service as ready during
|
|
762
|
+
// the restart window (readyAt is re-set by #launch once the child is up).
|
|
763
|
+
record.snapshot.readyAt = undefined;
|
|
764
|
+
record.snapshot.readyMatch = undefined;
|
|
751
765
|
record.snapshot.state = "restarting";
|
|
752
766
|
const delay = Math.min(1_000 * 2 ** Math.min(record.consecutiveFailures, 5), RESTART_MAX_DELAY_MS);
|
|
753
767
|
record.log?.append(
|
|
@@ -812,6 +826,12 @@ class DaemonBroker {
|
|
|
812
826
|
throw new Error(`Invalid wait regex: ${error instanceof Error ? error.message : String(error)}`);
|
|
813
827
|
}
|
|
814
828
|
}
|
|
829
|
+
// Readiness was actually observed: the sticky readyAt survives a fast
|
|
830
|
+
// ready→exit, a live "ready" state, or a "running" daemon with no ready spec.
|
|
831
|
+
const readyObserved = (): boolean =>
|
|
832
|
+
record.snapshot.readyAt !== undefined ||
|
|
833
|
+
record.snapshot.state === "ready" ||
|
|
834
|
+
(record.snapshot.state === "running" && !record.spec.ready);
|
|
815
835
|
const condition = (): boolean => {
|
|
816
836
|
if (pattern) {
|
|
817
837
|
const match = pattern.exec(record.readinessBuffer);
|
|
@@ -820,10 +840,16 @@ class DaemonBroker {
|
|
|
820
840
|
return true;
|
|
821
841
|
}
|
|
822
842
|
if (operation.for === "exit") return terminalState(record.snapshot.state);
|
|
823
|
-
|
|
843
|
+
// Wake on observed readiness or any terminal state so the wait never
|
|
844
|
+
// blocks for the full timeout; success is judged by readyObserved below.
|
|
845
|
+
return readyObserved() || terminalState(record.snapshot.state);
|
|
824
846
|
};
|
|
825
|
-
const
|
|
826
|
-
|
|
847
|
+
const woke = condition() || (await this.#waitUntil(record, condition, operation.timeoutMs));
|
|
848
|
+
// A for:"ready" wait that woke on a terminal exit without ever observing
|
|
849
|
+
// readiness is still "not ready" — surface it as timed out so callers and the
|
|
850
|
+
// renderer don't chain work against a dead process.
|
|
851
|
+
const timedOut = operation.for === "ready" && !pattern ? !readyObserved() : !woke;
|
|
852
|
+
return { op: "wait", daemon: record.snapshot, matched, timedOut };
|
|
827
853
|
}
|
|
828
854
|
|
|
829
855
|
async #send(operation: Extract<DaemonOperation, { op: "send" }>): Promise<DaemonRpcResult> {
|
package/src/lsp/client.ts
CHANGED
|
@@ -846,6 +846,29 @@ export async function getOrCreateClient(
|
|
|
846
846
|
return clientPromise;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
+
/** Return an active or already-starting client without starting a language server. */
|
|
850
|
+
export async function getActiveOrPendingClient(
|
|
851
|
+
config: ServerConfig,
|
|
852
|
+
cwd: string,
|
|
853
|
+
signal?: AbortSignal,
|
|
854
|
+
): Promise<LspClient | undefined> {
|
|
855
|
+
throwIfAborted(signal);
|
|
856
|
+
const client = clients.get(`${config.command}:${cwd}`);
|
|
857
|
+
if (client) {
|
|
858
|
+
client.lastActivity = Date.now();
|
|
859
|
+
return client;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const pending = clientLocks.get(`${config.command}:${cwd}`);
|
|
863
|
+
if (!pending) return undefined;
|
|
864
|
+
try {
|
|
865
|
+
return await untilAborted(signal, pending);
|
|
866
|
+
} catch {
|
|
867
|
+
throwIfAborted(signal);
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
849
872
|
/**
|
|
850
873
|
* Ensure a file is opened in the LSP client.
|
|
851
874
|
* Sends didOpen notification if the file is not already tracked.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Biome CLI-based linter client.
|
|
3
3
|
* Uses Biome's CLI with JSON output instead of LSP (which has stale diagnostics issues).
|
|
4
4
|
*/
|
|
5
|
-
import path from "node:path";
|
|
5
|
+
import * as path from "node:path";
|
|
6
6
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
|
|
8
8
|
|
|
@@ -141,14 +141,13 @@ export class BiomeClient implements LinterClient {
|
|
|
141
141
|
) {}
|
|
142
142
|
|
|
143
143
|
async format(filePath: string, content: string): Promise<string> {
|
|
144
|
-
//
|
|
144
|
+
// Keep the standalone LinterClient contract: callers supply the content to
|
|
145
|
+
// format, regardless of what is currently on disk.
|
|
145
146
|
await Bun.write(filePath, content);
|
|
146
147
|
|
|
147
|
-
// Run biome format --write
|
|
148
148
|
const result = await runBiome(["format", "--write", filePath], this.cwd, this.config.resolvedCommand);
|
|
149
149
|
|
|
150
150
|
if (result.success) {
|
|
151
|
-
// Read back formatted content
|
|
152
151
|
return await Bun.file(filePath).text();
|
|
153
152
|
}
|
|
154
153
|
|
package/src/lsp/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
ensureFileOpen,
|
|
21
21
|
FileChangeType,
|
|
22
22
|
getActiveClients,
|
|
23
|
+
getActiveOrPendingClient,
|
|
23
24
|
getOrCreateClient,
|
|
24
25
|
type LspServerStatus,
|
|
25
26
|
notifySaved,
|
|
@@ -211,6 +212,7 @@ async function syncFileContent(
|
|
|
211
212
|
cwd: string,
|
|
212
213
|
servers: Array<[string, ServerConfig]>,
|
|
213
214
|
signal?: AbortSignal,
|
|
215
|
+
createMissing = true,
|
|
214
216
|
): Promise<void> {
|
|
215
217
|
throwIfAborted(signal);
|
|
216
218
|
await Promise.allSettled(
|
|
@@ -219,11 +221,15 @@ async function syncFileContent(
|
|
|
219
221
|
if (serverConfig.createClient) {
|
|
220
222
|
return;
|
|
221
223
|
}
|
|
222
|
-
const client =
|
|
224
|
+
const client = createMissing
|
|
225
|
+
? await getOrCreateClient(serverConfig, cwd, undefined, signal)
|
|
226
|
+
: await getActiveOrPendingClient(serverConfig, cwd, signal);
|
|
227
|
+
if (!client) return;
|
|
223
228
|
throwIfAborted(signal);
|
|
224
229
|
await syncContent(client, absolutePath, content, signal);
|
|
225
230
|
}),
|
|
226
231
|
);
|
|
232
|
+
throwIfAborted(signal);
|
|
227
233
|
}
|
|
228
234
|
|
|
229
235
|
/**
|
|
@@ -239,6 +245,7 @@ async function notifyFileSaved(
|
|
|
239
245
|
cwd: string,
|
|
240
246
|
servers: Array<[string, ServerConfig]>,
|
|
241
247
|
signal?: AbortSignal,
|
|
248
|
+
createMissing = true,
|
|
242
249
|
): Promise<void> {
|
|
243
250
|
throwIfAborted(signal);
|
|
244
251
|
await Promise.allSettled(
|
|
@@ -247,10 +254,14 @@ async function notifyFileSaved(
|
|
|
247
254
|
if (serverConfig.createClient) {
|
|
248
255
|
return;
|
|
249
256
|
}
|
|
250
|
-
const client =
|
|
257
|
+
const client = createMissing
|
|
258
|
+
? await getOrCreateClient(serverConfig, cwd, undefined, signal)
|
|
259
|
+
: await getActiveOrPendingClient(serverConfig, cwd, signal);
|
|
260
|
+
if (!client) return;
|
|
251
261
|
await notifySaved(client, absolutePath, signal);
|
|
252
262
|
}),
|
|
253
263
|
);
|
|
264
|
+
throwIfAborted(signal);
|
|
254
265
|
}
|
|
255
266
|
|
|
256
267
|
// Cache config per cwd to avoid repeated file I/O
|
|
@@ -1333,7 +1344,8 @@ async function runLspWritethrough(
|
|
|
1333
1344
|
// Capture diagnostic versions BEFORE syncing to detect stale diagnostics
|
|
1334
1345
|
// Bound client creation by the writethrough budget: a hung/broken server
|
|
1335
1346
|
// must not add its full init wait (30s default) to every edit.
|
|
1336
|
-
const
|
|
1347
|
+
const minVersionsPromise = enableDiagnostics ? captureDiagnosticVersions(cwd, servers, 5_000, signal) : undefined;
|
|
1348
|
+
let minVersions = useCustomFormatter ? undefined : await minVersionsPromise;
|
|
1337
1349
|
let expectedDocumentVersions: ServerVersionMap | undefined;
|
|
1338
1350
|
|
|
1339
1351
|
let formatter: FileFormatResult | undefined;
|
|
@@ -1353,13 +1365,19 @@ async function runLspWritethrough(
|
|
|
1353
1365
|
operationSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
1354
1366
|
await untilAborted(operationSignal, async () => {
|
|
1355
1367
|
if (useCustomFormatter) {
|
|
1356
|
-
// Custom linters
|
|
1368
|
+
// Custom linters operate on on-disk input; the shared pre-write also
|
|
1369
|
+
// supports implementations that inspect the file before formatting.
|
|
1357
1370
|
await writeContent(content);
|
|
1358
|
-
|
|
1371
|
+
const [formattedContent, capturedVersions] = await Promise.all([
|
|
1372
|
+
formatContent(dst, content, cwd, customLinterServers, operationSignal),
|
|
1373
|
+
minVersionsPromise,
|
|
1374
|
+
]);
|
|
1375
|
+
finalContent = formattedContent;
|
|
1376
|
+
minVersions = capturedVersions;
|
|
1359
1377
|
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
1360
1378
|
await writeContent(finalContent);
|
|
1361
1379
|
await notifyWriteCommitted(operationSignal);
|
|
1362
|
-
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
|
|
1380
|
+
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal, enableDiagnostics);
|
|
1363
1381
|
} else {
|
|
1364
1382
|
// 1. Sync original content to LSP servers
|
|
1365
1383
|
await syncFileContent(dst, content, cwd, lspServers, operationSignal);
|
|
@@ -1385,7 +1403,7 @@ async function runLspWritethrough(
|
|
|
1385
1403
|
}
|
|
1386
1404
|
|
|
1387
1405
|
// 5. Notify saved to LSP servers
|
|
1388
|
-
await notifyFileSaved(dst, cwd, lspServers, operationSignal);
|
|
1406
|
+
await notifyFileSaved(dst, cwd, lspServers, operationSignal, !useCustomFormatter || enableDiagnostics);
|
|
1389
1407
|
});
|
|
1390
1408
|
synced = true;
|
|
1391
1409
|
} catch {
|
|
@@ -1565,7 +1583,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1565
1583
|
_context?: AgentToolContext,
|
|
1566
1584
|
): Promise<AgentToolResult<LspToolDetails>> {
|
|
1567
1585
|
const { action, file, line, symbol, query, new_name, apply, timeout } = params;
|
|
1568
|
-
const timeoutSec = clampTimeout("lsp", timeout);
|
|
1586
|
+
const timeoutSec = clampTimeout("lsp", timeout, this.session.settings.get("tools.maxTimeout"));
|
|
1569
1587
|
const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
|
|
1570
1588
|
const callerSignal = signal;
|
|
1571
1589
|
signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
|
package/src/main.ts
CHANGED
|
@@ -134,6 +134,7 @@ async function checkForNewVersion(currentVersion: string): Promise<string | unde
|
|
|
134
134
|
// embedders need project-level opt-outs for reminder/prelude prompt injection.
|
|
135
135
|
const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
|
|
136
136
|
"task.isolation.mode",
|
|
137
|
+
"task.isolation.apply",
|
|
137
138
|
"task.isolation.merge",
|
|
138
139
|
"task.isolation.commits",
|
|
139
140
|
"task.eager",
|
|
@@ -882,6 +883,7 @@ export async function buildSessionOptions(
|
|
|
882
883
|
cliProvider: parsed.provider,
|
|
883
884
|
cliModel: parsed.model,
|
|
884
885
|
modelRegistry,
|
|
886
|
+
availableModels: modelRegistry.getAvailable(),
|
|
885
887
|
settings: activeSettings,
|
|
886
888
|
preferences: modelMatchPreferences,
|
|
887
889
|
});
|
|
@@ -954,13 +956,22 @@ export async function buildSessionOptions(
|
|
|
954
956
|
if (resolved.warning) {
|
|
955
957
|
process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
|
|
956
958
|
}
|
|
959
|
+
// Prewalk is an optional optimization (off by default): switch to a fast
|
|
960
|
+
// model at the first edit. If its hand-off target can't be resolved or has
|
|
961
|
+
// no configured auth, warn and leave prewalk unarmed rather than aborting
|
|
962
|
+
// startup and locking the user out of the app (issue #6064).
|
|
957
963
|
if (resolved.error || !resolved.model) {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
964
|
+
const target = parsed.prewalkInto ?? DEFAULT_PREWALK_TARGET;
|
|
965
|
+
process.stderr.write(
|
|
966
|
+
`${chalk.yellow(`Warning: prewalk disabled — ${resolved.error ?? `model "${target}" not found`}`)}\n`,
|
|
967
|
+
);
|
|
968
|
+
} else if (!modelRegistry.hasConfiguredAuth(resolved.model)) {
|
|
969
|
+
process.stderr.write(
|
|
970
|
+
`${chalk.yellow(`Warning: prewalk disabled — no API key for ${resolved.model.provider}/${resolved.model.id}`)}\n`,
|
|
971
|
+
);
|
|
972
|
+
} else {
|
|
973
|
+
options.prewalk = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
962
974
|
}
|
|
963
|
-
options.prewalk = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
964
975
|
}
|
|
965
976
|
|
|
966
977
|
if (parsed.planYoloInto !== undefined && !parsed.planYolo) {
|