pi-oracle 0.3.2 → 0.3.3
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 +13 -0
- package/README.md +2 -0
- package/docs/ORACLE_DESIGN.md +1 -1
- package/extensions/oracle/lib/config.ts +112 -1
- package/extensions/oracle/lib/locks.ts +6 -3
- package/extensions/oracle/lib/tools.ts +13 -13
- package/extensions/oracle/worker/state-locks.d.mts +1 -0
- package/extensions/oracle/worker/state-locks.mjs +3 -1
- package/package.json +1 -1
- package/prompts/oracle.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3 - 2026-04-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `oracle_submit` now accepts canonical preset ids plus matching human-readable preset labels/common hyphen-space variants and normalizes them back to the canonical preset id at submit time
|
|
7
|
+
- lock sweeping now gives in-flight `.tmp-*` lock/state directories a dedicated grace window so concurrent sweep does not delete another process's atomic publish
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- oracle submit metadata/docs now point preset discovery at the canonical registry/README while keeping execute-time normalization for flexible caller input
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- closed a concurrent stale-lock sweep race that could reclaim another process's in-flight lock publish before metadata landed
|
|
14
|
+
- oracle sanity coverage now verifies preset alias validation/normalization and the `.tmp-*` grace window behavior end-to-end
|
|
15
|
+
|
|
3
16
|
## 0.3.2 - 2026-04-08
|
|
4
17
|
|
|
5
18
|
### Changed
|
package/README.md
CHANGED
|
@@ -125,6 +125,8 @@ Notes:
|
|
|
125
125
|
| `instant` | Instant |
|
|
126
126
|
| `instant_auto_switch` | Instant - Auto-switch to Thinking Enabled |
|
|
127
127
|
|
|
128
|
+
`oracle_submit` accepts either the canonical preset id or the matching human-readable preset label; common space/hyphen variants are normalized automatically at submit time. Keep `defaults.preset` in config on the canonical preset id.
|
|
129
|
+
|
|
128
130
|
Other useful settings:
|
|
129
131
|
- `browser.runMode`
|
|
130
132
|
- `browser.args`
|
package/docs/ORACLE_DESIGN.md
CHANGED
|
@@ -133,7 +133,7 @@ The authenticated seed profile remains the source of truth for future oracle run
|
|
|
133
133
|
|
|
134
134
|
### `oracle_submit`
|
|
135
135
|
|
|
136
|
-
Agent-facing submissions use **`preset`**; the canonical registry is `ORACLE_SUBMIT_PRESETS` in `extensions/oracle/lib/config.ts`. **`preset` is the only model-selection parameter** on `oracle_submit`. There are no `modelFamily`, `effort`, or `autoSwitchToThinking` fields.
|
|
136
|
+
Agent-facing submissions use **`preset`**; the canonical registry is `ORACLE_SUBMIT_PRESETS` in `extensions/oracle/lib/config.ts`. **`preset` is the only model-selection parameter** on `oracle_submit`. There are no `modelFamily`, `effort`, or `autoSwitchToThinking` fields. Submit-time inputs accept canonical preset ids plus matching human-readable labels/common hyphen-space variants, and the tool normalizes them back to the canonical id before persisting job state.
|
|
137
137
|
|
|
138
138
|
1. resolve the preset (submit-time or config default) into an execution snapshot
|
|
139
139
|
2. resolve optional `followUpJobId` into a prior `chatUrl` and `conversationId`
|
|
@@ -29,6 +29,117 @@ export type OracleSubmitPresetId = keyof typeof ORACLE_SUBMIT_PRESETS;
|
|
|
29
29
|
|
|
30
30
|
export type OracleSubmitPreset = typeof ORACLE_SUBMIT_PRESETS[OracleSubmitPresetId];
|
|
31
31
|
|
|
32
|
+
export const ORACLE_SUBMIT_PRESET_IDS = Object.freeze(Object.keys(ORACLE_SUBMIT_PRESETS) as OracleSubmitPresetId[]);
|
|
33
|
+
|
|
34
|
+
function normalizeOracleSubmitPresetLookupKey(value: string): string {
|
|
35
|
+
return value
|
|
36
|
+
.trim()
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[_-]+/g, " ")
|
|
39
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
40
|
+
.replace(/\s+/g, " ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function splitOracleSubmitPresetWords(value: string): string[] {
|
|
44
|
+
return value
|
|
45
|
+
.trim()
|
|
46
|
+
.replace(/[_-]+/g, " ")
|
|
47
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lowercaseWords(words: readonly string[]): string[] {
|
|
53
|
+
return words.map((word) => word.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function titleCaseWords(words: readonly string[]): string[] {
|
|
57
|
+
return words.map((word) => (word ? `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}` : word));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildOracleSubmitPresetSeparatorVariants(words: readonly string[]): string[] {
|
|
61
|
+
const normalizedWords = words.map((word) => word.trim()).filter(Boolean);
|
|
62
|
+
if (normalizedWords.length === 0) return [];
|
|
63
|
+
|
|
64
|
+
const variants = new Set<string>();
|
|
65
|
+
const build = (index: number, current: string): void => {
|
|
66
|
+
if (index >= normalizedWords.length) {
|
|
67
|
+
variants.add(current);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const separator of [" ", "-"] as const) {
|
|
71
|
+
build(index + 1, `${current}${separator}${normalizedWords[index]}`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
build(1, normalizedWords[0]!);
|
|
76
|
+
return [...variants];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildOracleSubmitPresetJoinVariants(words: readonly string[]): string[] {
|
|
80
|
+
const normalizedWords = words.map((word) => word.trim()).filter(Boolean);
|
|
81
|
+
if (normalizedWords.length === 0) return [];
|
|
82
|
+
|
|
83
|
+
const lowercase = lowercaseWords(normalizedWords);
|
|
84
|
+
const titleWords = titleCaseWords(lowercase);
|
|
85
|
+
return [
|
|
86
|
+
...buildOracleSubmitPresetSeparatorVariants(normalizedWords),
|
|
87
|
+
...buildOracleSubmitPresetSeparatorVariants(lowercase),
|
|
88
|
+
...buildOracleSubmitPresetSeparatorVariants(titleWords),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildOracleSubmitPresetAliases(id: OracleSubmitPresetId, preset: OracleSubmitPreset): string[] {
|
|
93
|
+
const idWords = splitOracleSubmitPresetWords(id);
|
|
94
|
+
const labelWords = splitOracleSubmitPresetWords(preset.label);
|
|
95
|
+
return [
|
|
96
|
+
id,
|
|
97
|
+
...buildOracleSubmitPresetJoinVariants(idWords),
|
|
98
|
+
preset.label,
|
|
99
|
+
preset.label.toLowerCase(),
|
|
100
|
+
...buildOracleSubmitPresetJoinVariants(labelWords),
|
|
101
|
+
].filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildOracleSubmitPresetLookupArtifacts(): {
|
|
105
|
+
acceptedInputs: readonly string[];
|
|
106
|
+
lookup: ReadonlyMap<string, OracleSubmitPresetId>;
|
|
107
|
+
} {
|
|
108
|
+
const lookup = new Map<string, OracleSubmitPresetId>();
|
|
109
|
+
const aliases = new Set<string>();
|
|
110
|
+
|
|
111
|
+
for (const [id, preset] of Object.entries(ORACLE_SUBMIT_PRESETS) as [OracleSubmitPresetId, OracleSubmitPreset][]) {
|
|
112
|
+
for (const alias of buildOracleSubmitPresetAliases(id, preset)) {
|
|
113
|
+
const normalized = normalizeOracleSubmitPresetLookupKey(alias);
|
|
114
|
+
if (!normalized) continue;
|
|
115
|
+
const existing = lookup.get(normalized);
|
|
116
|
+
if (existing && existing !== id) {
|
|
117
|
+
throw new Error(`Conflicting oracle_submit preset alias: ${alias} matches both ${existing} and ${id}`);
|
|
118
|
+
}
|
|
119
|
+
lookup.set(normalized, id);
|
|
120
|
+
if (alias !== id) aliases.add(alias);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
acceptedInputs: Object.freeze([...ORACLE_SUBMIT_PRESET_IDS, ...[...aliases].sort((left, right) => left.localeCompare(right))]),
|
|
126
|
+
lookup,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ORACLE_SUBMIT_PRESET_LOOKUP_ARTIFACTS = buildOracleSubmitPresetLookupArtifacts();
|
|
131
|
+
|
|
132
|
+
export const ORACLE_SUBMIT_PRESET_ACCEPTED_INPUTS = ORACLE_SUBMIT_PRESET_LOOKUP_ARTIFACTS.acceptedInputs;
|
|
133
|
+
|
|
134
|
+
export function coerceOracleSubmitPresetId(value: string): OracleSubmitPresetId {
|
|
135
|
+
const normalized = normalizeOracleSubmitPresetLookupKey(value);
|
|
136
|
+
const presetId = ORACLE_SUBMIT_PRESET_LOOKUP_ARTIFACTS.lookup.get(normalized);
|
|
137
|
+
if (presetId) return presetId;
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Unknown oracle_submit preset: ${value}. Use one of the canonical ids (${ORACLE_SUBMIT_PRESET_IDS.join(", ")}) or a matching preset label.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
32
143
|
export function getOracleSubmitPresetById(id: OracleSubmitPresetId): OracleSubmitPreset {
|
|
33
144
|
const found = ORACLE_SUBMIT_PRESETS[id];
|
|
34
145
|
if (!found) {
|
|
@@ -336,7 +447,7 @@ function normalizeLegacyBrowserConfig(root: Record<string, unknown>): Record<str
|
|
|
336
447
|
return root;
|
|
337
448
|
}
|
|
338
449
|
|
|
339
|
-
const PRESET_IDS =
|
|
450
|
+
const PRESET_IDS = ORACLE_SUBMIT_PRESET_IDS;
|
|
340
451
|
|
|
341
452
|
function validateOracleConfig(value: unknown): OracleConfig {
|
|
342
453
|
const root = normalizeLegacyBrowserConfig(expectObject(value, "root"));
|
|
@@ -12,6 +12,8 @@ const LEASES_DIR = join(ORACLE_STATE_DIR, "leases");
|
|
|
12
12
|
const DEFAULT_WAIT_MS = 30_000;
|
|
13
13
|
const POLL_MS = 200;
|
|
14
14
|
export const ORACLE_METADATA_WRITE_GRACE_MS = 1_000;
|
|
15
|
+
/** Incomplete `.tmp-*` dirs are in-flight atomic creates; a 1s grace is too short under multi-process sweep + slow FS. */
|
|
16
|
+
export const ORACLE_TMP_STATE_DIR_GRACE_MS = 60_000;
|
|
15
17
|
|
|
16
18
|
export interface OracleLockHandle {
|
|
17
19
|
path: string;
|
|
@@ -90,7 +92,8 @@ function isIncompleteStateDirStale(path: string, now = Date.now()): boolean {
|
|
|
90
92
|
try {
|
|
91
93
|
const stats = statSync(path);
|
|
92
94
|
const baselineMs = Math.max(stats.mtimeMs, stats.ctimeMs);
|
|
93
|
-
|
|
95
|
+
const graceMs = basename(path).startsWith(".tmp-") ? ORACLE_TMP_STATE_DIR_GRACE_MS : ORACLE_METADATA_WRITE_GRACE_MS;
|
|
96
|
+
return now - baselineMs >= graceMs;
|
|
94
97
|
} catch {
|
|
95
98
|
return false;
|
|
96
99
|
}
|
|
@@ -138,13 +141,13 @@ async function maybeReclaimStaleLock(path: string, now = Date.now()): Promise<bo
|
|
|
138
141
|
return true;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
export async function sweepStaleLocks(): Promise<string[]> {
|
|
144
|
+
export async function sweepStaleLocks(now = Date.now()): Promise<string[]> {
|
|
142
145
|
const dir = getLocksDir();
|
|
143
146
|
const removed: string[] = [];
|
|
144
147
|
|
|
145
148
|
for (const name of readdirSync(dir)) {
|
|
146
149
|
const path = join(dir, name);
|
|
147
|
-
if (await maybeReclaimStaleLock(path)) {
|
|
150
|
+
if (await maybeReclaimStaleLock(path, now)) {
|
|
148
151
|
removed.push(path);
|
|
149
152
|
}
|
|
150
153
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Purpose: Register oracle extension tools and implement submit/read/cancel behavior.
|
|
2
|
+
// Responsibilities: Validate tool parameters, create archives, enqueue or dispatch jobs, and surface job state.
|
|
3
|
+
// Scope: Tool-facing orchestration only; durable job storage, locks, runtime leases, and config live in sibling modules.
|
|
4
|
+
// Usage: Imported by the oracle extension entrypoint and sanity tests to register tools against the pi API.
|
|
5
|
+
// Invariants/Assumptions: The pi runtime validates TypeBox schemas before execute, while execute owns semantic normalization.
|
|
1
6
|
import { randomUUID } from "node:crypto";
|
|
2
7
|
import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
8
|
import { tmpdir } from "node:os";
|
|
@@ -6,10 +11,9 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
6
11
|
import { Type } from "@sinclair/typebox";
|
|
7
12
|
import { isLockTimeoutError, withGlobalReconcileLock, withLock } from "./locks.js";
|
|
8
13
|
import {
|
|
14
|
+
coerceOracleSubmitPresetId,
|
|
9
15
|
loadOracleConfig,
|
|
10
|
-
ORACLE_SUBMIT_PRESETS,
|
|
11
16
|
resolveOracleSubmitPreset,
|
|
12
|
-
type OracleSubmitPresetId,
|
|
13
17
|
} from "./config.js";
|
|
14
18
|
import {
|
|
15
19
|
appendCleanupWarnings,
|
|
@@ -48,10 +52,6 @@ import {
|
|
|
48
52
|
tryAcquireRuntimeLease,
|
|
49
53
|
} from "./runtime.js";
|
|
50
54
|
|
|
51
|
-
function stringEnum(values: readonly string[], description: string) {
|
|
52
|
-
return Type.Union(values.map((value) => Type.Literal(value)), { description });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
55
|
const ORACLE_SUBMIT_PARAMS = Type.Object({
|
|
56
56
|
prompt: Type.String({ description: "Prompt text to send to ChatGPT web." }),
|
|
57
57
|
files: Type.Array(Type.String({ description: "Project-relative file or directory path to include in the archive." }), {
|
|
@@ -59,10 +59,10 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
|
|
|
59
59
|
minItems: 1,
|
|
60
60
|
}),
|
|
61
61
|
preset: Type.Optional(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
),
|
|
62
|
+
Type.String({
|
|
63
|
+
description:
|
|
64
|
+
"ChatGPT model preset. Omit to use the configured default preset. Canonical ids are preferred; matching human-readable preset labels and common hyphen/space variants are normalized automatically.",
|
|
65
|
+
}),
|
|
66
66
|
),
|
|
67
67
|
followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose chat thread should be continued." })),
|
|
68
68
|
});
|
|
@@ -579,7 +579,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
579
579
|
label: "Oracle Submit",
|
|
580
580
|
description:
|
|
581
581
|
"Dispatch a background ChatGPT web oracle job after gathering context. Always pass a prompt and exact project-relative archive inputs. " +
|
|
582
|
-
"Optional ChatGPT model: set parameter `preset`, or omit it for configured defaults
|
|
582
|
+
"Optional ChatGPT model: set parameter `preset`, or omit it for configured defaults; canonical preset ids are listed in the README and ORACLE_SUBMIT_PRESETS registry, and matching labels are normalized at submit time.",
|
|
583
583
|
promptSnippet: "Dispatch a background ChatGPT web oracle job after gathering repo context.",
|
|
584
584
|
promptGuidelines: [
|
|
585
585
|
"Gather context before calling oracle_submit.",
|
|
@@ -591,7 +591,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
591
591
|
"If oracle_submit itself fails because the local archive still exceeds the upload limit after default exclusions and automatic generic generated-output-dir pruning, or for any other submit-time error, stop and report the error instead of retrying automatically.",
|
|
592
592
|
"If oracle_submit returns a queued job instead of an immediately dispatched one, treat that as success and stop exactly the same way.",
|
|
593
593
|
"Stop after dispatching oracle_submit; do not continue the task while the oracle job is running.",
|
|
594
|
-
"Use `preset` as the only model-selection parameter on oracle_submit.
|
|
594
|
+
"Use `preset` as the only model-selection parameter on oracle_submit. Canonical ids are preferred, and matching human-readable preset labels are normalized automatically. Omit preset to use the configured default.",
|
|
595
595
|
],
|
|
596
596
|
parameters: ORACLE_SUBMIT_PARAMS,
|
|
597
597
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -599,7 +599,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
599
599
|
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
600
600
|
const projectId = getProjectId(ctx.cwd);
|
|
601
601
|
const sessionId = getSessionId(originSessionFile, projectId);
|
|
602
|
-
const presetId =
|
|
602
|
+
const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
|
|
603
603
|
const selection = resolveOracleSubmitPreset(presetId);
|
|
604
604
|
const followUp = resolveFollowUp(params.followUpJobId, ctx.cwd);
|
|
605
605
|
try {
|
|
@@ -6,6 +6,7 @@ import { basename, join } from "node:path";
|
|
|
6
6
|
const DEFAULT_WAIT_MS = 30_000;
|
|
7
7
|
const POLL_MS = 200;
|
|
8
8
|
export const ORACLE_METADATA_WRITE_GRACE_MS = 1_000;
|
|
9
|
+
export const ORACLE_TMP_STATE_DIR_GRACE_MS = 60_000;
|
|
9
10
|
|
|
10
11
|
async function sleep(ms) {
|
|
11
12
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -76,7 +77,8 @@ function isIncompleteStateDirStale(path, now = Date.now()) {
|
|
|
76
77
|
try {
|
|
77
78
|
const stats = statSync(path);
|
|
78
79
|
const baselineMs = Math.max(stats.mtimeMs, stats.ctimeMs);
|
|
79
|
-
|
|
80
|
+
const graceMs = basename(path).startsWith(".tmp-") ? ORACLE_TMP_STATE_DIR_GRACE_MS : ORACLE_METADATA_WRITE_GRACE_MS;
|
|
81
|
+
return now - baselineMs >= graceMs;
|
|
80
82
|
} catch {
|
|
81
83
|
return false;
|
|
82
84
|
}
|
package/package.json
CHANGED
package/prompts/oracle.md
CHANGED
|
@@ -14,7 +14,8 @@ Required workflow:
|
|
|
14
14
|
6. Stop immediately after dispatching the oracle job.
|
|
15
15
|
|
|
16
16
|
Oracle model (`oracle_submit`):
|
|
17
|
-
- To choose a specific ChatGPT model, pass **`preset`** with one of the allowed ids from the
|
|
17
|
+
- To choose a specific ChatGPT model, pass **`preset`** with one of the allowed ids from the canonical preset registry.
|
|
18
|
+
- Matching human-readable preset labels and common hyphen/space variants are also accepted and normalized automatically, but prefer canonical ids when readily available.
|
|
18
19
|
- **Or** omit **`preset`** entirely to use the configured default model (from oracle config).
|
|
19
20
|
- **`preset`** is the only model-selection parameter on `oracle_submit`. Do not pass `modelFamily`, `effort`, or `autoSwitchToThinking`.
|
|
20
21
|
- If unsure which preset fits the task, ask the user.
|
|
@@ -27,7 +28,7 @@ Rules:
|
|
|
27
28
|
- If the request depends on git state or pending changes (for example code review, ship readiness, or release approval), create a tracked diff bundle file inside the repo (for example under `.pi/`) containing `git status` plus `git diff` output, include that file in the archive, and tell the oracle to use it because the `.git` directory is not included in oracle exports.
|
|
28
29
|
- When `files=["."]` and the post-exclusion archive is still too large, submit automatically prunes the largest nested directories matching generic generated-output names like `build/`, `dist/`, `out/`, `coverage/`, and `tmp/` outside obvious source roots like `src/` and `lib/` until the archive fits or no candidate remains. Successful submissions report what was pruned.
|
|
29
30
|
- If a submitted oracle job later fails because upload is rejected, retry with a smaller archive in this order: (1) remove the largest obviously irrelevant/generated content, (2) if still too large, include modified files plus adjacent files plus directly relevant subtrees, (3) if still too large, explain the cut or ask the user.
|
|
30
|
-
- Prefer the configured default (omit **`preset`**) unless the task clearly needs a different model; then choose a **`preset`** id
|
|
31
|
+
- Prefer the configured default (omit **`preset`**) unless the task clearly needs a different model; then choose a canonical **`preset`** id.
|
|
31
32
|
- If `oracle_submit` itself fails because the local archive still exceeds the upload limit after default exclusions and automatic generic generated-output-dir pruning, or for any other submit-time error, stop and report the error. Do not retry automatically.
|
|
32
33
|
- If `oracle_submit` returns a queued job instead of an immediately dispatched one, treat that as success and end your turn exactly the same way.
|
|
33
34
|
- After oracle_submit returns, end your turn. Do not keep working while the oracle runs.
|