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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.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
- private histories = new Map<string, { messageAt: string | null; at: number; history: History; submissions: Submission[]; artifacts: Artifact[] }>();
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() - cached.at >= 15_000) || (["running", "initializing"].includes(agent.status) && Date.now() - cached.at >= 15_000)) scans.push({ row, agent, repo });
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
- this.histories.set(agent.id, { messageAt: agent.lastUserMessageAt, at: Date.now(), history, submissions, artifacts });
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
- const children = await client.listProviderSubagents(agent.id);
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.listProviderSubagents(agent.id);
221
- if (children.error) throw new Error("Recovery subagent status unavailable");
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> {