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
8 changes: 6 additions & 2 deletions extensions/gentle-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2060,13 +2060,17 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
? `repository: ${fact.root} · clone: ${fact.cloneHash} · Git resolved at: ${fact.resolvedAt} (${fact.source})`
: "repository: unknown";
const rows = peers.map(peer => {
const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` : " · context: unknown";
const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` : peer.legacyLabel !== undefined
? ` · ${peer.legacyLabel || "unnamed"} · context: legacy (this session predates discovery publication; restart it to publish its workspace and repository)`
: " · context: unknown";
const tasks = peer.tasks?.map(task => `\n - ${task.label || task.id} [${task.status}] · launch workspace: ${task.workspace || "unknown"} · ${repository(peer.scope?.tasks.find(t => t.id === task.id)?.repository)}`).join("") ?? "";
const registered = peer.scope?.registered.map(fact => `\n registered: ${repository(fact)}`).join("") ?? "";
const gaps = peer.scope && !peer.scope.complete ? `\n (scope incomplete: ${peer.scope.omittedTasks} tasks, ${peer.scope.omittedRegistered} registered roots omitted)` : "";
const catalog = peer.catalog ? `\n recorded catalog (not Git identity): ${JSON.stringify(peer.catalog)}` : "\n recorded catalog: unknown";
const note = params.recipient_session_id ? `\n owner-curated recorded state (not consent or owner reply; authority none): ${peer.state ? JSON.stringify(peer.state) : "unknown"}` : "";
return `- ${peer.sessionId}${context} · ${repository(peer.scope?.host)} · metadata: ${peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
// A legacy peer never publishes its scope: say so instead of reporting an unknown.
const legacy = peer.legacyLabel !== undefined;
return `- ${peer.sessionId}${context} · ${legacy ? "repository: not published" : repository(peer.scope?.host)} · metadata: ${legacy ? "legacy" : peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
});
return peers.length === 0 ? text("No other sessions are currently advertised. Advertisements have unknown reachability and do not guarantee a live session.") : text(`Advertised sessions (reachability is unknown):\n${rows.join("\n")}`, { gentleAgents: { candidates: peers } });
} catch {
Expand Down
6 changes: 5 additions & 1 deletion lib/orchestrator-discovery.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { readCatalog, type CatalogPage } from "./orchestrator-catalog.ts";
import type { PublishedWork } from "./orchestrator-work.ts";
import type { PresenceRecord } from "./agents-session-transport.ts";
import { activationHash, listPresence, readDiscovery, sessionHash, type DiscoveryMetadata } from "./orchestrator-presence.ts";
import { activationHash, discoveryAbsent, listPresence, readDiscovery, sessionHash, type DiscoveryMetadata } from "./orchestrator-presence.ts";

export interface OrchestratorCandidate {
sessionId: string;
reachability: "unknown";
freshness: "unknown" | "recent" | "stale";
label?: string;
/** Display-only label of a live peer whose code predates discovery publication:
* its schema-1 header heartbeats, but it never writes a discovery sidecar. */
legacyLabel?: string;
aliases?: DiscoveryMetadata["aliases"];
workspace?: string;
tasks?: DiscoveryMetadata["tasks"];
Expand Down Expand Up @@ -36,6 +39,7 @@ export function discoverOrchestrators(profile: string, peers: readonly PresenceR
if (matches.length !== 1) return unknown;
const header = matches[0];
const metadata = readDiscovery(profile, header);
if (!metadata && header.recent && discoveryAbsent(profile, header)) return { ...unknown, legacyLabel: header.label };
if (metadata?.activation !== activationHash(activations[0])) return unknown;
if (!header.recent) return { ...unknown, freshness: "stale" };
const catalog = readCatalog(profile, header, metadata.activation, selection?.recipientSessionId === sessionId ? selection.cursor : undefined);
Expand Down
9 changes: 9 additions & 0 deletions lib/orchestrator-presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,15 @@ export function readActivity(profile: string, selection: Header): { activity?: A
} catch (error) { return { unavailable: reason(error) }; }
}

/** True only when no discovery sidecar exists for this header: the publisher
* predates discovery publication. A present but unreadable sidecar is not absent. */
export function discoveryAbsent(profile: string, h: Header): boolean {
try {
fs.lstatSync(join(rootFor(profile), filename(h, "discovery")));
return false;
} catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT"; }
}

/** Optional derived sidecar; failures never invalidate the schema-1 header. */
export function readDiscovery(profile: string, h: Header): DiscoveryMetadata | undefined {
try {
Expand Down
33 changes: 33 additions & 0 deletions tests/gentle-agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4728,6 +4728,39 @@ test("registered orchestrator_list joins peer metadata without child launches or
} finally { publisher.dispose(); }
});

test("orchestrator_list labels a live peer that predates discovery publication (#2305)", async () => {
const h = fakePi();
const runtime = deps();
const profile = realpathSync(mkdtempSync(join(root, "discovery-legacy-")));
const peer = { version: 1 as const, sessionId: "peer", endpoint: "/fixture/peer.sock", createdAt: 1 };
// A peer running pre-publication code heartbeats its schema-1 header only.
const publisher = PresencePublisher.start({ profile, sessionId: "peer", label: "gentle-shell", activity: [] });
try {
let ready = false;
const registry = { list: async () => [], listActivations: async () => [peer] };
runtime.deps.agentHome = profile;
runtime.deps.sessionTransport = {
createRegistry: async () => registry,
createListener: () => ({ registry, start: async () => { ready = true; }, close: async () => {} }),
createClient: () => ({ close() {}, sendNotification: async () => { throw new Error("no messages expected"); } }),
};
gentleAgents(h.pi, {}, runtime.deps);
const { ctx } = fakeContext();
await h.fire("session_start", ctx);
await eventually(() => ready, "discovery transport ready");
const result = await h.tools.get("orchestrator_list")!.execute("list", {}, undefined, undefined, ctx);
assert.equal(result.content[0].text, "Advertised sessions (reachability is unknown):\n"
+ "- peer · gentle-shell · context: legacy (this session predates discovery publication; restart it to publish its workspace and repository)"
+ " · repository: not published · metadata: legacy\n recorded catalog: unknown");
// Consultation stays fail-closed: a legacy label is display-only.
const consult = await h.tools.get("orchestrator_consult")!.execute("consult", { recipient_session_id: "peer" }, undefined, undefined, ctx);
assert.equal(JSON.parse(consult.content[0].text).status, "unavailable");
assert.equal(runtime.spawned.length, 0);
assert.equal(h.sent.length, 0);
await h.fire("session_shutdown", ctx);
} finally { publisher.dispose(); }
});

test("registered reasoning and revocation use live SDK host and published source only", async () => {
const h = fakePi(), runtime = deps();
const profile = realpathSync(mkdtempSync(join(root, "reasoning-runtime-")));
Expand Down
29 changes: 29 additions & 0 deletions tests/orchestrator-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,35 @@ test("published headers retain the frozen legacy schema-1 contract after metadat
assert.equal(listPresence(profile).entries.length, 1);
assert.equal(discoverOrchestrators(profile, [peer])[0].freshness, "unknown");
});
test("a live peer that predates discovery publication keeps its legacy header label (#2305)", (t) => {
const profile = fixture(t);
// A long-lived peer started before discovery publication shipped heartbeats
// the schema-1 header only and never writes a discovery sidecar.
const publisher = PresencePublisher.start({ profile, sessionId: "peer", label: "gentle-shell", activity: [] });
t.after(() => publisher.dispose());
const [candidate] = discoverOrchestrators(profile, [peer]);
assert.equal(candidate.freshness, "unknown");
assert.equal(candidate.legacyLabel, "gentle-shell");
assert.equal(candidate.workspace, undefined);
assert.equal(candidate.publication, undefined);
// An old heartbeat is not a live legacy peer.
assert.equal(discoverOrchestrators(profile, [peer], Date.now() + 20_000)[0].legacyLabel, undefined);
});
test("a present but invalid or foreign discovery sidecar never yields a legacy label", (t) => {
const profile = fixture(t);
const publisher = PresencePublisher.start({ profile, sessionId: "peer", label: "gentle-shell", activity: [] });
t.after(() => publisher.dispose());
publisher.updateDiscovery(peer, { workspace: "/repo", tasks: [] });
// Metadata bound to another activation fails closed, without a label.
const foreign = discoverOrchestrators(profile, [{ ...peer, endpoint: "/socket/replacement" }])[0];
assert.equal(foreign.freshness, "unknown");
assert.equal(foreign.legacyLabel, undefined);
const sidecar = join(profile, "gentle-agents", "presence", `${publisher.target.sessionHash}.${publisher.target.incarnation}.discovery.json`);
writeFileSync(sidecar, "{broken");
const broken = discoverOrchestrators(profile, [peer])[0];
assert.equal(broken.freshness, "unknown");
assert.equal(broken.legacyLabel, undefined);
});
test("joins activation-bound metadata without exporting prompts or thread output", (t) => {
const profile = fixture(t);
const publisher = PresencePublisher.start({ profile, sessionId: peer.sessionId, label: "Review auth", activity: [] });
Expand Down
Loading