@gtc6244/immediacy-daemon 0.2.5 → 0.2.7
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/src/agent/mock.js +21 -0
- package/dist/src/agent/mock.js.map +1 -1
- package/dist/src/gitops.js +14 -8
- package/dist/src/gitops.js.map +1 -1
- package/dist/src/protocol.d.ts +39 -4
- package/dist/src/protocol.js +6 -1
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/server.js +9 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/transport.d.ts +11 -0
- package/dist/src/transport.js +37 -8
- package/dist/src/transport.js.map +1 -1
- package/dist/src/turn-summarizer.d.ts +40 -0
- package/dist/src/turn-summarizer.js +168 -0
- package/dist/src/turn-summarizer.js.map +1 -0
- package/dist/src/workspace.d.ts +45 -0
- package/dist/src/workspace.js +141 -0
- package/dist/src/workspace.js.map +1 -1
- package/package.json +4 -2
package/dist/src/transport.js
CHANGED
|
@@ -91,6 +91,8 @@ export class Mux {
|
|
|
91
91
|
highWaterMark;
|
|
92
92
|
reassembler = new Reassembler();
|
|
93
93
|
seqs = new Map();
|
|
94
|
+
/** Tail of the per-channel send chain — serializes concurrent sends. */
|
|
95
|
+
sendChains = new Map();
|
|
94
96
|
onMsg = null;
|
|
95
97
|
constructor(transport, opts) {
|
|
96
98
|
this.transport = transport;
|
|
@@ -119,18 +121,45 @@ export class Mux {
|
|
|
119
121
|
* Send a logical message on `channelId`, chunked as needed. Resolves once all
|
|
120
122
|
* chunks are handed to the transport; awaits drain between chunks so a large
|
|
121
123
|
* message (or a terminal firehose) can't grow the queue without bound.
|
|
124
|
+
*
|
|
125
|
+
* Sends on the same channel are SERIALIZED: `send` awaits `whenWritable`
|
|
126
|
+
* mid-message, so a second concurrent `send` (e.g. a heartbeat pong while a
|
|
127
|
+
* large response is still draining) would otherwise interleave its frames and
|
|
128
|
+
* reuse the shared `seq` counter — the peer's reassembler mixes the two
|
|
129
|
+
* messages and its SecureChannel receive counter desyncs, breaking the session.
|
|
130
|
+
* The per-channel promise chain guarantees one message's frames are fully
|
|
131
|
+
* written (and `seq` advanced) before the next message on that channel starts.
|
|
122
132
|
*/
|
|
123
|
-
|
|
133
|
+
send(channelId, message) {
|
|
134
|
+
const prev = this.sendChains.get(channelId) ?? Promise.resolve();
|
|
135
|
+
// `prev.catch` so one failed send never poisons the ones queued behind it.
|
|
136
|
+
const next = prev.catch(() => { }).then(() => this.sendChunks(channelId, message));
|
|
137
|
+
this.sendChains.set(channelId, next);
|
|
138
|
+
// Drop the chain entry once it drains to the tail, so an idle channel's map
|
|
139
|
+
// entry doesn't pin the last message's promise forever.
|
|
140
|
+
void next.catch(() => { }).finally(() => {
|
|
141
|
+
if (this.sendChains.get(channelId) === next)
|
|
142
|
+
this.sendChains.delete(channelId);
|
|
143
|
+
});
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
async sendChunks(channelId, message) {
|
|
124
147
|
let seq = this.seqs.get(channelId) ?? 0;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
try {
|
|
149
|
+
for (const frame of splitMessage(channelId, seq, message)) {
|
|
150
|
+
this.transport.send(encodeFrame(frame));
|
|
151
|
+
seq = (frame.seq + 1) >>> 0;
|
|
152
|
+
if (this.transport.bufferedAmount() >= this.highWaterMark) {
|
|
153
|
+
await this.transport.whenWritable(this.highWaterMark);
|
|
154
|
+
}
|
|
131
155
|
}
|
|
132
156
|
}
|
|
133
|
-
|
|
157
|
+
finally {
|
|
158
|
+
// Persist progress even on a mid-message throw, so the next queued message
|
|
159
|
+
// never reuses seq numbers already emitted on the wire (which would collide
|
|
160
|
+
// at the peer's reassembler).
|
|
161
|
+
this.seqs.set(channelId, seq);
|
|
162
|
+
}
|
|
134
163
|
}
|
|
135
164
|
}
|
|
136
165
|
//# sourceMappingURL=transport.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn summarizer — condenses one agent turn (everything since the last user
|
|
3
|
+
* prompt) into a few markdown bullet points via a single cheap LLM call.
|
|
4
|
+
*
|
|
5
|
+
* Powers the condensed chat view: when the agent stops (done / awaiting a
|
|
6
|
+
* permission / asking a question), the workspace manager builds the turn slice
|
|
7
|
+
* and calls {@link generateTurnSummary}, persists the result as a `summary`
|
|
8
|
+
* ChatEntry, and broadcasts `event.turn_summary`. Clients in condensed view
|
|
9
|
+
* collapse the turn to these bullets with a button to expand back to full detail.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `titler.ts`: same vendored Claude Agent SDK, same cheap pinned model,
|
|
12
|
+
* tools disabled (a pure completion), best-effort with a null fallback so a slow
|
|
13
|
+
* or failing model call never wedges a turn. The pure helpers
|
|
14
|
+
* (`buildTurnSlice` / `renderSummaryPrompt` / `hasSummarizableContent`) are
|
|
15
|
+
* separated out so they are unit-testable without the LLM.
|
|
16
|
+
*/
|
|
17
|
+
import type { ChatEntry } from "./protocol.js";
|
|
18
|
+
/** Cheapest current model — a handful of bullets needs nothing larger. */
|
|
19
|
+
export declare const SUMMARY_MODEL = "claude-haiku-4-5";
|
|
20
|
+
/** The `seq` of the most recent `user` prompt (the start of the latest turn). */
|
|
21
|
+
export declare function lastUserSeq(entries: ChatEntry[]): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* The entries that make up one turn: everything AFTER the `user` prompt with
|
|
24
|
+
* `seq === turnStartSeq`, up to (but not including) the next `user` prompt.
|
|
25
|
+
* Prior summaries and hidden rename records are dropped. Empty if the start
|
|
26
|
+
* prompt isn't found.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildTurnSlice(entries: ChatEntry[], turnStartSeq: number): ChatEntry[];
|
|
29
|
+
/** Highest `seq` present in a slice (−1 if none) — the turn's content watermark. */
|
|
30
|
+
export declare function sliceContentSeq(slice: ChatEntry[]): number;
|
|
31
|
+
/** True when a slice has any agent output worth summarizing. */
|
|
32
|
+
export declare function hasSummarizableContent(slice: ChatEntry[]): boolean;
|
|
33
|
+
/** Render a turn slice into the model prompt (pure — no I/O). */
|
|
34
|
+
export declare function renderSummaryPrompt(slice: ChatEntry[]): string;
|
|
35
|
+
/**
|
|
36
|
+
* Produce a markdown bullet summary for one turn. Returns null on an empty
|
|
37
|
+
* slice, an empty result, or any error, so callers leave the turn un-condensed
|
|
38
|
+
* (shown in full) rather than blocking.
|
|
39
|
+
*/
|
|
40
|
+
export declare function generateTurnSummary(slice: ChatEntry[], sdkEnv?: Record<string, string | undefined>): Promise<string | null>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn summarizer — condenses one agent turn (everything since the last user
|
|
3
|
+
* prompt) into a few markdown bullet points via a single cheap LLM call.
|
|
4
|
+
*
|
|
5
|
+
* Powers the condensed chat view: when the agent stops (done / awaiting a
|
|
6
|
+
* permission / asking a question), the workspace manager builds the turn slice
|
|
7
|
+
* and calls {@link generateTurnSummary}, persists the result as a `summary`
|
|
8
|
+
* ChatEntry, and broadcasts `event.turn_summary`. Clients in condensed view
|
|
9
|
+
* collapse the turn to these bullets with a button to expand back to full detail.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `titler.ts`: same vendored Claude Agent SDK, same cheap pinned model,
|
|
12
|
+
* tools disabled (a pure completion), best-effort with a null fallback so a slow
|
|
13
|
+
* or failing model call never wedges a turn. The pure helpers
|
|
14
|
+
* (`buildTurnSlice` / `renderSummaryPrompt` / `hasSummarizableContent`) are
|
|
15
|
+
* separated out so they are unit-testable without the LLM.
|
|
16
|
+
*/
|
|
17
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
18
|
+
import { logger } from "./log.js";
|
|
19
|
+
/** Cheapest current model — a handful of bullets needs nothing larger. */
|
|
20
|
+
export const SUMMARY_MODEL = "claude-haiku-4-5";
|
|
21
|
+
const INSTRUCTION = "You are summarizing one turn of an AI coding agent's work for a compact " +
|
|
22
|
+
"mobile chat view. Given the turn transcript below, write 2-5 short markdown " +
|
|
23
|
+
'bullet points (each line starting with "- ") describing what the agent ' +
|
|
24
|
+
"actually did this turn: concrete actions, files changed, commands run, " +
|
|
25
|
+
"decisions made, and — if it stopped — what it is now waiting on (a " +
|
|
26
|
+
"permission or a question). Be terse and specific. Output ONLY the bullet " +
|
|
27
|
+
"list: no preamble, no headings, no closing remarks.\n\nTurn transcript:\n\n";
|
|
28
|
+
/** Per-entry and whole-prompt caps — a longer paste doesn't make better bullets. */
|
|
29
|
+
const MAX_ENTRY_CHARS = 600;
|
|
30
|
+
const MAX_PROMPT_CHARS = 8000;
|
|
31
|
+
/** The `seq` of the most recent `user` prompt (the start of the latest turn). */
|
|
32
|
+
export function lastUserSeq(entries) {
|
|
33
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
34
|
+
const e = entries[i];
|
|
35
|
+
if (e.role === "user" && typeof e.seq === "number")
|
|
36
|
+
return e.seq;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The entries that make up one turn: everything AFTER the `user` prompt with
|
|
42
|
+
* `seq === turnStartSeq`, up to (but not including) the next `user` prompt.
|
|
43
|
+
* Prior summaries and hidden rename records are dropped. Empty if the start
|
|
44
|
+
* prompt isn't found.
|
|
45
|
+
*/
|
|
46
|
+
export function buildTurnSlice(entries, turnStartSeq) {
|
|
47
|
+
const start = entries.findIndex((e) => e.role === "user" && e.seq === turnStartSeq);
|
|
48
|
+
if (start < 0)
|
|
49
|
+
return [];
|
|
50
|
+
const slice = [];
|
|
51
|
+
for (let i = start + 1; i < entries.length; i++) {
|
|
52
|
+
const e = entries[i];
|
|
53
|
+
if (e.role === "user")
|
|
54
|
+
break; // next turn begins
|
|
55
|
+
if (e.role === "summary" || e.role === "rename")
|
|
56
|
+
continue; // not agent output
|
|
57
|
+
slice.push(e);
|
|
58
|
+
}
|
|
59
|
+
return slice;
|
|
60
|
+
}
|
|
61
|
+
/** Highest `seq` present in a slice (−1 if none) — the turn's content watermark. */
|
|
62
|
+
export function sliceContentSeq(slice) {
|
|
63
|
+
let max = -1;
|
|
64
|
+
for (const e of slice) {
|
|
65
|
+
if (typeof e.seq === "number" && e.seq > max)
|
|
66
|
+
max = e.seq;
|
|
67
|
+
}
|
|
68
|
+
return max;
|
|
69
|
+
}
|
|
70
|
+
/** True when a slice has any agent output worth summarizing. */
|
|
71
|
+
export function hasSummarizableContent(slice) {
|
|
72
|
+
return slice.some((e) => e.role === "assistant" ||
|
|
73
|
+
e.role === "tool" ||
|
|
74
|
+
e.role === "assistant_image" ||
|
|
75
|
+
e.role === "permission" ||
|
|
76
|
+
e.role === "question");
|
|
77
|
+
}
|
|
78
|
+
function truncate(s, n) {
|
|
79
|
+
const t = s.trim();
|
|
80
|
+
return t.length > n ? `${t.slice(0, n)}…` : t;
|
|
81
|
+
}
|
|
82
|
+
/** Render a turn slice into the model prompt (pure — no I/O). */
|
|
83
|
+
export function renderSummaryPrompt(slice) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
for (const e of slice) {
|
|
86
|
+
switch (e.role) {
|
|
87
|
+
case "assistant":
|
|
88
|
+
lines.push(`Assistant: ${truncate(e.text, MAX_ENTRY_CHARS)}`);
|
|
89
|
+
break;
|
|
90
|
+
case "tool":
|
|
91
|
+
lines.push(`Tool: ${truncate(e.text, MAX_ENTRY_CHARS)}`);
|
|
92
|
+
break;
|
|
93
|
+
case "assistant_image":
|
|
94
|
+
lines.push("Assistant: [produced an image]");
|
|
95
|
+
break;
|
|
96
|
+
case "permission":
|
|
97
|
+
lines.push(`Permission ${e.resolved ? `(${e.resolved})` : "(pending)"}: ${truncate(e.text, MAX_ENTRY_CHARS)}`);
|
|
98
|
+
break;
|
|
99
|
+
case "question":
|
|
100
|
+
lines.push(`Question ${e.answers || e.response ? "(answered)" : "(pending)"}: ${truncate(e.text, MAX_ENTRY_CHARS)}`);
|
|
101
|
+
break;
|
|
102
|
+
case "limit":
|
|
103
|
+
lines.push("Notice: usage limit reached");
|
|
104
|
+
break;
|
|
105
|
+
case "system":
|
|
106
|
+
lines.push(`System: ${truncate(e.text, MAX_ENTRY_CHARS)}`);
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
break; // rename/summary/user never appear in a slice
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let body = lines.join("\n");
|
|
113
|
+
if (body.length > MAX_PROMPT_CHARS)
|
|
114
|
+
body = `${body.slice(0, MAX_PROMPT_CHARS)}\n…`;
|
|
115
|
+
return INSTRUCTION + body;
|
|
116
|
+
}
|
|
117
|
+
/** Trim and strip a wrapping code fence the model may have added. */
|
|
118
|
+
function clean(raw) {
|
|
119
|
+
return raw
|
|
120
|
+
.trim()
|
|
121
|
+
.replace(/^```[a-zA-Z]*\n?/, "")
|
|
122
|
+
.replace(/\n?```$/, "")
|
|
123
|
+
.trim();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Produce a markdown bullet summary for one turn. Returns null on an empty
|
|
127
|
+
* slice, an empty result, or any error, so callers leave the turn un-condensed
|
|
128
|
+
* (shown in full) rather than blocking.
|
|
129
|
+
*/
|
|
130
|
+
export async function generateTurnSummary(slice, sdkEnv) {
|
|
131
|
+
if (!hasSummarizableContent(slice))
|
|
132
|
+
return null;
|
|
133
|
+
try {
|
|
134
|
+
const response = query({
|
|
135
|
+
prompt: renderSummaryPrompt(slice),
|
|
136
|
+
options: {
|
|
137
|
+
model: SUMMARY_MODEL,
|
|
138
|
+
// No worktree work, no tools — this is a pure completion.
|
|
139
|
+
allowedTools: [],
|
|
140
|
+
maxTurns: 1,
|
|
141
|
+
...(sdkEnv ? { env: { ...process.env, ...sdkEnv } } : {}),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
let text = "";
|
|
145
|
+
for await (const msg of response) {
|
|
146
|
+
const m = msg;
|
|
147
|
+
if (m.type === "assistant") {
|
|
148
|
+
const content = m.message?.content;
|
|
149
|
+
if (Array.isArray(content)) {
|
|
150
|
+
for (const b of content) {
|
|
151
|
+
if (b && typeof b === "object" && b.type === "text") {
|
|
152
|
+
text += b.text ?? "";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const out = clean(text);
|
|
159
|
+
return out || null;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
logger.warn("model", "turn summary generation failed", {
|
|
163
|
+
data: { message: err instanceof Error ? err.message : String(err) },
|
|
164
|
+
});
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=turn-summarizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-summarizer.js","sourceRoot":"","sources":["../../src/turn-summarizer.ts"],"names":[],"mappings":"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"}
|
package/dist/src/workspace.d.ts
CHANGED
|
@@ -61,6 +61,17 @@ export declare class WorkspaceManager {
|
|
|
61
61
|
* `broadcastWorkspaceList` can no-op when a recompute yields identical rows.
|
|
62
62
|
*/
|
|
63
63
|
private lastListJson;
|
|
64
|
+
/**
|
|
65
|
+
* Per-workspace debounce timer coalescing a stop's burst of events into one
|
|
66
|
+
* turn-summary generation (see TURN_SUMMARY_DEBOUNCE_MS).
|
|
67
|
+
*/
|
|
68
|
+
private summaryTimers;
|
|
69
|
+
/**
|
|
70
|
+
* Guard against re-summarizing a turn whose content hasn't changed. Keyed by
|
|
71
|
+
* `${ws}:${turnStartSeq}` → the highest slice `seq` already summarized, so a
|
|
72
|
+
* later stop with no new entries (or the debounce firing twice) is a no-op.
|
|
73
|
+
*/
|
|
74
|
+
private summarizedUpTo;
|
|
64
75
|
constructor(hub: Hub,
|
|
65
76
|
/** Durable per-workspace transcript, so history survives phone reboots. */
|
|
66
77
|
chatlog?: ChatLog,
|
|
@@ -397,6 +408,40 @@ export declare class WorkspaceManager {
|
|
|
397
408
|
mediaStore(): MediaStore;
|
|
398
409
|
/** Ids of all currently-live workspaces (for the media orphan sweep). */
|
|
399
410
|
liveWorkspaceIds(): Set<string>;
|
|
411
|
+
/**
|
|
412
|
+
* A streamed work event (assistant text, a tool call, or an image) means the
|
|
413
|
+
* agent is actively coding *right now*. Backends emit their own `running` only
|
|
414
|
+
* once at spawn and a `done` at end-of-turn, so a work chunk that arrives after
|
|
415
|
+
* a premature or out-of-order `done` would otherwise leave the list/project
|
|
416
|
+
* badge stuck at `done` while the turn is still going — the exact symptom where
|
|
417
|
+
* the working indicator reads "done" mid-work. The client chat spinner already
|
|
418
|
+
* re-arms on these events (its `busySince` timer); this mirrors that on the
|
|
419
|
+
* wire status every list view binds to, so all clients stay accurate from one
|
|
420
|
+
* fix. Only lift the "not working" states (`done`/`idle`); never clobber an
|
|
421
|
+
* open permission gate or a surfaced error (a real work event never precedes
|
|
422
|
+
* those, and stomping them would hide an actionable card).
|
|
423
|
+
*/
|
|
424
|
+
private markWorking;
|
|
400
425
|
private setStatus;
|
|
426
|
+
/**
|
|
427
|
+
* Debounced turn-summary trigger. A single stop can emit several events (a
|
|
428
|
+
* question fires both `ask_question` and `status: awaiting_permission`); this
|
|
429
|
+
* collapses them into one generation and lets same-tick chatlog appends land
|
|
430
|
+
* before we read the transcript.
|
|
431
|
+
*/
|
|
432
|
+
private scheduleTurnSummary;
|
|
433
|
+
/**
|
|
434
|
+
* Generate + persist + broadcast the summary for a turn. Defaults to the latest
|
|
435
|
+
* turn (the one that just stopped); `turnStartSeq` targets a specific turn for
|
|
436
|
+
* on-demand backfill. `force` bypasses the "no new content" guard (used by the
|
|
437
|
+
* explicit backfill path). Best-effort: a null/failed generation is a no-op and
|
|
438
|
+
* leaves the turn shown in full.
|
|
439
|
+
*/
|
|
440
|
+
private runTurnSummary;
|
|
441
|
+
/**
|
|
442
|
+
* On-demand backfill for a specific turn (client `turn.summarize`). Re-broadcasts
|
|
443
|
+
* a cached summary if one exists; otherwise generates one. Idempotent.
|
|
444
|
+
*/
|
|
445
|
+
requestTurnSummary(id: string, turnStartSeq: number): Promise<void>;
|
|
401
446
|
shutdown(): Promise<void>;
|
|
402
447
|
}
|
package/dist/src/workspace.js
CHANGED
|
@@ -32,6 +32,7 @@ import { maybeSyncRepo } from "./repo-sync.js";
|
|
|
32
32
|
import { listPullRequests, prHeadRef, listIssues, createIssue } from "./github.js";
|
|
33
33
|
import { composeActionPrompt } from "./actions.js";
|
|
34
34
|
import { generateChatTitle } from "./titler.js";
|
|
35
|
+
import { buildTurnSlice, generateTurnSummary, hasSummarizableContent, lastUserSeq, sliceContentSeq, } from "./turn-summarizer.js";
|
|
35
36
|
const STATE_FILE = process.env.STATE_FILE ?? join(homedir(), ".immediacy", "state.json");
|
|
36
37
|
/** Root for repo-less chat workspaces' scratch dirs (one per chat). */
|
|
37
38
|
const CHATS_DIR = process.env.CHATS_DIR ?? join(homedir(), ".immediacy", "chats");
|
|
@@ -74,6 +75,17 @@ function modelSwitchNotice(prevModel, nextModel, nextEffort) {
|
|
|
74
75
|
* this only bites on a hang.
|
|
75
76
|
*/
|
|
76
77
|
const NAME_FROM_PROMPT_TIMEOUT_MS = 6000;
|
|
78
|
+
/**
|
|
79
|
+
* Coalesce the burst of stop-like events a single stop produces (e.g. a question
|
|
80
|
+
* emits both `ask_question` and a `status: awaiting_permission`) into one turn
|
|
81
|
+
* summary, and let same-tick chatlog appends land first. Short — this only debounces.
|
|
82
|
+
*/
|
|
83
|
+
const TURN_SUMMARY_DEBOUNCE_MS = 250;
|
|
84
|
+
/**
|
|
85
|
+
* Hard deadline for the turn-summary model call. Fire-and-forget (it never blocks
|
|
86
|
+
* the turn), but bounded so a hung API can't leave a generation dangling.
|
|
87
|
+
*/
|
|
88
|
+
const TURN_SUMMARY_TIMEOUT_MS = 20000;
|
|
77
89
|
export class WorkspaceManager {
|
|
78
90
|
hub;
|
|
79
91
|
chatlog;
|
|
@@ -90,6 +102,17 @@ export class WorkspaceManager {
|
|
|
90
102
|
* `broadcastWorkspaceList` can no-op when a recompute yields identical rows.
|
|
91
103
|
*/
|
|
92
104
|
lastListJson = "";
|
|
105
|
+
/**
|
|
106
|
+
* Per-workspace debounce timer coalescing a stop's burst of events into one
|
|
107
|
+
* turn-summary generation (see TURN_SUMMARY_DEBOUNCE_MS).
|
|
108
|
+
*/
|
|
109
|
+
summaryTimers = new Map();
|
|
110
|
+
/**
|
|
111
|
+
* Guard against re-summarizing a turn whose content hasn't changed. Keyed by
|
|
112
|
+
* `${ws}:${turnStartSeq}` → the highest slice `seq` already summarized, so a
|
|
113
|
+
* later stop with no new entries (or the debounce firing twice) is a no-op.
|
|
114
|
+
*/
|
|
115
|
+
summarizedUpTo = new Map();
|
|
93
116
|
constructor(hub,
|
|
94
117
|
/** Durable per-workspace transcript, so history survives phone reboots. */
|
|
95
118
|
chatlog = new ChatLog(),
|
|
@@ -1235,14 +1258,17 @@ export class WorkspaceManager {
|
|
|
1235
1258
|
const id = live.record.id;
|
|
1236
1259
|
switch (e.kind) {
|
|
1237
1260
|
case "assistant":
|
|
1261
|
+
this.markWorking(live);
|
|
1238
1262
|
this.hub.broadcast("event.assistant_message", id, { ws: id, text: e.text });
|
|
1239
1263
|
void this.chatlog.append(id, { ts: Date.now(), role: "assistant", text: e.text });
|
|
1240
1264
|
break;
|
|
1241
1265
|
case "image":
|
|
1242
1266
|
// Tool-result / assistant-content image — persist + broadcast metadata.
|
|
1267
|
+
this.markWorking(live);
|
|
1243
1268
|
void this.surfaceImage(live, e);
|
|
1244
1269
|
break;
|
|
1245
1270
|
case "tool_use":
|
|
1271
|
+
this.markWorking(live);
|
|
1246
1272
|
this.hub.broadcast("event.tool_use", id, {
|
|
1247
1273
|
ws: id,
|
|
1248
1274
|
tool: e.tool,
|
|
@@ -1301,6 +1327,8 @@ export class WorkspaceManager {
|
|
|
1301
1327
|
requestId: e.requestId,
|
|
1302
1328
|
questions: e.questions,
|
|
1303
1329
|
});
|
|
1330
|
+
// The agent stopped to ask a question — a turn stop; summarize it.
|
|
1331
|
+
this.scheduleTurnSummary(live);
|
|
1304
1332
|
break;
|
|
1305
1333
|
case "session":
|
|
1306
1334
|
// Attribute the id to the backend that minted it, so a later switch back
|
|
@@ -1317,6 +1345,10 @@ export class WorkspaceManager {
|
|
|
1317
1345
|
break;
|
|
1318
1346
|
case "status":
|
|
1319
1347
|
this.setStatus(live, e.status);
|
|
1348
|
+
// The agent paused for a permission — a turn stop. Summarize it for the
|
|
1349
|
+
// condensed view (debounced; the paired `permission` event lands too).
|
|
1350
|
+
if (e.status === "awaiting_permission")
|
|
1351
|
+
this.scheduleTurnSummary(live);
|
|
1320
1352
|
break;
|
|
1321
1353
|
case "done": {
|
|
1322
1354
|
// Surface any screenshots the agent wrote to disk this turn (the
|
|
@@ -1356,6 +1388,8 @@ export class WorkspaceManager {
|
|
|
1356
1388
|
ws: id,
|
|
1357
1389
|
context: e.summary ? { summary: e.summary } : undefined,
|
|
1358
1390
|
});
|
|
1391
|
+
// The turn is done — condense it for the condensed chat view.
|
|
1392
|
+
this.scheduleTurnSummary(live);
|
|
1359
1393
|
break;
|
|
1360
1394
|
}
|
|
1361
1395
|
case "limit":
|
|
@@ -1502,6 +1536,24 @@ export class WorkspaceManager {
|
|
|
1502
1536
|
liveWorkspaceIds() {
|
|
1503
1537
|
return new Set(this.workspaces.keys());
|
|
1504
1538
|
}
|
|
1539
|
+
/**
|
|
1540
|
+
* A streamed work event (assistant text, a tool call, or an image) means the
|
|
1541
|
+
* agent is actively coding *right now*. Backends emit their own `running` only
|
|
1542
|
+
* once at spawn and a `done` at end-of-turn, so a work chunk that arrives after
|
|
1543
|
+
* a premature or out-of-order `done` would otherwise leave the list/project
|
|
1544
|
+
* badge stuck at `done` while the turn is still going — the exact symptom where
|
|
1545
|
+
* the working indicator reads "done" mid-work. The client chat spinner already
|
|
1546
|
+
* re-arms on these events (its `busySince` timer); this mirrors that on the
|
|
1547
|
+
* wire status every list view binds to, so all clients stay accurate from one
|
|
1548
|
+
* fix. Only lift the "not working" states (`done`/`idle`); never clobber an
|
|
1549
|
+
* open permission gate or a surfaced error (a real work event never precedes
|
|
1550
|
+
* those, and stomping them would hide an actionable card).
|
|
1551
|
+
*/
|
|
1552
|
+
markWorking(live) {
|
|
1553
|
+
const s = live.record.status;
|
|
1554
|
+
if (s === "done" || s === "idle")
|
|
1555
|
+
this.setStatus(live, "running");
|
|
1556
|
+
}
|
|
1505
1557
|
setStatus(live, status) {
|
|
1506
1558
|
live.record.status = status;
|
|
1507
1559
|
const id = live.record.id;
|
|
@@ -1512,6 +1564,95 @@ export class WorkspaceManager {
|
|
|
1512
1564
|
status,
|
|
1513
1565
|
});
|
|
1514
1566
|
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Debounced turn-summary trigger. A single stop can emit several events (a
|
|
1569
|
+
* question fires both `ask_question` and `status: awaiting_permission`); this
|
|
1570
|
+
* collapses them into one generation and lets same-tick chatlog appends land
|
|
1571
|
+
* before we read the transcript.
|
|
1572
|
+
*/
|
|
1573
|
+
scheduleTurnSummary(live) {
|
|
1574
|
+
const id = live.record.id;
|
|
1575
|
+
const existing = this.summaryTimers.get(id);
|
|
1576
|
+
if (existing)
|
|
1577
|
+
clearTimeout(existing);
|
|
1578
|
+
const t = setTimeout(() => {
|
|
1579
|
+
this.summaryTimers.delete(id);
|
|
1580
|
+
void this.runTurnSummary(id);
|
|
1581
|
+
}, TURN_SUMMARY_DEBOUNCE_MS);
|
|
1582
|
+
// Don't keep the event loop alive just for a best-effort summary.
|
|
1583
|
+
t.unref?.();
|
|
1584
|
+
this.summaryTimers.set(id, t);
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Generate + persist + broadcast the summary for a turn. Defaults to the latest
|
|
1588
|
+
* turn (the one that just stopped); `turnStartSeq` targets a specific turn for
|
|
1589
|
+
* on-demand backfill. `force` bypasses the "no new content" guard (used by the
|
|
1590
|
+
* explicit backfill path). Best-effort: a null/failed generation is a no-op and
|
|
1591
|
+
* leaves the turn shown in full.
|
|
1592
|
+
*/
|
|
1593
|
+
async runTurnSummary(id, opts = {}) {
|
|
1594
|
+
const entries = this.chatlog.entries(id);
|
|
1595
|
+
const turnStartSeq = opts.turnStartSeq ?? lastUserSeq(entries);
|
|
1596
|
+
if (turnStartSeq === null || turnStartSeq === undefined)
|
|
1597
|
+
return;
|
|
1598
|
+
const slice = buildTurnSlice(entries, turnStartSeq);
|
|
1599
|
+
if (!hasSummarizableContent(slice))
|
|
1600
|
+
return;
|
|
1601
|
+
const key = `${id}:${turnStartSeq}`;
|
|
1602
|
+
const contentSeq = sliceContentSeq(slice);
|
|
1603
|
+
// Skip if this turn was already summarized at the same content watermark
|
|
1604
|
+
// (the debounce firing twice, or a later stop that added nothing new).
|
|
1605
|
+
if (!opts.force && this.summarizedUpTo.get(key) === contentSeq)
|
|
1606
|
+
return;
|
|
1607
|
+
// Claim the watermark before the await so a concurrent trigger doesn't
|
|
1608
|
+
// double-generate; release it on failure so a retry can happen later.
|
|
1609
|
+
const prevWatermark = this.summarizedUpTo.get(key);
|
|
1610
|
+
this.summarizedUpTo.set(key, contentSeq);
|
|
1611
|
+
const sdkEnv = claudeSdkEnv(this.appConfig.get(), this.credentials.getKey("anthropic"));
|
|
1612
|
+
const timeout = new Promise((resolve) => {
|
|
1613
|
+
const t = setTimeout(() => resolve(null), TURN_SUMMARY_TIMEOUT_MS);
|
|
1614
|
+
t.unref?.();
|
|
1615
|
+
});
|
|
1616
|
+
const text = await Promise.race([generateTurnSummary(slice, sdkEnv), timeout]);
|
|
1617
|
+
if (!text) {
|
|
1618
|
+
// Generation failed/timed out — un-claim so a future stop retries.
|
|
1619
|
+
if (this.summarizedUpTo.get(key) === contentSeq) {
|
|
1620
|
+
if (prevWatermark === undefined)
|
|
1621
|
+
this.summarizedUpTo.delete(key);
|
|
1622
|
+
else
|
|
1623
|
+
this.summarizedUpTo.set(key, prevWatermark);
|
|
1624
|
+
}
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
// Append first so the entry gets its `seq`, then broadcast with it.
|
|
1628
|
+
const entry = { ts: Date.now(), role: "summary", text, turnStartSeq };
|
|
1629
|
+
await this.chatlog.append(id, entry);
|
|
1630
|
+
this.hub.broadcast("event.turn_summary", id, {
|
|
1631
|
+
ws: id,
|
|
1632
|
+
turnStartSeq,
|
|
1633
|
+
text,
|
|
1634
|
+
seq: entry.seq,
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* On-demand backfill for a specific turn (client `turn.summarize`). Re-broadcasts
|
|
1639
|
+
* a cached summary if one exists; otherwise generates one. Idempotent.
|
|
1640
|
+
*/
|
|
1641
|
+
async requestTurnSummary(id, turnStartSeq) {
|
|
1642
|
+
const existing = this.chatlog
|
|
1643
|
+
.entries(id)
|
|
1644
|
+
.find((e) => e.role === "summary" && e.turnStartSeq === turnStartSeq);
|
|
1645
|
+
if (existing) {
|
|
1646
|
+
this.hub.broadcast("event.turn_summary", id, {
|
|
1647
|
+
ws: id,
|
|
1648
|
+
turnStartSeq,
|
|
1649
|
+
text: existing.text,
|
|
1650
|
+
seq: existing.seq,
|
|
1651
|
+
});
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
await this.runTurnSummary(id, { turnStartSeq, force: true });
|
|
1655
|
+
}
|
|
1515
1656
|
async shutdown() {
|
|
1516
1657
|
for (const live of this.workspaces.values()) {
|
|
1517
1658
|
await live.agent?.stop();
|