원클릭으로
mailbox-interactive
Interactive waiting-task and mailbox workflow.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Interactive waiting-task and mailbox workflow.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Iterative multi-round codebase audit with diminishing-returns detection. Run 5-20+ rounds, each focusing on one specific area. Built from 19 rounds of dogfooding pi-crew on itself.
Pi TUI crew widget data sources, display priority, and rendering performance.
Multi-phase orchestration for planners and executors.
Spawn 3 adversarial subagents (Skeptic, Pragmatist, Critic) to evaluate a decision, architecture choice, or plan. Anti-anchoring: each role receives ONLY the question, not conversation history. Aggregates votes into consensus recommendation with dissent tracking. Use when facing critical decisions, architecture choices, security tradeoffs, or plan reviews where single-perspective analysis is insufficient.
Background worker, heartbeat, stale-run, crash-recovery, and deadletter workflow. Use when debugging stuck/dead workers or changing async run reliability.
Child Pi worker spawning, lifecycle callbacks, and failure modes.
| name | mailbox-interactive |
| description | Interactive waiting-task and mailbox workflow. |
| origin | pi-crew |
| triggers | ["respond to worker","nudge agent","mailbox message","supervisor contact","waiting task"] |
Use this skill for live coordination between leader and workers. Mailbox provides an asynchronous message protocol for steer, follow-up, respond, and nudge operations.
Worker (waiting) ← mailbox inbox ← Leader (respond)
Worker (running) ← mailbox follow-ups ← Leader (followUp)
Leader → Worker: steer, followUp, nudge (non-blocking)
Worker → Leader: supervisor contact (blocking decision)
Each run has a mailbox directory at .crew/state/runs/<runId>/mailbox/:
inbox.jsonl — incoming messages (to worker)outbox.jsonl — sent messages (from worker)steering.jsonl — steer messages specificallyinterface MailboxMessage {
id: string;
direction: "inbox" | "outbox";
from: string; // taskId or "leader"
to: string; // taskId or "leader"
body: string; // message text
status: "pending" | "delivered" | "acknowledged" | "rejected";
priority: "low" | "normal" | "high";
sentAt: string; // ISO timestamp
deliveredAt?: string;
data?: Record<string, unknown>; // source, correlation, etc.
}
// Respond writes to inbox and transitions task back to running
async function respond(runId: string, taskId: string, body: string, priority = "normal") {
// 1. Write inbox message
const message = appendInboxMessage(manifest, { taskId, body, priority });
// 2. Re-read state inside lock
const { tasks } = loadRunManifestById(cwd, runId);
const task = tasks.find(t => t.id === taskId);
// 3. Verify task is waiting
if (task.status !== "waiting") {
throw new Error(`Cannot respond to non-waiting task: ${task.status}`);
}
// 4. Transition task back to running
const updated = { ...task, status: "running", waitingSince: undefined };
saveRunTasks(manifest, [updated]);
// 5. Emit event
appendEvent(eventsPath, { type: "task.responded", taskId, message: body });
return message;
}
// Steer sends a message to a running live agent
async function steerLiveAgent(agentId: string, message: string) {
const handle = getLiveAgent(agentId);
if (!handle) throw new Error(`Live agent '${agentId}' not found`);
// If session.steer is available, deliver immediately
if (typeof handle.session.steer === "function") {
await handle.session.steer(message);
handle.updatedAt = new Date().toISOString();
return handle;
}
// Otherwise, queue for delivery when session becomes ready
handle.pendingSteers.push(message);
return handle;
}
async function followUpLiveAgent(agentId: string, prompt: string) {
const handle = getLiveAgent(agentId);
if (!handle) throw new Error(`Live agent '${agentId}' not found`);
if (typeof handle.session.prompt === "function") {
await handle.session.prompt(prompt, { source: "api", expandPromptTemplates: false });
handle.updatedAt = new Date().toISOString();
return handle;
}
handle.pendingFollowUps.push(prompt);
return handle;
}
function nudgeAgent(manifest: TeamRunManifest, agentId: string, message?: string) {
const agent = readCrewAgents(manifest).find(a => a.id === agentId || a.taskId === agentId);
if (!agent) throw new Error(`Agent '${agentId}' not found`);
const text = message ?? "Please report your current status, blocker, or smallest next step.";
// Write to mailbox
const mailboxMessage = appendSteeringMessage(manifest, {
taskId: agent.taskId,
body: text,
priority: "normal",
data: { source: "nudge-agent" }
});
// Emit event
appendEvent(manifest.eventsPath, {
type: "agent.nudged",
runId: manifest.runId,
taskId: agent.taskId,
message: text,
data: { agentId: agent.id, mailboxMessageId: mailboxMessage.id }
});
return mailboxMessage;
}
Workers can contact the leader for blocking decisions:
// Worker stdout contains supervisor contact pattern:
// @supervisor need_confirmation: <decision-type>
// @supervisor need_input: <input-prompt>
// Parsed by parseSupervisorContactFromLine() in supervisor-contact.ts
function parseSupervisorContact(line: string): SupervisorContact | null {
const match = line.match(/@supervisor\s+(\w+):\s*(.*)/);
if (!match) return null;
return { type: match[1], prompt: match[2] };
}
// Recorded as events and surfaced in UI
appendEvent(eventsPath, {
type: "supervisor.contact",
runId: manifest.runId,
taskId: task.id,
message: contact.prompt,
data: { contactType: contact.type }
});
Contact types:
need_confirmation — worker needs explicit approvalneed_input — worker needs a text responseneed_selection — worker needs to choose from optionsMailbox queues can grow large with pending messages:
const MAX_PENDING_STEERS = 50;
const MAX_PENDING_FOLLOWUPS = 50;
// When queue exceeds limit, oldest messages are dropped
if (handle.pendingSteers.length >= MAX_PENDING_STEERS) {
handle.pendingSteers.shift(); // drop oldest
}
Backpressure signals:
pendingSteers.length > 20, warn in UIpendingFollowUps.length > 20, warn in UIMailbox JSONL files can become corrupt on crash. Handle gracefully:
function readMailboxMessages(path: string): MailboxMessage[] {
if (!fs.existsSync(path)) return [];
const lines = fs.readFileSync(path, "utf-8").split("\n");
const messages: MailboxMessage[] = [];
for (const line of lines) {
if (!line.trim()) continue;
try {
messages.push(JSON.parse(line) as MailboxMessage);
} catch {
// Skip corrupt lines: partial write from crash
// Try to recover by reading up to the last valid JSON object
continue;
}
}
return messages;
}
async function respondWithTimeout(
runId: string,
taskId: string,
body: string,
timeoutMs = 30_000
): Promise<MailboxMessage> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const message = await respond(runId, taskId, body);
clearTimeout(timeout);
return message;
} catch (error) {
clearTimeout(timeout);
if (error.name === "AbortError") {
throw new Error(`Respond timed out after ${timeoutMs}ms`);
}
throw error;
}
}
// Fire-and-forget: send message and return immediately
function followUpAsync(agentId: string, prompt: string): void {
const handle = getLiveAgent(agentId);
if (!handle) return;
if (typeof handle.session.prompt === "function") {
// Non-awaited: delivery happens in background
void handle.session.prompt(prompt, { source: "api", expandPromptTemplates: false })
.catch(() => { /* log if needed */ });
} else {
handle.pendingFollowUps.push(prompt);
}
}
Foreign sessions cannot mutate a run's mailbox:
// In respond.ts / cancel.ts
function verifyRunOwnership(manifest: TeamRunManifest, sessionId: string, force = false) {
if (!force && manifest.ownerSessionId && manifest.ownerSessionId !== sessionId) {
throw new Error(
`Run ${manifest.runId} is owned by session ${manifest.ownerSessionId}. ` +
`Cannot mutate from session ${sessionId}. Use force=true to override.`
);
}
}
Before responding to or mutating mailbox state, verify:
If ANY answer is NO → Stop. Verify mailbox state before mutating.
respond only works on waiting tasks. Resuming running tasks corrupts state.ownerSessionId before mutating.followUp (non-blocking) instead of respond for status updates.src/state/mailbox.ts — appendInboxMessage, appendSteeringMessage, readMailboxMessagessrc/extension/team-tool/respond.ts — respond tool handler, ownership verificationsrc/extension/team-tool/cancel.ts — cancel with mailbox cleanupsrc/extension/team-tool/api.ts — steer-agent, follow-up-agent, nudge-agentsrc/runtime/live-agent-manager.ts — pendingSteers, pendingFollowUps, steerLiveAgent, followUpLiveAgentsrc/runtime/supervisor-contact.ts — parseSupervisorContactFromLine, recordSupervisorContactsrc/ui/overlays/mailbox-detail-overlay.ts — mailbox UIcd pi-crew
# Verify mailbox structure
ls .crew/state/runs/<runId>/mailbox/ 2>/dev/null || echo "No mailbox yet"
# Test respond tool
node --experimental-strip-types --test test/unit/respond-tool.test.ts
# Test mailbox overlay
node --experimental-strip-types --test test/unit/mailbox-detail-overlay.test.ts
# TypeScript
npx tsc --noEmit
# All tests
npm test