@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.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/CHANGELOG.md +38 -17
- package/dist/application/task-lifecycle/advance.js +254 -4
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/shell-executor.js +4 -91
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +72 -42
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +180 -16
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/session-store.js +39 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/operator-actions.js +559 -68
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-CMHovlqG.js +32 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +34 -46
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
|
@@ -4,6 +4,56 @@ import { hydrateSessionThread } from "../../chat/turn-process.js";
|
|
|
4
4
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
5
5
|
import { CHAT_SESSION_LIST_LIMIT } from "../chat-view-types.js";
|
|
6
6
|
import { confirmationToken } from "./format.js";
|
|
7
|
+
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
8
|
+
export async function createSessionAndPrimeTitleWatcher(input) {
|
|
9
|
+
if (input.creatingRef?.current) {
|
|
10
|
+
throw new Error("CREATE_SESSION_IN_FLIGHT");
|
|
11
|
+
}
|
|
12
|
+
if (input.creatingRef)
|
|
13
|
+
input.creatingRef.current = true;
|
|
14
|
+
// Synchronous busy feedback: callers set creatingSession in the same render
|
|
15
|
+
// turn via setCreatingSession before the network await below.
|
|
16
|
+
input.setCreatingSession?.(true);
|
|
17
|
+
try {
|
|
18
|
+
const res = await (input.fetchFn ?? fetch)(`${input.origin}/api/operator/v1/chat/sessions`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
23
|
+
},
|
|
24
|
+
credentials: "include",
|
|
25
|
+
body: JSON.stringify(input.options ?? {}),
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const body = (await res.json().catch(() => ({})));
|
|
29
|
+
throw new Error(body.error?.message ?? `HTTP ${res.status}`);
|
|
30
|
+
}
|
|
31
|
+
const body = (await res.json());
|
|
32
|
+
if (!body.session)
|
|
33
|
+
throw new Error("创建会话响应缺少 durable session");
|
|
34
|
+
const durable = body.session;
|
|
35
|
+
const created = {
|
|
36
|
+
sessionId: durable.sessionId,
|
|
37
|
+
...runtimeSelectionFromRecord(durable),
|
|
38
|
+
};
|
|
39
|
+
const nextList = [
|
|
40
|
+
durable,
|
|
41
|
+
...input.sessionListRef.current.filter((item) => item.sessionId !== durable.sessionId),
|
|
42
|
+
].slice(0, CHAT_SESSION_LIST_LIMIT);
|
|
43
|
+
// Commit ref → state → eligibility → deferred refresh. The ref assignment
|
|
44
|
+
// makes the durable record observable before React's state commit.
|
|
45
|
+
input.sessionListRef.current = nextList;
|
|
46
|
+
input.setSessionList(nextList);
|
|
47
|
+
input.titleWatcher?.markEligible(durable.sessionId);
|
|
48
|
+
void input.refreshSessionList();
|
|
49
|
+
return created;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (input.creatingRef)
|
|
53
|
+
input.creatingRef.current = false;
|
|
54
|
+
input.setCreatingSession?.(false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
7
57
|
/** Session lifecycle: list / activate / create / delete / rename-archive /
|
|
8
58
|
* compact, plus browser-refresh recovery from the durable session store. */
|
|
9
59
|
export function useChatSessions(params) {
|
|
@@ -12,17 +62,34 @@ export function useChatSessions(params) {
|
|
|
12
62
|
const { setInput, setPendingImages } = composer;
|
|
13
63
|
const { setPreviews, setActivePreviewPath } = browser;
|
|
14
64
|
const [sessionList, setSessionList] = useState([]);
|
|
65
|
+
const sessionListRef = useRef([]);
|
|
66
|
+
sessionListRef.current = sessionList;
|
|
67
|
+
const titleWatcherRef = useRef(null);
|
|
68
|
+
// Shared busy gate for sidebar + Landing create entries (one render feedback).
|
|
69
|
+
const [creatingSession, setCreatingSession] = useState(false);
|
|
70
|
+
const creatingSessionRef = useRef(false);
|
|
15
71
|
// Only the latest activate/create/restore intent may commit active-session
|
|
16
72
|
// state. This prevents slow responses from an older selection overwriting a
|
|
17
73
|
// newer user choice.
|
|
18
74
|
const sessionSelectionVersionRef = useRef(0);
|
|
19
75
|
const resetActiveProjection = useCallback(() => {
|
|
76
|
+
// M4.5/M5.2: selection is a hard ownership boundary. Abort every request
|
|
77
|
+
// owned by the old projection, then invalidate its async callbacks.
|
|
78
|
+
refs.sessionGenerationRef.current += 1;
|
|
79
|
+
refs.abortRef.current?.abort();
|
|
80
|
+
refs.reconnectAbortRef.current?.abort();
|
|
81
|
+
for (const controller of refs.sessionAbortControllersRef.current.values())
|
|
82
|
+
controller.abort();
|
|
83
|
+
refs.sessionAbortControllersRef.current.clear();
|
|
84
|
+
refs.reconnectAbortRef.current = null;
|
|
85
|
+
refs.eventsStreamStateRef.current = null;
|
|
20
86
|
refs.streamingRef.current = false;
|
|
21
87
|
refs.abortRef.current = null;
|
|
22
88
|
refs.assistantIdRef.current = null;
|
|
23
89
|
refs.lastEventIdRef.current = null;
|
|
24
90
|
refs.seenEventIdsRef.current = new Set();
|
|
25
91
|
refs.turnAssistantIdsRef.current = new Map();
|
|
92
|
+
refs.settledTurnIdsRef.current = new Set();
|
|
26
93
|
refs.activeTurnIdRef.current = null;
|
|
27
94
|
resetForNewSession();
|
|
28
95
|
setInput("");
|
|
@@ -91,6 +158,7 @@ export function useChatSessions(params) {
|
|
|
91
158
|
setError("删除会话失败");
|
|
92
159
|
return;
|
|
93
160
|
}
|
|
161
|
+
titleWatcherRef.current?.cancel(sessionId);
|
|
94
162
|
if (refs.sessionRef.current?.sessionId === sessionId) {
|
|
95
163
|
sessionSelectionVersionRef.current += 1;
|
|
96
164
|
refs.sessionRef.current = null;
|
|
@@ -124,11 +192,13 @@ export function useChatSessions(params) {
|
|
|
124
192
|
sessionId: body.session.sessionId,
|
|
125
193
|
...runtimeSelectionFromRecord(body.session),
|
|
126
194
|
};
|
|
195
|
+
resetActiveProjection();
|
|
127
196
|
refs.sessionRef.current = next;
|
|
128
197
|
refs.streamingRef.current = Boolean(body.session.activeTurnId);
|
|
129
198
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
130
199
|
refs.seenEventIdsRef.current = new Set();
|
|
131
200
|
refs.turnAssistantIdsRef.current = new Map();
|
|
201
|
+
refs.settledTurnIdsRef.current = new Set();
|
|
132
202
|
refs.lastEventIdRef.current = null;
|
|
133
203
|
setSession(next);
|
|
134
204
|
refs.assistantIdRef.current = null;
|
|
@@ -159,12 +229,39 @@ export function useChatSessions(params) {
|
|
|
159
229
|
setPreviews,
|
|
160
230
|
setInput,
|
|
161
231
|
applyActivatedSession,
|
|
232
|
+
resetActiveProjection,
|
|
162
233
|
setActivePreviewPath,
|
|
163
234
|
setError,
|
|
164
235
|
]);
|
|
165
236
|
useEffect(() => {
|
|
166
237
|
void refreshSessionList();
|
|
167
238
|
}, [refreshSessionList]);
|
|
239
|
+
if (!titleWatcherRef.current) {
|
|
240
|
+
titleWatcherRef.current = new SessionTitleWatcher({
|
|
241
|
+
origin,
|
|
242
|
+
hasSession: (sessionId) => sessionListRef.current.some((item) => item.sessionId === sessionId),
|
|
243
|
+
onTitle: (sessionId, event) => {
|
|
244
|
+
setSessionList((current) => current.map((item) => {
|
|
245
|
+
if (item.sessionId !== sessionId)
|
|
246
|
+
return item;
|
|
247
|
+
// A manually assigned title is authoritative over late automatic output.
|
|
248
|
+
if (event.automatic && item.title)
|
|
249
|
+
return item;
|
|
250
|
+
return event.automatic
|
|
251
|
+
? { ...item, title: event.title }
|
|
252
|
+
: { ...item, displayTitle: event.title };
|
|
253
|
+
}));
|
|
254
|
+
},
|
|
255
|
+
refresh: refreshSessionList,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const watchSessionTitle = useCallback((sessionId) => titleWatcherRef.current?.startIfEligible(sessionId), []);
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
titleWatcherRef.current?.reconcile(sessionList
|
|
261
|
+
.filter((item) => item.state !== "archived" && !item.title)
|
|
262
|
+
.map((item) => item.sessionId));
|
|
263
|
+
}, [sessionList]);
|
|
264
|
+
useEffect(() => () => titleWatcherRef.current?.dispose(), []);
|
|
168
265
|
// Browser refresh recovery: retain only the durable session id locally, then
|
|
169
266
|
// re-read messages and operation refs/facts from the server-owned stores.
|
|
170
267
|
useEffect(() => {
|
|
@@ -189,6 +286,7 @@ export function useChatSessions(params) {
|
|
|
189
286
|
sessionId: body.session.sessionId,
|
|
190
287
|
...runtimeSelectionFromRecord(body.session),
|
|
191
288
|
};
|
|
289
|
+
refs.sessionGenerationRef.current += 1;
|
|
192
290
|
refs.sessionRef.current = restored;
|
|
193
291
|
// Do not seed Last-Event-ID here: session messages are restored
|
|
194
292
|
// from the durable store, but tool/gate/operation projections live
|
|
@@ -197,6 +295,7 @@ export function useChatSessions(params) {
|
|
|
197
295
|
refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
|
|
198
296
|
refs.seenEventIdsRef.current = new Set();
|
|
199
297
|
refs.turnAssistantIdsRef.current = new Map();
|
|
298
|
+
refs.settledTurnIdsRef.current = new Set();
|
|
200
299
|
refs.lastEventIdRef.current = null;
|
|
201
300
|
setSession(restored);
|
|
202
301
|
setInput(body.session.composerDraft ?? "");
|
|
@@ -217,29 +316,26 @@ export function useChatSessions(params) {
|
|
|
217
316
|
cancelled = true;
|
|
218
317
|
};
|
|
219
318
|
}, [origin, refs, setSession, setInput, setMessages]);
|
|
319
|
+
// The sole production create path delegates to the exported orchestration
|
|
320
|
+
// helper so its durable commit order is directly traceable and testable.
|
|
321
|
+
const createSessionAndPrimeTitleWatcherForHook = useCallback((options) => createSessionAndPrimeTitleWatcher({
|
|
322
|
+
origin,
|
|
323
|
+
sessionListRef,
|
|
324
|
+
setSessionList,
|
|
325
|
+
titleWatcher: titleWatcherRef.current,
|
|
326
|
+
refreshSessionList,
|
|
327
|
+
options,
|
|
328
|
+
setCreatingSession,
|
|
329
|
+
creatingRef: creatingSessionRef,
|
|
330
|
+
}), [origin, refreshSessionList]);
|
|
220
331
|
const createSession = useCallback(async (options) => {
|
|
332
|
+
// Suppress concurrent create from any entry (sidebar / Landing / chip).
|
|
333
|
+
if (creatingSessionRef.current)
|
|
334
|
+
return null;
|
|
221
335
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
222
336
|
setError(null);
|
|
223
337
|
try {
|
|
224
|
-
const
|
|
225
|
-
method: "POST",
|
|
226
|
-
headers: {
|
|
227
|
-
"Content-Type": "application/json",
|
|
228
|
-
"x-loop-console-confirmation": confirmationToken(),
|
|
229
|
-
},
|
|
230
|
-
credentials: "include",
|
|
231
|
-
body: JSON.stringify(options ?? {}),
|
|
232
|
-
});
|
|
233
|
-
if (!res.ok) {
|
|
234
|
-
const body = (await res.json().catch(() => ({})));
|
|
235
|
-
throw new Error(body.error?.message ?? `HTTP ${res.status}`);
|
|
236
|
-
}
|
|
237
|
-
const body = (await res.json());
|
|
238
|
-
const created = {
|
|
239
|
-
sessionId: body.sessionId,
|
|
240
|
-
...(body.model ? { model: body.model } : {}),
|
|
241
|
-
};
|
|
242
|
-
void refreshSessionList();
|
|
338
|
+
const created = await createSessionAndPrimeTitleWatcherForHook(options);
|
|
243
339
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
244
340
|
return null;
|
|
245
341
|
resetActiveProjection();
|
|
@@ -254,16 +350,18 @@ export function useChatSessions(params) {
|
|
|
254
350
|
catch (e) {
|
|
255
351
|
if (selectionVersion !== sessionSelectionVersionRef.current)
|
|
256
352
|
return null;
|
|
257
|
-
|
|
353
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
354
|
+
if (message !== "CREATE_SESSION_IN_FLIGHT") {
|
|
355
|
+
setError(message);
|
|
356
|
+
}
|
|
258
357
|
return null;
|
|
259
358
|
}
|
|
260
359
|
}, [
|
|
261
|
-
origin,
|
|
262
360
|
refs,
|
|
263
361
|
setSession,
|
|
264
362
|
setError,
|
|
265
363
|
resetActiveProjection,
|
|
266
|
-
|
|
364
|
+
createSessionAndPrimeTitleWatcherForHook,
|
|
267
365
|
]);
|
|
268
366
|
// Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
|
|
269
367
|
const compactSession = useCallback(async () => {
|
|
@@ -302,6 +400,9 @@ export function useChatSessions(params) {
|
|
|
302
400
|
return;
|
|
303
401
|
}
|
|
304
402
|
await refreshSessionList();
|
|
403
|
+
if (patch.state === "archived") {
|
|
404
|
+
titleWatcherRef.current?.cancel(sessionId);
|
|
405
|
+
}
|
|
305
406
|
if (patch.state === "archived" &&
|
|
306
407
|
refs.sessionRef.current?.sessionId === sessionId) {
|
|
307
408
|
sessionSelectionVersionRef.current += 1;
|
|
@@ -313,11 +414,13 @@ export function useChatSessions(params) {
|
|
|
313
414
|
};
|
|
314
415
|
return {
|
|
315
416
|
sessionList,
|
|
417
|
+
creatingSession,
|
|
316
418
|
activateSession,
|
|
317
419
|
refreshSessionList,
|
|
318
420
|
deleteSession,
|
|
319
421
|
createSession,
|
|
320
422
|
updateSession,
|
|
423
|
+
watchSessionTitle,
|
|
321
424
|
compactSession,
|
|
322
425
|
};
|
|
323
426
|
}
|