fix: export latest session task artifacts
This commit is contained in:
parent
1d9887e77b
commit
aa0c2323ea
76
dist/src/exportArtifacts.js
vendored
76
dist/src/exportArtifacts.js
vendored
@ -55,6 +55,7 @@ export async function exportXWorkmateArtifacts(input) {
|
||||
const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0);
|
||||
const includeContent = optionalBoolean(params.includeContent, true);
|
||||
const latestIfEmpty = optionalBoolean(params.latestIfEmpty, false);
|
||||
const latestTaskScopeIfEmpty = optionalBoolean(params.latestTaskScopeIfEmpty, false);
|
||||
const workspaceDir = resolveWorkspaceDir({
|
||||
config: input.config,
|
||||
pluginConfig,
|
||||
@ -76,18 +77,27 @@ export async function exportXWorkmateArtifacts(input) {
|
||||
});
|
||||
if (candidates.length === 0 && latestIfEmpty) {
|
||||
const latestWarnings = [];
|
||||
const latestCandidates = await collectCandidates({
|
||||
scanRoot: workspaceRoot,
|
||||
relativeRoot: workspaceRoot,
|
||||
sinceUnixMs: 0,
|
||||
skipTaskScopeRoot: true,
|
||||
warnings: latestWarnings,
|
||||
});
|
||||
const latestCandidates = latestTaskScopeIfEmpty
|
||||
? await collectLatestSessionTaskCandidates({
|
||||
workspaceRoot,
|
||||
sessionKey,
|
||||
warnings: latestWarnings,
|
||||
})
|
||||
: await collectCandidates({
|
||||
scanRoot: workspaceRoot,
|
||||
relativeRoot: workspaceRoot,
|
||||
sinceUnixMs: 0,
|
||||
skipTaskScopeRoot: true,
|
||||
warnings: latestWarnings,
|
||||
});
|
||||
if (latestCandidates.length > 0) {
|
||||
warnings.push(...latestWarnings);
|
||||
if (scopedExport) {
|
||||
warnings.push("scoped artifact directory is empty; exported latest workspace files instead");
|
||||
}
|
||||
else if (latestTaskScopeIfEmpty) {
|
||||
warnings.push("workspace export is empty; exported latest session task files instead");
|
||||
}
|
||||
candidates = latestCandidates;
|
||||
scopeKind = "workspace-latest";
|
||||
}
|
||||
@ -106,6 +116,8 @@ export async function exportXWorkmateArtifacts(input) {
|
||||
}
|
||||
const bytes = await fs.readFile(candidate.absolutePath);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
const artifactScopeForCandidate = candidate.artifactScope || (scopeKind === "task" && artifactScope ? artifactScope : "");
|
||||
const scopeKindForCandidate = candidate.scopeKind || scopeKind;
|
||||
const artifact = {
|
||||
relativePath: candidate.relativePath,
|
||||
label: path.posix.basename(candidate.relativePath),
|
||||
@ -115,16 +127,18 @@ export async function exportXWorkmateArtifacts(input) {
|
||||
artifactRef: signArtifactRef({
|
||||
v: 1,
|
||||
workspaceRootHash: workspaceRootHash(workspaceRoot),
|
||||
scopeKind,
|
||||
...(scopeKind === "task" && artifactScope ? { artifactScope } : {}),
|
||||
scopeKind: scopeKindForCandidate,
|
||||
...(scopeKindForCandidate === "task" && artifactScopeForCandidate
|
||||
? { artifactScope: artifactScopeForCandidate }
|
||||
: {}),
|
||||
relativePath: candidate.relativePath,
|
||||
sizeBytes: bytes.byteLength,
|
||||
sha256,
|
||||
}, pluginConfig),
|
||||
scopeKind,
|
||||
scopeKind: scopeKindForCandidate,
|
||||
};
|
||||
if (scopeKind === "task" && artifactScope) {
|
||||
artifact.artifactScope = artifactScope;
|
||||
if (scopeKindForCandidate === "task" && artifactScopeForCandidate) {
|
||||
artifact.artifactScope = artifactScopeForCandidate;
|
||||
}
|
||||
if (includeContent && bytes.byteLength <= maxInlineBytes) {
|
||||
artifact.encoding = "base64";
|
||||
@ -333,6 +347,44 @@ async function collectCandidates(input) {
|
||||
}
|
||||
}
|
||||
}
|
||||
async function collectLatestSessionTaskCandidates(input) {
|
||||
const sessionScope = [TASK_SCOPE_ROOT, safeScopeSegment(input.sessionKey)].join("/");
|
||||
const sessionRoot = path.join(input.workspaceRoot, sessionScope.split("/").join(path.sep));
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(sessionRoot, { withFileTypes: true });
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
const candidates = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name === "." || entry.name === "..") {
|
||||
continue;
|
||||
}
|
||||
const artifactScope = [sessionScope, entry.name].join("/");
|
||||
let scopeRoot;
|
||||
try {
|
||||
scopeRoot = resolveScopeRoot(input.workspaceRoot, artifactScope);
|
||||
}
|
||||
catch {
|
||||
continue;
|
||||
}
|
||||
const scopedCandidates = await collectCandidates({
|
||||
scanRoot: scopeRoot,
|
||||
relativeRoot: scopeRoot,
|
||||
sinceUnixMs: 0,
|
||||
skipTaskScopeRoot: false,
|
||||
warnings: input.warnings,
|
||||
});
|
||||
candidates.push(...scopedCandidates.map((candidate) => ({
|
||||
...candidate,
|
||||
artifactScope,
|
||||
scopeKind: "task",
|
||||
})));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
function artifactScopeFor(sessionKey, runId) {
|
||||
return [
|
||||
TASK_SCOPE_ROOT,
|
||||
|
||||
@ -171,6 +171,44 @@ describe("exportXWorkmateArtifacts", () => {
|
||||
expect(result.warnings).toContain("scoped artifact directory is empty; exported latest workspace files instead");
|
||||
});
|
||||
|
||||
it("falls back to latest session task files when requested", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-xworkmate-artifacts-"));
|
||||
const previousTask = await prepareXWorkmateArtifacts({
|
||||
params: { sessionKey: "thread-main", runId: "turn-previous" },
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
await fs.writeFile(path.join(previousTask.artifactDirectory, "k8s-networking.pdf"), "pdf");
|
||||
await fs.writeFile(path.join(previousTask.artifactDirectory, "k8s-networking.docx"), "docx");
|
||||
|
||||
const result = await exportXWorkmateArtifacts({
|
||||
params: {
|
||||
sessionKey: "thread-main",
|
||||
runId: "turn-follow-up",
|
||||
sinceUnixMs: Date.now() + 10_000,
|
||||
latestIfEmpty: true,
|
||||
latestTaskScopeIfEmpty: true,
|
||||
},
|
||||
pluginConfig: { workspaceDir: root },
|
||||
});
|
||||
|
||||
expect(result.scopeKind).toBe("workspace-latest");
|
||||
expect(result.artifactScope).toBeUndefined();
|
||||
expect(result.artifacts.map((entry) => entry.relativePath)).toEqual([
|
||||
"k8s-networking.docx",
|
||||
"k8s-networking.pdf",
|
||||
]);
|
||||
expect(
|
||||
result.artifacts.map((entry) => ({
|
||||
artifactScope: entry.artifactScope,
|
||||
scopeKind: entry.scopeKind,
|
||||
})),
|
||||
).toEqual([
|
||||
{ artifactScope: previousTask.artifactScope, scopeKind: "task" },
|
||||
{ artifactScope: previousTask.artifactScope, scopeKind: "task" },
|
||||
]);
|
||||
expect(result.warnings).toContain("workspace export is empty; exported latest session task files instead");
|
||||
});
|
||||
|
||||
it("leaves oversized artifacts out of inline content", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "tmp-xworkmate-artifacts-"));
|
||||
await fs.writeFile(path.join(root, "large.pdf"), Buffer.from("large-content"));
|
||||
|
||||
@ -87,6 +87,8 @@ type Candidate = {
|
||||
relativePath: string;
|
||||
sizeBytes: number;
|
||||
mtimeMs: number;
|
||||
artifactScope?: string;
|
||||
scopeKind?: XWorkmateArtifactScopeKind;
|
||||
};
|
||||
|
||||
export async function prepareXWorkmateArtifacts(input: ExportInput): Promise<XWorkmateArtifactPrepare> {
|
||||
@ -132,6 +134,7 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
||||
const sinceUnixMs = nonNegativeNumber(params.sinceUnixMs, 0);
|
||||
const includeContent = optionalBoolean(params.includeContent, true);
|
||||
const latestIfEmpty = optionalBoolean(params.latestIfEmpty, false);
|
||||
const latestTaskScopeIfEmpty = optionalBoolean(params.latestTaskScopeIfEmpty, false);
|
||||
const workspaceDir = resolveWorkspaceDir({
|
||||
config: input.config,
|
||||
pluginConfig,
|
||||
@ -154,17 +157,25 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
||||
|
||||
if (candidates.length === 0 && latestIfEmpty) {
|
||||
const latestWarnings: string[] = [];
|
||||
const latestCandidates = await collectCandidates({
|
||||
scanRoot: workspaceRoot,
|
||||
relativeRoot: workspaceRoot,
|
||||
sinceUnixMs: 0,
|
||||
skipTaskScopeRoot: true,
|
||||
warnings: latestWarnings,
|
||||
});
|
||||
const latestCandidates = latestTaskScopeIfEmpty
|
||||
? await collectLatestSessionTaskCandidates({
|
||||
workspaceRoot,
|
||||
sessionKey,
|
||||
warnings: latestWarnings,
|
||||
})
|
||||
: await collectCandidates({
|
||||
scanRoot: workspaceRoot,
|
||||
relativeRoot: workspaceRoot,
|
||||
sinceUnixMs: 0,
|
||||
skipTaskScopeRoot: true,
|
||||
warnings: latestWarnings,
|
||||
});
|
||||
if (latestCandidates.length > 0) {
|
||||
warnings.push(...latestWarnings);
|
||||
if (scopedExport) {
|
||||
warnings.push("scoped artifact directory is empty; exported latest workspace files instead");
|
||||
} else if (latestTaskScopeIfEmpty) {
|
||||
warnings.push("workspace export is empty; exported latest session task files instead");
|
||||
}
|
||||
candidates = latestCandidates;
|
||||
scopeKind = "workspace-latest";
|
||||
@ -186,6 +197,9 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
||||
}
|
||||
const bytes = await fs.readFile(candidate.absolutePath);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
const artifactScopeForCandidate =
|
||||
candidate.artifactScope || (scopeKind === "task" && artifactScope ? artifactScope : "");
|
||||
const scopeKindForCandidate = candidate.scopeKind || scopeKind;
|
||||
const artifact: XWorkmateArtifact = {
|
||||
relativePath: candidate.relativePath,
|
||||
label: path.posix.basename(candidate.relativePath),
|
||||
@ -196,18 +210,20 @@ export async function exportXWorkmateArtifacts(input: ExportInput): Promise<XWor
|
||||
{
|
||||
v: 1,
|
||||
workspaceRootHash: workspaceRootHash(workspaceRoot),
|
||||
scopeKind,
|
||||
...(scopeKind === "task" && artifactScope ? { artifactScope } : {}),
|
||||
scopeKind: scopeKindForCandidate,
|
||||
...(scopeKindForCandidate === "task" && artifactScopeForCandidate
|
||||
? { artifactScope: artifactScopeForCandidate }
|
||||
: {}),
|
||||
relativePath: candidate.relativePath,
|
||||
sizeBytes: bytes.byteLength,
|
||||
sha256,
|
||||
},
|
||||
pluginConfig,
|
||||
),
|
||||
scopeKind,
|
||||
scopeKind: scopeKindForCandidate,
|
||||
};
|
||||
if (scopeKind === "task" && artifactScope) {
|
||||
artifact.artifactScope = artifactScope;
|
||||
if (scopeKindForCandidate === "task" && artifactScopeForCandidate) {
|
||||
artifact.artifactScope = artifactScopeForCandidate;
|
||||
}
|
||||
if (includeContent && bytes.byteLength <= maxInlineBytes) {
|
||||
artifact.encoding = "base64";
|
||||
@ -441,6 +457,49 @@ async function collectCandidates(input: {
|
||||
}
|
||||
}
|
||||
|
||||
async function collectLatestSessionTaskCandidates(input: {
|
||||
workspaceRoot: string;
|
||||
sessionKey: string;
|
||||
warnings: string[];
|
||||
}): Promise<Candidate[]> {
|
||||
const sessionScope = [TASK_SCOPE_ROOT, safeScopeSegment(input.sessionKey)].join("/");
|
||||
const sessionRoot = path.join(input.workspaceRoot, sessionScope.split("/").join(path.sep));
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(sessionRoot, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const candidates: Candidate[] = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name === "." || entry.name === "..") {
|
||||
continue;
|
||||
}
|
||||
const artifactScope = [sessionScope, entry.name].join("/");
|
||||
let scopeRoot: string;
|
||||
try {
|
||||
scopeRoot = resolveScopeRoot(input.workspaceRoot, artifactScope);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const scopedCandidates = await collectCandidates({
|
||||
scanRoot: scopeRoot,
|
||||
relativeRoot: scopeRoot,
|
||||
sinceUnixMs: 0,
|
||||
skipTaskScopeRoot: false,
|
||||
warnings: input.warnings,
|
||||
});
|
||||
candidates.push(
|
||||
...scopedCandidates.map((candidate) => ({
|
||||
...candidate,
|
||||
artifactScope,
|
||||
scopeKind: "task" as const,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function artifactScopeFor(sessionKey: string, runId: string): string {
|
||||
return [
|
||||
TASK_SCOPE_ROOT,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user