pi-oracle 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +43 -12
- package/docs/ORACLE_DESIGN.md +32 -16
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +28 -1
- package/extensions/oracle/index.ts +1 -1
- package/extensions/oracle/lib/auth.ts +50 -0
- package/extensions/oracle/lib/commands.ts +57 -47
- package/extensions/oracle/lib/config.ts +53 -2
- package/extensions/oracle/lib/jobs.ts +31 -5
- package/extensions/oracle/lib/poller.ts +33 -4
- package/extensions/oracle/lib/runtime.ts +171 -7
- package/extensions/oracle/lib/tools.ts +726 -253
- package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +1 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +13 -0
- package/extensions/oracle/shared/job-observability-helpers.d.mts +2 -1
- package/extensions/oracle/shared/job-observability-helpers.mjs +28 -10
- package/extensions/oracle/worker/auth-bootstrap.mjs +49 -4
- package/extensions/oracle/worker/auth-flow-helpers.mjs +1 -1
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +106 -41
- package/extensions/oracle/worker/run-job.mjs +17 -13
- package/package.json +6 -2
- package/prompts/oracle-followup.md +48 -0
- package/prompts/oracle.md +18 -11
|
@@ -7,14 +7,16 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { basename, join, posix } from "node:path";
|
|
10
|
-
import
|
|
10
|
+
import { runOracleAuthBootstrap } from "./auth.js";
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
11
12
|
import { Type } from "@sinclair/typebox";
|
|
12
13
|
import { formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
|
|
13
|
-
import { transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
|
|
14
|
+
import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent, transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
|
|
14
15
|
import { isLockTimeoutError, withGlobalReconcileLock, withLock } from "./locks.js";
|
|
15
16
|
import {
|
|
16
17
|
coerceOracleSubmitPresetId,
|
|
17
18
|
loadOracleConfig,
|
|
19
|
+
ORACLE_SUBMIT_PRESET_IDS,
|
|
18
20
|
resolveOracleSubmitPreset,
|
|
19
21
|
} from "./config.js";
|
|
20
22
|
import {
|
|
@@ -44,9 +46,11 @@ import { getQueuePosition, promoteQueuedJobs, promoteQueuedJobsWithinAdmissionLo
|
|
|
44
46
|
import { refreshOracleStatus } from "./poller.js";
|
|
45
47
|
import {
|
|
46
48
|
allocateRuntime,
|
|
49
|
+
assertOracleSubmitPrerequisites,
|
|
47
50
|
cleanupRuntimeArtifacts,
|
|
48
51
|
getProjectId,
|
|
49
52
|
getSessionId,
|
|
53
|
+
hasPersistedSessionFile,
|
|
50
54
|
parseConversationId,
|
|
51
55
|
requirePersistedSessionFile,
|
|
52
56
|
tryAcquireConversationLease,
|
|
@@ -55,7 +59,11 @@ import {
|
|
|
55
59
|
|
|
56
60
|
const ORACLE_SUBMIT_PARAMS = Type.Object({
|
|
57
61
|
prompt: Type.String({ description: "Prompt text to send to ChatGPT web." }),
|
|
58
|
-
files: Type.Array(Type.String({
|
|
62
|
+
files: Type.Array(Type.String({
|
|
63
|
+
description: "Project-relative file or directory path to include in the archive.",
|
|
64
|
+
minLength: 1,
|
|
65
|
+
pattern: ".*\\S.*",
|
|
66
|
+
}), {
|
|
59
67
|
description: "Exact project-relative files/directories to include in the oracle archive.",
|
|
60
68
|
minItems: 1,
|
|
61
69
|
}),
|
|
@@ -565,7 +573,54 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
|
|
|
565
573
|
};
|
|
566
574
|
}
|
|
567
575
|
|
|
568
|
-
|
|
576
|
+
type OracleToolName = "oracle_auth" | "oracle_submit" | "oracle_read" | "oracle_cancel";
|
|
577
|
+
type OracleToolErrorSource = OracleToolName | "oracle_preflight";
|
|
578
|
+
type OracleQueueSnapshot = { queued: boolean; position?: number; depth?: number };
|
|
579
|
+
type OracleToolErrorDetails = {
|
|
580
|
+
code: string;
|
|
581
|
+
message: string;
|
|
582
|
+
rejectedValue?: string;
|
|
583
|
+
allowedValues?: string[];
|
|
584
|
+
suggestedNextStep?: string;
|
|
585
|
+
};
|
|
586
|
+
type OracleToolJobDetailsOptions = {
|
|
587
|
+
queue?: OracleQueueSnapshot;
|
|
588
|
+
archiveBytes?: number;
|
|
589
|
+
initialArchiveBytes?: number;
|
|
590
|
+
autoPrunedArchivePaths?: ArchiveSizeBreakdownRow[];
|
|
591
|
+
responsePreview?: string;
|
|
592
|
+
responseAvailable?: boolean;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_auth", "oracle_submit", "oracle_read", "oracle_cancel"]);
|
|
596
|
+
|
|
597
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
598
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
599
|
+
? value as Record<string, unknown>
|
|
600
|
+
: undefined;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function getErrorMessage(error: unknown): string {
|
|
604
|
+
return error instanceof Error ? error.message : String(error);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function buildOracleQueueSnapshot(
|
|
608
|
+
job: NonNullable<ReturnType<typeof readJob>>,
|
|
609
|
+
queuePosition?: { position: number; depth: number },
|
|
610
|
+
): OracleQueueSnapshot {
|
|
611
|
+
return {
|
|
612
|
+
queued: job.status === "queued",
|
|
613
|
+
position: queuePosition?.position,
|
|
614
|
+
depth: queuePosition?.depth,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function redactJobDetails(
|
|
619
|
+
job: NonNullable<ReturnType<typeof readJob>>,
|
|
620
|
+
options: OracleToolJobDetailsOptions = {},
|
|
621
|
+
) {
|
|
622
|
+
const lastEvent = getLatestOracleJobLifecycleEvent(job);
|
|
623
|
+
const terminalEvent = getLatestOracleTerminalLifecycleEvent(job);
|
|
569
624
|
return {
|
|
570
625
|
id: job.id,
|
|
571
626
|
status: job.status,
|
|
@@ -576,24 +631,416 @@ function redactJobDetails(job: NonNullable<ReturnType<typeof readJob>>) {
|
|
|
576
631
|
queuedAt: job.queuedAt,
|
|
577
632
|
submittedAt: job.submittedAt,
|
|
578
633
|
completedAt: job.completedAt,
|
|
634
|
+
promptPath: job.promptPath,
|
|
635
|
+
archivePath: job.archivePath,
|
|
636
|
+
archiveSha256: job.archiveSha256,
|
|
637
|
+
archiveBytes: options.archiveBytes,
|
|
638
|
+
initialArchiveBytes: options.initialArchiveBytes,
|
|
639
|
+
autoPrunedArchivePaths: options.autoPrunedArchivePaths ?? [],
|
|
640
|
+
queue: options.queue ?? buildOracleQueueSnapshot(job),
|
|
579
641
|
followUpToJobId: job.followUpToJobId,
|
|
580
642
|
chatUrl: job.chatUrl,
|
|
581
643
|
conversationId: job.conversationId,
|
|
582
644
|
responsePath: job.responsePath,
|
|
583
645
|
responseFormat: job.responseFormat,
|
|
646
|
+
responseAvailable: options.responseAvailable ?? false,
|
|
647
|
+
responsePreview: options.responsePreview,
|
|
648
|
+
artifactsPath: `${getJobDir(job.id)}/artifacts`,
|
|
584
649
|
artifactPaths: job.artifactPaths,
|
|
585
650
|
artifactFailureCount: job.artifactFailureCount,
|
|
586
651
|
artifactsManifestPath: job.artifactsManifestPath,
|
|
652
|
+
workerLogPath: job.workerLogPath,
|
|
587
653
|
archiveDeletedAfterUpload: job.archiveDeletedAfterUpload,
|
|
588
654
|
runtimeId: job.runtimeId,
|
|
589
655
|
cleanupWarnings: job.cleanupWarnings,
|
|
590
656
|
lastCleanupAt: job.lastCleanupAt,
|
|
657
|
+
terminalEvent: terminalEvent ? { ...terminalEvent } : undefined,
|
|
658
|
+
lastEvent: lastEvent ? { ...lastEvent } : undefined,
|
|
591
659
|
error: job.error,
|
|
592
660
|
lifecycleEvents: job.lifecycleEvents,
|
|
593
661
|
};
|
|
594
662
|
}
|
|
595
663
|
|
|
596
|
-
|
|
664
|
+
function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unknown, params: Record<string, unknown>): OracleToolErrorDetails {
|
|
665
|
+
const message = getErrorMessage(error);
|
|
666
|
+
|
|
667
|
+
if (toolName === "oracle_submit" && typeof params.preset === "string" && message.startsWith("Unknown oracle_submit preset:")) {
|
|
668
|
+
return {
|
|
669
|
+
code: "invalid_preset",
|
|
670
|
+
message,
|
|
671
|
+
rejectedValue: params.preset,
|
|
672
|
+
allowedValues: [...ORACLE_SUBMIT_PRESET_IDS],
|
|
673
|
+
suggestedNextStep: "Retry with one of the canonical preset ids, or omit preset to use the configured default.",
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (message.startsWith("Oracle requires a persisted pi session")) {
|
|
678
|
+
return {
|
|
679
|
+
code: "persisted_session_required",
|
|
680
|
+
message,
|
|
681
|
+
suggestedNextStep: "Start or save a persisted pi session, then retry oracle_submit.",
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (message.startsWith("Oracle auth seed profile not found: ")) {
|
|
686
|
+
return {
|
|
687
|
+
code: "auth_seed_profile_missing",
|
|
688
|
+
message,
|
|
689
|
+
rejectedValue: message.replace(/^Oracle auth seed profile not found: /, "").replace(/\. Run \/oracle-auth first\.$/, ""),
|
|
690
|
+
suggestedNextStep: "Call oracle_auth or run /oracle-auth once, then retry the oracle tool call.",
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (message.startsWith("Oracle auth seed profile is not readable: ")) {
|
|
695
|
+
return {
|
|
696
|
+
code: "auth_seed_profile_unreadable",
|
|
697
|
+
message,
|
|
698
|
+
rejectedValue: message.replace(/^Oracle auth seed profile is not readable: /, "").replace(/\. Fix its permissions or rerun \/oracle-auth\.$/, ""),
|
|
699
|
+
suggestedNextStep: "Fix the auth seed profile permissions or call oracle_auth / rerun /oracle-auth once, then retry.",
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (message.startsWith("Oracle auth seed profile is not a directory: ")) {
|
|
704
|
+
return {
|
|
705
|
+
code: "auth_seed_profile_invalid_type",
|
|
706
|
+
message,
|
|
707
|
+
rejectedValue: message.replace(/^Oracle auth seed profile is not a directory: /, "").replace(/\. Remove the invalid path or rerun \/oracle-auth\.$/, ""),
|
|
708
|
+
suggestedNextStep: "Remove the invalid auth seed path or call oracle_auth / rerun /oracle-auth once to recreate it.",
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (message.startsWith("Failed to parse oracle config ") || message.startsWith("Invalid oracle config:") || message.startsWith("Invalid oracle project config:")) {
|
|
713
|
+
return {
|
|
714
|
+
code: "oracle_config_invalid",
|
|
715
|
+
message,
|
|
716
|
+
suggestedNextStep: "Fix the oracle config and retry once the configured paths and values are valid.",
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (message.startsWith("Configured oracle browser executable does not exist: ")) {
|
|
721
|
+
return {
|
|
722
|
+
code: "browser_executable_missing",
|
|
723
|
+
message,
|
|
724
|
+
rejectedValue: message.replace(/^Configured oracle browser executable does not exist: /, "").replace(/\. Fix browser\.executablePath or install Chrome there\.$/, ""),
|
|
725
|
+
suggestedNextStep: "Fix browser.executablePath or install Chrome at that path, then retry.",
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (message.startsWith("Configured oracle browser executable is not executable: ")) {
|
|
730
|
+
return {
|
|
731
|
+
code: "browser_executable_not_executable",
|
|
732
|
+
message,
|
|
733
|
+
rejectedValue: message.replace(/^Configured oracle browser executable is not executable: /, "").replace(/\. Fix browser\.executablePath permissions or point it at a runnable Chrome binary\.$/, ""),
|
|
734
|
+
suggestedNextStep: "Fix browser.executablePath permissions or point it at a runnable Chrome binary, then retry.",
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (message.startsWith("Oracle prerequisite not found on PATH: ")) {
|
|
739
|
+
const rejectedValue = message.replace(/^Oracle prerequisite not found on PATH: /, "").replace(/\. Install .*$/, "");
|
|
740
|
+
return {
|
|
741
|
+
code: "local_dependency_missing",
|
|
742
|
+
message,
|
|
743
|
+
rejectedValue,
|
|
744
|
+
suggestedNextStep: `Install ${rejectedValue || "the missing dependency"} and retry.`,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (message.startsWith("Oracle runtime profiles directory is not writable: ")) {
|
|
749
|
+
return {
|
|
750
|
+
code: "runtime_profiles_dir_unwritable",
|
|
751
|
+
message,
|
|
752
|
+
rejectedValue: message.replace(/^Oracle runtime profiles directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
|
|
753
|
+
suggestedNextStep: "Fix browser.runtimeProfilesDir permissions or configure a writable directory, then retry.",
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (message.startsWith("Oracle jobs directory is not writable: ")) {
|
|
758
|
+
return {
|
|
759
|
+
code: "jobs_dir_unwritable",
|
|
760
|
+
message,
|
|
761
|
+
rejectedValue: message.replace(/^Oracle jobs directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
|
|
762
|
+
suggestedNextStep: "Fix PI_ORACLE_JOBS_DIR permissions or point it at a writable directory, then retry.",
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (toolName === "oracle_submit" && message === "oracle_submit requires at least one file or directory to archive") {
|
|
767
|
+
return {
|
|
768
|
+
code: "archive_input_required",
|
|
769
|
+
message,
|
|
770
|
+
suggestedNextStep: "Pass at least one project-relative file or directory in files.",
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (toolName === "oracle_submit" && message === "Archive input must be a non-empty project-relative path") {
|
|
775
|
+
return {
|
|
776
|
+
code: "archive_input_blank",
|
|
777
|
+
message,
|
|
778
|
+
suggestedNextStep: "Retry with a non-empty project-relative file or directory path. Use '.' only when you intentionally want a whole-repo archive.",
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (toolName === "oracle_submit" && message === "Archive input must use '.' exactly for a whole-repo archive") {
|
|
783
|
+
return {
|
|
784
|
+
code: "archive_input_whole_repo_sentinel_invalid",
|
|
785
|
+
message,
|
|
786
|
+
suggestedNextStep: "If you want a whole-repo archive, pass '.' exactly. Otherwise pass an exact project-relative path without extra padding.",
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (toolName === "oracle_submit" && message.startsWith("Archive input does not exist: ")) {
|
|
791
|
+
return {
|
|
792
|
+
code: "archive_input_missing",
|
|
793
|
+
message,
|
|
794
|
+
rejectedValue: message.replace(/^Archive input does not exist: /, ""),
|
|
795
|
+
suggestedNextStep: "Retry with an existing project-relative file or directory.",
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (toolName === "oracle_submit" && message.startsWith("Archive input must be inside the project cwd: ")) {
|
|
800
|
+
return {
|
|
801
|
+
code: "archive_input_outside_project",
|
|
802
|
+
message,
|
|
803
|
+
rejectedValue: message.replace(/^Archive input must be inside the project cwd: /, ""),
|
|
804
|
+
suggestedNextStep: "Retry with a path inside the current project cwd.",
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (toolName === "oracle_submit" && message.startsWith("Archive input must resolve inside the project cwd without symlink escapes: ")) {
|
|
809
|
+
return {
|
|
810
|
+
code: "archive_input_symlink_escape",
|
|
811
|
+
message,
|
|
812
|
+
rejectedValue: message.replace(/^Archive input must resolve inside the project cwd without symlink escapes: /, ""),
|
|
813
|
+
suggestedNextStep: "Retry with a real project-relative path that does not escape the repo through symlinks.",
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (toolName === "oracle_submit" && message.startsWith("Follow-up oracle job not found: ")) {
|
|
818
|
+
return {
|
|
819
|
+
code: "follow_up_job_not_found",
|
|
820
|
+
message,
|
|
821
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
822
|
+
suggestedNextStep: "Retry with a completed oracle job id from this project that has a persisted chat URL.",
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (toolName === "oracle_submit" && message.includes("belongs to a different project")) {
|
|
827
|
+
return {
|
|
828
|
+
code: "follow_up_job_wrong_project",
|
|
829
|
+
message,
|
|
830
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
831
|
+
suggestedNextStep: "Retry with a follow-up job id from the current project.",
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (toolName === "oracle_submit" && message.includes("is not complete")) {
|
|
836
|
+
return {
|
|
837
|
+
code: "follow_up_job_not_complete",
|
|
838
|
+
message,
|
|
839
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
840
|
+
suggestedNextStep: "Wait for the earlier oracle job to finish, then retry the follow-up.",
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (toolName === "oracle_submit" && message.includes("has no persisted chat URL")) {
|
|
845
|
+
return {
|
|
846
|
+
code: "follow_up_job_missing_chat_url",
|
|
847
|
+
message,
|
|
848
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
849
|
+
suggestedNextStep: "Retry with an earlier completed oracle job that recorded a chat URL.",
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if ((toolName === "oracle_read" || toolName === "oracle_cancel") && typeof params.jobId === "string" && message.startsWith("Oracle job not found in this project:")) {
|
|
854
|
+
return {
|
|
855
|
+
code: "job_not_found",
|
|
856
|
+
message,
|
|
857
|
+
rejectedValue: params.jobId,
|
|
858
|
+
suggestedNextStep: "Use /oracle-status to discover a valid job id for this project, then retry.",
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (toolName === "oracle_submit" && message.startsWith("Oracle archive exceeds ChatGPT upload limit")) {
|
|
863
|
+
return {
|
|
864
|
+
code: "archive_too_large",
|
|
865
|
+
message,
|
|
866
|
+
suggestedNextStep: "Retry with a narrower archive, starting with modified files plus adjacent files plus directly relevant subtrees.",
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
code: `${toolName}_failed`,
|
|
872
|
+
message,
|
|
873
|
+
suggestedNextStep: "Inspect the error message, correct the inputs or environment, and retry.",
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function buildOracleToolErrorResult(
|
|
878
|
+
toolName: OracleToolName,
|
|
879
|
+
error: unknown,
|
|
880
|
+
params: Record<string, unknown>,
|
|
881
|
+
options?: { job?: NonNullable<ReturnType<typeof readJob>>; jobDetails?: OracleToolJobDetailsOptions },
|
|
882
|
+
) {
|
|
883
|
+
const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
|
|
884
|
+
return {
|
|
885
|
+
content: [{
|
|
886
|
+
type: "text" as const,
|
|
887
|
+
text: [
|
|
888
|
+
errorDetails.message,
|
|
889
|
+
errorDetails.suggestedNextStep ? `Suggested next step: ${errorDetails.suggestedNextStep}` : undefined,
|
|
890
|
+
].filter(Boolean).join("\n"),
|
|
891
|
+
}],
|
|
892
|
+
details: {
|
|
893
|
+
job: options?.job ? redactJobDetails(options.job, options.jobDetails) : undefined,
|
|
894
|
+
error: errorDetails,
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
type OraclePreflightDetails = {
|
|
900
|
+
ready: boolean;
|
|
901
|
+
session: {
|
|
902
|
+
persisted: boolean;
|
|
903
|
+
sessionFile?: string;
|
|
904
|
+
};
|
|
905
|
+
config: {
|
|
906
|
+
ready: boolean;
|
|
907
|
+
};
|
|
908
|
+
auth: {
|
|
909
|
+
ready: boolean;
|
|
910
|
+
seedProfileDir?: string;
|
|
911
|
+
};
|
|
912
|
+
error?: OracleToolErrorDetails;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
function formatOraclePreflightResponse(details: OraclePreflightDetails): string {
|
|
916
|
+
if (details.ready) {
|
|
917
|
+
return [
|
|
918
|
+
"Oracle preflight ready.",
|
|
919
|
+
details.session.sessionFile ? `Persisted session: ${details.session.sessionFile}` : undefined,
|
|
920
|
+
details.auth.seedProfileDir ? `Auth seed profile: ${details.auth.seedProfileDir}` : undefined,
|
|
921
|
+
"You can continue with oracle context gathering and submission.",
|
|
922
|
+
].filter(Boolean).join("\n");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return [
|
|
926
|
+
`Oracle preflight blocked: ${details.error?.message ?? "unknown blocker"}`,
|
|
927
|
+
details.error?.suggestedNextStep ? `Suggested next step: ${details.error.suggestedNextStep}` : undefined,
|
|
928
|
+
].filter(Boolean).join("\n");
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePreflightDetails> {
|
|
932
|
+
const sessionFile = getSessionFile(ctx);
|
|
933
|
+
if (!hasPersistedSessionFile(sessionFile)) {
|
|
934
|
+
return {
|
|
935
|
+
ready: false,
|
|
936
|
+
session: { persisted: false },
|
|
937
|
+
config: { ready: false },
|
|
938
|
+
auth: { ready: false },
|
|
939
|
+
error: buildOracleToolErrorDetails(
|
|
940
|
+
"oracle_preflight",
|
|
941
|
+
new Error("Oracle requires a persisted pi session to submit oracle jobs. Start or save a real session before using oracle."),
|
|
942
|
+
{},
|
|
943
|
+
),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
let config;
|
|
948
|
+
try {
|
|
949
|
+
config = loadOracleConfig(ctx.cwd);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
return {
|
|
952
|
+
ready: false,
|
|
953
|
+
session: { persisted: true, sessionFile },
|
|
954
|
+
config: { ready: false },
|
|
955
|
+
auth: { ready: false },
|
|
956
|
+
error: buildOracleToolErrorDetails("oracle_preflight", error, {}),
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
await assertOracleSubmitPrerequisites(config);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, {});
|
|
964
|
+
return {
|
|
965
|
+
ready: false,
|
|
966
|
+
session: { persisted: true, sessionFile },
|
|
967
|
+
config: { ready: true },
|
|
968
|
+
auth: {
|
|
969
|
+
ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type"].includes(errorDetails.code),
|
|
970
|
+
seedProfileDir: config.browser.authSeedProfileDir,
|
|
971
|
+
},
|
|
972
|
+
error: errorDetails,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return {
|
|
977
|
+
ready: true,
|
|
978
|
+
session: { persisted: true, sessionFile },
|
|
979
|
+
config: { ready: true },
|
|
980
|
+
auth: {
|
|
981
|
+
ready: true,
|
|
982
|
+
seedProfileDir: config.browser.authSeedProfileDir,
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWorkerPath = workerPath): void {
|
|
988
|
+
pi.on("tool_result", async (event) => {
|
|
989
|
+
if (!ORACLE_TOOL_NAMES.has(event.toolName as OracleToolName)) return;
|
|
990
|
+
if (event.isError) return;
|
|
991
|
+
const details = asRecord(event.details);
|
|
992
|
+
const errorDetails = asRecord(details?.error);
|
|
993
|
+
if (typeof errorDetails?.code === "string" && typeof errorDetails?.message === "string") {
|
|
994
|
+
return { isError: true };
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
pi.registerTool({
|
|
999
|
+
name: "oracle_preflight",
|
|
1000
|
+
label: "Oracle Preflight",
|
|
1001
|
+
description: "Check whether oracle is ready in this session before spending time gathering context or preparing a submission.",
|
|
1002
|
+
promptSnippet: "Check oracle readiness before expensive /oracle preparation.",
|
|
1003
|
+
promptGuidelines: [
|
|
1004
|
+
"Call oracle_preflight before doing expensive /oracle preparation. If ready is false, stop immediately and report the suggested next step instead of reading files or crafting archive inputs.",
|
|
1005
|
+
],
|
|
1006
|
+
parameters: Type.Object({}),
|
|
1007
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
1008
|
+
const details = await runOraclePreflight(ctx);
|
|
1009
|
+
return {
|
|
1010
|
+
content: [{ type: "text" as const, text: formatOraclePreflightResponse(details) }],
|
|
1011
|
+
details,
|
|
1012
|
+
};
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
pi.registerTool({
|
|
1017
|
+
name: "oracle_auth",
|
|
1018
|
+
label: "Oracle Auth",
|
|
1019
|
+
description: "Refresh the shared oracle auth seed profile by importing ChatGPT cookies from your configured real Chrome profile.",
|
|
1020
|
+
promptSnippet: "Refresh oracle auth before retrying a login-required oracle run.",
|
|
1021
|
+
promptGuidelines: [
|
|
1022
|
+
"Call oracle_auth when an oracle run failed because ChatGPT login is required, the worker said to rerun /oracle-auth, or stale auth appears to be blocking submission execution.",
|
|
1023
|
+
"At most once per user request, refresh auth and then retry the blocked oracle submission.",
|
|
1024
|
+
"If oracle_auth itself fails, stop and report the failure instead of looping.",
|
|
1025
|
+
],
|
|
1026
|
+
parameters: Type.Object({}),
|
|
1027
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
1028
|
+
try {
|
|
1029
|
+
const projectCwd = getProjectId(ctx.cwd);
|
|
1030
|
+
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd);
|
|
1031
|
+
return {
|
|
1032
|
+
content: [{ type: "text" as const, text: message }],
|
|
1033
|
+
details: {
|
|
1034
|
+
refreshed: true,
|
|
1035
|
+
authSeedProfileDir: loadOracleConfig(projectCwd).browser.authSeedProfileDir,
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
return buildOracleToolErrorResult("oracle_auth", error, {});
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
|
|
597
1044
|
pi.registerTool({
|
|
598
1045
|
name: "oracle_submit",
|
|
599
1046
|
label: "Oracle Submit",
|
|
@@ -603,9 +1050,11 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
603
1050
|
promptSnippet: "Dispatch a background ChatGPT web oracle job after gathering repo context.",
|
|
604
1051
|
promptGuidelines: [
|
|
605
1052
|
"Gather context before calling oracle_submit.",
|
|
606
|
-
"
|
|
607
|
-
"
|
|
608
|
-
"
|
|
1053
|
+
"If the immediately preceding oracle run failed because ChatGPT login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission. Do not loop auth refreshes.",
|
|
1054
|
+
"Prefer context-rich archives up to the 250 MB ceiling because more relevant surrounding context is usually better than less.",
|
|
1055
|
+
"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.",
|
|
1056
|
+
"For narrower asks, still include nearby tests, docs, configs, and adjacent modules when they may improve answer quality. Only narrow aggressively when the user explicitly asks, privacy/sensitivity requires it, or size pressure forces it.",
|
|
1057
|
+
"Do not default to a one-file archive for a single function, file, or stack trace if the relevant surrounding context still fits comfortably within the limit.",
|
|
609
1058
|
"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.",
|
|
610
1059
|
"If a submitted oracle job later fails because upload is rejected, retry smaller: remove the largest obviously irrelevant/generated content first, then narrow to modified files plus adjacent files plus directly relevant subtrees, then explain the cut or ask the user if still needed.",
|
|
611
1060
|
"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.",
|
|
@@ -615,68 +1064,116 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
615
1064
|
],
|
|
616
1065
|
parameters: ORACLE_SUBMIT_PARAMS,
|
|
617
1066
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
618
|
-
const config = loadOracleConfig(ctx.cwd);
|
|
619
|
-
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
620
|
-
const projectId = getProjectId(ctx.cwd);
|
|
621
|
-
const sessionId = getSessionId(originSessionFile, projectId);
|
|
622
|
-
const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
|
|
623
|
-
const selection = resolveOracleSubmitPreset(presetId);
|
|
624
|
-
const followUp = resolveFollowUp(params.followUpJobId, ctx.cwd);
|
|
625
1067
|
try {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1068
|
+
const projectCwd = getProjectId(ctx.cwd);
|
|
1069
|
+
const config = loadOracleConfig(projectCwd);
|
|
1070
|
+
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
1071
|
+
const projectId = getProjectId(projectCwd);
|
|
1072
|
+
const sessionId = getSessionId(originSessionFile, projectId);
|
|
1073
|
+
const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
|
|
1074
|
+
const selection = resolveOracleSubmitPreset(presetId);
|
|
1075
|
+
const followUp = resolveFollowUp(params.followUpJobId, projectCwd);
|
|
1076
|
+
// Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
|
|
1077
|
+
resolveArchiveInputs(projectCwd, params.files);
|
|
1078
|
+
await assertOracleSubmitPrerequisites(config);
|
|
1079
|
+
try {
|
|
1080
|
+
await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: projectCwd }, async () => {
|
|
1081
|
+
await reconcileStaleOracleJobs();
|
|
1082
|
+
await pruneTerminalOracleJobs();
|
|
1083
|
+
});
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (!isLockTimeoutError(error, "reconcile", "global")) throw error;
|
|
1086
|
+
}
|
|
633
1087
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
1088
|
+
const jobId = randomUUID();
|
|
1089
|
+
const tempArchivePath = join(tmpdir(), `oracle-archive-${jobId}.tar.zst`);
|
|
1090
|
+
const runtime = allocateRuntime(config);
|
|
1091
|
+
let job: OracleJob | undefined;
|
|
1092
|
+
let archive: ArchiveCreationResult | undefined;
|
|
1093
|
+
let queued = false;
|
|
1094
|
+
let queuedSubmissionDurable = false;
|
|
1095
|
+
let runtimeLeaseAcquired = false;
|
|
1096
|
+
let conversationLeaseAcquired = false;
|
|
1097
|
+
let workerSpawned = false;
|
|
1098
|
+
let spawnedWorker: Awaited<ReturnType<typeof spawnWorker>> | undefined;
|
|
645
1099
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const admittedAt = new Date().toISOString();
|
|
653
|
-
const runtimeAttempt = await tryAcquireRuntimeLease(config, {
|
|
654
|
-
jobId,
|
|
655
|
-
runtimeId: runtime.runtimeId,
|
|
656
|
-
runtimeSessionName: runtime.runtimeSessionName,
|
|
657
|
-
runtimeProfileDir: runtime.runtimeProfileDir,
|
|
658
|
-
projectId,
|
|
659
|
-
sessionId,
|
|
660
|
-
createdAt: admittedAt,
|
|
661
|
-
});
|
|
1100
|
+
try {
|
|
1101
|
+
archive = await createArchive(projectCwd, params.files, tempArchivePath);
|
|
1102
|
+
const currentArchive = archive;
|
|
1103
|
+
await withLock("admission", "global", { jobId, processPid: process.pid }, async () => {
|
|
1104
|
+
await promoteQueuedJobsWithinAdmissionLock({ workerPath, source: "oracle_submit" });
|
|
662
1105
|
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
maxQueuedJobs,
|
|
673
|
-
maxQueuedArchiveBytes,
|
|
1106
|
+
const admittedAt = new Date().toISOString();
|
|
1107
|
+
const runtimeAttempt = await tryAcquireRuntimeLease(config, {
|
|
1108
|
+
jobId,
|
|
1109
|
+
runtimeId: runtime.runtimeId,
|
|
1110
|
+
runtimeSessionName: runtime.runtimeSessionName,
|
|
1111
|
+
runtimeProfileDir: runtime.runtimeProfileDir,
|
|
1112
|
+
projectId,
|
|
1113
|
+
sessionId,
|
|
1114
|
+
createdAt: admittedAt,
|
|
674
1115
|
});
|
|
675
|
-
|
|
676
|
-
|
|
1116
|
+
|
|
1117
|
+
if (!runtimeAttempt.acquired) {
|
|
1118
|
+
const queuePressure = await getQueuedArchivePressure();
|
|
1119
|
+
const maxQueuedJobs = config.browser.maxConcurrentJobs * MAX_QUEUED_JOBS_PER_ACTIVE_RUNTIME;
|
|
1120
|
+
const maxQueuedArchiveBytes = config.browser.maxConcurrentJobs * MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME;
|
|
1121
|
+
const queueAdmissionFailure = getQueueAdmissionFailure({
|
|
1122
|
+
queuePressure,
|
|
1123
|
+
archiveBytes: currentArchive.archiveBytes,
|
|
1124
|
+
activeJobs: runtimeAttempt.liveLeases.length,
|
|
1125
|
+
maxActiveJobs: config.browser.maxConcurrentJobs,
|
|
1126
|
+
maxQueuedJobs,
|
|
1127
|
+
maxQueuedArchiveBytes,
|
|
1128
|
+
});
|
|
1129
|
+
if (queueAdmissionFailure) {
|
|
1130
|
+
throw new Error(queueAdmissionFailure);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
queued = true;
|
|
1134
|
+
job = await createJob(
|
|
1135
|
+
jobId,
|
|
1136
|
+
{
|
|
1137
|
+
prompt: params.prompt,
|
|
1138
|
+
files: params.files,
|
|
1139
|
+
selection,
|
|
1140
|
+
followUpToJobId: followUp.followUpToJobId,
|
|
1141
|
+
chatUrl: followUp.chatUrl,
|
|
1142
|
+
requestSource: "tool",
|
|
1143
|
+
},
|
|
1144
|
+
projectCwd,
|
|
1145
|
+
originSessionFile,
|
|
1146
|
+
config,
|
|
1147
|
+
runtime,
|
|
1148
|
+
{ initialState: "queued", createdAt: admittedAt },
|
|
1149
|
+
);
|
|
1150
|
+
await rename(tempArchivePath, job.archivePath);
|
|
1151
|
+
job = await updateJob(job.id, (current) => ({
|
|
1152
|
+
...current,
|
|
1153
|
+
archiveSha256: currentArchive.sha256,
|
|
1154
|
+
}));
|
|
1155
|
+
queuedSubmissionDurable = true;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
runtimeLeaseAcquired = true;
|
|
1160
|
+
if (followUp.conversationId) {
|
|
1161
|
+
const conversationAttempt = await tryAcquireConversationLease({
|
|
1162
|
+
jobId,
|
|
1163
|
+
conversationId: followUp.conversationId,
|
|
1164
|
+
projectId,
|
|
1165
|
+
sessionId,
|
|
1166
|
+
createdAt: admittedAt,
|
|
1167
|
+
});
|
|
1168
|
+
if (!conversationAttempt.acquired) {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
`Oracle conversation ${followUp.conversationId} is already in use by job ${conversationAttempt.blocker?.jobId ?? "unknown"}. ` +
|
|
1171
|
+
"Concurrent follow-ups to the same ChatGPT thread are not allowed.",
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
conversationLeaseAcquired = true;
|
|
677
1175
|
}
|
|
678
1176
|
|
|
679
|
-
queued = true;
|
|
680
1177
|
job = await createJob(
|
|
681
1178
|
jobId,
|
|
682
1179
|
{
|
|
@@ -687,179 +1184,137 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
687
1184
|
chatUrl: followUp.chatUrl,
|
|
688
1185
|
requestSource: "tool",
|
|
689
1186
|
},
|
|
690
|
-
|
|
1187
|
+
projectCwd,
|
|
691
1188
|
originSessionFile,
|
|
692
1189
|
config,
|
|
693
1190
|
runtime,
|
|
694
|
-
{ initialState: "
|
|
1191
|
+
{ initialState: "submitted", createdAt: admittedAt },
|
|
695
1192
|
);
|
|
696
1193
|
await rename(tempArchivePath, job.archivePath);
|
|
1194
|
+
spawnedWorker = await spawnWorker(workerPath, job.id);
|
|
1195
|
+
workerSpawned = true;
|
|
1196
|
+
const worker = spawnedWorker;
|
|
697
1197
|
job = await updateJob(job.id, (current) => ({
|
|
698
1198
|
...current,
|
|
699
1199
|
archiveSha256: currentArchive.sha256,
|
|
1200
|
+
workerPid: worker.pid,
|
|
1201
|
+
workerNonce: worker.nonce,
|
|
1202
|
+
workerStartedAt: worker.startedAt,
|
|
700
1203
|
}));
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
runtimeLeaseAcquired = true;
|
|
706
|
-
if (followUp.conversationId) {
|
|
707
|
-
const conversationAttempt = await tryAcquireConversationLease({
|
|
708
|
-
jobId,
|
|
709
|
-
conversationId: followUp.conversationId,
|
|
710
|
-
projectId,
|
|
711
|
-
sessionId,
|
|
712
|
-
createdAt: admittedAt,
|
|
713
|
-
});
|
|
714
|
-
if (!conversationAttempt.acquired) {
|
|
715
|
-
throw new Error(
|
|
716
|
-
`Oracle conversation ${followUp.conversationId} is already in use by job ${conversationAttempt.blocker?.jobId ?? "unknown"}. ` +
|
|
717
|
-
"Concurrent follow-ups to the same ChatGPT thread are not allowed.",
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
conversationLeaseAcquired = true;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
job = await createJob(
|
|
724
|
-
jobId,
|
|
725
|
-
{
|
|
726
|
-
prompt: params.prompt,
|
|
727
|
-
files: params.files,
|
|
728
|
-
selection,
|
|
729
|
-
followUpToJobId: followUp.followUpToJobId,
|
|
730
|
-
chatUrl: followUp.chatUrl,
|
|
731
|
-
requestSource: "tool",
|
|
732
|
-
},
|
|
733
|
-
ctx.cwd,
|
|
734
|
-
originSessionFile,
|
|
735
|
-
config,
|
|
736
|
-
runtime,
|
|
737
|
-
{ initialState: "submitted", createdAt: admittedAt },
|
|
738
|
-
);
|
|
739
|
-
await rename(tempArchivePath, job.archivePath);
|
|
740
|
-
spawnedWorker = await spawnWorker(workerPath, job.id);
|
|
741
|
-
workerSpawned = true;
|
|
742
|
-
const worker = spawnedWorker;
|
|
743
|
-
job = await updateJob(job.id, (current) => ({
|
|
744
|
-
...current,
|
|
745
|
-
archiveSha256: currentArchive.sha256,
|
|
746
|
-
workerPid: worker.pid,
|
|
747
|
-
workerNonce: worker.nonce,
|
|
748
|
-
workerStartedAt: worker.startedAt,
|
|
749
|
-
}));
|
|
750
|
-
});
|
|
751
|
-
if (!job || !archive) throw new Error(`Oracle submission ${jobId} did not persist job metadata durably`);
|
|
752
|
-
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
753
|
-
|
|
754
|
-
const queuePosition = queued ? getQueuePosition(job.id) : undefined;
|
|
755
|
-
return {
|
|
756
|
-
content: [
|
|
757
|
-
{
|
|
758
|
-
type: "text",
|
|
759
|
-
text: formatOracleSubmitResponse(job, {
|
|
760
|
-
autoPrunedPrefixes: currentArchive.autoPrunedPrefixes,
|
|
761
|
-
queued,
|
|
762
|
-
queuePosition: queuePosition?.position,
|
|
763
|
-
queueDepth: queuePosition?.depth,
|
|
764
|
-
}),
|
|
765
|
-
},
|
|
766
|
-
],
|
|
767
|
-
details: {
|
|
768
|
-
jobId: job.id,
|
|
769
|
-
queued,
|
|
770
|
-
queuePosition: queuePosition?.position,
|
|
771
|
-
queueDepth: queuePosition?.depth,
|
|
772
|
-
archiveSha256: currentArchive.sha256,
|
|
773
|
-
archiveBytes: currentArchive.archiveBytes,
|
|
774
|
-
initialArchiveBytes: currentArchive.initialArchiveBytes,
|
|
775
|
-
autoPrunedArchivePaths: currentArchive.autoPrunedPrefixes,
|
|
776
|
-
runtimeId: job.runtimeId,
|
|
777
|
-
followUpToJobId: followUp.followUpToJobId,
|
|
778
|
-
},
|
|
779
|
-
};
|
|
780
|
-
} catch (error) {
|
|
781
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
782
|
-
const latest = job ? readJob(job.id) : undefined;
|
|
783
|
-
if (latest?.status === "queued" && queuedSubmissionDurable) {
|
|
1204
|
+
});
|
|
1205
|
+
if (!job || !archive) throw new Error(`Oracle submission ${jobId} did not persist job metadata durably`);
|
|
784
1206
|
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
785
|
-
|
|
1207
|
+
|
|
1208
|
+
const queuePosition = queued ? getQueuePosition(job.id) : undefined;
|
|
786
1209
|
return {
|
|
787
1210
|
content: [
|
|
788
1211
|
{
|
|
789
1212
|
type: "text",
|
|
790
|
-
text: formatOracleSubmitResponse(
|
|
791
|
-
autoPrunedPrefixes: archive
|
|
792
|
-
queued
|
|
1213
|
+
text: formatOracleSubmitResponse(job, {
|
|
1214
|
+
autoPrunedPrefixes: archive.autoPrunedPrefixes,
|
|
1215
|
+
queued,
|
|
793
1216
|
queuePosition: queuePosition?.position,
|
|
794
1217
|
queueDepth: queuePosition?.depth,
|
|
795
1218
|
}),
|
|
796
1219
|
},
|
|
797
1220
|
],
|
|
798
1221
|
details: {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
806
|
-
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
807
|
-
runtimeId: latest.runtimeId,
|
|
808
|
-
followUpToJobId: latest.followUpToJobId,
|
|
1222
|
+
job: redactJobDetails(job, {
|
|
1223
|
+
queue: buildOracleQueueSnapshot(job, queuePosition),
|
|
1224
|
+
archiveBytes: archive.archiveBytes,
|
|
1225
|
+
initialArchiveBytes: archive.initialArchiveBytes,
|
|
1226
|
+
autoPrunedArchivePaths: archive.autoPrunedPrefixes,
|
|
1227
|
+
}),
|
|
809
1228
|
},
|
|
810
1229
|
};
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
const message = getErrorMessage(error);
|
|
1232
|
+
const latest = job ? readJob(job.id) : undefined;
|
|
1233
|
+
if (latest?.status === "queued" && queuedSubmissionDurable) {
|
|
1234
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
1235
|
+
const queuePosition = getQueuePosition(latest.id);
|
|
1236
|
+
return {
|
|
1237
|
+
content: [
|
|
1238
|
+
{
|
|
1239
|
+
type: "text",
|
|
1240
|
+
text: formatOracleSubmitResponse(latest, {
|
|
1241
|
+
autoPrunedPrefixes: archive?.autoPrunedPrefixes ?? [],
|
|
1242
|
+
queued: true,
|
|
1243
|
+
queuePosition: queuePosition?.position,
|
|
1244
|
+
queueDepth: queuePosition?.depth,
|
|
1245
|
+
}),
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
details: {
|
|
1249
|
+
job: redactJobDetails(latest, {
|
|
1250
|
+
queue: buildOracleQueueSnapshot(latest, queuePosition),
|
|
1251
|
+
archiveBytes: archive?.archiveBytes,
|
|
1252
|
+
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
1253
|
+
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
821
1254
|
}),
|
|
822
1255
|
},
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (workerSpawned && latest && hasDurableWorkerHandoff(latest)) {
|
|
1259
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
1260
|
+
return {
|
|
1261
|
+
content: [
|
|
1262
|
+
{
|
|
1263
|
+
type: "text",
|
|
1264
|
+
text: formatOracleSubmitResponse(latest, {
|
|
1265
|
+
autoPrunedPrefixes: archive?.autoPrunedPrefixes ?? [],
|
|
1266
|
+
queued: false,
|
|
1267
|
+
}),
|
|
1268
|
+
},
|
|
1269
|
+
],
|
|
1270
|
+
details: {
|
|
1271
|
+
job: redactJobDetails(latest, {
|
|
1272
|
+
queue: buildOracleQueueSnapshot(latest),
|
|
1273
|
+
archiveBytes: archive?.archiveBytes,
|
|
1274
|
+
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
1275
|
+
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
1276
|
+
}),
|
|
1277
|
+
},
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
if (spawnedWorker) {
|
|
1281
|
+
await terminateWorkerPid(spawnedWorker.pid, spawnedWorker.startedAt).catch(() => undefined);
|
|
1282
|
+
}
|
|
1283
|
+
if (job && (!latest || !isTerminalOracleJob(latest))) {
|
|
1284
|
+
const failedAt = new Date().toISOString();
|
|
1285
|
+
await updateJob(job.id, (current) => transitionOracleJobPhase(current, "failed", {
|
|
1286
|
+
at: failedAt,
|
|
1287
|
+
source: "oracle:submit",
|
|
1288
|
+
message: `Submission failed before durable worker handoff: ${message}`,
|
|
1289
|
+
patch: {
|
|
1290
|
+
error: message,
|
|
1291
|
+
},
|
|
1292
|
+
})).catch(() => undefined);
|
|
1293
|
+
}
|
|
1294
|
+
const cleanupReport = await cleanupRuntimeArtifacts({
|
|
1295
|
+
runtimeId: runtimeLeaseAcquired ? runtime.runtimeId : undefined,
|
|
1296
|
+
runtimeProfileDir: runtimeLeaseAcquired ? runtime.runtimeProfileDir : undefined,
|
|
1297
|
+
runtimeSessionName: workerSpawned ? runtime.runtimeSessionName : undefined,
|
|
1298
|
+
conversationId: conversationLeaseAcquired ? followUp.conversationId : undefined,
|
|
1299
|
+
}).catch(() => ({ attempted: [], warnings: [] }));
|
|
1300
|
+
if (job && cleanupReport.warnings.length > 0) {
|
|
1301
|
+
await appendCleanupWarnings(job.id, cleanupReport.warnings).catch(() => undefined);
|
|
1302
|
+
}
|
|
1303
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
1304
|
+
return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>, {
|
|
1305
|
+
job: latest ?? job,
|
|
1306
|
+
jobDetails: {
|
|
1307
|
+
queue: latest ? buildOracleQueueSnapshot(latest, latest.status === "queued" ? getQueuePosition(latest.id) : undefined) : undefined,
|
|
828
1308
|
archiveBytes: archive?.archiveBytes,
|
|
829
1309
|
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
830
1310
|
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
831
|
-
runtimeId: latest.runtimeId,
|
|
832
|
-
followUpToJobId: latest.followUpToJobId,
|
|
833
1311
|
},
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
await terminateWorkerPid(spawnedWorker.pid, spawnedWorker.startedAt).catch(() => undefined);
|
|
838
|
-
}
|
|
839
|
-
if (job && (!latest || !isTerminalOracleJob(latest))) {
|
|
840
|
-
const failedAt = new Date().toISOString();
|
|
841
|
-
await updateJob(job.id, (current) => transitionOracleJobPhase(current, "failed", {
|
|
842
|
-
at: failedAt,
|
|
843
|
-
source: "oracle:submit",
|
|
844
|
-
message: `Submission failed before durable worker handoff: ${message}`,
|
|
845
|
-
patch: {
|
|
846
|
-
error: message,
|
|
847
|
-
},
|
|
848
|
-
})).catch(() => undefined);
|
|
849
|
-
}
|
|
850
|
-
const cleanupReport = await cleanupRuntimeArtifacts({
|
|
851
|
-
runtimeId: runtimeLeaseAcquired ? runtime.runtimeId : undefined,
|
|
852
|
-
runtimeProfileDir: runtimeLeaseAcquired ? runtime.runtimeProfileDir : undefined,
|
|
853
|
-
runtimeSessionName: workerSpawned ? runtime.runtimeSessionName : undefined,
|
|
854
|
-
conversationId: conversationLeaseAcquired ? followUp.conversationId : undefined,
|
|
855
|
-
}).catch(() => ({ attempted: [], warnings: [] }));
|
|
856
|
-
if (job && cleanupReport.warnings.length > 0) {
|
|
857
|
-
await appendCleanupWarnings(job.id, cleanupReport.warnings).catch(() => undefined);
|
|
1312
|
+
});
|
|
1313
|
+
} finally {
|
|
1314
|
+
await rm(tempArchivePath, { force: true }).catch(() => undefined);
|
|
858
1315
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
} finally {
|
|
862
|
-
await rm(tempArchivePath, { force: true }).catch(() => undefined);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>);
|
|
863
1318
|
}
|
|
864
1319
|
},
|
|
865
1320
|
});
|
|
@@ -870,40 +1325,54 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
870
1325
|
description: "Read the status and outputs of a previously dispatched oracle job.",
|
|
871
1326
|
parameters: ORACLE_READ_PARAMS,
|
|
872
1327
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
873
|
-
const job = readJob(params.jobId);
|
|
874
|
-
if (!job || job.projectId !== getProjectId(ctx.cwd)) {
|
|
875
|
-
throw new Error(`Oracle job not found in this project: ${params.jobId}`);
|
|
876
|
-
}
|
|
877
|
-
const latest = isTerminalOracleJob(job)
|
|
878
|
-
? await markWakeupSettled(job.id, {
|
|
879
|
-
source: "oracle_read",
|
|
880
|
-
sessionFile: getSessionFile(ctx),
|
|
881
|
-
cwd: ctx.cwd,
|
|
882
|
-
})
|
|
883
|
-
: job;
|
|
884
|
-
const current = latest ?? readJob(job.id) ?? job;
|
|
885
|
-
|
|
886
|
-
let responsePreview = "";
|
|
887
1328
|
try {
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1329
|
+
const job = readJob(params.jobId);
|
|
1330
|
+
if (!job || job.projectId !== getProjectId(ctx.cwd)) {
|
|
1331
|
+
throw new Error(`Oracle job not found in this project: ${params.jobId}`);
|
|
1332
|
+
}
|
|
1333
|
+
const latest = isTerminalOracleJob(job)
|
|
1334
|
+
? await markWakeupSettled(job.id, {
|
|
1335
|
+
source: "oracle_read",
|
|
1336
|
+
sessionFile: getSessionFile(ctx),
|
|
1337
|
+
cwd: ctx.cwd,
|
|
1338
|
+
})
|
|
1339
|
+
: job;
|
|
1340
|
+
const current = latest ?? readJob(job.id) ?? job;
|
|
1341
|
+
|
|
1342
|
+
let responsePreview: string | undefined;
|
|
1343
|
+
let responseAvailable = false;
|
|
1344
|
+
try {
|
|
1345
|
+
const response = await import("node:fs/promises").then((fs) => fs.readFile(current.responsePath || "", "utf8"));
|
|
1346
|
+
responsePreview = response.slice(0, 4000);
|
|
1347
|
+
responseAvailable = true;
|
|
1348
|
+
} catch {
|
|
1349
|
+
responsePreview = undefined;
|
|
1350
|
+
}
|
|
893
1351
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1352
|
+
const queuePosition = current.status === "queued" ? getQueuePosition(current.id) : undefined;
|
|
1353
|
+
return {
|
|
1354
|
+
content: [
|
|
1355
|
+
{
|
|
1356
|
+
type: "text",
|
|
1357
|
+
text: formatOracleJobSummary(current, {
|
|
1358
|
+
queuePosition,
|
|
1359
|
+
artifactsPath: `${getJobDir(current.id)}/artifacts`,
|
|
1360
|
+
responsePreview,
|
|
1361
|
+
responseAvailable,
|
|
1362
|
+
}),
|
|
1363
|
+
},
|
|
1364
|
+
],
|
|
1365
|
+
details: {
|
|
1366
|
+
job: redactJobDetails(current, {
|
|
1367
|
+
queue: buildOracleQueueSnapshot(current, queuePosition),
|
|
901
1368
|
responsePreview,
|
|
1369
|
+
responseAvailable,
|
|
902
1370
|
}),
|
|
903
1371
|
},
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1372
|
+
};
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
return buildOracleToolErrorResult("oracle_read", error, params as unknown as Record<string, unknown>);
|
|
1375
|
+
}
|
|
907
1376
|
},
|
|
908
1377
|
});
|
|
909
1378
|
|
|
@@ -913,26 +1382,30 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
913
1382
|
description: "Cancel a queued or active oracle job.",
|
|
914
1383
|
parameters: ORACLE_CANCEL_PARAMS,
|
|
915
1384
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1385
|
+
try {
|
|
1386
|
+
const job = readJob(params.jobId);
|
|
1387
|
+
if (!job || job.projectId !== getProjectId(ctx.cwd)) {
|
|
1388
|
+
throw new Error(`Oracle job not found in this project: ${params.jobId}`);
|
|
1389
|
+
}
|
|
1390
|
+
if (!isOpenOracleJob(job)) {
|
|
1391
|
+
return {
|
|
1392
|
+
content: [{ type: "text", text: `Oracle job ${job.id} is not cancellable (${job.status}).` }],
|
|
1393
|
+
details: { job: redactJobDetails(job, { queue: buildOracleQueueSnapshot(job, job.status === "queued" ? getQueuePosition(job.id) : undefined) }) },
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const cancelled = await cancelOracleJob(params.jobId);
|
|
1398
|
+
if (shouldAdvanceQueueAfterCancellation(cancelled)) {
|
|
1399
|
+
await promoteQueuedJobs({ workerPath, source: "oracle_cancel_tool" });
|
|
1400
|
+
}
|
|
1401
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
921
1402
|
return {
|
|
922
|
-
content: [{ type: "text", text: `
|
|
923
|
-
details: { job: redactJobDetails(
|
|
1403
|
+
content: [{ type: "text", text: cancelled.status === "cancelled" || cancelled.status === "failed" ? `Cancelled oracle job ${cancelled.id}.` : `Oracle job ${cancelled.id} was already ${cancelled.status}.` }],
|
|
1404
|
+
details: { job: redactJobDetails(cancelled, { queue: buildOracleQueueSnapshot(cancelled, cancelled.status === "queued" ? getQueuePosition(cancelled.id) : undefined) }) },
|
|
924
1405
|
};
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
return buildOracleToolErrorResult("oracle_cancel", error, params as unknown as Record<string, unknown>);
|
|
925
1408
|
}
|
|
926
|
-
|
|
927
|
-
const cancelled = await cancelOracleJob(params.jobId);
|
|
928
|
-
if (shouldAdvanceQueueAfterCancellation(cancelled)) {
|
|
929
|
-
await promoteQueuedJobs({ workerPath, source: "oracle_cancel_tool" });
|
|
930
|
-
}
|
|
931
|
-
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
932
|
-
return {
|
|
933
|
-
content: [{ type: "text", text: cancelled.status === "cancelled" || cancelled.status === "failed" ? `Cancelled oracle job ${cancelled.id}.` : `Oracle job ${cancelled.id} was already ${cancelled.status}.` }],
|
|
934
|
-
details: { job: redactJobDetails(cancelled) },
|
|
935
|
-
};
|
|
936
1409
|
},
|
|
937
1410
|
});
|
|
938
1411
|
}
|