thursday-agent 0.4.0 → 0.5.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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/artifact/route.js +4 -4
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js +3 -3
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/task/[id]/route.js +8 -8
- package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/task/route.js +6 -6
- package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js +4 -4
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js +2 -2
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js +2 -2
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js +2 -2
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js +2 -2
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js +11 -11
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js +6 -6
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/skills/route.js +6 -6
- package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js +8 -8
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/route.js +3 -2
- package/.next/server/app/api/workspace/route.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
- package/.next/server/app/artifact/[...path]/page.js +2 -2
- package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/page/server-reference-manifest.json +40 -40
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__026u77e._.js +5 -0
- package/.next/server/chunks/{[root-of-the-server]__02xjoto._.js → [root-of-the-server]__050zri4._.js} +1 -1
- package/.next/server/chunks/[root-of-the-server]__0bg9710._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0in-d5s._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0pa9k-f._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0qi_m0e._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0tq0m_k._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0xep6j_._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__144l7qt._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1jg8t86._.js +8 -0
- package/.next/server/chunks/{[root-of-the-server]__12cabg9._.js → [root-of-the-server]__1lldzy9._.js} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__01dkp4a._.js → [root-of-the-server]__1peqvsi._.js} +1 -1
- package/.next/server/chunks/[root-of-the-server]__1w8gm5h._.js +10 -0
- package/.next/server/chunks/[root-of-the-server]__1xt39nn._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__1zb8mfo._.js +8 -0
- package/.next/server/chunks/_0-fjql4._.js +11 -6
- package/.next/server/chunks/_00xh_q0._.js +1 -1
- package/.next/server/chunks/{_1_9mjx-._.js → _091bi8d._.js} +1 -1
- package/.next/server/chunks/_0fzqt2y._.js +29 -21
- package/.next/server/chunks/{_0u4aiy2._.js → _0lr92vd._.js} +1 -1
- package/.next/server/chunks/_0mecv0f._.js +11 -6
- package/.next/server/chunks/{_0sbynis._.js → _0si7xzn._.js} +1 -1
- package/.next/server/chunks/_1230wbx._.js +5 -5
- package/.next/server/chunks/{_0obojvv._.js → _18d7-5p._.js} +1 -1
- package/.next/server/chunks/_1c78vw5._.js +33 -25
- package/.next/server/chunks/_1d0j961._.js +2 -2
- package/.next/server/chunks/_1krd9rf._.js +1 -1
- package/.next/server/chunks/{_03a0897._.js → _1zkfbaa._.js} +1 -1
- package/.next/server/chunks/features_1qf3cei._.js +6 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js → node_modules_consola_dist_chunks_prompt_mjs_17k8irj._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js → node_modules_consola_dist_chunks_prompt_mjs_1jidylq._.js} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0-nob52._.js +4 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__202p40z._.js → [root-of-the-server]__0s4wb56._.js} +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__0xsog8n._.js → [root-of-the-server]__0sd3izg._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__1gtlw6v._.js → [root-of-the-server]__1-13tcn._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__0s9l2sv._.js → [root-of-the-server]__1zidpfl._.js} +1 -1
- package/.next/server/chunks/ssr/_001_ms1._.js +6 -1
- package/.next/server/chunks/ssr/{_0gb4gmf._.js → _04z2c7b._.js} +2 -2
- package/.next/server/chunks/ssr/{_1nnsk7z._.js → _06tssji._.js} +2 -2
- package/.next/server/chunks/ssr/_0fk4c7d._.js +3 -0
- package/.next/server/chunks/ssr/_0i_6q1w._.js +1 -1
- package/.next/server/chunks/ssr/{_1h0rw11._.js → _0m2lsd1._.js} +2 -2
- package/.next/server/chunks/ssr/_0mggveb._.js +3 -0
- package/.next/server/chunks/ssr/{_1jhd09t._.js → _0nbpc_8._.js} +2 -2
- package/.next/server/chunks/ssr/{_1fojd41._.js → _0rzucul._.js} +3 -3
- package/.next/server/chunks/ssr/_0s99bgb._.js +1 -1
- package/.next/server/chunks/ssr/_1-teee5._.js +4 -4
- package/.next/server/chunks/ssr/{_0fz4mf_._.js → _11j92tu._.js} +2 -2
- package/.next/server/chunks/ssr/{_1b2mjwr._.js → _1ebxla1._.js} +2 -2
- package/.next/server/chunks/ssr/{_1gd8q8l._.js → _1jdgjv9._.js} +2 -2
- package/.next/server/chunks/ssr/_1lz_cq2._.js +19 -11
- package/.next/server/chunks/ssr/{_0ilna52._.js → _1nj4niv._.js} +2 -2
- package/.next/server/chunks/ssr/_1ovmc9s._.js +3 -0
- package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/{node_modules_0pvd7q-._.js → node_modules_0-dr_o7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1_pc9kq._.js → node_modules_04sksn9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1vh5ntv._.js → node_modules_05-n6dz._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0b__sgl._.js → node_modules_056qgf-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0g7fzk3._.js → node_modules_07dsgkx._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_04jf_m8._.js → node_modules_0bvopy2._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0arus10._.js → node_modules_0fkeku3._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_14om4ar._.js → node_modules_0ifp389._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0cxaxk9._.js → node_modules_0iur59s._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1aaxuef._.js → node_modules_0m383r2._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1qpjvy5._.js → node_modules_0u9da1h._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1cx_ql6._.js → node_modules_0zeg3x_._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0_-_a--._.js → node_modules_1-y7nni._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_08dhkiq._.js → node_modules_16mkxuo._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_198nmll._.js → node_modules_1a0kmjl._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1efrqxi._.js → node_modules_1chvq5f._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0yugy6m._.js → node_modules_1j8j4pt._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_09q35v_._.js → node_modules_1k6n600._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1cz2hle._.js → node_modules_1k7z2j6._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1gdh31p._.js → node_modules_1k8j8n0._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1eg7m6c._.js → node_modules_1lz0oxc._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0940ns5._.js → node_modules_1t77e4n._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_00etblx._.js → node_modules_1uyk91w._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0had7xd._.js → node_modules_1x0ztf4._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_14q6g4n._.js → node_modules_1y79i83._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1hpopmo._.js → node_modules_20c28js._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1b748ic._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0bkwd0u._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1z4hm5e._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0p9a9su._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_06mk8id._.js → node_modules_@vercel_oidc_dist_1o553h-._.js} +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +41 -41
- package/.next/static/chunks/{39j0fvpybzabl.js → 0n3jle9kbqfr2.js} +2 -2
- package/.next/static/chunks/{0fl3gwe4-l6tz.js → 11r2yp_yh9_qn.js} +1 -1
- package/.next/static/chunks/{1jd09sxcjddu1.js → 126fj4pga_72k.js} +1 -1
- package/.next/static/chunks/{0gnqdjn2lvdad.js → 159udiexf6dom.js} +1 -1
- package/.next/static/chunks/{08qe4as2q428z.js → 18gi7o2ibrgh8.js} +1 -1
- package/.next/static/chunks/{2u7xt6mxg44lf.js → 1q9z1tohil7xn.js} +1 -1
- package/.next/static/chunks/{3zmcsnuo25zvq.js → 1sjgfets224re.js} +1 -1
- package/.next/static/chunks/{0wcprt5ot5she.js → 1wszsr83t01gh.js} +1 -1
- package/.next/static/chunks/{0w52rrrmuj9r9.js → 2szintyibu0s5.js} +1 -1
- package/.next/static/chunks/{0hwap1-p0m5sa.js → 3-b6b7entqazs.js} +1 -1
- package/.next/static/chunks/{17p6tk6k88dq6.js → 3dmbhsv2u_8kj.js} +1 -1
- package/.next/static/chunks/{214ya3n5gxeih.js → 3i0a5px-oufgc.js} +1 -1
- package/.next/static/chunks/{1w73ece_6bheu.js → 3wexk9qo8zuj3.js} +3 -3
- package/.next/static/chunks/3xre8bu_pk1p_.js +1 -0
- package/.next/static/chunks/{3woj-xd7ki5_i.js → 3zv6t3i77xhr_.js} +1 -1
- package/.next/static/chunks/{3akmqm77muj4-.js → 408ueu4hfvcg7.js} +1 -1
- package/.next/static/chunks/{15m575979un5f.js → 435atsnhs7ksu.js} +6 -6
- package/bin/thursday.mjs +0 -0
- package/features/ai/prompts/bot.prompt.ts +1 -1
- package/features/ai/tools/bot.tool.ts +3 -3
- package/features/ai/tools/skills.tool.ts +25 -8
- package/features/bot/bot.run.ts +184 -73
- package/features/bot/bot.runner.ts +48 -23
- package/features/bot/task.query.ts +107 -4
- package/features/bot/task.store.ts +3 -5
- package/features/workspace/workspace.ts +27 -20
- package/package.json +1 -1
- package/skills/browser/SKILL.md +6 -5
- package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +0 -10
- package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +0 -8
- package/.next/server/chunks/_1zjrdag._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +0 -4
- package/.next/server/chunks/ssr/_0n3toaw._.js +0 -3
- package/.next/server/chunks/ssr/_1-_etts._.js +0 -3
- package/.next/server/chunks/ssr/_1f-dg-6._.js +0 -3
- package/.next/static/chunks/00rd4-v8r7axp.js +0 -1
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → G6-gNoyGDEcgAsq-L4PE1}/_buildManifest.js +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → G6-gNoyGDEcgAsq-L4PE1}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → G6-gNoyGDEcgAsq-L4PE1}/_ssgManifest.js +0 -0
package/bin/thursday.mjs
CHANGED
|
File without changes
|
|
@@ -232,7 +232,7 @@ function methods(skills: SkillMetadata[]): string {
|
|
|
232
232
|
|
|
233
233
|
return `## Skills
|
|
234
234
|
|
|
235
|
-
Written-down methods. When the job
|
|
235
|
+
Written-down methods. Use the instructions already in your conversation. When the job needs a skill whose full instructions are absent, read it with \`${TOOL_NAMES.load_skill}\` before starting.
|
|
236
236
|
|
|
237
237
|
${skillLines(skills)}`;
|
|
238
238
|
}
|
|
@@ -34,12 +34,12 @@ export const delegateSpec = {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* `context` carries what the asking bot found, since its thread is not handed over (bot.run).
|
|
37
|
-
*
|
|
37
|
+
* A returning participant keeps its own thread; `context` adds only what this caller knows.
|
|
38
38
|
*/
|
|
39
39
|
export const askBotSpec = {
|
|
40
40
|
name: TOOL_NAMES.ask_bot,
|
|
41
41
|
description:
|
|
42
|
-
"
|
|
42
|
+
"Send a request to another bot on this job and wait for their reply. The same bot continues its own work and conversation whenever you ask it again. The job stays yours.",
|
|
43
43
|
parameters: z.object({
|
|
44
44
|
bot: z.string().describe("A name from the bot list."),
|
|
45
45
|
request: z
|
|
@@ -50,7 +50,7 @@ export const askBotSpec = {
|
|
|
50
50
|
context: z
|
|
51
51
|
.string()
|
|
52
52
|
.describe(
|
|
53
|
-
"
|
|
53
|
+
"Pass on what you know that they do not: exact values and paths they need. They keep their own previous work and your exchanges, but cannot read your private thread or tool output.",
|
|
54
54
|
),
|
|
55
55
|
}),
|
|
56
56
|
};
|
|
@@ -6,9 +6,8 @@ import { splitFrontmatter } from "@/features/skills/skills.query";
|
|
|
6
6
|
import type { Sandbox } from "@/lib/sandbox";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* The tool set is built per run, so the `loaded` set lives in this closure.
|
|
9
|
+
* Reuse instructions only while their full text remains in this bot's context.
|
|
10
|
+
* A resume can carry them; compaction can remove them.
|
|
12
11
|
*/
|
|
13
12
|
export const createSkillTools = ({
|
|
14
13
|
skills,
|
|
@@ -17,7 +16,6 @@ export const createSkillTools = ({
|
|
|
17
16
|
skills: SkillMetadata[];
|
|
18
17
|
sandbox: Sandbox;
|
|
19
18
|
}) => {
|
|
20
|
-
const loaded = new Set<string>();
|
|
21
19
|
return {
|
|
22
20
|
[TOOL_NAMES.load_skill]: tool({
|
|
23
21
|
description:
|
|
@@ -29,7 +27,7 @@ export const createSkillTools = ({
|
|
|
29
27
|
"Exact name from the Skills list. Do not guess names that are not on it.",
|
|
30
28
|
),
|
|
31
29
|
}),
|
|
32
|
-
execute: async ({ name }) => {
|
|
30
|
+
execute: async ({ name }, { messages }) => {
|
|
33
31
|
const skill = skills.find(
|
|
34
32
|
(s) => s.name.toLowerCase() === name.toLowerCase(),
|
|
35
33
|
);
|
|
@@ -41,10 +39,30 @@ export const createSkillTools = ({
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
const skillFile = `${skill.path}/SKILL.md`;
|
|
44
|
-
|
|
42
|
+
const carried = messages.some(
|
|
43
|
+
(message) =>
|
|
44
|
+
Array.isArray(message.content) &&
|
|
45
|
+
message.content.some((part) => {
|
|
46
|
+
if (
|
|
47
|
+
part.type !== "tool-result" ||
|
|
48
|
+
part.toolName !== TOOL_NAMES.load_skill ||
|
|
49
|
+
part.output.type !== "json"
|
|
50
|
+
)
|
|
51
|
+
return false;
|
|
52
|
+
const value = part.output.value as {
|
|
53
|
+
skillDirectory?: string;
|
|
54
|
+
content?: string;
|
|
55
|
+
} | null;
|
|
56
|
+
return (
|
|
57
|
+
value?.skillDirectory === skill.path &&
|
|
58
|
+
typeof value.content === "string"
|
|
59
|
+
);
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
if (carried) {
|
|
45
63
|
return {
|
|
46
64
|
skillDirectory: skill.path,
|
|
47
|
-
note: `
|
|
65
|
+
note: `Use the full '${skill.name}' instructions already in your conversation. To check for changes on disk, read ${skillFile} with the shell.`,
|
|
48
66
|
};
|
|
49
67
|
}
|
|
50
68
|
|
|
@@ -55,7 +73,6 @@ export const createSkillTools = ({
|
|
|
55
73
|
path: skill.path,
|
|
56
74
|
limit: 50,
|
|
57
75
|
});
|
|
58
|
-
loaded.add(skill.path);
|
|
59
76
|
|
|
60
77
|
return {
|
|
61
78
|
skillDirectory: skill.path,
|
package/features/bot/bot.run.ts
CHANGED
|
@@ -41,17 +41,21 @@ import {
|
|
|
41
41
|
type TokenUsage,
|
|
42
42
|
} from "@/features/bot/bot.schema";
|
|
43
43
|
import {
|
|
44
|
-
|
|
44
|
+
botBrowserSession,
|
|
45
45
|
filesOnDisk,
|
|
46
46
|
openBotFolder,
|
|
47
47
|
openJobScratch,
|
|
48
48
|
} from "@/features/workspace/workspace";
|
|
49
49
|
import { logger } from "@/lib/logger";
|
|
50
50
|
import { publicError } from "@/lib/public-error";
|
|
51
|
+
import { createKeyedLock } from "@/lib/queue";
|
|
51
52
|
import { estimateTokens } from "@/lib/tokens";
|
|
53
|
+
import { PromiseChain } from "@/lib/utils";
|
|
52
54
|
import { findJobBot } from "./bot.query";
|
|
53
55
|
import {
|
|
54
56
|
findTask,
|
|
57
|
+
listBorrowedTails,
|
|
58
|
+
listBotThread,
|
|
55
59
|
listWrittenPaths,
|
|
56
60
|
optionsOf,
|
|
57
61
|
writtenPathsIn,
|
|
@@ -65,6 +69,11 @@ import {
|
|
|
65
69
|
|
|
66
70
|
/** One thing that happened in a run. The runner adds which bot and inside which `ask_bot` call. */
|
|
67
71
|
export type BotEvent =
|
|
72
|
+
/** A request enters this participant's own thread before its model runs. */
|
|
73
|
+
| {
|
|
74
|
+
type: "request";
|
|
75
|
+
content: Extract<ModelMessage, { role: "user" }>["content"];
|
|
76
|
+
}
|
|
68
77
|
/** A finished chunk of prose. */
|
|
69
78
|
| { type: "text"; text: string }
|
|
70
79
|
/** A tool call. `id` is where the result lands. */
|
|
@@ -147,6 +156,8 @@ export type RunOptions = {
|
|
|
147
156
|
* in one window.
|
|
148
157
|
*/
|
|
149
158
|
session?: string | null;
|
|
159
|
+
/** Shared by every participant in this live segment; history itself lives in the DB. */
|
|
160
|
+
desks?: ReturnType<typeof createBotDesks>;
|
|
150
161
|
/**
|
|
151
162
|
* Drained once per step boundary; each string goes in front of the model as
|
|
152
163
|
* a user turn before the next step. Only the job's own bot has one.
|
|
@@ -189,11 +200,11 @@ export async function runBot(
|
|
|
189
200
|
): Promise<void> {
|
|
190
201
|
const depth = options.depth ?? 0;
|
|
191
202
|
const parent = options.parent ?? null;
|
|
192
|
-
const
|
|
203
|
+
const bot = await findJobBot(input.bot.trim());
|
|
204
|
+
const name = bot?.name ?? input.bot.trim();
|
|
193
205
|
const emit = (event: BotEvent) =>
|
|
194
206
|
options.emit({ ...event, bot: name, parent });
|
|
195
207
|
|
|
196
|
-
const bot = await findJobBot(name);
|
|
197
208
|
if (!bot) {
|
|
198
209
|
await emit({
|
|
199
210
|
type: "error",
|
|
@@ -205,7 +216,11 @@ export async function runBot(
|
|
|
205
216
|
// Switched off is not a bot to pick — the backstop for every fresh start
|
|
206
217
|
// (`delegate`, `ask_bot`, the screen). A resume carries `messages` instead and
|
|
207
218
|
// is let through: its thread already exists and the user can still answer it.
|
|
208
|
-
|
|
219
|
+
const previous =
|
|
220
|
+
"askedBy" in input && options.taskId
|
|
221
|
+
? await listBotThread(options.taskId, name)
|
|
222
|
+
: [];
|
|
223
|
+
if (bot.disabled && "request" in input && !previous.length) {
|
|
209
224
|
await emit({
|
|
210
225
|
type: "error",
|
|
211
226
|
message: `${bot.name} is switched off. Use a name from the list.`,
|
|
@@ -214,6 +229,30 @@ export async function runBot(
|
|
|
214
229
|
return;
|
|
215
230
|
}
|
|
216
231
|
|
|
232
|
+
options = { ...options, desks: options.desks ?? createBotDesks() };
|
|
233
|
+
let history: ModelMessage[];
|
|
234
|
+
if ("messages" in input) {
|
|
235
|
+
history = input.messages;
|
|
236
|
+
} else {
|
|
237
|
+
const content = previous.length
|
|
238
|
+
? `## ${input.askedBy} → ${name}: the next request\n\n${input.request.trim()}${input.context?.trim() ? `\n\n## What ${input.askedBy} passes on\n\n${input.context.trim()}` : ""}`
|
|
239
|
+
: buildHandoff({
|
|
240
|
+
chain: input.chain,
|
|
241
|
+
bots: [...input.above, name],
|
|
242
|
+
did: input.context ?? null,
|
|
243
|
+
part: input.request,
|
|
244
|
+
});
|
|
245
|
+
history = [
|
|
246
|
+
...resumeThread(
|
|
247
|
+
previous,
|
|
248
|
+
options.taskId ? await listBorrowedTails(options.taskId) : undefined,
|
|
249
|
+
),
|
|
250
|
+
{ role: "user", content },
|
|
251
|
+
];
|
|
252
|
+
options.signal?.throwIfAborted();
|
|
253
|
+
await emit({ type: "request", content });
|
|
254
|
+
}
|
|
255
|
+
|
|
217
256
|
// Tools are built on the model: web search runs on this bot's model (load-tools).
|
|
218
257
|
const model = await resolveModel(bot);
|
|
219
258
|
// The job's row: the same one for every seat on the job, a borrowed bot's too
|
|
@@ -254,21 +293,6 @@ export async function runBot(
|
|
|
254
293
|
("askedBy" in input ? 0 : row?.contextBudget) || Number.POSITIVE_INFINITY,
|
|
255
294
|
);
|
|
256
295
|
|
|
257
|
-
const history: ModelMessage[] =
|
|
258
|
-
"messages" in input
|
|
259
|
-
? input.messages
|
|
260
|
-
: [
|
|
261
|
-
{
|
|
262
|
-
role: "user",
|
|
263
|
-
content: buildHandoff({
|
|
264
|
-
chain: input.chain,
|
|
265
|
-
bots: [...input.above, name],
|
|
266
|
-
did: input.context ?? null,
|
|
267
|
-
part: input.request,
|
|
268
|
-
}),
|
|
269
|
-
},
|
|
270
|
-
];
|
|
271
|
-
|
|
272
296
|
/** Remaining `ask_back` calls for a borrowed bot; the tool counts it down. */
|
|
273
297
|
const asks = { left: BOT_RUN.askBack };
|
|
274
298
|
// A borrowed bot asks back instead of asking Thursday. Any seat above the
|
|
@@ -879,15 +903,20 @@ export async function runBotToText(
|
|
|
879
903
|
await runBot(input, {
|
|
880
904
|
...options,
|
|
881
905
|
emit: async (event) => {
|
|
882
|
-
if (
|
|
906
|
+
if (
|
|
907
|
+
event.parent === (options.parent ?? null) &&
|
|
908
|
+
event.type === "answer"
|
|
909
|
+
) {
|
|
883
910
|
outcome = event.stopped
|
|
884
911
|
? `${event.text}\n\n(As far as they got — they stopped before finishing.)`
|
|
885
912
|
: event.text;
|
|
886
913
|
}
|
|
887
|
-
if (event.type === "error")
|
|
914
|
+
if (event.parent === (options.parent ?? null) && event.type === "error")
|
|
915
|
+
failure = event.message;
|
|
888
916
|
await options.emit(event);
|
|
889
917
|
},
|
|
890
918
|
});
|
|
919
|
+
options.signal?.throwIfAborted();
|
|
891
920
|
if (failure) publicError(failure);
|
|
892
921
|
return outcome;
|
|
893
922
|
}
|
|
@@ -918,6 +947,11 @@ function withAskBot(
|
|
|
918
947
|
},
|
|
919
948
|
): ToolSet {
|
|
920
949
|
if (asker.peers.length === 0) return tools;
|
|
950
|
+
// Calls made in the same step answer from one evolving conversation.
|
|
951
|
+
const conversations = new WeakMap<
|
|
952
|
+
ModelMessage[],
|
|
953
|
+
{ messages: ModelMessage[]; answer: ReturnType<typeof PromiseChain> }
|
|
954
|
+
>();
|
|
921
955
|
|
|
922
956
|
const agentTools: ToolSet = {
|
|
923
957
|
...tools,
|
|
@@ -925,66 +959,143 @@ function withAskBot(
|
|
|
925
959
|
description: askBotSpec.description,
|
|
926
960
|
inputSchema: askBotSpec.parameters,
|
|
927
961
|
execute: async ({ bot, request, context }, call) => {
|
|
928
|
-
|
|
962
|
+
const found = await findJobBot(bot);
|
|
963
|
+
if (!found) return `No bot named "${bot}". Use a name from the list.`;
|
|
964
|
+
bot = found.name;
|
|
929
965
|
if (bot === asker.name || asker.above.includes(bot)) {
|
|
930
|
-
return `${bot} is already
|
|
931
|
-
}
|
|
932
|
-
// The borrowed bot works in the job's folder but drives a browser session
|
|
933
|
-
// of its own, under this call's id. When it answers, the same rule as a
|
|
934
|
-
// job's end: what it showed on their screen stays, what nobody can see
|
|
935
|
-
// closes (workspace.ts closeHiddenBrowser).
|
|
936
|
-
const session = asker.options.taskId
|
|
937
|
-
? `${asker.options.session ?? asker.options.taskId}-${call.toolCallId.slice(0, 8)}`
|
|
938
|
-
: null;
|
|
939
|
-
const exchanges: { question: string; answer: string }[] = [];
|
|
940
|
-
const answer = async (question: string) => {
|
|
941
|
-
const reply = await answerBack({
|
|
942
|
-
model: asker.model,
|
|
943
|
-
instructions: asker.instructions,
|
|
944
|
-
// Same tools, calling off (see compact).
|
|
945
|
-
tools: agentTools,
|
|
946
|
-
messages: call.messages,
|
|
947
|
-
asked: { bot, request },
|
|
948
|
-
question,
|
|
949
|
-
signal: asker.options.signal,
|
|
950
|
-
});
|
|
951
|
-
await asker.emit({ type: "answered", question, ...reply });
|
|
952
|
-
exchanges.push({ question, answer: reply.text });
|
|
953
|
-
return reply.text;
|
|
954
|
-
};
|
|
955
|
-
try {
|
|
956
|
-
const outcome = await runBotToText(
|
|
957
|
-
{
|
|
958
|
-
bot,
|
|
959
|
-
request,
|
|
960
|
-
context,
|
|
961
|
-
askedBy: asker.name,
|
|
962
|
-
chain: asker.chain,
|
|
963
|
-
above: [...asker.above, asker.name],
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
...asker.options,
|
|
967
|
-
// This call's signal, not the job's: a stream that ends — the job
|
|
968
|
-
// stopping, or the model going quiet — ends the part it waited on
|
|
969
|
-
signal: call.abortSignal ?? asker.options.signal,
|
|
970
|
-
depth: asker.depth + 1,
|
|
971
|
-
parent: call.toolCallId,
|
|
972
|
-
session,
|
|
973
|
-
answer,
|
|
974
|
-
},
|
|
975
|
-
);
|
|
976
|
-
return exchanges.length
|
|
977
|
-
? `${outcome}\n\n${exchangeLines(bot, exchanges)}`
|
|
978
|
-
: outcome;
|
|
979
|
-
} finally {
|
|
980
|
-
if (session) void closeHiddenBrowser(session);
|
|
966
|
+
return `${bot} is already above you and waiting on this part. Use ask_back for a question, or pick another bot.`;
|
|
981
967
|
}
|
|
968
|
+
const signal = call.abortSignal ?? asker.options.signal;
|
|
969
|
+
const desks = asker.options.desks!;
|
|
970
|
+
return desks.run(asker.name, bot, signal, async () => {
|
|
971
|
+
const session = asker.options.taskId
|
|
972
|
+
? botBrowserSession(asker.options.taskId, bot)
|
|
973
|
+
: null;
|
|
974
|
+
const exchanges: { question: string; answer: string }[] = [];
|
|
975
|
+
let conversation = conversations.get(call.messages);
|
|
976
|
+
if (!conversation) {
|
|
977
|
+
conversation = {
|
|
978
|
+
messages: [...call.messages],
|
|
979
|
+
answer: PromiseChain(),
|
|
980
|
+
};
|
|
981
|
+
conversations.set(call.messages, conversation);
|
|
982
|
+
}
|
|
983
|
+
const current = conversation;
|
|
984
|
+
const answer = (question: string) =>
|
|
985
|
+
current.answer(async () => {
|
|
986
|
+
signal?.throwIfAborted();
|
|
987
|
+
const reply = await answerBack({
|
|
988
|
+
model: asker.model,
|
|
989
|
+
instructions: asker.instructions,
|
|
990
|
+
tools: agentTools,
|
|
991
|
+
messages: current.messages,
|
|
992
|
+
asked: { bot, request },
|
|
993
|
+
question,
|
|
994
|
+
signal,
|
|
995
|
+
});
|
|
996
|
+
current.messages.push(
|
|
997
|
+
{
|
|
998
|
+
role: "user",
|
|
999
|
+
content: `${bot} asks about the part you handed over:\n\n${question}`,
|
|
1000
|
+
},
|
|
1001
|
+
{ role: "assistant", content: reply.text },
|
|
1002
|
+
);
|
|
1003
|
+
await asker.emit({ type: "answered", question, ...reply });
|
|
1004
|
+
exchanges.push({ question, answer: reply.text });
|
|
1005
|
+
return reply.text;
|
|
1006
|
+
});
|
|
1007
|
+
try {
|
|
1008
|
+
const outcome = await runBotToText(
|
|
1009
|
+
{
|
|
1010
|
+
bot,
|
|
1011
|
+
request,
|
|
1012
|
+
context,
|
|
1013
|
+
askedBy: asker.name,
|
|
1014
|
+
chain: asker.chain,
|
|
1015
|
+
above: [...asker.above, asker.name],
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
...asker.options,
|
|
1019
|
+
signal,
|
|
1020
|
+
depth: asker.depth + 1,
|
|
1021
|
+
parent: call.toolCallId,
|
|
1022
|
+
session,
|
|
1023
|
+
// Only the task's owner drains words sent by the user.
|
|
1024
|
+
notes: undefined,
|
|
1025
|
+
answer,
|
|
1026
|
+
},
|
|
1027
|
+
);
|
|
1028
|
+
return exchanges.length
|
|
1029
|
+
? `${outcome}\n\n${exchangeLines(bot, exchanges)}`
|
|
1030
|
+
: outcome;
|
|
1031
|
+
} catch (cause) {
|
|
1032
|
+
signal?.throwIfAborted();
|
|
1033
|
+
publicError(
|
|
1034
|
+
[modelErrorToString(cause), exchangeLines(bot, exchanges)]
|
|
1035
|
+
.filter(Boolean)
|
|
1036
|
+
.join("\n\n"),
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
982
1040
|
},
|
|
983
1041
|
}),
|
|
984
1042
|
};
|
|
985
1043
|
return agentTools;
|
|
986
1044
|
}
|
|
987
1045
|
|
|
1046
|
+
/** One execution per participant, with wait-cycle detection across concurrent branches. */
|
|
1047
|
+
export function createBotDesks() {
|
|
1048
|
+
const lock = createKeyedLock();
|
|
1049
|
+
const waiting = new Map<string, Map<string, number>>();
|
|
1050
|
+
const pending = new Set<Promise<unknown>>();
|
|
1051
|
+
const reaches = (
|
|
1052
|
+
from: string,
|
|
1053
|
+
to: string,
|
|
1054
|
+
seen = new Set<string>(),
|
|
1055
|
+
): boolean => {
|
|
1056
|
+
if (from === to) return true;
|
|
1057
|
+
if (seen.has(from)) return false;
|
|
1058
|
+
seen.add(from);
|
|
1059
|
+
return [...(waiting.get(from)?.keys() ?? [])].some((next) =>
|
|
1060
|
+
reaches(next, to, seen),
|
|
1061
|
+
);
|
|
1062
|
+
};
|
|
1063
|
+
return {
|
|
1064
|
+
async run<T>(
|
|
1065
|
+
from: string,
|
|
1066
|
+
to: string,
|
|
1067
|
+
signal: AbortSignal | undefined,
|
|
1068
|
+
work: () => Promise<T>,
|
|
1069
|
+
): Promise<T> {
|
|
1070
|
+
signal?.throwIfAborted();
|
|
1071
|
+
if (reaches(to, from))
|
|
1072
|
+
publicError(
|
|
1073
|
+
`${to} is waiting on your work. Finish this part before asking them, or choose another bot.`,
|
|
1074
|
+
);
|
|
1075
|
+
const edges = waiting.get(from) ?? new Map<string, number>();
|
|
1076
|
+
waiting.set(from, edges);
|
|
1077
|
+
edges.set(to, (edges.get(to) ?? 0) + 1);
|
|
1078
|
+
const run = lock(to, async () => {
|
|
1079
|
+
signal?.throwIfAborted();
|
|
1080
|
+
return work();
|
|
1081
|
+
});
|
|
1082
|
+
pending.add(run);
|
|
1083
|
+
try {
|
|
1084
|
+
return await run;
|
|
1085
|
+
} finally {
|
|
1086
|
+
pending.delete(run);
|
|
1087
|
+
const left = (edges.get(to) ?? 1) - 1;
|
|
1088
|
+
if (left) edges.set(to, left);
|
|
1089
|
+
else edges.delete(to);
|
|
1090
|
+
if (!edges.size) waiting.delete(from);
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
async settle() {
|
|
1094
|
+
while (pending.size) await Promise.allSettled([...pending]);
|
|
1095
|
+
},
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
988
1099
|
/** Appended under the answer: what the borrowed bot asked and what this bot answered. */
|
|
989
1100
|
function exchangeLines(
|
|
990
1101
|
bot: string,
|
|
@@ -33,7 +33,12 @@ import { desktopNotify } from "@/lib/desktop-notify";
|
|
|
33
33
|
import { logger } from "@/lib/logger";
|
|
34
34
|
import { publicError } from "@/lib/public-error";
|
|
35
35
|
import { createKeyedLock } from "@/lib/queue";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
createBotDesks,
|
|
38
|
+
resumeThread,
|
|
39
|
+
runBot,
|
|
40
|
+
type TaskEvent,
|
|
41
|
+
} from "./bot.run";
|
|
37
42
|
import {
|
|
38
43
|
TASK_CONTINUE,
|
|
39
44
|
type TaskPending,
|
|
@@ -44,7 +49,6 @@ import {
|
|
|
44
49
|
import {
|
|
45
50
|
addTaskUsage,
|
|
46
51
|
countStopsSinceSpoken,
|
|
47
|
-
deleteFinishedTasks,
|
|
48
52
|
deleteMessages,
|
|
49
53
|
deleteTask,
|
|
50
54
|
findTask,
|
|
@@ -275,7 +279,8 @@ export async function cancelTask(id: string) {
|
|
|
275
279
|
// write "Cancelled." over the thing it was run for.
|
|
276
280
|
if (task.endedAt) publicError("That job has already ended.");
|
|
277
281
|
forgetResume(id);
|
|
278
|
-
running.get(id)
|
|
282
|
+
const live = running.get(id);
|
|
283
|
+
live?.stop.abort();
|
|
279
284
|
await updateTask(id, {
|
|
280
285
|
status: "failed",
|
|
281
286
|
outcome: "Cancelled.",
|
|
@@ -286,24 +291,27 @@ export async function cancelTask(id: string) {
|
|
|
286
291
|
});
|
|
287
292
|
// Cancel is a real end, so the browser session closes here; the abort in
|
|
288
293
|
// answerTask is followed by a relaunch and must not close it.
|
|
289
|
-
|
|
294
|
+
await live?.done;
|
|
295
|
+
await closeJobShell(id);
|
|
290
296
|
});
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
/** Deletes the job, stopping it first if it is still running. */
|
|
294
300
|
export async function removeTask(id: string) {
|
|
295
|
-
return taskLock(id,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
301
|
+
return taskLock(id, () => removeLockedTask(id));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The caller holds taskLock, so a follow-up cannot race deletion or browser cleanup. */
|
|
305
|
+
async function removeLockedTask(id: string) {
|
|
306
|
+
const task = await findTask(id);
|
|
307
|
+
forgetResume(id);
|
|
308
|
+
const live = running.get(id);
|
|
309
|
+
live?.stop.abort();
|
|
310
|
+
await live?.done;
|
|
311
|
+
await closeJobShell(id);
|
|
312
|
+
const gone = await deleteTask(id);
|
|
313
|
+
if (gone && task) await removeJobScratch(id, task.label);
|
|
314
|
+
return gone;
|
|
307
315
|
}
|
|
308
316
|
|
|
309
317
|
/**
|
|
@@ -312,9 +320,16 @@ export async function removeTask(id: string) {
|
|
|
312
320
|
* and only one of the two is the database's.
|
|
313
321
|
*/
|
|
314
322
|
export async function removeFinishedTasks(): Promise<number> {
|
|
315
|
-
|
|
316
|
-
for (const task of
|
|
317
|
-
|
|
323
|
+
let removed = 0;
|
|
324
|
+
for (const task of await listTaskFolders()) {
|
|
325
|
+
if (task.status !== "done" && task.status !== "failed") continue;
|
|
326
|
+
await taskLock(task.id, async () => {
|
|
327
|
+
const current = await findTask(task.id);
|
|
328
|
+
if (current?.status !== "done" && current?.status !== "failed") return;
|
|
329
|
+
if (await removeLockedTask(task.id)) removed += 1;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return removed;
|
|
318
333
|
}
|
|
319
334
|
|
|
320
335
|
/**
|
|
@@ -345,6 +360,7 @@ export async function sweepJobFiles(): Promise<string[]> {
|
|
|
345
360
|
await taskLock(task.id, async () => {
|
|
346
361
|
const now = await findTask(task.id);
|
|
347
362
|
if (!now || !stale(now)) return;
|
|
363
|
+
await closeHiddenBrowser(task.id);
|
|
348
364
|
await removeJobScratch(task.id, task.label);
|
|
349
365
|
removed.push(folder);
|
|
350
366
|
});
|
|
@@ -652,6 +668,12 @@ class ThreadWriter {
|
|
|
652
668
|
if (this.closed) return;
|
|
653
669
|
const step = this.step(event);
|
|
654
670
|
switch (event.type) {
|
|
671
|
+
case "request":
|
|
672
|
+
return this.write(++this.seq, event, {
|
|
673
|
+
role: "user",
|
|
674
|
+
content: event.content,
|
|
675
|
+
});
|
|
676
|
+
|
|
655
677
|
case "text":
|
|
656
678
|
step.assistant ??= ++this.seq;
|
|
657
679
|
step.parts.assistant.push({ type: "text", text: event.text });
|
|
@@ -747,6 +769,7 @@ async function drive(
|
|
|
747
769
|
notes: string[],
|
|
748
770
|
) {
|
|
749
771
|
const thread = new ThreadWriter(id, await lastSeq(id));
|
|
772
|
+
const desks = createBotDesks();
|
|
750
773
|
|
|
751
774
|
let ending: {
|
|
752
775
|
status: TaskStatus;
|
|
@@ -767,6 +790,7 @@ async function drive(
|
|
|
767
790
|
await runBot(input, {
|
|
768
791
|
signal,
|
|
769
792
|
taskId: id,
|
|
793
|
+
desks,
|
|
770
794
|
notes: () => notes.splice(0),
|
|
771
795
|
emit: async (event) => {
|
|
772
796
|
await thread.on(event);
|
|
@@ -829,6 +853,8 @@ async function drive(
|
|
|
829
853
|
pending: null,
|
|
830
854
|
};
|
|
831
855
|
} finally {
|
|
856
|
+
// A resumed segment cannot overtake a participant still unwinding its tools.
|
|
857
|
+
await desks.settle();
|
|
832
858
|
thread.close();
|
|
833
859
|
running.delete(id);
|
|
834
860
|
}
|
|
@@ -881,10 +907,9 @@ async function drive(
|
|
|
881
907
|
return;
|
|
882
908
|
}
|
|
883
909
|
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
|
|
887
|
-
else void closeHiddenBrowser(id);
|
|
910
|
+
// Follow-ups can reopen a completed job. Its participants keep their browsers
|
|
911
|
+
// until cancellation, deletion, or the workspace retention sweep.
|
|
912
|
+
void pruneJobFiles();
|
|
888
913
|
|
|
889
914
|
const task = await findTask(id);
|
|
890
915
|
if (!task) return;
|