Skip to content

feat(remote): download ordinary recordings without closing the session - #4511

Open
bradenbiz wants to merge 6 commits into
callstack:mainfrom
bradenbiz:feat/remote-active-script-publication
Open

bradenbiz wants to merge 6 commits into
callstack:mainfrom
bradenbiz:feat/remote-active-script-publication

Conversation

@bradenbiz

@bradenbiz bradenbiz commented Nov 13, 2026 •

Copy link
Copy Markdown

Summary

I use externally owned HTTP sessions and need to export journeys without closing them:

agent-device open Settings --platform ios --session journey --save-script=journey.ad
agent-device wait 'label="General"' --session journey
agent-device session save-script --session journey

Use the same configured remote connection. A fresh open --save-script arms ordinary recording. session save-script downloads an authenticated session-script artifact; repeat it to retrieve the same immutable script after a collision or interrupted transfer. Use --force for an existing caller file. Publication does not close the session or change its owner/driver.

The implementation uses active-script-artifacts in the existing capability vocabulary, daemon-owned allocation, explicit remote delivery, registration-owned retention, and atomic caller writes. Older daemons refuse before recording RPC.

Related: #1810. Close-time/repaired-script downloads remain excluded. Plain close discards an unpublished remote recording. The diff touches 43 files. One internal workspace dependency is declared for registry constants; no external package was added.

Validation

Head: 2be928f1041dfad40f4a97e662bb3c7ac50bbfac. pnpm check:affected --run --head HEAD: failed, exactCommit=true. Format, lint, types, layering, DI, Fallow, native-duplication checks, MCP metadata and build passed. Packaging stopped at missing Android SDK configuration; the same package command fails on clean base b68ed4bf2. Remaining gates were not reached.

12 provider scenarios and 50 wire/mutation tests passed; docs and released-wire compatibility passed. Independent review findings were fixed. No live devices used.

The tenant-upload test passes on both unchanged base and revised head in controlled Linux CI. Its local failure remains unexplained; full upstream CI is still pending.

Created with gpt-6.1-sol in the Codex harness through T3 Code.


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

View guided diff

@thymikee

Copy link
Copy Markdown
Member

Reviewed 79ff421. Remote script publication can lose a recording when the caller's target file already exists, and the proxy help and docs still say remote publication is refused.

When session save-script <path> names an existing caller file without --force, the no-clobber precheck does not run, because it only covers open (daemon-artifacts.ts:120). The daemon writes to a fresh temp path and succeeds, and markActivePublicationDone sets the status to published (session-script-publication.ts:70). The client then fails with script_target_exists in materializeScriptArtifact (artifact-download.ts:237). A retry with --force is refused because a published session is not eligible (session-script-publication.ts:123-126). Locally the refusal happens before markActivePublicationDone, so the session stays armed and --force works. A remote agent that follows the usual "file exists, retry with --force" recovery cannot republish through the CLI. The rule should be: every caller-side refusal that local publication would raise happens before the daemon consumes the one-shot publication, or the published remote target stays re-downloadable through the CLI. The smallest fix is to apply the open-time existence check to the active branch too, and/or let session save-script on a published session with a clientPath re-emit its retained artifact instead of refusing. Please cover it with a test that publishes to an existing file, then retries with --force and gets the script.

The proxy command help still says remote requests refuse script publication (command-overrides.ts:163), and remote-proxy.md:106-112 says the same and points to a local daemon. This PR removes that refusal for open --save-script and session save-script (remote-host-input-policy.ts:59-74), so agents reading the help are told the opposite of what works. Please update both to say remote callers can arm with a fresh open --save-script and publish with session save-script, which downloads a session-script artifact, while other script publication (close/replay --save-script, batch steps) stays refused.

Not blocking, take or leave: remote versus local publication is inferred from the client's clientArtifactPaths.savedScript hint (session-script-writer.ts:128) rather than from internal.publicNetworkOnly at arm time, so a direct HTTP RPC caller without that hint gets no artifact and leaves an orphan script in daemon temp; retention after download matches artifactType === 'session-script' in artifact-tracking.ts:328 where a retainAfterDownload on the registration would match deleteAfterDownload; the client and the HTTP boundary each allocate a script path (http-authorization.ts:77); bare remote open --save-script defaults to recording-<Date.now()>.ad in the caller cwd, which replay-e2e.md does not document (daemon-artifacts.ts:117); ledger.json replaces existing rationale entries instead of amending them and re-escapes an unrelated #2198 entry; and the literal 'session_save_script' in daemon-client-rpc.ts:216 and daemon-artifacts.ts should be INTERNAL_COMMANDS.sessionSaveScript.

Could remote support be a DAEMON_CAPABILITIES entry on /health, read by readDaemonCapabilities, instead of the activeScriptArtifacts boolean? That would drop the bespoke field from contracts, transport, lifecycle, metadata and about seven ledger entries, and ADR 0006 already says an absent capability means "cannot". The same simplification would mark the target as remote-delivered from publicNetworkOnly at the HTTP boundary, with one allocator for the daemon path, and declare retention on the artifact registration. Making a published remote target re-downloadable through session save-script would also make the first problem recoverable without raw HTTP. Nothing needs to change first beyond adding the capability name and a health capabilities field.

No checks have reported on this draft head. The description says check:affected fails one test, remote-proxy-artifact-fetch-tenant.test.ts (500 instead of 401), and that it also fails on b68ed4b. That test covers the artifact download and tenant path this PR changes, so please show it failing on base in CI before calling it unrelated. I did not run any tests or a live remote proxy run. The next step is to fix the two problems above and get CI green on this head.

@bradenbiz

Copy link
Copy Markdown
Author

Note

🤖 gpt-6.1-sol responding on behalf of bradenbiz

Good point that consuming publication before a caller-file collision breaks normal recovery; 081d14b makes session save-script re-download the same retained script, with regression coverage for collision/--force, immutable bytes, original action count, and destination-specific overwrite permission.

The docs fix and simplification also use the shared capability parser, daemon-side allocation, explicit remote delivery, and registration-owned retention while preserving local HTTP behavior.

The controlled Linux CI comparison ran the tenant-upload test on both unchanged base b68ed4bf2 and revised head 2be928f10: both passed, so the local failure remains unexplained and I am not treating it as a proven upstream failure.

The exact-head local gate passed its code checks but stopped at Android SDK configuration during packaging, reproduced with the same command on clean base; will you please approve the PR’s CI workflow runs so the full upstream CI can run?

@thymikee

Copy link
Copy Markdown
Member

This PR is ready at 2be928f. It fixes the problems in the earlier review of 79ff421, and I found no remaining problems in the diff. Not blocking, and you can take or leave these: /health hand-writes capabilities:['active-script-artifacts'] while daemon.json publishes DAEMON_CAPABILITIES, so it could name the HTTP subset once next to DAEMON_CAPABILITIES and use it there; also, daemon-client-transport.ts loses about fifteen blank lines between unrelated functions, which you could restore unless a gate needs the line count.

No conflicts. I judged this from the diff and did not run tests or a live remote proxy run. Upstream CI has not reported on 2be928f yet. You report remote-proxy-artifact-fetch-tenant passing on base and head in your fork CI, and I did not open that run. That test covers the artifact download route this PR changes, so it must pass upstream on this head before merge.

The PR is still a draft. Please mark it ready for review when you consider it done, so the upstream checks can run.

@bradenbiz
bradenbiz marked this pull request as ready for review November 13, 2026 20:55
@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 13, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 issues found across 43 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/daemon/src/server/http/http-authorization-script-admission.test.ts">

<violation number="1" location="packages/daemon/src/server/http/http-authorization-script-admission.test.ts:46">
P3: The refusal cases all share one title, so a failure does not say which input carrier was accepted. Add a per-row label (for example a `name` field) or a row index placeholder such as `%#` to the title.</violation>
</file>

<file name="website/docs/docs/remote-proxy.md">

<violation number="1" location="website/docs/docs/remote-proxy.md:107">
P3: This sentence is unqualified, but the macOS app lease section refuses `open --save-script`. Name that exception here so the two sections agree.</violation>
</file>

<file name="docs/adr/0016-active-session-script-publication.md">

<violation number="1" location="docs/adr/0016-active-session-script-publication.md:91">
P2: This exception contradicts the unchanged failure list in the Artifact contract ("The recording is ABORTED or PUBLISHED") and the validation bullet "a repeated `session save-script` fails". Update those statements and the Rules at a glance so the remote PUBLISHED re-download case is stated once, consistently.</violation>

<violation number="2" location="docs/adr/0016-active-session-script-publication.md:92">
P3: "Remote recording" and the retained artifact re-download are not defined anywhere in this ADR. Add a dated amendment that defines the remote case and its retention, following the convention the other lifecycle changes use.</violation>
</file>

<file name="test/integration/provider-scenarios/remote-script-publication.test.ts">

<violation number="1" location="test/integration/provider-scenarios/remote-script-publication.test.ts:177">
P2: This negative check passes for any non-200 status, including a 500 or 404. Assert the auth refusal directly with `assert.equal(denied.status, 401)` so a broken auth gate on `/artifacts/<id>` cannot pass.</violation>
</file>

<file name="packages/remote/src/artifact-download.ts">

<violation number="1" location="packages/remote/src/artifact-download.ts:248">
P3: The hint is attached to every failure, but `--force` only helps when the reason is `script_target_exists`. For `script_download_failed` it points users at the wrong fix. Make the hint depend on `reason`.</violation>
</file>

<file name="packages/daemon/src/core/session-action-recorder.ts">

<violation number="1" location="packages/daemon/src/core/session-action-recorder.ts:76">
P2: The client delivery metadata passed here is dropped when no path is given. `resolveScriptTarget` returns early on `requested.path === undefined` without applying `clientPath` or `remote`, so a bare `--save-script` or `session save-script` over remote never produces a downloadable `session-script` artifact. If remote export should work without an explicit path, carry `clientPath` and `remote` through that early return. If default-target remote export is unsupported, reject it explicitly instead of silently ignoring the flag.</violation>
</file>

<file name="packages/daemon-client/src/daemon-client-rpc.ts">

<violation number="1" location="packages/daemon-client/src/daemon-client-rpc.ts:217">
P3: This save-script predicate is now duplicated in `sendHttpRequest` (daemon-client-transport.ts:800). Extract it into one helper, such as `requestNeedsActiveScriptArtifacts(req)`, and call it from both places so the capability gate and the network-access header cannot drift apart.</violation>

<violation number="2" location="packages/daemon-client/src/daemon-client-rpc.ts:225">
P3: The hint only names the upstream health response, but a direct daemon with no `upstream` field advertises `active-script-artifacts` at the top level. Reword the hint to say the capability must appear in the health response the client reads, or in the upstream response when a proxy forwards it.</violation>
</file>

<file name="packages/remote/src/__tests__/daemon-artifacts-script-download.test.ts">

<violation number="1" location="packages/remote/src/__tests__/daemon-artifacts-script-download.test.ts:26">
P3: The fake server never checks the requested path, so the test would pass even if the client downloaded a different artifact. Assert that `req.url` ends with `/script-id` (or matches the expected artifact route) in the handler.</violation>
</file>

<file name="packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts">

<violation number="1" location="packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts:376">
P3: This test's name says it covers regeneration from later actions, but the fixture has no actions after `publishedActionCount: 3`. Add a fourth recorded action after the publication point so the test proves later actions do not regenerate the expired artifact.</violation>
</file>

<file name="packages/remote/src/daemon-artifacts.ts">

<violation number="1" location="packages/remote/src/daemon-artifacts.ts:130">
P3: Empty `session save-script ""` skips the daemon's `cannot be empty` guard: the client strips the positional and resolves the empty path to the cwd directory. Reject a blank path here before publishing.</violation>

<violation number="2" location="packages/remote/src/daemon-artifacts.ts:468">
P1: `artifact.force` can authorize overwriting a different caller destination during the first export. Preserve the force grant only when it applies to this request’s destination, or have the daemon compute force per caller path.</violation>

<violation number="3" location="packages/remote/src/daemon-artifacts.ts:496">
P3: The doc comments above `finishesEarlierClientCapture` and `resolveMaterializedArtifactPath` still describe only stop and report requests as accepting an echoed `localPath`. Update them to cover `session save-script` as well, since this case now accepts it.</violation>

<violation number="4" location="packages/remote/src/daemon-artifacts.ts:497">
P2: `session save-script` trusts a daemon-returned `localPath` as the current caller’s output path. Use the current request’s `clientArtifactPaths` or resolve only a safe basename beneath the current caller cwd; do not treat persisted remote paths as local destinations.</violation>
</file>

<file name="packages/daemon-contracts/src/daemon-resolution.ts">

<violation number="1" location="packages/daemon-contracts/src/daemon-resolution.ts:151">
P2: This capability addition changes the published `daemon.json` shape and breaks the existing quiesce runtime test, which still expects only `['quiesce']`. Update that assertion (and related fixture expectations) to include `active-script-artifacts`.</violation>
</file>

<file name="packages/daemon/src/resources/artifact-tracking.ts">

<violation number="1" location="packages/daemon/src/resources/artifact-tracking.ts:331">
P2: Retained directory registrations lose their retention flag here, so the HTTP handler treats the generated tarball as consumable and deletes the source after the first download. Propagate `entry.retainAfterDownload` in the directory return as well.</violation>
</file>

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

// `downloadRemoteArtifact`).
isDirectory: artifact.artifactType === 'test-artifacts',
...(artifact.artifactType === 'session-script'
? { scriptForce: req.flags?.force === true || artifact.force === true }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: artifact.force can authorize overwriting a different caller destination during the first export. Preserve the force grant only when it applies to this request’s destination, or have the daemon compute force per caller path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/daemon-artifacts.ts, line 468:

<comment>`artifact.force` can authorize overwriting a different caller destination during the first export. Preserve the force grant only when it applies to this request’s destination, or have the daemon compute force per caller path.</comment>

<file context>
@@ -447,11 +464,17 @@ export async function materializeRemoteArtifacts(
       // `downloadRemoteArtifact`).
       isDirectory: artifact.artifactType === 'test-artifacts',
+      ...(artifact.artifactType === 'session-script'
+        ? { scriptForce: req.flags?.force === true || artifact.force === true }
+        : {}),
     });
</file context>

Thus, the caller can correct the target or permissions and retry.
ABORTED and PUBLISHED are terminal until the session is destroyed.
In either state, `session save-script` fails with an explicit error and never writes.
In either state, `session save-script` never records or publishes new script content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This exception contradicts the unchanged failure list in the Artifact contract ("The recording is ABORTED or PUBLISHED") and the validation bullet "a repeated session save-script fails". Update those statements and the Rules at a glance so the remote PUBLISHED re-download case is stated once, consistently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/adr/0016-active-session-script-publication.md, line 91:

<comment>This exception contradicts the unchanged failure list in the Artifact contract ("The recording is ABORTED or PUBLISHED") and the validation bullet "a repeated `session save-script` fails". Update those statements and the Rules at a glance so the remote PUBLISHED re-download case is stated once, consistently.</comment>

<file context>
@@ -88,7 +88,11 @@ An ordinary recording eligible for active-session publication has three states:
 Thus, the caller can correct the target or permissions and retry.
 ABORTED and PUBLISHED are terminal until the session is destroyed.
-In either state, `session save-script` fails with an explicit error and never writes.
+In either state, `session save-script` never records or publishes new script content.
+A PUBLISHED remote recording may return its retained immutable artifact for another caller-side download.
+This exception does not change the terminal recording state.
</file context>

assert.ok(artifact?.artifactId);
assert.equal(artifact.path, undefined);
const denied = await fetch(`${proxy.baseUrl}/artifacts/${artifact.artifactId}`);
assert.notEqual(denied.status, 200);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This negative check passes for any non-200 status, including a 500 or 404. Assert the auth refusal directly with assert.equal(denied.status, 401) so a broken auth gate on /artifacts/<id> cannot pass.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At test/integration/provider-scenarios/remote-script-publication.test.ts, line 177:

<comment>This negative check passes for any non-200 status, including a 500 or 404. Assert the auth refusal directly with `assert.equal(denied.status, 401)` so a broken auth gate on `/artifacts/<id>` cannot pass.</comment>

<file context>
@@ -0,0 +1,191 @@
+      assert.ok(artifact?.artifactId);
+      assert.equal(artifact.path, undefined);
+      const denied = await fetch(`${proxy.baseUrl}/artifacts/${artifact.artifactId}`);
+      assert.notEqual(denied.status, 200);
+      const downloaded = await fetch(`${proxy.baseUrl}/artifacts/${artifact.artifactId}`, {
+        headers: { authorization: 'Bearer raw-client' },
</file context>
Suggested change
assert.notEqual(denied.status, 200);
assert.equal(denied.status, 401);

entry.flags,
hostCwd,
entry.clientScriptPath,
entry.remoteScriptDelivery,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The client delivery metadata passed here is dropped when no path is given. resolveScriptTarget returns early on requested.path === undefined without applying clientPath or remote, so a bare --save-script or session save-script over remote never produces a downloadable session-script artifact. If remote export should work without an explicit path, carry clientPath and remote through that early return. If default-target remote export is unsupported, reject it explicitly instead of silently ignoring the flag.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon/src/core/session-action-recorder.ts, line 76:

<comment>The client delivery metadata passed here is dropped when no path is given. `resolveScriptTarget` returns early on `requested.path === undefined` without applying `clientPath` or `remote`, so a bare `--save-script` or `session save-script` over remote never produces a downloadable `session-script` artifact. If remote export should work without an explicit path, carry `clientPath` and `remote` through that early return. If default-target remote export is unsupported, reject it explicitly instead of silently ignoring the flag.</comment>

<file context>
@@ -65,7 +67,14 @@ export function recordActionEntry(
+      entry.flags,
+      hostCwd,
+      entry.clientScriptPath,
+      entry.remoteScriptDelivery,
+    );
   const fillLiteral = readRecordedFillLiteral(entry);
</file context>


/** What a daemon can do beyond the base RPC contract, published in its `daemon.json`. */
export const DAEMON_CAPABILITIES = ['quiesce'] as const;
export const DAEMON_CAPABILITIES = ['quiesce', 'active-script-artifacts'] as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This capability addition changes the published daemon.json shape and breaks the existing quiesce runtime test, which still expects only ['quiesce']. Update that assertion (and related fixture expectations) to include active-script-artifacts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon-contracts/src/daemon-resolution.ts, line 151:

<comment>This capability addition changes the published `daemon.json` shape and breaks the existing quiesce runtime test, which still expects only `['quiesce']`. Update that assertion (and related fixture expectations) to include `active-script-artifacts`.</comment>

<file context>
@@ -148,7 +148,7 @@ export function resolveDaemonTransportPreference(
 
 /** What a daemon can do beyond the base RPC contract, published in its `daemon.json`. */
-export const DAEMON_CAPABILITIES = ['quiesce'] as const;
+export const DAEMON_CAPABILITIES = ['quiesce', 'active-script-artifacts'] as const;
 export type DaemonCapability = (typeof DAEMON_CAPABILITIES)[number];
 
</file context>

if (
info.baseUrl &&
!info.capabilities?.includes('active-script-artifacts') &&
(req.flags?.saveScript || req.command === INTERNAL_COMMANDS.sessionSaveScript)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This save-script predicate is now duplicated in sendHttpRequest (daemon-client-transport.ts:800). Extract it into one helper, such as requestNeedsActiveScriptArtifacts(req), and call it from both places so the capability gate and the network-access header cannot drift apart.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon-client/src/daemon-client-rpc.ts, line 217:

<comment>This save-script predicate is now duplicated in `sendHttpRequest` (daemon-client-transport.ts:800). Extract it into one helper, such as `requestNeedsActiveScriptArtifacts(req)`, and call it from both places so the capability gate and the network-access header cannot drift apart.</comment>

<file context>
@@ -207,3 +208,22 @@ export function refuseOversizedDaemonRequest(payload: string, requestId: string
+  if (
+    info.baseUrl &&
+    !info.capabilities?.includes('active-script-artifacts') &&
+    (req.flags?.saveScript || req.command === INTERNAL_COMMANDS.sessionSaveScript)
+  ) {
+    throw new AppError(
</file context>

if (existing) fs.writeFileSync(destination, 'original');
const text = 'context platform=ios\nopen Demo\n';
const server = http.createServer((req, res) => {
expect(req.headers.authorization).toBe('Bearer test');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The fake server never checks the requested path, so the test would pass even if the client downloaded a different artifact. Assert that req.url ends with /script-id (or matches the expected artifact route) in the handler.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/__tests__/daemon-artifacts-script-download.test.ts, line 26:

<comment>The fake server never checks the requested path, so the test would pass even if the client downloaded a different artifact. Assert that `req.url` ends with `/script-id` (or matches the expected artifact route) in the handler.</comment>

<file context>
@@ -0,0 +1,124 @@
+    if (existing) fs.writeFileSync(destination, 'original');
+    const text = 'context platform=ios\nopen Demo\n';
+    const server = http.createServer((req, res) => {
+      expect(req.headers.authorization).toBe('Bearer test');
+      expect(req.headers['x-agent-device-tenant']).toBe('tenant');
+      expect(req.headers['x-agent-device-lease-id']).toBe('lease');
</file context>

expect(fs.existsSync(output)).toBe(true);
});

test('an expired remote publication is not regenerated from later actions', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This test's name says it covers regeneration from later actions, but the fixture has no actions after publishedActionCount: 3. Add a fourth recorded action after the publication point so the test proves later actions do not regenerate the expired artifact.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts, line 376:

<comment>This test's name says it covers regeneration from later actions, but the fixture has no actions after `publishedActionCount: 3`. Add a fourth recorded action after the publication point so the test proves later actions do not regenerate the expired artifact.</comment>

<file context>
@@ -348,3 +348,50 @@ test('refuses mutating find steps that cannot enforce target identity on replay'
+  expect(fs.existsSync(output)).toBe(true);
+});
+
+test('an expired remote publication is not regenerated from later actions', () => {
+  const output = path.join(root, 'expired.ad');
+  store.publish(
</file context>

function finishesEarlierClientCapture(req: DaemonWireRequest, artifact: DaemonArtifact): boolean {
const [first, second, third] = (req.positionals ?? []).map((value) => value.toLowerCase());
switch (req.command) {
case INTERNAL_COMMANDS.sessionSaveScript:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The doc comments above finishesEarlierClientCapture and resolveMaterializedArtifactPath still describe only stop and report requests as accepting an echoed localPath. Update them to cover session save-script as well, since this case now accepts it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/daemon-artifacts.ts, line 496:

<comment>The doc comments above `finishesEarlierClientCapture` and `resolveMaterializedArtifactPath` still describe only stop and report requests as accepting an echoed `localPath`. Update them to cover `session save-script` as well, since this case now accepts it.</comment>

<file context>
@@ -467,9 +490,11 @@ export async function materializeRemoteArtifacts(
+function finishesEarlierClientCapture(req: DaemonWireRequest, artifact: DaemonArtifact): boolean {
   const [first, second, third] = (req.positionals ?? []).map((value) => value.toLowerCase());
   switch (req.command) {
+    case INTERNAL_COMMANDS.sessionSaveScript:
+      return artifact.field === 'savedScript' && artifact.artifactType === 'session-script';
     case 'record':
</file context>

}
clientPaths.savedScript = localPath;
if (active) {
if (positionals.length === 1) positionals.length = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Empty session save-script "" skips the daemon's cannot be empty guard: the client strips the positional and resolves the empty path to the cwd directory. Reject a blank path here before publishing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/remote/src/daemon-artifacts.ts, line 130:

<comment>Empty `session save-script ""` skips the daemon's `cannot be empty` guard: the client strips the positional and resolves the empty path to the cwd directory. Reject a blank path here before publishing.</comment>

<file context>
@@ -95,25 +97,40 @@ export async function prepareRemoteRequestArtifacts(
+  }
+  clientPaths.savedScript = localPath;
+  if (active) {
+    if (positionals.length === 1) positionals.length = 0;
+    return flags;
+  }
</file context>
Suggested change
if (positionals.length === 1) positionals.length = 0;
if (positionals.length === 1) {
if (!positionals[0]?.trim()) {
throw new AppError('INVALID_ARGS', 'session save-script path cannot be empty.');
}
positionals.length = 0;
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants