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
5 changes: 5 additions & 0 deletions .changeset/prune-stale-facet-schedules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": patch
---

Prune stale sub-agent schedule rows when their owning facet registry entry no longer exists.
2 changes: 2 additions & 0 deletions examples/agents-as-tools/src/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import path from "node:path";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import agents from "agents/vite";
import { stripNodeModulesSourceMapReferences } from "../../../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

const testsDir = import.meta.dirname;

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
agents(),
cloudflareTest({
wrangler: {
Expand Down
2 changes: 2 additions & 0 deletions examples/assistant/src/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import path from "node:path";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import agents from "agents/vite";
import { stripNodeModulesSourceMapReferences } from "../../../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

const testsDir = import.meta.dirname;

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
agents(),
cloudflareTest({
wrangler: {
Expand Down
27 changes: 18 additions & 9 deletions packages/agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4304,7 +4304,7 @@ export class Agent<
async _cf_dispatchScheduledCallback(
ownerPath: ReadonlyArray<AgentPathStep>,
row: ScheduleStorageRow
): Promise<void> {
): Promise<boolean> {
const selfPath = this.selfPath;
if (!this._isSameAgentPathPrefix(selfPath, ownerPath)) {
throw new Error(
Expand All @@ -4314,24 +4314,33 @@ export class Agent<

if (selfPath.length === ownerPath.length) {
await this._executeScheduleCallback(row);
return;
return true;
}

const next = ownerPath[selfPath.length];
if (!this.hasSubAgent(next.className, next.name)) {
throw new Error(
`Scheduled sub-agent ${next.className} "${next.name}" no longer exists.`
);
// The target facet was deleted or its registry entry was lost. Since
// this schedule can no longer be dispatched through the public registry,
// prune root-side bookkeeping for the stale sub-tree instead of
// repeatedly re-arming the same impossible alarm.
const stalePath = ownerPath.slice(0, selfPath.length + 1);
if (this._isFacet) {
const root = await this._rootAlarmOwner();
await root._cf_cleanupFacetPrefix(stalePath);
} else {
await this._cf_cleanupFacetPrefix(stalePath);
}
return false;
}

const stub = await this._cf_resolveSubAgent(next.className, next.name);
const handle = stub as unknown as {
_cf_dispatchScheduledCallback(
ownerPath: ReadonlyArray<AgentPathStep>,
row: ScheduleStorageRow
): Promise<void>;
): Promise<boolean>;
};
await handle._cf_dispatchScheduledCallback(ownerPath, row);
return handle._cf_dispatchScheduledCallback(ownerPath, row);
}

/**
Expand Down Expand Up @@ -4680,7 +4689,7 @@ export class Agent<
if (row.owner_path) {
try {
const ownerPath = JSON.parse(row.owner_path) as AgentPathStep[];
await this._cf_dispatchScheduledCallback(ownerPath, row);
executed = await this._cf_dispatchScheduledCallback(ownerPath, row);
} catch (e) {
console.error(
`error dispatching scheduled callback "${row.callback}"`,
Expand Down Expand Up @@ -4710,8 +4719,8 @@ export class Agent<
}
} else {
await this._executeScheduleCallback(row);
executed = true;
}
executed = true;

if (this._destroyed) return;
if (!executed) continue;
Expand Down
12 changes: 4 additions & 8 deletions packages/agents/src/tests/sub-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ describe("SubAgent", () => {
).resolves.toMatch(/getSchedules\(\) is synchronous/);
});

it("should keep a schedule row when facet dispatch fails", async () => {
it("should prune a stale sub-agent schedule when the registry entry is gone", async () => {
const name = uniqueName();
const agent = await getAgentByName(env.TestSubAgentParent, name);

Expand All @@ -491,7 +491,7 @@ describe("SubAgent", () => {
await runDurableObjectAlarm(agent);

const rows = await agent.rootScheduleRows();
expect(rows.some((r) => r.id === scheduleId)).toBe(true);
expect(rows.some((r) => r.id === scheduleId)).toBe(false);
expect(await agent.subAgentScheduleLog("missing-registry-child")).toEqual(
[]
);
Expand Down Expand Up @@ -880,7 +880,7 @@ describe("SubAgent", () => {
expect(parentCancel).toHaveLength(0);
});

it("resets running=0 on interval rows when facet dispatch fails", async () => {
it("prunes stale interval rows when facet dispatch cannot reach the owner", async () => {
const name = uniqueName();
const agent = await getAgentByName(env.TestSubAgentParent, name);

Expand All @@ -896,13 +896,9 @@ describe("SubAgent", () => {

await runDurableObjectAlarm(agent);

// The row must still be there...
const rows = await agent.rootScheduleRows();
const row = rows.find((r) => r.id === intervalId);
expect(row).toBeDefined();
// ...and `running` must be back to 0 so the next alarm cycle
// actually retries instead of skipping the in-flight row.
expect(row?.running).toBe(0);
expect(row).toBeUndefined();
});

it("keepAlive() delegates heartbeat refs from a sub-agent to the root", async () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/agents/src/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import path from "node:path";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import agents from "agents/vite";
import { stripNodeModulesSourceMapReferences } from "../../../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

const testsDir = import.meta.dirname;

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
agents(),
cloudflareTest({
wrangler: {
Expand Down
2 changes: 2 additions & 0 deletions packages/ai-chat/src/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import path from "node:path";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import { stripNodeModulesSourceMapReferences } from "../../../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

const testsDir = import.meta.dirname;

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
cloudflareTest({
wrangler: {
configPath: path.join(testsDir, "wrangler.jsonc")
Expand Down
2 changes: 2 additions & 0 deletions packages/codemode/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import { stripNodeModulesSourceMapReferences } from "../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
cloudflareTest({
wrangler: { configPath: "./wrangler.jsonc" }
})
Expand Down
2 changes: 2 additions & 0 deletions packages/shell/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import { stripNodeModulesSourceMapReferences } from "../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
cloudflareTest({
wrangler: { configPath: "./wrangler.jsonc" }
})
Expand Down
2 changes: 2 additions & 0 deletions packages/think/src/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import path from "node:path";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import { stripNodeModulesSourceMapReferences } from "../../../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

const testsDir = import.meta.dirname;

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
cloudflareTest({
wrangler: {
configPath: path.join(testsDir, "wrangler.jsonc")
Expand Down
2 changes: 2 additions & 0 deletions packages/voice/src/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import path from "node:path";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import { stripNodeModulesSourceMapReferences } from "../../../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

const testsDir = import.meta.dirname;

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
cloudflareTest({
wrangler: {
configPath: path.join(testsDir, "wrangler.jsonc")
Expand Down
2 changes: 2 additions & 0 deletions packages/worker-bundler/src/tests/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import path from "node:path";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
import { stripNodeModulesSourceMapReferences } from "../../../../scripts/vitest/strip-node-modules-source-map-references";
import { defineConfig } from "vitest/config";

const testsDir = import.meta.dirname;

export default defineConfig({
plugins: [
stripNodeModulesSourceMapReferences(),
cloudflareTest({
wrangler: { configPath: path.join(testsDir, "wrangler.jsonc") }
})
Expand Down
33 changes: 33 additions & 0 deletions scripts/vitest/strip-node-modules-source-map-references.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { readFile } from "node:fs/promises";
import type { Plugin } from "vite";

const sourceMapCommentRE =
/(?:\/\/# sourceMappingURL=.*|\/\*# sourceMappingURL=.*?\*\/)\s*$/s;

/**
* Some published dependencies include source map references without publishing
* the original source files. Vite warns when it tries to hydrate those maps,
* which makes test output noisy without improving debuggability.
*/
export function stripNodeModulesSourceMapReferences(): Plugin {
return {
name: "strip-node-modules-source-map-references",
enforce: "pre",
async load(id) {
const file = id.split("?", 1)[0];
if (!file.includes("/node_modules/") || !/\.[cm]?js$/.test(file)) {
return null;
}

const code = await readFile(file, "utf8").catch(() => null);
if (!code?.includes("sourceMappingURL=")) {
return null;
}

return {
code: code.replace(sourceMapCommentRE, ""),
map: null
};
}
};
}
Loading