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