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
28 changes: 28 additions & 0 deletions slack-bridge/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@ type CommandDefinition = {

type EventHandler = (event: unknown, ctx: ExtensionContext) => Promise<unknown> | unknown;

const BROKER_MANAGED_PINET_ENV_KEYS = [
"PINET_BROKER_MANAGED",
"PINET_LAUNCH_SOURCE",
"PINET_BROKER_AGENT_ID",
"PINET_TMUX_SESSION",
] as const;

const originalBrokerManagedPinetEnv = Object.fromEntries(
BROKER_MANAGED_PINET_ENV_KEYS.map((key) => [key, process.env[key]]),
) as Record<(typeof BROKER_MANAGED_PINET_ENV_KEYS)[number], string | undefined>;

beforeEach(() => {
for (const key of BROKER_MANAGED_PINET_ENV_KEYS) {
delete process.env[key];
}
});

afterEach(() => {
for (const key of BROKER_MANAGED_PINET_ENV_KEYS) {
const value = originalBrokerManagedPinetEnv[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});

function stubIsTTY(stream: NodeJS.ReadStream | NodeJS.WriteStream, value: boolean): () => void {
const target = stream as unknown as Record<string, unknown>;
const hadOwnProperty = Object.prototype.hasOwnProperty.call(target, "isTTY");
Expand Down
2 changes: 2 additions & 0 deletions slack-bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import { createAgentCompletionRuntime } from "./agent-completion-runtime.js";
import { sendBrokerMessage } from "./broker/message-send.js";
import {
type SlackBridgeRuntimeMode,
isBrokerManagedFollowerLaunch,
resolveSlackBridgeStartupRuntimeMode,
} from "./runtime-mode.js";
import {
Expand Down Expand Up @@ -1498,6 +1499,7 @@ export default function (pi: ExtensionAPI) {
maybeWarnSlackUserAccess(ctx);
const startupMode = resolveSlackBridgeStartupRuntimeMode(settings, {
brokerSocketExists: fs.existsSync(DEFAULT_SOCKET_PATH),
brokerManagedFollowerLaunch: isBrokerManagedFollowerLaunch(),
});

try {
Expand Down
53 changes: 53 additions & 0 deletions slack-bridge/pinet-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,59 @@ describe("registerPinetTools", () => {
});
});

it("hides recently disconnected agents from pinet agents unless ghosts are requested", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-14T12:00:00Z"));

const listBrokerAgents = vi.fn(() => [
makeAgent({ id: "live", name: "Live Lynx" }),
makeAgent({
id: "exited",
name: "Exited Egret",
disconnectedAt: "2026-04-14T11:59:59.000Z",
}),
]);
const tools = registerWithDeps(createDeps({ listBrokerAgents }));

const defaultResult = (await tools.get("pinet")?.execute("tool-call-agents-live", {
action: "agents",
args: { full: true },
})) as { details: { data: { text: string; details: { agents: Array<{ id: string }> } } } };
expect(defaultResult.details.data.details.agents.map((agent) => agent.id)).toEqual(["live"]);
expect(defaultResult.details.data.text).not.toContain("Exited Egret");

const withGhostsResult = (await tools.get("pinet")?.execute("tool-call-agents-ghosts", {
action: "agents",
args: { full: true, include_ghosts: true },
})) as { details: { data: { text: string; details: { agents: Array<{ id: string }> } } } };
expect(withGhostsResult.details.data.details.agents.map((agent) => agent.id)).toEqual([
"live",
"exited",
]);
expect(withGhostsResult.details.data.text).toContain("Exited Egret");
});

it("passes the ghost visibility preference through follower agent listing", async () => {
const listFollowerAgents = vi.fn(async (_includeGhosts: boolean) => [
makeAgent({ id: "agent-2" }),
]);
const tools = registerWithDeps(
createDeps({ brokerRole: () => "follower", listFollowerAgents }),
);

await tools.get("pinet")?.execute("tool-call-follower-agents-live", {
action: "agents",
args: {},
});
await tools.get("pinet")?.execute("tool-call-follower-agents-ghosts", {
action: "agents",
args: { include_ghosts: true },
});

expect(listFollowerAgents).toHaveBeenNthCalledWith(1, false);
expect(listFollowerAgents).toHaveBeenNthCalledWith(2, true);
});

it("keeps pinet agents default cli details compact", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-14T12:00:00Z"));
Expand Down
8 changes: 7 additions & 1 deletion slack-bridge/pinet-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1250,7 +1250,7 @@ function runPinetAgentsAction(
throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
}

const includeGhosts = true;
const includeGhosts = params.include_ghosts === true;
const recentGhostWindowMs = DEFAULT_HEARTBEAT_TIMEOUT_MS * 2;
const nowMs = Date.now();
const hasHint = Boolean(
Expand Down Expand Up @@ -1423,6 +1423,12 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
Type.String({ description: "Comma-separated required capability/tool tags" }),
),
task: Type.Optional(Type.String({ description: "Optional natural-language task hint" })),
include_ghosts: Type.Optional(
Type.Boolean({
description:
"Include recently disconnected/resumable agents. Defaults false so graceful exits do not look like actionable ghosts.",
}),
),
...PINET_OUTPUT_OPTION_PARAMETERS,
}),
execute: (_id, params, output) => runPinetAgentsAction(params, deps, "pinet:agents", output),
Expand Down
65 changes: 65 additions & 0 deletions slack-bridge/runtime-mode.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
isBrokerManagedFollowerLaunch,
isPinetRuntimeMode,
normalizeSlackBridgeRuntimeMode,
resolveSlackBridgeStartupRuntimeMode,
Expand Down Expand Up @@ -32,6 +33,26 @@ describe("isPinetRuntimeMode", () => {
});
});

describe("isBrokerManagedFollowerLaunch", () => {
it("detects broker-managed tmux follower launches", () => {
expect(
isBrokerManagedFollowerLaunch({
PINET_BROKER_MANAGED: "1",
PINET_LAUNCH_SOURCE: "broker-tmux",
}),
).toBe(true);
});

it("does not treat other broker-managed metadata as the tmux follower path", () => {
expect(
isBrokerManagedFollowerLaunch({
PINET_BROKER_MANAGED: "1",
PINET_LAUNCH_SOURCE: "manual",
}),
).toBe(false);
});
});

describe("resolveSlackBridgeStartupRuntimeMode", () => {
it("defaults to off", () => {
expect(resolveSlackBridgeStartupRuntimeMode({})).toBe("off");
Expand Down Expand Up @@ -75,6 +96,50 @@ describe("resolveSlackBridgeStartupRuntimeMode", () => {
expect(resolveSlackBridgeStartupRuntimeMode({ runtimeMode: "broker" })).toBe("broker");
});

it("keeps broker-managed follower launches off even when persistent settings request broker mode", () => {
expect(
resolveSlackBridgeStartupRuntimeMode(
{ runtimeMode: "broker" },
{ brokerSocketExists: true, brokerManagedFollowerLaunch: true },
),
).toBe("off");
});

it("preserves explicit non-follower runtime mode over legacy autoFollow for broker-managed launches", () => {
for (const runtimeMode of ["off", "single", "broker"] as const) {
expect(
resolveSlackBridgeStartupRuntimeMode(
{ runtimeMode, autoFollow: true },
{ brokerSocketExists: true, brokerManagedFollowerLaunch: true },
),
).toBe("off");
}
});

it("keeps broker-managed follower launches off when legacy autoConnect would start single-player mode", () => {
expect(
resolveSlackBridgeStartupRuntimeMode(
{ autoConnect: true },
{ brokerSocketExists: true, brokerManagedFollowerLaunch: true },
),
).toBe("off");
});

it("still honors follower opt-in for broker-managed launches when a broker socket exists", () => {
expect(
resolveSlackBridgeStartupRuntimeMode(
{ runtimeMode: "follower" },
{ brokerSocketExists: true, brokerManagedFollowerLaunch: true },
),
).toBe("follower");
expect(
resolveSlackBridgeStartupRuntimeMode(
{ autoFollow: true },
{ brokerSocketExists: true, brokerManagedFollowerLaunch: true },
),
).toBe("follower");
});

it("downgrades explicit follower mode to off when no broker socket exists", () => {
expect(
resolveSlackBridgeStartupRuntimeMode(
Expand Down
12 changes: 12 additions & 0 deletions slack-bridge/runtime-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export function isPinetRuntimeMode(mode: SlackBridgeRuntimeMode): boolean {

export interface ResolveSlackBridgeStartupRuntimeModeOptions {
brokerSocketExists?: boolean;
brokerManagedFollowerLaunch?: boolean;
}

export function isBrokerManagedFollowerLaunch(env = process.env): boolean {
return env.PINET_BROKER_MANAGED === "1" && env.PINET_LAUNCH_SOURCE === "broker-tmux";
}

export function resolveSlackBridgeStartupRuntimeMode(
Expand All @@ -32,6 +37,13 @@ export function resolveSlackBridgeStartupRuntimeMode(
const explicitMode = normalizeSlackBridgeRuntimeMode(settings.runtimeMode);
const brokerSocketExists = options.brokerSocketExists ?? true;

if (options.brokerManagedFollowerLaunch) {
if (explicitMode === "follower" || (!explicitMode && settings.autoFollow)) {
return brokerSocketExists ? "follower" : "off";
}
return "off";
}

if (explicitMode) {
if (explicitMode === "follower" && !brokerSocketExists) {
return "off";
Expand Down
Loading