diff --git a/packages/adapter-utils/src/command-managed-runtime.ts b/packages/adapter-utils/src/command-managed-runtime.ts index dbcd5ab928c..d9a582df353 100644 --- a/packages/adapter-utils/src/command-managed-runtime.ts +++ b/packages/adapter-utils/src/command-managed-runtime.ts @@ -8,6 +8,7 @@ import { } from "./sandbox-managed-runtime.js"; import { preferredShellForSandbox, shellCommandArgs } from "./sandbox-shell.js"; import type { RunProcessResult } from "./server-utils.js"; +import type { RuntimeProgressSink } from "./runtime-progress.js"; export interface CommandManagedRuntimeRunner { execute(input: { @@ -146,6 +147,9 @@ export async function prepareCommandManagedRuntime(input: { installCommand?: string | null; /** When provided alongside `installCommand`, skip the install if `command -v ` succeeds. */ detectCommand?: string | null; + // Upload progress sink. Forwarded to prepareSandboxManagedRuntime; the child + // task wires it into the byte-counting writeFile/readFile transport. + onProgress?: RuntimeProgressSink; }): Promise { const timeoutMs = input.spec.timeoutMs && input.spec.timeoutMs > 0 ? input.spec.timeoutMs : 300_000; const workspaceRemoteDir = input.workspaceRemoteDir ?? input.spec.remoteCwd; @@ -193,6 +197,7 @@ export async function prepareCommandManagedRuntime(input: { workspaceExclude: mergeRuntimeExcludes(input.workspaceExclude), preserveAbsentOnRestore: input.preserveAbsentOnRestore, assets: input.assets, + onProgress: input.onProgress, }); } } @@ -227,5 +232,6 @@ export async function prepareCommandManagedRuntime(input: { workspaceExclude: mergeRuntimeExcludes(input.workspaceExclude), preserveAbsentOnRestore: input.preserveAbsentOnRestore, assets: input.assets, + onProgress: input.onProgress, }); } diff --git a/packages/adapter-utils/src/execution-target.ts b/packages/adapter-utils/src/execution-target.ts index 8b6d5bbbbdf..86d3f1a8bc9 100644 --- a/packages/adapter-utils/src/execution-target.ts +++ b/packages/adapter-utils/src/execution-target.ts @@ -28,6 +28,9 @@ import { } from "./server-utils.js"; import { sanitizeRemoteExecutionEnv } from "./remote-execution-env.js"; import { preferredShellForSandbox, shellCommandArgs } from "./sandbox-shell.js"; +import type { RuntimeProgressSink } from "./runtime-progress.js"; + +export type { RuntimeProgressSink } from "./runtime-progress.js"; export interface AdapterLocalExecutionTarget { kind: "local"; @@ -70,7 +73,7 @@ export interface PreparedAdapterExecutionTargetRuntime { workspaceRemoteDir: string | null; runtimeRootDir: string | null; assetDirs: Record; - restoreWorkspace(): Promise; + restoreWorkspace(onProgress?: RuntimeProgressSink): Promise; } export interface AdapterExecutionTargetProcessOptions { @@ -930,6 +933,11 @@ export async function prepareAdapterExecutionTargetRuntime(input: { installCommand?: string | null; /** When provided alongside `installCommand`, skip the install if the binary is already on PATH. */ detectCommand?: string | null; + // Optional progress sink for the workspace/asset upload. The returned + // `restoreWorkspace(onProgress?)` accepts its own sink for teardown. Both are + // forwarded down to the transport so the sandbox/SSH children can attach byte + // counters without further changes here. + onProgress?: RuntimeProgressSink; }): Promise { const target = input.target ?? { kind: "local" as const }; if (target.kind === "local") { @@ -950,6 +958,7 @@ export async function prepareAdapterExecutionTargetRuntime(input: { workspaceLocalDir: input.workspaceLocalDir, workspaceRemoteDir: input.workspaceRemoteDir, assets: input.assets, + onProgress: input.onProgress, }); return { target, @@ -980,6 +989,7 @@ export async function prepareAdapterExecutionTargetRuntime(input: { assets: input.assets, installCommand: input.installCommand, detectCommand: input.detectCommand, + onProgress: input.onProgress, }); return { target, diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts index fa5b7632d03..303c028ed2d 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -61,6 +61,15 @@ export { redactCommandText, } from "./command-redaction.js"; export { buildSandboxNpmInstallCommand } from "./sandbox-install-command.js"; +export { createRuntimeProgressReporter } from "./runtime-progress.js"; +export type { + RuntimeProgressSink, + RuntimeProgressPhase, + RuntimeProgressDirection, + RuntimeProgressTarget, + RuntimeProgressReporter, + RuntimeProgressReporterOptions, +} from "./runtime-progress.js"; export { inferOpenAiCompatibleBiller } from "./billing.js"; // Keep the root adapter-utils entry browser-safe because the UI imports it. // The sandbox callback bridge stays available via its dedicated subpath export. diff --git a/packages/adapter-utils/src/remote-managed-runtime.ts b/packages/adapter-utils/src/remote-managed-runtime.ts index 900824b44c3..56a2f9a9e39 100644 --- a/packages/adapter-utils/src/remote-managed-runtime.ts +++ b/packages/adapter-utils/src/remote-managed-runtime.ts @@ -6,6 +6,7 @@ import { syncDirectoryToSsh, } from "./ssh.js"; import { captureDirectorySnapshot } from "./workspace-restore-merge.js"; +import type { RuntimeProgressSink } from "./runtime-progress.js"; export interface RemoteManagedRuntimeAsset { key: string; @@ -20,7 +21,7 @@ export interface PreparedRemoteManagedRuntime { workspaceRemoteDir: string; runtimeRootDir: string; assetDirs: Record; - restoreWorkspace(): Promise; + restoreWorkspace(onProgress?: RuntimeProgressSink): Promise; } function asObject(value: unknown): Record { @@ -69,6 +70,9 @@ export async function prepareRemoteManagedRuntime(input: { workspaceLocalDir: string; workspaceRemoteDir?: string; assets?: RemoteManagedRuntimeAsset[]; + // Upload progress sink. Threaded for the byte-counting transport rewrite; the + // child task wires it into the workspace/asset transfers. + onProgress?: RuntimeProgressSink; }): Promise { const baseWorkspaceRemoteDir = input.workspaceRemoteDir ?? input.spec.remoteCwd; const workspaceRemoteDir = path.posix.join( @@ -120,7 +124,7 @@ export async function prepareRemoteManagedRuntime(input: { workspaceRemoteDir, runtimeRootDir, assetDirs, - restoreWorkspace: async () => { + restoreWorkspace: async (_onProgress?: RuntimeProgressSink) => { await restoreWorkspaceFromSshExecution({ spec: input.spec, localDir: input.workspaceLocalDir, diff --git a/packages/adapter-utils/src/runtime-progress.test.ts b/packages/adapter-utils/src/runtime-progress.test.ts new file mode 100644 index 00000000000..ee0800b71db --- /dev/null +++ b/packages/adapter-utils/src/runtime-progress.test.ts @@ -0,0 +1,208 @@ +import { describe, expect, it } from "vitest"; +import { createRuntimeProgressReporter } from "./runtime-progress.js"; + +const MB = 1024 * 1024; + +function makeClock(start = 0) { + let value = start; + return { + now: () => value, + advance: (ms: number) => { + value += ms; + }, + }; +} + +describe("createRuntimeProgressReporter", () => { + it("formats the message with phase, label, direction, target, percent and MB", async () => { + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Syncing", + label: "workspace", + direction: "to", + target: "sandbox", + }); + + await reporter.report(12.6 * MB, 31.4 * MB); + + expect(lines).toEqual(["[paperclip] Syncing workspace to sandbox: 40% (12.6/31.4 MB)\n"]); + }); + + it("omits the label when none is provided (e.g. git history)", async () => { + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Importing git history", + direction: "to", + target: "ssh", + }); + + await reporter.report(4 * MB, 4 * MB); + + expect(lines).toEqual(["[paperclip] Importing git history to ssh: 100% (4.0/4.0 MB)\n"]); + }); + + it("suppresses intermediate emits that neither cross a step nor exceed the interval", async () => { + const clock = makeClock(); + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Syncing", + label: "workspace", + direction: "to", + target: "sandbox", + now: clock.now, + }); + + // First report always emits (step 0 crossed). + await reporter.report(1 * MB, 100 * MB); // 1% + // Still within the same 10% step and under 2s -> suppressed. + await reporter.report(2 * MB, 100 * MB); // 2% + await reporter.report(5 * MB, 100 * MB); // 5% + + expect(lines).toHaveLength(1); + expect(lines[0]).toBe("[paperclip] Syncing workspace to sandbox: 1% (1.0/100.0 MB)\n"); + }); + + it("emits when the percentage crosses a 10% step", async () => { + const clock = makeClock(); + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Syncing", + label: "workspace", + direction: "to", + target: "sandbox", + now: clock.now, + }); + + await reporter.report(1 * MB, 100 * MB); // 1% -> emit (step 0) + await reporter.report(15 * MB, 100 * MB); // 15% -> crosses into step 1 -> emit + + expect(lines).toHaveLength(2); + expect(lines[1]).toBe("[paperclip] Syncing workspace to sandbox: 15% (15.0/100.0 MB)\n"); + }); + + it("emits on the time threshold even without a step crossing", async () => { + const clock = makeClock(); + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Syncing", + label: "workspace", + direction: "to", + target: "sandbox", + now: clock.now, + }); + + await reporter.report(1 * MB, 100 * MB); // emit + await reporter.report(2 * MB, 100 * MB); // suppressed (same step, no time elapsed) + clock.advance(2000); + await reporter.report(3 * MB, 100 * MB); // 3% same step, but 2s elapsed -> emit + + expect(lines).toHaveLength(2); + expect(lines[1]).toBe("[paperclip] Syncing workspace to sandbox: 3% (3.0/100.0 MB)\n"); + }); + + it("always emits the terminal 100% line via report reaching the total", async () => { + const clock = makeClock(); + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Restoring", + label: "workspace", + direction: "from", + target: "sandbox", + now: clock.now, + }); + + await reporter.report(1 * MB, 100 * MB); // emit + await reporter.report(100 * MB, 100 * MB); // terminal -> always emit + + expect(lines[lines.length - 1]).toBe( + "[paperclip] Restoring workspace from sandbox: 100% (100.0/100.0 MB)\n", + ); + }); + + it("complete() emits the terminal 100% line even when intermediate emits were throttled", async () => { + const clock = makeClock(); + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Syncing", + label: "workspace", + direction: "to", + target: "sandbox", + now: clock.now, + }); + + await reporter.report(1 * MB, 100 * MB); // emit + await reporter.report(5 * MB, 100 * MB); // suppressed + await reporter.complete(); + + expect(lines[lines.length - 1]).toBe( + "[paperclip] Syncing workspace to sandbox: 100% (100.0/100.0 MB)\n", + ); + }); + + it("complete() is idempotent and does not double-emit after a terminal report", async () => { + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Syncing", + label: "workspace", + direction: "to", + target: "sandbox", + }); + + await reporter.report(100 * MB, 100 * MB); // terminal + await reporter.complete(); + await reporter.complete(); + + expect(lines).toHaveLength(1); + }); + + it("reports MB-only (no percent) when the total is unknown, plus a completion line", async () => { + const clock = makeClock(); + const lines: string[] = []; + const reporter = createRuntimeProgressReporter({ + sink: (line) => { + lines.push(line); + }, + phase: "Restoring", + label: "workspace", + direction: "from", + target: "ssh", + now: clock.now, + }); + + await reporter.report(2 * MB, null); // first emit + await reporter.report(4 * MB, null); // suppressed (no time elapsed) + clock.advance(2000); + await reporter.report(8 * MB, null); // time elapsed -> emit + await reporter.complete(); + + expect(lines).toEqual([ + "[paperclip] Restoring workspace from ssh: 2.0 MB\n", + "[paperclip] Restoring workspace from ssh: 8.0 MB\n", + "[paperclip] Restoring workspace from ssh: 8.0 MB\n", + ]); + }); +}); diff --git a/packages/adapter-utils/src/runtime-progress.ts b/packages/adapter-utils/src/runtime-progress.ts new file mode 100644 index 00000000000..09f12d982a6 --- /dev/null +++ b/packages/adapter-utils/src/runtime-progress.ts @@ -0,0 +1,131 @@ +// Shared, throttled progress reporting for execution-target sync/restore. +// +// Transports (sandbox / SSH) own the byte counting and call `report()` as bytes +// move; orchestrators own the per-phase label and direction. The reporter +// throttles emits so a long transfer doesn't flood the log: a line is emitted +// only when the percentage crosses a step boundary (default every 10%) or once +// at least `minIntervalMs` has elapsed since the last emit. The terminal +// completion line is always emitted via `complete()` (or when `report()` reaches +// the known total). + +/** A sink for fully-formatted progress lines (newline included). */ +export type RuntimeProgressSink = (line: string) => void | Promise; + +export type RuntimeProgressPhase = + | "Syncing" + | "Restoring" + | "Importing git history" + | "Exporting git history"; + +export type RuntimeProgressDirection = "to" | "from"; + +export type RuntimeProgressTarget = "sandbox" | "ssh"; + +export interface RuntimeProgressReporterOptions { + sink: RuntimeProgressSink; + phase: RuntimeProgressPhase; + /** Optional per-phase label, e.g. "workspace" or an asset key. */ + label?: string; + direction: RuntimeProgressDirection; + target: RuntimeProgressTarget; + /** Emit when the percentage crosses this step. Default 10. */ + stepPercent?: number; + /** Emit when at least this many ms have elapsed since the last emit. Default 2000. */ + minIntervalMs?: number; + /** Injectable clock for deterministic tests. Default `Date.now`. */ + now?: () => number; +} + +export interface RuntimeProgressReporter { + /** + * Report progress. Throttled: only emits on a step crossing or after + * `minIntervalMs`. When `totalBytes` is known and `doneBytes` reaches it, the + * terminal 100% line is emitted and the reporter is marked complete. + */ + report(doneBytes: number, totalBytes: number | null): Promise; + /** + * Emit the terminal completion line if it hasn't been emitted yet. Idempotent. + */ + complete(doneBytes?: number, totalBytes?: number | null): Promise; +} + +const BYTES_PER_MB = 1024 * 1024; + +function formatMb(bytes: number): string { + return (Math.max(0, bytes) / BYTES_PER_MB).toFixed(1); +} + +function clampPercent(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.min(100, Math.max(0, Math.round(value))); +} + +export function createRuntimeProgressReporter( + options: RuntimeProgressReporterOptions, +): RuntimeProgressReporter { + const stepPercent = options.stepPercent && options.stepPercent > 0 ? options.stepPercent : 10; + const minIntervalMs = + options.minIntervalMs && options.minIntervalMs > 0 ? options.minIntervalMs : 2000; + const now = options.now ?? Date.now; + const prefix = `[paperclip] ${options.phase}${options.label ? ` ${options.label}` : ""} ${options.direction} ${options.target}`; + + let lastEmitAt: number | null = null; + let lastStep = -1; + let lastDoneBytes = 0; + let lastTotalBytes: number | null = null; + let completed = false; + + function buildLine(doneBytes: number, totalBytes: number | null): string { + if (totalBytes != null && totalBytes > 0) { + const pct = clampPercent((doneBytes / totalBytes) * 100); + return `${prefix}: ${pct}% (${formatMb(doneBytes)}/${formatMb(totalBytes)} MB)\n`; + } + return `${prefix}: ${formatMb(doneBytes)} MB\n`; + } + + async function emit(doneBytes: number, totalBytes: number | null): Promise { + lastEmitAt = now(); + if (totalBytes != null && totalBytes > 0) { + lastStep = Math.floor(((doneBytes / totalBytes) * 100) / stepPercent); + } + await options.sink(buildLine(doneBytes, totalBytes)); + } + + return { + async report(doneBytes, totalBytes) { + lastDoneBytes = doneBytes; + lastTotalBytes = totalBytes; + if (completed) return; + + const elapsedOk = lastEmitAt == null || now() - lastEmitAt >= minIntervalMs; + + if (totalBytes != null && totalBytes > 0) { + const terminal = doneBytes >= totalBytes; + const step = Math.floor(((doneBytes / totalBytes) * 100) / stepPercent); + const stepOk = step > lastStep; + if (terminal || stepOk || elapsedOk) { + await emit(doneBytes, totalBytes); + } + if (terminal) completed = true; + return; + } + + // Unknown total: no step boundaries, throttle purely on elapsed time. + if (elapsedOk) { + await emit(doneBytes, totalBytes); + } + }, + async complete(doneBytes, totalBytes) { + if (completed) return; + completed = true; + const total = totalBytes !== undefined ? totalBytes : lastTotalBytes; + const done = + doneBytes !== undefined + ? doneBytes + : total != null && total > 0 + ? total + : lastDoneBytes; + await options.sink(buildLine(done, total)); + }, + }; +} diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.ts b/packages/adapter-utils/src/sandbox-managed-runtime.ts index 81bd90b9c6b..d3c3ff82121 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.ts @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { captureDirectorySnapshot, mergeDirectoryWithBaseline } from "./workspace-restore-merge.js"; +import type { RuntimeProgressSink } from "./runtime-progress.js"; const execFile = promisify(execFileCallback); @@ -38,7 +39,7 @@ export interface PreparedSandboxManagedRuntime { workspaceRemoteDir: string; runtimeRootDir: string; assetDirs: Record; - restoreWorkspace(): Promise; + restoreWorkspace(onProgress?: RuntimeProgressSink): Promise; } function asObject(value: unknown): Record { @@ -270,6 +271,9 @@ export async function prepareSandboxManagedRuntime(input: { workspaceExclude?: string[]; preserveAbsentOnRestore?: string[]; assets?: SandboxManagedRuntimeAsset[]; + // Upload progress sink. Threaded for the byte-counting transport rewrite; the + // child task wires it into writeFile/readFile. + onProgress?: RuntimeProgressSink; }): Promise { const workspaceRemoteDir = input.workspaceRemoteDir ?? input.spec.remoteCwd; const runtimeRootDir = path.posix.join(workspaceRemoteDir, ".paperclip-runtime", input.adapterKey); @@ -334,7 +338,7 @@ export async function prepareSandboxManagedRuntime(input: { workspaceRemoteDir, runtimeRootDir, assetDirs, - restoreWorkspace: async () => { + restoreWorkspace: async (_onProgress?: RuntimeProgressSink) => { await withTempDir("paperclip-sandbox-restore-", async (tempDir) => { const remoteWorkspaceTar = path.posix.join(runtimeRootDir, "workspace-download.tar"); await input.client.run( diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index a265606183e..307d3cbfc39 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -484,6 +484,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise onLog("stdout", line), assets: [ { key: "skills", @@ -522,7 +523,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise preparedExecutionTargetRuntime.restoreWorkspace() + ? () => preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line)) : null; const effectivePromptBundleAddDir = executionTargetIsRemote ? preparedExecutionTargetRuntime?.assetDirs.skills ?? diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 2c52aa2bb38..ce99fda3037 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -397,6 +397,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise onLog("stdout", line), assets: [ { key: "home", @@ -414,7 +415,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise preparedExecutionTargetRuntime.restoreWorkspace() + ? () => preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line)) : null; let paperclipBridge: Awaited> = null; const remoteCodexHome = executionTargetIsRemote diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index 800a499fef2..40c3e8ff53b 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -364,13 +364,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise onLog("stdout", line), assets: [{ key: "skills", localDir: localSkillsDir, followSymlinks: true, }], }); - restoreRemoteWorkspace = () => preparedExecutionTargetRuntime.restoreWorkspace(); + restoreRemoteWorkspace = () => + preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line)); effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir ?? effectiveExecutionCwd; refreshPaperclipWorkspaceEnvForExecution({ env, diff --git a/packages/adapters/gemini-local/src/server/execute.ts b/packages/adapters/gemini-local/src/server/execute.ts index 5431eca5e74..be4e1b155f2 100644 --- a/packages/adapters/gemini-local/src/server/execute.ts +++ b/packages/adapters/gemini-local/src/server/execute.ts @@ -333,13 +333,15 @@ export async function execute(ctx: AdapterExecutionContext): Promise onLog("stdout", line), assets: [{ key: "skills", localDir: localSkillsDir, followSymlinks: true, }], }); - restoreRemoteWorkspace = () => preparedExecutionTargetRuntime.restoreWorkspace(); + restoreRemoteWorkspace = () => + preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line)); effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir ?? effectiveExecutionCwd; refreshPaperclipWorkspaceEnvForExecution({ env, diff --git a/packages/adapters/grok-local/src/server/execute.ts b/packages/adapters/grok-local/src/server/execute.ts index 0d25845adae..56e70a707e5 100644 --- a/packages/adapters/grok-local/src/server/execute.ts +++ b/packages/adapters/grok-local/src/server/execute.ts @@ -325,8 +325,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise onLog("stdout", line), }); - restoreRemoteWorkspace = () => preparedExecutionTargetRuntime.restoreWorkspace(); + restoreRemoteWorkspace = () => + preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line)); effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir ?? effectiveExecutionCwd; refreshPaperclipWorkspaceEnvForExecution({ env, diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index b7ae92bc4f0..247480b1982 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -377,6 +377,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise onLog("stdout", line), assets: [ { key: "skills", @@ -391,7 +392,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise preparedExecutionTargetRuntime.restoreWorkspace(); + restoreRemoteWorkspace = () => + preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line)); effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir ?? effectiveExecutionCwd; refreshPaperclipWorkspaceEnvForExecution({ env: preparedRuntimeConfig.env, diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index 5157966256c..30d4d16a5af 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -417,6 +417,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise onLog("stdout", line), assets: [ { key: "skills", @@ -431,7 +432,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise preparedRemoteRuntime.restoreWorkspace(); + restoreRemoteWorkspace = () => + preparedRemoteRuntime.restoreWorkspace((line) => onLog("stdout", line)); effectiveExecutionCwd = preparedRemoteRuntime.workspaceRemoteDir ?? effectiveExecutionCwd; refreshPaperclipWorkspaceEnvForExecution({ env,