@lelouchhe/webagent 0.6.0 → 0.8.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/dist/index.html +15 -6
- package/dist/js/app.XBFXH37R.js +2 -0
- package/dist/js/chunk.CT5WBNGZ.js +173 -0
- package/dist/js/chunk.UMQMOGWO.js +1 -0
- package/dist/js/{login.PYIK52HN.js → login.2WA6DTGM.js} +1 -1
- package/dist/js/viewer.CVWXSKJM.js +1 -0
- package/dist/login.html +2 -2
- package/dist/share-viewer.html +4 -4
- package/dist/{styles.01ky3cex.css → styles.00etlpgs.css} +94 -4
- package/dist/sw.js +19 -9
- package/lib/agent-commands.js +27 -0
- package/lib/bridge.js +60 -11
- package/lib/event-handler.js +75 -15
- package/lib/http-status.js +17 -0
- package/lib/message-cleanup.js +5 -4
- package/lib/push-service.js +2 -1
- package/lib/routes.js +468 -196
- package/lib/server.js +4 -3
- package/lib/session-manager.js +213 -23
- package/lib/session-state.js +70 -5
- package/lib/share/routes.js +106 -72
- package/lib/share/sanitize.js +2 -1
- package/lib/sse-manager.js +31 -7
- package/lib/store.js +25 -23
- package/package.json +1 -1
- package/dist/js/app.HJKQURHR.js +0 -4
- package/dist/js/chunk.D4ZYHJAM.js +0 -1
- package/dist/js/chunk.ON2W5X2T.js +0 -173
- package/dist/js/viewer.VY4PO2O5.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var s=Object.freeze({OK:200,CREATED:201,ACCEPTED:202,NO_CONTENT:204,BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,CONFLICT:409,GONE:410,PAYLOAD_TOO_LARGE:413,UNPROCESSABLE_CONTENT:422,INTERNAL_SERVER_ERROR:500,NOT_IMPLEMENTED:501,SERVICE_UNAVAILABLE:503});var c="wa_token";async function p(n,a={}){let i=a.fetch??fetch,o=n.trim();if(!o)return{ok:!1,error:"Token is required"};let r;try{r=await i("/api/v1/auth/verify",{method:"GET",headers:{Authorization:`Bearer ${o}`}})}catch(e){return{ok:!1,error:`Network error: ${e instanceof Error?e.message:String(e)}`}}if(r.status===s.OK){let e;try{e=await r.json()}catch{return{ok:!1,error:"Server returned invalid response"}}if(!e.ok||typeof e.name!="string"||typeof e.scope!="string")return{ok:!1,error:"Server returned malformed response"};try{localStorage.setItem(c,o)}catch(t){return{ok:!1,error:`Could not store token: ${t instanceof Error?t.message:String(t)}`}}return{ok:!0,name:e.name,scope:e.scope}}return r.status===s.UNAUTHORIZED?{ok:!1,error:"Invalid or rejected token"}:{ok:!1,error:`Unexpected response (status ${r.status})`}}export{s as a,c as b,p as c};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{b as r,c as i}from"./chunk.UMQMOGWO.js";import"./chunk.AJZBJBMO.js";localStorage.getItem(r)&&location.replace("/");var s=document.getElementById("login-form"),n=document.getElementById("token-input"),o=document.getElementById("error"),t=document.getElementById("submit-btn");function u(e){o&&(o.textContent=e,o.style.display=e?"block":"none")}s&&n&&t&&(s.addEventListener("submit",e=>{e.preventDefault(),u(""),t.disabled=!0,t.textContent="Verifying\u2026",(async()=>{try{let l=await i(n.value);l.ok?location.replace("/"):u(l.error)}finally{t.disabled=!1,t.textContent="Sign in"}})()}),n.focus());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{b as g,d as h,m as y,o as T,p as C}from"./chunk.CT5WBNGZ.js";import{a as p}from"./chunk.UMQMOGWO.js";import"./chunk.AJZBJBMO.js";var $=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function f(t){return String(t).padStart(2,"0")}function E(t,n){if(!t)return"";let s=new Date(t),i=s.getTime();if(isNaN(i))return"";let l=Math.floor((n.getTime()-i)/1e3);if(l<60)return"just now";let c=Math.floor(l/60);if(c<60)return`${c}m ago`;let o=Math.floor(c/60);if(o<24)return`${o}h ago`;let e=Math.floor(o/24);if(e<7)return`${e}d ago`;let m=s.getUTCFullYear()===n.getUTCFullYear(),r=$[s.getUTCMonth()],a=s.getUTCDate();return m?`${r} ${a}`:`${r} ${a}, ${s.getUTCFullYear()}`}function _(t){if(!t)return"";let n=new Date(t);return isNaN(n.getTime())?"":`${n.getUTCFullYear()}-${f(n.getUTCMonth()+1)}-${f(n.getUTCDate())} ${f(n.getUTCHours())}:${f(n.getUTCMinutes())} UTC`}function S(t){let n=/^\/api\/v1\/sessions\/[^/]+\/attachments\/([A-Za-z0-9._-]+)(?:\?.*)?$/;return s=>{let i=n.exec(s);return i?`/s/${encodeURIComponent(t)}/attachments/${i[1]}`:s}}function w(t){if(typeof t.data=="string")try{return JSON.parse(t.data)}catch{return{}}return t.data}function x(t,n,s,i){if(t.type!=="assistant_message"||n?.type!=="assistant_message"||t.seq!==n.seq+1||!s?.classList.contains("assistant"))return!1;let l=(s.getAttribute("data-raw")??"")+(typeof i.text=="string"?i.text:"");return s.setAttribute("data-raw",l),C(s,l,void 0,!0),g(s),!0}function v(t,n,s){let i=new Set;for(let r of t)if(r.type==="permission_response")try{let a=JSON.parse(r.data);typeof a.requestId=="string"&&i.add(a.requestId)}catch{}let l=new Map,c=new Map,o=null,e=null,m={rewriteAttachmentSrc:S(s),enhanceMarkdown:g,findToolCallEl:r=>l.get(r)??null,findPermissionEl:r=>c.get(r)??null,findBashEl:()=>o,isPermissionResolved:r=>i.has(r),finalAnswerToolText:null};for(let r=0;r<t.length;r++){let a=t[r];if(!y(a.type))continue;let u=w(a),A=r>0?t[r-1]:null,M=n.lastElementChild;if(x(a,A,M,u)){e=null;continue}m.finalAnswerToolText=a.type==="assistant_message"?e:null;let d=T(a.type,u,m);d?(n.appendChild(d),a.type==="tool_call"&&typeof u.id=="string"?l.set(u.id,d):a.type==="permission_request"&&typeof u.requestId=="string"?c.set(u.requestId,d):a.type==="bash_command"&&(o=d)):a.type==="bash_result"&&(o=null),a.type==="tool_call_update"?e=h(typeof u.status=="string"?u.status:"",Array.isArray(u.content)?u.content:void 0):e=null}}async function U(){try{let e=localStorage.getItem("theme")??"auto";document.documentElement.setAttribute("data-theme",e)}catch{}let t=/^\/s\/([A-Za-z0-9_-]{24})(?:[/?#]|$)/.exec(location.pathname);if(!t){document.body.textContent="invalid share URL";return}let n=t[1],s=document.getElementById("session-info"),i=document.getElementById("messages"),l=document.querySelector(".share-footer-author"),c=document.querySelector(".share-footer-meta");if(!i)return;let o;try{let e=await fetch(`/api/v1/shared/${encodeURIComponent(n)}/events`,{headers:{Accept:"application/json"},credentials:"omit"});if(e.status===p.GONE){document.body.innerHTML="<div class='share-gone'>\u6B64\u94FE\u63A5\u5DF2\u64A4\u9500\u6216\u8FC7\u671F\u3002</div>";return}if(!e.ok){document.body.textContent=`error ${e.status}`;return}o=await e.json()}catch(e){document.body.textContent=`failed to load: ${String(e)}`;return}if(document.title=o.share.session_title?`${o.share.session_title} \u2014 shared`:"shared session",s&&(s.textContent=o.share.session_title??"(untitled)"),l){let e=o.share.display_name??"";l.textContent=e?`by ${e}`:""}if(v(o.events,i,n),c){let e=o.share.shared_at??o.share.created_at;c.textContent=E(e,new Date),c instanceof HTMLElement&&(c.title=_(e))}}U();
|
package/dist/login.html
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
media="(prefers-color-scheme: light)"
|
|
18
18
|
/>
|
|
19
19
|
<script src="/theme-init.js"></script>
|
|
20
|
-
<link rel="stylesheet" href="/styles.
|
|
20
|
+
<link rel="stylesheet" href="/styles.00etlpgs.css" />
|
|
21
21
|
</head>
|
|
22
22
|
<body class="login-page">
|
|
23
23
|
<div class="login-card">
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
<code>webagent --create-token <name></code> on the server.
|
|
45
45
|
</p>
|
|
46
46
|
</div>
|
|
47
|
-
<script type="module" src="/js/login.
|
|
47
|
+
<script type="module" src="/js/login.2WA6DTGM.js"></script>
|
|
48
48
|
</body>
|
|
49
49
|
</html>
|
package/dist/share-viewer.html
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
media="(prefers-color-scheme: light)"
|
|
17
17
|
/>
|
|
18
18
|
<meta name="robots" content="noindex, nofollow" />
|
|
19
|
-
<link rel="stylesheet" href="/s/_/styles.
|
|
19
|
+
<link rel="stylesheet" href="/s/_/styles.00etlpgs.css" />
|
|
20
20
|
<link rel="stylesheet" href="/s/_/share-viewer.00gubshk.css" />
|
|
21
|
-
<link rel="modulepreload" href="/s/_/chunk.
|
|
22
|
-
<link rel="modulepreload" href="/s/_/chunk.
|
|
21
|
+
<link rel="modulepreload" href="/s/_/chunk.CT5WBNGZ.js">
|
|
22
|
+
<link rel="modulepreload" href="/s/_/chunk.UMQMOGWO.js">
|
|
23
23
|
<link rel="modulepreload" href="/s/_/chunk.AJZBJBMO.js">
|
|
24
24
|
</head>
|
|
25
25
|
<body class="share-viewer">
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
>
|
|
50
50
|
</div>
|
|
51
51
|
</footer>
|
|
52
|
-
<script type="module" src="/s/_/viewer.
|
|
52
|
+
<script type="module" src="/s/_/viewer.CVWXSKJM.js"></script>
|
|
53
53
|
</body>
|
|
54
54
|
</html>
|
|
@@ -84,6 +84,32 @@ body {
|
|
|
84
84
|
font-weight: bold;
|
|
85
85
|
color: var(--green);
|
|
86
86
|
}
|
|
87
|
+
#inbox-btn {
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 4px;
|
|
91
|
+
min-height: 28px;
|
|
92
|
+
padding: 2px 6px;
|
|
93
|
+
margin-left: -6px;
|
|
94
|
+
border: 0;
|
|
95
|
+
background: transparent;
|
|
96
|
+
color: inherit;
|
|
97
|
+
font: inherit;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
#inbox-btn:hover .logo,
|
|
101
|
+
#inbox-btn:focus-visible .logo {
|
|
102
|
+
color: var(--text);
|
|
103
|
+
}
|
|
104
|
+
#inbox-btn:focus-visible {
|
|
105
|
+
outline: 1px solid var(--text-dim);
|
|
106
|
+
outline-offset: 1px;
|
|
107
|
+
}
|
|
108
|
+
#inbox-count {
|
|
109
|
+
color: var(--red);
|
|
110
|
+
font-size: 13px;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
}
|
|
87
113
|
.header-side {
|
|
88
114
|
min-width: 0;
|
|
89
115
|
display: flex;
|
|
@@ -312,6 +338,22 @@ body {
|
|
|
312
338
|
font-weight: normal;
|
|
313
339
|
}
|
|
314
340
|
|
|
341
|
+
.msg.assistant .subagent-result {
|
|
342
|
+
color: var(--text-dim);
|
|
343
|
+
font-size: 14px;
|
|
344
|
+
margin: 2px 0;
|
|
345
|
+
}
|
|
346
|
+
.msg.assistant .subagent-result summary {
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
}
|
|
349
|
+
.msg.assistant .subagent-result-content {
|
|
350
|
+
color: var(--text);
|
|
351
|
+
margin: 4px 0 4px 16px;
|
|
352
|
+
}
|
|
353
|
+
.msg.assistant .assistant-continuation {
|
|
354
|
+
margin-top: 4px;
|
|
355
|
+
}
|
|
356
|
+
|
|
315
357
|
/* Thinking */
|
|
316
358
|
.thinking {
|
|
317
359
|
color: var(--text-dim);
|
|
@@ -494,6 +536,12 @@ body {
|
|
|
494
536
|
margin-bottom: 2px;
|
|
495
537
|
cursor: pointer;
|
|
496
538
|
}
|
|
539
|
+
.plan-summary::marker {
|
|
540
|
+
content: "";
|
|
541
|
+
}
|
|
542
|
+
.plan-summary::-webkit-details-marker {
|
|
543
|
+
display: none;
|
|
544
|
+
}
|
|
497
545
|
.plan-counts {
|
|
498
546
|
color: var(--text-dim);
|
|
499
547
|
font-weight: normal;
|
|
@@ -505,6 +553,28 @@ body {
|
|
|
505
553
|
.plan-entry {
|
|
506
554
|
padding: 1px 0;
|
|
507
555
|
}
|
|
556
|
+
.plan-status-pending {
|
|
557
|
+
color: var(--text-dim);
|
|
558
|
+
}
|
|
559
|
+
.plan-status-in_progress .plan-symbol,
|
|
560
|
+
.plan-counts .plan-status-in_progress {
|
|
561
|
+
color: var(--blue);
|
|
562
|
+
}
|
|
563
|
+
.plan-status-in_progress .plan-symbol {
|
|
564
|
+
display: inline-block;
|
|
565
|
+
background: var(--accent-soft);
|
|
566
|
+
border-radius: 2px;
|
|
567
|
+
}
|
|
568
|
+
.plan-status-completed .plan-symbol,
|
|
569
|
+
.plan-counts .plan-status-completed {
|
|
570
|
+
color: var(--green);
|
|
571
|
+
}
|
|
572
|
+
.plan-status-completed .plan-content {
|
|
573
|
+
color: var(--text-dim);
|
|
574
|
+
}
|
|
575
|
+
.plan-status-unknown {
|
|
576
|
+
color: var(--text-dim);
|
|
577
|
+
}
|
|
508
578
|
|
|
509
579
|
/* Permission dialog */
|
|
510
580
|
.permission {
|
|
@@ -795,6 +865,30 @@ body {
|
|
|
795
865
|
}
|
|
796
866
|
}
|
|
797
867
|
|
|
868
|
+
/* Panels attached to the input */
|
|
869
|
+
.input-panel {
|
|
870
|
+
background: var(--surface);
|
|
871
|
+
border: 1px solid var(--border);
|
|
872
|
+
border-bottom: none;
|
|
873
|
+
max-height: 220px;
|
|
874
|
+
}
|
|
875
|
+
#plan-panel {
|
|
876
|
+
flex: 0 0 auto;
|
|
877
|
+
}
|
|
878
|
+
#plan-panel[hidden] {
|
|
879
|
+
display: none;
|
|
880
|
+
}
|
|
881
|
+
#plan-panel > .plan-summary {
|
|
882
|
+
margin: 0;
|
|
883
|
+
padding: 6px 16px;
|
|
884
|
+
}
|
|
885
|
+
#plan-panel > .plan-entries {
|
|
886
|
+
max-height: 180px;
|
|
887
|
+
margin: 0;
|
|
888
|
+
padding: 0 16px 6px;
|
|
889
|
+
overflow-y: auto;
|
|
890
|
+
}
|
|
891
|
+
|
|
798
892
|
/* Slash command autocomplete */
|
|
799
893
|
#slash-menu {
|
|
800
894
|
display: none;
|
|
@@ -802,10 +896,6 @@ body {
|
|
|
802
896
|
bottom: 100%;
|
|
803
897
|
left: 0;
|
|
804
898
|
right: 0;
|
|
805
|
-
background: var(--surface);
|
|
806
|
-
border: 1px solid var(--border);
|
|
807
|
-
border-bottom: none;
|
|
808
|
-
max-height: 220px;
|
|
809
899
|
overflow-y: auto;
|
|
810
900
|
z-index: 10;
|
|
811
901
|
}
|
package/dist/sw.js
CHANGED
|
@@ -79,25 +79,35 @@ self.addEventListener("notificationclick", (e) => {
|
|
|
79
79
|
e.notification.close();
|
|
80
80
|
|
|
81
81
|
const data = e.notification.data || {};
|
|
82
|
-
// For inbox messages, route to the bound session (if set) or root.
|
|
83
|
-
// For session events, route to the session.
|
|
84
82
|
const sessionId = data.sessionId;
|
|
85
|
-
const
|
|
83
|
+
const messageId = data.messageId;
|
|
84
|
+
const target = sessionId
|
|
85
|
+
? { type: "navigate", sessionId }
|
|
86
|
+
: messageId
|
|
87
|
+
? { type: "navigate", messageId }
|
|
88
|
+
: { type: "navigate" };
|
|
89
|
+
const targetUrl = sessionId
|
|
90
|
+
? `/#${encodeURIComponent(sessionId)}`
|
|
91
|
+
: messageId
|
|
92
|
+
? `/?message=${encodeURIComponent(messageId)}`
|
|
93
|
+
: "/";
|
|
86
94
|
|
|
87
95
|
e.waitUntil(
|
|
88
96
|
self.clients
|
|
89
97
|
.matchAll({ type: "window", includeUncontrolled: true })
|
|
90
|
-
.then((clients) => {
|
|
91
|
-
// Focus existing window if open
|
|
98
|
+
.then(async (clients) => {
|
|
92
99
|
for (const client of clients) {
|
|
93
100
|
if (client.url.includes(self.location.origin)) {
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
try {
|
|
102
|
+
await client.focus();
|
|
103
|
+
} catch {
|
|
104
|
+
// Navigation still works if the browser refuses focus.
|
|
105
|
+
}
|
|
106
|
+
client.postMessage(target);
|
|
96
107
|
return;
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
|
-
|
|
100
|
-
return self.clients.openWindow(urlHash);
|
|
110
|
+
return self.clients.openWindow(targetUrl);
|
|
101
111
|
}),
|
|
102
112
|
);
|
|
103
113
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function splitFirstToken(text) {
|
|
2
|
+
const boundary = text.search(/\s/);
|
|
3
|
+
if (boundary < 0)
|
|
4
|
+
return { token: text, remainder: "" };
|
|
5
|
+
return {
|
|
6
|
+
token: text.slice(0, boundary),
|
|
7
|
+
remainder: text.slice(boundary),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function resolveAgentCommand(text, commands) {
|
|
11
|
+
if (!text.startsWith("//"))
|
|
12
|
+
return null;
|
|
13
|
+
const { token, remainder } = splitFirstToken(text);
|
|
14
|
+
const inputName = token.slice(2);
|
|
15
|
+
if (!inputName)
|
|
16
|
+
return null;
|
|
17
|
+
const command = commands.find((candidate) => candidate.name.toLowerCase() === inputName.toLowerCase());
|
|
18
|
+
if (!command)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
command: token,
|
|
22
|
+
agentText: `/${command.name}${remainder}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function agentCommandToken(text) {
|
|
26
|
+
return splitFirstToken(text).token || text;
|
|
27
|
+
}
|
package/lib/bridge.js
CHANGED
|
@@ -15,6 +15,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
15
15
|
pendingAborts = new Map();
|
|
16
16
|
deadReason = null;
|
|
17
17
|
stderrTail = "";
|
|
18
|
+
closedProcesses = new WeakSet();
|
|
18
19
|
agentCmd;
|
|
19
20
|
reloading = false;
|
|
20
21
|
attachmentDispatcher = null;
|
|
@@ -60,6 +61,9 @@ export class AgentBridge extends EventEmitter {
|
|
|
60
61
|
`\nCheck '${this.agentCmd}' is properly configured (e.g. authenticated).`;
|
|
61
62
|
this.markAgentDead(reason);
|
|
62
63
|
});
|
|
64
|
+
proc.once("close", () => {
|
|
65
|
+
this.closedProcesses.add(proc);
|
|
66
|
+
});
|
|
63
67
|
proc.on("error", (err) => {
|
|
64
68
|
if (this.reloading)
|
|
65
69
|
return;
|
|
@@ -156,7 +160,10 @@ export class AgentBridge extends EventEmitter {
|
|
|
156
160
|
});
|
|
157
161
|
return result.configOptions;
|
|
158
162
|
}
|
|
159
|
-
async prompt(sessionId, text, attachments
|
|
163
|
+
async prompt(sessionId, text, attachments,
|
|
164
|
+
/** Turn identity echoed back on this prompt's terminal event, so a
|
|
165
|
+
* completion that outlives its turn can be told apart from the live one. */
|
|
166
|
+
promptId) {
|
|
160
167
|
if (this.deadReason) {
|
|
161
168
|
this.emit("event", {
|
|
162
169
|
type: "error",
|
|
@@ -198,6 +205,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
198
205
|
type: "prompt_done",
|
|
199
206
|
sessionId,
|
|
200
207
|
stopReason: result.stopReason ?? "end_turn",
|
|
208
|
+
...(promptId ? { promptId } : {}),
|
|
201
209
|
});
|
|
202
210
|
}
|
|
203
211
|
catch (err) {
|
|
@@ -211,6 +219,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
211
219
|
type: "prompt_done",
|
|
212
220
|
sessionId,
|
|
213
221
|
stopReason: "cancelled",
|
|
222
|
+
...(promptId ? { promptId } : {}),
|
|
214
223
|
});
|
|
215
224
|
return;
|
|
216
225
|
}
|
|
@@ -218,6 +227,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
218
227
|
type: "error",
|
|
219
228
|
sessionId,
|
|
220
229
|
message,
|
|
230
|
+
...(promptId ? { promptId } : {}),
|
|
221
231
|
});
|
|
222
232
|
}
|
|
223
233
|
finally {
|
|
@@ -259,6 +269,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
259
269
|
});
|
|
260
270
|
abort(new Error(reason));
|
|
261
271
|
}
|
|
272
|
+
this.emit("event", { type: "agent_disconnected" });
|
|
262
273
|
this.conn = null;
|
|
263
274
|
}
|
|
264
275
|
/** Send a prompt and collect the full text response without emitting events. */
|
|
@@ -323,6 +334,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
323
334
|
if (this.reloading)
|
|
324
335
|
throw new Error("Already reloading");
|
|
325
336
|
this.reloading = true;
|
|
337
|
+
const liveSessionIds = [...sessions.liveSessions];
|
|
326
338
|
this.emit("event", { type: "agent_reloading" });
|
|
327
339
|
blog.info("reloading agent...");
|
|
328
340
|
try {
|
|
@@ -341,15 +353,24 @@ export class AgentBridge extends EventEmitter {
|
|
|
341
353
|
}
|
|
342
354
|
}
|
|
343
355
|
// 2. Flush buffers to persist partial content
|
|
344
|
-
for (const sessionId of
|
|
356
|
+
for (const sessionId of liveSessionIds) {
|
|
345
357
|
sessions.flushBuffers(sessionId);
|
|
346
358
|
}
|
|
347
359
|
// 3. Clean up SessionManager state
|
|
348
360
|
sessions.pendingPermissions.clear();
|
|
349
|
-
|
|
361
|
+
sessions.state.clearPlans();
|
|
362
|
+
const busySessionIds = new Set([
|
|
363
|
+
...sessions.activePrompts,
|
|
364
|
+
...sessions.pendingPromptSubmissions.keys(),
|
|
365
|
+
]);
|
|
366
|
+
for (const id of busySessionIds) {
|
|
350
367
|
sessions.state.patch(id, { runtime: { busy: null } });
|
|
351
368
|
}
|
|
369
|
+
for (const submissionId of sessions.pendingPromptSubmissions.values()) {
|
|
370
|
+
sessions.cancelledPromptSubmissions.add(submissionId);
|
|
371
|
+
}
|
|
352
372
|
sessions.activePrompts.clear();
|
|
373
|
+
sessions.pendingPromptSubmissions.clear();
|
|
353
374
|
// 4. Clean up bridge-side silent session state
|
|
354
375
|
this.silentSessions.clear();
|
|
355
376
|
this.silentBuffers.clear();
|
|
@@ -363,6 +384,13 @@ export class AgentBridge extends EventEmitter {
|
|
|
363
384
|
sessions.cachedConfigOptions = [];
|
|
364
385
|
// 6. Shutdown old process
|
|
365
386
|
await this.shutdown();
|
|
387
|
+
// Cancellation is asynchronous: the old agent may emit final chunks
|
|
388
|
+
// before shutdown completes. Persist that tail and make the terminal
|
|
389
|
+
// stream state authoritative before starting the replacement process.
|
|
390
|
+
for (const sessionId of liveSessionIds) {
|
|
391
|
+
sessions.flushBuffers(sessionId);
|
|
392
|
+
}
|
|
393
|
+
sessions.state.clearStreaming();
|
|
366
394
|
// 7. Start new process with retry (exponential backoff, max 3 attempts)
|
|
367
395
|
let lastError;
|
|
368
396
|
for (let i = 0; i < 3; i++) {
|
|
@@ -399,17 +427,28 @@ export class AgentBridge extends EventEmitter {
|
|
|
399
427
|
}
|
|
400
428
|
this.permissionResolvers.clear();
|
|
401
429
|
this.permissionRequestSessions.clear();
|
|
402
|
-
|
|
403
|
-
|
|
430
|
+
const proc = this.proc;
|
|
431
|
+
if (proc && !this.closedProcesses.has(proc)) {
|
|
404
432
|
await new Promise((resolve) => {
|
|
405
|
-
|
|
406
|
-
|
|
433
|
+
let settled = false;
|
|
434
|
+
const finish = () => {
|
|
435
|
+
if (settled)
|
|
436
|
+
return;
|
|
437
|
+
settled = true;
|
|
438
|
+
this.closedProcesses.add(proc);
|
|
439
|
+
clearTimeout(killTimer);
|
|
440
|
+
clearTimeout(drainTimer);
|
|
441
|
+
proc.off("close", finish);
|
|
407
442
|
resolve();
|
|
443
|
+
};
|
|
444
|
+
const killTimer = setTimeout(() => {
|
|
445
|
+
proc.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
|
408
446
|
}, 5000);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
447
|
+
// `close` follows `exit` after stdio has drained. Keep a bounded
|
|
448
|
+
// fallback for pathological child-process implementations that never
|
|
449
|
+
// deliver close even after SIGKILL.
|
|
450
|
+
const drainTimer = setTimeout(finish, 6000);
|
|
451
|
+
proc.once("close", finish);
|
|
413
452
|
proc.kill();
|
|
414
453
|
});
|
|
415
454
|
}
|
|
@@ -499,6 +538,16 @@ export class AgentBridge extends EventEmitter {
|
|
|
499
538
|
configOptions: update
|
|
500
539
|
.configOptions ?? [],
|
|
501
540
|
};
|
|
541
|
+
case "available_commands_update":
|
|
542
|
+
return {
|
|
543
|
+
type: "available_commands_update",
|
|
544
|
+
sessionId,
|
|
545
|
+
commands: update.availableCommands.map((command) => ({
|
|
546
|
+
name: command.name,
|
|
547
|
+
description: command.description,
|
|
548
|
+
...(command.input ? { input: { hint: command.input.hint } } : {}),
|
|
549
|
+
})),
|
|
550
|
+
};
|
|
502
551
|
default:
|
|
503
552
|
return null;
|
|
504
553
|
}
|
package/lib/event-handler.js
CHANGED
|
@@ -3,6 +3,7 @@ import { log } from "./log.js";
|
|
|
3
3
|
import { isAutopilotMode } from "./mode-bucket.js";
|
|
4
4
|
const ailog = log.scope("attachment-interceptor");
|
|
5
5
|
const plog = log.scope("push");
|
|
6
|
+
const clog = log.scope("cancel");
|
|
6
7
|
function handleConnected(event, sessions, config) {
|
|
7
8
|
event.cancelTimeout = config.cancelTimeout;
|
|
8
9
|
event.recentPathsLimit = config.recentPathsLimit;
|
|
@@ -10,14 +11,8 @@ function handleConnected(event, sessions, config) {
|
|
|
10
11
|
if (event.agent)
|
|
11
12
|
sessions.agentInfo = event.agent;
|
|
12
13
|
}
|
|
13
|
-
function handleConfigLikeEvent(event, sessions
|
|
14
|
-
|
|
15
|
-
sessions.cachedConfigOptions = event.configOptions;
|
|
16
|
-
for (const opt of event.configOptions) {
|
|
17
|
-
if (typeof opt.currentValue === "string") {
|
|
18
|
-
store.updateSessionConfig(event.sessionId, opt.id, opt.currentValue);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
14
|
+
function handleConfigLikeEvent(event, sessions) {
|
|
15
|
+
sessions.recordConfigOptions(event.sessionId, event.configOptions);
|
|
21
16
|
}
|
|
22
17
|
function handleMessageChunk(event, sessions) {
|
|
23
18
|
sessions.flushThinkingBuffer(event.sessionId);
|
|
@@ -48,7 +43,13 @@ function handleToolCall(event, sessions, store) {
|
|
|
48
43
|
function handlePlan(event, sessions, store) {
|
|
49
44
|
sessions.flushBuffers(event.sessionId);
|
|
50
45
|
sessions.state.patch(event.sessionId, {
|
|
51
|
-
runtime: {
|
|
46
|
+
runtime: {
|
|
47
|
+
streaming: { assistant: false, thinking: false },
|
|
48
|
+
plan: event.entries.length > 0 &&
|
|
49
|
+
!event.entries.every((entry) => entry.status === "completed")
|
|
50
|
+
? event.entries
|
|
51
|
+
: null,
|
|
52
|
+
},
|
|
52
53
|
});
|
|
53
54
|
store.saveEvent(event.sessionId, event.type, { entries: event.entries }, { from_ref: "agent" });
|
|
54
55
|
}
|
|
@@ -144,18 +145,57 @@ function handlePermissionRequest(event, sessions, store, bridge, sseManager, con
|
|
|
144
145
|
return false;
|
|
145
146
|
}
|
|
146
147
|
function handlePromptDone(event, sessions, store) {
|
|
147
|
-
sessions.
|
|
148
|
-
|
|
148
|
+
const cancelStatus = sessions.state.getState(event.sessionId).runtime.busy?.cancelStatus ?? null;
|
|
149
|
+
if (cancelStatus !== null) {
|
|
150
|
+
clog.info("agent completed after request", {
|
|
151
|
+
sessionId: event.sessionId.slice(0, 8),
|
|
152
|
+
requestedStatus: cancelStatus,
|
|
153
|
+
stopReason: event.stopReason,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// The tail this turn buffered must always land, even when the turn has
|
|
157
|
+
// already been superseded — it is the only copy of that text.
|
|
158
|
+
const isCurrent = sessions.isCurrentPrompt(event.sessionId, event.promptId);
|
|
159
|
+
if (isCurrent) {
|
|
160
|
+
sessions.activePrompts.delete(event.sessionId);
|
|
161
|
+
sessions.syncBusy(event.sessionId);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
clog.info("completion from a superseded turn", {
|
|
165
|
+
sessionId: event.sessionId.slice(0, 8),
|
|
166
|
+
promptId: event.promptId,
|
|
167
|
+
stopReason: event.stopReason,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
149
170
|
sessions.flushBuffers(event.sessionId);
|
|
150
171
|
sessions.state.patch(event.sessionId, {
|
|
151
172
|
runtime: { streaming: { assistant: false, thinking: false } },
|
|
152
173
|
});
|
|
153
|
-
store.saveEvent(event.sessionId, event.type, {
|
|
174
|
+
store.saveEvent(event.sessionId, event.type, {
|
|
175
|
+
stopReason: event.stopReason,
|
|
176
|
+
// Replay must be able to make the same judgement the live path does.
|
|
177
|
+
...(event.promptId ? { promptId: event.promptId } : {}),
|
|
178
|
+
}, { from_ref: "agent" });
|
|
154
179
|
}
|
|
155
180
|
function handleError(event, sessions) {
|
|
156
181
|
if (event.sessionId) {
|
|
157
|
-
|
|
158
|
-
|
|
182
|
+
// Same attribution as a completion: a superseded turn failing late must
|
|
183
|
+
// not end the turn that replaced it. The buffered tail still flushes.
|
|
184
|
+
if (sessions.isCurrentPrompt(event.sessionId, event.promptId)) {
|
|
185
|
+
sessions.activePrompts.delete(event.sessionId);
|
|
186
|
+
sessions.syncBusy(event.sessionId);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
clog.info("failure from a superseded turn", {
|
|
190
|
+
sessionId: event.sessionId.slice(0, 8),
|
|
191
|
+
promptId: event.promptId,
|
|
192
|
+
message: event.message,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
sessions.flushBuffers(event.sessionId);
|
|
196
|
+
sessions.state.patch(event.sessionId, {
|
|
197
|
+
runtime: { streaming: { assistant: false, thinking: false } },
|
|
198
|
+
});
|
|
159
199
|
}
|
|
160
200
|
}
|
|
161
201
|
function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager) {
|
|
@@ -166,7 +206,7 @@ function dispatchAgentEvent(event, sessions, store, bridge, config, sseManager)
|
|
|
166
206
|
return false;
|
|
167
207
|
case "session_created":
|
|
168
208
|
case "config_option_update":
|
|
169
|
-
handleConfigLikeEvent(event, sessions
|
|
209
|
+
handleConfigLikeEvent(event, sessions);
|
|
170
210
|
return false;
|
|
171
211
|
case "message_chunk":
|
|
172
212
|
handleMessageChunk(event, sessions);
|
|
@@ -213,6 +253,26 @@ function maybePushNotify(event, pushService) {
|
|
|
213
253
|
});
|
|
214
254
|
}
|
|
215
255
|
export function handleAgentEvent(event, sessions, store, bridge, config, sseManager, pushService, _clientRegistry) {
|
|
256
|
+
if (event.type === "available_commands_update") {
|
|
257
|
+
const snapshot = sessions.updateAgentCommands(event.sessionId, event.commands);
|
|
258
|
+
if (sessions.restoringSessions.has(event.sessionId))
|
|
259
|
+
return;
|
|
260
|
+
sseManager.broadcast({ ...event, ...snapshot });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (event.type === "agent_reloading" || event.type === "agent_disconnected") {
|
|
264
|
+
for (const sessionId of sessions.liveSessions) {
|
|
265
|
+
sessions.flushBuffers(sessionId);
|
|
266
|
+
}
|
|
267
|
+
sessions.state.clearStreaming();
|
|
268
|
+
sessions.state.clearPlans();
|
|
269
|
+
for (const snapshot of sessions.clearAgentCommands()) {
|
|
270
|
+
sseManager.broadcast({
|
|
271
|
+
type: "available_commands_update",
|
|
272
|
+
...snapshot,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
216
276
|
if ("sessionId" in event &&
|
|
217
277
|
event.sessionId &&
|
|
218
278
|
sessions.restoringSessions.has(event.sessionId))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const HTTP_STATUS = Object.freeze({
|
|
2
|
+
OK: 200,
|
|
3
|
+
CREATED: 201,
|
|
4
|
+
ACCEPTED: 202,
|
|
5
|
+
NO_CONTENT: 204,
|
|
6
|
+
BAD_REQUEST: 400,
|
|
7
|
+
UNAUTHORIZED: 401,
|
|
8
|
+
FORBIDDEN: 403,
|
|
9
|
+
NOT_FOUND: 404,
|
|
10
|
+
CONFLICT: 409,
|
|
11
|
+
GONE: 410,
|
|
12
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
13
|
+
UNPROCESSABLE_CONTENT: 422,
|
|
14
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
15
|
+
NOT_IMPLEMENTED: 501,
|
|
16
|
+
SERVICE_UNAVAILABLE: 503,
|
|
17
|
+
});
|
package/lib/message-cleanup.js
CHANGED
|
@@ -6,13 +6,14 @@ const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
6
6
|
* Returns the number of rows removed. `now` is injectable for tests.
|
|
7
7
|
* ttlDays=0 means "keep forever" — returns 0 without touching the DB.
|
|
8
8
|
*/
|
|
9
|
-
export function sweepOnce(store, ttlDays, now = Date.now()) {
|
|
9
|
+
export function sweepOnce(store, ttlDays, now = Date.now(), onPendingCountChange) {
|
|
10
10
|
if (ttlDays <= 0)
|
|
11
11
|
return 0;
|
|
12
12
|
const threshold = now - ttlDays * DAY_MS;
|
|
13
13
|
const removed = store.deleteOlderThan(threshold);
|
|
14
14
|
if (removed > 0) {
|
|
15
15
|
mlog.info("ttl sweep", { removed, ttl_days: ttlDays });
|
|
16
|
+
onPendingCountChange?.(store.countUnprocessed());
|
|
16
17
|
}
|
|
17
18
|
return removed;
|
|
18
19
|
}
|
|
@@ -23,14 +24,14 @@ export function sweepOnce(store, ttlDays, now = Date.now()) {
|
|
|
23
24
|
*
|
|
24
25
|
* ttlDays=0 disables the scheduler entirely (handle.armed=false).
|
|
25
26
|
*/
|
|
26
|
-
export function startMessageCleanup(store, ttlDays) {
|
|
27
|
-
sweepOnce(store, ttlDays);
|
|
27
|
+
export function startMessageCleanup(store, ttlDays, onPendingCountChange) {
|
|
28
|
+
sweepOnce(store, ttlDays, Date.now(), onPendingCountChange);
|
|
28
29
|
if (ttlDays <= 0) {
|
|
29
30
|
return { armed: false, stop: () => { } };
|
|
30
31
|
}
|
|
31
32
|
const timer = setInterval(() => {
|
|
32
33
|
try {
|
|
33
|
-
sweepOnce(store, ttlDays);
|
|
34
|
+
sweepOnce(store, ttlDays, Date.now(), onPendingCountChange);
|
|
34
35
|
}
|
|
35
36
|
catch (err) {
|
|
36
37
|
mlog.error("ttl sweep failed", { error: err });
|
package/lib/push-service.js
CHANGED
|
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { log } from "./log.js";
|
|
6
|
+
import { HTTP_STATUS } from "./http-status.js";
|
|
6
7
|
const slog = log.scope("push");
|
|
7
8
|
const elog = log.scope("egress");
|
|
8
9
|
const VAPID_FILE = "vapid.json";
|
|
@@ -267,7 +268,7 @@ export class PushService {
|
|
|
267
268
|
else {
|
|
268
269
|
fail++;
|
|
269
270
|
const err = result.reason;
|
|
270
|
-
if (err.statusCode ===
|
|
271
|
+
if (err.statusCode === HTTP_STATUS.GONE) {
|
|
271
272
|
fail410++;
|
|
272
273
|
this.store.removeSubscription(endpoint);
|
|
273
274
|
this.failureCounts.delete(endpoint);
|