Skip to content
Merged
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
7 changes: 4 additions & 3 deletions hosting/docker-compose/ee/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,10 @@ services:
AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM: ${AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM:-}
AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES: ${AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES:-}
# This service has no env_file (see above), so the session flags must be listed
# here or they can never be set. AGENTA_SESSIONS_RECONSTRUCT must be enabled
# together with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that
# carries only its last message loses the conversation.
# here or they can never be set. Both default ON: absent or empty means on, and
# only the literal "false" disables. Disable AGENTA_SESSIONS_RECONSTRUCT together
# with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that carries
# only its last message loses the conversation.
AGENTA_SESSIONS_RECONSTRUCT: ${AGENTA_SESSIONS_RECONSTRUCT:-}
AGENTA_RECORDS_DURABLE: ${AGENTA_RECORDS_DURABLE:-}
# === STORAGE ============================================== #
Expand Down
7 changes: 4 additions & 3 deletions hosting/docker-compose/ee/docker-compose.gh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,10 @@ services:
AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM: ${AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM:-}
AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES: ${AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES:-}
# This service has no env_file (see above), so the session flags must be listed
# here or they can never be set. AGENTA_SESSIONS_RECONSTRUCT must be enabled
# together with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that
# carries only its last message loses the conversation.
# here or they can never be set. Both default ON: absent or empty means on, and
# only the literal "false" disables. Disable AGENTA_SESSIONS_RECONSTRUCT together
# with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that carries
# only its last message loses the conversation.
AGENTA_SESSIONS_RECONSTRUCT: ${AGENTA_SESSIONS_RECONSTRUCT:-}
AGENTA_RECORDS_DURABLE: ${AGENTA_RECORDS_DURABLE:-}
# === SUBSCRIPTION MOUNTS (opt-in) ========================= #
Expand Down
17 changes: 9 additions & 8 deletions hosting/docker-compose/ee/env.ee.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,17 @@ AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER=local
# 5 min per tool call.
# AGENTA_RUNNER_TOOL_CALL_TIMEOUT_MS=300000

# --- Session history: last-message-only turns (all four default off) ---
# --- Session history: last-message-only turns ---
# The client sends only the newest user message and the runner rebuilds the earlier turns
# from the durable session record log, which keeps requests and traces small on a long
# conversation. The first two must be flipped together: with the web flag on and the runner
# flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=true
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=true
# Records become the only copy of the conversation, so make writing them durable and keep
# the structure of a record whose body exceeds the API size cap.
# AGENTA_RECORDS_DURABLE=true
# conversation. The three flags below default ON (absent or empty means on); set the
# literal "false" to disable. Disable the first two together: with the web flag on and
# the runner flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=false
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=false
# AGENTA_RECORDS_DURABLE=false
# Smart truncation preserves the structure of a record whose body exceeds the API size
# cap (higher-fidelity reconstruction). Still opt-in, default off.
# AGENTA_RECORDS_SMART_TRUNCATION=true

# ================================================================== #
Expand Down
17 changes: 9 additions & 8 deletions hosting/docker-compose/ee/env.ee.gh.example
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,17 @@ AGENTA_RUNNER_TOKEN=replace-me
# 5 min per tool call.
# AGENTA_RUNNER_TOOL_CALL_TIMEOUT_MS=300000

# --- Session history: last-message-only turns (all four default off) ---
# --- Session history: last-message-only turns ---
# The client sends only the newest user message and the runner rebuilds the earlier turns
# from the durable session record log, which keeps requests and traces small on a long
# conversation. The first two must be flipped together: with the web flag on and the runner
# flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=true
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=true
# Records become the only copy of the conversation, so make writing them durable and keep
# the structure of a record whose body exceeds the API size cap.
# AGENTA_RECORDS_DURABLE=true
# conversation. The three flags below default ON (absent or empty means on); set the
# literal "false" to disable. Disable the first two together: with the web flag on and
# the runner flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=false
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=false
# AGENTA_RECORDS_DURABLE=false
# Smart truncation preserves the structure of a record whose body exceeds the API size
# cap (higher-fidelity reconstruction). Still opt-in, default off.
# AGENTA_RECORDS_SMART_TRUNCATION=true

# ================================================================== #
Expand Down
7 changes: 4 additions & 3 deletions hosting/docker-compose/oss/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,10 @@ services:
AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM: ${AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM:-}
AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES: ${AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES:-}
# This service has no env_file (see above), so the session flags must be listed
# here or they can never be set. AGENTA_SESSIONS_RECONSTRUCT must be enabled
# together with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that
# carries only its last message loses the conversation.
# here or they can never be set. Both default ON: absent or empty means on, and
# only the literal "false" disables. Disable AGENTA_SESSIONS_RECONSTRUCT together
# with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that carries
# only its last message loses the conversation.
AGENTA_SESSIONS_RECONSTRUCT: ${AGENTA_SESSIONS_RECONSTRUCT:-}
AGENTA_RECORDS_DURABLE: ${AGENTA_RECORDS_DURABLE:-}
# === STORAGE ============================================== #
Expand Down
7 changes: 4 additions & 3 deletions hosting/docker-compose/oss/docker-compose.gh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,10 @@ services:
AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM: ${AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM:-}
AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES: ${AGENTA_RUNNER_DAYTONA_AUTODELETE_MINUTES:-}
# This service has no env_file (see above), so the session flags must be listed
# here or they can never be set. AGENTA_SESSIONS_RECONSTRUCT must be enabled
# together with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that
# carries only its last message loses the conversation.
# here or they can never be set. Both default ON: absent or empty means on, and
# only the literal "false" disables. Disable AGENTA_SESSIONS_RECONSTRUCT together
# with the web NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY, or a turn that carries
# only its last message loses the conversation.
AGENTA_SESSIONS_RECONSTRUCT: ${AGENTA_SESSIONS_RECONSTRUCT:-}
AGENTA_RECORDS_DURABLE: ${AGENTA_RECORDS_DURABLE:-}
# === SUBSCRIPTION MOUNTS (opt-in) ========================= #
Expand Down
17 changes: 9 additions & 8 deletions hosting/docker-compose/oss/env.oss.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,17 @@ AGENTA_RUNNER_DEFAULT_SANDBOX_PROVIDER=local
# 5 min per tool call.
# AGENTA_RUNNER_TOOL_CALL_TIMEOUT_MS=300000

# --- Session history: last-message-only turns (all four default off) ---
# --- Session history: last-message-only turns ---
# The client sends only the newest user message and the runner rebuilds the earlier turns
# from the durable session record log, which keeps requests and traces small on a long
# conversation. The first two must be flipped together: with the web flag on and the runner
# flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=true
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=true
# Records become the only copy of the conversation, so make writing them durable and keep
# the structure of a record whose body exceeds the API size cap.
# AGENTA_RECORDS_DURABLE=true
# conversation. The three flags below default ON (absent or empty means on); set the
# literal "false" to disable. Disable the first two together: with the web flag on and
# the runner flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=false
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=false
# AGENTA_RECORDS_DURABLE=false
# Smart truncation preserves the structure of a record whose body exceeds the API size
# cap (higher-fidelity reconstruction). Still opt-in, default off.
# AGENTA_RECORDS_SMART_TRUNCATION=true

# ================================================================== #
Expand Down
17 changes: 9 additions & 8 deletions hosting/docker-compose/oss/env.oss.gh.example
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,17 @@ AGENTA_RUNNER_TOKEN=replace-me
# 5 min per tool call.
# AGENTA_RUNNER_TOOL_CALL_TIMEOUT_MS=300000

# --- Session history: last-message-only turns (all four default off) ---
# --- Session history: last-message-only turns ---
# The client sends only the newest user message and the runner rebuilds the earlier turns
# from the durable session record log, which keeps requests and traces small on a long
# conversation. The first two must be flipped together: with the web flag on and the runner
# flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=true
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=true
# Records become the only copy of the conversation, so make writing them durable and keep
# the structure of a record whose body exceeds the API size cap.
# AGENTA_RECORDS_DURABLE=true
# conversation. The three flags below default ON (absent or empty means on); set the
# literal "false" to disable. Disable the first two together: with the web flag on and
# the runner flag off, a turn arrives with no history at all.
# AGENTA_SESSIONS_RECONSTRUCT=false
# NEXT_PUBLIC_SESSIONS_LAST_MESSAGE_ONLY=false
# AGENTA_RECORDS_DURABLE=false
# Smart truncation preserves the structure of a record whose body exceeds the API size
# cap (higher-fidelity reconstruction). Still opt-in, default off.
# AGENTA_RECORDS_SMART_TRUNCATION=true

# ================================================================== #
Expand Down
10 changes: 6 additions & 4 deletions services/runner/src/engines/sandbox_agent/reconstruct-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* Seam that lets the runner rebuild prior conversation from the durable record log instead of
* trusting a full inbound history — the server side of "client sends only the last message".
*
* Flag-gated (`AGENTA_SESSIONS_RECONSTRUCT`) and a strict no-op until BOTH the flag is on AND the
* client actually sent a minimal history (`carriesMinimalHistory`). When it does not apply, the
* inbound history is left untouched.
* Flag-gated (`AGENTA_SESSIONS_RECONSTRUCT`, ON unless set to the literal "false") and a strict
* no-op until BOTH the flag is on AND the client actually sent a minimal history
* (`carriesMinimalHistory`). When it does not apply, the inbound history is left untouched.
*
* When it DOES apply it is no longer best-effort, because the client kept no copy of the
* conversation: an unreadable log, or one known to have dropped a record, fails the turn rather
Expand All @@ -22,9 +22,11 @@ import { recordsIncomplete } from "../../sessions/persist.ts";
import { reconstructMessages } from "../../sessions/reconstruct.ts";
import { carriesMinimalHistory } from "./session-identity.ts";

// ON unless the literal "false"; absent AND empty both mean on (compose passes `${VAR:-}`,
// which sets an empty string when the shell has no value).
function reconstructEnabled(): boolean {
return (
String(process.env.AGENTA_SESSIONS_RECONSTRUCT ?? "").toLowerCase() === "true"
String(process.env.AGENTA_SESSIONS_RECONSTRUCT ?? "").trim().toLowerCase() !== "false"
);
}

Expand Down
10 changes: 5 additions & 5 deletions services/runner/src/engines/sandbox_agent/run-turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,11 @@ export async function runTurn(
});

try {
// Server-side history reconstruction (flag-gated, no-op by default): when the client sent a
// minimal history, rebuild prior turns from the durable record log so a cold turn still has
// full context. Runs before the current user turn is persisted, so records hold only prior
// turns. Reassigns `request` so every downstream reader (turnText, priorMessages, responder,
// otel) sees the same reconstructed history.
// Server-side history reconstruction (on by default; AGENTA_SESSIONS_RECONSTRUCT=false
// disables): when the client sent a minimal history, rebuild prior turns from the durable
// record log so a cold turn still has full context. Runs before the current user turn is
// persisted, so records hold only prior turns. Reassigns `request` so every downstream
// reader (turnText, priorMessages, responder, otel) sees the same reconstructed history.
const reconstructed = await reconstructHistoryIfNeeded(
request,
sessionId,
Expand Down
17 changes: 10 additions & 7 deletions services/runner/src/sessions/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
* - The run is never blocked on persistence mid-stream (chain is fire-and-forget).
* - The run DOES drain before teardown so the last event is not lost to the race.
* - A persist failure is logged and swallowed; the SDK's in-memory replay store is the
* backstop. Three retries with linear backoff before the event is dropped. With
* `AGENTA_RECORDS_DURABLE=true` the retry is stronger (more attempts, exponential backoff)
* and a drop is COUNTED per session (see `takePersistFailures`), so the turn-end drain can
* tell whether the durable history is complete enough to reconstruct model context from.
* backstop. By default (durable mode, on unless `AGENTA_RECORDS_DURABLE=false`) the retry
* is strong (more attempts, exponential backoff) and a drop is COUNTED per session (see
* `takePersistFailures`), so the turn-end drain can tell whether the durable history is
* complete enough to reconstruct model context from. With the flag set to "false", three
* retries with linear backoff before the event is dropped, uncounted (legacy path).
* - `record_source` marks who authored the record: "agent" for engine-emitted events,
* "user" for the inbound user turn persisted at run start.
*/
Expand All @@ -39,10 +40,12 @@ const DURABLE_INGEST_MAX_RETRIES = 6;
// stops being a tuning knob and becomes a hung turn.
const DURABLE_INGEST_MAX_RETRIES_CAP = 12;

/** Durable-records upgrades (stronger retry + drop counting) are opt-in and read at call time
* so the flag can be toggled per test. Off → the fire-and-forget legacy path, unchanged. */
/** Durable-records upgrades (stronger retry + drop counting) are ON unless the flag is the
* literal "false"; read at call time so the flag can be toggled per test. Absent AND empty both
* mean on — the compose files pass the var through as `${AGENTA_RECORDS_DURABLE:-}`, which sets
* an empty string when unset. "false" → the fire-and-forget legacy path, unchanged. */
function durableRecordsEnabled(): boolean {
return String(process.env.AGENTA_RECORDS_DURABLE ?? "").toLowerCase() === "true";
return String(process.env.AGENTA_RECORDS_DURABLE ?? "").trim().toLowerCase() !== "false";
}

/** Attempts before a durable-mode drop; env-overridable for ops tuning (and fast tests). */
Expand Down
7 changes: 7 additions & 0 deletions services/runner/tests/setup/hermetic-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,18 @@ const SCRUBBED = [

for (const name of SCRUBBED) delete process.env[name];

// Server-side history reconstruction defaults ON, so any single-user-message turn with a
// session id would reach for a live records endpoint mid-test. Engine suites are not
// reconstruction tests: pin it off here. session-reconstruct-history.test.ts deletes the pin
// in its own beforeEach to exercise the real default.
process.env.AGENTA_SESSIONS_RECONSTRUCT = "false";

// Re-scrub per test: a prior test may have set one and not restored it. Also drop the memoized
// runner config so the next `loadRunnerConfig()` re-parses the scrubbed environment.
beforeEach(() => {
for (const name of SCRUBBED) delete process.env[name];
// Restore the stub bundle so a prior test's override (to force a failed install) cannot leak.
process.env.SANDBOX_AGENT_EXTENSION_BUNDLE = STUB_EXTENSION_BUNDLE;
process.env.AGENTA_SESSIONS_RECONSTRUCT = "false";
resetRunnerConfigCache();
});
26 changes: 25 additions & 1 deletion services/runner/tests/unit/session-persist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,8 @@ describe("buildPersistingEmitter API contract", () => {
});

describe("durable records (AGENTA_RECORDS_DURABLE)", () => {
it("legacy (flag off): a permanent failure is dropped and NOT counted", async () => {
it('legacy (explicitly "false"): a permanent failure is dropped and NOT counted', async () => {
vi.stubEnv("AGENTA_RECORDS_DURABLE", "false");
fetchFailCount = 99; // never succeeds
const { emit, flush } = buildPersistingEmitter("sess-legacy", () => "t");
emit({ type: "message", text: "x" });
Expand All @@ -494,6 +495,29 @@ describe("durable records (AGENTA_RECORDS_DURABLE)", () => {
assert.equal(takePersistFailures("sess-durable"), 0);
});

it("durable is the default: an absent flag counts the drop", async () => {
vi.stubEnv("AGENTA_RECORDS_INGEST_MAX_RETRIES", "2"); // keep the test fast
fetchFailCount = 99;
const { emit } = buildPersistingEmitter("sess-default", () => "t");
emit({ type: "message", text: "x" });
await drainPersist("sess-default");

assert.equal(postedBodies.length, 0);
assert.equal(takePersistFailures("sess-default"), 1);
});

it('an empty flag (compose "${VAR:-}" passthrough) still means durable', async () => {
vi.stubEnv("AGENTA_RECORDS_DURABLE", "");
vi.stubEnv("AGENTA_RECORDS_INGEST_MAX_RETRIES", "2");
fetchFailCount = 99;
const { emit } = buildPersistingEmitter("sess-empty", () => "t");
emit({ type: "message", text: "x" });
await drainPersist("sess-empty");

assert.equal(postedBodies.length, 0);
assert.equal(takePersistFailures("sess-empty"), 1);
});

it("durable: the turn-end flush consumes the drop count (warns + clears)", async () => {
vi.stubEnv("AGENTA_RECORDS_DURABLE", "true");
vi.stubEnv("AGENTA_RECORDS_INGEST_MAX_RETRIES", "2");
Expand Down
Loading
Loading