pi-oracle 0.7.9 → 0.7.11

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 CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.11 - 2026-06-15
6
+
7
+ ### Changed
8
+ - updated the local pi development and validation baseline to `@earendil-works/*` `0.79.4`
9
+ - refreshed oracle docs and sanity-check baselines for pi `0.79.4`
10
+
11
+ ### Validation
12
+ - ran the full `npm run verify:oracle` release gate under pi `0.79.4`
13
+
14
+ ## 0.7.10 - 2026-06-13
15
+
16
+ ### Added
17
+ - added explicit existing ChatGPT browser-thread targeting for `/oracle`, `oracle_preflight`, and `oracle_submit` through optional `chatGptConversationId`, accepting raw ChatGPT conversation ids or full `https://chatgpt.com/c/...` / `https://chat.openai.com/c/...` URLs while preserving fresh-thread defaults when omitted
18
+
19
+ ### Validation
20
+ - verified existing-thread preflight and submit flows in isolated `pi` sessions, including persisted `chatUrl` / `conversationId` job metadata with a fake worker
21
+ - ran the full `npm run verify:oracle` release gate
22
+
5
23
  ## 0.7.9 - 2026-06-11
6
24
 
7
25
  ### Fixed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `pi-oracle` lets a `pi` agent send hard, long-running work to ChatGPT.com or Grok through the web app, with repo archives, background execution, saved results, and a best-effort wake-up back into `pi` when the answer is ready.
4
4
 
5
- > Status: experimental public beta. Validated on macOS, Linux, and Windows native with Chromium-family browsers and pi `0.79.1`. Pi `0.79.1+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
5
+ > Status: experimental public beta. Validated on macOS, Linux, and Windows native with Chromium-family browsers and pi `0.79.4`. Pi `0.79.4+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
6
6
 
7
7
  ## What a successful run looks like
8
8
 
@@ -77,7 +77,7 @@ You need:
77
77
 
78
78
  - macOS, Linux, or Windows native
79
79
  - Node.js 22 or newer
80
- - Suggested tested floor: `pi` 0.79.1 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
80
+ - Suggested tested floor: `pi` 0.79.4 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
81
81
  - Google Chrome/Chromium or another Chromium-family browser
82
82
  - ChatGPT or Grok already signed in to the configured local browser profile for the provider you plan to use
83
83
  - `agent-browser`, `tar`, and `zstd` available on the machine
@@ -134,6 +134,10 @@ If the wake-up does not arrive, run:
134
134
  /oracle-followup <job-id> Tighten the migration plan around rollback risk, and include the most relevant surrounding files/docs as long as the archive stays comfortably within the 250 MiB limit.
135
135
  ```
136
136
 
137
+ ```text
138
+ /oracle Continue existing ChatGPT conversation 6a28ab5c-e4d4-83e8-b8be-dd39f38a26d6. Review the current auth code and include enough surrounding context to propose concrete fixes.
139
+ ```
140
+
137
141
  ## How it works
138
142
 
139
143
  ```mermaid
@@ -153,13 +157,14 @@ Key design choices:
153
157
  - **Tools own execution.** `oracle_submit` builds the archive, admits or queues the job, starts the worker, and returns immediately.
154
158
  - **Auth uses a seed profile.** `/oracle-auth` imports cookies into an isolated seed profile; each job clones that seed into its own temporary runtime profile.
155
159
  - **Follow-ups preserve provider thread state.** `/oracle-followup <job-id> ...` resolves the prior job's saved provider URL and submits the next prompt with `followUpJobId`.
160
+ - **Existing ChatGPT browser threads are opt-in.** Normal `/oracle` jobs still start a fresh provider thread. When the user explicitly provides a ChatGPT conversation id or `https://chatgpt.com/c/...` URL, the agent passes `chatGptConversationId` so `oracle_submit` opens that existing thread in the isolated runtime.
156
161
  - **Wake-up is best effort, storage is durable.** A missed wake-up does not lose the result.
157
162
 
158
163
  ## Commands and tools
159
164
 
160
165
  User-facing commands:
161
166
 
162
- - `/oracle <request>` — prepare context and dispatch a ChatGPT or Grok web oracle job
167
+ - `/oracle <request>` — prepare context and dispatch a ChatGPT or Grok web oracle job. If the request explicitly includes an existing ChatGPT conversation id/URL, the agent can continue that browser-created thread; otherwise `/oracle` starts a fresh thread as before.
163
168
  - `/oracle-followup <job-id> <request>` — continue an earlier oracle job in the same provider thread
164
169
  - `/oracle-auth [chatgpt|grok]` — sync provider cookies into the isolated oracle auth seed profile
165
170
  - `/oracle-read [job-id]` — inspect job status and saved response preview
@@ -171,7 +176,7 @@ Agent-facing tools:
171
176
 
172
177
  - `oracle_preflight`
173
178
  - `oracle_auth`
174
- - `oracle_submit`
179
+ - `oracle_submit` (`chatGptConversationId` is optional and only for explicitly continuing an existing ChatGPT browser conversation id/URL; omit it for the default fresh thread)
175
180
  - `oracle_read`
176
181
  - `oracle_cancel`
177
182
 
@@ -179,7 +184,7 @@ Agent-facing tools:
179
184
 
180
185
  Most users can start with defaults. Set an agent-level config only when you need a non-default provider, mode, preset, or browser profile.
181
186
 
182
- Pi 0.79.1 gates project-local inputs behind project trust. `pi-oracle` preserves its historical risk-on extension behavior for existing users: project-local `.pi/extensions/oracle.json` safe overrides still load by default for compatibility. They are ignored when you explicitly opt out of project-local inputs with `--no-approve` or save a “do not trust” decision for the project. Privileged browser/auth settings still come only from the agent-level config.
187
+ Pi 0.79.4 gates project-local inputs behind project trust. `pi-oracle` preserves its historical risk-on extension behavior for existing users: project-local `.pi/extensions/oracle.json` safe overrides still load by default for compatibility. They are ignored when you explicitly opt out of project-local inputs with `--no-approve` or save a “do not trust” decision for the project. Privileged browser/auth settings still come only from the agent-level config.
183
188
 
184
189
  `~/.pi/agent/extensions/oracle.json`
185
190
 
@@ -394,7 +399,7 @@ For macOS, Ubuntu, and Windows native package/build plus packed runtime validati
394
399
  npm run release:check
395
400
  ```
396
401
 
397
- The real runtime suite defaults to deterministic installed-tool execution so platform proof stays bounded. Provider/model defaults remain `zai/glm-5.1` for doctor/config and for optional model-agent debugging; override with `PI_ORACLE_REAL_TEST_PROVIDER` and `PI_ORACLE_REAL_TEST_MODEL` when needed. For inner-loop source loading only, use `npm run smoke:real:source`; it is not release proof. Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` only when debugging the slower model-agent path. The optional second real-agent negative symlink check is opt-in via `PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK=1`; `npm run sanity:oracle` covers archive/symlink rejection by default without adding another model-agent turn to the platform release gate.
402
+ The real runtime suite defaults to deterministic installed-tool execution so platform proof stays bounded. Provider/model defaults remain `zai/glm-5.2` for doctor/config and for optional model-agent debugging; override with `PI_ORACLE_REAL_TEST_PROVIDER` and `PI_ORACLE_REAL_TEST_MODEL` when needed. For inner-loop source loading only, use `npm run smoke:real:source`; it is not release proof. Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` only when debugging the slower model-agent path. The optional second real-agent negative symlink check is opt-in via `PI_ORACLE_REAL_TEST_NEGATIVE_SYMLINK=1`; `npm run sanity:oracle` covers archive/symlink rejection by default without adding another model-agent turn to the platform release gate.
398
403
 
399
404
  For manual end-to-end local-extension smoke testing, use [`docs/ORACLE_ISOLATED_PI_VALIDATION.md`](docs/ORACLE_ISOLATED_PI_VALIDATION.md). That workflow launches isolated `pi` coding-agent sessions against this checkout and uses `instant` or `thinking_light`, as required by the project validation policy.
400
405
 
@@ -7,7 +7,7 @@ Companion doc:
7
7
  - `docs/ORACLE_RECOVERY_DRILL.md` — safe expired-auth recovery validation drill
8
8
 
9
9
  Compatibility target:
10
- - `pi` 0.79.1+ is the suggested tested floor for current project-trust-aware package/runtime validation
10
+ - `pi` 0.79.4+ is the suggested tested floor for current project-trust-aware package/runtime validation
11
11
  - package metadata keeps pi runtime packages as optional wildcard peers, so this suggested floor is not enforced as a hard npm install requirement
12
12
  - current extension lifecycle only; no backward-compatibility shims for removed `session_switch` / `session_fork` events
13
13
 
@@ -157,7 +157,10 @@ The authenticated seed profile remains the source of truth for future oracle run
157
157
  Agent-facing submissions resolve a provider first. ChatGPT submissions use **`preset`**; the canonical registry is `ORACLE_SUBMIT_PRESETS` in `extensions/oracle/lib/config.ts`. Grok submissions use **`mode: "heavy"`** today and reject ChatGPT-only presets. For ChatGPT, **`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. Prompt-template guidance biases toward omitting provider/model fields and using configured defaults unless the task or user explicitly asks for one. It also biases toward context-rich archives up to the provider ceiling, narrowing only when the user explicitly asks for a tight archive, privacy/sensitivity requires it, or size pressure forces it. When local archive creation still exceeds that ceiling after default exclusions and whole-repo auto-pruning, prompt guidance now treats the failure as a retryable archive-selection miss rather than a terminal dead end: agents should cut scope automatically, retry once or twice, and only surface the cut decisions if the archive still cannot fit.
158
158
 
159
159
  1. resolve the provider and preset/mode (submit-time or config default) into an execution snapshot
160
- 2. resolve optional `followUpJobId` into a prior `chatUrl` and `conversationId`
160
+ 2. resolve optional thread targeting:
161
+ - `followUpJobId` into a prior oracle job `chatUrl` and `conversationId`, or
162
+ - `chatGptConversationId` into a user/browser-created ChatGPT `https://chatgpt.com/c/<id>` URL
163
+ Omit both for the default fresh-thread behavior.
161
164
  3. build the archive first into a temporary path
162
165
  4. allocate a unique runtime:
163
166
  - `runtimeId`
@@ -166,7 +169,7 @@ Agent-facing submissions resolve a provider first. ChatGPT submissions use **`pr
166
169
  5. under the global admission lock, first promote any older queued jobs that can now run
167
170
  6. if runtime capacity is still available:
168
171
  - acquire the runtime lease
169
- - acquire the conversation lease for follow-up jobs
172
+ - acquire the conversation lease for same-thread jobs, including follow-ups and explicit existing ChatGPT conversation ids
170
173
  - create `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/...` job state as `submitted`
171
174
  7. otherwise create `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/...` job state as `queued`
172
175
  8. move the prepared archive into the job directory with a unique filename
@@ -184,7 +187,8 @@ Per job:
184
187
  - the job’s `runtimeProfileDir`
185
188
  - headless by default
186
189
  3. open either:
187
- - the saved `chatUrl` for follow-up jobs, or
190
+ - the saved `chatUrl` for follow-up jobs,
191
+ - the normalized `chatGptConversationId` URL for explicit existing ChatGPT browser threads, or
188
192
  - the configured provider URL
189
193
  4. classify page state before touching the UI
190
194
  5. fail fast on:
@@ -230,7 +234,7 @@ Merged config locations:
230
234
  - global: `~/.pi/agent/extensions/oracle.json`
231
235
  - project: `.pi/extensions/oracle.json`
232
236
 
233
- Project config remains restricted to safe overrides only. On Pi 0.79.1+, pi itself gates project-local inputs behind project trust, but `pi-oracle` keeps its historical risk-on extension behavior for this package-specific safe override file: `.pi/extensions/oracle.json` loads by default for compatibility, and is ignored when Pi reports the project is untrusted, including `--no-approve` or saved “do not trust” decisions. This preserves the existing extension experience while still honoring explicit opt-out/distrust decisions. Browser/auth settings remain global-only because they control local privileged browser state.
237
+ Project config remains restricted to safe overrides only. On Pi 0.79.4+, pi itself gates project-local inputs behind project trust, but `pi-oracle` keeps its historical risk-on extension behavior for this package-specific safe override file: `.pi/extensions/oracle.json` loads by default for compatibility, and is ignored when Pi reports the project is untrusted, including `--no-approve` or saved “do not trust” decisions. This preserves the existing extension experience while still honoring explicit opt-out/distrust decisions. Browser/auth settings remain global-only because they control local privileged browser state.
234
238
 
235
239
  ### Current config shape
236
240
 
@@ -495,11 +499,13 @@ Same-thread continuity is persisted as data, not runtime browser state.
495
499
 
496
500
  Approach:
497
501
 
498
- - expose `/oracle-followup <job-id> <request>` as the user-facing way to continue the same provider thread later
502
+ - expose `/oracle-followup <job-id> <request>` as the user-facing way to continue an oracle-created provider thread later
503
+ - allow `/oracle`/`oracle_submit` to opt into a browser-created ChatGPT thread only when the user explicitly supplies `chatGptConversationId` as a raw id or `https://chatgpt.com/c/...` URL
499
504
  - store `chatUrl` only after the conversation URL stabilizes
500
505
  - derive and persist `conversationId` from that URL when possible
501
506
  - for a follow-up job, resolve `followUpJobId` to the prior `chatUrl`
502
- - acquire a conversation lease before launching the follow-up
507
+ - for an explicit existing ChatGPT thread, normalize `chatGptConversationId` to `https://chatgpt.com/c/<id>` without requiring prior oracle job state
508
+ - acquire a conversation lease before launching the same-thread job
503
509
  - launch a fresh isolated browser using a fresh runtime clone of the auth seed
504
510
  - open that URL
505
511
  - continue there if authentication and page-state checks pass
@@ -27,7 +27,7 @@ The extension is loaded from the local checkout with:
27
27
  pi --approve --no-extensions -e "$REPO/extensions/oracle/index.ts"
28
28
  ```
29
29
 
30
- That ensures the session is exercising the in-repo code, not a globally installed package. `--approve` is intentional for this isolated workflow on Pi 0.79.1+: the test fixture is this trusted checkout, and non-interactive/scripted validation must not block on the project-trust prompt.
30
+ That ensures the session is exercising the in-repo code, not a globally installed package. `--approve` is intentional for this isolated workflow on Pi 0.79.4+: the test fixture is this trusted checkout, and non-interactive/scripted validation must not block on the project-trust prompt.
31
31
 
32
32
  The local extension now intercepts TUI `/oracle` and `/oracle-followup` before prompt-template expansion, re-injects the compact slash request as the visible user message for prompt-history/up-arrow recall, and reads the in-repo prompt files as hidden dispatch instructions, so do not pass `--prompt-template` for normal local-extension validation. In print/json/rpc modes, the extension contributes the prompt templates itself.
33
33
 
@@ -36,7 +36,7 @@ Target setup:
36
36
  Real runtime suite auth:
37
37
 
38
38
  - Default deterministic installed-tool smoke does not require provider API keys.
39
- - Provider/model defaults remain `zai/glm-5.1` for optional model-agent debugging.
39
+ - Provider/model defaults remain `zai/glm-5.2` for optional model-agent debugging.
40
40
  - Set `PI_ORACLE_REAL_TEST_MODEL_AGENT=1` to run the slower model-agent path; then the provider auth env is required (`ZAI_API_KEY` by default, reported only as present/redacted).
41
41
  - Override with `PI_ORACLE_REAL_TEST_PROVIDER` and `PI_ORACLE_REAL_TEST_MODEL`; auth variable names live in `platform-smoke.config.mjs`.
42
42
 
@@ -90,7 +90,7 @@ On each required target, `platform-build`:
90
90
  5. runs `npm pack`;
91
91
  6. creates a fresh target-local pi project;
92
92
  7. runs `npm install --no-save <packed tarball>`;
93
- 8. runs `pi install -l ./node_modules/pi-oracle --approve` so Pi 0.79.1 project-trust gating intentionally trusts the temporary fixture;
93
+ 8. runs `pi install -l ./node_modules/pi-oracle --approve` so Pi 0.79.4 project-trust gating intentionally trusts the temporary fixture;
94
94
  9. runs `pi list --approve`;
95
95
  10. asserts the installed package came from `node_modules/pi-oracle` and did not use `pi -e` / source-extension shortcuts.
96
96
 
@@ -6,7 +6,7 @@
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
- import { getAgentDir, hasProjectTrustInputs, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
9
+ import { getAgentDir, hasTrustRequiringProjectResources, ProjectTrustStore } from "@earendil-works/pi-coding-agent";
10
10
  import { isAbsolute, join, normalize } from "node:path";
11
11
  import {
12
12
  assertNotKnownBrowserUserDataPath,
@@ -351,7 +351,7 @@ function isProjectConfigTrusted(cwd: string, agentDir: string, projectConfigExis
351
351
  const trustCwd = options?.projectConfigTrustCwd ?? cwd;
352
352
  const cliOverride = getProjectTrustCliOverride();
353
353
  if (cliOverride !== undefined) return cliOverride;
354
- if (!projectConfigExists && !hasProjectTrustInputs(trustCwd)) return true;
354
+ if (!projectConfigExists && !hasTrustRequiringProjectResources(trustCwd)) return true;
355
355
  try {
356
356
  const trustStore = new ProjectTrustStore(agentDir);
357
357
  const trustDecision = trustStore.get(trustCwd);
@@ -25,6 +25,7 @@ import {
25
25
  resolveOracleConfigForProvider,
26
26
  resolveOracleGrokMode,
27
27
  resolveOracleSubmitPreset,
28
+ type OracleConfig,
28
29
  type OracleProvider,
29
30
  } from "./config.js";
30
31
  import {
@@ -94,11 +95,21 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
94
95
  }),
95
96
  ),
96
97
  followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose chat thread should be continued." })),
98
+ chatGptConversationId: Type.Optional(Type.String({
99
+ description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, to continue. Omit for default behavior: starting a fresh oracle thread. Do not combine with followUpJobId.",
100
+ minLength: 1,
101
+ pattern: "^.*\\S.*$",
102
+ })),
97
103
  });
98
104
 
99
105
  const ORACLE_PREFLIGHT_PARAMS = Type.Object({
100
106
  provider: Type.Optional(Type.String({ description: `Provider readiness to check. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}.` })),
101
107
  followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose provider/thread readiness should be checked." })),
108
+ chatGptConversationId: Type.Optional(Type.String({
109
+ description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, whose provider/thread readiness should be checked. Do not combine with followUpJobId.",
110
+ minLength: 1,
111
+ pattern: "^.*\\S.*$",
112
+ })),
102
113
  });
103
114
 
104
115
  const ORACLE_AUTH_PARAMS = Type.Object({
@@ -750,12 +761,59 @@ export function getQueueAdmissionFailure(args: {
750
761
  return undefined;
751
762
  }
752
763
 
753
- function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
764
+ type OracleConversationTarget = {
754
765
  followUpToJobId?: string;
755
766
  chatUrl?: string;
756
767
  conversationId?: string;
757
768
  provider?: "chatgpt" | "grok";
758
- } {
769
+ label?: string;
770
+ };
771
+
772
+ const CHATGPT_CONVERSATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{7,}$/;
773
+ const CHATGPT_CONVERSATION_URL_HOSTS = new Set(["chatgpt.com", "chat.openai.com"]);
774
+
775
+ function chatGptConversationOrigin(config: Pick<OracleConfig, "browser">): string {
776
+ try {
777
+ const parsed = new URL(config.browser.chatUrl);
778
+ if (CHATGPT_CONVERSATION_URL_HOSTS.has(parsed.hostname.toLowerCase())) return parsed.origin;
779
+ } catch {
780
+ // Fall through to the canonical ChatGPT origin.
781
+ }
782
+ return "https://chatgpt.com";
783
+ }
784
+
785
+ export function resolveChatGptConversationReference(
786
+ rawReference: string | undefined,
787
+ config: Pick<OracleConfig, "browser">,
788
+ ): { chatUrl: string; conversationId: string } | undefined {
789
+ if (rawReference === undefined) return undefined;
790
+ const reference = rawReference.trim();
791
+ if (!reference) throw new Error("ChatGPT conversation id must be a non-empty string");
792
+
793
+ try {
794
+ const parsed = new URL(reference);
795
+ const host = parsed.hostname.toLowerCase();
796
+ const conversationId = parseConversationId(parsed.toString());
797
+ if (parsed.protocol !== "https:" || !CHATGPT_CONVERSATION_URL_HOSTS.has(host) || !conversationId) {
798
+ throw new Error();
799
+ }
800
+ return {
801
+ chatUrl: `${parsed.origin}/c/${conversationId}`,
802
+ conversationId,
803
+ };
804
+ } catch {
805
+ if (!CHATGPT_CONVERSATION_ID_PATTERN.test(reference)) {
806
+ throw new Error(`Invalid ChatGPT conversation id or URL: ${rawReference}`);
807
+ }
808
+ const origin = chatGptConversationOrigin(config);
809
+ return {
810
+ chatUrl: `${origin}/c/${reference}`,
811
+ conversationId: reference,
812
+ };
813
+ }
814
+ }
815
+
816
+ function resolveFollowUp(previousJobId: string | undefined, cwd: string): OracleConversationTarget {
759
817
  if (!previousJobId) return {};
760
818
  const previous = readJob(previousJobId);
761
819
  if (!previous) {
@@ -770,14 +828,37 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
770
828
  if (!previous.chatUrl) {
771
829
  throw new Error(`Follow-up oracle job ${previousJobId} has no persisted chat URL`);
772
830
  }
831
+ const conversationId = previous.conversationId || parseConversationId(previous.chatUrl);
773
832
  return {
774
833
  followUpToJobId: previous.id,
775
834
  chatUrl: previous.chatUrl,
776
- conversationId: previous.conversationId || parseConversationId(previous.chatUrl),
835
+ conversationId,
777
836
  provider: previous.selection?.provider === "grok" ? "grok" : "chatgpt",
837
+ label: `follow-up job ${previous.id}`,
778
838
  };
779
839
  }
780
840
 
841
+ function resolveConversationTarget(args: {
842
+ followUpJobId?: string;
843
+ chatGptConversationId?: string;
844
+ cwd: string;
845
+ config: OracleConfig;
846
+ }): OracleConversationTarget {
847
+ if (args.followUpJobId !== undefined && args.chatGptConversationId !== undefined) {
848
+ throw new Error("Pass either followUpJobId or chatGptConversationId, not both");
849
+ }
850
+ if (args.chatGptConversationId !== undefined) {
851
+ const target = resolveChatGptConversationReference(args.chatGptConversationId, args.config);
852
+ if (!target) return {};
853
+ return {
854
+ ...target,
855
+ provider: "chatgpt",
856
+ label: `ChatGPT conversation ${target.conversationId}`,
857
+ };
858
+ }
859
+ return resolveFollowUp(args.followUpJobId, args.cwd);
860
+ }
861
+
781
862
  type OracleToolName = "oracle_auth" | "oracle_submit" | "oracle_read" | "oracle_cancel";
782
863
  type OracleToolErrorSource = OracleToolName | "oracle_preflight";
783
864
  type OracleQueueSnapshot = { queued: boolean; position?: number; depth?: number };
@@ -1036,6 +1117,23 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
1036
1117
  };
1037
1118
  }
1038
1119
 
1120
+ if ((toolName === "oracle_submit" || toolName === "oracle_preflight") && message === "Pass either followUpJobId or chatGptConversationId, not both") {
1121
+ return {
1122
+ code: "oracle_thread_target_conflict",
1123
+ message,
1124
+ suggestedNextStep: "Retry with exactly one same-thread target: followUpJobId for an oracle-created thread, or chatGptConversationId for an existing ChatGPT browser thread.",
1125
+ };
1126
+ }
1127
+
1128
+ if ((toolName === "oracle_submit" || toolName === "oracle_preflight") && (message === "ChatGPT conversation id must be a non-empty string" || message.startsWith("Invalid ChatGPT conversation id or URL: "))) {
1129
+ return {
1130
+ code: "invalid_chatgpt_conversation_id",
1131
+ message,
1132
+ rejectedValue: typeof params.chatGptConversationId === "string" ? params.chatGptConversationId : undefined,
1133
+ suggestedNextStep: "Retry with a ChatGPT conversation id like 6a28ab5c-e4d4-83e8-b8be-dd39f38a26d6 or a full https://chatgpt.com/c/... URL, or omit chatGptConversationId to start a fresh thread.",
1134
+ };
1135
+ }
1136
+
1039
1137
  if (toolName === "oracle_submit" && message.startsWith("Follow-up oracle job not found: ")) {
1040
1138
  return {
1041
1139
  code: "follow_up_job_not_found",
@@ -1164,7 +1262,7 @@ function isProjectTrusted(ctx: ExtensionContext): boolean {
1164
1262
  return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
1165
1263
  }
1166
1264
 
1167
- async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: unknown; followUpJobId?: unknown } = {}): Promise<OraclePreflightDetails> {
1265
+ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: unknown; followUpJobId?: unknown; chatGptConversationId?: unknown } = {}): Promise<OraclePreflightDetails> {
1168
1266
  const sessionFile = getSessionFile(ctx);
1169
1267
  if (!hasPersistedSessionFile(sessionFile)) {
1170
1268
  return {
@@ -1184,14 +1282,18 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
1184
1282
  let provider: OracleProvider | undefined;
1185
1283
  try {
1186
1284
  const followUpJobId = params.followUpJobId;
1285
+ const chatGptConversationId = params.chatGptConversationId;
1187
1286
  if (followUpJobId !== undefined && typeof followUpJobId !== "string") {
1188
1287
  throw new Error("oracle_preflight followUpJobId must be a string");
1189
1288
  }
1289
+ if (chatGptConversationId !== undefined && typeof chatGptConversationId !== "string") {
1290
+ throw new Error("oracle_preflight chatGptConversationId must be a string");
1291
+ }
1190
1292
  const baseConfig = loadOracleConfig(ctx.cwd, { projectConfigTrusted: isProjectTrusted(ctx) });
1191
- const followUp = resolveFollowUp(followUpJobId, ctx.cwd);
1192
- provider = normalizeOracleProvider(params.provider, followUp.provider ?? baseConfig.defaults.provider, "oracle_preflight");
1193
- if (followUp.provider && provider !== followUp.provider) {
1194
- throw new Error(`Follow-up job ${followUpJobId} uses provider ${followUp.provider}; cannot check it with ${provider}.`);
1293
+ const target = resolveConversationTarget({ followUpJobId, chatGptConversationId, cwd: ctx.cwd, config: baseConfig });
1294
+ provider = normalizeOracleProvider(params.provider, target.provider ?? baseConfig.defaults.provider, "oracle_preflight");
1295
+ if (target.provider && provider !== target.provider) {
1296
+ throw new Error(`${target.label ?? "Oracle conversation"} requires provider ${target.provider}; cannot check it with ${provider}.`);
1195
1297
  }
1196
1298
  config = resolveOracleConfigForProvider(baseConfig, provider);
1197
1299
  } catch (error) {
@@ -1201,14 +1303,14 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
1201
1303
  session: { persisted: true, sessionFile },
1202
1304
  config: { ready: false },
1203
1305
  auth: { ready: false },
1204
- error: buildOracleToolErrorDetails("oracle_preflight", error, {}),
1306
+ error: buildOracleToolErrorDetails("oracle_preflight", error, asRecord(params) ?? {}),
1205
1307
  };
1206
1308
  }
1207
1309
 
1208
1310
  try {
1209
1311
  await assertOracleSubmitPrerequisites(config);
1210
1312
  } catch (error) {
1211
- const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, {});
1313
+ const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, asRecord(params) ?? {});
1212
1314
  return {
1213
1315
  ready: false,
1214
1316
  provider,
@@ -1251,7 +1353,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1251
1353
  description: "Check whether oracle is ready in this session before spending time gathering context or preparing a submission.",
1252
1354
  promptSnippet: "Check oracle readiness before expensive /oracle preparation.",
1253
1355
  promptGuidelines: [
1254
- "Call oracle_preflight before doing expensive /oracle preparation. Pass provider='grok' when the user explicitly asks for Grok, or followUpJobId for same-thread follow-ups. If ready is false, stop immediately and report the suggested next step instead of reading files or crafting archive inputs.",
1356
+ "Call oracle_preflight before doing expensive /oracle preparation. Pass provider='grok' when the user explicitly asks for Grok, followUpJobId for same-thread follow-ups from oracle-created jobs, or chatGptConversationId when the user explicitly provides an existing ChatGPT browser conversation id/URL. If ready is false, stop immediately and report the suggested next step instead of reading files or crafting archive inputs.",
1255
1357
  ],
1256
1358
  parameters: ORACLE_PREFLIGHT_PARAMS,
1257
1359
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -1299,10 +1401,12 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1299
1401
  label: "Oracle Submit",
1300
1402
  description:
1301
1403
  "Dispatch a background ChatGPT or Grok web oracle job after gathering context. Always pass a prompt and exact project-relative archive inputs. " +
1302
- "Optional provider: set `provider` to `grok` when the user asks for Grok; Grok currently supports only Heavy. 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.",
1404
+ "Optional provider: set `provider` to `grok` when the user asks for Grok; Grok currently supports only Heavy. 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. " +
1405
+ "Optional thread target: pass `chatGptConversationId` only when the user explicitly provides an existing ChatGPT browser conversation id/URL to continue; omit it for the default fresh oracle thread.",
1303
1406
  promptSnippet: "Dispatch a background ChatGPT or Grok web oracle job after gathering repo context.",
1304
1407
  promptGuidelines: [
1305
1408
  "Gather context before calling oracle_submit.",
1409
+ "If the user explicitly provides an existing ChatGPT browser conversation id or https://chatgpt.com/c/... URL, pass it as chatGptConversationId and force provider='chatgpt'; otherwise omit chatGptConversationId so oracle_submit starts a fresh thread by default.",
1306
1410
  "If the immediately preceding oracle run failed because ChatGPT or Grok login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission; pass provider='grok' for Grok retries. Do not loop auth refreshes.",
1307
1411
  "Prefer context-rich archives up to the provider ceiling because more relevant surrounding context is usually better than less: 250 MB for ChatGPT and 200 MiB for Grok.",
1308
1412
  "By default, archive the whole repo by passing '.' for broad or unclear requests; default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like .env files, key material, credential dotfiles, local database files, and nested secrets directories anywhere in the repo.",
@@ -1328,10 +1432,15 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1328
1432
  const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
1329
1433
  const projectId = getProjectId(projectCwd);
1330
1434
  const sessionId = getSessionId(originSessionFile, projectId);
1331
- const followUp = resolveFollowUp(params.followUpJobId, projectCwd);
1332
- const provider = normalizeOracleProvider(params.provider, followUp.provider ?? baseConfig.defaults.provider, "oracle_submit");
1333
- if (followUp.provider && provider !== followUp.provider) {
1334
- throw new Error(`Follow-up job ${params.followUpJobId} uses provider ${followUp.provider}; cannot continue it with ${provider}.`);
1435
+ const target = resolveConversationTarget({
1436
+ followUpJobId: params.followUpJobId,
1437
+ chatGptConversationId: params.chatGptConversationId,
1438
+ cwd: projectCwd,
1439
+ config: baseConfig,
1440
+ });
1441
+ const provider = normalizeOracleProvider(params.provider, target.provider ?? baseConfig.defaults.provider, "oracle_submit");
1442
+ if (target.provider && provider !== target.provider) {
1443
+ throw new Error(`${target.label ?? "Oracle conversation"} requires provider ${target.provider}; cannot continue it with ${provider}.`);
1335
1444
  }
1336
1445
  if (provider === "grok" && typeof params.preset === "string") {
1337
1446
  throw new Error("oracle_submit preset is only valid for ChatGPT. For Grok, use provider='grok' and mode='heavy'.");
@@ -1340,7 +1449,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1340
1449
  ? resolveOracleGrokMode(normalizeGrokMode(params.mode, baseConfig.defaults.grokMode))
1341
1450
  : resolveOracleSubmitPreset(typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : baseConfig.defaults.preset);
1342
1451
  const config = resolveOracleConfigForProvider(baseConfig, provider);
1343
- const targetChatUrl = followUp.chatUrl;
1452
+ const targetChatUrl = target.chatUrl;
1344
1453
  // Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
1345
1454
  resolveArchiveInputs(projectCwd, params.files);
1346
1455
  await assertOracleSubmitPrerequisites(config);
@@ -1405,7 +1514,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1405
1514
  prompt: params.prompt,
1406
1515
  files: params.files,
1407
1516
  selection,
1408
- followUpToJobId: followUp.followUpToJobId,
1517
+ followUpToJobId: target.followUpToJobId,
1409
1518
  chatUrl: targetChatUrl,
1410
1519
  requestSource: "tool",
1411
1520
  },
@@ -1425,18 +1534,18 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1425
1534
  }
1426
1535
 
1427
1536
  runtimeLeaseAcquired = true;
1428
- if (followUp.conversationId) {
1537
+ if (target.conversationId) {
1429
1538
  const conversationAttempt = await tryAcquireConversationLease({
1430
1539
  jobId,
1431
- conversationId: followUp.conversationId,
1540
+ conversationId: target.conversationId,
1432
1541
  projectId,
1433
1542
  sessionId,
1434
1543
  createdAt: admittedAt,
1435
1544
  });
1436
1545
  if (!conversationAttempt.acquired) {
1437
1546
  throw new Error(
1438
- `Oracle conversation ${followUp.conversationId} is already in use by job ${conversationAttempt.blocker?.jobId ?? "unknown"}. ` +
1439
- "Concurrent follow-ups to the same ChatGPT thread are not allowed.",
1547
+ `Oracle conversation ${target.conversationId} is already in use by job ${conversationAttempt.blocker?.jobId ?? "unknown"}. ` +
1548
+ "Concurrent jobs targeting the same ChatGPT thread are not allowed.",
1440
1549
  );
1441
1550
  }
1442
1551
  conversationLeaseAcquired = true;
@@ -1448,7 +1557,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1448
1557
  prompt: params.prompt,
1449
1558
  files: params.files,
1450
1559
  selection,
1451
- followUpToJobId: followUp.followUpToJobId,
1560
+ followUpToJobId: target.followUpToJobId,
1452
1561
  chatUrl: targetChatUrl,
1453
1562
  requestSource: "tool",
1454
1563
  },
@@ -1563,7 +1672,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1563
1672
  runtimeId: runtimeLeaseAcquired ? runtime.runtimeId : undefined,
1564
1673
  runtimeProfileDir: runtimeLeaseAcquired ? runtime.runtimeProfileDir : undefined,
1565
1674
  runtimeSessionName: workerSpawned ? runtime.runtimeSessionName : undefined,
1566
- conversationId: conversationLeaseAcquired ? followUp.conversationId : undefined,
1675
+ conversationId: conversationLeaseAcquired ? target.conversationId : undefined,
1567
1676
  }).catch(() => ({ attempted: [], warnings: [] }));
1568
1677
  if (job && cleanupReport.warnings.length > 0) {
1569
1678
  await appendCleanupWarnings(job.id, cleanupReport.warnings).catch(() => undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -80,8 +80,8 @@
80
80
  "protobufjs": "7.6.1"
81
81
  },
82
82
  "devDependencies": {
83
- "@earendil-works/pi-ai": "0.79.1",
84
- "@earendil-works/pi-coding-agent": "0.79.1",
83
+ "@earendil-works/pi-ai": "0.79.4",
84
+ "@earendil-works/pi-coding-agent": "0.79.4",
85
85
  "@types/node": "^22.19.19",
86
86
  "esbuild": "^0.28.0",
87
87
  "tsx": "^4.22.3",
@@ -40,7 +40,7 @@ export default {
40
40
  nodeValidationMajor: 24,
41
41
  realSmoke: {
42
42
  defaultProvider: "zai",
43
- defaultModel: "glm-5.1",
43
+ defaultModel: "glm-5.2",
44
44
  authEnvByProvider: {
45
45
  zai: ["ZAI_API_KEY"],
46
46
  openai: ["OPENAI_API_KEY"],
package/prompts/oracle.md CHANGED
@@ -13,7 +13,7 @@ Hard requirements:
13
13
  - After a successful or queued `oracle_submit`, your final answer must be only a terse dispatch summary with the job id and response path. Do not ask questions, offer to watch/poll/read, list next steps, or continue working.
14
14
 
15
15
  Required workflow:
16
- 1. Call `oracle_preflight` immediately. If the user says to use Grok, pass `provider: "grok"` to `oracle_preflight`. If the user says to use ChatGPT, pass `provider: "chatgpt"`. If the user says to use ChatGPT Instant, pass `provider: "chatgpt"` and later call `oracle_submit` with `preset: "instant"`.
16
+ 1. Call `oracle_preflight` immediately. If the user says to use Grok, pass `provider: "grok"` to `oracle_preflight`. If the user says to use ChatGPT, pass `provider: "chatgpt"`. If the user says to use ChatGPT Instant, pass `provider: "chatgpt"` and later call `oracle_submit` with `preset: "instant"`. If the user explicitly provides an existing ChatGPT conversation id or `https://chatgpt.com/c/...` URL, pass it as `chatGptConversationId` to `oracle_preflight` and force `provider: "chatgpt"`. Omit `chatGptConversationId` unless the user explicitly asks to continue an existing browser-created ChatGPT thread.
17
17
  2. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, prepare archive inputs, or call `oracle_auth` automatically. Report the blocking issue plus the suggested next step.
18
18
  3. Understand the request and decide whether it is explicitly narrow or genuinely broad.
19
19
  4. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250 MB for ChatGPT, 200 MiB for Grok.
@@ -21,12 +21,13 @@ Required workflow:
21
21
  6. If the request is broad, architectural, release-oriented, or otherwise repo-wide, gather broader context and usually archive `.`.
22
22
  7. Choose archive inputs for the oracle job.
23
23
  8. Craft a concise but complete oracle prompt for the selected web provider.
24
- 9. Call `oracle_submit` with the prompt and exact archive inputs. Do not ask for confirmation before this submit step unless `oracle_preflight` or `oracle_submit` returns a blocker that requires user action.
24
+ 9. Call `oracle_submit` with the prompt and exact archive inputs. Include `chatGptConversationId` only when the user explicitly provided an existing ChatGPT conversation id/URL to continue; otherwise omit it so the default remains a fresh oracle thread. Do not ask for confirmation before this submit step unless `oracle_preflight` or `oracle_submit` returns a blocker that requires user action.
25
25
  10. Stop immediately after dispatching the oracle job. “Stop” means no follow-up questions, no offers to poll/watch/read, and no extra next-step list.
26
26
 
27
27
  Oracle provider/model (`oracle_submit`):
28
28
  - If the user says to use Grok (for example “Use the oracle to Grok about ...”), pass **`provider: "grok"`**. Grok currently supports only **`mode: "heavy"`**; omit `mode` unless the user explicitly says Heavy.
29
29
  - If the user says ChatGPT, pass **`provider: "chatgpt"`**. Never route a ChatGPT request to Grok.
30
+ - If the user provides an existing ChatGPT conversation id such as `6a28ab5c-e4d4-83e8-b8be-dd39f38a26d6` or a `https://chatgpt.com/c/...` URL, pass it as **`chatGptConversationId`** to both `oracle_preflight` and `oracle_submit`, and pass **`provider: "chatgpt"`**. This is only for explicit existing browser-created ChatGPT threads; omit it for normal `/oracle` jobs.
30
31
  - Otherwise omit **`provider`** to use the configured default provider, or pass **`provider: "chatgpt"`** only when needed for clarity.
31
32
  - To choose a specific ChatGPT model, pass **`preset`** with one of the allowed ids from the canonical preset registry.
32
33
  - Matching human-readable preset labels and common hyphen/space variants are also accepted and normalized automatically, but prefer canonical ids when readily available.
@@ -14,7 +14,7 @@ const require = createRequire(import.meta.url);
14
14
  const tsxCli = require.resolve("tsx/cli");
15
15
 
16
16
  const DEFAULT_PROVIDER = "zai";
17
- const DEFAULT_MODEL = "glm-5.1";
17
+ const DEFAULT_MODEL = "glm-5.2";
18
18
  const DEFAULT_TIMEOUT_MS = 180_000;
19
19
  const PACKAGE_NAME = "pi-oracle";
20
20
 
@@ -54,7 +54,7 @@ function realSmokeProvider(config = {}) {
54
54
  }
55
55
 
56
56
  function realSmokeModel(config = {}) {
57
- return process.env.PI_ORACLE_REAL_TEST_MODEL || config.realSmoke?.defaultModel || "glm-5.1";
57
+ return process.env.PI_ORACLE_REAL_TEST_MODEL || config.realSmoke?.defaultModel || "glm-5.2";
58
58
  }
59
59
 
60
60
  function truthy(value) {
@@ -57,7 +57,7 @@ Environment:
57
57
  PLATFORM_SMOKE_WINDOWS_WORK_ROOT Windows work root
58
58
  PI_ORACLE_SMOKE_* Project-specific aliases for the PLATFORM_SMOKE_* knobs above
59
59
  PI_ORACLE_REAL_TEST_PROVIDER Real smoke provider (default: zai)
60
- PI_ORACLE_REAL_TEST_MODEL Real smoke model (default: glm-5.1)
60
+ PI_ORACLE_REAL_TEST_MODEL Real smoke model (default: glm-5.2)
61
61
  ZAI_API_KEY Default real-smoke provider API key for optional model-agent debugging
62
62
  `);
63
63
  }