paseo-follow-up 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/daemon.ts +30 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-follow-up",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"description": "Follow every workspace PR stack in Paseo, answer agent questions, batch repairs for the main agent, and see what needs you.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
package/server/daemon.ts
CHANGED
|
@@ -87,12 +87,30 @@ export async function sessionGuardOwnsInterruption(agentId: string, interruption
|
|
|
87
87
|
return !!guard.ownsInterruption(await guard.read(), agentId, interruption);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const HISTORY_RETRY_MS = 15_000;
|
|
91
|
+
const HISTORY_RETRY_MAX_MS = 3_600_000;
|
|
92
|
+
export function historyRetryDelay(failures: number): number {
|
|
93
|
+
return failures <= 0 ? HISTORY_RETRY_MS : Math.min(HISTORY_RETRY_MS * 2 ** (failures - 1), HISTORY_RETRY_MAX_MS);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A closed agent has no live runtime, so it cannot have running provider subagents. Asking the
|
|
97
|
+
* daemon anyway resumes the session from persistence (spawning its provider process) only for the
|
|
98
|
+
* idle-runtime collector to close it again, so each poll would resume it.
|
|
99
|
+
*/
|
|
100
|
+
async function subagentsRunning(client: Daemon, agent: Agent, failure: string): Promise<boolean> {
|
|
101
|
+
if (agent.status === "closed") return false;
|
|
102
|
+
const children = await client.listProviderSubagents(agent.id);
|
|
103
|
+
if (children.error) throw new Error(failure);
|
|
104
|
+
return children.subagents.some(child => child.status === "running");
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
export class PaseoGateway implements Gateway {
|
|
91
108
|
constructor(private connect: typeof withDaemon = withDaemon, private guard = new SessionGuard(join(paseoHome, "plugin-data/session-guard")), private compacting = compactionPending) {}
|
|
92
109
|
private directory: { agents: { agent: Agent }[]; workspaces: WorkspaceRow[] } | null = null;
|
|
93
110
|
private dirty = new Set<string>();
|
|
94
111
|
markDirty(agentId: string): void { this.dirty.add(agentId); }
|
|
95
|
-
|
|
112
|
+
// `failures` counts consecutive unreadable reads of one transcript; `retryAt` backs them off.
|
|
113
|
+
private histories = new Map<string, { messageAt: string | null; at: number; failures: number; retryAt: number; history: History; submissions: Submission[]; artifacts: Artifact[] }>();
|
|
96
114
|
async discover(state: State, workspaceId?: string): Promise<DiscoveredWorkspace[]> {
|
|
97
115
|
return this.connect(async client => {
|
|
98
116
|
const [agents, workspaces, catalog] = await Promise.all([
|
|
@@ -124,7 +142,7 @@ export class PaseoGateway implements Gateway {
|
|
|
124
142
|
const repo = project?.projectKind === "git" ? /^remote:github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(project.projectKey)?.[1] ?? null : null;
|
|
125
143
|
for (const agent of roots) {
|
|
126
144
|
const cached = this.histories.get(agent.id);
|
|
127
|
-
if (!cached || cached.messageAt !== agent.lastUserMessageAt || this.dirty.has(agent.id) || (cached.history.error && Date.now()
|
|
145
|
+
if (!cached || cached.messageAt !== agent.lastUserMessageAt || this.dirty.has(agent.id) || (cached.history.error && Date.now() >= cached.retryAt) || (["running", "initializing"].includes(agent.status) && Date.now() - cached.at >= 15_000)) scans.push({ row, agent, repo });
|
|
128
146
|
}
|
|
129
147
|
discovered.push(row);
|
|
130
148
|
}
|
|
@@ -151,7 +169,13 @@ export class PaseoGateway implements Gateway {
|
|
|
151
169
|
history = extractHistory(items, complete, complete ? null : "History exceeds the recovery window; inspect older transcripts during inventory");
|
|
152
170
|
submissions = submittedPrs(items, agent.id, repo);
|
|
153
171
|
} catch { history = extractHistory([], false, "History could not be read; inspect the session or register submitted PRs explicitly"); }
|
|
154
|
-
|
|
172
|
+
// Reading a closed session's timeline resumes its runtime, so an unreadable one is retried
|
|
173
|
+
// with backoff instead of every poll. New user messages and live turns retry immediately.
|
|
174
|
+
const previous = this.histories.get(agent.id);
|
|
175
|
+
const live = ["running", "initializing"].includes(agent.status);
|
|
176
|
+
const failures = !history.error ? 0 : previous?.history.error && previous.messageAt === agent.lastUserMessageAt && !live ? previous.failures + 1 : 1;
|
|
177
|
+
const at = Date.now();
|
|
178
|
+
this.histories.set(agent.id, { messageAt: agent.lastUserMessageAt, at, failures, retryAt: at + historyRetryDelay(failures), history, submissions, artifacts });
|
|
155
179
|
this.dirty.delete(agent.id);
|
|
156
180
|
}
|
|
157
181
|
for (const row of discovered) {
|
|
@@ -205,9 +229,7 @@ export class PaseoGateway implements Gateway {
|
|
|
205
229
|
for (const { agent } of agents.filter(row => row.agent.workspaceId === w.id)) {
|
|
206
230
|
if (recoveryIds.has(agent.id) || agent.labels["follow-through.recovery"]) continue;
|
|
207
231
|
if (["running", "initializing"].includes(agent.status)) { busy = true; if (agent.id !== w.mainAgentId) backgroundBusy = true; }
|
|
208
|
-
|
|
209
|
-
if (children.error) throw new Error("Subagent observation unavailable");
|
|
210
|
-
if (children.subagents.some(child => child.status === "running")) { busy = true; backgroundBusy = true; }
|
|
232
|
+
if (await subagentsRunning(client, agent, "Subagent observation unavailable")) { busy = true; backgroundBusy = true; }
|
|
211
233
|
}
|
|
212
234
|
return { ...context, busy, reason: null, archived: false, backgroundBusy };
|
|
213
235
|
});
|
|
@@ -217,9 +239,8 @@ export class PaseoGateway implements Gateway {
|
|
|
217
239
|
const agent = batch.recipientAgentId ? (await client.fetchAgent(batch.recipientAgentId))?.agent : null;
|
|
218
240
|
if (!agent || agent.archivedAt || agent.workspaceId !== w.id) return { busy: false, reason: "Recovery agent is unavailable. Check its result before retrying." };
|
|
219
241
|
if (this.guard.waiting(await this.guard.read(), { agentIds: [agent.id] })) return { busy: true, reason: "Waiting for Session Guard to resume recovery." };
|
|
220
|
-
const children = await client
|
|
221
|
-
|
|
222
|
-
return { busy: ["running", "initializing"].includes(agent.status) || !!agent.pendingPermissions.length || children.subagents.some(child => child.status === "running"), reason: agent.pendingPermissions.length ? "Recovery is waiting for a permission or answer." : agent.status === "error" ? "Recovery failed. Check its agent before retrying." : null };
|
|
242
|
+
const children = await subagentsRunning(client, agent, "Recovery subagent status unavailable");
|
|
243
|
+
return { busy: ["running", "initializing"].includes(agent.status) || !!agent.pendingPermissions.length || children, reason: agent.pendingPermissions.length ? "Recovery is waiting for a permission or answer." : agent.status === "error" ? "Recovery failed. Check its agent before retrying." : null };
|
|
223
244
|
});
|
|
224
245
|
}
|
|
225
246
|
async send(w: Workspace, batch: Batch, text: string): Promise<void> {
|