pi-oracle 0.4.0 → 0.5.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 +17 -0
- package/README.md +25 -8
- package/docs/ORACLE_DESIGN.md +14 -8
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +28 -1
- package/extensions/oracle/lib/commands.ts +14 -5
- package/extensions/oracle/lib/config.ts +51 -2
- package/extensions/oracle/lib/jobs.ts +17 -2
- package/extensions/oracle/lib/poller.ts +25 -2
- package/extensions/oracle/lib/runtime.ts +42 -5
- package/extensions/oracle/lib/tools.ts +615 -247
- 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 +26 -8
- package/extensions/oracle/worker/auth-bootstrap.mjs +49 -4
- package/package.json +1 -1
- package/prompts/oracle.md +16 -10
|
@@ -7,14 +7,15 @@ 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 type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
11
11
|
import { Type } from "@sinclair/typebox";
|
|
12
12
|
import { formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
|
|
13
|
-
import { transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
|
|
13
|
+
import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent, transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
|
|
14
14
|
import { isLockTimeoutError, withGlobalReconcileLock, withLock } from "./locks.js";
|
|
15
15
|
import {
|
|
16
16
|
coerceOracleSubmitPresetId,
|
|
17
17
|
loadOracleConfig,
|
|
18
|
+
ORACLE_SUBMIT_PRESET_IDS,
|
|
18
19
|
resolveOracleSubmitPreset,
|
|
19
20
|
} from "./config.js";
|
|
20
21
|
import {
|
|
@@ -44,9 +45,11 @@ import { getQueuePosition, promoteQueuedJobs, promoteQueuedJobsWithinAdmissionLo
|
|
|
44
45
|
import { refreshOracleStatus } from "./poller.js";
|
|
45
46
|
import {
|
|
46
47
|
allocateRuntime,
|
|
48
|
+
assertOracleSubmitPrerequisites,
|
|
47
49
|
cleanupRuntimeArtifacts,
|
|
48
50
|
getProjectId,
|
|
49
51
|
getSessionId,
|
|
52
|
+
hasPersistedSessionFile,
|
|
50
53
|
parseConversationId,
|
|
51
54
|
requirePersistedSessionFile,
|
|
52
55
|
tryAcquireConversationLease,
|
|
@@ -565,7 +568,54 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
|
|
|
565
568
|
};
|
|
566
569
|
}
|
|
567
570
|
|
|
568
|
-
|
|
571
|
+
type OracleToolName = "oracle_submit" | "oracle_read" | "oracle_cancel";
|
|
572
|
+
type OracleToolErrorSource = OracleToolName | "oracle_preflight";
|
|
573
|
+
type OracleQueueSnapshot = { queued: boolean; position?: number; depth?: number };
|
|
574
|
+
type OracleToolErrorDetails = {
|
|
575
|
+
code: string;
|
|
576
|
+
message: string;
|
|
577
|
+
rejectedValue?: string;
|
|
578
|
+
allowedValues?: string[];
|
|
579
|
+
suggestedNextStep?: string;
|
|
580
|
+
};
|
|
581
|
+
type OracleToolJobDetailsOptions = {
|
|
582
|
+
queue?: OracleQueueSnapshot;
|
|
583
|
+
archiveBytes?: number;
|
|
584
|
+
initialArchiveBytes?: number;
|
|
585
|
+
autoPrunedArchivePaths?: ArchiveSizeBreakdownRow[];
|
|
586
|
+
responsePreview?: string;
|
|
587
|
+
responseAvailable?: boolean;
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_submit", "oracle_read", "oracle_cancel"]);
|
|
591
|
+
|
|
592
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
593
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
594
|
+
? value as Record<string, unknown>
|
|
595
|
+
: undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getErrorMessage(error: unknown): string {
|
|
599
|
+
return error instanceof Error ? error.message : String(error);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function buildOracleQueueSnapshot(
|
|
603
|
+
job: NonNullable<ReturnType<typeof readJob>>,
|
|
604
|
+
queuePosition?: { position: number; depth: number },
|
|
605
|
+
): OracleQueueSnapshot {
|
|
606
|
+
return {
|
|
607
|
+
queued: job.status === "queued",
|
|
608
|
+
position: queuePosition?.position,
|
|
609
|
+
depth: queuePosition?.depth,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function redactJobDetails(
|
|
614
|
+
job: NonNullable<ReturnType<typeof readJob>>,
|
|
615
|
+
options: OracleToolJobDetailsOptions = {},
|
|
616
|
+
) {
|
|
617
|
+
const lastEvent = getLatestOracleJobLifecycleEvent(job);
|
|
618
|
+
const terminalEvent = getLatestOracleTerminalLifecycleEvent(job);
|
|
569
619
|
return {
|
|
570
620
|
id: job.id,
|
|
571
621
|
status: job.status,
|
|
@@ -576,24 +626,319 @@ function redactJobDetails(job: NonNullable<ReturnType<typeof readJob>>) {
|
|
|
576
626
|
queuedAt: job.queuedAt,
|
|
577
627
|
submittedAt: job.submittedAt,
|
|
578
628
|
completedAt: job.completedAt,
|
|
629
|
+
promptPath: job.promptPath,
|
|
630
|
+
archivePath: job.archivePath,
|
|
631
|
+
archiveSha256: job.archiveSha256,
|
|
632
|
+
archiveBytes: options.archiveBytes,
|
|
633
|
+
initialArchiveBytes: options.initialArchiveBytes,
|
|
634
|
+
autoPrunedArchivePaths: options.autoPrunedArchivePaths ?? [],
|
|
635
|
+
queue: options.queue ?? buildOracleQueueSnapshot(job),
|
|
579
636
|
followUpToJobId: job.followUpToJobId,
|
|
580
637
|
chatUrl: job.chatUrl,
|
|
581
638
|
conversationId: job.conversationId,
|
|
582
639
|
responsePath: job.responsePath,
|
|
583
640
|
responseFormat: job.responseFormat,
|
|
641
|
+
responseAvailable: options.responseAvailable ?? false,
|
|
642
|
+
responsePreview: options.responsePreview,
|
|
643
|
+
artifactsPath: `${getJobDir(job.id)}/artifacts`,
|
|
584
644
|
artifactPaths: job.artifactPaths,
|
|
585
645
|
artifactFailureCount: job.artifactFailureCount,
|
|
586
646
|
artifactsManifestPath: job.artifactsManifestPath,
|
|
647
|
+
workerLogPath: job.workerLogPath,
|
|
587
648
|
archiveDeletedAfterUpload: job.archiveDeletedAfterUpload,
|
|
588
649
|
runtimeId: job.runtimeId,
|
|
589
650
|
cleanupWarnings: job.cleanupWarnings,
|
|
590
651
|
lastCleanupAt: job.lastCleanupAt,
|
|
652
|
+
terminalEvent: terminalEvent ? { ...terminalEvent } : undefined,
|
|
653
|
+
lastEvent: lastEvent ? { ...lastEvent } : undefined,
|
|
591
654
|
error: job.error,
|
|
592
655
|
lifecycleEvents: job.lifecycleEvents,
|
|
593
656
|
};
|
|
594
657
|
}
|
|
595
658
|
|
|
659
|
+
function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unknown, params: Record<string, unknown>): OracleToolErrorDetails {
|
|
660
|
+
const message = getErrorMessage(error);
|
|
661
|
+
|
|
662
|
+
if (toolName === "oracle_submit" && typeof params.preset === "string" && message.startsWith("Unknown oracle_submit preset:")) {
|
|
663
|
+
return {
|
|
664
|
+
code: "invalid_preset",
|
|
665
|
+
message,
|
|
666
|
+
rejectedValue: params.preset,
|
|
667
|
+
allowedValues: [...ORACLE_SUBMIT_PRESET_IDS],
|
|
668
|
+
suggestedNextStep: "Retry with one of the canonical preset ids, or omit preset to use the configured default.",
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (message.startsWith("Oracle requires a persisted pi session")) {
|
|
673
|
+
return {
|
|
674
|
+
code: "persisted_session_required",
|
|
675
|
+
message,
|
|
676
|
+
suggestedNextStep: "Start or save a persisted pi session, then retry oracle_submit.",
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (message.startsWith("Oracle auth seed profile not found: ")) {
|
|
681
|
+
return {
|
|
682
|
+
code: "auth_seed_profile_missing",
|
|
683
|
+
message,
|
|
684
|
+
rejectedValue: message.replace(/^Oracle auth seed profile not found: /, "").replace(/\. Run \/oracle-auth first\.$/, ""),
|
|
685
|
+
suggestedNextStep: "Run /oracle-auth first, then retry the oracle tool call.",
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (message.startsWith("Oracle auth seed profile is not readable: ")) {
|
|
690
|
+
return {
|
|
691
|
+
code: "auth_seed_profile_unreadable",
|
|
692
|
+
message,
|
|
693
|
+
rejectedValue: message.replace(/^Oracle auth seed profile is not readable: /, "").replace(/\. Fix its permissions or rerun \/oracle-auth\.$/, ""),
|
|
694
|
+
suggestedNextStep: "Fix the auth seed profile permissions or rerun /oracle-auth, then retry.",
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (message.startsWith("Oracle auth seed profile is not a directory: ")) {
|
|
699
|
+
return {
|
|
700
|
+
code: "auth_seed_profile_invalid_type",
|
|
701
|
+
message,
|
|
702
|
+
rejectedValue: message.replace(/^Oracle auth seed profile is not a directory: /, "").replace(/\. Remove the invalid path or rerun \/oracle-auth\.$/, ""),
|
|
703
|
+
suggestedNextStep: "Remove the invalid auth seed path or rerun /oracle-auth to recreate it.",
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (message.startsWith("Failed to parse oracle config ") || message.startsWith("Invalid oracle config:") || message.startsWith("Invalid oracle project config:")) {
|
|
708
|
+
return {
|
|
709
|
+
code: "oracle_config_invalid",
|
|
710
|
+
message,
|
|
711
|
+
suggestedNextStep: "Fix the oracle config and retry once the configured paths and values are valid.",
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (toolName === "oracle_submit" && message === "oracle_submit requires at least one file or directory to archive") {
|
|
716
|
+
return {
|
|
717
|
+
code: "archive_input_required",
|
|
718
|
+
message,
|
|
719
|
+
suggestedNextStep: "Pass at least one project-relative file or directory in files.",
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (toolName === "oracle_submit" && message.startsWith("Archive input does not exist: ")) {
|
|
724
|
+
return {
|
|
725
|
+
code: "archive_input_missing",
|
|
726
|
+
message,
|
|
727
|
+
rejectedValue: message.replace(/^Archive input does not exist: /, ""),
|
|
728
|
+
suggestedNextStep: "Retry with an existing project-relative file or directory.",
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (toolName === "oracle_submit" && message.startsWith("Archive input must be inside the project cwd: ")) {
|
|
733
|
+
return {
|
|
734
|
+
code: "archive_input_outside_project",
|
|
735
|
+
message,
|
|
736
|
+
rejectedValue: message.replace(/^Archive input must be inside the project cwd: /, ""),
|
|
737
|
+
suggestedNextStep: "Retry with a path inside the current project cwd.",
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (toolName === "oracle_submit" && message.startsWith("Archive input must resolve inside the project cwd without symlink escapes: ")) {
|
|
742
|
+
return {
|
|
743
|
+
code: "archive_input_symlink_escape",
|
|
744
|
+
message,
|
|
745
|
+
rejectedValue: message.replace(/^Archive input must resolve inside the project cwd without symlink escapes: /, ""),
|
|
746
|
+
suggestedNextStep: "Retry with a real project-relative path that does not escape the repo through symlinks.",
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (toolName === "oracle_submit" && message.startsWith("Follow-up oracle job not found: ")) {
|
|
751
|
+
return {
|
|
752
|
+
code: "follow_up_job_not_found",
|
|
753
|
+
message,
|
|
754
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
755
|
+
suggestedNextStep: "Retry with a completed oracle job id from this project that has a persisted chat URL.",
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (toolName === "oracle_submit" && message.includes("belongs to a different project")) {
|
|
760
|
+
return {
|
|
761
|
+
code: "follow_up_job_wrong_project",
|
|
762
|
+
message,
|
|
763
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
764
|
+
suggestedNextStep: "Retry with a follow-up job id from the current project.",
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (toolName === "oracle_submit" && message.includes("is not complete")) {
|
|
769
|
+
return {
|
|
770
|
+
code: "follow_up_job_not_complete",
|
|
771
|
+
message,
|
|
772
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
773
|
+
suggestedNextStep: "Wait for the earlier oracle job to finish, then retry the follow-up.",
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (toolName === "oracle_submit" && message.includes("has no persisted chat URL")) {
|
|
778
|
+
return {
|
|
779
|
+
code: "follow_up_job_missing_chat_url",
|
|
780
|
+
message,
|
|
781
|
+
rejectedValue: typeof params.followUpJobId === "string" ? params.followUpJobId : undefined,
|
|
782
|
+
suggestedNextStep: "Retry with an earlier completed oracle job that recorded a chat URL.",
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if ((toolName === "oracle_read" || toolName === "oracle_cancel") && typeof params.jobId === "string" && message.startsWith("Oracle job not found in this project:")) {
|
|
787
|
+
return {
|
|
788
|
+
code: "job_not_found",
|
|
789
|
+
message,
|
|
790
|
+
rejectedValue: params.jobId,
|
|
791
|
+
suggestedNextStep: "Use /oracle-status to discover a valid job id for this project, then retry.",
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (toolName === "oracle_submit" && message.startsWith("Oracle archive exceeds ChatGPT upload limit")) {
|
|
796
|
+
return {
|
|
797
|
+
code: "archive_too_large",
|
|
798
|
+
message,
|
|
799
|
+
suggestedNextStep: "Retry with a narrower archive, starting with modified files plus adjacent files plus directly relevant subtrees.",
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
code: `${toolName}_failed`,
|
|
805
|
+
message,
|
|
806
|
+
suggestedNextStep: "Inspect the error message, correct the inputs or environment, and retry.",
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function buildOracleToolErrorResult(
|
|
811
|
+
toolName: OracleToolName,
|
|
812
|
+
error: unknown,
|
|
813
|
+
params: Record<string, unknown>,
|
|
814
|
+
options?: { job?: NonNullable<ReturnType<typeof readJob>>; jobDetails?: OracleToolJobDetailsOptions },
|
|
815
|
+
) {
|
|
816
|
+
const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
|
|
817
|
+
return {
|
|
818
|
+
content: [{ type: "text" as const, text: errorDetails.message }],
|
|
819
|
+
details: {
|
|
820
|
+
job: options?.job ? redactJobDetails(options.job, options.jobDetails) : undefined,
|
|
821
|
+
error: errorDetails,
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
type OraclePreflightDetails = {
|
|
827
|
+
ready: boolean;
|
|
828
|
+
session: {
|
|
829
|
+
persisted: boolean;
|
|
830
|
+
sessionFile?: string;
|
|
831
|
+
};
|
|
832
|
+
config: {
|
|
833
|
+
ready: boolean;
|
|
834
|
+
};
|
|
835
|
+
auth: {
|
|
836
|
+
ready: boolean;
|
|
837
|
+
seedProfileDir?: string;
|
|
838
|
+
};
|
|
839
|
+
error?: OracleToolErrorDetails;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
function formatOraclePreflightResponse(details: OraclePreflightDetails): string {
|
|
843
|
+
if (details.ready) {
|
|
844
|
+
return [
|
|
845
|
+
"Oracle preflight ready.",
|
|
846
|
+
details.session.sessionFile ? `Persisted session: ${details.session.sessionFile}` : undefined,
|
|
847
|
+
details.auth.seedProfileDir ? `Auth seed profile: ${details.auth.seedProfileDir}` : undefined,
|
|
848
|
+
"You can continue with oracle context gathering and submission.",
|
|
849
|
+
].filter(Boolean).join("\n");
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return [
|
|
853
|
+
`Oracle preflight blocked: ${details.error?.message ?? "unknown blocker"}`,
|
|
854
|
+
details.error?.suggestedNextStep ? `Suggested next step: ${details.error.suggestedNextStep}` : undefined,
|
|
855
|
+
].filter(Boolean).join("\n");
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePreflightDetails> {
|
|
859
|
+
const sessionFile = getSessionFile(ctx);
|
|
860
|
+
if (!hasPersistedSessionFile(sessionFile)) {
|
|
861
|
+
return {
|
|
862
|
+
ready: false,
|
|
863
|
+
session: { persisted: false },
|
|
864
|
+
config: { ready: false },
|
|
865
|
+
auth: { ready: false },
|
|
866
|
+
error: buildOracleToolErrorDetails(
|
|
867
|
+
"oracle_preflight",
|
|
868
|
+
new Error("Oracle requires a persisted pi session to submit oracle jobs. Start or save a real session before using oracle."),
|
|
869
|
+
{},
|
|
870
|
+
),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
let config;
|
|
875
|
+
try {
|
|
876
|
+
config = loadOracleConfig(ctx.cwd);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
return {
|
|
879
|
+
ready: false,
|
|
880
|
+
session: { persisted: true, sessionFile },
|
|
881
|
+
config: { ready: false },
|
|
882
|
+
auth: { ready: false },
|
|
883
|
+
error: buildOracleToolErrorDetails("oracle_preflight", error, {}),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
await assertOracleSubmitPrerequisites(config);
|
|
889
|
+
} catch (error) {
|
|
890
|
+
return {
|
|
891
|
+
ready: false,
|
|
892
|
+
session: { persisted: true, sessionFile },
|
|
893
|
+
config: { ready: true },
|
|
894
|
+
auth: {
|
|
895
|
+
ready: false,
|
|
896
|
+
seedProfileDir: config.browser.authSeedProfileDir,
|
|
897
|
+
},
|
|
898
|
+
error: buildOracleToolErrorDetails("oracle_preflight", error, {}),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
ready: true,
|
|
904
|
+
session: { persisted: true, sessionFile },
|
|
905
|
+
config: { ready: true },
|
|
906
|
+
auth: {
|
|
907
|
+
ready: true,
|
|
908
|
+
seedProfileDir: config.browser.authSeedProfileDir,
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
596
913
|
export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void {
|
|
914
|
+
pi.on("tool_result", async (event) => {
|
|
915
|
+
if (!ORACLE_TOOL_NAMES.has(event.toolName as OracleToolName)) return;
|
|
916
|
+
if (event.isError) return;
|
|
917
|
+
const details = asRecord(event.details);
|
|
918
|
+
const errorDetails = asRecord(details?.error);
|
|
919
|
+
if (typeof errorDetails?.code === "string" && typeof errorDetails?.message === "string") {
|
|
920
|
+
return { isError: true };
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
pi.registerTool({
|
|
925
|
+
name: "oracle_preflight",
|
|
926
|
+
label: "Oracle Preflight",
|
|
927
|
+
description: "Check whether oracle is ready in this session before spending time gathering context or preparing a submission.",
|
|
928
|
+
promptSnippet: "Check oracle readiness before expensive /oracle preparation.",
|
|
929
|
+
promptGuidelines: [
|
|
930
|
+
"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.",
|
|
931
|
+
],
|
|
932
|
+
parameters: Type.Object({}),
|
|
933
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
934
|
+
const details = await runOraclePreflight(ctx);
|
|
935
|
+
return {
|
|
936
|
+
content: [{ type: "text" as const, text: formatOraclePreflightResponse(details) }],
|
|
937
|
+
details,
|
|
938
|
+
};
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
|
|
597
942
|
pi.registerTool({
|
|
598
943
|
name: "oracle_submit",
|
|
599
944
|
label: "Oracle Submit",
|
|
@@ -615,68 +960,115 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
615
960
|
],
|
|
616
961
|
parameters: ORACLE_SUBMIT_PARAMS,
|
|
617
962
|
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
963
|
try {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
964
|
+
const config = loadOracleConfig(ctx.cwd);
|
|
965
|
+
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
966
|
+
const projectId = getProjectId(ctx.cwd);
|
|
967
|
+
const sessionId = getSessionId(originSessionFile, projectId);
|
|
968
|
+
const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
|
|
969
|
+
const selection = resolveOracleSubmitPreset(presetId);
|
|
970
|
+
const followUp = resolveFollowUp(params.followUpJobId, ctx.cwd);
|
|
971
|
+
// Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
|
|
972
|
+
resolveArchiveInputs(ctx.cwd, params.files);
|
|
973
|
+
await assertOracleSubmitPrerequisites(config);
|
|
974
|
+
try {
|
|
975
|
+
await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: ctx.cwd }, async () => {
|
|
976
|
+
await reconcileStaleOracleJobs();
|
|
977
|
+
await pruneTerminalOracleJobs();
|
|
978
|
+
});
|
|
979
|
+
} catch (error) {
|
|
980
|
+
if (!isLockTimeoutError(error, "reconcile", "global")) throw error;
|
|
981
|
+
}
|
|
633
982
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
983
|
+
const jobId = randomUUID();
|
|
984
|
+
const tempArchivePath = join(tmpdir(), `oracle-archive-${jobId}.tar.zst`);
|
|
985
|
+
const runtime = allocateRuntime(config);
|
|
986
|
+
let job: OracleJob | undefined;
|
|
987
|
+
let archive: ArchiveCreationResult | undefined;
|
|
988
|
+
let queued = false;
|
|
989
|
+
let queuedSubmissionDurable = false;
|
|
990
|
+
let runtimeLeaseAcquired = false;
|
|
991
|
+
let conversationLeaseAcquired = false;
|
|
992
|
+
let workerSpawned = false;
|
|
993
|
+
let spawnedWorker: Awaited<ReturnType<typeof spawnWorker>> | undefined;
|
|
645
994
|
|
|
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
|
-
});
|
|
995
|
+
try {
|
|
996
|
+
archive = await createArchive(ctx.cwd, params.files, tempArchivePath);
|
|
997
|
+
const currentArchive = archive;
|
|
998
|
+
await withLock("admission", "global", { jobId, processPid: process.pid }, async () => {
|
|
999
|
+
await promoteQueuedJobsWithinAdmissionLock({ workerPath, source: "oracle_submit" });
|
|
662
1000
|
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
maxQueuedJobs,
|
|
673
|
-
maxQueuedArchiveBytes,
|
|
1001
|
+
const admittedAt = new Date().toISOString();
|
|
1002
|
+
const runtimeAttempt = await tryAcquireRuntimeLease(config, {
|
|
1003
|
+
jobId,
|
|
1004
|
+
runtimeId: runtime.runtimeId,
|
|
1005
|
+
runtimeSessionName: runtime.runtimeSessionName,
|
|
1006
|
+
runtimeProfileDir: runtime.runtimeProfileDir,
|
|
1007
|
+
projectId,
|
|
1008
|
+
sessionId,
|
|
1009
|
+
createdAt: admittedAt,
|
|
674
1010
|
});
|
|
675
|
-
|
|
676
|
-
|
|
1011
|
+
|
|
1012
|
+
if (!runtimeAttempt.acquired) {
|
|
1013
|
+
const queuePressure = await getQueuedArchivePressure();
|
|
1014
|
+
const maxQueuedJobs = config.browser.maxConcurrentJobs * MAX_QUEUED_JOBS_PER_ACTIVE_RUNTIME;
|
|
1015
|
+
const maxQueuedArchiveBytes = config.browser.maxConcurrentJobs * MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME;
|
|
1016
|
+
const queueAdmissionFailure = getQueueAdmissionFailure({
|
|
1017
|
+
queuePressure,
|
|
1018
|
+
archiveBytes: currentArchive.archiveBytes,
|
|
1019
|
+
activeJobs: runtimeAttempt.liveLeases.length,
|
|
1020
|
+
maxActiveJobs: config.browser.maxConcurrentJobs,
|
|
1021
|
+
maxQueuedJobs,
|
|
1022
|
+
maxQueuedArchiveBytes,
|
|
1023
|
+
});
|
|
1024
|
+
if (queueAdmissionFailure) {
|
|
1025
|
+
throw new Error(queueAdmissionFailure);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
queued = true;
|
|
1029
|
+
job = await createJob(
|
|
1030
|
+
jobId,
|
|
1031
|
+
{
|
|
1032
|
+
prompt: params.prompt,
|
|
1033
|
+
files: params.files,
|
|
1034
|
+
selection,
|
|
1035
|
+
followUpToJobId: followUp.followUpToJobId,
|
|
1036
|
+
chatUrl: followUp.chatUrl,
|
|
1037
|
+
requestSource: "tool",
|
|
1038
|
+
},
|
|
1039
|
+
ctx.cwd,
|
|
1040
|
+
originSessionFile,
|
|
1041
|
+
config,
|
|
1042
|
+
runtime,
|
|
1043
|
+
{ initialState: "queued", createdAt: admittedAt },
|
|
1044
|
+
);
|
|
1045
|
+
await rename(tempArchivePath, job.archivePath);
|
|
1046
|
+
job = await updateJob(job.id, (current) => ({
|
|
1047
|
+
...current,
|
|
1048
|
+
archiveSha256: currentArchive.sha256,
|
|
1049
|
+
}));
|
|
1050
|
+
queuedSubmissionDurable = true;
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
runtimeLeaseAcquired = true;
|
|
1055
|
+
if (followUp.conversationId) {
|
|
1056
|
+
const conversationAttempt = await tryAcquireConversationLease({
|
|
1057
|
+
jobId,
|
|
1058
|
+
conversationId: followUp.conversationId,
|
|
1059
|
+
projectId,
|
|
1060
|
+
sessionId,
|
|
1061
|
+
createdAt: admittedAt,
|
|
1062
|
+
});
|
|
1063
|
+
if (!conversationAttempt.acquired) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
`Oracle conversation ${followUp.conversationId} is already in use by job ${conversationAttempt.blocker?.jobId ?? "unknown"}. ` +
|
|
1066
|
+
"Concurrent follow-ups to the same ChatGPT thread are not allowed.",
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
conversationLeaseAcquired = true;
|
|
677
1070
|
}
|
|
678
1071
|
|
|
679
|
-
queued = true;
|
|
680
1072
|
job = await createJob(
|
|
681
1073
|
jobId,
|
|
682
1074
|
{
|
|
@@ -691,175 +1083,133 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
691
1083
|
originSessionFile,
|
|
692
1084
|
config,
|
|
693
1085
|
runtime,
|
|
694
|
-
{ initialState: "
|
|
1086
|
+
{ initialState: "submitted", createdAt: admittedAt },
|
|
695
1087
|
);
|
|
696
1088
|
await rename(tempArchivePath, job.archivePath);
|
|
1089
|
+
spawnedWorker = await spawnWorker(workerPath, job.id);
|
|
1090
|
+
workerSpawned = true;
|
|
1091
|
+
const worker = spawnedWorker;
|
|
697
1092
|
job = await updateJob(job.id, (current) => ({
|
|
698
1093
|
...current,
|
|
699
1094
|
archiveSha256: currentArchive.sha256,
|
|
1095
|
+
workerPid: worker.pid,
|
|
1096
|
+
workerNonce: worker.nonce,
|
|
1097
|
+
workerStartedAt: worker.startedAt,
|
|
700
1098
|
}));
|
|
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) {
|
|
1099
|
+
});
|
|
1100
|
+
if (!job || !archive) throw new Error(`Oracle submission ${jobId} did not persist job metadata durably`);
|
|
784
1101
|
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
785
|
-
|
|
1102
|
+
|
|
1103
|
+
const queuePosition = queued ? getQueuePosition(job.id) : undefined;
|
|
786
1104
|
return {
|
|
787
1105
|
content: [
|
|
788
1106
|
{
|
|
789
1107
|
type: "text",
|
|
790
|
-
text: formatOracleSubmitResponse(
|
|
791
|
-
autoPrunedPrefixes: archive
|
|
792
|
-
queued
|
|
1108
|
+
text: formatOracleSubmitResponse(job, {
|
|
1109
|
+
autoPrunedPrefixes: archive.autoPrunedPrefixes,
|
|
1110
|
+
queued,
|
|
793
1111
|
queuePosition: queuePosition?.position,
|
|
794
1112
|
queueDepth: queuePosition?.depth,
|
|
795
1113
|
}),
|
|
796
1114
|
},
|
|
797
1115
|
],
|
|
798
1116
|
details: {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
806
|
-
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
807
|
-
runtimeId: latest.runtimeId,
|
|
808
|
-
followUpToJobId: latest.followUpToJobId,
|
|
1117
|
+
job: redactJobDetails(job, {
|
|
1118
|
+
queue: buildOracleQueueSnapshot(job, queuePosition),
|
|
1119
|
+
archiveBytes: archive.archiveBytes,
|
|
1120
|
+
initialArchiveBytes: archive.initialArchiveBytes,
|
|
1121
|
+
autoPrunedArchivePaths: archive.autoPrunedPrefixes,
|
|
1122
|
+
}),
|
|
809
1123
|
},
|
|
810
1124
|
};
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
const message = getErrorMessage(error);
|
|
1127
|
+
const latest = job ? readJob(job.id) : undefined;
|
|
1128
|
+
if (latest?.status === "queued" && queuedSubmissionDurable) {
|
|
1129
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
1130
|
+
const queuePosition = getQueuePosition(latest.id);
|
|
1131
|
+
return {
|
|
1132
|
+
content: [
|
|
1133
|
+
{
|
|
1134
|
+
type: "text",
|
|
1135
|
+
text: formatOracleSubmitResponse(latest, {
|
|
1136
|
+
autoPrunedPrefixes: archive?.autoPrunedPrefixes ?? [],
|
|
1137
|
+
queued: true,
|
|
1138
|
+
queuePosition: queuePosition?.position,
|
|
1139
|
+
queueDepth: queuePosition?.depth,
|
|
1140
|
+
}),
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
details: {
|
|
1144
|
+
job: redactJobDetails(latest, {
|
|
1145
|
+
queue: buildOracleQueueSnapshot(latest, queuePosition),
|
|
1146
|
+
archiveBytes: archive?.archiveBytes,
|
|
1147
|
+
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
1148
|
+
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
821
1149
|
}),
|
|
822
1150
|
},
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
if (workerSpawned && latest && hasDurableWorkerHandoff(latest)) {
|
|
1154
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
1155
|
+
return {
|
|
1156
|
+
content: [
|
|
1157
|
+
{
|
|
1158
|
+
type: "text",
|
|
1159
|
+
text: formatOracleSubmitResponse(latest, {
|
|
1160
|
+
autoPrunedPrefixes: archive?.autoPrunedPrefixes ?? [],
|
|
1161
|
+
queued: false,
|
|
1162
|
+
}),
|
|
1163
|
+
},
|
|
1164
|
+
],
|
|
1165
|
+
details: {
|
|
1166
|
+
job: redactJobDetails(latest, {
|
|
1167
|
+
queue: buildOracleQueueSnapshot(latest),
|
|
1168
|
+
archiveBytes: archive?.archiveBytes,
|
|
1169
|
+
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
1170
|
+
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
1171
|
+
}),
|
|
1172
|
+
},
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
if (spawnedWorker) {
|
|
1176
|
+
await terminateWorkerPid(spawnedWorker.pid, spawnedWorker.startedAt).catch(() => undefined);
|
|
1177
|
+
}
|
|
1178
|
+
if (job && (!latest || !isTerminalOracleJob(latest))) {
|
|
1179
|
+
const failedAt = new Date().toISOString();
|
|
1180
|
+
await updateJob(job.id, (current) => transitionOracleJobPhase(current, "failed", {
|
|
1181
|
+
at: failedAt,
|
|
1182
|
+
source: "oracle:submit",
|
|
1183
|
+
message: `Submission failed before durable worker handoff: ${message}`,
|
|
1184
|
+
patch: {
|
|
1185
|
+
error: message,
|
|
1186
|
+
},
|
|
1187
|
+
})).catch(() => undefined);
|
|
1188
|
+
}
|
|
1189
|
+
const cleanupReport = await cleanupRuntimeArtifacts({
|
|
1190
|
+
runtimeId: runtimeLeaseAcquired ? runtime.runtimeId : undefined,
|
|
1191
|
+
runtimeProfileDir: runtimeLeaseAcquired ? runtime.runtimeProfileDir : undefined,
|
|
1192
|
+
runtimeSessionName: workerSpawned ? runtime.runtimeSessionName : undefined,
|
|
1193
|
+
conversationId: conversationLeaseAcquired ? followUp.conversationId : undefined,
|
|
1194
|
+
}).catch(() => ({ attempted: [], warnings: [] }));
|
|
1195
|
+
if (job && cleanupReport.warnings.length > 0) {
|
|
1196
|
+
await appendCleanupWarnings(job.id, cleanupReport.warnings).catch(() => undefined);
|
|
1197
|
+
}
|
|
1198
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
1199
|
+
return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>, {
|
|
1200
|
+
job: latest ?? job,
|
|
1201
|
+
jobDetails: {
|
|
1202
|
+
queue: latest ? buildOracleQueueSnapshot(latest, latest.status === "queued" ? getQueuePosition(latest.id) : undefined) : undefined,
|
|
828
1203
|
archiveBytes: archive?.archiveBytes,
|
|
829
1204
|
initialArchiveBytes: archive?.initialArchiveBytes,
|
|
830
1205
|
autoPrunedArchivePaths: archive?.autoPrunedPrefixes,
|
|
831
|
-
runtimeId: latest.runtimeId,
|
|
832
|
-
followUpToJobId: latest.followUpToJobId,
|
|
833
1206
|
},
|
|
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);
|
|
1207
|
+
});
|
|
1208
|
+
} finally {
|
|
1209
|
+
await rm(tempArchivePath, { force: true }).catch(() => undefined);
|
|
858
1210
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
} finally {
|
|
862
|
-
await rm(tempArchivePath, { force: true }).catch(() => undefined);
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>);
|
|
863
1213
|
}
|
|
864
1214
|
},
|
|
865
1215
|
});
|
|
@@ -870,40 +1220,54 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
870
1220
|
description: "Read the status and outputs of a previously dispatched oracle job.",
|
|
871
1221
|
parameters: ORACLE_READ_PARAMS,
|
|
872
1222
|
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
1223
|
try {
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1224
|
+
const job = readJob(params.jobId);
|
|
1225
|
+
if (!job || job.projectId !== getProjectId(ctx.cwd)) {
|
|
1226
|
+
throw new Error(`Oracle job not found in this project: ${params.jobId}`);
|
|
1227
|
+
}
|
|
1228
|
+
const latest = isTerminalOracleJob(job)
|
|
1229
|
+
? await markWakeupSettled(job.id, {
|
|
1230
|
+
source: "oracle_read",
|
|
1231
|
+
sessionFile: getSessionFile(ctx),
|
|
1232
|
+
cwd: ctx.cwd,
|
|
1233
|
+
})
|
|
1234
|
+
: job;
|
|
1235
|
+
const current = latest ?? readJob(job.id) ?? job;
|
|
1236
|
+
|
|
1237
|
+
let responsePreview: string | undefined;
|
|
1238
|
+
let responseAvailable = false;
|
|
1239
|
+
try {
|
|
1240
|
+
const response = await import("node:fs/promises").then((fs) => fs.readFile(current.responsePath || "", "utf8"));
|
|
1241
|
+
responsePreview = response.slice(0, 4000);
|
|
1242
|
+
responseAvailable = true;
|
|
1243
|
+
} catch {
|
|
1244
|
+
responsePreview = undefined;
|
|
1245
|
+
}
|
|
893
1246
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1247
|
+
const queuePosition = current.status === "queued" ? getQueuePosition(current.id) : undefined;
|
|
1248
|
+
return {
|
|
1249
|
+
content: [
|
|
1250
|
+
{
|
|
1251
|
+
type: "text",
|
|
1252
|
+
text: formatOracleJobSummary(current, {
|
|
1253
|
+
queuePosition,
|
|
1254
|
+
artifactsPath: `${getJobDir(current.id)}/artifacts`,
|
|
1255
|
+
responsePreview,
|
|
1256
|
+
responseAvailable,
|
|
1257
|
+
}),
|
|
1258
|
+
},
|
|
1259
|
+
],
|
|
1260
|
+
details: {
|
|
1261
|
+
job: redactJobDetails(current, {
|
|
1262
|
+
queue: buildOracleQueueSnapshot(current, queuePosition),
|
|
901
1263
|
responsePreview,
|
|
1264
|
+
responseAvailable,
|
|
902
1265
|
}),
|
|
903
1266
|
},
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1267
|
+
};
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
return buildOracleToolErrorResult("oracle_read", error, params as unknown as Record<string, unknown>);
|
|
1270
|
+
}
|
|
907
1271
|
},
|
|
908
1272
|
});
|
|
909
1273
|
|
|
@@ -913,26 +1277,30 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
913
1277
|
description: "Cancel a queued or active oracle job.",
|
|
914
1278
|
parameters: ORACLE_CANCEL_PARAMS,
|
|
915
1279
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1280
|
+
try {
|
|
1281
|
+
const job = readJob(params.jobId);
|
|
1282
|
+
if (!job || job.projectId !== getProjectId(ctx.cwd)) {
|
|
1283
|
+
throw new Error(`Oracle job not found in this project: ${params.jobId}`);
|
|
1284
|
+
}
|
|
1285
|
+
if (!isOpenOracleJob(job)) {
|
|
1286
|
+
return {
|
|
1287
|
+
content: [{ type: "text", text: `Oracle job ${job.id} is not cancellable (${job.status}).` }],
|
|
1288
|
+
details: { job: redactJobDetails(job, { queue: buildOracleQueueSnapshot(job, job.status === "queued" ? getQueuePosition(job.id) : undefined) }) },
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const cancelled = await cancelOracleJob(params.jobId);
|
|
1293
|
+
if (shouldAdvanceQueueAfterCancellation(cancelled)) {
|
|
1294
|
+
await promoteQueuedJobs({ workerPath, source: "oracle_cancel_tool" });
|
|
1295
|
+
}
|
|
1296
|
+
if (ctx.hasUI) refreshOracleStatus(ctx);
|
|
921
1297
|
return {
|
|
922
|
-
content: [{ type: "text", text: `
|
|
923
|
-
details: { job: redactJobDetails(
|
|
1298
|
+
content: [{ type: "text", text: cancelled.status === "cancelled" || cancelled.status === "failed" ? `Cancelled oracle job ${cancelled.id}.` : `Oracle job ${cancelled.id} was already ${cancelled.status}.` }],
|
|
1299
|
+
details: { job: redactJobDetails(cancelled, { queue: buildOracleQueueSnapshot(cancelled, cancelled.status === "queued" ? getQueuePosition(cancelled.id) : undefined) }) },
|
|
924
1300
|
};
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
return buildOracleToolErrorResult("oracle_cancel", error, params as unknown as Record<string, unknown>);
|
|
925
1303
|
}
|
|
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
1304
|
},
|
|
937
1305
|
});
|
|
938
1306
|
}
|