Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"id": "compact_ridr6adoo0go",
"version": 1,
"type": "compacted",
"compactedAt": "2026-10-15T19:57:17.694Z",
"sourceTrajectories": [
"traj_fesap23qytsg"
],
"dateRange": {
"start": "2026-10-15T19:49:55.679Z",
"end": "2026-10-15T19:56:51.350Z"
},
"summary": {
"totalDecisions": 1,
"totalEvents": 2,
"uniqueAgents": [
"default"
]
},
"decisionGroups": [
{
"category": "api",
"decisions": [
{
"question": "Warn once after a dispatched release acknowledgement, before optional identity cleanup",
"chosen": "Warn once after a dispatched release acknowledgement, before optional identity cleanup",
"reasoning": "The release endpoint acknowledges worker dispatch only. Emitting through the existing stderr warning sink preserves the single JSON stdout document and covers delete-agent variants without invoking Cloud/provider deletion.",
"fromTrajectory": "traj_fesap23qytsg"
}
]
}
],
"keyLearnings": [],
"keyFindings": [],
"filesAffected": [],
"commits": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Trajectory Compaction: Oct 16, 2026 - Oct 16, 2026

## Summary
- Sessions: 1
- Decisions: 1
- Events: 2
- Agents: default
- Files: 0
- Commits: 0

## Api
- Warn once after a dispatched release acknowledgement, before optional identity cleanup -> Warn once after a dispatched release acknowledgement, before optional identity cleanup (traj_fesap23qytsg)

## Key Learnings
- None

## Key Findings
- None
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"trajectoryId": "traj_fesap23qytsg",
"compactedInto": "compact_ridr6adoo0go",
"compactedAt": "2026-10-15T19:57:17.709Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Trajectory: Warn when fleet release leaves Cloud sandboxes running

> **Status:** ✅ Completed
> **Task:** 1922
> **Confidence:** 90%
> **Started:** October 16, 2026 at 01:19 AM
> **Completed:** October 16, 2026 at 01:26 AM

---

## Summary

Added the dispatched-release sandbox cost warning without changing JSON stdout or requesting provider teardown. Added seven regressions, one RelayFlow bugfix proof, and a patch changelog entry. Independent review found no actionable issues. Full Windows suite limitations are documented for maintainer validation.

**Approach:** Standard approach

---

## Key Decisions

### Warn once after a dispatched release acknowledgement, before optional identity cleanup
- **Chose:** Warn once after a dispatched release acknowledgement, before optional identity cleanup
- **Reasoning:** The release endpoint acknowledges worker dispatch only. Emitting through the existing stderr warning sink preserves the single JSON stdout document and covers delete-agent variants without invoking Cloud/provider deletion.

---

## Chapters

### 1. Work
*Agent: default*

- Warn once after a dispatched release acknowledgement, before optional identity cleanup: Warn once after a dispatched release acknowledgement, before optional identity cleanup
- The seven new regressions and all 18 release tests pass. Both proof observations match the expected bug/fixed behavior. Typecheck and formatting pass; full Windows validation has 342 failures and a worker OOM, and the fleet Unix-path mismatch is confirmed on unchanged main.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"id": "traj_fesap23qytsg",
"version": 1,
"task": {
"title": "Warn when fleet release leaves Cloud sandboxes running",
"source": {
"system": "plain",
"id": "1922"
}
},
"status": "completed",
"startedAt": "2026-10-15T19:49:55.679Z",
"completedAt": "2026-10-15T19:56:51.350Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-10-15T19:51:26.854Z"
}
],
"chapters": [
{
"id": "chap_w5couri5corw",
"title": "Work",
"agentName": "default",
"startedAt": "2026-10-15T19:51:26.854Z",
"endedAt": "2026-10-15T19:56:51.350Z",
"events": [
{
"ts": 1792093886855,
"type": "decision",
"content": "Warn once after a dispatched release acknowledgement, before optional identity cleanup: Warn once after a dispatched release acknowledgement, before optional identity cleanup",
"raw": {
"question": "Warn once after a dispatched release acknowledgement, before optional identity cleanup",
"chosen": "Warn once after a dispatched release acknowledgement, before optional identity cleanup",
"alternatives": [],
"reasoning": "The release endpoint acknowledges worker dispatch only. Emitting through the existing stderr warning sink preserves the single JSON stdout document and covers delete-agent variants without invoking Cloud/provider deletion."
},
"significance": "high"
},
{
"ts": 1792094210071,
"type": "reflection",
"content": "The seven new regressions and all 18 release tests pass. Both proof observations match the expected bug/fixed behavior. Typecheck and formatting pass; full Windows validation has 342 failures and a worker OOM, and the fleet Unix-path mismatch is confirmed on unchanged main.",
"raw": {
"confidence": 0.9
},
"significance": "high",
"tags": [
"confidence:0.9"
]
}
]
}
],
"retrospective": {
"summary": "Added the dispatched-release sandbox cost warning without changing JSON stdout or requesting provider teardown. Added seven regressions, one RelayFlow bugfix proof, and a patch changelog entry. Independent review found no actionable issues. Full Windows suite limitations are documented for maintainer validation.",
"approach": "Standard approach",
"confidence": 0.9
},
"commits": [],
"filesChanged": [],
"projectId": "AgentWorkforce/relay",
"tags": [],
"_trace": {
"startRef": "c1ffabbc4a933585ca2ddda729b22a34e3b2a3f2",
"endRef": "c1ffabbc4a933585ca2ddda729b22a34e3b2a3f2"
}
}
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ All notable changes to Agent Relay will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [Unreleased - Patch]

### Fixed

- `fleet release` warns that a dispatched worker release leaves Cloud/provider sandboxes running and may continue incurring cost, while preserving JSON output on stdout.

## [14.0.0] - 2026-10-13

Expand Down
79 changes: 79 additions & 0 deletions packages/cli/src/cli/commands/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5356,6 +5356,85 @@ describe('fleet command support', () => {
expect(createAgentRelay).not.toHaveBeenCalled();
});

describe('fleet release sandbox warning', () => {
function fixture(status = 'dispatched') {
const stdout: string[] = [];
const stderr: string[] = [];
const receipt = { invocationId: 'inv_release', actionName: 'agent.release', status };
const release = vi.fn(async () => receipt);
const deleteSandbox = vi.fn();
const program = new Command();
program.exitOverride();
registerFleetCommands(program, {
sdk: {
createAgentRelay: vi.fn() as never,
createWorkspaceRelay: (() => ({
agents: {
get: async () => {
throw { status: 404 };
},
list: vi.fn(),
},
})) as never,
createWorkspace: vi.fn() as never,
log: (message: unknown) => stdout.push(String(message)),
error: (message: unknown) => stderr.push(String(message)),
exit: ((code: number) => {
throw new Error(`CLI exit ${code}`);
}) as never,
},
createFleetWorkspaceClient: (() => ({ agents: { release } })) as never,
deleteCloudFleetSandbox: deleteSandbox as never,
retireOwnedBindings: vi.fn(async () => undefined),
warn: (message: unknown) => stderr.push(String(message)),
});
return { program, stdout, stderr, receipt, release, deleteSandbox };
}

it.each([{ options: [] }, { options: ['--delete-agent'] }, { options: ['--delete-agent', '--wait'] }])(
'warns once for a dispatched release with options $options and preserves JSON stdout',
async ({ options }) => {
const { program, stdout, stderr, receipt, deleteSandbox } = fixture();
await program.parseAsync(['fleet', 'release', 'worker', '--workspace-key', 'rk_test', ...options], {
from: 'user',
});

expect(stdout).toHaveLength(1);
expect(JSON.parse(stdout[0]!)).toMatchObject(receipt);
expect(stderr).toHaveLength(1);
expect(stderr[0]).toContain('Release was dispatched');
expect(stderr[0]).toContain('Cloud/provider sandbox may keep running and incurring cost');
expect(stderr[0]).toContain('`fleet release` does not tear it down');
expect(deleteSandbox).not.toHaveBeenCalled();
}
);

it.each(['pending', 'completed', 'failed'])(
'does not claim dispatch for a %s release acknowledgement',
async (status) => {
const { program, stdout, stderr, receipt } = fixture(status);
await program.parseAsync(['fleet', 'release', 'worker', '--workspace-key', 'rk_test'], {
from: 'user',
});
expect(stdout).toHaveLength(1);
expect(JSON.parse(stdout[0]!)).toEqual(receipt);
expect(stderr).toEqual([]);
}
);

it('does not claim dispatch when release fails', async () => {
const { program, stdout, stderr, release } = fixture();
release.mockRejectedValueOnce(new Error('release unavailable'));
await expect(
program.parseAsync(['fleet', 'release', 'worker', '--workspace-key', 'rk_test'], {
from: 'user',
})
).rejects.toThrow('CLI exit 1');
expect(stdout).toEqual([]);
expect(stderr).toEqual(['release unavailable']);
});
});

it('fleet release delegates to the workspace lifecycle API with a default reason and actor', async () => {
const release = vi.fn(async () => ({
name: 'api-worker',
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/cli/commands/fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,11 @@ export function registerFleetCommands(
reason,
deleteAgent: false,
});
if (released.status === 'dispatched') {
deps.warn(
'Release was dispatched. If this worker is sandbox-backed, its Cloud/provider sandbox may keep running and incurring cost; `fleet release` does not tear it down.'
);
}
if (options.unsubscribeBindings === true || deleteAgent) {
try {
// Helper progress (`Retired ...`, `Unsubscribed ...`) must not land
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": 1,
"id": "1922-fleet-release-sandbox-warning",
"kind": "bugfix",
"title": "Warn that fleet release does not tear down Cloud sandboxes",
"runner": {
"command": ["node", "tests/relayflows/cases/1922-fleet-release-sandbox-warning/run.mjs"]
},
"requirements": [],
"timeoutSeconds": 900,
"expected": {
"base": {
"outcome": "bug",
"signature": "fleet_release_missing_sandbox_warning"
},
"head": {
"outcome": "fixed",
"signature": "fleet_release_warns_sandbox_may_keep_running"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import fs from 'node:fs';

import { Command } from 'commander';
import { expect, test, vi } from 'vitest';

// The runner copies this probe into <target>/.relay-pr-proof/.
import { registerFleetCommands } from '../packages/cli/src/cli/commands/fleet.js';

test('a dispatched fleet release keeps JSON stdout and reports sandbox costs on stderr', async () => {
const observationPath = process.env.RELAY_PR1922_OBSERVATION_PATH;
if (!observationPath) throw new Error('Missing RELAY_PR1922_OBSERVATION_PATH.');

const stdout: string[] = [];
const stderr: string[] = [];
const errors: string[] = [];
const receipt = {
invocationId: 'inv_release_proof',
actionName: 'agent.release',
status: 'dispatched',
};
const release = vi.fn(async () => receipt);
const deleteSandbox = vi.fn();
const program = new Command();
program.exitOverride();
// Exercise the production default log/warn sinks; only the remote lifecycle API is stubbed.
const log = vi.spyOn(console, 'log').mockImplementation((message) => stdout.push(String(message)));
const warn = vi.spyOn(console, 'warn').mockImplementation((message) => stderr.push(String(message)));
const error = vi.spyOn(console, 'error').mockImplementation((message) => errors.push(String(message)));

try {
registerFleetCommands(program, {
createFleetWorkspaceClient: (() => ({ agents: { release } })) as never,
deleteCloudFleetSandbox: deleteSandbox as never,
});
await program.parseAsync(['fleet', 'release', 'proof-worker', '--workspace-key', 'rk_proof'], {
from: 'user',
});

expect(errors).toEqual([]);
expect(stdout).toHaveLength(1);
expect(JSON.parse(stdout[0]!)).toEqual(receipt);
expect(release).toHaveBeenCalledExactlyOnceWith({
name: 'proof-worker',
reason: expect.stringMatching(/^fleet agent released \(actor: .+\)$/),
deleteAgent: false,
});
expect(deleteSandbox).not.toHaveBeenCalled();

const missingWarning = stderr.length === 0;
const hasWarning =
stderr.length === 1 &&
stderr[0]!.includes('Release was dispatched') &&
stderr[0]!.includes('Cloud/provider sandbox may keep running and incurring cost') &&
stderr[0]!.includes('`fleet release` does not tear it down');
expect(missingWarning || hasWarning, JSON.stringify(stderr)).toBe(true);
fs.writeFileSync(observationPath, JSON.stringify({ missingWarning, hasWarning }));
} finally {
log.mockRestore();
warn.mockRestore();
error.mockRestore();
}
});
Loading