@dorokuma/herdsman 0.9.0 → 0.11.5
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/README.ja.md +4 -2
- package/README.md +5 -3
- package/dist/src/agent-history/antigravity-proto.js +1 -1
- package/dist/src/agent-history/antigravity-proto.js.map +1 -1
- package/dist/src/agent-history/antigravity-reader.d.ts +13 -1
- package/dist/src/agent-history/antigravity-reader.d.ts.map +1 -1
- package/dist/src/agent-history/antigravity-reader.js +22 -1
- package/dist/src/agent-history/antigravity-reader.js.map +1 -1
- package/dist/src/agent-history/discovery.d.ts +8 -2
- package/dist/src/agent-history/discovery.d.ts.map +1 -1
- package/dist/src/agent-history/discovery.js +89 -9
- package/dist/src/agent-history/discovery.js.map +1 -1
- package/dist/src/agent-history/readers.d.ts +15 -1
- package/dist/src/agent-history/readers.d.ts.map +1 -1
- package/dist/src/agent-history/readers.js +77 -31
- package/dist/src/agent-history/readers.js.map +1 -1
- package/dist/src/agent-history/service.d.ts +4 -2
- package/dist/src/agent-history/service.d.ts.map +1 -1
- package/dist/src/agent-history/service.js +22 -22
- package/dist/src/agent-history/service.js.map +1 -1
- package/dist/src/agent-history/source-fingerprint.d.ts +7 -0
- package/dist/src/agent-history/source-fingerprint.d.ts.map +1 -0
- package/dist/src/agent-history/source-fingerprint.js +26 -0
- package/dist/src/agent-history/source-fingerprint.js.map +1 -0
- package/dist/src/agent-history/text.d.ts.map +1 -1
- package/dist/src/agent-history/text.js +5 -0
- package/dist/src/agent-history/text.js.map +1 -1
- package/dist/src/cli/herdsman.d.ts.map +1 -1
- package/dist/src/cli/herdsman.js +31 -9
- package/dist/src/cli/herdsman.js.map +1 -1
- package/dist/src/daemon/agent-event-reconciler.d.ts +25 -4
- package/dist/src/daemon/agent-event-reconciler.d.ts.map +1 -1
- package/dist/src/daemon/agent-event-reconciler.js +84 -6
- package/dist/src/daemon/agent-event-reconciler.js.map +1 -1
- package/dist/src/daemon/herdr-session-watch-manager.d.ts +1 -0
- package/dist/src/daemon/herdr-session-watch-manager.d.ts.map +1 -1
- package/dist/src/daemon/herdr-session-watch-manager.js +176 -55
- package/dist/src/daemon/herdr-session-watch-manager.js.map +1 -1
- package/dist/src/daemon/observability-server.d.ts +3 -0
- package/dist/src/daemon/observability-server.d.ts.map +1 -1
- package/dist/src/daemon/observability-server.js +73 -24
- package/dist/src/daemon/observability-server.js.map +1 -1
- package/dist/src/daemon/process-manager.d.ts +41 -2
- package/dist/src/daemon/process-manager.d.ts.map +1 -1
- package/dist/src/daemon/process-manager.js +340 -19
- package/dist/src/daemon/process-manager.js.map +1 -1
- package/dist/src/daemon/service.d.ts +46 -0
- package/dist/src/daemon/service.d.ts.map +1 -1
- package/dist/src/daemon/service.js +172 -12
- package/dist/src/daemon/service.js.map +1 -1
- package/dist/src/db/agent-events.d.ts +34 -1
- package/dist/src/db/agent-events.d.ts.map +1 -1
- package/dist/src/db/agent-events.js +168 -28
- package/dist/src/db/agent-events.js.map +1 -1
- package/dist/src/db/agent-history-cache.d.ts +4 -0
- package/dist/src/db/agent-history-cache.d.ts.map +1 -1
- package/dist/src/db/agent-history-cache.js +27 -0
- package/dist/src/db/agent-history-cache.js.map +1 -1
- package/dist/src/db/agent-orchestrator-scopes.d.ts +8 -0
- package/dist/src/db/agent-orchestrator-scopes.d.ts.map +1 -1
- package/dist/src/db/agent-orchestrator-scopes.js +13 -0
- package/dist/src/db/agent-orchestrator-scopes.js.map +1 -1
- package/dist/src/db/agents.d.ts +44 -0
- package/dist/src/db/agents.d.ts.map +1 -1
- package/dist/src/db/agents.js +91 -5
- package/dist/src/db/agents.js.map +1 -1
- package/dist/src/db/client.d.ts +1 -0
- package/dist/src/db/client.d.ts.map +1 -1
- package/dist/src/db/client.js +12 -0
- package/dist/src/db/client.js.map +1 -1
- package/dist/src/db/schema.d.ts +391 -0
- package/dist/src/db/schema.d.ts.map +1 -1
- package/dist/src/db/schema.js +34 -0
- package/dist/src/db/schema.js.map +1 -1
- package/dist/src/db/status-event-plans.d.ts +62 -0
- package/dist/src/db/status-event-plans.d.ts.map +1 -0
- package/dist/src/db/status-event-plans.js +147 -0
- package/dist/src/db/status-event-plans.js.map +1 -0
- package/dist/src/herdr/pane-identity-resolver.d.ts +4 -0
- package/dist/src/herdr/pane-identity-resolver.d.ts.map +1 -1
- package/dist/src/herdr/pane-identity-resolver.js +13 -1
- package/dist/src/herdr/pane-identity-resolver.js.map +1 -1
- package/dist/src/herdr/socket-client.d.ts +2 -0
- package/dist/src/herdr/socket-client.d.ts.map +1 -1
- package/dist/src/herdr/socket-client.js +64 -20
- package/dist/src/herdr/socket-client.js.map +1 -1
- package/dist/src/observability/agent-context-service.d.ts +17 -1
- package/dist/src/observability/agent-context-service.d.ts.map +1 -1
- package/dist/src/observability/agent-context-service.js +85 -21
- package/dist/src/observability/agent-context-service.js.map +1 -1
- package/dist/src/observability/agent-index-service.d.ts +47 -2
- package/dist/src/observability/agent-index-service.d.ts.map +1 -1
- package/dist/src/observability/agent-index-service.js +1125 -115
- package/dist/src/observability/agent-index-service.js.map +1 -1
- package/dist/src/observability/agent-orchestrator-service.d.ts.map +1 -1
- package/dist/src/observability/agent-orchestrator-service.js +34 -12
- package/dist/src/observability/agent-orchestrator-service.js.map +1 -1
- package/dist/src/observability/contracts.d.ts +2 -1
- package/dist/src/observability/contracts.d.ts.map +1 -1
- package/dist/src/observability/contracts.js.map +1 -1
- package/dist/src/observability/interactive-pi.d.ts +1 -1
- package/dist/src/observability/interactive-pi.d.ts.map +1 -1
- package/dist/src/observability/interactive-pi.js +1 -1
- package/dist/src/observability/interactive-pi.js.map +1 -1
- package/dist/src/observability/schemas.d.ts +1 -0
- package/dist/src/observability/schemas.d.ts.map +1 -1
- package/dist/src/observability/schemas.js +1 -0
- package/dist/src/observability/schemas.js.map +1 -1
- package/dist/src/observability/turn-completion.d.ts +2 -1
- package/dist/src/observability/turn-completion.d.ts.map +1 -1
- package/dist/src/observability/turn-completion.js +10 -1
- package/dist/src/observability/turn-completion.js.map +1 -1
- package/dist/src/shared/unix-peercred.d.ts +14 -0
- package/dist/src/shared/unix-peercred.d.ts.map +1 -0
- package/dist/src/shared/unix-peercred.js +144 -0
- package/dist/src/shared/unix-peercred.js.map +1 -0
- package/drizzle/0008_pane_tombstones.sql +9 -0
- package/drizzle/0009_high_lake.sql +24 -0
- package/drizzle/meta/0008_snapshot.json +823 -0
- package/drizzle/meta/0009_snapshot.json +975 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +24 -22
|
@@ -1,21 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { safeAllowedSessionPath, sessionPathAllowedByShape } from "../agent-history/discovery.js";
|
|
2
3
|
import { createAgentHistoryService } from "../agent-history/service.js";
|
|
4
|
+
import { hasNonEmptyAssistantMessage } from "../db/agent-events.js";
|
|
5
|
+
import { runSqliteTransaction } from "../db/client.js";
|
|
3
6
|
import { normalizeHerdrSessionSnapshot } from "../herdr/session-snapshot.js";
|
|
4
7
|
import { HerdrSocketClient } from "../herdr/socket-client.js";
|
|
5
8
|
import { AgentContextService } from "../observability/agent-context-service.js";
|
|
6
9
|
import { parseAgentStatus, } from "../observability/contracts.js";
|
|
7
10
|
import { TURN_SIGNAL_WAIT_MS } from "../observability/turn-completion.js";
|
|
11
|
+
export function historyHasAdvanced(current, baseline, options) {
|
|
12
|
+
if (current.lastAssistantMessage?.ref &&
|
|
13
|
+
current.lastAssistantMessage.ref !== (baseline?.lastAssistantMessage?.ref ?? null)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (current.lastAssistantMessage?.text &&
|
|
17
|
+
current.lastAssistantMessage.text !== (baseline?.lastAssistantMessage?.text ?? null)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (options?.requireAssistantChange) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return current.messageCount > (baseline?.messageCount ?? 0);
|
|
24
|
+
}
|
|
25
|
+
export const PLAN_WAITING_HISTORY = "PLAN_WAITING_HISTORY";
|
|
26
|
+
export class PlanWaitingHistoryError extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(PLAN_WAITING_HISTORY);
|
|
29
|
+
this.name = "PlanWaitingHistoryError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sentinel returned by #appendStatusEvents when the plan must be CANCELLED
|
|
34
|
+
* rather than skipped (skip still maps to completed in #runPlanRow). The agent
|
|
35
|
+
* row is gone, so appending would create an undeliverable dangling event.
|
|
36
|
+
*/
|
|
37
|
+
const PLAN_CANCELLED = Symbol("herdsman.status-event-plan-cancelled");
|
|
8
38
|
export class AgentIndexService {
|
|
39
|
+
#activeWaiters = new Map();
|
|
40
|
+
#clearRetry;
|
|
9
41
|
#clientFactory;
|
|
10
42
|
#context;
|
|
11
43
|
#stores;
|
|
12
44
|
#mutationEpochBySession = new Map();
|
|
13
45
|
#pendingPiSessionRefs = new Map();
|
|
46
|
+
#planTailByAgent = new Map();
|
|
14
47
|
#refreshInFlightBySession = new Map();
|
|
48
|
+
#scheduleRetry;
|
|
15
49
|
#sessionOperationTail = new Map();
|
|
50
|
+
#sleep;
|
|
16
51
|
#turnCompletions;
|
|
52
|
+
#waitingHistoryTimers = new Map();
|
|
17
53
|
constructor(options) {
|
|
54
|
+
this.#clearRetry = options.clearRetry ?? ((timer) => clearTimeout(timer));
|
|
18
55
|
this.#clientFactory = options.clientFactory ?? ((input) => new HerdrSocketClient(input));
|
|
56
|
+
this.#scheduleRetry = options.scheduleRetry ?? ((cb, delay) => setTimeout(cb, delay));
|
|
57
|
+
this.#sleep = options.sleep ?? sleep;
|
|
19
58
|
this.#stores = options.stores;
|
|
20
59
|
this.#turnCompletions = options.turnCompletions;
|
|
21
60
|
if (options.context) {
|
|
@@ -38,12 +77,26 @@ export class AgentIndexService {
|
|
|
38
77
|
});
|
|
39
78
|
}
|
|
40
79
|
}
|
|
80
|
+
refreshHerdrSessionFast(input) {
|
|
81
|
+
this.#incrementMutationEpoch(input.herdrSessionName);
|
|
82
|
+
return this.#enqueueSessionOperation(input.herdrSessionName, () => this.#refreshHerdrSessionNow(input));
|
|
83
|
+
}
|
|
41
84
|
refreshHerdrSession(input) {
|
|
42
85
|
const epoch = this.#mutationEpochBySession.get(input.herdrSessionName) ?? 0;
|
|
43
86
|
const existing = this.#refreshInFlightBySession.get(input.herdrSessionName);
|
|
44
87
|
if (existing?.epoch === epoch)
|
|
45
88
|
return existing.promise;
|
|
46
|
-
const promise = this.#enqueueSessionOperation(input.herdrSessionName, () => this.#refreshHerdrSessionNow(input))
|
|
89
|
+
const promise = this.#enqueueSessionOperation(input.herdrSessionName, () => this.#refreshHerdrSessionNow(input)).then(async (intermediate) => {
|
|
90
|
+
const statusEvents = await Promise.all(intermediate.statusEventPlans.map((plan) => this.executeStatusEventPlan(plan)));
|
|
91
|
+
return {
|
|
92
|
+
agents: intermediate.agents,
|
|
93
|
+
contextChangedScopes: intermediate.contextChangedScopes,
|
|
94
|
+
events: [
|
|
95
|
+
...intermediate.events,
|
|
96
|
+
...statusEvents.filter((event) => event !== undefined),
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
});
|
|
47
100
|
this.#refreshInFlightBySession.set(input.herdrSessionName, { epoch, promise });
|
|
48
101
|
const clear = () => {
|
|
49
102
|
if (this.#refreshInFlightBySession.get(input.herdrSessionName)?.promise === promise) {
|
|
@@ -53,10 +106,530 @@ export class AgentIndexService {
|
|
|
53
106
|
void promise.then(clear, clear);
|
|
54
107
|
return promise;
|
|
55
108
|
}
|
|
56
|
-
|
|
109
|
+
handleHerdrEventFast(input) {
|
|
57
110
|
this.#incrementMutationEpoch(input.herdrSessionName);
|
|
58
111
|
return this.#enqueueSessionOperation(input.herdrSessionName, () => this.#handleHerdrEventNow(input));
|
|
59
112
|
}
|
|
113
|
+
async handleHerdrEvent(input) {
|
|
114
|
+
const intermediate = await this.handleHerdrEventFast(input);
|
|
115
|
+
const statusEvents = await Promise.all(intermediate.statusEventPlans.map((plan) => this.executeStatusEventPlan(plan)));
|
|
116
|
+
return {
|
|
117
|
+
contextChangedScopes: intermediate.contextChangedScopes,
|
|
118
|
+
events: [
|
|
119
|
+
...intermediate.events,
|
|
120
|
+
...statusEvents.filter((event) => event !== undefined),
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
stopWaitingHistoryRetries() {
|
|
125
|
+
for (const timer of this.#waitingHistoryTimers.values()) {
|
|
126
|
+
this.#clearRetry(timer);
|
|
127
|
+
}
|
|
128
|
+
this.#waitingHistoryTimers.clear();
|
|
129
|
+
}
|
|
130
|
+
async drainInFlightPlans() {
|
|
131
|
+
const tails = Array.from(this.#planTailByAgent.values());
|
|
132
|
+
await Promise.allSettled(tails);
|
|
133
|
+
}
|
|
134
|
+
#clearWaitingTimer(planId) {
|
|
135
|
+
const existing = this.#waitingHistoryTimers.get(planId);
|
|
136
|
+
if (existing !== undefined) {
|
|
137
|
+
this.#clearRetry(existing);
|
|
138
|
+
this.#waitingHistoryTimers.delete(planId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
#scheduleWaitingHistoryRetry(planId, plan) {
|
|
142
|
+
this.#clearWaitingTimer(planId);
|
|
143
|
+
const timer = this.#scheduleRetry(async () => {
|
|
144
|
+
try {
|
|
145
|
+
const store = this.#stores.statusEventPlans;
|
|
146
|
+
if (!store)
|
|
147
|
+
return;
|
|
148
|
+
const current = store.get(planId);
|
|
149
|
+
if (current.status !== "pending" || current.lastError !== PLAN_WAITING_HISTORY) {
|
|
150
|
+
this.#waitingHistoryTimers.delete(planId);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
await this.#enqueueAgentPlan(plan.agent.id, () => this.#retryWaitingPlanRow(planId, plan));
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.debug("Herdsman waiting history retry callback error", error);
|
|
157
|
+
}
|
|
158
|
+
}, 10_000);
|
|
159
|
+
this.#waitingHistoryTimers.set(planId, timer);
|
|
160
|
+
}
|
|
161
|
+
async #retryWaitingPlanRow(planId, plan) {
|
|
162
|
+
const store = this.#stores.statusEventPlans;
|
|
163
|
+
if (!store)
|
|
164
|
+
return;
|
|
165
|
+
let current;
|
|
166
|
+
try {
|
|
167
|
+
current = store.get(planId);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
this.#waitingHistoryTimers.delete(planId);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (current.status !== "pending" || current.lastError !== PLAN_WAITING_HISTORY) {
|
|
174
|
+
this.#waitingHistoryTimers.delete(planId);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let retryPlan;
|
|
178
|
+
try {
|
|
179
|
+
const refreshed = await this.#context.refreshAgent({
|
|
180
|
+
agent: plan.agent,
|
|
181
|
+
forceRefresh: true,
|
|
182
|
+
identityChanged: false,
|
|
183
|
+
});
|
|
184
|
+
retryPlan = {
|
|
185
|
+
...plan,
|
|
186
|
+
attempts: current.attempts,
|
|
187
|
+
compactHistory: refreshed.snapshot.compactHistory,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
console.warn("Herdsman failed to refresh agent before retry plan row, keeping waiting", {
|
|
192
|
+
planId,
|
|
193
|
+
agentId: plan.agent.id,
|
|
194
|
+
error: err,
|
|
195
|
+
});
|
|
196
|
+
store.markRetry(planId, new PlanWaitingHistoryError());
|
|
197
|
+
const updated = store.get(planId);
|
|
198
|
+
if (updated.status === "pending") {
|
|
199
|
+
this.#scheduleWaitingHistoryRetry(planId, plan);
|
|
200
|
+
}
|
|
201
|
+
else if (updated.status === "failed") {
|
|
202
|
+
console.warn("Herdsman status event plan failed after max attempts", {
|
|
203
|
+
planId,
|
|
204
|
+
agentId: plan.agent.id,
|
|
205
|
+
attempts: updated.attempts,
|
|
206
|
+
from: plan.from,
|
|
207
|
+
to: plan.to,
|
|
208
|
+
herdrSessionName: plan.agent.herdrSessionName,
|
|
209
|
+
});
|
|
210
|
+
this.#appendPlanFailedEvent({
|
|
211
|
+
agent: plan.agent,
|
|
212
|
+
attempts: updated.attempts,
|
|
213
|
+
compactHistory: plan.compactHistory,
|
|
214
|
+
from: plan.from,
|
|
215
|
+
planId,
|
|
216
|
+
reason: updated.lastError,
|
|
217
|
+
to: plan.to,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
await this.#runPlanRow(current, retryPlan);
|
|
223
|
+
}
|
|
224
|
+
async executeStatusEventPlan(plan) {
|
|
225
|
+
if (plan.from === plan.to)
|
|
226
|
+
return undefined;
|
|
227
|
+
if (!this.#stores.statusEventPlans) {
|
|
228
|
+
return this.#enqueueAgentPlan(plan.agent.id, async () => {
|
|
229
|
+
try {
|
|
230
|
+
const event = await this.#appendStatusEvents(plan);
|
|
231
|
+
return event === PLAN_CANCELLED ? undefined : event;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (error instanceof PlanWaitingHistoryError) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const inserted = plan.planId !== undefined
|
|
242
|
+
? this.#stores.statusEventPlans.get(plan.planId)
|
|
243
|
+
: this.#stores.statusEventPlans.insertPending({
|
|
244
|
+
agent: plan.agent,
|
|
245
|
+
from: plan.from,
|
|
246
|
+
herdrEventKey: plan.herdrEventKey ?? null,
|
|
247
|
+
to: plan.to,
|
|
248
|
+
...(plan.compactHistory ? { compactHistory: plan.compactHistory } : {}),
|
|
249
|
+
});
|
|
250
|
+
return this.#enqueueAgentPlan(plan.agent.id, () => this.#runPlanRow(inserted, plan));
|
|
251
|
+
}
|
|
252
|
+
async drainPendingPlans() {
|
|
253
|
+
if (!this.#stores.statusEventPlans)
|
|
254
|
+
return;
|
|
255
|
+
this.#stores.statusEventPlans.resetRunningToPending();
|
|
256
|
+
const rows = this.#stores.statusEventPlans.listUnfinished();
|
|
257
|
+
const tasks = rows.map((row) => {
|
|
258
|
+
try {
|
|
259
|
+
return this.#enqueueAgentPlan(row.agentId, () => this.#drainPlanRow(row));
|
|
260
|
+
}
|
|
261
|
+
catch (_error) {
|
|
262
|
+
// A row whose agent cannot even be resolved must never take down the
|
|
263
|
+
// daemon boot drain (that failure mode used to boot-loop systemd).
|
|
264
|
+
try {
|
|
265
|
+
this.#stores.statusEventPlans.markCancelled(row.id);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// The row may already be gone; the drain must continue regardless.
|
|
269
|
+
}
|
|
270
|
+
console.warn("Herdsman cancelling status event plan for missing agent", {
|
|
271
|
+
agentId: row.agentId,
|
|
272
|
+
herdrSessionName: row.herdrSessionName,
|
|
273
|
+
paneId: row.paneId,
|
|
274
|
+
from: row.fromStatus,
|
|
275
|
+
to: row.toStatus,
|
|
276
|
+
});
|
|
277
|
+
return Promise.resolve();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
// Individual rows may reject (after markRetry) but the drain itself never
|
|
281
|
+
// rejects: every row is either drained, cancelled, or retried.
|
|
282
|
+
await Promise.allSettled(tasks);
|
|
283
|
+
await this.#backfillFailedPlanEvents();
|
|
284
|
+
}
|
|
285
|
+
async #backfillFailedPlanEvents() {
|
|
286
|
+
const store = this.#stores.statusEventPlans;
|
|
287
|
+
if (!store)
|
|
288
|
+
return;
|
|
289
|
+
for (const row of store.listFailed()) {
|
|
290
|
+
const existing = this.#stores.sqlite
|
|
291
|
+
.prepare("select 1 from agent_events where herdr_session_name = ? and idempotency_key = ?")
|
|
292
|
+
.get(row.herdrSessionName, `agent.failed:plan:${row.id}`);
|
|
293
|
+
if (existing)
|
|
294
|
+
continue;
|
|
295
|
+
let agent;
|
|
296
|
+
try {
|
|
297
|
+
agent = this.#stores.agents.get(row.agentId);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
agent = this.#minimalAgentFromLatestEvent(row.agentId, row.herdrSessionName);
|
|
301
|
+
if (!agent) {
|
|
302
|
+
console.warn("Herdsman skipping agent.failed backfill; no agent row or events", {
|
|
303
|
+
agentId: row.agentId,
|
|
304
|
+
herdrSessionName: row.herdrSessionName,
|
|
305
|
+
planId: row.id,
|
|
306
|
+
});
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
this.#appendPlanFailedEvent({
|
|
311
|
+
agent,
|
|
312
|
+
attempts: row.attempts,
|
|
313
|
+
compactHistory: row.compactHistory,
|
|
314
|
+
from: row.fromStatus,
|
|
315
|
+
planId: row.id,
|
|
316
|
+
reason: row.lastError,
|
|
317
|
+
to: row.toStatus,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
#minimalAgentFromLatestEvent(agentId, herdrSessionName) {
|
|
322
|
+
const latest = this.#stores.sqlite
|
|
323
|
+
.prepare(`select pane_id, pane_generation, terminal_id, workspace_id, herdr_session_name
|
|
324
|
+
from agent_events where agent_id = ? order by id desc limit 1`)
|
|
325
|
+
.get(agentId);
|
|
326
|
+
if (!latest?.pane_id || !latest.workspace_id)
|
|
327
|
+
return undefined;
|
|
328
|
+
return {
|
|
329
|
+
agent: null,
|
|
330
|
+
agentSession: null,
|
|
331
|
+
agentStatus: "unknown",
|
|
332
|
+
cwd: null,
|
|
333
|
+
firstSeenAt: new Date(0),
|
|
334
|
+
focused: false,
|
|
335
|
+
foregroundCwd: null,
|
|
336
|
+
herdrSessionName: latest.herdr_session_name || herdrSessionName,
|
|
337
|
+
id: agentId,
|
|
338
|
+
lastSeenAt: new Date(0),
|
|
339
|
+
name: null,
|
|
340
|
+
paneId: latest.pane_id,
|
|
341
|
+
paneRevision: null,
|
|
342
|
+
...(latest.pane_generation === null ? {} : { paneGeneration: latest.pane_generation }),
|
|
343
|
+
tabId: null,
|
|
344
|
+
terminalId: latest.terminal_id,
|
|
345
|
+
workspaceId: latest.workspace_id,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async #drainPlanRow(row) {
|
|
349
|
+
let agent;
|
|
350
|
+
try {
|
|
351
|
+
agent =
|
|
352
|
+
this.#stores.agents.findByPane({
|
|
353
|
+
herdrSessionName: row.herdrSessionName,
|
|
354
|
+
paneId: row.paneId,
|
|
355
|
+
paneGeneration: row.paneGeneration,
|
|
356
|
+
}) ?? this.#stores.agents.get(row.agentId);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
agent = undefined;
|
|
360
|
+
}
|
|
361
|
+
if (!agent) {
|
|
362
|
+
this.#stores.statusEventPlans.markCancelled(row.id);
|
|
363
|
+
console.warn("Herdsman cancelling status event plan for missing agent", {
|
|
364
|
+
agentId: row.agentId,
|
|
365
|
+
herdrSessionName: row.herdrSessionName,
|
|
366
|
+
paneId: row.paneId,
|
|
367
|
+
from: row.fromStatus,
|
|
368
|
+
to: row.toStatus,
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
let activeCompact = row.compactHistory;
|
|
373
|
+
try {
|
|
374
|
+
const refreshed = await this.#context.refreshAgent({
|
|
375
|
+
agent,
|
|
376
|
+
forceRefresh: true,
|
|
377
|
+
identityChanged: false,
|
|
378
|
+
});
|
|
379
|
+
activeCompact = refreshed.snapshot.compactHistory;
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
this.#stores.statusEventPlans.markRetry(row.id, new PlanWaitingHistoryError());
|
|
383
|
+
const updated = this.#stores.statusEventPlans.get(row.id);
|
|
384
|
+
if (updated.status === "pending") {
|
|
385
|
+
console.warn("Herdsman failed to refresh agent during drain plan row, keeping waiting", {
|
|
386
|
+
planId: row.id,
|
|
387
|
+
agentId: agent.id,
|
|
388
|
+
error: err,
|
|
389
|
+
});
|
|
390
|
+
this.#scheduleWaitingHistoryRetry(row.id, {
|
|
391
|
+
agent,
|
|
392
|
+
compactHistory: row.compactHistory,
|
|
393
|
+
from: row.fromStatus,
|
|
394
|
+
to: row.toStatus,
|
|
395
|
+
...(row.herdrEventKey ? { herdrEventKey: row.herdrEventKey } : {}),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
else if (updated.status === "failed") {
|
|
399
|
+
this.#clearWaitingTimer(row.id);
|
|
400
|
+
console.warn("Herdsman plan marked failed", {
|
|
401
|
+
planId: row.id,
|
|
402
|
+
agentId: agent.id,
|
|
403
|
+
from: row.fromStatus,
|
|
404
|
+
to: row.toStatus,
|
|
405
|
+
reason: updated.lastError,
|
|
406
|
+
});
|
|
407
|
+
this.#appendPlanFailedEvent({
|
|
408
|
+
agent,
|
|
409
|
+
attempts: updated.attempts,
|
|
410
|
+
compactHistory: row.compactHistory,
|
|
411
|
+
from: row.fromStatus,
|
|
412
|
+
planId: row.id,
|
|
413
|
+
reason: updated.lastError,
|
|
414
|
+
to: row.toStatus,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const plan = {
|
|
420
|
+
agent,
|
|
421
|
+
attempts: row.attempts,
|
|
422
|
+
compactHistory: activeCompact,
|
|
423
|
+
from: row.fromStatus,
|
|
424
|
+
to: row.toStatus,
|
|
425
|
+
...(row.herdrEventKey ? { herdrEventKey: row.herdrEventKey } : {}),
|
|
426
|
+
};
|
|
427
|
+
await this.#runPlanRow(row, plan);
|
|
428
|
+
}
|
|
429
|
+
#enqueueAgentPlan(agentId, operation) {
|
|
430
|
+
const previous = this.#planTailByAgent.get(agentId) ?? Promise.resolve();
|
|
431
|
+
const result = previous.then(operation, operation);
|
|
432
|
+
const tail = result.then(() => undefined, () => undefined);
|
|
433
|
+
this.#planTailByAgent.set(agentId, tail);
|
|
434
|
+
void tail.finally(() => {
|
|
435
|
+
if (this.#planTailByAgent.get(agentId) === tail) {
|
|
436
|
+
this.#planTailByAgent.delete(agentId);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
async #runPlanRow(row, plan) {
|
|
442
|
+
const store = this.#stores.statusEventPlans;
|
|
443
|
+
if (!store)
|
|
444
|
+
return undefined;
|
|
445
|
+
const current = store.get(row.id);
|
|
446
|
+
if (current.status === "completed" ||
|
|
447
|
+
current.status === "cancelled" ||
|
|
448
|
+
current.status === "failed") {
|
|
449
|
+
this.#clearWaitingTimer(row.id);
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
store.markRunning(row.id);
|
|
453
|
+
let activePlan = {
|
|
454
|
+
...plan,
|
|
455
|
+
attempts: row.attempts,
|
|
456
|
+
};
|
|
457
|
+
if (row.attempts > 0) {
|
|
458
|
+
try {
|
|
459
|
+
const refreshed = await this.#context.refreshAgent({
|
|
460
|
+
agent: plan.agent,
|
|
461
|
+
forceRefresh: true,
|
|
462
|
+
identityChanged: false,
|
|
463
|
+
});
|
|
464
|
+
activePlan = {
|
|
465
|
+
...plan,
|
|
466
|
+
attempts: row.attempts,
|
|
467
|
+
compactHistory: refreshed.snapshot.compactHistory,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
console.warn("Herdsman failed to refresh agent before running retry row, keeping waiting", {
|
|
472
|
+
planId: row.id,
|
|
473
|
+
agentId: plan.agent.id,
|
|
474
|
+
error: err,
|
|
475
|
+
});
|
|
476
|
+
store.markRetry(row.id, new PlanWaitingHistoryError());
|
|
477
|
+
const updated = store.get(row.id);
|
|
478
|
+
if (updated.status === "pending") {
|
|
479
|
+
this.#scheduleWaitingHistoryRetry(row.id, plan);
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
this.#clearWaitingTimer(row.id);
|
|
483
|
+
if (updated.status === "failed") {
|
|
484
|
+
console.warn("Herdsman status event plan failed after max attempts", {
|
|
485
|
+
planId: row.id,
|
|
486
|
+
agentId: row.agentId,
|
|
487
|
+
attempts: updated.attempts,
|
|
488
|
+
from: row.fromStatus,
|
|
489
|
+
to: row.toStatus,
|
|
490
|
+
herdrSessionName: row.herdrSessionName,
|
|
491
|
+
});
|
|
492
|
+
return this.#appendPlanFailedEvent({
|
|
493
|
+
agent: plan.agent,
|
|
494
|
+
attempts: updated.attempts,
|
|
495
|
+
compactHistory: plan.compactHistory,
|
|
496
|
+
from: row.fromStatus,
|
|
497
|
+
planId: row.id,
|
|
498
|
+
reason: updated.lastError,
|
|
499
|
+
to: row.toStatus,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const event = await this.#appendStatusEvents(activePlan);
|
|
508
|
+
if (event === PLAN_CANCELLED) {
|
|
509
|
+
this.#clearWaitingTimer(row.id);
|
|
510
|
+
store.markCancelled(row.id);
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
if (event === undefined &&
|
|
514
|
+
this.#stores.agents.isPaneClosed({
|
|
515
|
+
herdrSessionName: plan.agent.herdrSessionName,
|
|
516
|
+
paneId: plan.agent.paneId,
|
|
517
|
+
paneGeneration: plan.agent.paneGeneration ?? null,
|
|
518
|
+
})) {
|
|
519
|
+
this.#clearWaitingTimer(row.id);
|
|
520
|
+
store.markCancelled(row.id);
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
this.#clearWaitingTimer(row.id);
|
|
524
|
+
store.markCompleted(row.id);
|
|
525
|
+
return event;
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
if (error instanceof PlanWaitingHistoryError) {
|
|
529
|
+
store.markRetry(row.id, error);
|
|
530
|
+
const updated = store.get(row.id);
|
|
531
|
+
if (updated.status === "pending") {
|
|
532
|
+
this.#scheduleWaitingHistoryRetry(row.id, plan);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
this.#clearWaitingTimer(row.id);
|
|
536
|
+
if (updated.status === "failed") {
|
|
537
|
+
console.warn("Herdsman status event plan failed after max attempts", {
|
|
538
|
+
planId: row.id,
|
|
539
|
+
agentId: row.agentId,
|
|
540
|
+
attempts: updated.attempts,
|
|
541
|
+
from: row.fromStatus,
|
|
542
|
+
to: row.toStatus,
|
|
543
|
+
herdrSessionName: row.herdrSessionName,
|
|
544
|
+
});
|
|
545
|
+
return this.#appendPlanFailedEvent({
|
|
546
|
+
agent: plan.agent,
|
|
547
|
+
attempts: updated.attempts,
|
|
548
|
+
compactHistory: activePlan.compactHistory,
|
|
549
|
+
from: row.fromStatus,
|
|
550
|
+
planId: row.id,
|
|
551
|
+
reason: updated.lastError,
|
|
552
|
+
to: row.toStatus,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
const err = error instanceof Error ? error : undefined;
|
|
559
|
+
const isPaneClosed = this.#stores.agents.isPaneClosed({
|
|
560
|
+
herdrSessionName: plan.agent.herdrSessionName,
|
|
561
|
+
paneId: plan.agent.paneId,
|
|
562
|
+
paneGeneration: plan.agent.paneGeneration ?? null,
|
|
563
|
+
}) ||
|
|
564
|
+
err?.name === "AbortError" ||
|
|
565
|
+
err?.message?.includes("aborted");
|
|
566
|
+
if (isPaneClosed) {
|
|
567
|
+
this.#clearWaitingTimer(row.id);
|
|
568
|
+
store.markCancelled(row.id);
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
this.#clearWaitingTimer(row.id);
|
|
572
|
+
store.markRetry(row.id, error);
|
|
573
|
+
const updated = store.get(row.id);
|
|
574
|
+
if (updated.status === "failed") {
|
|
575
|
+
console.warn("Herdsman status event plan failed after max attempts", {
|
|
576
|
+
planId: row.id,
|
|
577
|
+
agentId: row.agentId,
|
|
578
|
+
attempts: updated.attempts,
|
|
579
|
+
from: row.fromStatus,
|
|
580
|
+
to: row.toStatus,
|
|
581
|
+
herdrSessionName: row.herdrSessionName,
|
|
582
|
+
});
|
|
583
|
+
return this.#appendPlanFailedEvent({
|
|
584
|
+
agent: plan.agent,
|
|
585
|
+
attempts: updated.attempts,
|
|
586
|
+
compactHistory: activePlan.compactHistory,
|
|
587
|
+
from: row.fromStatus,
|
|
588
|
+
planId: row.id,
|
|
589
|
+
reason: updated.lastError,
|
|
590
|
+
to: row.toStatus,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async #waitForHistoryAdvance(input) {
|
|
597
|
+
const maxAttempts = input.maxAttempts ?? 8;
|
|
598
|
+
let refreshed = input.initial
|
|
599
|
+
? { snapshot: { compactHistory: input.initial } }
|
|
600
|
+
: await this.#context.refreshAgent({
|
|
601
|
+
agent: input.agent,
|
|
602
|
+
forceRefresh: true,
|
|
603
|
+
identityChanged: false,
|
|
604
|
+
});
|
|
605
|
+
for (let attempt = 0; attempt < maxAttempts &&
|
|
606
|
+
!historyHasAdvanced(refreshed.snapshot.compactHistory, input.baseline, {
|
|
607
|
+
requireAssistantChange: input.requireAssistantChange,
|
|
608
|
+
}) &&
|
|
609
|
+
!input.controller.signal.aborted; attempt += 1) {
|
|
610
|
+
await this.#sleep(500, input.controller.signal);
|
|
611
|
+
if (input.controller.signal.aborted ||
|
|
612
|
+
this.#stores.agents.isPaneClosed({
|
|
613
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
614
|
+
paneId: input.agent.paneId,
|
|
615
|
+
paneGeneration: input.agent.paneGeneration ?? null,
|
|
616
|
+
})) {
|
|
617
|
+
console.debug("Herdsman skipping status event generation because wait was aborted during history refresh", {
|
|
618
|
+
aborted: input.controller.signal.aborted,
|
|
619
|
+
agentId: input.agent.id,
|
|
620
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
621
|
+
paneId: input.agent.paneId,
|
|
622
|
+
});
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
refreshed = await this.#context.refreshAgent({
|
|
626
|
+
agent: input.agent,
|
|
627
|
+
forceRefresh: true,
|
|
628
|
+
identityChanged: false,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return refreshed.snapshot.compactHistory;
|
|
632
|
+
}
|
|
60
633
|
registerPiSessionRef(input) {
|
|
61
634
|
this.#incrementMutationEpoch(input.herdrSessionName);
|
|
62
635
|
return this.#enqueueSessionOperation(input.herdrSessionName, () => this.#registerPiSessionRefNow(input));
|
|
@@ -76,23 +649,38 @@ export class AgentIndexService {
|
|
|
76
649
|
refreshEpoch,
|
|
77
650
|
mutationEpoch,
|
|
78
651
|
});
|
|
79
|
-
return { agents: previous, contextChangedScopes: [], events: [] };
|
|
80
|
-
}
|
|
81
|
-
if (mutationEpoch !== refreshEpoch) {
|
|
82
|
-
console.warn("Herdsman discarding stale Herdr session refresh", {
|
|
83
|
-
sessionName: input.herdrSessionName,
|
|
84
|
-
refreshEpoch,
|
|
85
|
-
mutationEpoch,
|
|
86
|
-
});
|
|
87
|
-
return { agents: previous, contextChangedScopes: [], events: [] };
|
|
652
|
+
return { agents: previous, contextChangedScopes: [], events: [], statusEventPlans: [] };
|
|
88
653
|
}
|
|
89
|
-
const
|
|
654
|
+
const overlayByPane = new Map();
|
|
655
|
+
for (const pane of snapshot.panes) {
|
|
90
656
|
const value = record(pane);
|
|
91
657
|
const paneId = stringValue(value.pane_id) ?? stringValue(value.paneId);
|
|
658
|
+
if (!paneId)
|
|
659
|
+
continue;
|
|
92
660
|
const revision = integerValue(value.revision);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
661
|
+
const terminalTitle = stringValue(value.terminal_title) ?? stringValue(value.terminalTitle);
|
|
662
|
+
if (revision === undefined && !terminalTitle)
|
|
663
|
+
continue;
|
|
664
|
+
overlayByPane.set(paneId, {
|
|
665
|
+
...(revision === undefined ? {} : { revision }),
|
|
666
|
+
...(terminalTitle ? { terminalTitle } : {}),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const snapshotAgents = snapshot.agents.map((agent) => withPaneRevision(agent, overlayByPane));
|
|
670
|
+
// A live pane that reports a pane_generation overrides any generation-less
|
|
671
|
+
// (legacy) close: drop the legacy tombstone before consulting
|
|
672
|
+
// isPaneClosed so the generation-carrying agent is indexed instead of
|
|
673
|
+
// being swallowed by the phantom close. Generation-scoped tombstones stay.
|
|
674
|
+
for (const agent of snapshotAgents) {
|
|
675
|
+
const paneId = stringValue(agent.pane_id) ?? stringValue(agent.paneId);
|
|
676
|
+
if (paneId && paneGenerationOf(agent)) {
|
|
677
|
+
this.#stores.agents.clearLegacyPaneTombstone({
|
|
678
|
+
herdrSessionName: input.herdrSessionName,
|
|
679
|
+
paneId,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const liveSnapshotAgents = snapshotAgents.filter((agent) => !this.#isClosedPaneAgent(input.herdrSessionName, agent));
|
|
96
684
|
this.#stores.herdrSessions.upsertRunning({
|
|
97
685
|
name: input.herdrSessionName,
|
|
98
686
|
sessionDir: input.sessionDir,
|
|
@@ -103,7 +691,7 @@ export class AgentIndexService {
|
|
|
103
691
|
workspaces: snapshot.workspaces.map(record),
|
|
104
692
|
});
|
|
105
693
|
const indexedAgents = this.#stores.agents.replaceForSession({
|
|
106
|
-
agents:
|
|
694
|
+
agents: liveSnapshotAgents,
|
|
107
695
|
herdrSessionName: input.herdrSessionName,
|
|
108
696
|
});
|
|
109
697
|
const agents = indexedAgents.map((agent) => {
|
|
@@ -122,6 +710,7 @@ export class AgentIndexService {
|
|
|
122
710
|
});
|
|
123
711
|
const scopes = new Map();
|
|
124
712
|
const events = [];
|
|
713
|
+
const statusEventPlans = [];
|
|
125
714
|
const currentIds = new Set(agents.map((agent) => agent.id));
|
|
126
715
|
for (const prior of previous) {
|
|
127
716
|
if (!currentIds.has(prior.id))
|
|
@@ -133,10 +722,21 @@ export class AgentIndexService {
|
|
|
133
722
|
const identityChanged = !prior || !sameIdentity(prior, agent);
|
|
134
723
|
const metadataChanged = !prior || !sameContextMetadata(prior, agent);
|
|
135
724
|
const cached = this.#context.getAgentSnapshot(agent.id);
|
|
725
|
+
const occupiedByOthers = this.#context.occupiedSessionPathsFor(agent);
|
|
726
|
+
const occupancyConflict = Boolean(cached?.historyRef?.path && occupiedByOthers.has(cached.historyRef.path));
|
|
727
|
+
const sessionReady = agent.agentSession?.kind === "path" &&
|
|
728
|
+
safeAllowedSessionPath(agent.agentSession.value) !== null;
|
|
729
|
+
const sessionUnbound = sessionReady &&
|
|
730
|
+
(cached?.historyRef?.kind !== "agent_session" ||
|
|
731
|
+
cached.historyRef.path !== agent.agentSession?.value);
|
|
136
732
|
const dirty = !cached ||
|
|
137
733
|
agent.paneRevision === null ||
|
|
138
734
|
cached.paneRevision !== agent.paneRevision ||
|
|
139
|
-
identityChanged
|
|
735
|
+
identityChanged ||
|
|
736
|
+
cached.historyRef == null ||
|
|
737
|
+
cached.historyRef.kind === "discovered_file" ||
|
|
738
|
+
occupancyConflict ||
|
|
739
|
+
sessionUnbound;
|
|
140
740
|
let refreshed = cached;
|
|
141
741
|
if (dirty) {
|
|
142
742
|
const occupiedForCurrent = new Set(occupiedSessionPaths);
|
|
@@ -157,14 +757,12 @@ export class AgentIndexService {
|
|
|
157
757
|
addScope(scopes, scopeOf(agent));
|
|
158
758
|
}
|
|
159
759
|
if (prior && prior.agentStatus !== agent.agentStatus) {
|
|
160
|
-
|
|
760
|
+
statusEventPlans.push({
|
|
161
761
|
agent,
|
|
162
762
|
compactHistory: refreshed?.compactHistory ?? this.#context.getAgentSnapshot(agent.id)?.compactHistory,
|
|
163
763
|
from: prior.agentStatus,
|
|
164
764
|
to: agent.agentStatus,
|
|
165
765
|
});
|
|
166
|
-
if (event)
|
|
167
|
-
events.push(event);
|
|
168
766
|
addScope(scopes, scopeOf(agent));
|
|
169
767
|
}
|
|
170
768
|
}
|
|
@@ -172,6 +770,7 @@ export class AgentIndexService {
|
|
|
172
770
|
agents,
|
|
173
771
|
contextChangedScopes: sortedScopes(scopes),
|
|
174
772
|
events,
|
|
773
|
+
statusEventPlans,
|
|
175
774
|
};
|
|
176
775
|
}
|
|
177
776
|
finally {
|
|
@@ -182,9 +781,14 @@ export class AgentIndexService {
|
|
|
182
781
|
const event = record(input.event);
|
|
183
782
|
const paneId = stringValue(event.pane_id) ?? stringValue(event.paneId);
|
|
184
783
|
if (!paneId)
|
|
185
|
-
return { contextChangedScopes: [], events: [] };
|
|
784
|
+
return { contextChangedScopes: [], events: [], statusEventPlans: [] };
|
|
186
785
|
if (event.type === "pane.closed") {
|
|
187
786
|
const closedGeneration = paneGenerationFromEvent(event);
|
|
787
|
+
this.#abortPendingWaiters({
|
|
788
|
+
herdrSessionName: input.herdrSessionName,
|
|
789
|
+
paneId,
|
|
790
|
+
paneGeneration: closedGeneration,
|
|
791
|
+
});
|
|
188
792
|
this.#stores.agentEvents.invalidatePane({
|
|
189
793
|
herdrSessionName: input.herdrSessionName,
|
|
190
794
|
paneId,
|
|
@@ -204,7 +808,7 @@ export class AgentIndexService {
|
|
|
204
808
|
});
|
|
205
809
|
const scopes = new Map();
|
|
206
810
|
const closedTerminalId = stringValue(event.terminal_id) ?? stringValue(event.terminalId);
|
|
207
|
-
if (
|
|
811
|
+
if (closedTerminalId) {
|
|
208
812
|
for (const agent of retired) {
|
|
209
813
|
const scope = scopeOf(agent);
|
|
210
814
|
const change = this.#stores.agentOrchestratorScopes?.releaseIfOwnerIdentity({
|
|
@@ -223,10 +827,12 @@ export class AgentIndexService {
|
|
|
223
827
|
return {
|
|
224
828
|
contextChangedScopes: sortedScopes(scopes),
|
|
225
829
|
events: [],
|
|
830
|
+
statusEventPlans: [],
|
|
226
831
|
};
|
|
227
832
|
}
|
|
228
|
-
if (event.type !== "pane.agent_status_changed")
|
|
229
|
-
return { contextChangedScopes: [], events: [] };
|
|
833
|
+
if (event.type !== "pane.agent_status_changed") {
|
|
834
|
+
return { contextChangedScopes: [], events: [], statusEventPlans: [] };
|
|
835
|
+
}
|
|
230
836
|
const eventGeneration = paneGenerationFromEvent(event);
|
|
231
837
|
let agent = this.#stores.agents.findByPane({
|
|
232
838
|
herdrSessionName: input.herdrSessionName,
|
|
@@ -235,6 +841,27 @@ export class AgentIndexService {
|
|
|
235
841
|
});
|
|
236
842
|
let recovered;
|
|
237
843
|
if (!agent) {
|
|
844
|
+
if (eventGeneration) {
|
|
845
|
+
// A generation-scoped tombstone still closes its own generation.
|
|
846
|
+
if (this.#stores.agents.hasGenerationScopedTombstone({
|
|
847
|
+
herdrSessionName: input.herdrSessionName,
|
|
848
|
+
paneId,
|
|
849
|
+
paneGeneration: eventGeneration,
|
|
850
|
+
})) {
|
|
851
|
+
return { contextChangedScopes: [], events: [], statusEventPlans: [] };
|
|
852
|
+
}
|
|
853
|
+
// No generation-scoped close: a live generation-carrying status
|
|
854
|
+
// overrides a generation-less close, so recover the pane (the refresh
|
|
855
|
+
// below clears the legacy tombstone only when the agent is actually
|
|
856
|
+
// present in the snapshot).
|
|
857
|
+
}
|
|
858
|
+
else if (this.#stores.agents.isPaneClosed({
|
|
859
|
+
herdrSessionName: input.herdrSessionName,
|
|
860
|
+
paneId,
|
|
861
|
+
paneGeneration: null,
|
|
862
|
+
})) {
|
|
863
|
+
return { contextChangedScopes: [], events: [], statusEventPlans: [] };
|
|
864
|
+
}
|
|
238
865
|
recovered = await this.#refreshHerdrSessionNow(input);
|
|
239
866
|
agent = this.#stores.agents.findByPane({
|
|
240
867
|
herdrSessionName: input.herdrSessionName,
|
|
@@ -244,19 +871,17 @@ export class AgentIndexService {
|
|
|
244
871
|
}
|
|
245
872
|
if (!agent) {
|
|
246
873
|
return recovered
|
|
247
|
-
? {
|
|
248
|
-
|
|
874
|
+
? {
|
|
875
|
+
contextChangedScopes: recovered.contextChangedScopes,
|
|
876
|
+
events: recovered.events,
|
|
877
|
+
statusEventPlans: recovered.statusEventPlans,
|
|
878
|
+
}
|
|
879
|
+
: { contextChangedScopes: [], events: [], statusEventPlans: [] };
|
|
249
880
|
}
|
|
250
881
|
const from = recovered ? "unknown" : agent.agentStatus;
|
|
251
882
|
const to = parseAgentStatus(event.agent_status);
|
|
252
883
|
const herdrEventKey = herdrInputIdempotencyKey(input.herdrSessionName, paneId, event, to);
|
|
253
|
-
const
|
|
254
|
-
agentStatus: to,
|
|
255
|
-
herdrSessionName: input.herdrSessionName,
|
|
256
|
-
paneId,
|
|
257
|
-
paneGeneration: eventGeneration,
|
|
258
|
-
});
|
|
259
|
-
const current = updated ?? { ...agent, agentStatus: to };
|
|
884
|
+
const current = { ...agent, agentStatus: to };
|
|
260
885
|
const refreshed = await this.#context.refreshAgent({ agent: current, identityChanged: false });
|
|
261
886
|
const scopes = new Map();
|
|
262
887
|
for (const scope of recovered?.contextChangedScopes ?? [])
|
|
@@ -264,26 +889,52 @@ export class AgentIndexService {
|
|
|
264
889
|
if (refreshed.changed || from !== to)
|
|
265
890
|
addScope(scopes, scopeOf(current));
|
|
266
891
|
const events = [...(recovered?.events ?? [])];
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
892
|
+
const statusEventPlans = [...(recovered?.statusEventPlans ?? [])];
|
|
893
|
+
const equivalent = statusEventPlans.some((candidate) => candidate.agent.id === current.id && candidate.to === to);
|
|
894
|
+
const statusInput = {
|
|
895
|
+
agentStatus: to,
|
|
896
|
+
herdrSessionName: input.herdrSessionName,
|
|
897
|
+
paneId,
|
|
898
|
+
paneGeneration: eventGeneration,
|
|
899
|
+
};
|
|
900
|
+
if (from === to) {
|
|
901
|
+
this.#stores.agents.updateStatus(statusInput);
|
|
902
|
+
}
|
|
903
|
+
else if (!equivalent) {
|
|
904
|
+
const inserted = runSqliteTransaction(this.#stores.sqlite, () => {
|
|
905
|
+
const updated = this.#stores.agents.updateStatus(statusInput) ?? current;
|
|
906
|
+
const plan = this.#stores.statusEventPlans.insertPending({
|
|
907
|
+
agent: updated,
|
|
908
|
+
from,
|
|
909
|
+
herdrEventKey: herdrEventKey ?? null,
|
|
910
|
+
to,
|
|
911
|
+
...(refreshed.snapshot.compactHistory
|
|
912
|
+
? { compactHistory: refreshed.snapshot.compactHistory }
|
|
913
|
+
: {}),
|
|
914
|
+
});
|
|
915
|
+
return { plan, updated };
|
|
916
|
+
});
|
|
917
|
+
statusEventPlans.push({
|
|
918
|
+
agent: inserted.updated,
|
|
273
919
|
compactHistory: refreshed.snapshot.compactHistory,
|
|
274
920
|
from,
|
|
275
|
-
herdrEventKey,
|
|
921
|
+
...(herdrEventKey ? { herdrEventKey } : {}),
|
|
922
|
+
planId: inserted.plan.id,
|
|
276
923
|
to,
|
|
277
924
|
});
|
|
278
|
-
if (statusEvent)
|
|
279
|
-
events.push(statusEvent);
|
|
280
925
|
}
|
|
281
|
-
|
|
926
|
+
else {
|
|
927
|
+
this.#stores.agents.updateStatus(statusInput);
|
|
928
|
+
}
|
|
929
|
+
return { contextChangedScopes: sortedScopes(scopes), events, statusEventPlans };
|
|
282
930
|
}
|
|
283
931
|
async #registerPiSessionRefNow(input) {
|
|
284
932
|
const key = terminalSessionKey(input.herdrSessionName, input.terminalId);
|
|
285
933
|
if (input.sessionRef.kind === "path" && !safeAllowedSessionPath(input.sessionRef.value)) {
|
|
286
|
-
|
|
934
|
+
if (existsSync(input.sessionRef.value) ||
|
|
935
|
+
!sessionPathAllowedByShape(input.sessionRef.value)) {
|
|
936
|
+
return { agent: undefined, contextChangedScopes: [] };
|
|
937
|
+
}
|
|
287
938
|
}
|
|
288
939
|
const previous = this.#stores.agents.findByTerminal(input);
|
|
289
940
|
if (!previous) {
|
|
@@ -320,89 +971,392 @@ export class AgentIndexService {
|
|
|
320
971
|
});
|
|
321
972
|
return result;
|
|
322
973
|
}
|
|
974
|
+
#isClosedPaneAgent(herdrSessionName, agent) {
|
|
975
|
+
const paneId = stringValue(agent.pane_id) ?? stringValue(agent.paneId);
|
|
976
|
+
if (!paneId)
|
|
977
|
+
return false;
|
|
978
|
+
return this.#stores.agents.isPaneClosed({
|
|
979
|
+
herdrSessionName,
|
|
980
|
+
paneId,
|
|
981
|
+
paneGeneration: paneGenerationOf(agent),
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
#registerActiveWaiter(agent, controller) {
|
|
985
|
+
const key = `${agent.herdrSessionName}\0${agent.paneId}\0${agent.paneGeneration ?? ""}`;
|
|
986
|
+
let set = this.#activeWaiters.get(key);
|
|
987
|
+
if (!set) {
|
|
988
|
+
set = new Set();
|
|
989
|
+
this.#activeWaiters.set(key, set);
|
|
990
|
+
}
|
|
991
|
+
set.add(controller);
|
|
992
|
+
return () => {
|
|
993
|
+
set.delete(controller);
|
|
994
|
+
if (set.size === 0 && this.#activeWaiters.get(key) === set) {
|
|
995
|
+
this.#activeWaiters.delete(key);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
#abortPendingWaiters(input) {
|
|
1000
|
+
for (const [key, controllers] of this.#activeWaiters) {
|
|
1001
|
+
const [sessionName, paneId, paneGen] = key.split("\0");
|
|
1002
|
+
if (sessionName === input.herdrSessionName && paneId === input.paneId) {
|
|
1003
|
+
if (!input.paneGeneration || !paneGen || input.paneGeneration === paneGen) {
|
|
1004
|
+
for (const controller of controllers) {
|
|
1005
|
+
controller.abort();
|
|
1006
|
+
}
|
|
1007
|
+
controllers.clear();
|
|
1008
|
+
this.#activeWaiters.delete(key);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
323
1013
|
async #appendStatusEvents(input) {
|
|
1014
|
+
// Invariant: compactHistory is always populated as an object before calling #appendStatusEvents; !input.compactHistory check below is defensive and unreachable in normal operation.
|
|
324
1015
|
if (input.from === input.to || !input.compactHistory)
|
|
325
1016
|
return undefined;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
input.agent.agent === "pi" &&
|
|
334
|
-
input.agent.terminalId !== null) {
|
|
335
|
-
const turn = await this.#turnCompletions.waitForSignal({
|
|
1017
|
+
if (this.#stores.agents.isPaneClosed({
|
|
1018
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1019
|
+
paneId: input.agent.paneId,
|
|
1020
|
+
paneGeneration: input.agent.paneGeneration ?? null,
|
|
1021
|
+
})) {
|
|
1022
|
+
console.debug("Herdsman skipping status event generation because pane is closed", {
|
|
1023
|
+
agentId: input.agent.id,
|
|
336
1024
|
herdrSessionName: input.agent.herdrSessionName,
|
|
337
|
-
|
|
338
|
-
terminalId: input.agent.terminalId,
|
|
1025
|
+
paneId: input.agent.paneId,
|
|
339
1026
|
});
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
const latest = this.#stores.agentEvents.latestStatusTransition(input.agent.id, input.agent.herdrSessionName);
|
|
1030
|
+
let skipStatusChanged = false;
|
|
1031
|
+
if (latest && statusTransitionMatches(latest, input.from, input.to)) {
|
|
1032
|
+
const hasTerminalAfter = this.#stores.agentEvents.hasTerminalEventAfter(input.agent.id, input.agent.herdrSessionName, latest.id);
|
|
1033
|
+
if (!hasTerminalAfter) {
|
|
1034
|
+
if (statusEventType(input.to) === undefined) {
|
|
1035
|
+
console.debug("Herdsman skipping duplicate status transition event", {
|
|
1036
|
+
agentId: input.agent.id,
|
|
1037
|
+
from: input.from,
|
|
1038
|
+
to: input.to,
|
|
352
1039
|
});
|
|
1040
|
+
return undefined;
|
|
353
1041
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1042
|
+
skipStatusChanged = true;
|
|
1043
|
+
}
|
|
1044
|
+
if (hasTerminalAfter && !input.herdrEventKey) {
|
|
1045
|
+
const lastTerminal = this.#stores.agentEvents.latestTerminalEvent(input.agent.id, input.agent.herdrSessionName);
|
|
1046
|
+
if (sameTerminalAssistantContent(input.compactHistory, lastTerminal?.compactHistory, input.agent.agent)) {
|
|
1047
|
+
if ((input.attempts ?? 0) > 0) {
|
|
1048
|
+
throw new PlanWaitingHistoryError();
|
|
1049
|
+
}
|
|
1050
|
+
console.debug("Herdsman skipping legacy duplicate status transition event with identical history ref", {
|
|
1051
|
+
agentId: input.agent.id,
|
|
1052
|
+
from: input.from,
|
|
1053
|
+
to: input.to,
|
|
1054
|
+
});
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const observationId = `transition:${latest?.id ?? 0}`;
|
|
1060
|
+
const controller = new AbortController();
|
|
1061
|
+
const unregister = this.#registerActiveWaiter(input.agent, controller);
|
|
1062
|
+
try {
|
|
1063
|
+
let compactHistory = input.compactHistory;
|
|
1064
|
+
let payloadExtra = {};
|
|
1065
|
+
if (this.#turnCompletions !== undefined &&
|
|
1066
|
+
(input.to === "done" || input.to === "blocked") &&
|
|
1067
|
+
input.agent.terminalId !== null &&
|
|
1068
|
+
input.agent.agent === "pi") {
|
|
1069
|
+
const isRetry = (input.attempts ?? 0) > 0;
|
|
1070
|
+
const latestTerminal = this.#stores.agentEvents.latestTerminalEvent(input.agent.id, input.agent.herdrSessionName);
|
|
1071
|
+
const baseline = isRetry && latestTerminal?.compactHistory
|
|
1072
|
+
? latestTerminal.compactHistory
|
|
1073
|
+
: input.compactHistory;
|
|
1074
|
+
const isAlreadyEmittedInRetry = isRetry &&
|
|
1075
|
+
latestTerminal?.compactHistory &&
|
|
1076
|
+
sameTerminalAssistantContent(input.compactHistory, latestTerminal.compactHistory, "pi");
|
|
1077
|
+
const turn = await this.#turnCompletions.waitForSignal({
|
|
357
1078
|
herdrSessionName: input.agent.herdrSessionName,
|
|
1079
|
+
recordedAfterMs: Date.now() - TURN_SIGNAL_WAIT_MS,
|
|
1080
|
+
signal: controller.signal,
|
|
358
1081
|
terminalId: input.agent.terminalId,
|
|
359
1082
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1083
|
+
if (controller.signal.aborted ||
|
|
1084
|
+
this.#stores.agents.isPaneClosed({
|
|
1085
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1086
|
+
paneId: input.agent.paneId,
|
|
1087
|
+
paneGeneration: input.agent.paneGeneration ?? null,
|
|
1088
|
+
})) {
|
|
1089
|
+
console.debug("Herdsman skipping status event generation because turn wait was aborted or pane closed", {
|
|
1090
|
+
aborted: controller.signal.aborted,
|
|
1091
|
+
agentId: input.agent.id,
|
|
1092
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1093
|
+
paneId: input.agent.paneId,
|
|
1094
|
+
});
|
|
1095
|
+
return undefined;
|
|
1096
|
+
}
|
|
1097
|
+
if (turn?.received) {
|
|
1098
|
+
const fresh = (await this.#context.refreshAgent({
|
|
365
1099
|
agent: input.agent,
|
|
366
|
-
identityChanged: false,
|
|
367
1100
|
forceRefresh: true,
|
|
1101
|
+
identityChanged: false,
|
|
1102
|
+
})).snapshot.compactHistory;
|
|
1103
|
+
if (!isAlreadyEmittedInRetry && hasNonEmptyAssistantMessage(fresh)) {
|
|
1104
|
+
compactHistory = fresh;
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
const advanced = await this.#waitForHistoryAdvance({
|
|
1108
|
+
agent: input.agent,
|
|
1109
|
+
baseline,
|
|
1110
|
+
controller,
|
|
1111
|
+
initial: fresh,
|
|
1112
|
+
maxAttempts: 8,
|
|
1113
|
+
requireAssistantChange: isRetry,
|
|
1114
|
+
});
|
|
1115
|
+
if (advanced === undefined)
|
|
1116
|
+
return undefined;
|
|
1117
|
+
if (isRetry &&
|
|
1118
|
+
!historyHasAdvanced(advanced, baseline, { requireAssistantChange: true })) {
|
|
1119
|
+
throw new PlanWaitingHistoryError();
|
|
1120
|
+
}
|
|
1121
|
+
compactHistory = advanced;
|
|
1122
|
+
}
|
|
1123
|
+
console.log(`Herdsman emitted pi agent.${input.to} after turn completion signal (confirmed=${turn.confirmed})`, {
|
|
1124
|
+
agentId: input.agent.id,
|
|
1125
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1126
|
+
terminalId: input.agent.terminalId,
|
|
368
1127
|
});
|
|
369
|
-
compactHistory = refreshed.snapshot.compactHistory;
|
|
370
1128
|
}
|
|
371
|
-
|
|
1129
|
+
else if (!controller.signal.aborted) {
|
|
1130
|
+
const advanced = await this.#waitForHistoryAdvance({
|
|
1131
|
+
agent: input.agent,
|
|
1132
|
+
baseline,
|
|
1133
|
+
controller,
|
|
1134
|
+
maxAttempts: 8,
|
|
1135
|
+
requireAssistantChange: isRetry,
|
|
1136
|
+
});
|
|
1137
|
+
if (advanced === undefined)
|
|
1138
|
+
return undefined;
|
|
1139
|
+
if (isRetry &&
|
|
1140
|
+
!historyHasAdvanced(advanced, baseline, { requireAssistantChange: true })) {
|
|
1141
|
+
throw new PlanWaitingHistoryError();
|
|
1142
|
+
}
|
|
1143
|
+
if (!isRetry &&
|
|
1144
|
+
!historyHasAdvanced(advanced, input.compactHistory, { requireAssistantChange: true })) {
|
|
1145
|
+
compactHistory = { ...advanced, lastAssistantMessage: null };
|
|
1146
|
+
payloadExtra = { noAdvance: true, staleSnapshot: false };
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
compactHistory = advanced;
|
|
1150
|
+
}
|
|
1151
|
+
console.warn(`Herdsman emitted pi agent.${input.to} without a turn completion signal after ${TURN_SIGNAL_WAIT_MS}ms`, {
|
|
1152
|
+
agentId: input.agent.id,
|
|
1153
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1154
|
+
terminalId: input.agent.terminalId,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
else if (input.agent.agent !== "pi" && (input.to === "idle" || input.to === "done")) {
|
|
1159
|
+
// Note: blocked is an interactive intermediate state, exempt from ready gate and empty delivery gate.
|
|
1160
|
+
const isReadyNonPi = (compact) => {
|
|
1161
|
+
if (!compact || !hasNonEmptyAssistantMessage(compact))
|
|
1162
|
+
return false;
|
|
1163
|
+
const latestTerminal = this.#stores.agentEvents.latestTerminalEvent(input.agent.id, input.agent.herdrSessionName);
|
|
1164
|
+
if (compact.source === "antigravity-sqlite" || input.agent.agent === "agy") {
|
|
1165
|
+
const prevRef = latestTerminal?.compactHistory?.lastAssistantMessage?.ref ?? null;
|
|
1166
|
+
const currentRef = compact.lastAssistantMessage?.ref ?? null;
|
|
1167
|
+
return currentRef !== null && currentRef !== prevRef;
|
|
1168
|
+
}
|
|
1169
|
+
if (!latestTerminal)
|
|
1170
|
+
return true;
|
|
1171
|
+
const currentMsg = compact.lastAssistantMessage;
|
|
1172
|
+
const prevMsg = latestTerminal.compactHistory?.lastAssistantMessage;
|
|
1173
|
+
const refChanged = (currentMsg?.ref ?? null) !== (prevMsg?.ref ?? null);
|
|
1174
|
+
const textChanged = (currentMsg?.text ?? "") !== (prevMsg?.text ?? "");
|
|
1175
|
+
const same = sameTerminalAssistantContent(compact, latestTerminal.compactHistory, input.agent.agent);
|
|
1176
|
+
return !same || refChanged || textChanged;
|
|
1177
|
+
};
|
|
1178
|
+
const latestTerminalForSkip = this.#stores.agentEvents.latestTerminalEvent(input.agent.id, input.agent.herdrSessionName);
|
|
1179
|
+
if (input.agent.agent !== "agy" &&
|
|
1180
|
+
compactHistory?.source !== "antigravity-sqlite" &&
|
|
1181
|
+
input.herdrEventKey &&
|
|
1182
|
+
latestTerminalForSkip &&
|
|
1183
|
+
hasNonEmptyAssistantMessage(compactHistory)) {
|
|
1184
|
+
const currentMsg = compactHistory.lastAssistantMessage;
|
|
1185
|
+
const prevMsg = latestTerminalForSkip.compactHistory?.lastAssistantMessage;
|
|
1186
|
+
const refChanged = (currentMsg?.ref ?? null) !== (prevMsg?.ref ?? null);
|
|
1187
|
+
const textChanged = (currentMsg?.text ?? "") !== (prevMsg?.text ?? "");
|
|
1188
|
+
const same = sameTerminalAssistantContent(compactHistory, latestTerminalForSkip.compactHistory, input.agent.agent);
|
|
1189
|
+
if (same && !refChanged && !textChanged) {
|
|
1190
|
+
return undefined;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (!isReadyNonPi(compactHistory)) {
|
|
1194
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1195
|
+
await this.#sleep(500, controller.signal);
|
|
1196
|
+
if (controller.signal.aborted ||
|
|
1197
|
+
this.#stores.agents.isPaneClosed({
|
|
1198
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1199
|
+
paneId: input.agent.paneId,
|
|
1200
|
+
paneGeneration: input.agent.paneGeneration ?? null,
|
|
1201
|
+
})) {
|
|
1202
|
+
console.debug("Herdsman skipping status event generation because wait was aborted or pane closed", {
|
|
1203
|
+
aborted: controller.signal.aborted,
|
|
1204
|
+
agentId: input.agent.id,
|
|
1205
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1206
|
+
paneId: input.agent.paneId,
|
|
1207
|
+
});
|
|
1208
|
+
return undefined;
|
|
1209
|
+
}
|
|
1210
|
+
try {
|
|
1211
|
+
const refreshed = await this.#context.refreshAgent({
|
|
1212
|
+
agent: input.agent,
|
|
1213
|
+
forceRefresh: true,
|
|
1214
|
+
identityChanged: false,
|
|
1215
|
+
});
|
|
1216
|
+
compactHistory = refreshed.snapshot.compactHistory;
|
|
1217
|
+
}
|
|
1218
|
+
catch (err) {
|
|
1219
|
+
console.debug("Herdsman failed to refresh agent during ready wait", err);
|
|
1220
|
+
}
|
|
1221
|
+
if (isReadyNonPi(compactHistory)) {
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (!isReadyNonPi(compactHistory)) {
|
|
1227
|
+
throw new PlanWaitingHistoryError();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (controller.signal.aborted ||
|
|
1231
|
+
this.#stores.agents.isPaneClosed({
|
|
1232
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1233
|
+
paneId: input.agent.paneId,
|
|
1234
|
+
paneGeneration: input.agent.paneGeneration ?? null,
|
|
1235
|
+
})) {
|
|
1236
|
+
console.debug("Herdsman skipping status event generation because turn wait was aborted or pane closed before append", {
|
|
1237
|
+
aborted: controller.signal.aborted,
|
|
372
1238
|
agentId: input.agent.id,
|
|
373
1239
|
herdrSessionName: input.agent.herdrSessionName,
|
|
374
|
-
|
|
1240
|
+
paneId: input.agent.paneId,
|
|
375
1241
|
});
|
|
1242
|
+
return undefined;
|
|
376
1243
|
}
|
|
377
|
-
|
|
378
|
-
const lastEvent = this.#appendAndAckSelfEvent({
|
|
379
|
-
agentId: input.agent.id,
|
|
380
|
-
compactHistory,
|
|
381
|
-
herdrSessionName: input.agent.herdrSessionName,
|
|
382
|
-
idempotencyKey: idempotencyKey("agent.status.changed", input.agent, input.from, input.to, `${observationId}:${input.herdrEventKey ?? "legacy"}:agent.status.changed`),
|
|
383
|
-
paneId: input.agent.paneId,
|
|
384
|
-
paneGeneration: input.agent.paneGeneration ?? null,
|
|
385
|
-
payload: payload(input.agent, input.from, input.to),
|
|
386
|
-
terminalId: input.agent.terminalId,
|
|
387
|
-
type: "agent.status.changed",
|
|
388
|
-
workspaceId: input.agent.workspaceId,
|
|
389
|
-
});
|
|
390
|
-
const statusType = statusEventType(input.to);
|
|
391
|
-
if (statusType) {
|
|
392
|
-
return this.#appendAndAckSelfEvent({
|
|
393
|
-
agentId: input.agent.id,
|
|
394
|
-
compactHistory,
|
|
1244
|
+
const currentAgent = this.#stores.agents.findByPane({
|
|
395
1245
|
herdrSessionName: input.agent.herdrSessionName,
|
|
396
|
-
idempotencyKey: idempotencyKey(statusType, input.agent, input.from, input.to, `${observationId}:${input.herdrEventKey ?? "legacy"}:${statusType}`),
|
|
397
1246
|
paneId: input.agent.paneId,
|
|
398
1247
|
paneGeneration: input.agent.paneGeneration ?? null,
|
|
399
|
-
payload: payload(input.agent, input.from, input.to),
|
|
400
|
-
terminalId: input.agent.terminalId,
|
|
401
|
-
type: statusType,
|
|
402
|
-
workspaceId: input.agent.workspaceId,
|
|
403
1248
|
});
|
|
1249
|
+
if (!currentAgent) {
|
|
1250
|
+
// The agent row is gone: appending would create a dangling event with
|
|
1251
|
+
// a dead agent_id that only the reconciler could sweep. Cancel the
|
|
1252
|
+
// plan instead of marking it completed (the skip->completed branch
|
|
1253
|
+
// must stay reserved for genuine skips).
|
|
1254
|
+
console.debug("Herdsman cancelling terminal status plan because agent row is gone", {
|
|
1255
|
+
agentId: input.agent.id,
|
|
1256
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1257
|
+
paneId: input.agent.paneId,
|
|
1258
|
+
from: input.from,
|
|
1259
|
+
to: input.to,
|
|
1260
|
+
});
|
|
1261
|
+
return PLAN_CANCELLED;
|
|
1262
|
+
}
|
|
1263
|
+
let targetAgent;
|
|
1264
|
+
if (currentAgent.agentStatus !== input.to) {
|
|
1265
|
+
if (input.to === "done" ||
|
|
1266
|
+
input.to === "blocked" ||
|
|
1267
|
+
(input.to === "idle" && currentAgent.agent === "agy")) {
|
|
1268
|
+
console.info("Herdsman emitting terminal status event after subsequent status change", {
|
|
1269
|
+
agentId: input.agent.id,
|
|
1270
|
+
current: currentAgent.agentStatus,
|
|
1271
|
+
expected: input.to,
|
|
1272
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1273
|
+
paneId: input.agent.paneId,
|
|
1274
|
+
});
|
|
1275
|
+
targetAgent = currentAgent;
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
console.debug("Herdsman skipping status event generation due to status mismatch", {
|
|
1279
|
+
agentId: input.agent.id,
|
|
1280
|
+
current: currentAgent.agentStatus,
|
|
1281
|
+
expected: input.to,
|
|
1282
|
+
herdrSessionName: input.agent.herdrSessionName,
|
|
1283
|
+
paneId: input.agent.paneId,
|
|
1284
|
+
});
|
|
1285
|
+
return undefined;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
else {
|
|
1289
|
+
targetAgent = currentAgent;
|
|
1290
|
+
}
|
|
1291
|
+
const eventPayload = { ...payload(targetAgent, input.from, input.to), ...payloadExtra };
|
|
1292
|
+
const lastEvent = skipStatusChanged
|
|
1293
|
+
? undefined
|
|
1294
|
+
: this.#appendAndAckSelfEvent({
|
|
1295
|
+
agentId: targetAgent.id,
|
|
1296
|
+
compactHistory,
|
|
1297
|
+
herdrSessionName: targetAgent.herdrSessionName,
|
|
1298
|
+
idempotencyKey: idempotencyKey("agent.status.changed", targetAgent, input.from, input.to, `${observationId}:${input.herdrEventKey ?? "legacy"}:agent.status.changed`),
|
|
1299
|
+
paneId: targetAgent.paneId,
|
|
1300
|
+
paneGeneration: targetAgent.paneGeneration ?? null,
|
|
1301
|
+
payload: eventPayload,
|
|
1302
|
+
terminalId: targetAgent.terminalId,
|
|
1303
|
+
type: "agent.status.changed",
|
|
1304
|
+
workspaceId: targetAgent.workspaceId,
|
|
1305
|
+
});
|
|
1306
|
+
const statusType = statusEventType(input.to);
|
|
1307
|
+
if (statusType) {
|
|
1308
|
+
return this.#appendAndAckSelfEvent({
|
|
1309
|
+
agentId: targetAgent.id,
|
|
1310
|
+
compactHistory,
|
|
1311
|
+
herdrSessionName: targetAgent.herdrSessionName,
|
|
1312
|
+
idempotencyKey: idempotencyKey(statusType, targetAgent, input.from, input.to, `${observationId}:${input.herdrEventKey ?? "legacy"}:${statusType}`),
|
|
1313
|
+
paneId: targetAgent.paneId,
|
|
1314
|
+
paneGeneration: targetAgent.paneGeneration ?? null,
|
|
1315
|
+
payload: eventPayload,
|
|
1316
|
+
terminalId: targetAgent.terminalId,
|
|
1317
|
+
type: statusType,
|
|
1318
|
+
workspaceId: targetAgent.workspaceId,
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
return lastEvent;
|
|
1322
|
+
}
|
|
1323
|
+
finally {
|
|
1324
|
+
unregister();
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
#appendPlanFailedEvent(input) {
|
|
1328
|
+
let agent = input.agent;
|
|
1329
|
+
try {
|
|
1330
|
+
agent = this.#stores.agents.get(input.agent.id);
|
|
404
1331
|
}
|
|
405
|
-
|
|
1332
|
+
catch {
|
|
1333
|
+
const stitched = this.#minimalAgentFromLatestEvent(input.agent.id, input.agent.herdrSessionName);
|
|
1334
|
+
if (stitched)
|
|
1335
|
+
agent = stitched;
|
|
1336
|
+
}
|
|
1337
|
+
return this.#appendAndAckSelfEvent({
|
|
1338
|
+
agentId: agent.id,
|
|
1339
|
+
compactHistory: input.compactHistory ?? null,
|
|
1340
|
+
herdrSessionName: agent.herdrSessionName,
|
|
1341
|
+
idempotencyKey: `agent.failed:plan:${input.planId}`,
|
|
1342
|
+
paneId: agent.paneId,
|
|
1343
|
+
paneGeneration: agent.paneGeneration ?? null,
|
|
1344
|
+
payload: {
|
|
1345
|
+
agent: agent.agent,
|
|
1346
|
+
attempts: input.attempts,
|
|
1347
|
+
from: input.from,
|
|
1348
|
+
herdrSessionName: agent.herdrSessionName,
|
|
1349
|
+
name: agent.name,
|
|
1350
|
+
paneId: agent.paneId,
|
|
1351
|
+
reason: input.reason ?? "unknown",
|
|
1352
|
+
terminalId: agent.terminalId,
|
|
1353
|
+
to: input.to,
|
|
1354
|
+
workspaceId: agent.workspaceId,
|
|
1355
|
+
},
|
|
1356
|
+
terminalId: agent.terminalId,
|
|
1357
|
+
type: "agent.failed",
|
|
1358
|
+
workspaceId: agent.workspaceId,
|
|
1359
|
+
});
|
|
406
1360
|
}
|
|
407
1361
|
#appendAndAckSelfEvent(input) {
|
|
408
1362
|
const event = this.#stores.agentEvents.append(input);
|
|
@@ -417,13 +1371,33 @@ export class AgentIndexService {
|
|
|
417
1371
|
return event;
|
|
418
1372
|
}
|
|
419
1373
|
}
|
|
420
|
-
function withPaneRevision(agent,
|
|
1374
|
+
function withPaneRevision(agent, overlayByPane) {
|
|
421
1375
|
const raw = record(agent);
|
|
422
|
-
if (integerValue(raw.revision) !== undefined)
|
|
423
|
-
return raw;
|
|
424
1376
|
const paneId = stringValue(raw.pane_id) ?? stringValue(raw.paneId);
|
|
425
|
-
const
|
|
426
|
-
|
|
1377
|
+
const overlay = paneId ? overlayByPane.get(paneId) : undefined;
|
|
1378
|
+
const revision = integerValue(raw.revision) ?? overlay?.revision;
|
|
1379
|
+
const terminalTitle = stringValue(raw.terminal_title) ?? stringValue(raw.terminalTitle) ?? overlay?.terminalTitle;
|
|
1380
|
+
if (revision === undefined && !terminalTitle)
|
|
1381
|
+
return raw;
|
|
1382
|
+
// Collapse dual keys onto herdr-canonical snake_case `terminal_title`.
|
|
1383
|
+
// Incoming snapshots may carry camelCase `terminalTitle`; spreading `raw`
|
|
1384
|
+
// would otherwise leave both keys on the overlay object. Downstream
|
|
1385
|
+
// HerdrAgentLike readers (AgentStore.replaceForSession) still accept
|
|
1386
|
+
// camelCase as fallback, and AgentIndexRecord.terminalTitle is populated
|
|
1387
|
+
// later from whichever key is present.
|
|
1388
|
+
const rest = { ...raw };
|
|
1389
|
+
delete rest.terminalTitle;
|
|
1390
|
+
return {
|
|
1391
|
+
...rest,
|
|
1392
|
+
...(revision === undefined ? {} : { revision }),
|
|
1393
|
+
...(terminalTitle ? { terminal_title: terminalTitle } : {}),
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
function paneGenerationOf(agent) {
|
|
1397
|
+
return (stringValue(agent.pane_generation) ??
|
|
1398
|
+
stringValue(agent.paneGeneration) ??
|
|
1399
|
+
stringValue(agent.creation_id) ??
|
|
1400
|
+
stringValue(agent.creationId));
|
|
427
1401
|
}
|
|
428
1402
|
function matchingPrior(agent, previousByTerminal, previousByPane) {
|
|
429
1403
|
const terminalMatch = agent.terminalId ? previousByTerminal.get(agent.terminalId) : undefined;
|
|
@@ -495,12 +1469,11 @@ function payload(agent, from, to) {
|
|
|
495
1469
|
function idempotencyKey(type, agent, from, to, observationId) {
|
|
496
1470
|
return `${type}:${agent.herdrSessionName}:${agent.paneId}:${from}:${to}:${observationId}`;
|
|
497
1471
|
}
|
|
498
|
-
function herdrInputIdempotencyKey(sessionName, paneId, event,
|
|
1472
|
+
function herdrInputIdempotencyKey(sessionName, paneId, event, _targetStatus) {
|
|
499
1473
|
const eventId = stringValue(event.id) ?? stringValue(event.event_id) ?? stringValue(event.eventId);
|
|
500
|
-
if (eventId)
|
|
501
|
-
return
|
|
502
|
-
|
|
503
|
-
return `${sessionName}:${paneId}:${revision}:${targetStatus}`;
|
|
1474
|
+
if (!eventId)
|
|
1475
|
+
return undefined;
|
|
1476
|
+
return `${sessionName}:${paneId}:${String(event.type)}:${eventId}`;
|
|
504
1477
|
}
|
|
505
1478
|
function statusTransitionMatches(event, from, to) {
|
|
506
1479
|
const eventPayload = event.payload;
|
|
@@ -521,4 +1494,41 @@ function stringValue(value) {
|
|
|
521
1494
|
function integerValue(value) {
|
|
522
1495
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
523
1496
|
}
|
|
1497
|
+
function sleep(ms, signal) {
|
|
1498
|
+
if (signal?.aborted)
|
|
1499
|
+
return Promise.resolve();
|
|
1500
|
+
return new Promise((resolve) => {
|
|
1501
|
+
let timer;
|
|
1502
|
+
const onAbort = () => {
|
|
1503
|
+
if (timer !== undefined)
|
|
1504
|
+
clearTimeout(timer);
|
|
1505
|
+
resolve();
|
|
1506
|
+
};
|
|
1507
|
+
timer = setTimeout(() => {
|
|
1508
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1509
|
+
resolve();
|
|
1510
|
+
}, ms);
|
|
1511
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
function sameTerminalAssistantContent(current, previous, agentKind) {
|
|
1515
|
+
const currentMsg = current?.lastAssistantMessage;
|
|
1516
|
+
const prevMsg = previous?.lastAssistantMessage;
|
|
1517
|
+
if (!currentMsg || !prevMsg)
|
|
1518
|
+
return false;
|
|
1519
|
+
if (agentKind === "agy" || current?.source === "antigravity-sqlite") {
|
|
1520
|
+
if (currentMsg.ref !== null && prevMsg.ref !== null) {
|
|
1521
|
+
return currentMsg.ref === prevMsg.ref;
|
|
1522
|
+
}
|
|
1523
|
+
return Boolean(currentMsg.text && currentMsg.text === prevMsg.text);
|
|
1524
|
+
}
|
|
1525
|
+
// Non-agy (pi, claude, codex, grok, opencode):
|
|
1526
|
+
if (currentMsg.ref !== null && prevMsg.ref !== null) {
|
|
1527
|
+
return currentMsg.ref === prevMsg.ref;
|
|
1528
|
+
}
|
|
1529
|
+
if (currentMsg.text.length > 0 && prevMsg.text.length > 0) {
|
|
1530
|
+
return currentMsg.text === prevMsg.text;
|
|
1531
|
+
}
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
524
1534
|
//# sourceMappingURL=agent-index-service.js.map
|