tide-commander 1.110.2 → 1.110.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{BossLogsModal-DBeMak5-.js → BossLogsModal-BBO4P4qG.js} +1 -1
- package/dist/assets/{BossSpawnModal-DDl6qgnq.js → BossSpawnModal-DJ1DmEWp.js} +1 -1
- package/dist/assets/{ControlsModal-BBUukX-p.js → ControlsModal-DeRColMt.js} +1 -1
- package/dist/assets/{DockerLogsModal-BWf3XDEI.js → DockerLogsModal-DGpatLiw.js} +1 -1
- package/dist/assets/{EmbeddedEditor-CIOdhnaW.js → EmbeddedEditor-Dsgf6hMF.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-qG-_WeOb.js → GmailOAuthSetup-CGis0yjA.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BK3lzU4n.js → GoogleOAuthSetup-Bnhf_nts.js} +1 -1
- package/dist/assets/{IframeModal-D_NEs_Ld.js → IframeModal-BahnrxAm.js} +1 -1
- package/dist/assets/{IntegrationsPanel-yphWX1ZD.js → IntegrationsPanel-BUs_7XBA.js} +2 -2
- package/dist/assets/{LogViewerModal-B0h9NNRk.js → LogViewerModal-DXxjS8NR.js} +1 -1
- package/dist/assets/{MonitoringModal-Ciw7KnXH.js → MonitoringModal-DPeyASIV.js} +1 -1
- package/dist/assets/{PM2LogsModal-C1dYQaff.js → PM2LogsModal-kNNjOmdB.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-DOwzfu-D.js → RestoreArchivedAreaModal-CVHj28LX.js} +1 -1
- package/dist/assets/{Scene2DCanvas-MHfv4Rzg.js → Scene2DCanvas-BShs3yx7.js} +1 -1
- package/dist/assets/{SceneManager-cm1Kj0YD.js → SceneManager-CF89Ktyf.js} +1 -1
- package/dist/assets/{SkillsPanel-DLwMkORc.js → SkillsPanel-BVRP74vV.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-Dql_wPMX.js → SlackMultiInstanceSetup-f6boDwm1.js} +1 -1
- package/dist/assets/{SpawnModal-DzPTSOKV.js → SpawnModal-Bm8rg45q.js} +1 -1
- package/dist/assets/StatisticsModal-DuNj8UzS.js +1 -0
- package/dist/assets/{SubordinateAssignmentModal-CoLzD3z2.js → SubordinateAssignmentModal-Cn3e2lNE.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-BOi2zr7N.js → TriggerManagerPanel-Deo2dE4T.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-DB1ygPDS.js → WorkflowEditorPanel-D-_duRlu.js} +1 -1
- package/dist/assets/{index-DKASWGtq.js → index-Bb-7Dvp1.js} +1 -1
- package/dist/assets/{index-C6EHHgC0.js → index-BsykE0PB.js} +1 -1
- package/dist/assets/{index-D4XW6MSF.js → index-C9A7qvBF.js} +2 -2
- package/dist/assets/{index-DKdZI-Ar.js → index-CZGINGY-.js} +1 -1
- package/dist/assets/{index-6Wak7CPm.js → index-DZu9t3BI.js} +1 -1
- package/dist/assets/{index-BZsQOteM.js → index-DeeuvLUM.js} +1 -1
- package/dist/assets/index-v1ImDk7h.js +11 -0
- package/dist/assets/{index-BXJSMT4a.js → index-y3mA_lBK.js} +3 -3
- package/dist/assets/{index-ThbMNdRp.js → index-zphXsHhS.js} +1 -1
- package/dist/assets/{main-BxA3Bhkv.js → main-CyZg1kKu.js} +94 -94
- package/dist/assets/{main-BLrGoeQ_.css → main-VciFPjA1.css} +1 -1
- package/dist/assets/{web-zdygXcYu.js → web-BKgFBlgl.js} +1 -1
- package/dist/assets/{web-BskC98UQ.js → web-BZcyP_IR.js} +1 -1
- package/dist/assets/{web-CSRmX8Ge.js → web-VEWFSkZI.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +54 -1
- package/dist/src/packages/server/routes/agents.js +43 -0
- package/dist/src/packages/server/services/collapse-context.js +72 -0
- package/dist/src/packages/server/services/runtime-service.js +46 -0
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +27 -12
- package/package.json +1 -1
- package/dist/assets/index-BP6ukiYG.js +0 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{cm as s}from"./main-CyZg1kKu.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{cm as a}from"./main-CyZg1kKu.js";import{ImpactStyle as i,NotificationType as r}from"./index-C9A7qvBF.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{cm as t}from"./main-CyZg1kKu.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-CyZg1kKu.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-VciFPjA1.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -47,5 +47,58 @@ EOF
|
|
|
47
47
|
- Always prefix messages with your identity so the receiver knows who sent it
|
|
48
48
|
- Use heredoc syntax (\`-d @- <<'EOF'\`) to avoid JSON escaping problems
|
|
49
49
|
- Keep messages simple - avoid special characters like backslashes or nested quotes
|
|
50
|
-
- Works with all agent types including boss agents
|
|
50
|
+
- Works with all agent types including boss agents
|
|
51
|
+
|
|
52
|
+
## Collapsing an Agent's Context (\`/compact\`)
|
|
53
|
+
|
|
54
|
+
\`POST /api/agents/AGENT_ID/message\` cannot deliver Claude Code slash commands —
|
|
55
|
+
the slash is passed through as message body instead of being intercepted by the
|
|
56
|
+
CLI. For that, use the dedicated endpoint:
|
|
57
|
+
|
|
58
|
+
\`\`\`bash
|
|
59
|
+
curl -s -X POST http://localhost:5174/api/agents/AGENT_ID/collapse-context
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
This sends Claude Code's \`/compact\` slash command to the agent's runner, which
|
|
63
|
+
compresses its conversation history. Works for ANY agent including yourself
|
|
64
|
+
(auto-collapse from a cron or end-of-flow step).
|
|
65
|
+
|
|
66
|
+
Response shape:
|
|
67
|
+
|
|
68
|
+
\`\`\`json
|
|
69
|
+
{ "success": true, "agentId": "AGENT_ID", "status": "collapse-initiated" }
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
Status codes:
|
|
73
|
+
- \`200\` + \`status: "collapse-initiated"\` — \`/compact\` dispatched
|
|
74
|
+
- \`200\` + \`status: "queued"\` — agent was busy; \`/compact\` is queued and will fire automatically when the agent next goes idle (only when you pass \`waitForIdle: true\` in the body)
|
|
75
|
+
- \`404\` + \`status: "not-found"\` — agent id doesn't exist
|
|
76
|
+
- \`409\` + \`status: "busy"\` + \`currentStatus\` — agent isn't idle and \`waitForIdle\` was not set; retry once it goes idle
|
|
77
|
+
- \`500\` + \`status: "error"\` — runtime error sending the command
|
|
78
|
+
|
|
79
|
+
### Auto-collapse from inside your OWN turn (\`waitForIdle\`)
|
|
80
|
+
|
|
81
|
+
When the agent calling this endpoint is the SAME agent being collapsed — e.g. a
|
|
82
|
+
cron-driven end-of-flow step that wraps up by collapsing its own context — the
|
|
83
|
+
target is by definition still \`working\` at the time of the request, so the
|
|
84
|
+
default behavior returns \`409 busy\`. Opt into queueing by passing
|
|
85
|
+
\`waitForIdle: true\`:
|
|
86
|
+
|
|
87
|
+
\`\`\`bash
|
|
88
|
+
curl -s -X POST http://localhost:5174/api/agents/YOUR_AGENT_ID/collapse-context \\
|
|
89
|
+
-H "Content-Type: application/json" \\
|
|
90
|
+
-d '{"waitForIdle": true}'
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
Response is \`200 + {"status": "queued"}\` — the \`/compact\` is held in an
|
|
94
|
+
in-process queue and dispatched the first time the agent's status transitions
|
|
95
|
+
to \`idle\`. Coalesced: stacking multiple \`waitForIdle\` calls for the same
|
|
96
|
+
agent results in a single drained \`/compact\`, not N.
|
|
97
|
+
|
|
98
|
+
Use this for the final step of any agent's autonomous flow when you want the
|
|
99
|
+
next turn to start with a compressed context.
|
|
100
|
+
|
|
101
|
+
**When to use which endpoint:**
|
|
102
|
+
- \`/message\` — deliver content / instructions to an agent (what you usually want)
|
|
103
|
+
- \`/collapse-context\` — trigger the \`/compact\` slash command (the only path that works for slash commands; \`/message\` will not)`,
|
|
51
104
|
};
|
|
@@ -1065,6 +1065,49 @@ router.post('/:id/message', async (req, res) => {
|
|
|
1065
1065
|
res.status(500).json({ error: err.message });
|
|
1066
1066
|
}
|
|
1067
1067
|
});
|
|
1068
|
+
// POST /api/agents/:id/collapse-context — Send Claude Code's /compact slash command
|
|
1069
|
+
// to the agent's runner. Use case: any agent (including the same agent via a
|
|
1070
|
+
// scheduled cron) auto-collapsing its own context. Plain /message can't carry
|
|
1071
|
+
// slash commands; this is the only correct path.
|
|
1072
|
+
//
|
|
1073
|
+
// Body (optional):
|
|
1074
|
+
// { "waitForIdle": true } — if the agent is currently busy, queue the
|
|
1075
|
+
// /compact and fire it automatically the first time the agent goes idle.
|
|
1076
|
+
// The main use case is the SAME agent calling this from inside its own
|
|
1077
|
+
// turn (e.g. end-of-flow cron step) — by definition still `working` when
|
|
1078
|
+
// the request lands. Default false keeps the existing 409-busy behavior.
|
|
1079
|
+
router.post('/:id/collapse-context', async (req, res) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const agentId = req.params.id;
|
|
1082
|
+
const body = (req.body ?? {});
|
|
1083
|
+
const waitForIdle = body.waitForIdle === true;
|
|
1084
|
+
const result = await runtimeService.collapseAgentContext(agentId, { waitForIdle });
|
|
1085
|
+
switch (result.status) {
|
|
1086
|
+
case 'collapse-initiated':
|
|
1087
|
+
log.log(`API collapse-context dispatched for agent ${agentId}`);
|
|
1088
|
+
res.status(200).json({ success: true, agentId, status: 'collapse-initiated' });
|
|
1089
|
+
return;
|
|
1090
|
+
case 'queued':
|
|
1091
|
+
log.log(`API collapse-context queued for agent ${agentId} (waiting for idle)`);
|
|
1092
|
+
res.status(200).json({ success: true, agentId, status: 'queued' });
|
|
1093
|
+
return;
|
|
1094
|
+
case 'not-found':
|
|
1095
|
+
res.status(404).json({ success: false, agentId, status: 'not-found', error: `Agent not found: ${agentId}` });
|
|
1096
|
+
return;
|
|
1097
|
+
case 'busy':
|
|
1098
|
+
res.status(409).json({ success: false, agentId, status: 'busy', currentStatus: result.currentStatus, error: 'Cannot collapse context while agent is busy' });
|
|
1099
|
+
return;
|
|
1100
|
+
case 'error':
|
|
1101
|
+
log.error(` Failed to collapse context for ${agentId}: ${result.error}`);
|
|
1102
|
+
res.status(500).json({ success: false, agentId, status: 'error', error: result.error });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
catch (err) {
|
|
1107
|
+
log.error(' Failed to collapse context (route):', err);
|
|
1108
|
+
res.status(500).json({ error: err?.message || 'Failed to collapse context' });
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1068
1111
|
// POST /api/agents/:id/report-task - Subordinate reports task completion to its boss
|
|
1069
1112
|
router.post('/:id/report-task', async (req, res) => {
|
|
1070
1113
|
try {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse-Context Service
|
|
3
|
+
*
|
|
4
|
+
* Owns the small queue of pending `/compact` requests that need to fire as
|
|
5
|
+
* soon as their agent transitions back to idle. Two callers use it: the REST
|
|
6
|
+
* endpoint `POST /api/agents/:id/collapse-context` and the WS
|
|
7
|
+
* `collapse_context` handler.
|
|
8
|
+
*
|
|
9
|
+
* Why a separate module: the queue + idle-drain logic is the only piece of
|
|
10
|
+
* state we want to unit-test in isolation, and `runtime-service` is heavy with
|
|
11
|
+
* unrelated deps. We expose a factory so tests can wire fake `getAgent` /
|
|
12
|
+
* `sendCommand` / `subscribe` deps and exercise every branch without spinning
|
|
13
|
+
* up the real agent service.
|
|
14
|
+
*/
|
|
15
|
+
export function createCollapseContextService(deps) {
|
|
16
|
+
const pending = new Set();
|
|
17
|
+
let unsubscribe = null;
|
|
18
|
+
// Lazy subscription — we only attach the listener once a real queue exists.
|
|
19
|
+
// Stays attached for the process lifetime; the pending set is the gate that
|
|
20
|
+
// decides whether the listener does anything on each event.
|
|
21
|
+
function ensureSubscribed() {
|
|
22
|
+
if (unsubscribe)
|
|
23
|
+
return;
|
|
24
|
+
unsubscribe = deps.subscribe((event, data) => {
|
|
25
|
+
if (event !== 'updated' || !data || typeof data === 'string')
|
|
26
|
+
return;
|
|
27
|
+
const agent = data;
|
|
28
|
+
if (agent.status !== 'idle')
|
|
29
|
+
return;
|
|
30
|
+
if (!pending.has(agent.id))
|
|
31
|
+
return;
|
|
32
|
+
pending.delete(agent.id);
|
|
33
|
+
void drain(agent.id);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function drain(agentId) {
|
|
37
|
+
try {
|
|
38
|
+
await deps.sendCommand(agentId, '/compact');
|
|
39
|
+
deps.log?.info?.(`Drained queued collapse for ${agentId} after idle transition`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
43
|
+
deps.log?.error?.(`Failed to drain queued collapse for ${agentId}: ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function collapse(agentId, opts = {}) {
|
|
47
|
+
const agent = deps.getAgent(agentId);
|
|
48
|
+
if (!agent)
|
|
49
|
+
return { status: 'not-found' };
|
|
50
|
+
if (agent.status === 'idle') {
|
|
51
|
+
try {
|
|
52
|
+
await deps.sendCommand(agentId, '/compact');
|
|
53
|
+
return { status: 'collapse-initiated' };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return { status: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (opts.waitForIdle) {
|
|
60
|
+
ensureSubscribed();
|
|
61
|
+
// Set.add is idempotent — multiple `waitForIdle` calls coalesce into one drain.
|
|
62
|
+
pending.add(agentId);
|
|
63
|
+
return { status: 'queued' };
|
|
64
|
+
}
|
|
65
|
+
return { status: 'busy', currentStatus: agent.status };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
collapse,
|
|
69
|
+
hasPending: (id) => pending.has(id),
|
|
70
|
+
pendingCount: () => pending.size,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -187,6 +187,52 @@ export async function sendCommand(agentId, command, systemPrompt, forceNewSessio
|
|
|
187
187
|
export async function sendSilentCommand(agentId, command) {
|
|
188
188
|
await commandExecution.sendSilentCommand(agentId, command);
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Send Claude Code's `/compact` slash command to collapse an agent's context.
|
|
192
|
+
* Only fires when the agent exists AND is idle — the CLI rejects slash commands
|
|
193
|
+
* mid-turn. Pass `waitForIdle: true` to queue the `/compact` for when the agent
|
|
194
|
+
* next goes idle (the main use case is an agent auto-collapsing its OWN context
|
|
195
|
+
* at the end of a turn — by definition still `working` when it makes the call).
|
|
196
|
+
*
|
|
197
|
+
* Callers: WS `collapse_context` handler, REST `POST /api/agents/:id/collapse-context`.
|
|
198
|
+
* Plain `/message` cannot carry slash commands (they're sent as message body, not
|
|
199
|
+
* intercepted by the CLI) — this helper is the only correct path.
|
|
200
|
+
*
|
|
201
|
+
* Backed by a small in-module queue + a one-shot agent-event subscriber; see
|
|
202
|
+
* `./collapse-context.ts` for the unit-testable factory.
|
|
203
|
+
*/
|
|
204
|
+
import { createCollapseContextService } from './collapse-context.js';
|
|
205
|
+
let collapseService = null;
|
|
206
|
+
function getCollapseService() {
|
|
207
|
+
if (collapseService)
|
|
208
|
+
return collapseService;
|
|
209
|
+
collapseService = createCollapseContextService({
|
|
210
|
+
getAgent: (id) => {
|
|
211
|
+
const a = agentService.getAgent(id);
|
|
212
|
+
return a ? { id: a.id, status: a.status } : undefined;
|
|
213
|
+
},
|
|
214
|
+
sendCommand: (id, cmd) => commandExecution.sendCommand(id, cmd),
|
|
215
|
+
subscribe: (listener) => agentService.subscribe((event, data) => {
|
|
216
|
+
// agentService emits agent objects on 'updated' — narrow to the
|
|
217
|
+
// { id, status } slice the collapse-context module cares about.
|
|
218
|
+
if (event === 'updated' && data && typeof data !== 'string') {
|
|
219
|
+
listener(event, { id: data.id, status: data.status });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
listener(event, typeof data === 'string' ? data : undefined);
|
|
223
|
+
}
|
|
224
|
+
}),
|
|
225
|
+
log: {
|
|
226
|
+
info: (m) => log.log(m),
|
|
227
|
+
warn: (m) => log.warn(m),
|
|
228
|
+
error: (m) => log.error(m),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
return collapseService;
|
|
232
|
+
}
|
|
233
|
+
export async function collapseAgentContext(agentId, opts) {
|
|
234
|
+
return getCollapseService().collapse(agentId, opts);
|
|
235
|
+
}
|
|
190
236
|
export async function stopAgent(agentId) {
|
|
191
237
|
await commandExecution.stopAgent(agentId);
|
|
192
238
|
}
|
|
@@ -227,22 +227,37 @@ export function handleRequestSessionHistory(ctx, payload) {
|
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
/**
|
|
230
|
-
* Handle collapse_context message - sends /compact command to collapse context
|
|
230
|
+
* Handle collapse_context message - sends /compact command to collapse context.
|
|
231
|
+
* Thin wrapper around runtimeService.collapseAgentContext that maps each
|
|
232
|
+
* discriminated result back to the WS activity stream.
|
|
231
233
|
*/
|
|
232
234
|
export async function handleCollapseContext(ctx, payload) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// WS path never opts into waitForIdle — the UI button only makes sense when
|
|
236
|
+
// the agent is already idle. Default behavior (409-equivalent activity msg
|
|
237
|
+
// for busy) is preserved.
|
|
238
|
+
const result = await runtimeService.collapseAgentContext(payload.agentId);
|
|
239
|
+
switch (result.status) {
|
|
240
|
+
case 'collapse-initiated':
|
|
237
241
|
ctx.sendActivity(payload.agentId, 'Context collapse initiated');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
242
|
+
return;
|
|
243
|
+
case 'queued':
|
|
244
|
+
// Not reachable from this path (we don't pass waitForIdle), but the
|
|
245
|
+
// exhaustive switch keeps the type system honest if a future caller
|
|
246
|
+
// flips the flag here.
|
|
247
|
+
ctx.sendActivity(payload.agentId, 'Context collapse queued — will fire when agent is idle');
|
|
248
|
+
return;
|
|
249
|
+
case 'busy':
|
|
250
|
+
ctx.sendActivity(payload.agentId, 'Cannot collapse context while agent is busy');
|
|
251
|
+
return;
|
|
252
|
+
case 'not-found':
|
|
253
|
+
// No agent → no activity stream to write to; quietly drop (the UI shouldn't
|
|
254
|
+
// be able to reach this state, and logging keeps the breadcrumb).
|
|
255
|
+
log.warn(`collapse_context for unknown agent ${payload.agentId}`);
|
|
256
|
+
return;
|
|
257
|
+
case 'error':
|
|
258
|
+
log.error(` Failed to collapse context: ${result.error}`);
|
|
241
259
|
ctx.sendActivity(payload.agentId, 'Failed to collapse context');
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
ctx.sendActivity(payload.agentId, 'Cannot collapse context while agent is busy');
|
|
260
|
+
return;
|
|
246
261
|
}
|
|
247
262
|
}
|
|
248
263
|
/**
|