@hyperdrive.bot/paseo-server 0.3.41 → 0.3.43
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/server/server/agent/agent-manager.d.ts +15 -0
- package/dist/server/server/agent/agent-manager.js +142 -25
- package/dist/server/server/agent/mcp-server.js +6 -1
- package/dist/server/server/agent/providers/claude/background-task-tracker.d.ts +38 -1
- package/dist/server/server/agent/providers/claude/background-task-tracker.js +114 -9
- package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +95 -0
- package/dist/server/server/agent/providers/claude/background-work-kinds.js +73 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.js +3 -0
- package/dist/server/server/agent/providers/claude/transport/pty.d.ts +17 -0
- package/dist/server/server/agent/providers/claude/transport/pty.js +51 -1
- package/dist/server/server/agent/providers/claude/transport/tmux.d.ts +74 -0
- package/dist/server/server/agent/providers/claude/transport/tmux.js +157 -0
- package/dist/server/server/agent/providers/claude/transport/types.d.ts +6 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +7 -0
- package/dist/server/server/agent/providers/opencode-agent.js +51 -1
- package/dist/server/server/bootstrap.js +1 -0
- package/dist/server/server/session.js +1 -0
- package/dist/server/server/workflow/workflow-agent-resolution.d.ts +82 -0
- package/dist/server/server/workflow/workflow-agent-resolution.js +105 -0
- package/dist/server/server/workflow/workflow-manager.d.ts +155 -20
- package/dist/server/server/workflow/workflow-manager.js +439 -31
- package/dist/server/server/workflow/workflow-progress.d.ts +53 -0
- package/dist/server/server/workflow/workflow-progress.js +96 -0
- package/dist/server/server/workspace-directory.js +32 -14
- package/dist/server/web-ui/_expo/static/js/web/{index-cb251ddad56c08c3021036af3a43fc0f.js → index-2ff48a7009ad2309c577d5a43b925f69.js} +18 -18
- package/dist/server/web-ui/_expo/static/js/web/index-2ff48a7009ad2309c577d5a43b925f69.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-2ff48a7009ad2309c577d5a43b925f69.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-cb251ddad56c08c3021036af3a43fc0f.js.map.br → index-2ff48a7009ad2309c577d5a43b925f69.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-cb251ddad56c08c3021036af3a43fc0f.js.map.gz → index-2ff48a7009ad2309c577d5a43b925f69.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.gz +0 -0
|
@@ -142,6 +142,14 @@ interface ManagedAgentBase {
|
|
|
142
142
|
lastUsage?: AgentUsage;
|
|
143
143
|
lastError?: string;
|
|
144
144
|
attention: AttentionState;
|
|
145
|
+
/**
|
|
146
|
+
* How many times a "finished" attention event was suppressed on this agent
|
|
147
|
+
* because it still owned armed background work (see checkAndSetAttention).
|
|
148
|
+
* Diagnostic only — never persisted, never projected to clients. It exists so
|
|
149
|
+
* the deliberately-accepted gap (a prose question swallowed mid-monitor) can
|
|
150
|
+
* be MEASURED from real sessions instead of argued about.
|
|
151
|
+
*/
|
|
152
|
+
suppressedAttentionCount?: number;
|
|
145
153
|
foregroundTurnWaiters: Set<ForegroundTurnWaiter>;
|
|
146
154
|
finalizedForegroundTurnIds: Set<string>;
|
|
147
155
|
unsubscribeSession: (() => void) | null;
|
|
@@ -472,6 +480,13 @@ export declare class AgentManager {
|
|
|
472
480
|
private recordTimeline;
|
|
473
481
|
private emitState;
|
|
474
482
|
private syncFeaturesFromSession;
|
|
483
|
+
/**
|
|
484
|
+
* Live background work owned by this agent: monitors, crons, and backgrounded
|
|
485
|
+
* shells the provider is still tracking. Mirrors what `agent-projections`
|
|
486
|
+
* publishes as `activeBackgroundTaskCount`, read through the same optional
|
|
487
|
+
* hook so providers that do not implement it simply report zero.
|
|
488
|
+
*/
|
|
489
|
+
private getActiveBackgroundTaskCount;
|
|
475
490
|
private checkAndSetAttention;
|
|
476
491
|
private enqueueBackgroundPersist;
|
|
477
492
|
private enqueueDurableTimelineAppend;
|
|
@@ -228,7 +228,10 @@ export class AgentManager {
|
|
|
228
228
|
this.mcpAuthToken = options?.mcpAuthToken ?? null;
|
|
229
229
|
this.configurePaseoTools(options);
|
|
230
230
|
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
|
231
|
-
this.logger = options.logger.child({
|
|
231
|
+
this.logger = options.logger.child({
|
|
232
|
+
module: "agent",
|
|
233
|
+
component: "agent-manager",
|
|
234
|
+
});
|
|
232
235
|
this.rescueTimeouts = {
|
|
233
236
|
reloadSessionCloseMs: options.rescueTimeouts?.reloadSessionCloseMs ?? RELOAD_SESSION_CLOSE_TIMEOUT_MS,
|
|
234
237
|
interruptSessionMs: options.rescueTimeouts?.interruptSessionMs ?? INTERRUPT_SESSION_TIMEOUT_MS,
|
|
@@ -467,7 +470,9 @@ export class AgentManager {
|
|
|
467
470
|
}
|
|
468
471
|
}
|
|
469
472
|
async listDraftCommands(config) {
|
|
470
|
-
const normalizedConfig = await this.normalizeConfig(config, {
|
|
473
|
+
const normalizedConfig = await this.normalizeConfig(config, {
|
|
474
|
+
resolveDefaultModel: false,
|
|
475
|
+
});
|
|
471
476
|
const client = this.requireClient(normalizedConfig.provider);
|
|
472
477
|
if (!normalizedConfig.model) {
|
|
473
478
|
return [];
|
|
@@ -496,7 +501,9 @@ export class AgentManager {
|
|
|
496
501
|
}
|
|
497
502
|
}
|
|
498
503
|
async listDraftFeatures(config) {
|
|
499
|
-
const normalizedConfig = await this.normalizeConfig(config, {
|
|
504
|
+
const normalizedConfig = await this.normalizeConfig(config, {
|
|
505
|
+
resolveDefaultModel: false,
|
|
506
|
+
});
|
|
500
507
|
const client = this.requireClient(normalizedConfig.provider);
|
|
501
508
|
if (!normalizedConfig.model) {
|
|
502
509
|
return [];
|
|
@@ -600,7 +607,9 @@ export class AgentManager {
|
|
|
600
607
|
async importProviderSession(input) {
|
|
601
608
|
const resolvedAgentId = validateAgentId(this.idFactory(), "importProviderSession");
|
|
602
609
|
this.requireEnabledProvider(input.provider);
|
|
603
|
-
const client = await this.requireAvailableClient({
|
|
610
|
+
const client = await this.requireAvailableClient({
|
|
611
|
+
provider: input.provider,
|
|
612
|
+
});
|
|
604
613
|
if (!client.importSession) {
|
|
605
614
|
throw new Error(`Provider '${input.provider}' does not support importing sessions`);
|
|
606
615
|
}
|
|
@@ -908,7 +917,10 @@ export class AgentManager {
|
|
|
908
917
|
async markRecordArchived(record) {
|
|
909
918
|
const registry = this.requireRegistry();
|
|
910
919
|
const archivedAt = new Date().toISOString();
|
|
911
|
-
const archivedRecord = buildArchivedAgentRecord(record, {
|
|
920
|
+
const archivedRecord = buildArchivedAgentRecord(record, {
|
|
921
|
+
archivedAt,
|
|
922
|
+
updatedAt: archivedAt,
|
|
923
|
+
});
|
|
912
924
|
await registry.upsert(archivedRecord);
|
|
913
925
|
await this.archiveNativeSessionBestEffort(record.provider, record.persistence);
|
|
914
926
|
if (this.agents.has(record.id)) {
|
|
@@ -1024,7 +1036,10 @@ export class AgentManager {
|
|
|
1024
1036
|
throw new Error("Agent session does not support setting features");
|
|
1025
1037
|
}
|
|
1026
1038
|
await agent.session.setFeature(featureId, value);
|
|
1027
|
-
agent.config.featureValues = {
|
|
1039
|
+
agent.config.featureValues = {
|
|
1040
|
+
...agent.config.featureValues,
|
|
1041
|
+
[featureId]: value,
|
|
1042
|
+
};
|
|
1028
1043
|
this.touchUpdatedAt(agent);
|
|
1029
1044
|
this.emitState(agent);
|
|
1030
1045
|
}
|
|
@@ -1072,7 +1087,9 @@ export class AgentManager {
|
|
|
1072
1087
|
const priorColumn = touchesColumn && this.registry
|
|
1073
1088
|
? ((await this.registry.get(agentId))?.labels?.[KANBAN_LABEL_KEY] ?? null)
|
|
1074
1089
|
: null;
|
|
1075
|
-
const nextRecord = await this.writeStoredMetadata(agentId, {
|
|
1090
|
+
const nextRecord = await this.writeStoredMetadata(agentId, {
|
|
1091
|
+
labels: patch,
|
|
1092
|
+
});
|
|
1076
1093
|
if (touchesColumn) {
|
|
1077
1094
|
this.cardMoveLog?.record(agentId, priorColumn, nextRecord.labels?.[KANBAN_LABEL_KEY] ?? null);
|
|
1078
1095
|
}
|
|
@@ -1107,7 +1124,9 @@ export class AgentManager {
|
|
|
1107
1124
|
}
|
|
1108
1125
|
return { record, live: true, previousParentAgentId: null };
|
|
1109
1126
|
}
|
|
1110
|
-
const { record } = await this.writeLabels(agentId, {
|
|
1127
|
+
const { record } = await this.writeLabels(agentId, {
|
|
1128
|
+
[PARENT_AGENT_ID_LABEL]: null,
|
|
1129
|
+
});
|
|
1111
1130
|
if (!record) {
|
|
1112
1131
|
throw new Error(`Agent not found in storage after detach: ${agentId}`);
|
|
1113
1132
|
}
|
|
@@ -1121,7 +1140,9 @@ export class AgentManager {
|
|
|
1121
1140
|
if (!previousParentAgentId) {
|
|
1122
1141
|
return { record, live: false, previousParentAgentId: null };
|
|
1123
1142
|
}
|
|
1124
|
-
const result = await this.writeLabels(agentId, {
|
|
1143
|
+
const result = await this.writeLabels(agentId, {
|
|
1144
|
+
[PARENT_AGENT_ID_LABEL]: null,
|
|
1145
|
+
});
|
|
1125
1146
|
if (!result.record) {
|
|
1126
1147
|
throw new Error(`Agent not found in storage after detach: ${agentId}`);
|
|
1127
1148
|
}
|
|
@@ -1292,7 +1313,9 @@ export class AgentManager {
|
|
|
1292
1313
|
});
|
|
1293
1314
|
return;
|
|
1294
1315
|
}
|
|
1295
|
-
this.dispatchStream(agent.id, event, {
|
|
1316
|
+
this.dispatchStream(agent.id, event, {
|
|
1317
|
+
timestamp: new Date().toISOString(),
|
|
1318
|
+
});
|
|
1296
1319
|
};
|
|
1297
1320
|
void (async () => {
|
|
1298
1321
|
try {
|
|
@@ -1432,7 +1455,10 @@ export class AgentManager {
|
|
|
1432
1455
|
mutableAgent.lifecycle = nextLifecycle;
|
|
1433
1456
|
const persistenceHandle = mutableAgent.session.describePersistence() ??
|
|
1434
1457
|
(mutableAgent.runtimeInfo?.sessionId
|
|
1435
|
-
? {
|
|
1458
|
+
? {
|
|
1459
|
+
provider: mutableAgent.provider,
|
|
1460
|
+
sessionId: mutableAgent.runtimeInfo.sessionId,
|
|
1461
|
+
}
|
|
1436
1462
|
: null);
|
|
1437
1463
|
if (persistenceHandle) {
|
|
1438
1464
|
mutableAgent.persistence = attachPersistenceCwd(persistenceHandle, mutableAgent.cwd);
|
|
@@ -1589,7 +1615,9 @@ export class AgentManager {
|
|
|
1589
1615
|
const bufferedResolution = agent.bufferedPermissionResolutions.get(requestId);
|
|
1590
1616
|
if (bufferedResolution) {
|
|
1591
1617
|
agent.bufferedPermissionResolutions.delete(requestId);
|
|
1592
|
-
this.dispatchStream(agent.id, bufferedResolution, {
|
|
1618
|
+
this.dispatchStream(agent.id, bufferedResolution, {
|
|
1619
|
+
timestamp: new Date().toISOString(),
|
|
1620
|
+
});
|
|
1593
1621
|
}
|
|
1594
1622
|
return result;
|
|
1595
1623
|
}
|
|
@@ -1626,7 +1654,11 @@ export class AgentManager {
|
|
|
1626
1654
|
continue;
|
|
1627
1655
|
}
|
|
1628
1656
|
for (const task of agent.session.listBackgroundTasks()) {
|
|
1629
|
-
rows.push({
|
|
1657
|
+
rows.push({
|
|
1658
|
+
agentId: agent.id,
|
|
1659
|
+
agentTitle: agent.config.title ?? null,
|
|
1660
|
+
task,
|
|
1661
|
+
});
|
|
1630
1662
|
}
|
|
1631
1663
|
}
|
|
1632
1664
|
return rows;
|
|
@@ -1767,7 +1799,10 @@ export class AgentManager {
|
|
|
1767
1799
|
this.logger.info({ agentId, provider: agent.provider, messageId, mode }, "agent.rewind.start");
|
|
1768
1800
|
await invokeRewindCapability(agent.session, { messageId, mode });
|
|
1769
1801
|
if (mode !== "files") {
|
|
1770
|
-
await this.hydrateTimelineFromProvider(agentId, {
|
|
1802
|
+
await this.hydrateTimelineFromProvider(agentId, {
|
|
1803
|
+
force: true,
|
|
1804
|
+
broadcast: true,
|
|
1805
|
+
});
|
|
1771
1806
|
}
|
|
1772
1807
|
await this.refreshRuntimeInfo(agent);
|
|
1773
1808
|
await this.persistSnapshot(agent);
|
|
@@ -2013,14 +2048,18 @@ export class AgentManager {
|
|
|
2013
2048
|
this.agents.set(resolvedAgentId, managed);
|
|
2014
2049
|
// Initialize previousStatus to track transitions
|
|
2015
2050
|
this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
|
|
2016
|
-
await this.refreshRuntimeInfo(managed, {
|
|
2051
|
+
await this.refreshRuntimeInfo(managed, {
|
|
2052
|
+
emit: !options?.publishWhenReady,
|
|
2053
|
+
});
|
|
2017
2054
|
await this.persistSnapshot(managed, {
|
|
2018
2055
|
title: initialPersistedTitle,
|
|
2019
2056
|
});
|
|
2020
2057
|
if (!options?.publishWhenReady) {
|
|
2021
2058
|
this.emitState(managed, { persist: false });
|
|
2022
2059
|
}
|
|
2023
|
-
await this.refreshSessionState(managed, {
|
|
2060
|
+
await this.refreshSessionState(managed, {
|
|
2061
|
+
emit: !options?.publishWhenReady,
|
|
2062
|
+
});
|
|
2024
2063
|
managed.lifecycle = "idle";
|
|
2025
2064
|
await this.persistSnapshot(managed);
|
|
2026
2065
|
this.emitState(managed, { persist: false });
|
|
@@ -2280,7 +2319,9 @@ export class AgentManager {
|
|
|
2280
2319
|
this.agentStreamCoalescer.flushAndDiscard(agent.id);
|
|
2281
2320
|
await this.deleteCommittedTimeline(agent.id);
|
|
2282
2321
|
this.timelineStore.delete(agent.id);
|
|
2283
|
-
this.timelineStore.initialize(agent.id, {
|
|
2322
|
+
this.timelineStore.initialize(agent.id, {
|
|
2323
|
+
timestamp: new Date().toISOString(),
|
|
2324
|
+
});
|
|
2284
2325
|
agent.historyPrimed = true;
|
|
2285
2326
|
for (const event of historyEvents) {
|
|
2286
2327
|
const row = this.recordTimeline(agent.id, event.item, event.timestamp ? { timestamp: event.timestamp } : undefined);
|
|
@@ -2348,7 +2389,10 @@ export class AgentManager {
|
|
|
2348
2389
|
}
|
|
2349
2390
|
this.agentStreamCoalescer.flushFor(agent.id);
|
|
2350
2391
|
}
|
|
2351
|
-
const flags = {
|
|
2392
|
+
const flags = {
|
|
2393
|
+
shouldDispatchEvent: true,
|
|
2394
|
+
shouldNotifyWaiters: true,
|
|
2395
|
+
};
|
|
2352
2396
|
const dispatchPromise = this.dispatchStreamEventByType({
|
|
2353
2397
|
agent,
|
|
2354
2398
|
event,
|
|
@@ -2364,7 +2408,9 @@ export class AgentManager {
|
|
|
2364
2408
|
this.finalizeForegroundTurn(agent, eventTurnId);
|
|
2365
2409
|
}
|
|
2366
2410
|
if (!options?.fromHistory && flags.shouldDispatchEvent) {
|
|
2367
|
-
this.dispatchStream(agent.id, event, {
|
|
2411
|
+
this.dispatchStream(agent.id, event, {
|
|
2412
|
+
timestamp: new Date().toISOString(),
|
|
2413
|
+
});
|
|
2368
2414
|
}
|
|
2369
2415
|
this.traceHandleStreamEventEnd(agent, event, eventTurnId, flags);
|
|
2370
2416
|
return flags.shouldNotifyWaiters;
|
|
@@ -2417,7 +2463,10 @@ export class AgentManager {
|
|
|
2417
2463
|
agent.currentModeId = event.currentModeId;
|
|
2418
2464
|
agent.availableModes = event.availableModes;
|
|
2419
2465
|
if (agent.runtimeInfo) {
|
|
2420
|
-
agent.runtimeInfo = {
|
|
2466
|
+
agent.runtimeInfo = {
|
|
2467
|
+
...agent.runtimeInfo,
|
|
2468
|
+
modeId: event.currentModeId,
|
|
2469
|
+
};
|
|
2421
2470
|
}
|
|
2422
2471
|
flags.shouldDispatchEvent = false;
|
|
2423
2472
|
this.emitState(agent);
|
|
@@ -2425,7 +2474,10 @@ export class AgentManager {
|
|
|
2425
2474
|
case "model_changed":
|
|
2426
2475
|
agent.runtimeInfo = event.runtimeInfo;
|
|
2427
2476
|
if (!agent.persistence && event.runtimeInfo.sessionId) {
|
|
2428
|
-
agent.persistence = attachPersistenceCwd({
|
|
2477
|
+
agent.persistence = attachPersistenceCwd({
|
|
2478
|
+
provider: agent.provider,
|
|
2479
|
+
sessionId: event.runtimeInfo.sessionId,
|
|
2480
|
+
}, agent.cwd);
|
|
2429
2481
|
}
|
|
2430
2482
|
agent.currentModeId = event.runtimeInfo.modeId ?? agent.currentModeId;
|
|
2431
2483
|
flags.shouldDispatchEvent = false;
|
|
@@ -2442,9 +2494,20 @@ export class AgentManager {
|
|
|
2442
2494
|
this.emitState(agent);
|
|
2443
2495
|
return undefined;
|
|
2444
2496
|
case "timeline":
|
|
2445
|
-
return this.onStreamTimelineEvent({
|
|
2497
|
+
return this.onStreamTimelineEvent({
|
|
2498
|
+
agent,
|
|
2499
|
+
event,
|
|
2500
|
+
options,
|
|
2501
|
+
isForegroundEvent,
|
|
2502
|
+
flags,
|
|
2503
|
+
});
|
|
2446
2504
|
case "turn_completed":
|
|
2447
|
-
this.onStreamTurnCompleted({
|
|
2505
|
+
this.onStreamTurnCompleted({
|
|
2506
|
+
agent,
|
|
2507
|
+
event,
|
|
2508
|
+
eventTurnId,
|
|
2509
|
+
isForegroundEvent,
|
|
2510
|
+
});
|
|
2448
2511
|
return undefined;
|
|
2449
2512
|
case "turn_failed":
|
|
2450
2513
|
return this.onStreamTurnFailed({
|
|
@@ -2455,7 +2518,13 @@ export class AgentManager {
|
|
|
2455
2518
|
options,
|
|
2456
2519
|
});
|
|
2457
2520
|
case "turn_canceled":
|
|
2458
|
-
this.onStreamTurnCanceled({
|
|
2521
|
+
this.onStreamTurnCanceled({
|
|
2522
|
+
agent,
|
|
2523
|
+
event,
|
|
2524
|
+
eventTurnId,
|
|
2525
|
+
isForegroundEvent,
|
|
2526
|
+
options,
|
|
2527
|
+
});
|
|
2459
2528
|
return undefined;
|
|
2460
2529
|
case "turn_started":
|
|
2461
2530
|
this.onStreamTurnStarted({ agent, eventTurnId, isForegroundEvent });
|
|
@@ -2704,6 +2773,18 @@ export class AgentManager {
|
|
|
2704
2773
|
agent.features = agent.session.features;
|
|
2705
2774
|
}
|
|
2706
2775
|
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Live background work owned by this agent: monitors, crons, and backgrounded
|
|
2778
|
+
* shells the provider is still tracking. Mirrors what `agent-projections`
|
|
2779
|
+
* publishes as `activeBackgroundTaskCount`, read through the same optional
|
|
2780
|
+
* hook so providers that do not implement it simply report zero.
|
|
2781
|
+
*/
|
|
2782
|
+
getActiveBackgroundTaskCount(agent) {
|
|
2783
|
+
if (!("session" in agent)) {
|
|
2784
|
+
return 0;
|
|
2785
|
+
}
|
|
2786
|
+
return agent.session?.getActiveBackgroundTaskCount?.() ?? 0;
|
|
2787
|
+
}
|
|
2707
2788
|
checkAndSetAttention(agent) {
|
|
2708
2789
|
const previousStatus = this.previousStatuses.get(agent.id);
|
|
2709
2790
|
const currentStatus = agent.lifecycle;
|
|
@@ -2719,6 +2800,40 @@ export class AgentManager {
|
|
|
2719
2800
|
}
|
|
2720
2801
|
// Check if agent transitioned from running to idle (finished)
|
|
2721
2802
|
if (previousStatus === "running" && currentStatus === "idle") {
|
|
2803
|
+
// ...but "the lifecycle went running -> idle" is NOT the same fact as
|
|
2804
|
+
// "this agent is finished and wants you".
|
|
2805
|
+
//
|
|
2806
|
+
// A monitor tick, a cron firing, or a background shell reporting progress
|
|
2807
|
+
// all WAKE the session: status goes idle -> running, the agent prints a
|
|
2808
|
+
// progress line ("1/12 done, I'll let you know"), the turn ends, and the
|
|
2809
|
+
// lifecycle settles back to idle. Under the old rule that transition set
|
|
2810
|
+
// requiresAttention="finished" on EVERY tick, which lit the Unread chip,
|
|
2811
|
+
// the dock badge and a push notification for output nobody needs to act
|
|
2812
|
+
// on. With several sessions in parallel that made Unread — the one chip
|
|
2813
|
+
// you actually watch — useless.
|
|
2814
|
+
//
|
|
2815
|
+
// So: if the agent still owns armed background work, the turn ending is a
|
|
2816
|
+
// progress beat, not a handoff. Stay quiet and let the bucket derivation
|
|
2817
|
+
// surface it as "pending" (alive, will come back on its own) instead.
|
|
2818
|
+
//
|
|
2819
|
+
// KNOWN GAP, deliberately named rather than silently accepted: a turn that
|
|
2820
|
+
// ends with the agent asking a real question in PROSE (not a permission
|
|
2821
|
+
// request, which still routes through needs_input and is unaffected) is
|
|
2822
|
+
// also suppressed while background work is armed. Prose questions are not
|
|
2823
|
+
// machine-detectable here. The counter below exists so that gap is
|
|
2824
|
+
// MEASURED rather than assumed — feed it to the Jarvis judge in shadow
|
|
2825
|
+
// mode (PASEO_JARVIS_SHADOW=1) to size the real distribution before
|
|
2826
|
+
// deciding whether it needs rescuing.
|
|
2827
|
+
const activeBackgroundTaskCount = this.getActiveBackgroundTaskCount(agent);
|
|
2828
|
+
if (activeBackgroundTaskCount > 0) {
|
|
2829
|
+
agent.suppressedAttentionCount = (agent.suppressedAttentionCount ?? 0) + 1;
|
|
2830
|
+
this.logger.debug({
|
|
2831
|
+
agentId: agent.id,
|
|
2832
|
+
activeBackgroundTaskCount,
|
|
2833
|
+
suppressedAttentionCount: agent.suppressedAttentionCount,
|
|
2834
|
+
}, "Suppressed finished-attention: agent still owns armed background work");
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2722
2837
|
agent.attention = {
|
|
2723
2838
|
requiresAttention: true,
|
|
2724
2839
|
attentionReason: "finished",
|
|
@@ -2938,7 +3053,9 @@ export class AgentManager {
|
|
|
2938
3053
|
if (this.paseoToolsEnabled &&
|
|
2939
3054
|
client.capabilities.supportsNativePaseoTools &&
|
|
2940
3055
|
this.paseoToolCatalogFactory) {
|
|
2941
|
-
context.paseoTools = await this.paseoToolCatalogFactory({
|
|
3056
|
+
context.paseoTools = await this.paseoToolCatalogFactory({
|
|
3057
|
+
callerAgentId: agentId,
|
|
3058
|
+
});
|
|
2942
3059
|
}
|
|
2943
3060
|
return context;
|
|
2944
3061
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { WorkflowNotFoundError } from "../workflow/workflow-manager.js";
|
|
4
|
-
import { WorkflowSnapshotSchema, WorkflowStatusSchema, WorkflowTaskGraphSchema, SessionDigestSchema, } from "../messages.js";
|
|
4
|
+
import { WorkflowAgentPresetSchema, WorkflowSnapshotSchema, WorkflowStatusSchema, WorkflowTaskGraphSchema, SessionDigestSchema, } from "../messages.js";
|
|
5
5
|
import { AgentStatusEnum } from "./mcp-shared.js";
|
|
6
6
|
import { expandUserPath } from "../path-utils.js";
|
|
7
7
|
import { ensureValidJson } from "../json-utils.js";
|
|
@@ -28,6 +28,10 @@ const workflowStartArgsSchema = z.object({
|
|
|
28
28
|
.record(z.string(), z.string())
|
|
29
29
|
.optional()
|
|
30
30
|
.describe("Optional labels copied onto every spawned child alongside the workflow id"),
|
|
31
|
+
agentPresets: z
|
|
32
|
+
.record(z.string(), WorkflowAgentPresetSchema)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Optional map of agent name to partial session config; a task's agentType selects one"),
|
|
31
35
|
});
|
|
32
36
|
const workflowIdArgsSchema = z.object({
|
|
33
37
|
workflowId: z.string().describe("Workflow id returned by workflow_start"),
|
|
@@ -238,6 +242,7 @@ export async function createAgentMcpServer(options) {
|
|
|
238
242
|
baseConfig,
|
|
239
243
|
...(args.title ? { title: args.title } : {}),
|
|
240
244
|
...(args.labels ? { labels: args.labels } : {}),
|
|
245
|
+
...(args.agentPresets ? { agentPresets: args.agentPresets } : {}),
|
|
241
246
|
...(callerAgentId ? { parentAgentId: callerAgentId } : {}),
|
|
242
247
|
});
|
|
243
248
|
const started = await workflowManager.startWorkflow(workflow.id);
|
|
@@ -27,9 +27,25 @@
|
|
|
27
27
|
* output file's real birth time, reading the output back) is the caller's job.
|
|
28
28
|
*/
|
|
29
29
|
export type ClaudeBackgroundTaskStatus = "running" | "completed" | "failed" | "canceled";
|
|
30
|
+
/**
|
|
31
|
+
* What kind of background work a record represents. Surfaced so the UI can say
|
|
32
|
+
* WHAT is holding a session open ("pending · monitoring deploy.log") instead of
|
|
33
|
+
* an unexplained spinner, which is how users end up force-quitting sessions.
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ Adding a member here is not enough to make it tracked — it must also get a
|
|
36
|
+
* start pattern above AND a retirement path, and be listed in the exhaustiveness
|
|
37
|
+
* test. See `background-work-kinds.ts`.
|
|
38
|
+
*/
|
|
39
|
+
export type ClaudeBackgroundTaskKind = "shell" | "monitor" | "cron";
|
|
40
|
+
export interface BackgroundTaskStart {
|
|
41
|
+
id: string;
|
|
42
|
+
kind: ClaudeBackgroundTaskKind;
|
|
43
|
+
}
|
|
30
44
|
export interface ClaudeBackgroundTaskRecord {
|
|
31
45
|
/** Harness-assigned shell id, e.g. `bjuk0pif4`. Stable across start and end. */
|
|
32
46
|
id: string;
|
|
47
|
+
/** Which flavour of background work this is. Defaults to "shell" (legacy). */
|
|
48
|
+
kind: ClaudeBackgroundTaskKind;
|
|
33
49
|
/** The shell command, from the originating `tool_use` input. Null if unseen. */
|
|
34
50
|
command: string | null;
|
|
35
51
|
/** The tool call's human description ("Run the test suite"), if it carried one. */
|
|
@@ -68,8 +84,29 @@ export declare function isTerminalTaskStatus(status: string | null | undefined):
|
|
|
68
84
|
* terminal that isn't recognisably a failure or a cancel is a plain completion.
|
|
69
85
|
*/
|
|
70
86
|
export declare function toTerminalRecordStatus(status: string | null | undefined): Exclude<ClaudeBackgroundTaskStatus, "running">;
|
|
71
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Extract every background-task id announced in a Bash tool_result's text.
|
|
89
|
+
*
|
|
90
|
+
* Bash shells only — kept at this name and signature because it is part of the
|
|
91
|
+
* module's public surface. For all tracked kinds use
|
|
92
|
+
* {@link extractBackgroundTaskStarts}.
|
|
93
|
+
*/
|
|
72
94
|
export declare function extractBackgroundTaskIds(text: string | null | undefined): string[];
|
|
95
|
+
/** Ids of monitors started in this text. */
|
|
96
|
+
export declare function extractMonitorTaskIds(text: string | null | undefined): string[];
|
|
97
|
+
/** Ids of cron jobs scheduled in this text. */
|
|
98
|
+
export declare function extractCronTaskIds(text: string | null | undefined): string[];
|
|
99
|
+
/** Ids of cron jobs cancelled in this text (a cron's only retirement signal). */
|
|
100
|
+
export declare function extractCancelledCronTaskIds(text: string | null | undefined): string[];
|
|
101
|
+
/**
|
|
102
|
+
* Every background-work start announced in one tool_result, tagged by kind.
|
|
103
|
+
*
|
|
104
|
+
* This is the single place that decides "does this text start something that
|
|
105
|
+
* keeps the agent alive?". A new kind of background work is invisible until it
|
|
106
|
+
* is added HERE — which is precisely how monitors and crons went untracked
|
|
107
|
+
* while `deriveAgentStateBucket` happily reported those sessions as done.
|
|
108
|
+
*/
|
|
109
|
+
export declare function extractBackgroundTaskStarts(text: string | null | undefined): BackgroundTaskStart[];
|
|
73
110
|
/** Extract the output-file path the harness announced alongside the id, if any. */
|
|
74
111
|
export declare function extractBackgroundOutputFile(text: string | null | undefined): string | null;
|
|
75
112
|
export declare class ClaudeBackgroundTaskTracker {
|
|
@@ -47,6 +47,45 @@ const BACKGROUND_ID_PATTERN = /Command running in background with ID:\s*([A-Za-z
|
|
|
47
47
|
* path from the id (the directory is session-scoped and not derivable here).
|
|
48
48
|
*/
|
|
49
49
|
const OUTPUT_FILE_PATTERN = /Output is being written to:\s*(\S+)/;
|
|
50
|
+
/**
|
|
51
|
+
* Matches the harness line for a started **Monitor** (the long-running watcher
|
|
52
|
+
* tool), which announces itself with a completely different sentence from a
|
|
53
|
+
* backgrounded Bash:
|
|
54
|
+
*
|
|
55
|
+
* Monitor started (task b6dxcqe9y, timeout 20000ms). You will be notified on
|
|
56
|
+
* each event. Keep working — do not poll or sleep.
|
|
57
|
+
*
|
|
58
|
+
* The id is followed by a comma rather than a period, so it needs its own
|
|
59
|
+
* capture. Crucially the monitor's END is already handled: it retires through
|
|
60
|
+
* the SAME `<task-notification>` envelope carrying the same `<task-id>` and a
|
|
61
|
+
* `<status>completed</status>`. Only the start was invisible, which is why a
|
|
62
|
+
* session holding a live monitor reported zero background tasks and settled to
|
|
63
|
+
* "done" while the monitor was still watching.
|
|
64
|
+
*
|
|
65
|
+
* Captured verbatim from a live session on 2026-08-22, not transcribed.
|
|
66
|
+
*/
|
|
67
|
+
const MONITOR_ID_PATTERN = /Monitor started \(task\s+([A-Za-z0-9._-]+)/g;
|
|
68
|
+
/**
|
|
69
|
+
* Matches a scheduled **cron** job's announcement. Two shapes, one per mode:
|
|
70
|
+
*
|
|
71
|
+
* Scheduled one-shot task dcdae8f2 (17 4 1 1 *). Session-only ...
|
|
72
|
+
* Scheduled recurring job b8df03d3 (Every Wednesday at 4:23 AM). Session-only ...
|
|
73
|
+
*
|
|
74
|
+
* Note the noun changes with the mode ("task" vs "job"), so both are accepted.
|
|
75
|
+
* Captured verbatim from a live session on 2026-08-22.
|
|
76
|
+
*/
|
|
77
|
+
const CRON_ID_PATTERN = /Scheduled\s+(?:one-shot|recurring)\s+(?:task|job)\s+([A-Za-z0-9._-]+)/g;
|
|
78
|
+
/**
|
|
79
|
+
* Matches a cancelled cron job. A cron has no `<task-notification>`, so this
|
|
80
|
+
* tool_result line is its ONLY retirement signal:
|
|
81
|
+
*
|
|
82
|
+
* Cancelled job b8df03d3.
|
|
83
|
+
*
|
|
84
|
+
* The trailing period is glued to the id — the exact defect class documented in
|
|
85
|
+
* {@link stripTrailingSentencePunctuation}. It is stripped, or the id would
|
|
86
|
+
* never match the record and the agent would sit in "pending" forever.
|
|
87
|
+
*/
|
|
88
|
+
const CRON_CANCEL_PATTERN = /Cancelled job\s+([A-Za-z0-9._-]+)/g;
|
|
50
89
|
/**
|
|
51
90
|
* Trim the sentence punctuation the harness's prose leaves glued to a capture.
|
|
52
91
|
*
|
|
@@ -109,24 +148,66 @@ export function toTerminalRecordStatus(status) {
|
|
|
109
148
|
}
|
|
110
149
|
return "completed";
|
|
111
150
|
}
|
|
112
|
-
/**
|
|
113
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Run one module-scoped global pattern over `text` and return every stripped
|
|
153
|
+
* capture. Shared by all start/cancel scanners so the two easy-to-forget
|
|
154
|
+
* details — resetting `lastIndex` on a module-scoped global regex, and
|
|
155
|
+
* stripping the sentence punctuation the harness's prose glues to a capture —
|
|
156
|
+
* are written once instead of per pattern.
|
|
157
|
+
*/
|
|
158
|
+
function scanIds(pattern, text) {
|
|
114
159
|
if (typeof text !== "string" || text.length === 0) {
|
|
115
160
|
return [];
|
|
116
161
|
}
|
|
117
162
|
const ids = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
let match = BACKGROUND_ID_PATTERN.exec(text);
|
|
163
|
+
pattern.lastIndex = 0;
|
|
164
|
+
let match = pattern.exec(text);
|
|
121
165
|
while (match !== null) {
|
|
122
166
|
const id = match[1] ? stripTrailingSentencePunctuation(match[1]) : "";
|
|
123
167
|
if (id) {
|
|
124
168
|
ids.push(id);
|
|
125
169
|
}
|
|
126
|
-
match =
|
|
170
|
+
match = pattern.exec(text);
|
|
127
171
|
}
|
|
128
172
|
return ids;
|
|
129
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Extract every background-task id announced in a Bash tool_result's text.
|
|
176
|
+
*
|
|
177
|
+
* Bash shells only — kept at this name and signature because it is part of the
|
|
178
|
+
* module's public surface. For all tracked kinds use
|
|
179
|
+
* {@link extractBackgroundTaskStarts}.
|
|
180
|
+
*/
|
|
181
|
+
export function extractBackgroundTaskIds(text) {
|
|
182
|
+
return scanIds(BACKGROUND_ID_PATTERN, text);
|
|
183
|
+
}
|
|
184
|
+
/** Ids of monitors started in this text. */
|
|
185
|
+
export function extractMonitorTaskIds(text) {
|
|
186
|
+
return scanIds(MONITOR_ID_PATTERN, text);
|
|
187
|
+
}
|
|
188
|
+
/** Ids of cron jobs scheduled in this text. */
|
|
189
|
+
export function extractCronTaskIds(text) {
|
|
190
|
+
return scanIds(CRON_ID_PATTERN, text);
|
|
191
|
+
}
|
|
192
|
+
/** Ids of cron jobs cancelled in this text (a cron's only retirement signal). */
|
|
193
|
+
export function extractCancelledCronTaskIds(text) {
|
|
194
|
+
return scanIds(CRON_CANCEL_PATTERN, text);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Every background-work start announced in one tool_result, tagged by kind.
|
|
198
|
+
*
|
|
199
|
+
* This is the single place that decides "does this text start something that
|
|
200
|
+
* keeps the agent alive?". A new kind of background work is invisible until it
|
|
201
|
+
* is added HERE — which is precisely how monitors and crons went untracked
|
|
202
|
+
* while `deriveAgentStateBucket` happily reported those sessions as done.
|
|
203
|
+
*/
|
|
204
|
+
export function extractBackgroundTaskStarts(text) {
|
|
205
|
+
return [
|
|
206
|
+
...extractBackgroundTaskIds(text).map((id) => ({ id, kind: "shell" })),
|
|
207
|
+
...extractMonitorTaskIds(text).map((id) => ({ id, kind: "monitor" })),
|
|
208
|
+
...extractCronTaskIds(text).map((id) => ({ id, kind: "cron" })),
|
|
209
|
+
];
|
|
210
|
+
}
|
|
130
211
|
/** Extract the output-file path the harness announced alongside the id, if any. */
|
|
131
212
|
export function extractBackgroundOutputFile(text) {
|
|
132
213
|
if (typeof text !== "string" || text.length === 0) {
|
|
@@ -158,15 +239,19 @@ export class ClaudeBackgroundTaskTracker {
|
|
|
158
239
|
const outputFile = extractBackgroundOutputFile(normalized.text);
|
|
159
240
|
const startedAt = normalized.startedAt ?? new Date().toISOString();
|
|
160
241
|
const added = [];
|
|
161
|
-
for (const id of
|
|
242
|
+
for (const { id, kind } of extractBackgroundTaskStarts(normalized.text)) {
|
|
162
243
|
if (this.running.has(id)) {
|
|
163
244
|
continue;
|
|
164
245
|
}
|
|
165
246
|
this.running.set(id, {
|
|
166
247
|
id,
|
|
248
|
+
kind,
|
|
167
249
|
command: normalized.command ?? null,
|
|
168
250
|
description: normalized.description ?? null,
|
|
169
|
-
|
|
251
|
+
// Only a backgrounded shell announces an output file on START. A
|
|
252
|
+
// monitor announces its own on its terminal notification instead, and a
|
|
253
|
+
// cron never has one.
|
|
254
|
+
outputFile: kind === "shell" ? outputFile : null,
|
|
170
255
|
startedAt,
|
|
171
256
|
endedAt: null,
|
|
172
257
|
status: "running",
|
|
@@ -174,6 +259,22 @@ export class ClaudeBackgroundTaskTracker {
|
|
|
174
259
|
});
|
|
175
260
|
added.push(id);
|
|
176
261
|
}
|
|
262
|
+
// A cron has no `<task-notification>`, so its cancellation arrives as
|
|
263
|
+
// another tool_result in this same stream. Retire it here or it stays live
|
|
264
|
+
// forever and pins the agent to "pending" — the "never retires" failure
|
|
265
|
+
// this module already documents for a mis-captured shell id.
|
|
266
|
+
for (const id of extractCancelledCronTaskIds(normalized.text)) {
|
|
267
|
+
const record = this.running.get(id);
|
|
268
|
+
if (!record || record.kind !== "cron") {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
this.running.delete(id);
|
|
272
|
+
this.retire({
|
|
273
|
+
...record,
|
|
274
|
+
status: "canceled",
|
|
275
|
+
endedAt: new Date().toISOString(),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
177
278
|
return added;
|
|
178
279
|
}
|
|
179
280
|
/**
|
|
@@ -211,7 +312,11 @@ export class ClaudeBackgroundTaskTracker {
|
|
|
211
312
|
return false;
|
|
212
313
|
}
|
|
213
314
|
this.running.delete(taskId);
|
|
214
|
-
this.retire({
|
|
315
|
+
this.retire({
|
|
316
|
+
...record,
|
|
317
|
+
status: "canceled",
|
|
318
|
+
endedAt: new Date().toISOString(),
|
|
319
|
+
});
|
|
215
320
|
return true;
|
|
216
321
|
}
|
|
217
322
|
has(taskId) {
|