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
1 change: 0 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"ignore": [
"@corbits/agent-directory",
"@corbits/agent-events",
"@corbits/agent-lifecycle",
"@corbits/agent-runtime",
"@corbits/api-query",
"@corbits/approvals",
Expand Down
1 change: 0 additions & 1 deletion apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"dependencies": {
"@chat-adapter/state-memory": "^4.35.0",
"@corbits/agent-directory": "workspace:*",
"@corbits/agent-lifecycle": "workspace:*",
"@corbits/agent-runtime": "workspace:*",
"@corbits/approvals": "workspace:*",
"@corbits/artifacts": "github:corbitsdev/corbits-artifacts#dc435ba749b4378f19e5de772159c2a57e932938",
Expand Down
39 changes: 0 additions & 39 deletions apps/hub/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,6 @@ const HubEnv = type({
"HUB_SIDECAR_WEBSOCKET_URL?": type(/^wss?:\/\/.+$/).describe(
"the ws(s):// URL a provisioned sidecar container dials back to reach this hub; unset (default) derives it from BASE_URL, which is wrong for a docker sidecar provisioner — that container's own localhost is itself, not the hub host — so set this whenever SIDECAR_PROVISIONER=docker",
),
"HUB_CHAT_IDLE_REAP_MS?": type("string").describe(
"how long a chat resident (host or invited agent) may sit idle before the hub reaps it via a state-preserving undeploy, in milliseconds; unset defaults to 30 minutes",
),
});

const DEFAULT_SIGNUP_RATE_LIMIT_WINDOW_SECONDS = 60;
Expand All @@ -161,34 +158,6 @@ const DEFAULT_SIGNUP_RATE_LIMIT_MAX = 5;
const DEFAULT_SIGNIN_RATE_LIMIT_WINDOW_SECONDS = 60;
const DEFAULT_SIGNIN_RATE_LIMIT_MAX = 10;

/**
* Production default for `HUB_CHAT_IDLE_REAP_MS`: 30 minutes,
* matching the old sidecar-side `WORKBENCH_CHILD_IDLE_REAP_MS` default
* this replaces for chat.
*/
export const DEFAULT_CHAT_IDLE_REAP_MS = 30 * 60_000;

/**
* Parse an optional positive-integer-milliseconds env value, defaulting
* to `fallback` when unset. Rejects zero and negative values — unlike
* the sidecar's now-deleted reap knob, there is no "disable reaping"
* mode here.
*/
function parsePositiveMsEnv(
raw: string | undefined,
name: string,
fallback: number,
): number {
if (raw === undefined) return fallback;
const n = Number(raw);
if (!Number.isInteger(n) || n <= 0) {
throw new Error(
`invalid hub environment: ${name} must be a positive integer (milliseconds); got ${JSON.stringify(raw)}`,
);
}
return n;
}

// One member per implemented `SidecarProvisioner` backend. Adding a new
// backend (e.g. a remote sandbox) is: implement the contract in its own
// package, add a member here with its settings, add its id to
Expand Down Expand Up @@ -289,9 +258,6 @@ export type HubConfig = {
/** Overrides the ws(s):// URL a provisioned sidecar dials back to reach
* this hub. Unset derives it from baseUrl instead. */
readonly sidecarWebSocketUrl?: string;
/** How long an idle chat resident may sit before the hub reaps it via
* a state-preserving undeploy. Defaults to `DEFAULT_CHAT_IDLE_REAP_MS`. */
readonly chatIdleReapMs: number;
};

type ParsedHubEnv = typeof HubEnv.infer;
Expand Down Expand Up @@ -535,11 +501,6 @@ export function readHubConfig(
? Number(parsed.SIGNIN_RATE_LIMIT_MAX)
: DEFAULT_SIGNIN_RATE_LIMIT_MAX,
},
chatIdleReapMs: parsePositiveMsEnv(
parsed.HUB_CHAT_IDLE_REAP_MS,
"HUB_CHAT_IDLE_REAP_MS",
DEFAULT_CHAT_IDLE_REAP_MS,
),
};
if (parsed.HUB_ALLOW_GIT_INSIDE_WORK_TREE !== undefined)
hubConfig.allowGitInsideWorkTree = true;
Expand Down
44 changes: 11 additions & 33 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,20 +281,12 @@ const TENANT_PREFIX = "/api/tenants/:tenantId";
const SIGN_UP_EMAIL_PATH = "/sign-up/email";
const SIGN_IN_EMAIL_PATH = "/sign-in/email";
const SignInEmailBody = type({ email: "string" });
// Chat residents carry a real hub-driven idle-reap again (reversing
// CL-5477's removal): the sidecar's own park/wake scheme it was meant to
// replace has itself been retired in favor of a simpler reap-and-relaunch
// model. `createHubChatPlatform`'s `lifecycle` binding below tags every
// idle eviction with `@corbits/agent-lifecycle`'s
// `IDLE_HIBERNATE_UNDEPLOY_REASON`, which the sidecar's `agent.undeploy`
// handler matches to choose the state-preserving teardown flavor
// (`reclaimDirs: false` — deployment record, step-state, and slug all
// survive) rather than the destructive default a caller-initiated
// undeploy gets. That is what makes this safe to re-enable where the old
// bare `"idle"`-tagged undeploy this ticket's `CHAT_IDLE_SLEEP_MS`
// emergency bump (8ca85543) band-aided around was genuinely lossy: a
// later `wakeByAddress` relaunch resumes the same run rather than
// starting a fresh one.
// CL-8187: idle hibernation is the sidecar's own decision now — it tracks
// per-run last activity itself and tears an idle run down with a
// state-preserving teardown, keeping the deployment record, step-state,
// and slug so a later `wakeByAddress` relaunch resumes the same run
// rather than starting a fresh one. The hub no longer drives this at
// all: `createHubChatPlatform` carries no idle-reap wiring.

// Email+password signup stays available through better-auth; the hub
// applies only coarse throttling around it, never an operator gate.
Expand Down Expand Up @@ -1301,13 +1293,6 @@ export async function createHub(config: HubConfig) {
),
hubUrl: config.baseUrl,
}),
// Chat residents are undeployed on idle again (see the comment above
// this function): `chatIdleReapMs` (env-overridable via
// `HUB_CHAT_IDLE_REAP_MS`, default 30 minutes) is
// state-preserving (`IDLE_HIBERNATE_UNDEPLOY_REASON`), unlike a
// destructive undeploy.
lifecycle: { idleSleepMs: config.chatIdleReapMs },
//
// A hand-authored definition with no model requirements of its own
// (see `@corbits/agent-directory`'s `createAgentDefinitionCore`
// doc) still launches on invite by falling back to this same
Expand Down Expand Up @@ -1351,11 +1336,7 @@ export async function createHub(config: HubConfig) {
// and a gate-blocked run's approval park into an in-chat approve
// block, by subscribing to the sidecar's own event stream, replacing
// the old per-agent reply-bridge machinery armed (and re-armed) from
// inside the routes. `chatPlatform.recordActivity` is the same
// idle-sleep lifecycle `chatPlatform` itself drives — wiring it here
// too is what keeps a replying agent's activity clock current even
// though the reply never goes through `chatPlatform.sendMail`'s own
// `recordActivity` call. `approvals` is the same `ApprovalStore` the
// inside the routes. `approvals` is the same `ApprovalStore` the
// platform's own approve/reject routes read and write — this
// orchestrator only ever reads it.
const agentTurns = createDrizzleAgentTurnStore(db);
Expand All @@ -1368,7 +1349,6 @@ export async function createHub(config: HubConfig) {
platform: chatPlatform,
events: sidecarRouter.events,
approvals: createApprovalStore(db),
recordActivity: chatPlatform.recordActivity,
claims: writeClaims,
threads: threadStore,
turnMailCorrelation,
Expand Down Expand Up @@ -1554,12 +1534,10 @@ export async function createHub(config: HubConfig) {
return userRow?.name ?? undefined;
},
commands: commandRegistry,
// The same native undeploy call the idle-sleep lifecycle uses to
// tear an invited agent's instance down (see `chatPlatform`'s own
// `lifecycle.undeploy` above) — wired here too so removing an agent
// from a chat's participants releases its running instance the
// same way, rather than leaving it deployed with nothing routing
// messages to it.
// The native undeploy call this hub uses to tear an invited agent's
// instance down when it is removed from a chat's participants,
// rather than leaving it deployed with nothing routing messages to
// it.
releaseAgentInstance: (address, reason) =>
sidecarRouter.sendAgentUndeploy(address, reason),
// CL-7450: fans a sent human message into every human participant's
Expand Down
2 changes: 0 additions & 2 deletions apps/hub/test/boot-does-not-seed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ describeIfDb("createHub on a scratch database inserts no tenant", () => {
socialProviders: {},
allowPlaintextSecrets: true,
sidecarProvisioners: [],
chatIdleReapMs: 30 * 60_000,
};
const hub = await createHub(config);
const stop = async () => {
Expand Down Expand Up @@ -348,7 +347,6 @@ describeIfDb("boot with provider env vars plants nothing (CL-7579)", () => {
socialProviders: {},
allowPlaintextSecrets: true,
sidecarProvisioners: [],
chatIdleReapMs: 30 * 60_000,
};

// Phase 1: mint the operator tenant the old plant targeted.
Expand Down
1 change: 0 additions & 1 deletion apps/hub/test/chat-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const config: HubConfig = {
// credential-cipher seam, so the dev opt-in keeps boot working.
allowPlaintextSecrets: true,
sidecarProvisioners: [],
chatIdleReapMs: 30 * 60_000,
};

const closers: (() => Promise<void>)[] = [];
Expand Down
1 change: 0 additions & 1 deletion apps/hub/test/composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const config: HubConfig = {
// credential-cipher seam, so the dev opt-in keeps boot working.
allowPlaintextSecrets: true,
sidecarProvisioners: [],
chatIdleReapMs: 30 * 60_000,
};

const closers: (() => Promise<void>)[] = [];
Expand Down
32 changes: 0 additions & 32 deletions apps/hub/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ describe("readHubConfig", () => {
allowPlaintextSecrets: false,
sidecarProvisioners: [{ id: "process" }],
defaultSidecarProvisionerId: "process",
chatIdleReapMs: 30 * 60_000,
});
});

Expand All @@ -51,7 +50,6 @@ describe("readHubConfig", () => {
[
"allowPlaintextSecrets",
"baseUrl",
"chatIdleReapMs",
"databaseUrl",
"defaultSidecarProvisionerId",
"hubDataDir",
Expand Down Expand Up @@ -459,36 +457,6 @@ describe("readHubConfig", () => {
});
});

describe("chatIdleReapMs", () => {
test("defaults to 30 minutes when HUB_CHAT_IDLE_REAP_MS is unset", () => {
expect(readHubConfig(validEnv).chatIdleReapMs).toBe(30 * 60_000);
});

test("is read from HUB_CHAT_IDLE_REAP_MS when set", () => {
const config = readHubConfig({
...validEnv,
HUB_CHAT_IDLE_REAP_MS: "5000",
});
expect(config.chatIdleReapMs).toBe(5000);
});

test("rejects zero", () => {
const message = readExpectingError({
...validEnv,
HUB_CHAT_IDLE_REAP_MS: "0",
});
expect(message).toContain("HUB_CHAT_IDLE_REAP_MS");
});

test("rejects a non-integer value", () => {
const message = readExpectingError({
...validEnv,
HUB_CHAT_IDLE_REAP_MS: "not-a-number",
});
expect(message).toContain("HUB_CHAT_IDLE_REAP_MS");
});
});

test("an empty environment reports every variable in one error", () => {
const message = readExpectingError({});
for (const name of [
Expand Down
1 change: 0 additions & 1 deletion apps/hub/test/credential-cipher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const baseConfig: HubConfig = {
socialProviders: {},
allowPlaintextSecrets: false,
sidecarProvisioners: [],
chatIdleReapMs: 30 * 60_000,
};

describe("credentialCipherFrom", () => {
Expand Down
1 change: 0 additions & 1 deletion apps/hub/test/presence-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const config: HubConfig = {
socialProviders: {},
allowPlaintextSecrets: true,
sidecarProvisioners: [],
chatIdleReapMs: 30 * 60_000,
};

const closers: (() => Promise<void>)[] = [];
Expand Down
1 change: 0 additions & 1 deletion apps/hub/test/relaunch-sweep-close.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const config: HubConfig = {
signInRateLimit: { windowSeconds: 60, max: 10 },
socialProviders: {},
sidecarProvisioners: [],
chatIdleReapMs: 30 * 60_000,
allowPlaintextSecrets: true,
};

Expand Down
1 change: 0 additions & 1 deletion apps/sidecar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"test": "bun test"
},
"dependencies": {
"@corbits/agent-lifecycle": "workspace:*",
"@corbits/codex-provider": "workspace:*",
"@corbits/credential-providers": "workspace:*",
"@corbits/error-sink": "workspace:*",
Expand Down
17 changes: 17 additions & 0 deletions apps/sidecar/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ const SidecarEnv = type({
// `SIDECAR_CACHE_MAX_BYTES`-style numeric env keys.
"CONSUMED_RETENTION_MS?": "string",
"CHILD_READY_TIMEOUT_MS?": "string",
// CL-8187: how long a deployment may sit with no observed inbound mail
// before this sidecar's own idle-hibernate sweep tears it down with a
// state-preserving undeploy (deployment record, step-state, and slug
// survive so a later wake resumes the same run). Unset disables the
// sweep entirely -- nothing is tracked, no interval runs -- matching
// the behavior before this knob existed.
"CHAT_IDLE_HIBERNATE_MS?": "string",
});

/**
Expand Down Expand Up @@ -148,6 +155,12 @@ export type SidecarConfig = {
* override it; the supervisor applies `DEFAULT_READY_TIMEOUT_MS` (30s).
*/
readonly readyTimeoutMs: number | undefined;
/**
* How long a deployment may sit idle before this sidecar's own
* idle-hibernate sweep tears it down. `undefined` means the operator
* did not set `CHAT_IDLE_HIBERNATE_MS`; the sweep does not run at all.
*/
readonly idleHibernateMs: number | undefined;
};

/**
Expand Down Expand Up @@ -219,5 +232,9 @@ export function readSidecarConfig(
parsed.CHILD_READY_TIMEOUT_MS,
"CHILD_READY_TIMEOUT_MS",
),
idleHibernateMs: parsePositiveMsEnv(
parsed.CHAT_IDLE_HIBERNATE_MS,
"CHAT_IDLE_HIBERNATE_MS",
),
};
}
9 changes: 8 additions & 1 deletion apps/sidecar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,15 @@ const orchestrator = createSidecarOrchestrator({
readyTimeoutMs: config.readyTimeoutMs,
}
: deployRouterConfigWithConsumedRetentionMs;
const deployRouterConfigWithIdleHibernateMs =
config.idleHibernateMs !== undefined
? {
...deployRouterConfigWithReadyTimeoutMs,
idleHibernateMs: config.idleHibernateMs,
}
: deployRouterConfigWithReadyTimeoutMs;
const router = createSidecarDeployRouter(
deployRouterConfigWithReadyTimeoutMs,
deployRouterConfigWithIdleHibernateMs,
);
capturedRouter = router;
return router;
Expand Down
Loading
Loading