agent-relay-server 0.53.0 → 0.55.0
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/docs/openapi.json +1 -1
- package/package.json +1 -1
- package/public/assets/{PlanGraphPanel-C81um9s6.js → PlanGraphPanel-CQ0CH7e9.js} +2 -2
- package/public/assets/{PlanGraphPanel-C81um9s6.js.map → PlanGraphPanel-CQ0CH7e9.js.map} +1 -1
- package/public/assets/{activity-DwVgT07D.js → activity-CxnH-3Zt.js} +2 -2
- package/public/assets/{activity-DwVgT07D.js.map → activity-CxnH-3Zt.js.map} +1 -1
- package/public/assets/{agent-profiles-BZ_QdiHU.js → agent-profiles-JhWQY-pg.js} +2 -2
- package/public/assets/{agent-profiles-BZ_QdiHU.js.map → agent-profiles-JhWQY-pg.js.map} +1 -1
- package/public/assets/agents-Bn7CXOMZ.js +2 -0
- package/public/assets/{agents-DV5y_tF1.js.map → agents-Bn7CXOMZ.js.map} +1 -1
- package/public/assets/{analytics-CzmIT4LJ.js → analytics-BCVgYihk.js} +3 -3
- package/public/assets/{analytics-CzmIT4LJ.js.map → analytics-BCVgYihk.js.map} +1 -1
- package/public/assets/{automation-Bcx2SOdF.js → automation-Cu5J1Yh4.js} +2 -2
- package/public/assets/{automation-Bcx2SOdF.js.map → automation-Cu5J1Yh4.js.map} +1 -1
- package/public/assets/{badge-BR4bvEsy.js → badge-D6yb-Kia.js} +2 -2
- package/public/assets/{badge-BR4bvEsy.js.map → badge-D6yb-Kia.js.map} +1 -1
- package/public/assets/branch-state-badge-fOM74RDq.js +2 -0
- package/public/assets/{branch-state-badge-CzlsNakd.js.map → branch-state-badge-fOM74RDq.js.map} +1 -1
- package/public/assets/{button-BwqJZS4O.js → button-B-w6a1Ng.js} +2 -2
- package/public/assets/{button-BwqJZS4O.js.map → button-B-w6a1Ng.js.map} +1 -1
- package/public/assets/card-j6bjYxys.js +2 -0
- package/public/assets/{card-C7JtSyuM.js.map → card-j6bjYxys.js.map} +1 -1
- package/public/assets/{channels-By6jRWVP.js → channels-D4q8I1q-.js} +2 -2
- package/public/assets/{channels-By6jRWVP.js.map → channels-D4q8I1q-.js.map} +1 -1
- package/public/assets/chat-6QaNdZGN.js +2 -0
- package/public/assets/{chat-9KhGPR6T.js.map → chat-6QaNdZGN.js.map} +1 -1
- package/public/assets/{connectors-DzhH5WYg.js → connectors-DuWMf8KT.js} +2 -2
- package/public/assets/{connectors-DzhH5WYg.js.map → connectors-DuWMf8KT.js.map} +1 -1
- package/public/assets/{copy-button-nbDhTE0M.js → copy-button-CJDmQ9UB.js} +2 -2
- package/public/assets/{copy-button-nbDhTE0M.js.map → copy-button-CJDmQ9UB.js.map} +1 -1
- package/public/assets/{dist-BIAvrITs.js → dist-1iqvQR2q.js} +2 -2
- package/public/assets/{dist-BIAvrITs.js.map → dist-1iqvQR2q.js.map} +1 -1
- package/public/assets/{dist-MdPDPcyo.js → dist-BNF6RqQf.js} +2 -2
- package/public/assets/{dist-MdPDPcyo.js.map → dist-BNF6RqQf.js.map} +1 -1
- package/public/assets/{dist-Dvspm7RE.js → dist-Bg5mAaEO.js} +2 -2
- package/public/assets/{dist-Dvspm7RE.js.map → dist-Bg5mAaEO.js.map} +1 -1
- package/public/assets/{dist-C7UYAwBf.js → dist-BtFzbpiK.js} +2 -2
- package/public/assets/{dist-C7UYAwBf.js.map → dist-BtFzbpiK.js.map} +1 -1
- package/public/assets/{dist-svixSeDF.js → dist-CLUvHFPE.js} +2 -2
- package/public/assets/{dist-svixSeDF.js.map → dist-CLUvHFPE.js.map} +1 -1
- package/public/assets/{dist-BiVJ8IWp.js → dist-Cl4PfJEy.js} +2 -2
- package/public/assets/{dist-BiVJ8IWp.js.map → dist-Cl4PfJEy.js.map} +1 -1
- package/public/assets/dist-D1g0W-kO.js +2 -0
- package/public/assets/{dist-BkDVLgYg.js.map → dist-D1g0W-kO.js.map} +1 -1
- package/public/assets/{dist-DvLO--_O.js → dist-DLc4vQoq.js} +2 -2
- package/public/assets/{dist-DvLO--_O.js.map → dist-DLc4vQoq.js.map} +1 -1
- package/public/assets/{dist-CSISzAHD.js → dist-HMkrkMzb.js} +2 -2
- package/public/assets/{dist-CSISzAHD.js.map → dist-HMkrkMzb.js.map} +1 -1
- package/public/assets/dist-MX7mVzej.js +2 -0
- package/public/assets/{dist-D5y1HX8f.js.map → dist-MX7mVzej.js.map} +1 -1
- package/public/assets/dist-fe-f7EIl.js +2 -0
- package/public/assets/{dist-tX4uIBFU.js.map → dist-fe-f7EIl.js.map} +1 -1
- package/public/assets/{es2015-Cu7l1536.js → es2015-4dO99MHc.js} +2 -2
- package/public/assets/{es2015-Cu7l1536.js.map → es2015-4dO99MHc.js.map} +1 -1
- package/public/assets/{formatted-body-impl-tuH3hn4f.js → formatted-body-impl-CTv-ZFNo.js} +2 -2
- package/public/assets/{formatted-body-impl-tuH3hn4f.js.map → formatted-body-impl-CTv-ZFNo.js.map} +1 -1
- package/public/assets/index-Cobj8Cqd.js +21 -0
- package/public/assets/index-Cobj8Cqd.js.map +1 -0
- package/public/assets/index-D7FDcoef.css +2 -0
- package/public/assets/{input-YoPXrp72.js → input-EazHb1Bc.js} +2 -2
- package/public/assets/{input-YoPXrp72.js.map → input-EazHb1Bc.js.map} +1 -1
- package/public/assets/{insights-Cmkftpne.js → insights-Ucwp3dHJ.js} +2 -2
- package/public/assets/{insights-Cmkftpne.js.map → insights-Ucwp3dHJ.js.map} +1 -1
- package/public/assets/{integrations-zGR2Hmle.js → integrations-eRsF4CjO.js} +2 -2
- package/public/assets/{integrations-zGR2Hmle.js.map → integrations-eRsF4CjO.js.map} +1 -1
- package/public/assets/{lib-D8ccisdr.js → lib-C7poqE0I.js} +3 -3
- package/public/assets/{lib-D8ccisdr.js.map → lib-C7poqE0I.js.map} +1 -1
- package/public/assets/{lucide-react-ChOGD4vc.js → lucide-react-Dr0-sUx6.js} +2 -2
- package/public/assets/{lucide-react-ChOGD4vc.js.map → lucide-react-Dr0-sUx6.js.map} +1 -1
- package/public/assets/{maintenance-Dr0Y-QW1.js → maintenance-BW56fE-T.js} +2 -2
- package/public/assets/{maintenance-Dr0Y-QW1.js.map → maintenance-BW56fE-T.js.map} +1 -1
- package/public/assets/{managed-agents-Dd_CLfkU.js → managed-agents-BkFAu1Ub.js} +2 -2
- package/public/assets/{managed-agents-Dd_CLfkU.js.map → managed-agents-BkFAu1Ub.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CasNfCMM.js → markdown-preview-impl-BT43MQ7E.js} +2 -2
- package/public/assets/{markdown-preview-impl-CasNfCMM.js.map → markdown-preview-impl-BT43MQ7E.js.map} +1 -1
- package/public/assets/memory-Cw279dr5.js +2 -0
- package/public/assets/{memory-FCZUrHDG.js.map → memory-Cw279dr5.js.map} +1 -1
- package/public/assets/{messages-BVidQ6qj.js → messages-5InjjgUv.js} +2 -2
- package/public/assets/{messages-BVidQ6qj.js.map → messages-5InjjgUv.js.map} +1 -1
- package/public/assets/{orchestrators-CDLPID66.js → orchestrators-B78CQxLl.js} +2 -2
- package/public/assets/{orchestrators-CDLPID66.js.map → orchestrators-B78CQxLl.js.map} +1 -1
- package/public/assets/{overview-CBBOJR6C.js → overview-C9V2X2oo.js} +2 -2
- package/public/assets/{overview-CBBOJR6C.js.map → overview-C9V2X2oo.js.map} +1 -1
- package/public/assets/{pairs-DT1DGrb8.js → pairs-Cc3tOjXB.js} +2 -2
- package/public/assets/{pairs-DT1DGrb8.js.map → pairs-Cc3tOjXB.js.map} +1 -1
- package/public/assets/{react-dom-DgWuIw1o.js → react-dom-CQSYEHV9.js} +2 -2
- package/public/assets/{react-dom-DgWuIw1o.js.map → react-dom-CQSYEHV9.js.map} +1 -1
- package/public/assets/{security-BQgWIgl4.js → security-CpBET4KN.js} +2 -2
- package/public/assets/{security-BQgWIgl4.js.map → security-CpBET4KN.js.map} +1 -1
- package/public/assets/{select-yVL8li9x.js → select-FNVeUxFZ.js} +2 -2
- package/public/assets/{select-yVL8li9x.js.map → select-FNVeUxFZ.js.map} +1 -1
- package/public/assets/settings-CPto3TdF.js +10 -0
- package/public/assets/settings-CPto3TdF.js.map +1 -0
- package/public/assets/{store-BlsqvkSE.js → store-h-egULYA.js} +3 -3
- package/public/assets/{store-BlsqvkSE.js.map → store-h-egULYA.js.map} +1 -1
- package/public/assets/{tasks-CySWSOjp.js → tasks-BPplMcnM.js} +2 -2
- package/public/assets/{tasks-CySWSOjp.js.map → tasks-BPplMcnM.js.map} +1 -1
- package/public/assets/teams-CCeuRt7v.js +2 -0
- package/public/assets/{teams-DhFfsGOb.js.map → teams-CCeuRt7v.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BUozTzVL.js → terminal-viewer-impl-DRlbl8Yv.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BUozTzVL.js.map → terminal-viewer-impl-DRlbl8Yv.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-BY99X7eI.js → use-keyboard-viewport-Iaz6IT4p.js} +2 -2
- package/public/assets/{use-keyboard-viewport-BY99X7eI.js.map → use-keyboard-viewport-Iaz6IT4p.js.map} +1 -1
- package/public/assets/{work-queue-CYeuuHjM.js → work-queue-qtjloIIF.js} +2 -2
- package/public/assets/{work-queue-CYeuuHjM.js.map → work-queue-qtjloIIF.js.map} +1 -1
- package/public/assets/workspaces-BcnSfMmi.js +3 -0
- package/public/assets/{workspaces-DrJwKq9X.js.map → workspaces-BcnSfMmi.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/bus.ts +3 -8
- package/src/db/agents.ts +8 -4
- package/src/db/claim-sql.ts +28 -0
- package/src/db/message-reads.ts +98 -4
- package/src/db/stats.ts +2 -1
- package/src/db/tasks.ts +2 -1
- package/src/maintenance.ts +3 -1
- package/src/mcp.ts +2 -0
- package/src/provider-state.ts +66 -0
- package/src/routes/workspaces.ts +2 -2
- package/src/services/rate-limit-resume.ts +96 -0
- package/src/services/register-agent.ts +4 -1
- package/src/services/send-message.ts +13 -1
- package/src/services/spawn-agent.ts +15 -0
- package/src/services/transient-agent-reaper.ts +5 -1
- package/src/spawn-command.ts +8 -0
- package/src/workspace-actions.ts +2 -2
- package/src/workspace-merge.ts +4 -1
- package/public/assets/agents-DV5y_tF1.js +0 -2
- package/public/assets/branch-state-badge-CzlsNakd.js +0 -2
- package/public/assets/card-C7JtSyuM.js +0 -2
- package/public/assets/chat-9KhGPR6T.js +0 -2
- package/public/assets/dist-BkDVLgYg.js +0 -2
- package/public/assets/dist-D5y1HX8f.js +0 -2
- package/public/assets/dist-tX4uIBFU.js +0 -2
- package/public/assets/index-BRi2QbMp.js +0 -21
- package/public/assets/index-BRi2QbMp.js.map +0 -1
- package/public/assets/index-t-8AiaRW.css +0 -2
- package/public/assets/memory-FCZUrHDG.js +0 -2
- package/public/assets/settings-C5Mg9eH8.js +0 -10
- package/public/assets/settings-C5Mg9eH8.js.map +0 -1
- package/public/assets/teams-DhFfsGOb.js +0 -2
- package/public/assets/workspaces-DrJwKq9X.js +0 -3
package/src/db/message-reads.ts
CHANGED
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
17
|
import { getAgent } from "./agents.ts";
|
|
18
|
+
import { sendMessageWithResult } from "./messages.ts";
|
|
18
19
|
import { deleteArtifactLinksForEntity } from "./artifacts.ts";
|
|
19
20
|
import { getDb, timedQuery } from "./connection.ts";
|
|
20
|
-
import { legacyChannelTargets } from "./delivery.ts";
|
|
21
|
+
import { legacyChannelTargets, isChannelAgentId } from "./delivery.ts";
|
|
22
|
+
import { messageTaskClosed } from "./claim-sql.ts";
|
|
21
23
|
import { MSG_SELECT, rowToMessage } from "./mappers.ts";
|
|
22
24
|
import type {
|
|
23
25
|
AgentCard,
|
|
@@ -202,9 +204,13 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
202
204
|
}
|
|
203
205
|
conditions.push("delivery_status != 'queued' AND delivery_status NOT IN ('failed', 'dead')");
|
|
204
206
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
207
|
+
// A claimable message is available to this agent when it is non-claimable, currently held by
|
|
208
|
+
// THIS agent, or unclaimed/expired AND its task is not already closed. The closed-task guard
|
|
209
|
+
// must cover BOTH the null-claim and the expired-claim branch: when a managed agent is
|
|
210
|
+
// pruned/deleted its message claims are released to NULL (see pruneOfflineAgents/deleteAgent),
|
|
211
|
+
// so without it a finished task's message resurrects as fresh work on the next restart (which
|
|
212
|
+
// mints a new agent id, so message_reads no longer dedups it) — the automation re-delivery bug.
|
|
213
|
+
conditions.push(`(claimable = 0 OR claimed_by = ? OR ((claimed_by IS NULL OR claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT ${messageTaskClosed("m.id")}))`);
|
|
208
214
|
params.push(query.for, Date.now());
|
|
209
215
|
|
|
210
216
|
if (query.sinceId !== undefined) {
|
|
@@ -319,6 +325,94 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
|
|
|
319
325
|
.map((message) => replyObligationFromMessage(message, agentId));
|
|
320
326
|
}
|
|
321
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Channel-answer linking: a channel agent answers the human OUT-OF-BAND through its bridge — e.g.
|
|
330
|
+
* `agent-relay /message telegram '{"action":"reply",...}'` — which stores an outbound to the channel
|
|
331
|
+
* agent with NO reply_to / thread / conversation linkage (the bridge protocol body is opaque to the
|
|
332
|
+
* relay, so the inbound's channel + conversation never make it onto the answer). Without linkage the
|
|
333
|
+
* obligation never clears, and the Stop hook nags the agent into a redundant `/reply` that routes back
|
|
334
|
+
* through the channel as a second message to the human (the Telegram double-message friction).
|
|
335
|
+
*
|
|
336
|
+
* When the agent posts an unlinked message into a channel agent and has EXACTLY ONE open obligation on
|
|
337
|
+
* that channel AT THAT MOMENT, the conversation is unambiguous: this send is the answer. We snapshot
|
|
338
|
+
* that here, at send time, by returning the obligation id to link the outbound to — a decision a later
|
|
339
|
+
* sibling conversation can never retroactively confuse (the reason query-time coverage is unsafe). When
|
|
340
|
+
* two conversations are open the send is unattributable; we leave it unlinked for an explicit `/reply`,
|
|
341
|
+
* which carries the conversation precisely. Provider-agnostic: keys only on the channel agent target,
|
|
342
|
+
* never on the body.
|
|
343
|
+
*
|
|
344
|
+
* Returns the inbound message id this outbound answers, or null when nothing should be auto-linked.
|
|
345
|
+
*/
|
|
346
|
+
export function channelObligationAnsweredBy(
|
|
347
|
+
agentId: string,
|
|
348
|
+
channelAgentId: string,
|
|
349
|
+
): number | null {
|
|
350
|
+
if (!isChannelAgentId(channelAgentId)) return null;
|
|
351
|
+
const open = listPendingReplyObligations(agentId, 50).filter(
|
|
352
|
+
(obligation) => obligation.kind === "channel.event" && obligation.from === channelAgentId,
|
|
353
|
+
);
|
|
354
|
+
return open.length === 1 ? open[0]!.messageId : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* #413 / vent #121 — seed a pending reply obligation for a freshly-registered spawned worker so
|
|
359
|
+
* it cannot strand its verdict. A spawned agent gets its task as the positional spawn prompt, NOT
|
|
360
|
+
* as a relay message, so it has no `message_reads` row → no obligation → the Stop hook (#196/#199)
|
|
361
|
+
* finds nothing to enforce and lets the worker idle with its result undelivered. This recovers the
|
|
362
|
+
* spawn command by its `spawnRequestId` (same lookup as `seedContinuationObjectiveFromSpawnPrompt`),
|
|
363
|
+
* and — only when the spawn carried `expectReply` — posts a message FROM the spawner TO the worker
|
|
364
|
+
* and marks it read, making it an obligation immediately. The Stop hook then refuses to let the
|
|
365
|
+
* worker idle until it `/reply`s, which auto-routes back to the spawner and wakes it.
|
|
366
|
+
*
|
|
367
|
+
* Call only on first registration; idempotent regardless (idempotencyKey dedups a double-fire).
|
|
368
|
+
* Never throws — a failure here must not abort registration. Returns the obligation message id,
|
|
369
|
+
* or null when nothing was seeded.
|
|
370
|
+
*/
|
|
371
|
+
export function seedSpawnReplyObligation(
|
|
372
|
+
agentId: string,
|
|
373
|
+
spawnedBy: string | undefined,
|
|
374
|
+
spawnRequestId: string | undefined,
|
|
375
|
+
now = Date.now(),
|
|
376
|
+
): number | null {
|
|
377
|
+
if (!spawnRequestId || !spawnedBy) return null;
|
|
378
|
+
// The spawner must still be registered to author the obligation message.
|
|
379
|
+
if (!getAgent(spawnedBy)) return null;
|
|
380
|
+
try {
|
|
381
|
+
const row = getDb().query(`
|
|
382
|
+
SELECT params FROM commands
|
|
383
|
+
WHERE type = 'agent.spawn' AND correlation_id = ?
|
|
384
|
+
ORDER BY created_at DESC
|
|
385
|
+
LIMIT 1
|
|
386
|
+
`).get(spawnRequestId) as { params: string } | undefined;
|
|
387
|
+
const params = row ? parseJson<unknown>(row.params, {}) : {};
|
|
388
|
+
if (!isRecord(params) || params.expectReply !== true) return null;
|
|
389
|
+
|
|
390
|
+
const objective = typeof params.prompt === "string" ? params.prompt.trim() : "";
|
|
391
|
+
const contract = `Report-back requested by ${spawnedBy}. When you finish your spawned task, reply here with your result so the coordinator is notified — you should not go idle before delivering it.`;
|
|
392
|
+
const body = objective ? `${contract}\n\nYour task was:\n${objective}` : contract;
|
|
393
|
+
|
|
394
|
+
const { message } = sendMessageWithResult({
|
|
395
|
+
from: spawnedBy,
|
|
396
|
+
to: agentId,
|
|
397
|
+
subject: "Report your result when done",
|
|
398
|
+
body,
|
|
399
|
+
replyExpected: true,
|
|
400
|
+
// `payload.source` is the server-owned "this requires a reply" signal (see messageRequiresReply).
|
|
401
|
+
payload: { source: "spawn-obligation", spawnRequestId },
|
|
402
|
+
idempotencyKey: `spawn-obligation:${spawnRequestId}`,
|
|
403
|
+
occurredAt: now,
|
|
404
|
+
});
|
|
405
|
+
// Pre-read so it is a *pending* obligation immediately: the worker got its task via the
|
|
406
|
+
// positional spawn prompt and may finish in one turn without ever polling its inbox, so an
|
|
407
|
+
// unread message would never become an obligation — the exact gap this closes.
|
|
408
|
+
markRead(message.id, agentId);
|
|
409
|
+
return message.id;
|
|
410
|
+
} catch {
|
|
411
|
+
// Registration must succeed even if obligation seeding fails (e.g. a racing spawner exit).
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
322
416
|
export function markRead(messageId: number, agentId: string): boolean {
|
|
323
417
|
const exists = getDb().query("SELECT 1 FROM messages WHERE id = ?").get(messageId);
|
|
324
418
|
if (!exists) return false;
|
package/src/db/stats.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_ME
|
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
17
|
import { listAgents } from "./agents.ts";
|
|
18
18
|
import { listChannels } from "./channels.ts";
|
|
19
|
+
import { messageTaskClosed } from "./claim-sql.ts";
|
|
19
20
|
import { getDb } from "./connection.ts";
|
|
20
21
|
import type {
|
|
21
22
|
AgentCard,
|
|
@@ -245,7 +246,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
245
246
|
});
|
|
246
247
|
|
|
247
248
|
const expiredMessageClaims = (getDb()
|
|
248
|
-
.query(
|
|
249
|
+
.query(`SELECT COUNT(*) as c FROM messages WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT ${messageTaskClosed("messages.id")}`)
|
|
249
250
|
.get(now) as any).c as number;
|
|
250
251
|
checks.push({
|
|
251
252
|
name: "expired-message-claims",
|
package/src/db/tasks.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
|
|
16
16
|
import { matchAgents } from "../agent-ref";
|
|
17
17
|
import { getAgent, settleSingleTargetOnDemandTasks, validateAgentSession } from "./agents.ts";
|
|
18
|
+
import { messageTaskClosed } from "./claim-sql.ts";
|
|
18
19
|
import { ClaimError, getDb } from "./connection.ts";
|
|
19
20
|
import { rowToTask, rowToTaskEvent } from "./mappers.ts";
|
|
20
21
|
import { claimMessageRow } from "./messages.ts";
|
|
@@ -169,7 +170,7 @@ export function recordTaskEvent(taskId: number, input: {
|
|
|
169
170
|
export function releaseExpiredClaims(now: number = Date.now()): { messageIds: number[]; tasks: Task[] } {
|
|
170
171
|
return getDb().transaction(() => {
|
|
171
172
|
settleSingleTargetOnDemandTasks("claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?)", [now], now, "claim-lease-expired");
|
|
172
|
-
const releasableMessageClaim =
|
|
173
|
+
const releasableMessageClaim = `claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT ${messageTaskClosed("messages.id")}`;
|
|
173
174
|
const messageRows = getDb()
|
|
174
175
|
.query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
|
|
175
176
|
.all(now) as any[];
|
package/src/maintenance.ts
CHANGED
|
@@ -63,6 +63,7 @@ import { notifySystemMessage } from "./notify";
|
|
|
63
63
|
import { pruneExpiredTokenRecords } from "./token-db";
|
|
64
64
|
import { dissolveTeamById } from "./services/team";
|
|
65
65
|
import { reapTransientAgents, resetTransientTrackerForTests } from "./services/transient-agent-reaper";
|
|
66
|
+
import { resumeRateLimitedAgents, resetRateLimitResumeTrackerForTests } from "./services/rate-limit-resume";
|
|
66
67
|
import type { Command, MaintenanceJob, MaintenanceJobRun } from "./types";
|
|
67
68
|
|
|
68
69
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -135,7 +136,7 @@ const orphanReapEnabled = () => process.env.AGENT_RELAY_ORPHAN_REAP !== "0";
|
|
|
135
136
|
// orchestratorId + session identity -> when we first saw it orphaned (and last reaped).
|
|
136
137
|
const orphanTracker = new Map<string, { firstOrphanedAt: number; lastReapAt?: number }>();
|
|
137
138
|
|
|
138
|
-
export function resetOrphanTrackerForTests(): void { orphanTracker.clear(); resetTransientTrackerForTests(); }
|
|
139
|
+
export function resetOrphanTrackerForTests(): void { orphanTracker.clear(); resetTransientTrackerForTests(); resetRateLimitResumeTrackerForTests(); }
|
|
139
140
|
|
|
140
141
|
interface MaintenanceJobDefinition {
|
|
141
142
|
id: string;
|
|
@@ -300,6 +301,7 @@ const definitions: MaintenanceJobDefinition[] = [
|
|
|
300
301
|
handler: reapOrphanedSessions,
|
|
301
302
|
},
|
|
302
303
|
{ id: "transient-agent-reaper", title: "Transient agent reaper", description: "Cleanly shut down transient spawned agents once they are idle and any isolated branch work has landed.", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: reapTransientAgents },
|
|
304
|
+
{ id: "rate-limit-resume", title: "Rate-limit resume", description: "Auto-resume agents held on a provider usage/rate-limit stall once their reset window has passed (#286).", intervalMs: REAP_INTERVAL_MS, runOnStart: false, handler: resumeRateLimitedAgents },
|
|
303
305
|
{
|
|
304
306
|
id: "team-reap",
|
|
305
307
|
title: "Team lifecycle close-out",
|
package/src/mcp.ts
CHANGED
|
@@ -307,6 +307,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
307
307
|
effort: { type: "string", enum: VALID_EFFORTS },
|
|
308
308
|
approvalMode: { type: "string", enum: APPROVAL_MODES, description: "Permission posture for the worker. Defaults to YOUR OWN approval mode when omitted (a headless `guarded` worker wedges on the first approval prompt), so a coordinator running `open` spawns workers that can act autonomously. Pass an explicit value to narrow a child (e.g. `read-only` reviewer)." },
|
|
309
309
|
prompt: { type: "string", description: "Initial task/message delivered to the agent on launch — spawn and hand it its first instruction in one call (no separate follow-up message needed)." },
|
|
310
|
+
expectReply: { type: "boolean", description: "Require the worker to report back: it gets a pending reply obligation to you and CANNOT go idle until it delivers its result (which routes to your inbox and wakes you). Defaults true when you pass a prompt — set false for a true fire-and-forget worker that needn't report (e.g. one that only lands a branch)." },
|
|
310
311
|
systemPromptAppend: { type: "string" },
|
|
311
312
|
profile: { type: "string", description: "Agent profile name to apply (env, instructions, permissions, MCP/skills, spawn quota)." },
|
|
312
313
|
tags: { type: "array", items: { type: "string" } },
|
|
@@ -868,6 +869,7 @@ async function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknow
|
|
|
868
869
|
policyName: optionalString(args.policyName, "policyName", 120),
|
|
869
870
|
profile: optionalString(args.profile, "profile", 120),
|
|
870
871
|
prompt: optionalString(args.prompt, "prompt", 16_000),
|
|
872
|
+
expectReply: typeof args.expectReply === "boolean" ? args.expectReply : undefined,
|
|
871
873
|
systemPromptAppend: optionalString(args.systemPromptAppend, "systemPromptAppend", 64_000),
|
|
872
874
|
tags: optionalStringArray(args.tags, "tags"),
|
|
873
875
|
capabilities: optionalStringArray(args.capabilities, "capabilities"),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isRecord } from "agent-relay-sdk";
|
|
2
|
+
|
|
3
|
+
// One server-side home for reading an agent's provider runtime state (the
|
|
4
|
+
// `meta.providerState` that both Claude and Codex ride for blocked/approval
|
|
5
|
+
// states — see runner setProviderStatus). Imported by the bus transition audit,
|
|
6
|
+
// the transient-agent reaper (don't reap a held agent), and the rate-limit
|
|
7
|
+
// resume sweep. The dashboard has its own client-side copy in display.ts.
|
|
8
|
+
|
|
9
|
+
export interface ProviderStateView {
|
|
10
|
+
state: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Unix ms the provider's limit window resets (rate-limit holds). */
|
|
14
|
+
resetAt?: number;
|
|
15
|
+
/** Unix ms the hold was entered (used for the resume fallback when resetAt is absent). */
|
|
16
|
+
enteredAt?: number;
|
|
17
|
+
/** Raw provider error_type, kept for empirical observation of which type a real limit fires. */
|
|
18
|
+
errorType?: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HasMeta {
|
|
23
|
+
meta?: Record<string, unknown> | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The agent's provider runtime state, or null when none is set / malformed. */
|
|
27
|
+
export function agentProviderState(agent: HasMeta | null | undefined): ProviderStateView | null {
|
|
28
|
+
const value = agent?.meta?.providerState;
|
|
29
|
+
if (!isRecord(value) || typeof value.state !== "string") return null;
|
|
30
|
+
return value as ProviderStateView;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when the agent is in any blocked provider state (rate-limit, approval, model-unavailable). */
|
|
34
|
+
export function isProviderBlocked(agent: HasMeta | null | undefined): boolean {
|
|
35
|
+
return agentProviderState(agent)?.state === "blocked";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The `reason` value a rate-limit hold carries (matches the runner's control-server seam). */
|
|
39
|
+
export const RATE_LIMIT_BLOCK_REASON = "rate_limit";
|
|
40
|
+
|
|
41
|
+
/** True when the agent is specifically held on a provider usage/rate limit. */
|
|
42
|
+
export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
|
|
43
|
+
const state = agentProviderState(agent);
|
|
44
|
+
return state?.state === "blocked" && state.reason === RATE_LIMIT_BLOCK_REASON;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fallback window when the provider gave no reset time: retry after this long
|
|
48
|
+
// rather than waiting the full (unknown) limit window. Re-entering the hold on a
|
|
49
|
+
// still-limited retry is cheap, and a subscription limit almost always carries a
|
|
50
|
+
// reset time. Read at call-time so operators/tests can tune it.
|
|
51
|
+
const rateLimitFallbackMs = (): number =>
|
|
52
|
+
Number(process.env.AGENT_RELAY_RATE_LIMIT_FALLBACK_MS) || 60 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* When a rate-limit hold should be lifted. The provider's `resetAt` is
|
|
56
|
+
* authoritative; otherwise fall back to `enteredAt + fallback window`. Null only
|
|
57
|
+
* when neither is known (shouldn't happen — the runner always stamps enteredAt).
|
|
58
|
+
*/
|
|
59
|
+
export function rateLimitResumeAt(state: ProviderStateView | null): number | null {
|
|
60
|
+
if (!state) return null;
|
|
61
|
+
if (typeof state.resetAt === "number" && Number.isFinite(state.resetAt)) return state.resetAt;
|
|
62
|
+
if (typeof state.enteredAt === "number" && Number.isFinite(state.enteredAt)) {
|
|
63
|
+
return state.enteredAt + rateLimitFallbackMs();
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -321,8 +321,8 @@ export const postWorkspaceAction: Handler = async (req, params) => {
|
|
|
321
321
|
detail: cleanString(parsed.body.detail, "detail", { max: 4000 }),
|
|
322
322
|
metadata: cleanMeta(parsed.body.metadata) ?? {},
|
|
323
323
|
// No inline fallback: an absent strategy stays undefined and the shared core
|
|
324
|
-
// (requestWorkspaceMerge) applies
|
|
325
|
-
// MCP land tool — one resolution point, no per-surface drift
|
|
324
|
+
// (requestWorkspaceMerge) applies the instance-aware default, identically to
|
|
325
|
+
// the MCP land tool — one resolution point, no per-surface drift.
|
|
326
326
|
strategy: optionalEnum(parsed.body.strategy, "strategy", LAND_STRATEGIES) as WorkspaceMergeStrategy | undefined,
|
|
327
327
|
deleteBranch: typeof parsed.body.deleteBranch === "boolean" ? parsed.body.deleteBranch : undefined,
|
|
328
328
|
force: parsed.body.force === true,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createActivityEvent, listAgents } from "../db";
|
|
2
|
+
import { notifySystemMessage } from "../notify";
|
|
3
|
+
import { agentProviderState, isRateLimitHold, rateLimitResumeAt } from "../provider-state";
|
|
4
|
+
|
|
5
|
+
// #286 Phase 2: auto-resume agents held on a usage/rate-limit stall once their
|
|
6
|
+
// window resets. The runner reports the hold as `providerState: blocked` (reason
|
|
7
|
+
// rate_limit) with a resetAt; this sweep finds holds whose reset has passed and
|
|
8
|
+
// delivers a waking `continue` message. The agent's next turn clears the hold
|
|
9
|
+
// (the runner drops rateLimitHold on busy), so the blocked badge lifts on its own.
|
|
10
|
+
//
|
|
11
|
+
// Fan-out safe by construction: it resumes EVERY held agent past its reset, not
|
|
12
|
+
// one. Durable across restarts: resetAt lives in the agent's persisted meta, and
|
|
13
|
+
// the runner keeps re-asserting the hold while connected, so a relay restart just
|
|
14
|
+
// re-reads it. The cooldown tracker only debounces the brief window between
|
|
15
|
+
// sending the resume and the agent actually going busy.
|
|
16
|
+
|
|
17
|
+
const envMsOrDefault = (name: string, fallback: number): number => {
|
|
18
|
+
const v = Number(process.env[name]);
|
|
19
|
+
return Number.isFinite(v) && v >= 0 ? v : fallback;
|
|
20
|
+
};
|
|
21
|
+
// Don't re-send a resume to the same agent within this window — covers the gap
|
|
22
|
+
// between delivery and the agent flipping to busy (which clears the hold).
|
|
23
|
+
const resumeCooldownMs = () => envMsOrDefault("AGENT_RELAY_RATE_LIMIT_RESUME_COOLDOWN_MS", 5 * 60 * 1000);
|
|
24
|
+
|
|
25
|
+
const resumeTracker = new Map<string, number>();
|
|
26
|
+
|
|
27
|
+
export function resetRateLimitResumeTrackerForTests(): void {
|
|
28
|
+
resumeTracker.clear();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const RESUME_BODY =
|
|
32
|
+
"▶️ Your usage/rate limit has reset — resuming. Continue the task you were working on when the limit interrupted you. If you had already finished, no action is needed.";
|
|
33
|
+
|
|
34
|
+
export function resumeRateLimitedAgents(): Record<string, unknown> {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const cooldown = resumeCooldownMs();
|
|
37
|
+
const seen = new Set<string>();
|
|
38
|
+
const resumed: string[] = [];
|
|
39
|
+
const waiting: string[] = [];
|
|
40
|
+
const cooling: string[] = [];
|
|
41
|
+
|
|
42
|
+
for (const agent of listAgents()) {
|
|
43
|
+
if (!isRateLimitHold(agent)) continue;
|
|
44
|
+
seen.add(agent.id);
|
|
45
|
+
|
|
46
|
+
const state = agentProviderState(agent);
|
|
47
|
+
const resumeAt = rateLimitResumeAt(state);
|
|
48
|
+
if (resumeAt !== null && now < resumeAt) { waiting.push(agent.id); continue; }
|
|
49
|
+
|
|
50
|
+
const lastResume = resumeTracker.get(agent.id);
|
|
51
|
+
if (lastResume !== undefined && now - lastResume < cooldown) { cooling.push(agent.id); continue; }
|
|
52
|
+
resumeTracker.set(agent.id, now);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
notifySystemMessage(agent.id, {
|
|
56
|
+
subject: "Usage limit reset — resume",
|
|
57
|
+
body: RESUME_BODY,
|
|
58
|
+
payload: {
|
|
59
|
+
kind: "agent.rate_limit_resume",
|
|
60
|
+
agentId: agent.id,
|
|
61
|
+
resetAt: state?.resetAt,
|
|
62
|
+
errorType: state?.errorType,
|
|
63
|
+
},
|
|
64
|
+
replyExpected: false,
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
// A stale/unreachable target must never break the sweep; it store-aheads
|
|
68
|
+
// and retries next pass once the cooldown lapses.
|
|
69
|
+
resumeTracker.delete(agent.id);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resumed.push(agent.id);
|
|
74
|
+
createActivityEvent({
|
|
75
|
+
clientId: `rate-limit-resume-${agent.id}-${now}`,
|
|
76
|
+
kind: "state",
|
|
77
|
+
title: "Rate-limit hold resumed",
|
|
78
|
+
body: `Usage window reset — sent resume to ${agent.label ?? agent.id}`,
|
|
79
|
+
meta: agent.id,
|
|
80
|
+
icon: "ti-player-play",
|
|
81
|
+
view: "agents",
|
|
82
|
+
agentId: agent.id,
|
|
83
|
+
metadata: {
|
|
84
|
+
source: "server",
|
|
85
|
+
maintenanceJobId: "rate-limit-resume",
|
|
86
|
+
resetAt: state?.resetAt,
|
|
87
|
+
errorType: state?.errorType,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Forget agents no longer held so a future hold resumes immediately.
|
|
93
|
+
for (const id of resumeTracker.keys()) if (!seen.has(id)) resumeTracker.delete(id);
|
|
94
|
+
|
|
95
|
+
return { held: seen.size, resumed, waiting, cooling, tracked: resumeTracker.size };
|
|
96
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// managed came-up-running reconcile (delegated to managed-running.ts), the single
|
|
10
10
|
// status broadcast, the single timeline note, the spawned-child parent-wake, and
|
|
11
11
|
// the first-registration audit row.
|
|
12
|
-
import { createActivityEvent, getAgent, upsertAgent } from "../db";
|
|
12
|
+
import { createActivityEvent, getAgent, seedSpawnReplyObligation, upsertAgent } from "../db";
|
|
13
13
|
import { emitAgentStatus } from "../sse";
|
|
14
14
|
import { noteAgentTimelineEvent } from "../compaction-watch";
|
|
15
15
|
import { notifyAgentReady } from "../agent-lifecycle-events";
|
|
@@ -59,6 +59,9 @@ export function registerAgent(input: RegisterAgentInput, ctx: AuthContext): Agen
|
|
|
59
59
|
if (agent.ready && !existing?.ready) notifyAgentReady(agent.id);
|
|
60
60
|
|
|
61
61
|
if (!existing) {
|
|
62
|
+
// #413 — a spawned worker with a report-back contract gets a pending reply obligation to its
|
|
63
|
+
// spawner so it can't idle with its verdict undelivered (vent #121). First-registration only.
|
|
64
|
+
seedSpawnReplyObligation(agent.id, agent.spawnedBy, metaStr(agent.meta, "spawnRequestId"));
|
|
62
65
|
createActivityEvent({
|
|
63
66
|
clientId: `server-agent-${agent.id}-registered`,
|
|
64
67
|
kind: "state",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// OUT OF SCOPE (transport-edge, excluded from the parity facet): automatic memory injection
|
|
23
23
|
// (needs request-derived context) and per-transport audit (HTTP domain row vs MCP tool-call row).
|
|
24
24
|
import { isMechanicalMessageKind, isRecord, isReservedAgentId } from "agent-relay-sdk";
|
|
25
|
-
import { ValidationError, getMessage, listAgents, sendMessageWithResult } from "../db";
|
|
25
|
+
import { ValidationError, getMessage, listAgents, sendMessageWithResult, channelObligationAnsweredBy } from "../db";
|
|
26
26
|
import { planSend, type DeliveryReceipt } from "../agent-ref";
|
|
27
27
|
import { emitMessageQueued, emitNewMessage } from "../sse";
|
|
28
28
|
import { getLifecycleManager } from "../lifecycle-manager";
|
|
@@ -159,6 +159,18 @@ export function sendMessageService(
|
|
|
159
159
|
// Resolve BEFORE authorize so both run against the canonical id (kills the HTTP-authorizes-raw
|
|
160
160
|
// vs MCP-authorizes-resolved drift).
|
|
161
161
|
const receipt = resolveSendTarget(input, input.from);
|
|
162
|
+
// Channel-answer linking: an agent answering the human through a channel bridge posts a body-less
|
|
163
|
+
// message to the channel agent (e.g. `agent-relay /message telegram '{"action":"reply",...}'`) that
|
|
164
|
+
// carries no reply/conversation linkage, so its reply obligation never clears and the Stop hook nags
|
|
165
|
+
// it into a redundant `/reply` that leaks back to the human as a second message. When the agent has
|
|
166
|
+
// exactly ONE open obligation on that channel this send is unambiguously the answer — link it here,
|
|
167
|
+
// at send time, so the obligation clears with no extra message. (`channelObligationAnsweredBy` is a
|
|
168
|
+
// no-op unless `to` resolved to a channel agent with a single open obligation; deciding at send time
|
|
169
|
+
// — not query time — keeps a later sibling conversation from retroactively mis-attributing it.)
|
|
170
|
+
if (!input.replyTo && !isMechanicalMessageKind(input.kind)) {
|
|
171
|
+
const answered = channelObligationAnsweredBy(input.from, input.to);
|
|
172
|
+
if (answered !== null) input.replyTo = answered;
|
|
173
|
+
}
|
|
162
174
|
authorizeSend(input, ctx, opts, reservedSink);
|
|
163
175
|
|
|
164
176
|
const result = sendMessageWithResult(input);
|
|
@@ -62,6 +62,15 @@ export interface SpawnAgentInput {
|
|
|
62
62
|
profile?: string;
|
|
63
63
|
prompt?: string;
|
|
64
64
|
systemPromptAppend?: string;
|
|
65
|
+
/**
|
|
66
|
+
* #413 — does this spawn carry a report-back contract? When true, relay seeds a pending
|
|
67
|
+
* reply obligation from the spawner to the child at registration, so the child can't go
|
|
68
|
+
* idle until it delivers its result (vent #121). Defaults true for an agent-initiated spawn
|
|
69
|
+
* that hands the child a prompt (a coordinator handing a worker a task almost always wants
|
|
70
|
+
* the result back); off for system/dashboard/automation spawns (no caller to report to).
|
|
71
|
+
* Pass false to opt a fire-and-forget agent spawn out.
|
|
72
|
+
*/
|
|
73
|
+
expectReply?: boolean;
|
|
65
74
|
tags?: string[];
|
|
66
75
|
capabilities?: string[];
|
|
67
76
|
providerArgs?: string[];
|
|
@@ -204,6 +213,11 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
204
213
|
...(callerTeam ? { teamId: callerTeam.id } : {}),
|
|
205
214
|
});
|
|
206
215
|
|
|
216
|
+
// #413 — default the report-back obligation ON for an agent coordinator handing a worker a
|
|
217
|
+
// task (callerId + a prompt); OFF for system/dashboard/automation spawns (no caller to report
|
|
218
|
+
// back to). Explicit input wins, so a coordinator can opt a fire-and-forget worker out.
|
|
219
|
+
const expectReply = input.expectReply ?? Boolean(callerId && input.prompt && input.prompt.trim());
|
|
220
|
+
|
|
207
221
|
const command = createCommand({
|
|
208
222
|
type: "agent.spawn",
|
|
209
223
|
source: "system",
|
|
@@ -225,6 +239,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
|
|
|
225
239
|
providerArgs: input.providerArgs ?? [],
|
|
226
240
|
prompt: input.prompt,
|
|
227
241
|
systemPromptAppend: input.systemPromptAppend,
|
|
242
|
+
...(expectReply ? { expectReply } : {}),
|
|
228
243
|
policyName: input.policyName,
|
|
229
244
|
spawnRequestId,
|
|
230
245
|
env,
|
|
@@ -2,6 +2,7 @@ import { createCommand } from "../commands-db";
|
|
|
2
2
|
import { emitRelayEvent } from "../events";
|
|
3
3
|
import { createActivityEvent, getAgent, listAgents, listWorkspaces } from "../db";
|
|
4
4
|
import { notifySystemMessage } from "../notify";
|
|
5
|
+
import { isProviderBlocked } from "../provider-state";
|
|
5
6
|
import { TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
|
|
6
7
|
|
|
7
8
|
const envMsOrDefault = (name: string, fallback: number): number => {
|
|
@@ -64,7 +65,10 @@ export function reapTransientAgents(): Record<string, unknown> {
|
|
|
64
65
|
for (const agent of listAgents()) {
|
|
65
66
|
if (agent.lifecycle !== "transient") continue;
|
|
66
67
|
seen.add(agent.id);
|
|
67
|
-
|
|
68
|
+
// A held agent (rate-limit hold #286, or any provider-blocked state) reads as
|
|
69
|
+
// idle+ready but is NOT done — reaping it would kill work that's just waiting
|
|
70
|
+
// for the limit to reset. Treat it as not-idle; the clock restarts on resume.
|
|
71
|
+
if (agent.status !== "idle" || !agent.ready || isProviderBlocked(agent)) { transientTracker.delete(agent.id); continue; }
|
|
68
72
|
const entry = transientTracker.get(agent.id) ?? { firstIdleAt: now };
|
|
69
73
|
transientTracker.set(agent.id, entry);
|
|
70
74
|
const idleForMs = now - entry.firstIdleAt;
|
package/src/spawn-command.ts
CHANGED
|
@@ -91,6 +91,13 @@ interface BuildSpawnCommandOptions {
|
|
|
91
91
|
providerArgs?: string[];
|
|
92
92
|
prompt?: string;
|
|
93
93
|
systemPromptAppend?: string;
|
|
94
|
+
/**
|
|
95
|
+
* #413 — when true, the child registering with this spawnRequestId gets a pending reply
|
|
96
|
+
* obligation back to its spawner, so the Stop hook refuses to let it idle until it delivers
|
|
97
|
+
* its result (closes the "spawned worker strands its verdict" gap, vent #121). Read at
|
|
98
|
+
* registration from the persisted spawn command params.
|
|
99
|
+
*/
|
|
100
|
+
expectReply?: boolean;
|
|
94
101
|
profile?: string;
|
|
95
102
|
agentProfile?: unknown;
|
|
96
103
|
headless?: boolean;
|
|
@@ -138,6 +145,7 @@ export function buildSpawnCommand(opts: BuildSpawnCommandOptions): Record<string
|
|
|
138
145
|
...def(opts.providerArgs, "providerArgs"),
|
|
139
146
|
...def(opts.prompt, "prompt"),
|
|
140
147
|
...def(opts.systemPromptAppend, "systemPromptAppend"),
|
|
148
|
+
...def(opts.expectReply, "expectReply"),
|
|
141
149
|
...def(opts.headless, "headless"),
|
|
142
150
|
...def(opts.policyName, "policyName"),
|
|
143
151
|
...def(opts.automationId, "automationId"),
|
package/src/workspace-actions.ts
CHANGED
|
@@ -167,8 +167,8 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
|
|
|
167
167
|
const result = requestWorkspaceMerge(workspace, {
|
|
168
168
|
requestedBy: agentId ?? "dashboard",
|
|
169
169
|
// Pass the strategy through verbatim (undefined when unset) — the single
|
|
170
|
-
// default lives in requestWorkspaceMerge, so
|
|
171
|
-
// identically instead of each applying its own fallback
|
|
170
|
+
// config-aware default lives in requestWorkspaceMerge, so land surfaces
|
|
171
|
+
// resolve it identically instead of each applying its own fallback.
|
|
172
172
|
strategy: input.strategy,
|
|
173
173
|
deleteBranch: input.deleteBranch !== false,
|
|
174
174
|
prTitle: input.prTitle,
|
package/src/workspace-merge.ts
CHANGED
|
@@ -176,7 +176,10 @@ export function requestWorkspaceMerge(workspace: WorkspaceRecord, opts: RequestW
|
|
|
176
176
|
releaseMergeLease({ repoRoot: workspace.repoRoot, workspaceId: workspace.id });
|
|
177
177
|
return { ok: false, status: 404, error: "workspace not found" };
|
|
178
178
|
}
|
|
179
|
-
const effectiveStrategy =
|
|
179
|
+
const effectiveStrategy = resolveLandStrategy({
|
|
180
|
+
explicit: opts.strategy,
|
|
181
|
+
instanceStrategy: getLandingConfig().strategy,
|
|
182
|
+
});
|
|
180
183
|
|
|
181
184
|
// Slice 6 — issue auto-linkage: append "Closes #N" to the PR body when the
|
|
182
185
|
// workspace metadata carries an issue number, WITHOUT clobbering an explicit body.
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{B as t,Dt as n,Ft as r,G as i,I as a,J as o,P as s,Tn as c,Un as l,bn as u,g as d,i as f,jn as p,kn as m,l as h,m as g,ot as _}from"./lucide-react-ChOGD4vc.js";import{o as v,t as y}from"./store-BlsqvkSE.js";import{B as b,G as x,H as ee,W as te,a as S,d as C,i as ne,n as re}from"./display-CXhnBwGf.js";import{o as w,r as T}from"./dist-CSISzAHD.js";import{t as E}from"./dist-DvLO--_O.js";import{i as D,n as O}from"./dist-MdPDPcyo.js";import{t as k}from"./dist-BiVJ8IWp.js";import{t as A}from"./dist-BIAvrITs.js";import{a as j,i as M,n as N,o as P,r as ie,s as ae,t as F}from"./es2015-Cu7l1536.js";import{a as I,i as oe,n as se,r as L,t as R}from"./dist-svixSeDF.js";import{t as z}from"./badge-BR4bvEsy.js";import{t as B}from"./button-BwqJZS4O.js";import{t as ce}from"./copy-button-nbDhTE0M.js";import{B as le,H as V,P as ue,S as H,U,V as W,d as de,g as fe,h as pe,l as me,m as he,p as ge,u as _e,x as G}from"./index-BRi2QbMp.js";import{n as ve,t as ye}from"./card-C7JtSyuM.js";import{n as be,t as xe}from"./branch-state-badge-CzlsNakd.js";var K=e(l(),1),q=p(),J=`Popover`,[Se,Ce]=D(J,[I]),Y=I(),[we,X]=Se(J),Te=e=>{let{__scopePopover:t,children:n,open:r,defaultOpen:i,onOpenChange:a,modal:o=!1}=e,s=Y(t),c=K.useRef(null),[l,u]=K.useState(!1),[d,f]=k({prop:r,defaultProp:i??!1,onChange:a,caller:J});return(0,q.jsx)(oe,{...s,children:(0,q.jsx)(we,{scope:t,contentId:ae(),triggerRef:c,open:d,onOpenChange:f,onOpenToggle:K.useCallback(()=>f(e=>!e),[f]),hasCustomAnchor:l,onCustomAnchorAdd:K.useCallback(()=>u(!0),[]),onCustomAnchorRemove:K.useCallback(()=>u(!1),[]),modal:o,children:n})})};Te.displayName=J;var Ee=`PopoverAnchor`,De=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Ee,n),a=Y(n),{onCustomAnchorAdd:o,onCustomAnchorRemove:s}=i;return K.useEffect(()=>(o(),()=>s()),[o,s]),(0,q.jsx)(R,{...a,...r,ref:t})});De.displayName=Ee;var Oe=`PopoverTrigger`,ke=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X(Oe,n),a=Y(n),o=w(t,i.triggerRef),s=(0,q.jsx)(E.button,{type:`button`,"aria-haspopup":`dialog`,"aria-expanded":i.open,"aria-controls":i.contentId,"data-state":Ve(i.open),...r,ref:o,onClick:O(e.onClick,i.onOpenToggle)});return i.hasCustomAnchor?s:(0,q.jsx)(R,{asChild:!0,...a,children:s})});ke.displayName=Oe;var Z=`PopoverPortal`,[Ae,je]=Se(Z,{forceMount:void 0}),Me=e=>{let{__scopePopover:t,forceMount:n,children:r,container:i}=e,a=X(Z,t);return(0,q.jsx)(Ae,{scope:t,forceMount:n,children:(0,q.jsx)(A,{present:n||a.open,children:(0,q.jsx)(M,{asChild:!0,container:i,children:r})})})};Me.displayName=Z;var Q=`PopoverContent`,Ne=K.forwardRef((e,t)=>{let n=je(Q,e.__scopePopover),{forceMount:r=n.forceMount,...i}=e,a=X(Q,e.__scopePopover);return(0,q.jsx)(A,{present:r||a.open,children:a.modal?(0,q.jsx)(Fe,{...i,ref:t}):(0,q.jsx)(Ie,{...i,ref:t})})});Ne.displayName=Q;var Pe=T(`PopoverContent.RemoveScroll`),Fe=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(null),i=w(t,r),a=K.useRef(!1);return K.useEffect(()=>{let e=r.current;if(e)return F(e)},[]),(0,q.jsx)(N,{as:Pe,allowPinchZoom:!0,children:(0,q.jsx)(Le,{...e,ref:i,trapFocus:n.open,disableOutsidePointerEvents:!0,onCloseAutoFocus:O(e.onCloseAutoFocus,e=>{e.preventDefault(),a.current||n.triggerRef.current?.focus()}),onPointerDownOutside:O(e.onPointerDownOutside,e=>{let t=e.detail.originalEvent,n=t.button===0&&t.ctrlKey===!0;a.current=t.button===2||n},{checkForDefaultPrevented:!1}),onFocusOutside:O(e.onFocusOutside,e=>e.preventDefault(),{checkForDefaultPrevented:!1})})})}),Ie=K.forwardRef((e,t)=>{let n=X(Q,e.__scopePopover),r=K.useRef(!1),i=K.useRef(!1);return(0,q.jsx)(Le,{...e,ref:t,trapFocus:!1,disableOutsidePointerEvents:!1,onCloseAutoFocus:t=>{e.onCloseAutoFocus?.(t),t.defaultPrevented||(r.current||n.triggerRef.current?.focus(),t.preventDefault()),r.current=!1,i.current=!1},onInteractOutside:t=>{e.onInteractOutside?.(t),t.defaultPrevented||(r.current=!0,t.detail.originalEvent.type===`pointerdown`&&(i.current=!0));let a=t.target;n.triggerRef.current?.contains(a)&&t.preventDefault(),t.detail.originalEvent.type===`focusin`&&i.current&&t.preventDefault()}})}),Le=K.forwardRef((e,t)=>{let{__scopePopover:n,trapFocus:r,onOpenAutoFocus:i,onCloseAutoFocus:a,disableOutsidePointerEvents:o,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onInteractOutside:u,...d}=e,f=X(Q,n),p=Y(n);return ie(),(0,q.jsx)(j,{asChild:!0,loop:!0,trapped:r,onMountAutoFocus:i,onUnmountAutoFocus:a,children:(0,q.jsx)(P,{asChild:!0,disableOutsidePointerEvents:o,onInteractOutside:u,onEscapeKeyDown:s,onPointerDownOutside:c,onFocusOutside:l,onDismiss:()=>f.onOpenChange(!1),children:(0,q.jsx)(L,{"data-state":Ve(f.open),role:`dialog`,id:f.contentId,...p,...d,ref:t,style:{...d.style,"--radix-popover-content-transform-origin":`var(--radix-popper-transform-origin)`,"--radix-popover-content-available-width":`var(--radix-popper-available-width)`,"--radix-popover-content-available-height":`var(--radix-popper-available-height)`,"--radix-popover-trigger-width":`var(--radix-popper-anchor-width)`,"--radix-popover-trigger-height":`var(--radix-popper-anchor-height)`}})})})}),$=`PopoverClose`,Re=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=X($,n);return(0,q.jsx)(E.button,{type:`button`,...r,ref:t,onClick:O(e.onClick,()=>i.onOpenChange(!1))})});Re.displayName=$;var ze=`PopoverArrow`,Be=K.forwardRef((e,t)=>{let{__scopePopover:n,...r}=e,i=Y(n);return(0,q.jsx)(se,{...i,...r,ref:t})});Be.displayName=ze;function Ve(e){return e?`open`:`closed`}var He=Te,Ue=ke,We=Me,Ge=Ne;function Ke({...e}){return(0,q.jsx)(He,{"data-slot":`popover`,...e})}function qe({...e}){return(0,q.jsx)(Ue,{"data-slot":`popover-trigger`,...e})}function Je({className:e,align:t=`center`,sideOffset:n=4,...r}){return(0,q.jsx)(We,{children:(0,q.jsx)(Ge,{"data-slot":`popover-content`,align:t,sideOffset:n,className:m(`z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95`,e),...r})})}function Ye({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-header`,className:m(`flex flex-col gap-0.5 text-sm`,e),...t})}function Xe({className:e,...t}){return(0,q.jsx)(`div`,{"data-slot":`popover-title`,className:m(`font-medium`,e),...t})}function Ze({agent:e,childAgents:a=[],parentAgent:c}){let l=v();y(e=>e.set);let u=y(e=>e.switchView),f=y(e=>e.openInboxThread),p=y(e=>e.openAgentDetail),m=y(e=>e.openPairInvite),w=y(e=>e.openRename),T=y(e=>e.openConfirm),E=y(e=>e.doAgentAction),D=y(e=>e.doDeleteAgent),O=y(e=>e.openFilesForAgent),k=y(e=>e.workspaceAction),A=y(e=>e.openWorkspaceFocus),{terminalOpen:j,terminalTarget:M,terminalOpening:N,openTerminal:P,closeTerminal:ie}=be(e.id),ae=le(),F=ne(l,e,ue(e),ae[e.id]||null),I=S(e,`shutdown`),oe=S(e,`compact`),se=S(e,`clearContext`),L=!!(e.providerCapabilities?.terminal?.live?.read||e.providerCapabilities?.terminal?.attach?.create),R=!!(e.providerCapabilities?.terminal?.live?.write||e.providerCapabilities?.model?.provider===`claude`),V=re(e),U=c?C(c):e.spawnedBy,W=a.length>0;return(0,q.jsxs)(q.Fragment,{children:[(0,q.jsx)(ye,{className:`group hover:border-zinc-600 transition-colors cursor-pointer`,onClick:()=>p(e),children:(0,q.jsxs)(ve,{className:`p-4`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2.5 mb-2.5`,children:[(0,q.jsx)(H,{agent:e,now:l,className:`mt-0.5`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,q.jsx)(`div`,{className:`text-sm font-medium truncate`,children:C(e)}),(0,q.jsx)(`div`,{className:`text-xs mt-0.5 ${te(F.tone)}`,children:F.label}),typeof e.meta?.cwd==`string`&&e.meta.cwd&&(0,q.jsx)(`button`,{type:`button`,className:`mt-0.5 block max-w-full truncate font-mono text-[10px] text-muted-foreground hover:text-foreground`,title:`Open ${e.meta.cwd} in Files`,onClick:t=>{t.stopPropagation(),O(e)},children:b(e.meta.cwd)})]}),e.branchState&&e.branchWorkspaceId&&(0,q.jsx)(xe,{state:e.branchState,className:`mt-0.5 shrink-0`,onClick:t=>{t.stopPropagation(),A(e.branchWorkspaceId)}})]}),F.badges.length>0&&(0,q.jsx)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:F.badges.map(e=>(0,q.jsx)(`span`,{className:`text-[10px] px-1.5 py-0.5 rounded-full font-medium ${e.className}`,children:e.label},e.label))}),(W||e.spawnedBy&&U)&&(0,q.jsxs)(`div`,{className:`mb-2 flex flex-wrap gap-1.5`,onClick:e=>e.stopPropagation(),children:[W&&(0,q.jsxs)(Ke,{children:[(0,q.jsx)(qe,{asChild:!0,children:(0,q.jsxs)(`button`,{type:`button`,className:`inline-flex h-6 max-w-full items-center gap-1.5 rounded-full border border-sky-500/30 bg-sky-500/10 px-2 text-[10px] font-medium text-sky-300 transition-colors hover:bg-sky-500/15`,title:`${a.length} live child ${a.length===1?`agent`:`agents`}`,children:[(0,q.jsx)(h,{className:`h-3 w-3`}),(0,q.jsx)(`span`,{children:a.length}),(0,q.jsx)(`span`,{className:`flex -space-x-1`,children:a.slice(0,4).map(e=>(0,q.jsx)(H,{agent:e,now:l,className:`h-2 w-2 ring-1 ring-background`},e.id))})]})}),(0,q.jsxs)(Je,{align:`start`,className:`w-80`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(Ye,{children:(0,q.jsx)(Xe,{children:`Team`})}),(0,q.jsx)(`div`,{className:`space-y-1.5`,children:a.map(e=>(0,q.jsxs)(`div`,{className:`rounded-md border border-border bg-card/60 p-2`,children:[(0,q.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,q.jsx)(H,{agent:e,now:l,className:`mt-1`}),(0,q.jsx)(G,{agent:e}),(0,q.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,q.jsx)(`div`,{className:`truncate text-xs font-medium`,children:C(e)}),(0,q.jsx)(`div`,{className:`truncate text-[10px] text-muted-foreground`,children:e.providerCapabilities?.model?.alias||e.providerCapabilities?.model?.id||e.providerCapabilities?.model?.provider||e.status})]}),(0,q.jsx)(z,{variant:`outline`,className:`h-4 px-1.5 text-[9px]`,children:e.status})]}),(0,q.jsxs)(`div`,{className:`mt-2 flex gap-1`,children:[(0,q.jsx)(B,{size:`xs`,variant:`outline`,onClick:()=>p(e),children:`Open`}),(0,q.jsxs)(B,{size:`xs`,variant:`ghost`,onClick:()=>{f(e.id),u(`chat`)},children:[(0,q.jsx)(o,{className:`h-3 w-3`}),`Chat`]})]})]},e.id))})]})]}),e.spawnedBy&&U&&(0,q.jsx)(z,{variant:`outline`,className:`max-w-full text-[10px] text-muted-foreground`,children:(0,q.jsxs)(`span`,{className:`truncate`,children:[`spawned by `,U]})})]}),(e.tags.length>0||x(e.capabilities).length>0)&&(0,q.jsxs)(`div`,{className:`flex flex-wrap gap-1 mb-2`,children:[e.tags.slice(0,3).map(e=>(0,q.jsx)(z,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e)),x(e.capabilities).slice(0,2).map(e=>(0,q.jsx)(z,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))]}),(0,q.jsxs)(`div`,{className:`space-y-2 mb-2`,children:[(0,q.jsx)(pe,{agent:e}),(0,q.jsx)(he,{agent:e,compact:!0}),(0,q.jsx)(fe,{agent:e})]}),(0,q.jsx)(`div`,{className:`text-xs text-muted-foreground`,children:e.id!==`user`&&e.id!==`system`&&ee(l,e.lastSeen)}),(0,q.jsxs)(`div`,{className:`flex gap-1 mt-2.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity`,onClick:e=>e.stopPropagation(),children:[(0,q.jsx)(ce,{value:e.id,label:`Copy agent ID`,size:`icon`,className:`h-7 w-7`,iconClassName:`w-3 h-3`}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Chat`,onClick:()=>{f(e.id),u(`chat`)},children:(0,q.jsx)(o,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Pair`,onClick:()=>m(e.id),children:(0,q.jsx)(_,{className:`w-3 h-3`})}),(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Rename`,onClick:()=>w(e),children:(0,q.jsx)(t,{className:`w-3 h-3`})}),L&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Terminal`,disabled:N,onClick:()=>void P(),children:(0,q.jsx)(d,{className:`w-3 h-3`})}),(e.branchState===`changes`||e.branchState===`idle`)&&e.branchWorkspaceId&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-amber-400 hover:text-amber-300`,title:`Mark workspace ready — hand off to the auto-merge`,onClick:()=>T(`Mark Workspace Ready`,`Mark ${C(e)}'s branch ready to land? The relay auto-merge will rebase and land it.`,()=>k(e.branchWorkspaceId,`ready`)),children:(0,q.jsx)(n,{className:`w-3 h-3`})}),oe&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Compact`,onClick:()=>T(`Compact Agent`,`Compact context for ${C(e)}?`,()=>E(e,`compact`,`agents-grid`)),children:(0,q.jsx)(i,{className:`w-3 h-3`})}),se&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Clear Context`,onClick:()=>T(`Clear Context`,`Clear context for ${C(e)}?`,()=>E(e,`clearContext`,`agents-grid`)),children:(0,q.jsx)(r,{className:`w-3 h-3`})}),I&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Shutdown`,onClick:()=>T(`Shutdown Agent`,`Shutdown ${C(e)}?`,()=>E(e,`shutdown`,`agents-grid`)),children:(0,q.jsx)(s,{className:`w-3 h-3`})}),V&&(0,q.jsx)(B,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Forget stale agent`,onClick:()=>T(`Forget Agent`,`Forget ${C(e)}?`,()=>D(e.id)),children:(0,q.jsx)(g,{className:`w-3 h-3`})})]})]})}),M&&(0,q.jsx)(ge,{open:j,onOpenChange:ie,orchestratorId:M.orchestratorId,session:M.session,interactive:M.interactive||R})]})}function Qe(){let e=y(e=>e.agentPresetFilter),t=y(e=>e.agentStatusFilter),n=y(e=>e.agentTagFilter),r=y(e=>e.agentHostFilter),i=y(e=>e.agentSort),o=y(e=>e.agentSortDir),s=y(e=>e.showOffline),l=y(e=>e.set),d=y(e=>e.openAgentSpawn),p=y(e=>e.agentsById),m=v(),h=W(),g=U(),_=V(),b=me(h,m),x=de(h,m),ee=e||t||n||r;function te(){l({agentPresetFilter:``,agentStatusFilter:``,agentTagFilter:``,agentHostFilter:``})}function S(){l({agentSortDir:o===`asc`?`desc`:`asc`})}return(0,q.jsxs)(`div`,{className:`space-y-4`,children:[(0,q.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,q.jsxs)(`select`,{value:e,onChange:e=>l({agentPresetFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`All agents`}),(0,q.jsx)(`option`,{value:`active`,children:`Active`}),(0,q.jsx)(`option`,{value:`claude`,children:`Claude`}),(0,q.jsx)(`option`,{value:`codex`,children:`Codex`}),(0,q.jsx)(`option`,{value:`paired`,children:`Paired`}),(0,q.jsx)(`option`,{value:`unpaired`,children:`Unpaired`}),(0,q.jsx)(`option`,{value:`offline_stale`,children:`Offline / Stale`})]}),(0,q.jsxs)(`select`,{value:t,onChange:e=>l({agentStatusFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any status`}),(0,q.jsx)(`option`,{value:`online`,children:`Online`}),(0,q.jsx)(`option`,{value:`idle`,children:`Idle`}),(0,q.jsx)(`option`,{value:`busy`,children:`Busy`}),(0,q.jsx)(`option`,{value:`offline`,children:`Offline`}),(0,q.jsx)(`option`,{value:`starting`,children:`Starting`})]}),g.length>0&&(0,q.jsxs)(`select`,{value:n,onChange:e=>l({agentTagFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any tag`}),g.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),_.length>1&&(0,q.jsxs)(`select`,{value:r,onChange:e=>l({agentHostFilter:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:``,children:`Any host`}),_.map(e=>(0,q.jsx)(`option`,{value:e,children:e},e))]}),(0,q.jsxs)(`select`,{value:i,onChange:e=>l({agentSort:e.target.value}),className:`h-8 rounded-md border border-border bg-card text-sm px-2 text-foreground`,children:[(0,q.jsx)(`option`,{value:`status`,children:`Sort: Status`}),(0,q.jsx)(`option`,{value:`name`,children:`Sort: Name`}),(0,q.jsx)(`option`,{value:`lastSeen`,children:`Sort: Last seen`}),(0,q.jsx)(`option`,{value:`created`,children:`Sort: Created`})]}),(0,q.jsx)(B,{size:`icon`,variant:`outline`,className:`h-8 w-8`,title:`Toggle sort direction`,onClick:S,children:o===`asc`?(0,q.jsx)(u,{className:`w-3.5 h-3.5`}):(0,q.jsx)(c,{className:`w-3.5 h-3.5`})}),(0,q.jsxs)(`label`,{className:`flex items-center gap-1.5 text-sm text-muted-foreground cursor-pointer select-none`,children:[(0,q.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>l({showOffline:e.target.checked}),className:`rounded`}),`Show offline`]}),ee&&(0,q.jsxs)(B,{size:`sm`,variant:`ghost`,className:`h-8 text-muted-foreground hover:text-foreground gap-1`,onClick:te,children:[(0,q.jsx)(f,{className:`w-3 h-3`}),`Clear filters`]}),(0,q.jsx)(`div`,{className:`ml-auto`,children:(0,q.jsxs)(B,{size:`sm`,onClick:d,className:`gap-1`,children:[(0,q.jsx)(a,{className:`w-3.5 h-3.5`}),`Spawn`]})})]}),x.length===0?(0,q.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No agents match the current filters.`}):(0,q.jsx)(`div`,{className:`grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3`,children:x.map(e=>(0,q.jsx)(Ze,{agent:e,parentAgent:e.spawnedBy?p[e.spawnedBy]??null:null,childAgents:_e(b.get(e.id)??[])},e.id))})]})}export{Qe as AgentsView};
|
|
2
|
-
//# sourceMappingURL=agents-DV5y_tF1.js.map
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./chunk-CilyBKbf.js";import{$t as t,Jt as n,Mn as r,Un as i,a,bt as o,jn as s,kn as c,p as l}from"./lucide-react-ChOGD4vc.js";import{t as u}from"./store-BlsqvkSE.js";var d=e(i(),1);function f(e){let t=u(e=>e.showError),[n,i]=(0,d.useState)(!1),[a,o]=(0,d.useState)(null),[s,c]=(0,d.useState)(!1);async function l(){if(!(!e||s)){c(!0);try{o(await r(`POST`,`/agents/`+encodeURIComponent(e)+`/terminal-session`)),i(!0)}catch(e){t(`Terminal Failed`,e.message)}finally{c(!1)}}}function f(t){if(t){i(!0);return}i(!1);let n=a;o(null),e&&n?.mode===`guest`&&r(`DELETE`,`/agents/`+encodeURIComponent(e)+`/terminal-session/`+encodeURIComponent(n.session)).catch(()=>{})}return{terminalOpen:n,terminalTarget:a,terminalOpening:s,openTerminal:l,closeTerminal:f}}var p=s(),m={idle:{label:`idle`,tip:`Branch worktree — nothing to land yet.`,className:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,Icon:n},changes:{label:`changes`,tip:`Unlanded work in this branch — use "Mark workspace ready" to land it.`,className:`bg-amber-500/10 text-amber-400 border-amber-500/20`,Icon:o},ready:{label:`ready`,tip:`Handed off — the relay auto-merge will land it (~2 min sweep).`,className:`bg-blue-500/10 text-blue-400 border-blue-500/20`,Icon:t},steward:{label:`steward`,tip:`Under reconciliation — a steward is landing it. No action needed.`,className:`bg-orange-500/10 text-orange-400 border-orange-500/20`,Icon:a},blocked:{label:`blocked`,tip:`Escalated — the auto-merge/steward path is stuck. Needs human attention.`,className:`bg-red-500/10 text-red-400 border-red-500/20`,Icon:l}};function h({state:e,onClick:t,iconOnly:n=!1,className:r}){let i=m[e],{Icon:a}=i,o=!!t,s=o?`${i.tip} Click to open this workspace.`:i.tip;return(0,p.jsxs)(`span`,{role:o?`button`:void 0,tabIndex:o?0:void 0,title:s,onClick:t,onKeyDown:o?e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),t?.(e))}:void 0,className:c(`inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium leading-none`,i.className,o&&`cursor-pointer hover:brightness-125 focus:outline-none focus:ring-1 focus:ring-current`,r),children:[(0,p.jsx)(a,{className:`h-2.5 w-2.5 shrink-0`}),!n&&(0,p.jsx)(`span`,{children:i.label})]})}export{f as n,h as t};
|
|
2
|
-
//# sourceMappingURL=branch-state-badge-CzlsNakd.js.map
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import"./chunk-CilyBKbf.js";import{Un as e,jn as t,kn as n}from"./lucide-react-ChOGD4vc.js";e();var r=t();function i({className:e,size:t=`default`,...i}){return(0,r.jsx)(`div`,{"data-slot":`card`,"data-size":t,className:n(`group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl`,e),...i})}function a({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-header`,className:n(`group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3`,e),...t})}function o({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-title`,className:n(`font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm`,e),...t})}function s({className:e,...t}){return(0,r.jsx)(`div`,{"data-slot":`card-content`,className:n(`px-4 group-data-[size=sm]/card:px-3`,e),...t})}export{o as i,s as n,a as r,i as t};
|
|
2
|
-
//# sourceMappingURL=card-C7JtSyuM.js.map
|