fix: POST for file creation (Gitea 1.25+), add edit_issue tool, persist poll state

- files.ts: Use POST for new files, PUT for updates (Gitea 1.25 requires this)
- issues.ts: Add editIssue() for state/title/body changes
- write-tools.ts: Add gitea_edit_issue tool (open/close/edit issues)
- webhook/server.ts: Persist lastPollAt to disk to prevent duplicate
  events on reload; use followUp delivery to queue events during LLM turns
- index.ts: Use deliverAs:'followUp' for sendUserMessage
This commit is contained in:
2026-03-13 17:14:06 -07:00
parent 578e2f91cb
commit b868ad4df5
5 changed files with 105 additions and 9 deletions

View File

@@ -20,6 +20,8 @@ const POLL_INTERVAL = parseInt(process.env.PI_BOT_POLL_INTERVAL ?? "300", 10);
const EVENT_POLL_INTERVAL = parseInt(process.env.PI_EVENT_POLL_INTERVAL ?? "60", 10);
const BOT_USER = process.env.PI_GIT_USER ?? "";
const POLL_STATE_FILE = "/home/pibot/.pi/agent/gitea-poll-state.json";
let server: Server | null = null;
let processingQueue: Array<{ event: any; timestamp: number }> = [];
let maxQueueDepth = 50;
@@ -37,6 +39,31 @@ let pollOnlyRepos: Map<string, { addedAt: number; lastPollAt: string }> = new Ma
/** Track all known repos so we don't re-attempt webhook install every cycle */
let knownRepos: Set<string> = new Set();
/** Load poll timestamps from disk (survives reloads) */
async function loadPollState(): Promise<Record<string, string>> {
try {
const fs = await import("node:fs/promises");
const data = await fs.readFile(POLL_STATE_FILE, "utf-8");
return JSON.parse(data);
} catch {
return {};
}
}
/** Save poll timestamps to disk */
async function savePollState(): Promise<void> {
try {
const fs = await import("node:fs/promises");
const state: Record<string, string> = {};
for (const [name, s] of pollOnlyRepos) {
state[name] = s.lastPollAt;
}
await fs.writeFile(POLL_STATE_FILE, JSON.stringify(state), "utf-8");
} catch (err) {
console.error("[gitea-polling] Error saving poll state:", err instanceof Error ? err.message : err);
}
}
export function setSendMessage(fn: (message: string) => Promise<void>) {
sendMessage = fn;
}
@@ -273,6 +300,7 @@ async function discoverRepos() {
try {
const client = new GiteaClient();
const repos = await client.get<any[]>("/user/repos?limit=100");
const savedState = await loadPollState();
let newWebhooks = 0;
let newPollOnly = 0;
@@ -303,11 +331,11 @@ async function discoverRepos() {
} catch (err) {
if (err instanceof GiteaError && err.status === 403) {
// No admin access — fall back to polling
// Set lastPollAt to 5 minutes ago so we catch recent events
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
// Use persisted timestamp if available, otherwise 5 min ago
const lastPoll = savedState[name] ?? new Date(Date.now() - 5 * 60 * 1000).toISOString();
pollOnlyRepos.set(name, {
addedAt: Date.now(),
lastPollAt: fiveMinAgo,
lastPollAt: lastPoll,
});
knownRepos.add(name);
newPollOnly++;
@@ -319,10 +347,10 @@ async function discoverRepos() {
}
} else {
// No webhook URL configured — all repos are poll-only
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
const lastPoll = savedState[name] ?? new Date(Date.now() - 5 * 60 * 1000).toISOString();
pollOnlyRepos.set(name, {
addedAt: Date.now(),
lastPollAt: fiveMinAgo,
lastPollAt: lastPoll,
});
knownRepos.add(name);
newPollOnly++;
@@ -430,6 +458,9 @@ async function pollForEvents() {
console.error(`[gitea-poll-events] Error polling ${repoName}: ${err instanceof Error ? err.message : err}`);
}
}
// Persist poll timestamps
await savePollState();
}
// ── Public API ───────────────────────────────────────────────────────────────