@lelouchhe/webagent 0.7.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 +14 -5
- package/dist/js/app.XBFXH37R.js +2 -0
- package/dist/js/chunk.CT5WBNGZ.js +173 -0
- package/dist/js/viewer.CVWXSKJM.js +1 -0
- package/dist/login.html +1 -1
- package/dist/share-viewer.html +3 -3
- package/dist/{styles.01ky3cex.css → styles.00etlpgs.css} +94 -4
- package/dist/sw.js +19 -9
- package/lib/bridge.js +49 -11
- package/lib/event-handler.js +60 -15
- package/lib/message-cleanup.js +5 -4
- package/lib/routes.js +199 -44
- package/lib/server.js +4 -3
- package/lib/session-manager.js +170 -22
- package/lib/session-state.js +70 -5
- package/lib/sse-manager.js +31 -7
- package/lib/store.js +25 -23
- package/package.json +1 -1
- package/dist/js/app.KFQAFHA3.js +0 -4
- package/dist/js/chunk.3CCHZCG7.js +0 -173
- package/dist/js/viewer.T32M5AZZ.js +0 -1
|
@@ -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">
|
package/dist/share-viewer.html
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
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.
|
|
21
|
+
<link rel="modulepreload" href="/s/_/chunk.CT5WBNGZ.js">
|
|
22
22
|
<link rel="modulepreload" href="/s/_/chunk.UMQMOGWO.js">
|
|
23
23
|
<link rel="modulepreload" href="/s/_/chunk.AJZBJBMO.js">
|
|
24
24
|
</head>
|
|
@@ -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
|
});
|
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 {
|
|
@@ -324,6 +334,7 @@ export class AgentBridge extends EventEmitter {
|
|
|
324
334
|
if (this.reloading)
|
|
325
335
|
throw new Error("Already reloading");
|
|
326
336
|
this.reloading = true;
|
|
337
|
+
const liveSessionIds = [...sessions.liveSessions];
|
|
327
338
|
this.emit("event", { type: "agent_reloading" });
|
|
328
339
|
blog.info("reloading agent...");
|
|
329
340
|
try {
|
|
@@ -342,15 +353,24 @@ export class AgentBridge extends EventEmitter {
|
|
|
342
353
|
}
|
|
343
354
|
}
|
|
344
355
|
// 2. Flush buffers to persist partial content
|
|
345
|
-
for (const sessionId of
|
|
356
|
+
for (const sessionId of liveSessionIds) {
|
|
346
357
|
sessions.flushBuffers(sessionId);
|
|
347
358
|
}
|
|
348
359
|
// 3. Clean up SessionManager state
|
|
349
360
|
sessions.pendingPermissions.clear();
|
|
350
|
-
|
|
361
|
+
sessions.state.clearPlans();
|
|
362
|
+
const busySessionIds = new Set([
|
|
363
|
+
...sessions.activePrompts,
|
|
364
|
+
...sessions.pendingPromptSubmissions.keys(),
|
|
365
|
+
]);
|
|
366
|
+
for (const id of busySessionIds) {
|
|
351
367
|
sessions.state.patch(id, { runtime: { busy: null } });
|
|
352
368
|
}
|
|
369
|
+
for (const submissionId of sessions.pendingPromptSubmissions.values()) {
|
|
370
|
+
sessions.cancelledPromptSubmissions.add(submissionId);
|
|
371
|
+
}
|
|
353
372
|
sessions.activePrompts.clear();
|
|
373
|
+
sessions.pendingPromptSubmissions.clear();
|
|
354
374
|
// 4. Clean up bridge-side silent session state
|
|
355
375
|
this.silentSessions.clear();
|
|
356
376
|
this.silentBuffers.clear();
|
|
@@ -364,6 +384,13 @@ export class AgentBridge extends EventEmitter {
|
|
|
364
384
|
sessions.cachedConfigOptions = [];
|
|
365
385
|
// 6. Shutdown old process
|
|
366
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();
|
|
367
394
|
// 7. Start new process with retry (exponential backoff, max 3 attempts)
|
|
368
395
|
let lastError;
|
|
369
396
|
for (let i = 0; i < 3; i++) {
|
|
@@ -400,17 +427,28 @@ export class AgentBridge extends EventEmitter {
|
|
|
400
427
|
}
|
|
401
428
|
this.permissionResolvers.clear();
|
|
402
429
|
this.permissionRequestSessions.clear();
|
|
403
|
-
|
|
404
|
-
|
|
430
|
+
const proc = this.proc;
|
|
431
|
+
if (proc && !this.closedProcesses.has(proc)) {
|
|
405
432
|
await new Promise((resolve) => {
|
|
406
|
-
|
|
407
|
-
|
|
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);
|
|
408
442
|
resolve();
|
|
443
|
+
};
|
|
444
|
+
const killTimer = setTimeout(() => {
|
|
445
|
+
proc.kill(process.platform === "win32" ? undefined : "SIGKILL");
|
|
409
446
|
}, 5000);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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);
|
|
414
452
|
proc.kill();
|
|
415
453
|
});
|
|
416
454
|
}
|
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);
|
|
@@ -221,6 +261,11 @@ export function handleAgentEvent(event, sessions, store, bridge, config, sseMana
|
|
|
221
261
|
return;
|
|
222
262
|
}
|
|
223
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();
|
|
224
269
|
for (const snapshot of sessions.clearAgentCommands()) {
|
|
225
270
|
sseManager.broadcast({
|
|
226
271
|
type: "available_commands_update",
|
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 });
|