Spaces:
Running
Running
File size: 5,207 Bytes
df112f9 dce99f0 df112f9 dce99f0 df112f9 dce99f0 df112f9 dce99f0 df112f9 dce99f0 df112f9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
const SAFE = /^[A-Za-z0-9_-]+$/;
function paneIdentity() {
const amId = process.env.AM_ID;
const runId = process.env.AM_RUN_ID;
if (process.env.AM_CLI !== 'opencode' || !SAFE.test(amId || '') || !SAFE.test(runId || '')) return null;
if (String(process.pid) !== process.env.AM_PANE_PID) return null;
return { amId, runId };
}
function report(sessionID, cwd, source) {
const identity = paneIdentity();
if (!identity) return;
const { amId, runId } = identity;
if (!/^ses_[A-Za-z0-9_-]+$/.test(sessionID || '') || typeof cwd !== 'string') return;
const dir = process.env.AM_REPIN_DIR || path.join(os.tmpdir(), 'am-repin');
const file = path.join(dir, `${amId}.opencode.json`);
const tmp = `${file}.${process.pid}.tmp`;
try {
// OpenCode dispatches generic event hooks without awaiting their Promise.
// Keep this tiny local write synchronous so /clear followed immediately by
// quit cannot terminate the process between mkdir/write/rename.
mkdirSync(dir, { recursive: true });
writeFileSync(tmp, JSON.stringify({
amId,
runId,
cli: 'opencode',
pluginPid: process.pid,
payload: { session_id: sessionID, cwd, source },
}));
renameSync(tmp, file);
} catch { /* telemetry must never interfere with the user's prompt */ }
}
function syncInputRequired(pending) {
const identity = paneIdentity();
if (!identity) return;
const { amId, runId } = identity;
const dir = process.env.AM_INPUT_REQUIRED_DIR || path.join(os.tmpdir(), 'am-input-required');
const file = path.join(dir, `${amId}.json`);
const item = pending.values().next().value;
if (!item) {
try {
const existing = JSON.parse(readFileSync(file, 'utf8'));
if (existing?.runId === runId && existing?.source === 'opencode-event') unlinkSync(file);
} catch { /* already absent or no longer ours */ }
return;
}
const tmp = `${file}.${process.pid}.tmp`;
try {
mkdirSync(dir, { recursive: true });
writeFileSync(tmp, JSON.stringify({
amId,
runId,
cli: 'opencode',
kind: item.kind,
source: 'opencode-event',
requestId: item.id,
at: item.at,
}));
renameSync(tmp, file);
} catch { /* an attention signal must never interfere with the TUI */ }
}
// OpenCode creates a new root session for /new (alias /clear). chat.message
// additionally follows an explicit switch to an existing session; runner.js
// verifies that id against the database and rejects child/subagent sessions.
export const AgentManagerRepin = async ({ directory }) => {
// OpenCode's own TUI uses the same asked/replied event pairs. Keep every
// queued request: resolving one must not hide another waiting behind it.
const pending = new Map();
const changed = () => syncInputRequired(pending);
const drop = (id) => {
if (!id || !pending.delete(id)) return;
changed();
};
return ({
event: async ({ event }) => {
const props = event?.properties || {};
if (event?.type === 'session.created') {
const info = props.info;
if (info?.id && !info.parentID) report(info.id, info.directory || directory, 'session.created');
return;
}
if (event?.type === 'permission.asked' || event?.type === 'question.asked') {
if (!props.id || pending.has(props.id)) return;
pending.set(props.id, {
id: props.id,
sessionID: props.sessionID,
kind: event.type === 'question.asked' ? 'question' : 'permission',
tool: props.tool || null,
at: Date.now(),
});
changed();
return;
}
if (event?.type === 'permission.replied'
|| event?.type === 'question.replied'
|| event?.type === 'question.rejected') {
drop(props.requestID);
return;
}
// A Question tool can complete without question.replied. OpenCode's TUI
// has this same recovery path; mirror it so the marker cannot stick.
if (event?.type === 'message.part.updated') {
const part = props.part;
if (part?.type !== 'tool' || part.tool !== 'question'
|| (part.state?.status !== 'completed' && part.state?.status !== 'error')) return;
let dirty = false;
for (const [id, item] of pending) {
if (item.kind !== 'question' || !item.tool) continue;
if (item.tool.messageID === part.messageID && item.tool.callID === part.callID) {
pending.delete(id);
dirty = true;
}
}
if (dirty) changed();
}
},
'chat.message': async ({ sessionID }) => {
report(sessionID, directory, 'chat.message');
},
// Tool shells must not pass the pane's private attribution markers to an
// agent launched inside them. Empty values override OpenCode's process.env
// merge and make the nested plugin a no-op.
'shell.env': async (_input, output) => {
output.env.AM_ID = '';
output.env.AM_RUN_ID = '';
output.env.AM_CLI = '';
output.env.AM_PANE_PID = '';
},
});
};
|