@seele00/seele 0.1.63 → 0.1.64
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/README.md +5 -3
- package/bin/seele-codex +1 -1
- package/libexec/seele-server +0 -0
- package/libexec/seele-worker +0 -0
- package/package.json +1 -4
- package/scripts/deploy/bootstrap.sh +14 -3
- package/scripts/deploy/start.sh +14 -9
- package/web/dist/assets/{abnfDiagram-VRR7QNED-DXJQwnn-.js → abnfDiagram-VRR7QNED-BB7DT0BN.js} +1 -1
- package/web/dist/assets/{arc-D_47-IET.js → arc-BlEU_mit.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-CKUldIzR.js → architectureDiagram-ZJ3FMSHR-Dy9cu2Mg.js} +1 -1
- package/web/dist/assets/{blockDiagram-677ZJIJ3-BD4Ym8Xi.js → blockDiagram-677ZJIJ3-DP4jMSfo.js} +1 -1
- package/web/dist/assets/{c4Diagram-LMCZKHZV-Cn268vLB.js → c4Diagram-LMCZKHZV-BtqFzX1w.js} +1 -1
- package/web/dist/assets/channel-mhSuRKJT.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-D00lQ7mJ.js → chunk-2Q5K7J3B-BK9F5a0L.js} +1 -1
- package/web/dist/assets/{chunk-32BRIVSS-z7gv8Mry.js → chunk-32BRIVSS-nuQI6gtU.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BZJeO_rz.js → chunk-5VM5RSS4-D4IT05bt.js} +1 -1
- package/web/dist/assets/{chunk-EX3LRPZG-DoIg9UGx.js → chunk-EX3LRPZG-CrOtgxcr.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-C1LcQus2.js → chunk-JWPE2WC7-Dn9JHm4W.js} +1 -1
- package/web/dist/assets/{chunk-MOJQB5TN--JPbFu4n.js → chunk-MOJQB5TN-VtD0_fGF.js} +1 -1
- package/web/dist/assets/{chunk-RYQCIY6F-BVv30W60.js → chunk-RYQCIY6F-TqRYsCot.js} +1 -1
- package/web/dist/assets/{chunk-V7JOEXUC-DYoSFgRT.js → chunk-V7JOEXUC-U9b5a7vL.js} +1 -1
- package/web/dist/assets/{chunk-VR4S4FIN-C8BRkTOa.js → chunk-VR4S4FIN-B3qLfeNJ.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-CDfofIob.js → chunk-XXDRQBXY-DazVAZe-.js} +1 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-DYQSxD4g.js +1 -0
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-DYQSxD4g.js +1 -0
- package/web/dist/assets/{core-C10OFTno.js → core-2RdBW0rZ.js} +1 -1
- package/web/dist/assets/{cose-bilkent-JH36ORCC-g9E8J4Fz.js → cose-bilkent-JH36ORCC-vvKttQXU.js} +1 -1
- package/web/dist/assets/{cynefin-VYW2F7L2-Dc689veM.js → cynefin-VYW2F7L2-BKMZmA-n.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-TSTJHNR4-BAAq_jvr.js → cynefinDiagram-TSTJHNR4-ckdzxzlr.js} +1 -1
- package/web/dist/assets/{dagre-VKFMJZFB-CPtiniZQ.js → dagre-VKFMJZFB-CZw8DTb1.js} +1 -1
- package/web/dist/assets/{diagram-FQU43EPY-CvbIL3lu.js → diagram-FQU43EPY-C06X0Kru.js} +1 -1
- package/web/dist/assets/{diagram-G47NLZAW-u5m78Utg.js → diagram-G47NLZAW-HX9CdauD.js} +1 -1
- package/web/dist/assets/{diagram-NH7WQ7WH-4f-Ipubv.js → diagram-NH7WQ7WH-Dlo1tIod.js} +1 -1
- package/web/dist/assets/{diagram-OA4YK3LP-SUl1EQ56.js → diagram-OA4YK3LP-C9GXFhgw.js} +1 -1
- package/web/dist/assets/{diagram-WEI45ONY-C_Upd46l.js → diagram-WEI45ONY-Ch1Xx234.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-CCIWWBDH-CHp2D7Q9.js → ebnfDiagram-CCIWWBDH-Q0_rEUgk.js} +1 -1
- package/web/dist/assets/{erDiagram-Q63AITRT-DQliHxOw.js → erDiagram-Q63AITRT-BsDRdpx6.js} +1 -1
- package/web/dist/assets/{flowDiagram-23GEKE2U-BcOYSJFM.js → flowDiagram-23GEKE2U-DDFuMRJN.js} +1 -1
- package/web/dist/assets/{ganttDiagram-NO4QXBWP-CDLHtJq0.js → ganttDiagram-NO4QXBWP-Cjpm08-d.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-2j8C3mlk.js → gitGraphDiagram-IHSO6WYX-CnIosh2K.js} +1 -1
- package/web/dist/assets/index-eN6ddNRA.js +111 -0
- package/web/dist/assets/{infoDiagram-FWYZ7A6U-ClxdyQnM.js → infoDiagram-FWYZ7A6U-CH163HyC.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-Bc2nIRgb.js → ishikawaDiagram-FXEZZL3T-DkT-3n3D.js} +1 -1
- package/web/dist/assets/{journeyDiagram-5HDEW3XC-R-vJ00LW.js → journeyDiagram-5HDEW3XC-34TnH3wG.js} +1 -1
- package/web/dist/assets/{kanban-definition-HUTT4EX6-CR0s_Mo8.js → kanban-definition-HUTT4EX6-CJbOm0lN.js} +1 -1
- package/web/dist/assets/{linear-BQgrawZS.js → linear-CTKXTLwe.js} +1 -1
- package/web/dist/assets/{mermaid.core-faLMbjSU.js → mermaid.core-CAVP16_-.js} +4 -4
- package/web/dist/assets/{mindmap-definition-LN4V7U3C-BZULnQui.js → mindmap-definition-LN4V7U3C-rlppHZj_.js} +1 -1
- package/web/dist/assets/{pegDiagram-2B236MQR-DUm1p_1o.js → pegDiagram-2B236MQR-MbcWBNJi.js} +1 -1
- package/web/dist/assets/{pieDiagram-ENE6RG2P-r3LCASRK.js → pieDiagram-ENE6RG2P-DKU7a4tv.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-CGaCp0Px.js → quadrantDiagram-ABIIQ3AL-DpDsJVLi.js} +1 -1
- package/web/dist/assets/{railroadDiagram-RFXS5EU6-DmEKGO1A.js → railroadDiagram-RFXS5EU6-DrPM3aKC.js} +1 -1
- package/web/dist/assets/{requirementDiagram-TGXJPOKE-Byc4ivmp.js → requirementDiagram-TGXJPOKE-ZzS_iVag.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-HTMAVEWB-BtJiL4zX.js → sankeyDiagram-HTMAVEWB-DrRrN0Lc.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-Bg3vdDGW.js → sequenceDiagram-DBY2YBRQ-CP8GBGZM.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-CCRwjaWU.js → sizeCapture-X5ZJPWSS-BYVg_dQd.js} +1 -1
- package/web/dist/assets/{stateDiagram-2N3HPSRC-CQU-udox.js → stateDiagram-2N3HPSRC-CsJ3M80-.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CaFOY1Xb.js +1 -0
- package/web/dist/assets/{swimlanes-5IMT3BWC-r3tJnQLJ.js → swimlanes-5IMT3BWC-XgRKQ20Q.js} +2 -2
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-DaKP_iDy.js +8 -0
- package/web/dist/assets/{timeline-definition-FHXFAJF6-DryYrdts.js → timeline-definition-FHXFAJF6-CU_Cq10d.js} +1 -1
- package/web/dist/assets/{vennDiagram-L72KCM5P-ZLPc0qvQ.js → vennDiagram-L72KCM5P-CLSDbg6o.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-EHGQE667-RmgA6vQ9.js → wardleyDiagram-EHGQE667-DnAOuvzv.js} +1 -1
- package/web/dist/assets/{xychartDiagram-FW5EYKEG-BGIzispI.js → xychartDiagram-FW5EYKEG-CyC6V11l.js} +1 -1
- package/web/dist/index.html +1 -1
- package/codex/dist/app-server.js +0 -63
- package/codex/dist/errors.js +0 -16
- package/codex/dist/index.js +0 -4
- package/codex/dist/rpc-client.js +0 -96
- package/codex/dist/transports/stdio.js +0 -81
- package/codex/dist/types.js +0 -1
- package/codex/package.json +0 -12
- package/scripts/deploy/npm/postinstall.mjs +0 -10
- package/server/dist/auth/session.js +0 -71
- package/server/dist/auth/users.js +0 -276
- package/server/dist/codex-runtime/run-streamed-turn.js +0 -587
- package/server/dist/config/config.js +0 -71
- package/server/dist/contracts/artifact-rendering.js +0 -14
- package/server/dist/contracts/global-environment.js +0 -23
- package/server/dist/contracts/index.js +0 -114
- package/server/dist/contracts/managed-assets.js +0 -13
- package/server/dist/contracts/response-style.js +0 -23
- package/server/dist/contracts/task-clarification.js +0 -13
- package/server/dist/contracts/types.js +0 -1
- package/server/dist/contracts/wakeup.js +0 -13
- package/server/dist/contracts/workspace-boundary.js +0 -20
- package/server/dist/contracts/workspace-knowledge.js +0 -48
- package/server/dist/contracts/workspace-memory.js +0 -32
- package/server/dist/contracts/workspace-responsibility.js +0 -23
- package/server/dist/contracts/workspace-transcript.js +0 -26
- package/server/dist/conversations/knowledge.js +0 -380
- package/server/dist/conversations/store.js +0 -2466
- package/server/dist/doctor.js +0 -105
- package/server/dist/realtime/sse.js +0 -12
- package/server/dist/runtime-worker/client.js +0 -405
- package/server/dist/runtime-worker/process.js +0 -216
- package/server/dist/runtime-worker/protocol.js +0 -1
- package/server/dist/server.js +0 -4605
- package/server/dist/storage/sqlite.js +0 -39
- package/web/dist/assets/channel-Uz6zF6Xw.js +0 -1
- package/web/dist/assets/classDiagram-OUVF2IWQ-D6mpE1t2.js +0 -1
- package/web/dist/assets/classDiagram-v2-EOCWNBFH-D6mpE1t2.js +0 -1
- package/web/dist/assets/index-BqSosI7W.js +0 -111
- package/web/dist/assets/stateDiagram-v2-6OUMAXLB-YNSWW4ip.js +0 -1
- package/web/dist/assets/swimlanesDiagram-G3AALYLV-BGZAc11_.js +0 -8
package/server/dist/server.js
DELETED
|
@@ -1,4605 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { execFile, spawn } from "node:child_process";
|
|
3
|
-
import { constants as fsConstants, createReadStream, mkdirSync, writeFileSync, } from "node:fs";
|
|
4
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
|
-
import { loadConfig } from "./config/config.js";
|
|
9
|
-
import { clearSessionCookie, readSessionPayload, setSessionCookie, } from "./auth/session.js";
|
|
10
|
-
import { UserStore } from "./auth/users.js";
|
|
11
|
-
import { buildRuntimeAdditionalContext } from "./contracts/index.js";
|
|
12
|
-
import { openSse, sendSse } from "./realtime/sse.js";
|
|
13
|
-
import { RuntimeWorkerClient } from "./runtime-worker/client.js";
|
|
14
|
-
import { appendEvent, appendMessage, cancelWorkspaceWakeup, countConversationUnreadTurns, createConversation, createConversationTurn, createWorkspaceWakeup, createWorkspaceGroup, deleteConversation, deleteConversationEnvironmentVariable, deleteGlobalEnvironmentVariable, deleteWorkspaceGroup, getConversation, getConversationEnvironmentDescriptions, getConversationEnvironmentMap, getConversationHistoryPage, getLatestConversationNotification, getConversationRevision, getConversationTurn, getConversationWorkspaceSettings, getConversationStoreDiagnostics, getAppSettingValue, formatMemoryItems, getGlobalEnvironmentDescriptions, getGlobalEnvironmentMap, getNextScheduledWorkspaceWakeup, hasWorkspaceGroup, initializeConversationStore, isConversationSandbox, listGlobalEnvironmentVariables, listDueWorkspaceWakeups, listConversationEventsByType, listConversationTurnEvents, listAllConversationVisibleMessages, listConversationTranscriptMessagesBefore, listConversationTranscriptMessageRange, listRuntimeResets, listTranscriptRangeSummaries, listConversationEnvironmentVariables, listWorkspaceWakeups, listConversations, listWorkspaceGroups, markConversationRead, recoverInterruptedWakeupsOnStartup, recoverInterruptedTurnsOnStartup, recordRuntimeReset, revealGlobalEnvironmentVariable, revealConversationEnvironmentVariable, appendConversationContextVersion, setConversationGroup, setConversationMemory, setConversationMemoryItems, setConversationResponsibility, setConversationWorkspaceSetting, deleteConversationWorkspaceSetting, setConversationSandbox, setConversationStatus, setConversationThread, setConversationTitle, updateWorkspaceWakeup, setWorkspaceGroupTitle, upsertTranscriptRangeSummary, upsertAppSetting, upsertConversationEnvironmentVariable, upsertGlobalEnvironmentVariable, updateConversationTurn, normalizeMemoryItems, parseMemoryItems, } from "./conversations/store.js";
|
|
15
|
-
import { createWorkspaceKnowledge, deleteWorkspaceKnowledge, listWorkspaceKnowledge, summarizeWorkspaceKnowledge, updateWorkspaceKnowledge, } from "./conversations/knowledge.js";
|
|
16
|
-
const config = loadConfig();
|
|
17
|
-
const userStore = new UserStore(config.centerDbPath, {
|
|
18
|
-
username: config.authUsername,
|
|
19
|
-
password: config.authPassword,
|
|
20
|
-
});
|
|
21
|
-
if (!isLoopbackHost(config.host) && !userStore.enabled) {
|
|
22
|
-
throw new Error("SEELE_AUTH_PASSWORD is required to create the first admin user when Seele is exposed");
|
|
23
|
-
}
|
|
24
|
-
let cachedAppMeta = null;
|
|
25
|
-
const PROFUSE_PLATFORM = "seele";
|
|
26
|
-
const PROFUSE_NOTIFY_URL = "https://profuse.antgroup-inc.cn/api/shao-meta/agent/notify";
|
|
27
|
-
const PROFUSE_AGENT_TOKEN = "shao-kb-7f9e2a3d4b5c";
|
|
28
|
-
const PROFUSE_NOTIFY_TIMEOUT_MS = 5000;
|
|
29
|
-
const CODEX_NPM_REGISTRY = "https://registry.npmmirror.com/";
|
|
30
|
-
const CODEX_UPDATE_TIMEOUT_MS = 120_000;
|
|
31
|
-
const CODEX_LATEST_LOOKUP_TIMEOUT_MS = 15_000;
|
|
32
|
-
const CODEX_VERSION_READ_TIMEOUT_MS = 30_000;
|
|
33
|
-
const CODEX_VERSION_VERIFY_TIMEOUT_MS = 60_000;
|
|
34
|
-
const RUNTIME_MODEL_CATALOG_CACHE_KEY = "runtime.model_catalog";
|
|
35
|
-
const RUNTIME_RATE_LIMITS_CACHE_KEY = "runtime.rate_limits";
|
|
36
|
-
const WAKEUP_POLL_INTERVAL_MS = 5_000;
|
|
37
|
-
const RUNTIME_WATCHDOG_INTERVAL_MS = 10_000;
|
|
38
|
-
const TURN_LONG_RUNNING_AFTER_MS = 15 * 60 * 1000;
|
|
39
|
-
const TURN_PHASE_BOUNDARY_OPERATION_COUNT = 20;
|
|
40
|
-
const LOGIN_MAX_FAILURES = 5;
|
|
41
|
-
const LOGIN_USER_MAX_FAILURES = 10;
|
|
42
|
-
const LOGIN_SOURCE_MAX_FAILURES = 30;
|
|
43
|
-
const LOGIN_MAX_CONCURRENT_VERIFICATIONS = 4;
|
|
44
|
-
const LOGIN_FAILURE_WINDOW_MS = 15 * 60 * 1000;
|
|
45
|
-
const LOGIN_LOCKOUT_MS = 15 * 60 * 1000;
|
|
46
|
-
const RESPONSIBILITY_UPDATE_GUIDE_RELATIVE_PATH = ".seele/skills/responsibility-update.md";
|
|
47
|
-
const KNOWLEDGE_SOURCE_GUIDE_RELATIVE_PATH = ".seele/skills/knowledge-source.md";
|
|
48
|
-
let codexUpdateJob = null;
|
|
49
|
-
let codexLatestVersion = null;
|
|
50
|
-
let runtimeModelCatalogRefresh = null;
|
|
51
|
-
let runtimeRateLimitsRefresh = null;
|
|
52
|
-
let activeLoginVerifications = 0;
|
|
53
|
-
const RESPONSIBILITY_UPDATE_GUIDE = `# Seele 职责总结与更新指南
|
|
54
|
-
|
|
55
|
-
仅当你需要总结或更新 workspace 的长期职责时再阅读本文件。它不是每轮默认注入内容。
|
|
56
|
-
|
|
57
|
-
## 职责是什么
|
|
58
|
-
|
|
59
|
-
职责是用户批准的长期角色定位和岗位说明。它会被注入到后续 turn 中,用来锚定计划、假设、沟通风格、边界判断、上下文保护和长任务拆分方式。
|
|
60
|
-
|
|
61
|
-
职责不是当前任务内容本身,也不是详细技能库。
|
|
62
|
-
|
|
63
|
-
## 什么时候提交职责建议
|
|
64
|
-
|
|
65
|
-
- 当前职责为空。
|
|
66
|
-
- 当前职责明显过期、过泛,或不再匹配 workspace 的长期角色。
|
|
67
|
-
- 用户要求你总结、更新或校准这个 workspace 的职责。
|
|
68
|
-
|
|
69
|
-
不要因为单个任务不同就修改职责。长期角色不清楚时,优先询问用户。
|
|
70
|
-
|
|
71
|
-
## 职责正文怎么写
|
|
72
|
-
|
|
73
|
-
职责正文建议用简洁 Markdown。它应该描述:
|
|
74
|
-
|
|
75
|
-
- 角色定位:这个 workspace 长期扮演什么角色。
|
|
76
|
-
- 工作范围:哪些工作属于这里,哪些不属于这里。
|
|
77
|
-
- 关键维度:判断优先级、质量、风险、取舍和何时询问用户的稳定标准。
|
|
78
|
-
- 工作方式:沟通、验证、上下文保护、长任务拆分等角色层面的协作方式。
|
|
79
|
-
|
|
80
|
-
职责应使用稳定、可长期维护、用户可审阅的中文表达。技术名词、API 名、文件名、产品名可保留原文。
|
|
81
|
-
|
|
82
|
-
## 不应该写进职责的内容
|
|
83
|
-
|
|
84
|
-
不要把详细技能、SOP、实现细节、API 字段、脚本、命令配方、日志、临时结论、任务进度、项目文件地图或经常变化的资料写进职责。
|
|
85
|
-
|
|
86
|
-
这些内容应放在技能、文档、Memory、普通文件或未来更合适的知识机制里。职责可以提示应该参考某类来源,但不应复制细节。
|
|
87
|
-
|
|
88
|
-
## 提交格式
|
|
89
|
-
|
|
90
|
-
不要静默修改职责。提交完整替换建议时,只使用下面这个格式:
|
|
91
|
-
|
|
92
|
-
\`\`\`seele-responsibility-proposal
|
|
93
|
-
{
|
|
94
|
-
"responsibility": "职责全文"
|
|
95
|
-
}
|
|
96
|
-
\`\`\`
|
|
97
|
-
|
|
98
|
-
Seele 会隐藏该控制块,并向用户展示审批卡片。用户可以在保存前自行微调。
|
|
99
|
-
`;
|
|
100
|
-
const KNOWLEDGE_SOURCE_GUIDE = `# Seele Knowledge Source Guide
|
|
101
|
-
|
|
102
|
-
Use this guide when the user gives durable external material, asks you to turn
|
|
103
|
-
material into a skill, or you think a source should be preserved for long-lived
|
|
104
|
-
workspace work.
|
|
105
|
-
|
|
106
|
-
## What belongs here
|
|
107
|
-
|
|
108
|
-
Workspace knowledge is for reusable, source-backed material:
|
|
109
|
-
|
|
110
|
-
- external documents, platform pages, APIs, git repositories, manuals, protocol specs
|
|
111
|
-
- durable working guides, domain skills, checklists, source-backed summaries
|
|
112
|
-
- material this workspace may need to re-read in future turns
|
|
113
|
-
|
|
114
|
-
Do not use knowledge for one-off temporary context that is only useful for the
|
|
115
|
-
current answer. Do not store secrets here.
|
|
116
|
-
|
|
117
|
-
Knowledge must be a meaningful reusable asset, not a pile of tiny notes. Do not
|
|
118
|
-
create one-line or few-line knowledge entries, and do not add a new entry for
|
|
119
|
-
every individual chat message. Aggregate related teaching, source clues, and
|
|
120
|
-
working rules into one topic-level knowledge item. Update an existing item when
|
|
121
|
-
the durable topic is the same.
|
|
122
|
-
|
|
123
|
-
Chat is only a source clue, not knowledge by itself. Do not preserve raw chat
|
|
124
|
-
snippets, temporary facts, recent task status, concrete one-off examples, or a
|
|
125
|
-
few lines the user just said as Knowledge. If the source is chat, cite only a
|
|
126
|
-
\`chat_range\` message id range, then abstract it into a stable reusable
|
|
127
|
-
principle, workflow, quality standard, or source-backed skill, and explain why
|
|
128
|
-
it should survive beyond the current task.
|
|
129
|
-
|
|
130
|
-
If a proposed item only has chat clues and no durable abstraction, stable
|
|
131
|
-
purpose, or reusable future use, do not submit a knowledge proposal. Keep it in
|
|
132
|
-
the current turn, ask whether the user wants it preserved, or propose Memory
|
|
133
|
-
only when it is short always-injected context.
|
|
134
|
-
|
|
135
|
-
## Classification
|
|
136
|
-
|
|
137
|
-
When the user provides material, classify it first:
|
|
138
|
-
|
|
139
|
-
- Temporary reference: useful only now; use it in the current task and do not register it.
|
|
140
|
-
- Knowledge source: durable source material; preserve source clues, local copy, digest, and acquisition method.
|
|
141
|
-
- Workspace skill: reusable guidance derived from source material; keep the source relationship visible.
|
|
142
|
-
|
|
143
|
-
If unsure whether something should be tracked long term, ask the user to confirm.
|
|
144
|
-
|
|
145
|
-
## Reliability maintenance
|
|
146
|
-
|
|
147
|
-
Knowledge is not only storage; it is a reliability-maintained working asset.
|
|
148
|
-
When using knowledge, actively check whether it is trustworthy, current,
|
|
149
|
-
consistent with observed facts, and traceable to a clear source.
|
|
150
|
-
|
|
151
|
-
If a problem is in source-backed material or the acquisition path, tell the user
|
|
152
|
-
which source appears wrong, missing, stale, inaccessible, or contradictory. Do
|
|
153
|
-
not silently rewrite the source of truth.
|
|
154
|
-
|
|
155
|
-
If a problem is in a local digest, summary, skill, or other derived knowledge
|
|
156
|
-
that Seele maintains, submit an update proposal for approval. Do not keep
|
|
157
|
-
working around the same outdated or misleading summary in repeated tasks.
|
|
158
|
-
|
|
159
|
-
## Approval requirement
|
|
160
|
-
|
|
161
|
-
Never silently create or update workspace knowledge. The user must approve a
|
|
162
|
-
proposal before it becomes persistent workspace Knowledge.
|
|
163
|
-
|
|
164
|
-
When a task may depend on durable workspace knowledge, first scan the injected
|
|
165
|
-
STATE KNOWLEDGE index using id, name, and purpose. If an item's purpose matches
|
|
166
|
-
the task, fetch that item's body through the Knowledge body API and use content
|
|
167
|
-
before answering or acting.
|
|
168
|
-
|
|
169
|
-
Create proposals with exactly this shape:
|
|
170
|
-
|
|
171
|
-
\`\`\`seele-knowledge-proposal
|
|
172
|
-
{
|
|
173
|
-
"action": "create",
|
|
174
|
-
"name": "短标题",
|
|
175
|
-
"purpose": "短索引触发说明,用于判断何时需要读取正文",
|
|
176
|
-
"content": "正文:未来取到后可以直接应用的规则、标准、流程、资料摘要或操作原则"
|
|
177
|
-
}
|
|
178
|
-
\`\`\`
|
|
179
|
-
|
|
180
|
-
Update proposals must use exactly this shape:
|
|
181
|
-
|
|
182
|
-
\`\`\`seele-knowledge-proposal
|
|
183
|
-
{
|
|
184
|
-
"action": "update",
|
|
185
|
-
"knowledgeId": "kn_xxxxxx",
|
|
186
|
-
"name": "短标题",
|
|
187
|
-
"purpose": "短索引触发说明,用于判断何时需要读取正文",
|
|
188
|
-
"content": "更新后的正文"
|
|
189
|
-
}
|
|
190
|
-
\`\`\`
|
|
191
|
-
|
|
192
|
-
Do not add fields other than action, knowledgeId, name, purpose, and content. Do
|
|
193
|
-
not use arrays, comments, Markdown inside the JSON, multiple JSON objects, or
|
|
194
|
-
prose inside the fenced block.
|
|
195
|
-
|
|
196
|
-
- name: short stable title in the Knowledge index.
|
|
197
|
-
- purpose: short index trigger. It is shown in the always-injected index and is
|
|
198
|
-
used to decide whether to fetch content. Do not put the full rule here.
|
|
199
|
-
- content: durable working body. Write the reusable rule, standard, source
|
|
200
|
-
summary, or operating principle in concise Chinese so a future turn can apply
|
|
201
|
-
it directly after fetching it.
|
|
202
|
-
|
|
203
|
-
Example after approval:
|
|
204
|
-
|
|
205
|
-
\`\`\`text
|
|
206
|
-
STATE KNOWLEDGE: kn_xxxxxx | name=用餐工具基本规则 | purpose=处理吃饭工具、餐具选择、用餐礼仪或相关判断时先应用此规则...
|
|
207
|
-
\`\`\`
|
|
208
|
-
|
|
209
|
-
When this purpose matches the current task, fetch kn_xxxxxx through the Knowledge
|
|
210
|
-
body API and use its content.
|
|
211
|
-
|
|
212
|
-
This follows Seele's managed-assets rule. Do not bypass this workflow by
|
|
213
|
-
directly editing \`.seele/knowledge\`, \`.seele/skills/knowledge-source.md\`,
|
|
214
|
-
\`item.json\`, \`digest.md\`, \`skill.md\`, \`fetch.sh\`, \`raw/\`, or other local
|
|
215
|
-
knowledge files. Treat those paths as Seele-managed state. You may read
|
|
216
|
-
documented readable projections when needed, but durable knowledge changes must
|
|
217
|
-
go through an approval proposal.
|
|
218
|
-
|
|
219
|
-
Use this proposal only for durable source-backed or reusable guidance. If the
|
|
220
|
-
content is short and should be injected every turn, propose Memory instead. If
|
|
221
|
-
the content defines the workspace's role, propose Responsibility instead. If it
|
|
222
|
-
is just temporary task context, do not register it.
|
|
223
|
-
|
|
224
|
-
## Source and reliability notes
|
|
225
|
-
|
|
226
|
-
If source handling, credentials, refresh, or acquisition scripts matter, explain
|
|
227
|
-
that in the normal answer and ask the user what to do next. Do not encode those
|
|
228
|
-
operational details into the Knowledge proposal JSON.
|
|
229
|
-
|
|
230
|
-
Use Knowledge content for the durable working memory that future turns should
|
|
231
|
-
apply. Keep changing implementation details, raw source copies, and temporary
|
|
232
|
-
task status out of the Knowledge content unless they are part of a stable rule
|
|
233
|
-
or reusable source summary.
|
|
234
|
-
`;
|
|
235
|
-
const GLOBAL_MEMORY_ITEMS_SETTING_KEY = "memory.global.items";
|
|
236
|
-
const RUNTIME_MODEL_SETTING_KEY = "runtime.model";
|
|
237
|
-
const RUNTIME_REASONING_EFFORT_SETTING_KEY = "runtime.reasoningEffort";
|
|
238
|
-
const RUNTIME_FAST_MODE_SETTING_KEY = "runtime.fastMode";
|
|
239
|
-
const RUNTIME_SANDBOX_SETTING_KEY = "runtime.sandbox";
|
|
240
|
-
const RUNTIME_NOTIFY_ENABLED_SETTING_KEY = "runtime.notifyEnabled";
|
|
241
|
-
const activeConversationTurns = new Map();
|
|
242
|
-
const turnConversations = new Map();
|
|
243
|
-
const turnWakeups = new Map();
|
|
244
|
-
const turnEventBacklog = new Map();
|
|
245
|
-
const turnSubscribers = new Map();
|
|
246
|
-
const turnTerminalEvents = new Map();
|
|
247
|
-
const locallyStoppedTurns = new Set();
|
|
248
|
-
const wakeupsInFlight = new Set();
|
|
249
|
-
const workspaceTurnQueues = new Map();
|
|
250
|
-
const staleRuntimeTurns = new Set();
|
|
251
|
-
const longRunningAdvisedTurns = new Set();
|
|
252
|
-
const phaseBoundaryAdvisedTurns = new Set();
|
|
253
|
-
const turnOperationCounts = new Map();
|
|
254
|
-
const latestTurnContextUsage = new Map();
|
|
255
|
-
const activeContextCompactions = new Map();
|
|
256
|
-
const completedContextCompactionsAwaitingUsage = new Map();
|
|
257
|
-
const execFileAsync = promisify(execFile);
|
|
258
|
-
initializeConversationStore({
|
|
259
|
-
centerDbPath: config.centerDbPath,
|
|
260
|
-
workspaceRoot: config.workspaceRoot,
|
|
261
|
-
defaultSandbox: config.defaultSandbox,
|
|
262
|
-
defaultOwnerId: userStore.listUsers().find((user) => user.role === "admin")?.id ?? "local-admin",
|
|
263
|
-
});
|
|
264
|
-
const startupRecovery = recoverInterruptedTurnsOnStartup();
|
|
265
|
-
const wakeupStartupRecovery = recoverInterruptedWakeupsOnStartup();
|
|
266
|
-
const runtime = new RuntimeWorkerClient({
|
|
267
|
-
onEvent: ({ turnId, event, data }) => recordTurnEvent(turnId, event, data),
|
|
268
|
-
onCompleted: ({ turnId, result }) => completeTurn(turnId, result),
|
|
269
|
-
onFailed: ({ turnId, message }) => failTurn(turnId, message),
|
|
270
|
-
});
|
|
271
|
-
const server = createServer(async (request, response) => {
|
|
272
|
-
setCorsHeaders(response);
|
|
273
|
-
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
274
|
-
if (request.method === "OPTIONS") {
|
|
275
|
-
response.writeHead(204);
|
|
276
|
-
response.end();
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (request.method === "GET" && url.pathname === "/api/auth/session") {
|
|
280
|
-
response.setHeader("Cache-Control", "no-store");
|
|
281
|
-
const user = readAuthenticatedUser(request);
|
|
282
|
-
sendJson(response, 200, {
|
|
283
|
-
enabled: userStore.enabled,
|
|
284
|
-
authenticated: Boolean(user),
|
|
285
|
-
user: user ? publicUser(user) : null,
|
|
286
|
-
});
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (request.method === "POST" && url.pathname === "/api/auth/login") {
|
|
290
|
-
response.setHeader("Cache-Control", "no-store");
|
|
291
|
-
const body = await readJsonBody(request);
|
|
292
|
-
const username = typeof body.username === "string" ? body.username : "";
|
|
293
|
-
const password = typeof body.password === "string" ? body.password : "";
|
|
294
|
-
const loginBuckets = loginAttemptBuckets(request, username);
|
|
295
|
-
const blockedUntil = userStore.loginBlockedUntil(loginBuckets.map((bucket) => bucket.key));
|
|
296
|
-
if (blockedUntil > Date.now()) {
|
|
297
|
-
response.setHeader("Retry-After", String(Math.max(1, Math.ceil((blockedUntil - Date.now()) / 1000))));
|
|
298
|
-
sendJson(response, 429, { error: "login_rate_limited" });
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (activeLoginVerifications >= LOGIN_MAX_CONCURRENT_VERIFICATIONS) {
|
|
302
|
-
response.setHeader("Retry-After", "1");
|
|
303
|
-
sendJson(response, 429, { error: "login_rate_limited" });
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
activeLoginVerifications += 1;
|
|
307
|
-
let user;
|
|
308
|
-
try {
|
|
309
|
-
user = await userStore.authenticate(username, password);
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
activeLoginVerifications -= 1;
|
|
313
|
-
}
|
|
314
|
-
if (!user) {
|
|
315
|
-
userStore.recordLoginFailure(loginBuckets, LOGIN_FAILURE_WINDOW_MS, LOGIN_LOCKOUT_MS);
|
|
316
|
-
sendJson(response, 401, { error: "invalid_credentials" });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
userStore.clearLoginFailures(loginBuckets.slice(0, 2).map((bucket) => bucket.key));
|
|
320
|
-
setSessionCookie(response, request, {
|
|
321
|
-
userId: user.id,
|
|
322
|
-
sessionVersion: user.sessionVersion,
|
|
323
|
-
ttlSeconds: config.authSessionTtlSeconds,
|
|
324
|
-
secret: userStore.sessionSecret,
|
|
325
|
-
path: config.publicBasePath,
|
|
326
|
-
});
|
|
327
|
-
sendJson(response, 200, { ok: true, user: publicUser(user) });
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (request.method === "POST" && url.pathname === "/api/auth/logout") {
|
|
331
|
-
response.setHeader("Cache-Control", "no-store");
|
|
332
|
-
clearSessionCookie(response, request, config.publicBasePath);
|
|
333
|
-
sendJson(response, 200, { ok: true });
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
337
|
-
const health = await buildHealthSnapshot();
|
|
338
|
-
sendJson(response, health.ok ? 200 : 503, health);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
const isProfuseCallback = request.method === "POST" && url.pathname === "/callback/profuse/message";
|
|
342
|
-
const isProtectedRequest = url.pathname.startsWith("/api/") || url.pathname.startsWith("/agent/");
|
|
343
|
-
const authenticatedUser = readAuthenticatedUser(request);
|
|
344
|
-
if (isProtectedRequest &&
|
|
345
|
-
!isProfuseCallback &&
|
|
346
|
-
!authenticatedUser) {
|
|
347
|
-
sendJson(response, 401, { error: "authentication_required" });
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const requestedWorkspaceId = workspaceIdFromApiPath(url.pathname) ?? workspaceIdFromAgentPath(url.pathname);
|
|
351
|
-
if (authenticatedUser &&
|
|
352
|
-
requestedWorkspaceId &&
|
|
353
|
-
!canAccessWorkspace(authenticatedUser, requestedWorkspaceId)) {
|
|
354
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
const requestedGroupId = workspaceGroupIdFromApiPath(url.pathname);
|
|
358
|
-
if (authenticatedUser &&
|
|
359
|
-
requestedGroupId &&
|
|
360
|
-
!canManageWorkspaceGroup(authenticatedUser, requestedGroupId)) {
|
|
361
|
-
sendJson(response, 404, { error: "workspace_group_not_found" });
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (authenticatedUser &&
|
|
365
|
-
requiresAdminAccess(request.method ?? "GET", url.pathname) &&
|
|
366
|
-
authenticatedUser.role !== "admin") {
|
|
367
|
-
sendJson(response, 403, { error: "admin_required" });
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (request.method === "GET" && url.pathname === "/api/diagnostics") {
|
|
371
|
-
sendJson(response, 200, await buildDiagnosticsSnapshot());
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
if (request.method === "GET" && url.pathname === "/api/admin/users") {
|
|
375
|
-
sendJson(response, 200, { users: userStore.listUsers().map(publicUser) });
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (request.method === "POST" && url.pathname === "/api/admin/users") {
|
|
379
|
-
const body = await readJsonBody(request);
|
|
380
|
-
try {
|
|
381
|
-
const user = userStore.createUser({
|
|
382
|
-
username: typeof body.username === "string" ? body.username : "",
|
|
383
|
-
password: typeof body.password === "string" ? body.password : "",
|
|
384
|
-
role: normalizeUserRole(body.role),
|
|
385
|
-
});
|
|
386
|
-
sendJson(response, 201, { user: publicUser(user) });
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
sendJson(response, 400, {
|
|
390
|
-
error: userMutationError(error),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
const adminUserMatch = url.pathname.match(/^\/api\/admin\/users\/([^/]+)$/);
|
|
396
|
-
if (request.method === "PATCH" && adminUserMatch) {
|
|
397
|
-
const userId = decodeURIComponent(adminUserMatch[1]);
|
|
398
|
-
const current = userStore.getUser(userId);
|
|
399
|
-
if (!current) {
|
|
400
|
-
sendJson(response, 404, { error: "user_not_found" });
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
const body = await readJsonBody(request);
|
|
404
|
-
const role = body.role === undefined ? undefined : normalizeUserRole(body.role);
|
|
405
|
-
const active = body.active === undefined ? undefined : body.active === true;
|
|
406
|
-
if (userId === authenticatedUser.id &&
|
|
407
|
-
((role !== undefined && role !== "admin") || active === false)) {
|
|
408
|
-
sendJson(response, 409, { error: "cannot_remove_own_admin_access" });
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
if (current.role === "admin" &&
|
|
412
|
-
current.active &&
|
|
413
|
-
((role !== undefined && role !== "admin") || active === false) &&
|
|
414
|
-
activeAdminCount() <= 1) {
|
|
415
|
-
sendJson(response, 409, { error: "last_admin_required" });
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
try {
|
|
419
|
-
const user = userStore.updateUser(userId, {
|
|
420
|
-
username: typeof body.username === "string" ? body.username : undefined,
|
|
421
|
-
password: typeof body.password === "string" ? body.password : undefined,
|
|
422
|
-
role,
|
|
423
|
-
active,
|
|
424
|
-
});
|
|
425
|
-
if (userId === authenticatedUser.id &&
|
|
426
|
-
typeof body.password === "string" &&
|
|
427
|
-
body.password.length > 0) {
|
|
428
|
-
setSessionCookie(response, request, {
|
|
429
|
-
userId: user.id,
|
|
430
|
-
sessionVersion: user.sessionVersion,
|
|
431
|
-
ttlSeconds: config.authSessionTtlSeconds,
|
|
432
|
-
secret: userStore.sessionSecret,
|
|
433
|
-
path: config.publicBasePath,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
sendJson(response, 200, { user: publicUser(user) });
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
sendJson(response, 400, { error: userMutationError(error) });
|
|
440
|
-
}
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (request.method === "DELETE" && adminUserMatch) {
|
|
444
|
-
const userId = decodeURIComponent(adminUserMatch[1]);
|
|
445
|
-
const current = userStore.getUser(userId);
|
|
446
|
-
if (!current) {
|
|
447
|
-
sendJson(response, 404, { error: "user_not_found" });
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (userId === authenticatedUser.id) {
|
|
451
|
-
sendJson(response, 409, { error: "cannot_delete_current_user" });
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (listConversations().some((conversation) => conversation.ownerId === userId)) {
|
|
455
|
-
sendJson(response, 409, { error: "user_owns_workspaces" });
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
if (listWorkspaceGroups().some((group) => group.ownerId === userId)) {
|
|
459
|
-
sendJson(response, 409, { error: "user_owns_workspace_groups" });
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
if (current.role === "admin" && current.active && activeAdminCount() <= 1) {
|
|
463
|
-
sendJson(response, 409, { error: "last_admin_required" });
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
userStore.deleteUser(userId);
|
|
467
|
-
sendJson(response, 200, { ok: true });
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
if (request.method === "GET" && url.pathname === "/api/meta") {
|
|
471
|
-
sendJson(response, 200, await getAppMeta());
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
const agentTranscriptMatch = url.pathname.match(/^\/agent\/workspaces\/([^/]+)\/transcript$/);
|
|
475
|
-
if (request.method === "GET" && agentTranscriptMatch) {
|
|
476
|
-
const conversation = getConversation(decodeURIComponent(agentTranscriptMatch[1]));
|
|
477
|
-
if (!conversation) {
|
|
478
|
-
sendJson(response, 404, { error: "workspace_not_found" });
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
const limit = readPositiveInteger(url.searchParams.get("limit")) ?? 100;
|
|
482
|
-
const before = url.searchParams.get("before");
|
|
483
|
-
const messages = listConversationTranscriptMessagesBefore(conversation, before, limit);
|
|
484
|
-
sendJson(response, 200, {
|
|
485
|
-
workspace: {
|
|
486
|
-
id: conversation.id,
|
|
487
|
-
name: conversation.title,
|
|
488
|
-
},
|
|
489
|
-
order: "chronological",
|
|
490
|
-
messages,
|
|
491
|
-
page: {
|
|
492
|
-
limit: Math.max(1, Math.min(500, Math.trunc(limit))),
|
|
493
|
-
oldestMessageId: messages[0]?.id ?? null,
|
|
494
|
-
nextOlderUrl: messages[0]
|
|
495
|
-
? `${getAgentTranscriptApiBase(conversation)}?limit=${Math.max(1, Math.min(500, Math.trunc(limit)))}&before=${encodeURIComponent(messages[0].id)}`
|
|
496
|
-
: null,
|
|
497
|
-
},
|
|
498
|
-
});
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
const agentTranscriptRangeMatch = url.pathname.match(/^\/agent\/workspaces\/([^/]+)\/transcript\/range$/);
|
|
502
|
-
if (request.method === "GET" && agentTranscriptRangeMatch) {
|
|
503
|
-
const conversation = getConversation(decodeURIComponent(agentTranscriptRangeMatch[1]));
|
|
504
|
-
if (!conversation) {
|
|
505
|
-
sendJson(response, 404, { error: "workspace_not_found" });
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const startMessageId = url.searchParams.get("startMessageId") ?? "";
|
|
509
|
-
const endMessageId = url.searchParams.get("endMessageId") ?? "";
|
|
510
|
-
if (!isTranscriptMessageId(startMessageId) || !isTranscriptMessageId(endMessageId)) {
|
|
511
|
-
sendJson(response, 400, { error: "invalid_message_range" });
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
const messages = listConversationTranscriptMessageRange(conversation, startMessageId, endMessageId, readPositiveInteger(url.searchParams.get("limit")) ?? 500);
|
|
515
|
-
sendJson(response, 200, {
|
|
516
|
-
workspace: {
|
|
517
|
-
id: conversation.id,
|
|
518
|
-
name: conversation.title,
|
|
519
|
-
},
|
|
520
|
-
order: "chronological",
|
|
521
|
-
range: {
|
|
522
|
-
startMessageId,
|
|
523
|
-
endMessageId,
|
|
524
|
-
},
|
|
525
|
-
messages,
|
|
526
|
-
});
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
const agentTranscriptSummariesMatch = url.pathname.match(/^\/agent\/workspaces\/([^/]+)\/transcript\/summaries$/);
|
|
530
|
-
if (request.method === "GET" && agentTranscriptSummariesMatch) {
|
|
531
|
-
const conversation = getConversation(decodeURIComponent(agentTranscriptSummariesMatch[1]));
|
|
532
|
-
if (!conversation) {
|
|
533
|
-
sendJson(response, 404, { error: "workspace_not_found" });
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
sendJson(response, 200, {
|
|
537
|
-
workspace: {
|
|
538
|
-
id: conversation.id,
|
|
539
|
-
name: conversation.title,
|
|
540
|
-
},
|
|
541
|
-
summaries: listTranscriptRangeSummaries(conversation),
|
|
542
|
-
});
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
const agentKnowledgeMatch = url.pathname.match(/^\/agent\/workspaces\/([^/]+)\/knowledge$/);
|
|
546
|
-
if (request.method === "GET" && agentKnowledgeMatch) {
|
|
547
|
-
const conversation = getConversation(decodeURIComponent(agentKnowledgeMatch[1]));
|
|
548
|
-
if (!conversation) {
|
|
549
|
-
sendJson(response, 404, { error: "workspace_not_found" });
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
sendJson(response, 200, {
|
|
553
|
-
workspace: {
|
|
554
|
-
id: conversation.id,
|
|
555
|
-
name: conversation.title,
|
|
556
|
-
},
|
|
557
|
-
items: listWorkspaceKnowledge(conversation).map((item) => ({
|
|
558
|
-
id: item.id,
|
|
559
|
-
name: item.name,
|
|
560
|
-
purpose: item.purpose,
|
|
561
|
-
updatedAt: item.updatedAt,
|
|
562
|
-
bodyUrl: `${getAgentKnowledgeApiBase(conversation)}/${encodeURIComponent(item.id)}`,
|
|
563
|
-
})),
|
|
564
|
-
});
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const agentKnowledgeBodyMatch = url.pathname.match(/^\/agent\/workspaces\/([^/]+)\/knowledge\/([^/]+)$/);
|
|
568
|
-
if (request.method === "GET" && agentKnowledgeBodyMatch) {
|
|
569
|
-
const conversation = getConversation(decodeURIComponent(agentKnowledgeBodyMatch[1]));
|
|
570
|
-
if (!conversation) {
|
|
571
|
-
sendJson(response, 404, { error: "workspace_not_found" });
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
const knowledgeId = decodeURIComponent(agentKnowledgeBodyMatch[2]);
|
|
575
|
-
const item = listWorkspaceKnowledge(conversation).find((candidate) => candidate.id === knowledgeId);
|
|
576
|
-
if (!item) {
|
|
577
|
-
sendJson(response, 404, { error: "knowledge_not_found" });
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
sendJson(response, 200, {
|
|
581
|
-
workspace: {
|
|
582
|
-
id: conversation.id,
|
|
583
|
-
name: conversation.title,
|
|
584
|
-
},
|
|
585
|
-
knowledge: {
|
|
586
|
-
id: item.id,
|
|
587
|
-
name: item.name,
|
|
588
|
-
purpose: item.purpose,
|
|
589
|
-
content: item.description,
|
|
590
|
-
updatedAt: item.updatedAt,
|
|
591
|
-
},
|
|
592
|
-
});
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (request.method === "GET" && url.pathname === "/api/conversations") {
|
|
596
|
-
sendJson(response, 200, {
|
|
597
|
-
conversations: visibleConversations(authenticatedUser),
|
|
598
|
-
});
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (request.method === "GET" && url.pathname === "/api/workspace-unread") {
|
|
602
|
-
sendJson(response, 200, {
|
|
603
|
-
workspaces: visibleConversations(authenticatedUser).map((conversation) => ({
|
|
604
|
-
id: conversation.id,
|
|
605
|
-
unreadTurnCount: conversation.unreadTurnCount,
|
|
606
|
-
})),
|
|
607
|
-
});
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
if (request.method === "GET" && url.pathname === "/api/workspace-groups") {
|
|
611
|
-
sendJson(response, 200, {
|
|
612
|
-
groups: visibleWorkspaceGroups(authenticatedUser),
|
|
613
|
-
});
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (request.method === "GET" && url.pathname === "/api/settings/environment") {
|
|
617
|
-
sendJson(response, 200, {
|
|
618
|
-
variables: listGlobalEnvironmentVariables().map((item) => ({
|
|
619
|
-
key: item.key,
|
|
620
|
-
description: item.description,
|
|
621
|
-
hasValue: item.value.length > 0,
|
|
622
|
-
createdAt: item.createdAt,
|
|
623
|
-
updatedAt: item.updatedAt,
|
|
624
|
-
})),
|
|
625
|
-
});
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
if (request.method === "PUT" && url.pathname === "/api/settings/environment") {
|
|
629
|
-
const body = await readJsonBody(request);
|
|
630
|
-
const key = typeof body.key === "string" ? body.key.trim() : "";
|
|
631
|
-
const value = typeof body.value === "string" ? body.value : "";
|
|
632
|
-
const description = typeof body.description === "string" ? body.description.trim() : "";
|
|
633
|
-
if (!isValidEnvironmentVariableKey(key)) {
|
|
634
|
-
sendJson(response, 400, { error: "invalid_environment_key" });
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const variable = upsertGlobalEnvironmentVariable(key, value, description);
|
|
638
|
-
sendJson(response, 200, {
|
|
639
|
-
variable: {
|
|
640
|
-
key: variable.key,
|
|
641
|
-
description: variable.description,
|
|
642
|
-
hasValue: variable.value.length > 0,
|
|
643
|
-
createdAt: variable.createdAt,
|
|
644
|
-
updatedAt: variable.updatedAt,
|
|
645
|
-
},
|
|
646
|
-
});
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
if (request.method === "GET" && url.pathname === "/api/settings/runtime") {
|
|
650
|
-
sendJson(response, 200, {
|
|
651
|
-
settings: getRuntimeSettings(),
|
|
652
|
-
});
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (request.method === "GET" && url.pathname === "/api/settings/runtime/codex") {
|
|
656
|
-
const codexVersion = await readManagedCodexPackageVersion();
|
|
657
|
-
const codexReleasedAt = codexVersion
|
|
658
|
-
? await getCodexReleaseTime(codexVersion)
|
|
659
|
-
: null;
|
|
660
|
-
sendJson(response, 200, {
|
|
661
|
-
codexVersion: codexVersion ? `codex-cli ${codexVersion}` : "unknown",
|
|
662
|
-
codexReleasedAt,
|
|
663
|
-
});
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (request.method === "GET" && url.pathname === "/api/settings/runtime/models") {
|
|
667
|
-
try {
|
|
668
|
-
let cached = readRuntimeModelCatalogCache();
|
|
669
|
-
const shouldRefresh = url.searchParams.get("refresh") !== "0";
|
|
670
|
-
if (!cached) {
|
|
671
|
-
const catalog = await refreshRuntimeModelCatalog();
|
|
672
|
-
cached = {
|
|
673
|
-
catalog,
|
|
674
|
-
updatedAt: new Date().toISOString(),
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
else if (shouldRefresh) {
|
|
678
|
-
void refreshRuntimeModelCatalog().catch((error) => {
|
|
679
|
-
console.error("runtime model catalog refresh failed", error);
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
const catalog = cached.catalog;
|
|
683
|
-
const codexReleasedAt = await getCodexReleaseTime(catalog.codexVersion);
|
|
684
|
-
sendJson(response, 200, {
|
|
685
|
-
...catalog,
|
|
686
|
-
codexReleasedAt,
|
|
687
|
-
catalogUpdatedAt: cached.updatedAt,
|
|
688
|
-
refreshing: runtimeModelCatalogRefresh !== null,
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
catch (error) {
|
|
692
|
-
sendJson(response, 502, {
|
|
693
|
-
error: "runtime_model_catalog_unavailable",
|
|
694
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (request.method === "GET" && url.pathname === "/api/settings/runtime/rate-limits") {
|
|
700
|
-
const cached = readRuntimeRateLimitsCache();
|
|
701
|
-
if (url.searchParams.get("refresh") !== "1") {
|
|
702
|
-
sendJson(response, 200, { usage: cached });
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
try {
|
|
706
|
-
const usage = await refreshRuntimeRateLimits();
|
|
707
|
-
sendJson(response, 200, { usage });
|
|
708
|
-
}
|
|
709
|
-
catch (error) {
|
|
710
|
-
if (cached) {
|
|
711
|
-
sendJson(response, 200, {
|
|
712
|
-
usage: cached,
|
|
713
|
-
refreshError: error instanceof Error ? error.message : String(error),
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
sendJson(response, 502, {
|
|
718
|
-
error: "runtime_rate_limits_unavailable",
|
|
719
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
if (request.method === "GET" && url.pathname === "/api/settings/runtime/codex/update") {
|
|
726
|
-
if (url.searchParams.get("refreshLatest") === "1") {
|
|
727
|
-
codexLatestVersion = await fetchLatestCodexVersion();
|
|
728
|
-
}
|
|
729
|
-
sendJson(response, 200, {
|
|
730
|
-
job: codexUpdateJob,
|
|
731
|
-
latest: codexLatestVersion,
|
|
732
|
-
});
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
if (request.method === "POST" && url.pathname === "/api/settings/runtime/codex/update") {
|
|
736
|
-
if (codexUpdateJob?.status === "running") {
|
|
737
|
-
sendJson(response, 409, {
|
|
738
|
-
error: "codex_update_in_progress",
|
|
739
|
-
job: codexUpdateJob,
|
|
740
|
-
});
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
if (activeConversationTurns.size > 0) {
|
|
744
|
-
sendJson(response, 409, {
|
|
745
|
-
error: "runtime_busy",
|
|
746
|
-
detail: "Wait for running workspace turns to finish before updating Codex CLI.",
|
|
747
|
-
});
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
codexUpdateJob = {
|
|
751
|
-
id: randomUUID(),
|
|
752
|
-
status: "running",
|
|
753
|
-
startedAt: new Date().toISOString(),
|
|
754
|
-
finishedAt: null,
|
|
755
|
-
previousVersion: null,
|
|
756
|
-
currentVersion: null,
|
|
757
|
-
codexReleasedAt: null,
|
|
758
|
-
logs: [],
|
|
759
|
-
error: null,
|
|
760
|
-
};
|
|
761
|
-
appendCodexUpdateLog(codexUpdateJob, "system", "Codex CLI update requested.");
|
|
762
|
-
sendJson(response, 202, { job: codexUpdateJob });
|
|
763
|
-
void runCodexUpdateJob(codexUpdateJob);
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
if (request.method === "GET" && url.pathname === "/api/settings/memory") {
|
|
767
|
-
const items = getGlobalMemoryItems();
|
|
768
|
-
sendJson(response, 200, {
|
|
769
|
-
memory: formatMemoryItems(items),
|
|
770
|
-
items,
|
|
771
|
-
});
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
if (request.method === "PUT" && url.pathname === "/api/settings/memory") {
|
|
775
|
-
const body = await readJsonBody(request);
|
|
776
|
-
if (!Array.isArray(body.items)) {
|
|
777
|
-
sendJson(response, 400, { error: "invalid_memory" });
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
const validationError = validateMemoryRequestItems(body.items);
|
|
781
|
-
if (validationError) {
|
|
782
|
-
sendJson(response, 400, validationError);
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
const items = normalizeMemoryRequestItems(body.items);
|
|
786
|
-
upsertAppSetting(GLOBAL_MEMORY_ITEMS_SETTING_KEY, JSON.stringify(items));
|
|
787
|
-
sendJson(response, 200, {
|
|
788
|
-
memory: formatMemoryItems(items),
|
|
789
|
-
items,
|
|
790
|
-
});
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if (request.method === "PUT" && url.pathname === "/api/settings/runtime") {
|
|
794
|
-
const body = await readJsonBody(request);
|
|
795
|
-
const model = typeof body.model === "string" ? body.model.trim() : "";
|
|
796
|
-
const reasoningEffort = normalizeRuntimeReasoningEffort(body.reasoningEffort);
|
|
797
|
-
const fastMode = body.fastMode === true;
|
|
798
|
-
const sandbox = isConversationSandbox(body.sandbox)
|
|
799
|
-
? body.sandbox
|
|
800
|
-
: getGlobalRuntimeSettings().sandbox;
|
|
801
|
-
const notifyEnabled = body.notifyEnabled !== false;
|
|
802
|
-
upsertAppSetting(RUNTIME_MODEL_SETTING_KEY, model);
|
|
803
|
-
upsertAppSetting(RUNTIME_REASONING_EFFORT_SETTING_KEY, reasoningEffort);
|
|
804
|
-
upsertAppSetting(RUNTIME_FAST_MODE_SETTING_KEY, fastMode ? "true" : "false");
|
|
805
|
-
upsertAppSetting(RUNTIME_SANDBOX_SETTING_KEY, sandbox);
|
|
806
|
-
upsertAppSetting(RUNTIME_NOTIFY_ENABLED_SETTING_KEY, notifyEnabled ? "true" : "false");
|
|
807
|
-
sendJson(response, 200, {
|
|
808
|
-
settings: getRuntimeSettings(),
|
|
809
|
-
});
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
const globalEnvironmentMatch = url.pathname.match(/^\/api\/settings\/environment\/([^/]+)$/);
|
|
813
|
-
const globalEnvironmentRevealMatch = url.pathname.match(/^\/api\/settings\/environment\/([^/]+)\/reveal$/);
|
|
814
|
-
if (request.method === "GET" && globalEnvironmentRevealMatch) {
|
|
815
|
-
const key = decodeURIComponent(globalEnvironmentRevealMatch[1]);
|
|
816
|
-
if (!isValidEnvironmentVariableKey(key)) {
|
|
817
|
-
sendJson(response, 400, { error: "invalid_environment_key" });
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const variable = revealGlobalEnvironmentVariable(key);
|
|
821
|
-
if (!variable) {
|
|
822
|
-
sendJson(response, 404, { error: "environment_variable_not_found" });
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
sendJson(response, 200, {
|
|
826
|
-
variable: {
|
|
827
|
-
key: variable.key,
|
|
828
|
-
value: variable.value,
|
|
829
|
-
},
|
|
830
|
-
});
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
if (request.method === "DELETE" && globalEnvironmentMatch) {
|
|
834
|
-
const key = decodeURIComponent(globalEnvironmentMatch[1]);
|
|
835
|
-
if (!isValidEnvironmentVariableKey(key)) {
|
|
836
|
-
sendJson(response, 400, { error: "invalid_environment_key" });
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
if (!deleteGlobalEnvironmentVariable(key)) {
|
|
840
|
-
sendJson(response, 404, { error: "environment_variable_not_found" });
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
sendJson(response, 200, { ok: true });
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
if (request.method === "POST" && url.pathname === "/api/workspace-groups") {
|
|
847
|
-
const body = await readJsonBody(request);
|
|
848
|
-
if (typeof body.title !== "string" || !body.title.trim()) {
|
|
849
|
-
sendJson(response, 400, { error: "title_required" });
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
const group = createWorkspaceGroup(body.title.trim(), authenticatedUser.id);
|
|
853
|
-
sendJson(response, 201, { group });
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
const workspaceGroupMatch = url.pathname.match(/^\/api\/workspace-groups\/([^/]+)$/);
|
|
857
|
-
if (request.method === "PATCH" && workspaceGroupMatch) {
|
|
858
|
-
if (!hasWorkspaceGroup(workspaceGroupMatch[1])) {
|
|
859
|
-
sendJson(response, 404, { error: "workspace_group_not_found" });
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
const body = await readJsonBody(request);
|
|
863
|
-
if (typeof body.title !== "string" || !body.title.trim()) {
|
|
864
|
-
sendJson(response, 400, { error: "title_required" });
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
const group = setWorkspaceGroupTitle(workspaceGroupMatch[1], body.title.trim());
|
|
868
|
-
if (!group) {
|
|
869
|
-
sendJson(response, 404, { error: "workspace_group_not_found" });
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
sendJson(response, 200, { group });
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
if (request.method === "DELETE" && workspaceGroupMatch) {
|
|
876
|
-
if (!hasWorkspaceGroup(workspaceGroupMatch[1])) {
|
|
877
|
-
sendJson(response, 404, { error: "workspace_group_not_found" });
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
if (!deleteWorkspaceGroup(workspaceGroupMatch[1])) {
|
|
881
|
-
sendJson(response, 409, { error: "workspace_group_not_deletable" });
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
sendJson(response, 200, {
|
|
885
|
-
conversations: visibleConversations(authenticatedUser),
|
|
886
|
-
groups: visibleWorkspaceGroups(authenticatedUser),
|
|
887
|
-
});
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
if (request.method === "POST" && url.pathname === "/api/conversations") {
|
|
891
|
-
const body = await readJsonBody(request);
|
|
892
|
-
if (typeof body.title !== "string" || !body.title.trim()) {
|
|
893
|
-
sendJson(response, 400, { error: "title_required" });
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
const requestedGroupId = typeof body.groupId === "string" ? body.groupId : "default";
|
|
897
|
-
if (!canAccessWorkspaceGroup(authenticatedUser, requestedGroupId)) {
|
|
898
|
-
sendJson(response, 400, { error: "invalid_group" });
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const conversation = createConversation({
|
|
902
|
-
title: body.title.trim(),
|
|
903
|
-
groupId: typeof body.groupId === "string" ? body.groupId : undefined,
|
|
904
|
-
workspaceRoot: config.workspaceRoot,
|
|
905
|
-
sandbox: getGlobalRuntimeSettings().sandbox,
|
|
906
|
-
ownerId: authenticatedUser.id,
|
|
907
|
-
});
|
|
908
|
-
sendJson(response, 201, {
|
|
909
|
-
conversation: {
|
|
910
|
-
...conversation,
|
|
911
|
-
nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
|
|
912
|
-
unreadTurnCount: 0,
|
|
913
|
-
},
|
|
914
|
-
});
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const conversationReadMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/read$/);
|
|
918
|
-
if (request.method === "POST" && conversationReadMatch) {
|
|
919
|
-
const conversation = getConversation(conversationReadMatch[1]);
|
|
920
|
-
if (!conversation) {
|
|
921
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
markConversationRead(conversation, authenticatedUser.id);
|
|
925
|
-
sendJson(response, 200, { ok: true, unreadTurnCount: 0 });
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
const conversationMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)$/);
|
|
929
|
-
if (request.method === "GET" && conversationMatch) {
|
|
930
|
-
const conversation = getConversation(conversationMatch[1]);
|
|
931
|
-
if (!conversation) {
|
|
932
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
const page = getConversationHistoryPage(conversation, {
|
|
936
|
-
before: url.searchParams.get("before"),
|
|
937
|
-
limit: readPositiveInteger(url.searchParams.get("limit")),
|
|
938
|
-
});
|
|
939
|
-
sendJson(response, 200, {
|
|
940
|
-
conversation: {
|
|
941
|
-
...conversation,
|
|
942
|
-
messages: page.messages,
|
|
943
|
-
events: page.events,
|
|
944
|
-
history: page.history,
|
|
945
|
-
nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
|
|
946
|
-
unreadTurnCount: countConversationUnreadTurns(conversation, authenticatedUser.id),
|
|
947
|
-
},
|
|
948
|
-
});
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
if (request.method === "PATCH" && conversationMatch) {
|
|
952
|
-
const conversation = getConversation(conversationMatch[1]);
|
|
953
|
-
if (!conversation) {
|
|
954
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
const body = await readJsonBody(request);
|
|
958
|
-
const hasTitle = Object.prototype.hasOwnProperty.call(body, "title");
|
|
959
|
-
const hasSandbox = Object.prototype.hasOwnProperty.call(body, "sandbox");
|
|
960
|
-
const hasGroup = Object.prototype.hasOwnProperty.call(body, "groupId");
|
|
961
|
-
const hasMemory = Object.prototype.hasOwnProperty.call(body, "memory");
|
|
962
|
-
const hasMemoryItems = Object.prototype.hasOwnProperty.call(body, "memoryItems");
|
|
963
|
-
const hasResponsibility = Object.prototype.hasOwnProperty.call(body, "responsibility");
|
|
964
|
-
if (!hasTitle &&
|
|
965
|
-
!hasSandbox &&
|
|
966
|
-
!hasGroup &&
|
|
967
|
-
!hasMemory &&
|
|
968
|
-
!hasMemoryItems &&
|
|
969
|
-
!hasResponsibility) {
|
|
970
|
-
sendJson(response, 400, { error: "patch_required" });
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
if (hasTitle) {
|
|
974
|
-
if (typeof body.title !== "string" || !body.title.trim()) {
|
|
975
|
-
sendJson(response, 400, { error: "title_required" });
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
if (hasSandbox) {
|
|
980
|
-
if (!isConversationSandbox(body.sandbox)) {
|
|
981
|
-
sendJson(response, 400, { error: "invalid_sandbox" });
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
if (conversation.status === "running") {
|
|
985
|
-
sendJson(response, 409, { error: "conversation_running" });
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
if (hasGroup) {
|
|
990
|
-
if (typeof body.groupId !== "string" ||
|
|
991
|
-
!canAccessWorkspaceGroup(authenticatedUser, body.groupId)) {
|
|
992
|
-
sendJson(response, 400, { error: "invalid_group" });
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
if (hasMemory && typeof body.memory !== "string") {
|
|
997
|
-
sendJson(response, 400, { error: "invalid_memory" });
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
if (hasMemoryItems && !Array.isArray(body.memoryItems)) {
|
|
1001
|
-
sendJson(response, 400, { error: "invalid_memory_items" });
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (hasMemoryItems) {
|
|
1005
|
-
const validationError = validateMemoryRequestItems(body.memoryItems);
|
|
1006
|
-
if (validationError) {
|
|
1007
|
-
sendJson(response, 400, validationError);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
if (hasResponsibility && typeof body.responsibility !== "string") {
|
|
1012
|
-
sendJson(response, 400, { error: "invalid_responsibility" });
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
if (hasTitle) {
|
|
1016
|
-
setConversationTitle(conversation, body.title.trim());
|
|
1017
|
-
}
|
|
1018
|
-
if (hasSandbox) {
|
|
1019
|
-
setConversationSandbox(conversation, body.sandbox);
|
|
1020
|
-
}
|
|
1021
|
-
if (hasGroup) {
|
|
1022
|
-
setConversationGroup(conversation, body.groupId);
|
|
1023
|
-
}
|
|
1024
|
-
if (hasMemory) {
|
|
1025
|
-
setConversationMemory(conversation, body.memory);
|
|
1026
|
-
}
|
|
1027
|
-
if (hasMemoryItems) {
|
|
1028
|
-
setConversationMemoryItems(conversation, normalizeMemoryRequestItems(body.memoryItems));
|
|
1029
|
-
}
|
|
1030
|
-
if (hasResponsibility) {
|
|
1031
|
-
setConversationResponsibility(conversation, body.responsibility);
|
|
1032
|
-
}
|
|
1033
|
-
sendJson(response, 200, {
|
|
1034
|
-
conversation: {
|
|
1035
|
-
...conversation,
|
|
1036
|
-
nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
|
|
1037
|
-
},
|
|
1038
|
-
});
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
const conversationRuntimeThreadReadMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/runtime\/thread-read$/);
|
|
1042
|
-
if (request.method === "GET" && conversationRuntimeThreadReadMatch) {
|
|
1043
|
-
const conversation = getConversation(conversationRuntimeThreadReadMatch[1]);
|
|
1044
|
-
if (!conversation) {
|
|
1045
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
if (!conversation.codexThreadId) {
|
|
1049
|
-
sendJson(response, 409, { error: "thread_not_started" });
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
try {
|
|
1053
|
-
const result = await runtime.readThread(config, {
|
|
1054
|
-
cwd: conversation.workspacePath,
|
|
1055
|
-
threadId: conversation.codexThreadId,
|
|
1056
|
-
});
|
|
1057
|
-
sendJson(response, 200, { result });
|
|
1058
|
-
}
|
|
1059
|
-
catch (error) {
|
|
1060
|
-
sendJson(response, 500, {
|
|
1061
|
-
error: "thread_read_failed",
|
|
1062
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
const conversationRuntimeTokenUsageMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/runtime\/token-usage$/);
|
|
1068
|
-
if (request.method === "GET" && conversationRuntimeTokenUsageMatch) {
|
|
1069
|
-
const conversation = getConversation(conversationRuntimeTokenUsageMatch[1]);
|
|
1070
|
-
if (!conversation) {
|
|
1071
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
sendJson(response, 200, {
|
|
1075
|
-
event: getLatestConversationNotification(conversation, "thread/tokenUsage/updated"),
|
|
1076
|
-
});
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
const conversationRuntimeAdditionalContextMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/runtime\/additional-context$/);
|
|
1080
|
-
if (request.method === "GET" && conversationRuntimeAdditionalContextMatch) {
|
|
1081
|
-
const conversation = getConversation(conversationRuntimeAdditionalContextMatch[1]);
|
|
1082
|
-
if (!conversation) {
|
|
1083
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
const runtimeConfig = withWorkspaceRuntimeSettings(config, conversation);
|
|
1087
|
-
const injectionContext = buildRuntimeInjectionContext(conversation);
|
|
1088
|
-
const context = buildRuntimeAdditionalContext({
|
|
1089
|
-
...injectionContext,
|
|
1090
|
-
workspacePath: conversation.workspacePath,
|
|
1091
|
-
globalEnvironment: describeRuntimeEnvironment(runtimeConfig),
|
|
1092
|
-
});
|
|
1093
|
-
sendJson(response, 200, {
|
|
1094
|
-
context,
|
|
1095
|
-
generatedAt: new Date().toISOString(),
|
|
1096
|
-
});
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
const conversationRuntimeCompactionsMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/runtime\/compactions$/);
|
|
1100
|
-
if (request.method === "GET" && conversationRuntimeCompactionsMatch) {
|
|
1101
|
-
const conversation = getConversation(conversationRuntimeCompactionsMatch[1]);
|
|
1102
|
-
if (!conversation) {
|
|
1103
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
const events = listConversationEventsByType(conversation, [
|
|
1107
|
-
"runtime.compaction.started",
|
|
1108
|
-
"runtime.compaction.completed",
|
|
1109
|
-
"runtime.compaction.usage_observed",
|
|
1110
|
-
], 300);
|
|
1111
|
-
sendJson(response, 200, {
|
|
1112
|
-
compactions: summarizeRuntimeCompactions(events),
|
|
1113
|
-
});
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
const conversationRuntimeResetMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/runtime\/reset$/);
|
|
1117
|
-
const conversationRuntimeResetPreviewMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/runtime\/reset-preview$/);
|
|
1118
|
-
if (request.method === "GET" && conversationRuntimeResetPreviewMatch) {
|
|
1119
|
-
const conversation = getConversation(conversationRuntimeResetPreviewMatch[1]);
|
|
1120
|
-
if (!conversation) {
|
|
1121
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
const plan = buildRuntimeResetPlan(conversation);
|
|
1125
|
-
sendJson(response, 200, {
|
|
1126
|
-
conversationId: conversation.id,
|
|
1127
|
-
currentThreadId: conversation.codexThreadId,
|
|
1128
|
-
generatedAt: new Date().toISOString(),
|
|
1129
|
-
fingerprint: plan.fingerprint,
|
|
1130
|
-
injectedItemCount: plan.visibleMessages.length,
|
|
1131
|
-
injectedUserMessageCount: plan.injectedUserMessageCount,
|
|
1132
|
-
injectedAssistantMessageCount: plan.injectedAssistantMessageCount,
|
|
1133
|
-
items: plan.injectedItems,
|
|
1134
|
-
});
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
if (request.method === "POST" && conversationRuntimeResetMatch) {
|
|
1138
|
-
const conversation = getConversation(conversationRuntimeResetMatch[1]);
|
|
1139
|
-
if (!conversation) {
|
|
1140
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
if (conversation.status === "running") {
|
|
1144
|
-
sendJson(response, 409, { error: "conversation_running" });
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
const body = await readJsonBody(request);
|
|
1148
|
-
if (typeof body.previewFingerprint !== "string") {
|
|
1149
|
-
sendJson(response, 400, { error: "reset_preview_required" });
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
const plan = buildRuntimeResetPlan(conversation);
|
|
1153
|
-
if (body.previewFingerprint !== plan.fingerprint) {
|
|
1154
|
-
sendJson(response, 409, {
|
|
1155
|
-
error: "reset_preview_changed",
|
|
1156
|
-
message: "Visible history changed after the reset preview was generated.",
|
|
1157
|
-
});
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
try {
|
|
1161
|
-
const result = await resetConversationRuntimeThread(conversation, {
|
|
1162
|
-
warmup: body.warmup !== false,
|
|
1163
|
-
}, plan);
|
|
1164
|
-
sendJson(response, 200, {
|
|
1165
|
-
ok: true,
|
|
1166
|
-
...result,
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
catch (error) {
|
|
1170
|
-
sendJson(response, 500, {
|
|
1171
|
-
ok: false,
|
|
1172
|
-
error: "runtime_reset_failed",
|
|
1173
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
return;
|
|
1177
|
-
}
|
|
1178
|
-
const conversationRuntimeResetsMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/runtime\/resets$/);
|
|
1179
|
-
if (request.method === "GET" && conversationRuntimeResetsMatch) {
|
|
1180
|
-
const conversation = getConversation(conversationRuntimeResetsMatch[1]);
|
|
1181
|
-
if (!conversation) {
|
|
1182
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
const limit = Number(url.searchParams.get("limit") ?? "10");
|
|
1186
|
-
sendJson(response, 200, {
|
|
1187
|
-
resets: listRuntimeResets(conversation, Number.isFinite(limit) ? limit : 10),
|
|
1188
|
-
});
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
|
-
const conversationRevisionMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/revision$/);
|
|
1192
|
-
if (request.method === "GET" && conversationRevisionMatch) {
|
|
1193
|
-
const conversation = getConversation(conversationRevisionMatch[1]);
|
|
1194
|
-
if (!conversation) {
|
|
1195
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
sendJson(response, 200, {
|
|
1199
|
-
revision: getConversationRevision(conversation),
|
|
1200
|
-
});
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
const conversationRuntimeSettingsMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/settings\/runtime$/);
|
|
1204
|
-
if (request.method === "GET" && conversationRuntimeSettingsMatch) {
|
|
1205
|
-
const conversation = getConversation(conversationRuntimeSettingsMatch[1]);
|
|
1206
|
-
if (!conversation) {
|
|
1207
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
sendJson(response, 200, {
|
|
1211
|
-
settings: getWorkspaceRuntimeSettings(conversation),
|
|
1212
|
-
});
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
if (request.method === "PUT" && conversationRuntimeSettingsMatch) {
|
|
1216
|
-
const conversation = getConversation(conversationRuntimeSettingsMatch[1]);
|
|
1217
|
-
if (!conversation) {
|
|
1218
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (conversation.status === "running") {
|
|
1222
|
-
sendJson(response, 409, { error: "conversation_running" });
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
const body = await readJsonBody(request);
|
|
1226
|
-
try {
|
|
1227
|
-
applyWorkspaceRuntimeSettingUpdate(conversation, body);
|
|
1228
|
-
}
|
|
1229
|
-
catch (error) {
|
|
1230
|
-
sendJson(response, 400, {
|
|
1231
|
-
error: "invalid_runtime_settings",
|
|
1232
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1233
|
-
});
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
sendJson(response, 200, {
|
|
1237
|
-
settings: getWorkspaceRuntimeSettings(conversation),
|
|
1238
|
-
conversation: {
|
|
1239
|
-
...conversation,
|
|
1240
|
-
nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
|
|
1241
|
-
},
|
|
1242
|
-
});
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
const conversationEnvironmentMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/settings\/environment$/);
|
|
1246
|
-
if (request.method === "GET" && conversationEnvironmentMatch) {
|
|
1247
|
-
const conversation = getConversation(conversationEnvironmentMatch[1]);
|
|
1248
|
-
if (!conversation) {
|
|
1249
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1250
|
-
return;
|
|
1251
|
-
}
|
|
1252
|
-
sendJson(response, 200, {
|
|
1253
|
-
variables: listConversationEnvironmentVariables(conversation).map((item) => ({
|
|
1254
|
-
key: item.key,
|
|
1255
|
-
description: item.description,
|
|
1256
|
-
hasValue: item.value.length > 0,
|
|
1257
|
-
createdAt: item.createdAt,
|
|
1258
|
-
updatedAt: item.updatedAt,
|
|
1259
|
-
})),
|
|
1260
|
-
});
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
if (request.method === "PUT" && conversationEnvironmentMatch) {
|
|
1264
|
-
const conversation = getConversation(conversationEnvironmentMatch[1]);
|
|
1265
|
-
if (!conversation) {
|
|
1266
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
const body = await readJsonBody(request);
|
|
1270
|
-
const key = typeof body.key === "string" ? body.key.trim() : "";
|
|
1271
|
-
const value = typeof body.value === "string" ? body.value : "";
|
|
1272
|
-
const description = typeof body.description === "string" ? body.description.trim() : "";
|
|
1273
|
-
if (!isValidEnvironmentVariableKey(key)) {
|
|
1274
|
-
sendJson(response, 400, { error: "invalid_environment_key" });
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
const variable = upsertConversationEnvironmentVariable(conversation, key, value, description);
|
|
1278
|
-
sendJson(response, 200, {
|
|
1279
|
-
variable: {
|
|
1280
|
-
key: variable.key,
|
|
1281
|
-
description: variable.description,
|
|
1282
|
-
hasValue: variable.value.length > 0,
|
|
1283
|
-
createdAt: variable.createdAt,
|
|
1284
|
-
updatedAt: variable.updatedAt,
|
|
1285
|
-
},
|
|
1286
|
-
conversation: {
|
|
1287
|
-
...conversation,
|
|
1288
|
-
nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
|
|
1289
|
-
},
|
|
1290
|
-
});
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
const conversationEnvironmentKeyMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/settings\/environment\/([^/]+)$/);
|
|
1294
|
-
const conversationEnvironmentRevealMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/settings\/environment\/([^/]+)\/reveal$/);
|
|
1295
|
-
if (request.method === "GET" && conversationEnvironmentRevealMatch) {
|
|
1296
|
-
const conversation = getConversation(conversationEnvironmentRevealMatch[1]);
|
|
1297
|
-
const key = decodeURIComponent(conversationEnvironmentRevealMatch[2]);
|
|
1298
|
-
if (!conversation) {
|
|
1299
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
if (!isValidEnvironmentVariableKey(key)) {
|
|
1303
|
-
sendJson(response, 400, { error: "invalid_environment_key" });
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const variable = revealConversationEnvironmentVariable(conversation, key);
|
|
1307
|
-
if (!variable) {
|
|
1308
|
-
sendJson(response, 404, { error: "environment_variable_not_found" });
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
sendJson(response, 200, {
|
|
1312
|
-
variable: {
|
|
1313
|
-
key: variable.key,
|
|
1314
|
-
value: variable.value,
|
|
1315
|
-
},
|
|
1316
|
-
});
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
if (request.method === "DELETE" && conversationEnvironmentKeyMatch) {
|
|
1320
|
-
const conversation = getConversation(conversationEnvironmentKeyMatch[1]);
|
|
1321
|
-
const key = decodeURIComponent(conversationEnvironmentKeyMatch[2]);
|
|
1322
|
-
if (!conversation) {
|
|
1323
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
if (!isValidEnvironmentVariableKey(key)) {
|
|
1327
|
-
sendJson(response, 400, { error: "invalid_environment_key" });
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
if (!deleteConversationEnvironmentVariable(conversation, key)) {
|
|
1331
|
-
sendJson(response, 404, { error: "environment_variable_not_found" });
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
sendJson(response, 200, {
|
|
1335
|
-
ok: true,
|
|
1336
|
-
conversation: {
|
|
1337
|
-
...conversation,
|
|
1338
|
-
nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
|
|
1339
|
-
},
|
|
1340
|
-
});
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
const conversationKnowledgeMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/knowledge$/);
|
|
1344
|
-
if (conversationKnowledgeMatch) {
|
|
1345
|
-
const conversation = getConversation(conversationKnowledgeMatch[1]);
|
|
1346
|
-
if (!conversation) {
|
|
1347
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
if (request.method === "GET") {
|
|
1351
|
-
sendJson(response, 200, {
|
|
1352
|
-
items: listWorkspaceKnowledge(conversation),
|
|
1353
|
-
});
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
if (request.method === "POST") {
|
|
1357
|
-
const body = await readJsonBody(request);
|
|
1358
|
-
const item = createWorkspaceKnowledge(conversation, body);
|
|
1359
|
-
sendJson(response, 201, { item });
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
const conversationKnowledgeItemMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/knowledge\/([^/]+)$/);
|
|
1364
|
-
if (conversationKnowledgeItemMatch) {
|
|
1365
|
-
const conversation = getConversation(conversationKnowledgeItemMatch[1]);
|
|
1366
|
-
if (!conversation) {
|
|
1367
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
const knowledgeId = decodeURIComponent(conversationKnowledgeItemMatch[2]);
|
|
1371
|
-
if (request.method === "PATCH") {
|
|
1372
|
-
const body = await readJsonBody(request);
|
|
1373
|
-
const item = updateWorkspaceKnowledge(conversation, knowledgeId, body);
|
|
1374
|
-
if (!item) {
|
|
1375
|
-
sendJson(response, 404, { error: "knowledge_not_found" });
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
sendJson(response, 200, { item });
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
if (request.method === "DELETE") {
|
|
1382
|
-
if (!deleteWorkspaceKnowledge(conversation, knowledgeId)) {
|
|
1383
|
-
sendJson(response, 404, { error: "knowledge_not_found" });
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
sendJson(response, 200, { ok: true });
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
if (request.method === "POST" &&
|
|
1391
|
-
url.pathname === "/callback/profuse/message") {
|
|
1392
|
-
if (!isAuthorizedProfuseRequest(request)) {
|
|
1393
|
-
sendJson(response, 401, { error: "unauthorized" });
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
const body = await readJsonBody(request);
|
|
1397
|
-
const conversationId = typeof body.instance_id === "string" ? body.instance_id.trim() : "";
|
|
1398
|
-
const content = typeof body.content === "string"
|
|
1399
|
-
? body.content
|
|
1400
|
-
: typeof body.message === "string"
|
|
1401
|
-
? body.message
|
|
1402
|
-
: "";
|
|
1403
|
-
if (!conversationId) {
|
|
1404
|
-
sendJson(response, 400, { error: "instance_id_required" });
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
if (!content.trim()) {
|
|
1408
|
-
sendJson(response, 400, { error: "content_required" });
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
|
-
const conversation = getConversation(conversationId);
|
|
1412
|
-
if (!conversation) {
|
|
1413
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
try {
|
|
1417
|
-
const result = enqueueConversationTurn(conversation, {
|
|
1418
|
-
message: content,
|
|
1419
|
-
source: "profuse",
|
|
1420
|
-
});
|
|
1421
|
-
sendJson(response, 202, {
|
|
1422
|
-
success: true,
|
|
1423
|
-
mode: result.mode,
|
|
1424
|
-
turnId: result.turnId,
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
catch (error) {
|
|
1428
|
-
setConversationStatus(conversation, "error");
|
|
1429
|
-
sendJson(response, 500, {
|
|
1430
|
-
error: "turn_start_failed",
|
|
1431
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
const conversationFileMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/files$/);
|
|
1437
|
-
if (request.method === "GET" && conversationFileMatch) {
|
|
1438
|
-
const conversation = getConversation(conversationFileMatch[1]);
|
|
1439
|
-
if (!conversation) {
|
|
1440
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
const requestedFilePath = url.searchParams.get("path");
|
|
1444
|
-
if (!requestedFilePath) {
|
|
1445
|
-
sendJson(response, 400, { error: "path_required" });
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
await serveConversationFile(request, response, conversation.workspacePath, requestedFilePath);
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
if (request.method === "DELETE" && conversationMatch) {
|
|
1452
|
-
const conversation = getConversation(conversationMatch[1]);
|
|
1453
|
-
if (!conversation) {
|
|
1454
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
deleteConversation(conversation.id);
|
|
1458
|
-
sendJson(response, 200, { conversation });
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
const conversationContextResetMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/context\/reset$/);
|
|
1462
|
-
if (request.method === "POST" && conversationContextResetMatch) {
|
|
1463
|
-
const conversation = getConversation(conversationContextResetMatch[1]);
|
|
1464
|
-
if (!conversation) {
|
|
1465
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
const body = await readJsonBody(request);
|
|
1469
|
-
const summary = typeof body.summary === "string" && body.summary.trim()
|
|
1470
|
-
? body.summary.trim()
|
|
1471
|
-
: null;
|
|
1472
|
-
const contextVersion = appendConversationContextVersion(conversation, {
|
|
1473
|
-
codexThreadId: conversation.codexThreadId,
|
|
1474
|
-
source: "context-reset",
|
|
1475
|
-
summary,
|
|
1476
|
-
});
|
|
1477
|
-
sendJson(response, 200, { conversation, contextVersion });
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
const conversationWakeupsMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/wakeups$/);
|
|
1481
|
-
if (conversationWakeupsMatch) {
|
|
1482
|
-
const conversation = getConversation(conversationWakeupsMatch[1]);
|
|
1483
|
-
if (!conversation) {
|
|
1484
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
if (request.method === "GET") {
|
|
1488
|
-
sendJson(response, 200, {
|
|
1489
|
-
wakeups: listWorkspaceWakeups(conversation.id),
|
|
1490
|
-
});
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
if (request.method === "POST") {
|
|
1494
|
-
const body = await readJsonBody(request);
|
|
1495
|
-
const instruction = typeof body.instruction === "string" ? body.instruction.trim() : "";
|
|
1496
|
-
const wakeAt = resolveWakeAt(body);
|
|
1497
|
-
if (!instruction) {
|
|
1498
|
-
sendJson(response, 400, { error: "instruction_required" });
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
if (!wakeAt) {
|
|
1502
|
-
sendJson(response, 400, { error: "wake_at_required" });
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
const wakeup = createWorkspaceWakeup({
|
|
1506
|
-
workspaceId: conversation.id,
|
|
1507
|
-
wakeAt,
|
|
1508
|
-
instruction,
|
|
1509
|
-
createdBy: "user",
|
|
1510
|
-
});
|
|
1511
|
-
appendEvent(conversation, "server.wakeup.scheduled", {
|
|
1512
|
-
wakeupId: wakeup.id,
|
|
1513
|
-
wakeAt: wakeup.wakeAt,
|
|
1514
|
-
instruction: wakeup.instruction,
|
|
1515
|
-
});
|
|
1516
|
-
sendJson(response, 201, { wakeup });
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
const conversationWakeupCancelMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/wakeups\/([^/]+)\/cancel$/);
|
|
1521
|
-
if (request.method === "POST" && conversationWakeupCancelMatch) {
|
|
1522
|
-
const conversation = getConversation(conversationWakeupCancelMatch[1]);
|
|
1523
|
-
if (!conversation) {
|
|
1524
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
const wakeupId = conversationWakeupCancelMatch[2];
|
|
1528
|
-
const wakeup = cancelWorkspaceWakeup(wakeupId, {
|
|
1529
|
-
workspaceId: conversation.id,
|
|
1530
|
-
});
|
|
1531
|
-
if (!wakeup) {
|
|
1532
|
-
sendJson(response, 404, { error: "scheduled_wakeup_not_found" });
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
appendEvent(conversation, "server.wakeup.cancelled", {
|
|
1536
|
-
wakeupId: wakeup.id,
|
|
1537
|
-
reason: wakeup.error,
|
|
1538
|
-
});
|
|
1539
|
-
sendJson(response, 200, { wakeup });
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
const conversationInterruptMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/interrupt$/);
|
|
1543
|
-
if (request.method === "POST" && conversationInterruptMatch) {
|
|
1544
|
-
const conversation = getConversation(conversationInterruptMatch[1]);
|
|
1545
|
-
if (!conversation) {
|
|
1546
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
const activeTurnId = activeConversationTurns.get(conversation.id);
|
|
1550
|
-
if (!activeTurnId || conversation.status !== "running") {
|
|
1551
|
-
sendJson(response, 409, { error: "turn_not_running" });
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
try {
|
|
1555
|
-
stopTurnLocally(activeTurnId, conversation);
|
|
1556
|
-
sendJson(response, 202, { ok: true });
|
|
1557
|
-
}
|
|
1558
|
-
catch (error) {
|
|
1559
|
-
appendEvent(conversation, "server.stop.error", {
|
|
1560
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1561
|
-
});
|
|
1562
|
-
sendJson(response, 500, {
|
|
1563
|
-
error: "stop_failed",
|
|
1564
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
const conversationSteerMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/steer$/);
|
|
1570
|
-
if (request.method === "POST" && conversationSteerMatch) {
|
|
1571
|
-
const conversation = getConversation(conversationSteerMatch[1]);
|
|
1572
|
-
if (!conversation) {
|
|
1573
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1576
|
-
const body = await readJsonBody(request);
|
|
1577
|
-
if (!body.message || typeof body.message !== "string") {
|
|
1578
|
-
sendJson(response, 400, { error: "message_required" });
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
const activeTurnId = activeConversationTurns.get(conversation.id);
|
|
1582
|
-
if (!activeTurnId || conversation.status !== "running") {
|
|
1583
|
-
sendJson(response, 409, { error: "turn_not_running" });
|
|
1584
|
-
return;
|
|
1585
|
-
}
|
|
1586
|
-
try {
|
|
1587
|
-
const message = steerConversationTurn(conversation, activeTurnId, body.message, body);
|
|
1588
|
-
sendJson(response, 202, { ok: true, message });
|
|
1589
|
-
}
|
|
1590
|
-
catch (error) {
|
|
1591
|
-
appendEvent(conversation, "server.steer.error", {
|
|
1592
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1593
|
-
});
|
|
1594
|
-
sendJson(response, 500, {
|
|
1595
|
-
error: "steer_failed",
|
|
1596
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
const conversationCurrentTurnEventsMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/current\/events$/);
|
|
1602
|
-
if (request.method === "GET" && conversationCurrentTurnEventsMatch) {
|
|
1603
|
-
const conversation = getConversation(conversationCurrentTurnEventsMatch[1]);
|
|
1604
|
-
if (!conversation) {
|
|
1605
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1606
|
-
return;
|
|
1607
|
-
}
|
|
1608
|
-
const activeTurnId = activeConversationTurns.get(conversation.id);
|
|
1609
|
-
if (!activeTurnId || conversation.status !== "running") {
|
|
1610
|
-
if (!activeTurnId && conversation.status === "running") {
|
|
1611
|
-
setConversationStatus(conversation, "done");
|
|
1612
|
-
}
|
|
1613
|
-
sendJson(response, 409, { error: "turn_not_running" });
|
|
1614
|
-
return;
|
|
1615
|
-
}
|
|
1616
|
-
openSse(response);
|
|
1617
|
-
subscribeTurnEvents(activeTurnId, response);
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
|
-
const conversationTurnEventsMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/([^/]+)\/events$/);
|
|
1621
|
-
if (request.method === "GET" && conversationTurnEventsMatch) {
|
|
1622
|
-
const conversation = getConversation(conversationTurnEventsMatch[1]);
|
|
1623
|
-
if (!conversation) {
|
|
1624
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
const turnId = conversationTurnEventsMatch[2];
|
|
1628
|
-
const storedTurn = getConversationTurn(conversation, turnId);
|
|
1629
|
-
if (!storedTurn) {
|
|
1630
|
-
sendJson(response, 404, { error: "turn_not_found" });
|
|
1631
|
-
return;
|
|
1632
|
-
}
|
|
1633
|
-
const activeTurnId = activeConversationTurns.get(conversation.id);
|
|
1634
|
-
if (turnConversations.get(turnId) !== conversation.id ||
|
|
1635
|
-
activeTurnId !== turnId ||
|
|
1636
|
-
conversation.status !== "running") {
|
|
1637
|
-
if (!activeTurnId && conversation.status === "running") {
|
|
1638
|
-
setConversationStatus(conversation, storedTurn.status === "failed" ? "error" : "done");
|
|
1639
|
-
}
|
|
1640
|
-
sendJson(response, 410, {
|
|
1641
|
-
error: "turn_not_active",
|
|
1642
|
-
turnId,
|
|
1643
|
-
turnStatus: storedTurn.status,
|
|
1644
|
-
});
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
openSse(response);
|
|
1648
|
-
subscribeTurnEvents(turnId, response);
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
const conversationTurnHistoryEventsMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/([^/]+)\/history-events$/);
|
|
1652
|
-
if (request.method === "GET" && conversationTurnHistoryEventsMatch) {
|
|
1653
|
-
const conversation = getConversation(conversationTurnHistoryEventsMatch[1]);
|
|
1654
|
-
if (!conversation) {
|
|
1655
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
const turnId = conversationTurnHistoryEventsMatch[2];
|
|
1659
|
-
sendJson(response, 200, {
|
|
1660
|
-
events: listConversationTurnEvents(conversation, turnId),
|
|
1661
|
-
});
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
const conversationTurnControlMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/([^/]+)\/(stop|steer)$/);
|
|
1665
|
-
if (request.method === "POST" && conversationTurnControlMatch) {
|
|
1666
|
-
const conversation = getConversation(conversationTurnControlMatch[1]);
|
|
1667
|
-
if (!conversation) {
|
|
1668
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
const turnId = conversationTurnControlMatch[2];
|
|
1672
|
-
const action = conversationTurnControlMatch[3];
|
|
1673
|
-
if (turnConversations.get(turnId) !== conversation.id) {
|
|
1674
|
-
sendJson(response, 404, { error: "turn_not_found" });
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
if (activeConversationTurns.get(conversation.id) !== turnId || conversation.status !== "running") {
|
|
1678
|
-
sendJson(response, 409, { error: "turn_not_running" });
|
|
1679
|
-
return;
|
|
1680
|
-
}
|
|
1681
|
-
if (action === "stop") {
|
|
1682
|
-
stopTurnLocally(turnId, conversation);
|
|
1683
|
-
sendJson(response, 202, { ok: true });
|
|
1684
|
-
return;
|
|
1685
|
-
}
|
|
1686
|
-
const body = await readJsonBody(request);
|
|
1687
|
-
if (!body.message || typeof body.message !== "string") {
|
|
1688
|
-
sendJson(response, 400, { error: "message_required" });
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
const message = steerConversationTurn(conversation, turnId, body.message, body);
|
|
1692
|
-
sendJson(response, 202, { ok: true, message });
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
const conversationTurnApprovalMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/([^/]+)\/approvals\/([^/]+)$/);
|
|
1696
|
-
if (request.method === "POST" && conversationTurnApprovalMatch) {
|
|
1697
|
-
const conversation = getConversation(conversationTurnApprovalMatch[1]);
|
|
1698
|
-
if (!conversation) {
|
|
1699
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
const turnId = conversationTurnApprovalMatch[2];
|
|
1703
|
-
const requestId = decodeURIComponent(conversationTurnApprovalMatch[3]);
|
|
1704
|
-
if (turnConversations.get(turnId) !== conversation.id) {
|
|
1705
|
-
sendJson(response, 404, { error: "turn_not_found" });
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
const body = await readJsonBody(request);
|
|
1709
|
-
if (!isCommandApprovalDecision(body.decision)) {
|
|
1710
|
-
sendJson(response, 400, { error: "invalid_decision" });
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
runtime.respondApproval(turnId, requestId, body.decision);
|
|
1714
|
-
recordTurnEvent(turnId, "server.approval.decision", {
|
|
1715
|
-
turnId,
|
|
1716
|
-
requestId,
|
|
1717
|
-
decision: body.decision,
|
|
1718
|
-
});
|
|
1719
|
-
updateConversationTurn(conversation, turnId, {
|
|
1720
|
-
status: "running",
|
|
1721
|
-
});
|
|
1722
|
-
sendJson(response, 202, { ok: true });
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
const responsibilityProposalMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/responsibility-proposals\/([^/]+)$/);
|
|
1726
|
-
if (request.method === "POST" && responsibilityProposalMatch) {
|
|
1727
|
-
const conversation = getConversation(responsibilityProposalMatch[1]);
|
|
1728
|
-
if (!conversation) {
|
|
1729
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
const proposalId = decodeURIComponent(responsibilityProposalMatch[2]);
|
|
1733
|
-
const proposalEvent = conversation.events.find((event) => event.id === proposalId &&
|
|
1734
|
-
event.event === "server.responsibility.proposed");
|
|
1735
|
-
if (!proposalEvent) {
|
|
1736
|
-
sendJson(response, 404, { error: "proposal_not_found" });
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
const body = await readJsonBody(request);
|
|
1740
|
-
if (body.decision !== "accept" && body.decision !== "decline") {
|
|
1741
|
-
sendJson(response, 400, { error: "invalid_decision" });
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
const proposal = readRecord(proposalEvent.data);
|
|
1745
|
-
const proposedResponsibility = typeof proposal.responsibility === "string" ? proposal.responsibility : "";
|
|
1746
|
-
const responsibility = typeof body.responsibility === "string"
|
|
1747
|
-
? body.responsibility
|
|
1748
|
-
: proposedResponsibility;
|
|
1749
|
-
if (body.decision === "accept" && !responsibility.trim()) {
|
|
1750
|
-
sendJson(response, 400, { error: "responsibility_required" });
|
|
1751
|
-
return;
|
|
1752
|
-
}
|
|
1753
|
-
if (body.decision === "accept") {
|
|
1754
|
-
setConversationResponsibility(conversation, responsibility);
|
|
1755
|
-
}
|
|
1756
|
-
appendEvent(conversation, "server.responsibility.decision", {
|
|
1757
|
-
proposalId,
|
|
1758
|
-
decision: body.decision,
|
|
1759
|
-
responsibility: body.decision === "accept" ? responsibility : null,
|
|
1760
|
-
edited: body.decision === "accept" && responsibility !== proposedResponsibility,
|
|
1761
|
-
}, { turnId: proposalEvent.turnId });
|
|
1762
|
-
sendJson(response, 200, {
|
|
1763
|
-
ok: true,
|
|
1764
|
-
conversation: {
|
|
1765
|
-
...conversation,
|
|
1766
|
-
nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
|
|
1767
|
-
},
|
|
1768
|
-
});
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1771
|
-
const knowledgeProposalMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/knowledge-proposals\/([^/]+)$/);
|
|
1772
|
-
if (request.method === "POST" && knowledgeProposalMatch) {
|
|
1773
|
-
const conversation = getConversation(knowledgeProposalMatch[1]);
|
|
1774
|
-
if (!conversation) {
|
|
1775
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1776
|
-
return;
|
|
1777
|
-
}
|
|
1778
|
-
const proposalId = decodeURIComponent(knowledgeProposalMatch[2]);
|
|
1779
|
-
const proposalEvent = conversation.events.find((event) => event.id === proposalId && event.event === "server.knowledge.proposed");
|
|
1780
|
-
if (!proposalEvent) {
|
|
1781
|
-
sendJson(response, 404, { error: "proposal_not_found" });
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
const body = await readJsonBody(request);
|
|
1785
|
-
if (body.decision !== "accept" && body.decision !== "decline") {
|
|
1786
|
-
sendJson(response, 400, { error: "invalid_decision" });
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
const proposal = readRecord(proposalEvent.data);
|
|
1790
|
-
const proposedKnowledge = readRecord(proposal.knowledge);
|
|
1791
|
-
const submittedKnowledge = readRecord(body.knowledge);
|
|
1792
|
-
const knowledge = Object.keys(submittedKnowledge).length > 0
|
|
1793
|
-
? submittedKnowledge
|
|
1794
|
-
: proposedKnowledge;
|
|
1795
|
-
const action = typeof knowledge.action === "string" ? knowledge.action : proposal.action;
|
|
1796
|
-
const knowledgeId = typeof knowledge.knowledgeId === "string"
|
|
1797
|
-
? knowledge.knowledgeId
|
|
1798
|
-
: typeof proposal.knowledgeId === "string"
|
|
1799
|
-
? proposal.knowledgeId
|
|
1800
|
-
: undefined;
|
|
1801
|
-
let item = null;
|
|
1802
|
-
if (body.decision === "accept") {
|
|
1803
|
-
const validationError = validateKnowledgeProposalForPersistence(knowledge);
|
|
1804
|
-
if (validationError) {
|
|
1805
|
-
sendJson(response, 400, { error: validationError });
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
const input = knowledgeProposalToInput(knowledge);
|
|
1809
|
-
if (action === "update" && knowledgeId) {
|
|
1810
|
-
item = updateWorkspaceKnowledge(conversation, knowledgeId, input);
|
|
1811
|
-
if (!item) {
|
|
1812
|
-
sendJson(response, 404, { error: "knowledge_not_found" });
|
|
1813
|
-
return;
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
else {
|
|
1817
|
-
item = createWorkspaceKnowledge(conversation, input);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
appendEvent(conversation, "server.knowledge.decision", {
|
|
1821
|
-
proposalId,
|
|
1822
|
-
action,
|
|
1823
|
-
decision: body.decision,
|
|
1824
|
-
knowledgeId: item?.id ?? knowledgeId ?? null,
|
|
1825
|
-
knowledge: body.decision === "accept" ? item : null,
|
|
1826
|
-
edited: body.decision === "accept" &&
|
|
1827
|
-
JSON.stringify(knowledge) !== JSON.stringify(proposedKnowledge),
|
|
1828
|
-
}, { turnId: proposalEvent.turnId });
|
|
1829
|
-
sendJson(response, 200, {
|
|
1830
|
-
ok: true,
|
|
1831
|
-
item,
|
|
1832
|
-
items: listWorkspaceKnowledge(conversation),
|
|
1833
|
-
});
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
const conversationTurnMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns$/);
|
|
1837
|
-
if (request.method === "POST" && conversationTurnMatch) {
|
|
1838
|
-
const conversation = getConversation(conversationTurnMatch[1]);
|
|
1839
|
-
if (!conversation) {
|
|
1840
|
-
sendJson(response, 404, { error: "conversation_not_found" });
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
1843
|
-
try {
|
|
1844
|
-
const body = await readJsonBody(request);
|
|
1845
|
-
if (!body.message || typeof body.message !== "string") {
|
|
1846
|
-
sendJson(response, 400, { error: "message_required" });
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
const result = enqueueConversationTurn(conversation, body);
|
|
1850
|
-
sendJson(response, 202, result);
|
|
1851
|
-
}
|
|
1852
|
-
catch (error) {
|
|
1853
|
-
setConversationStatus(conversation, "error");
|
|
1854
|
-
sendJson(response, 500, {
|
|
1855
|
-
error: "turn_start_failed",
|
|
1856
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
const legacyConversationTurnMatch = url.pathname.match(/^\/api\/conversations\/([^/]+)\/turns\/stream$/);
|
|
1862
|
-
if (request.method === "POST" && legacyConversationTurnMatch) {
|
|
1863
|
-
sendJson(response, 410, {
|
|
1864
|
-
error: "stream_endpoint_replaced",
|
|
1865
|
-
message: "Use POST /turns and GET /turns/:turnId/events.",
|
|
1866
|
-
});
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
if (request.method === "POST" && url.pathname === "/api/turns/stream") {
|
|
1870
|
-
const conversation = createConversation({
|
|
1871
|
-
workspaceRoot: config.workspaceRoot,
|
|
1872
|
-
sandbox: getGlobalRuntimeSettings().sandbox,
|
|
1873
|
-
ownerId: authenticatedUser.id,
|
|
1874
|
-
});
|
|
1875
|
-
response.statusCode = 307;
|
|
1876
|
-
response.setHeader("Location", `/api/conversations/${conversation.id}/turns/stream`);
|
|
1877
|
-
response.end();
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
if (request.method === "GET" || request.method === "HEAD") {
|
|
1881
|
-
const served = await serveWebAsset(request, response, url.pathname);
|
|
1882
|
-
if (served) {
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
response.writeHead(404, { "Content-Type": "application/json" });
|
|
1887
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
1888
|
-
});
|
|
1889
|
-
server.listen(config.port, config.host, () => {
|
|
1890
|
-
console.log(`seele-codex server listening on http://${config.host}:${config.port}`);
|
|
1891
|
-
});
|
|
1892
|
-
startWakeupScheduler();
|
|
1893
|
-
startRuntimeWatchdog();
|
|
1894
|
-
function enqueueConversationTurn(conversation, body) {
|
|
1895
|
-
const userMessage = String(body.message ?? "");
|
|
1896
|
-
const activeTurnId = activeConversationTurns.get(conversation.id);
|
|
1897
|
-
if (activeTurnId) {
|
|
1898
|
-
if (body.mode === "queue") {
|
|
1899
|
-
const { turnId, message } = createQueuedConversationTurn(conversation, body, userMessage);
|
|
1900
|
-
const queue = workspaceTurnQueues.get(conversation.id) ?? [];
|
|
1901
|
-
queue.push({ turnId, body, userMessage });
|
|
1902
|
-
workspaceTurnQueues.set(conversation.id, queue);
|
|
1903
|
-
appendEvent(conversation, "server.turn.queued", {
|
|
1904
|
-
turnId,
|
|
1905
|
-
activeTurnId,
|
|
1906
|
-
queuePosition: queue.length,
|
|
1907
|
-
}, { turnId });
|
|
1908
|
-
return {
|
|
1909
|
-
turnId,
|
|
1910
|
-
mode: "queued",
|
|
1911
|
-
queuePosition: queue.length,
|
|
1912
|
-
message,
|
|
1913
|
-
};
|
|
1914
|
-
}
|
|
1915
|
-
const message = steerConversationTurn(conversation, activeTurnId, userMessage, body);
|
|
1916
|
-
return {
|
|
1917
|
-
turnId: activeTurnId,
|
|
1918
|
-
mode: "steered",
|
|
1919
|
-
queuePosition: 0,
|
|
1920
|
-
message,
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
const { turnId, message } = createQueuedConversationTurn(conversation, body, userMessage);
|
|
1924
|
-
startQueuedConversationTurn(conversation, { turnId, body, userMessage });
|
|
1925
|
-
return {
|
|
1926
|
-
turnId,
|
|
1927
|
-
mode: "running",
|
|
1928
|
-
queuePosition: 0,
|
|
1929
|
-
message,
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
function readRuntimeModelCatalogCache() {
|
|
1933
|
-
const value = getAppSettingValue(RUNTIME_MODEL_CATALOG_CACHE_KEY);
|
|
1934
|
-
if (!value) {
|
|
1935
|
-
return null;
|
|
1936
|
-
}
|
|
1937
|
-
try {
|
|
1938
|
-
const parsed = JSON.parse(value);
|
|
1939
|
-
if (!parsed.catalog ||
|
|
1940
|
-
!Array.isArray(parsed.catalog.models) ||
|
|
1941
|
-
typeof parsed.catalog.codexVersion !== "string" ||
|
|
1942
|
-
typeof parsed.updatedAt !== "string") {
|
|
1943
|
-
return null;
|
|
1944
|
-
}
|
|
1945
|
-
return { catalog: parsed.catalog, updatedAt: parsed.updatedAt };
|
|
1946
|
-
}
|
|
1947
|
-
catch {
|
|
1948
|
-
return null;
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
function writeRuntimeModelCatalogCache(catalog) {
|
|
1952
|
-
upsertAppSetting(RUNTIME_MODEL_CATALOG_CACHE_KEY, JSON.stringify({ catalog, updatedAt: new Date().toISOString() }));
|
|
1953
|
-
}
|
|
1954
|
-
function refreshRuntimeModelCatalog() {
|
|
1955
|
-
if (runtimeModelCatalogRefresh) {
|
|
1956
|
-
return runtimeModelCatalogRefresh;
|
|
1957
|
-
}
|
|
1958
|
-
runtimeModelCatalogRefresh = runtime
|
|
1959
|
-
.listModels(config)
|
|
1960
|
-
.then((catalog) => {
|
|
1961
|
-
writeRuntimeModelCatalogCache(catalog);
|
|
1962
|
-
return catalog;
|
|
1963
|
-
})
|
|
1964
|
-
.finally(() => {
|
|
1965
|
-
runtimeModelCatalogRefresh = null;
|
|
1966
|
-
});
|
|
1967
|
-
return runtimeModelCatalogRefresh;
|
|
1968
|
-
}
|
|
1969
|
-
function readRuntimeRateLimitsCache() {
|
|
1970
|
-
const value = getAppSettingValue(RUNTIME_RATE_LIMITS_CACHE_KEY);
|
|
1971
|
-
if (!value) {
|
|
1972
|
-
return null;
|
|
1973
|
-
}
|
|
1974
|
-
try {
|
|
1975
|
-
return normalizeCachedRateLimitUsage(JSON.parse(value));
|
|
1976
|
-
}
|
|
1977
|
-
catch {
|
|
1978
|
-
return null;
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
function writeRuntimeRateLimitsCache(usage) {
|
|
1982
|
-
upsertAppSetting(RUNTIME_RATE_LIMITS_CACHE_KEY, JSON.stringify(usage));
|
|
1983
|
-
}
|
|
1984
|
-
function refreshRuntimeRateLimits() {
|
|
1985
|
-
if (runtimeRateLimitsRefresh) {
|
|
1986
|
-
return runtimeRateLimitsRefresh;
|
|
1987
|
-
}
|
|
1988
|
-
runtimeRateLimitsRefresh = runtime
|
|
1989
|
-
.readRateLimits(config)
|
|
1990
|
-
.then((result) => {
|
|
1991
|
-
const usage = normalizeRuntimeRateLimits(result, new Date().toISOString());
|
|
1992
|
-
if (!usage) {
|
|
1993
|
-
throw new Error("Codex returned no usable rate-limit windows");
|
|
1994
|
-
}
|
|
1995
|
-
writeRuntimeRateLimitsCache(usage);
|
|
1996
|
-
return usage;
|
|
1997
|
-
})
|
|
1998
|
-
.finally(() => {
|
|
1999
|
-
runtimeRateLimitsRefresh = null;
|
|
2000
|
-
});
|
|
2001
|
-
return runtimeRateLimitsRefresh;
|
|
2002
|
-
}
|
|
2003
|
-
function persistRateLimitsFromEvent(event, data, updatedAt) {
|
|
2004
|
-
if (event !== "codex.notification") {
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
const notification = readRecord(data);
|
|
2008
|
-
if (notification.method !== "account/rateLimits/updated") {
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2011
|
-
const params = readRecord(notification.params);
|
|
2012
|
-
const usage = normalizeRateLimitSnapshot(params.rateLimits, updatedAt);
|
|
2013
|
-
if (usage) {
|
|
2014
|
-
writeRuntimeRateLimitsCache(usage);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
function normalizeRuntimeRateLimits(result, updatedAt) {
|
|
2018
|
-
return normalizeRateLimitSnapshot(result.rateLimits, updatedAt);
|
|
2019
|
-
}
|
|
2020
|
-
function normalizeRateLimitSnapshot(value, updatedAt) {
|
|
2021
|
-
const snapshot = readRecord(value);
|
|
2022
|
-
const windows = Array.isArray(snapshot.windows)
|
|
2023
|
-
? snapshot.windows
|
|
2024
|
-
.map((window) => normalizeRateLimitWindow(window))
|
|
2025
|
-
.filter((window) => window !== null)
|
|
2026
|
-
: [snapshot.primary, snapshot.secondary]
|
|
2027
|
-
.map((window) => normalizeRateLimitWindow(window))
|
|
2028
|
-
.filter((window) => window !== null);
|
|
2029
|
-
return windows.length > 0 ? { windows, updatedAt } : null;
|
|
2030
|
-
}
|
|
2031
|
-
function normalizeRateLimitWindow(value) {
|
|
2032
|
-
const window = readRecord(value);
|
|
2033
|
-
return typeof window.usedPercent === "number" &&
|
|
2034
|
-
typeof window.resetsAt === "number" &&
|
|
2035
|
-
typeof window.windowDurationMins === "number"
|
|
2036
|
-
? {
|
|
2037
|
-
usedPercent: window.usedPercent,
|
|
2038
|
-
resetsAt: window.resetsAt,
|
|
2039
|
-
windowDurationMins: window.windowDurationMins,
|
|
2040
|
-
}
|
|
2041
|
-
: null;
|
|
2042
|
-
}
|
|
2043
|
-
function normalizeCachedRateLimitUsage(value) {
|
|
2044
|
-
const cached = readRecord(value);
|
|
2045
|
-
return typeof cached.updatedAt === "string"
|
|
2046
|
-
? normalizeRateLimitSnapshot(cached, cached.updatedAt)
|
|
2047
|
-
: null;
|
|
2048
|
-
}
|
|
2049
|
-
async function getCodexReleaseTime(versionOutput) {
|
|
2050
|
-
const version = normalizeCodexVersion(versionOutput);
|
|
2051
|
-
if (!version) {
|
|
2052
|
-
return null;
|
|
2053
|
-
}
|
|
2054
|
-
const cacheKey = `codex.release_time.${version}`;
|
|
2055
|
-
const cached = getAppSettingValue(cacheKey);
|
|
2056
|
-
if (cached) {
|
|
2057
|
-
return cached;
|
|
2058
|
-
}
|
|
2059
|
-
try {
|
|
2060
|
-
const { stdout } = await execFileAsync("npm", [
|
|
2061
|
-
"view",
|
|
2062
|
-
`@openai/codex@${version}`,
|
|
2063
|
-
"time",
|
|
2064
|
-
"--json",
|
|
2065
|
-
`--registry=${CODEX_NPM_REGISTRY}`,
|
|
2066
|
-
"--fetch-retries=0",
|
|
2067
|
-
`--fetch-timeout=${CODEX_LATEST_LOOKUP_TIMEOUT_MS}`,
|
|
2068
|
-
], {
|
|
2069
|
-
env: codexNpmEnvironment(),
|
|
2070
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
2071
|
-
timeout: CODEX_LATEST_LOOKUP_TIMEOUT_MS,
|
|
2072
|
-
});
|
|
2073
|
-
const times = JSON.parse(stdout);
|
|
2074
|
-
const releasedAt = typeof times[version] === "string" ? times[version] : null;
|
|
2075
|
-
if (releasedAt) {
|
|
2076
|
-
upsertAppSetting(cacheKey, releasedAt);
|
|
2077
|
-
}
|
|
2078
|
-
return releasedAt;
|
|
2079
|
-
}
|
|
2080
|
-
catch {
|
|
2081
|
-
return null;
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
async function readManagedCodexPackageVersion() {
|
|
2085
|
-
const runtimeDir = process.env.SEELE_RUNTIME_DIR ?? path.join(config.seeleRoot, ".runtime");
|
|
2086
|
-
for (const prefix of [path.join(runtimeDir, "codex", "current"), runtimeDir]) {
|
|
2087
|
-
const version = await readCodexPackageVersion(prefix);
|
|
2088
|
-
if (version) {
|
|
2089
|
-
return version;
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
const versionOutput = await readCodexVersion(path.join(runtimeDir, "bin", "codex"));
|
|
2093
|
-
return versionOutput ? normalizeCodexVersion(versionOutput) : null;
|
|
2094
|
-
}
|
|
2095
|
-
function normalizeCodexVersion(versionOutput) {
|
|
2096
|
-
const version = versionOutput.replace(/^codex-cli\s+/i, "").trim();
|
|
2097
|
-
return /^[0-9A-Za-z.-]+$/.test(version) ? version : null;
|
|
2098
|
-
}
|
|
2099
|
-
async function fetchLatestCodexVersion() {
|
|
2100
|
-
const checkedAt = new Date().toISOString();
|
|
2101
|
-
try {
|
|
2102
|
-
const { stdout } = await execFileAsync("npm", [
|
|
2103
|
-
"view",
|
|
2104
|
-
"@openai/codex@latest",
|
|
2105
|
-
"version",
|
|
2106
|
-
"--json",
|
|
2107
|
-
`--registry=${CODEX_NPM_REGISTRY}`,
|
|
2108
|
-
"--fetch-retries=0",
|
|
2109
|
-
`--fetch-timeout=${CODEX_LATEST_LOOKUP_TIMEOUT_MS}`,
|
|
2110
|
-
], {
|
|
2111
|
-
env: codexNpmEnvironment(),
|
|
2112
|
-
maxBuffer: 1024 * 1024,
|
|
2113
|
-
timeout: CODEX_LATEST_LOOKUP_TIMEOUT_MS,
|
|
2114
|
-
});
|
|
2115
|
-
const parsed = JSON.parse(stdout);
|
|
2116
|
-
const version = typeof parsed === "string" ? parsed.trim() : "";
|
|
2117
|
-
if (!version) {
|
|
2118
|
-
throw new Error("npm returned an empty latest version");
|
|
2119
|
-
}
|
|
2120
|
-
const releasedAt = await getCodexReleaseTime(version);
|
|
2121
|
-
return { version, releasedAt, checkedAt, error: null };
|
|
2122
|
-
}
|
|
2123
|
-
catch (error) {
|
|
2124
|
-
return {
|
|
2125
|
-
version: null,
|
|
2126
|
-
releasedAt: null,
|
|
2127
|
-
checkedAt,
|
|
2128
|
-
error: formatCodexUpdateError(error),
|
|
2129
|
-
};
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
async function runCodexUpdateJob(job) {
|
|
2133
|
-
try {
|
|
2134
|
-
job.previousVersion = await readCodexVersion(config.codexBin ?? "codex");
|
|
2135
|
-
appendCodexUpdateLog(job, "system", `Current version: ${job.previousVersion ?? "unavailable"}`);
|
|
2136
|
-
const catalog = await updateManagedCodexCli(job);
|
|
2137
|
-
writeRuntimeModelCatalogCache(catalog);
|
|
2138
|
-
job.currentVersion = catalog.codexVersion;
|
|
2139
|
-
job.codexReleasedAt = await getCodexReleaseTime(catalog.codexVersion);
|
|
2140
|
-
job.status = "succeeded";
|
|
2141
|
-
appendCodexUpdateLog(job, "system", `Update completed: ${catalog.codexVersion}`);
|
|
2142
|
-
}
|
|
2143
|
-
catch (error) {
|
|
2144
|
-
job.status = "failed";
|
|
2145
|
-
job.error = formatCodexUpdateError(error);
|
|
2146
|
-
appendCodexUpdateLog(job, "stderr", job.error);
|
|
2147
|
-
}
|
|
2148
|
-
finally {
|
|
2149
|
-
job.finishedAt = new Date().toISOString();
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
async function updateManagedCodexCli(job) {
|
|
2153
|
-
const runtimeDir = process.env.SEELE_RUNTIME_DIR ?? path.join(config.seeleRoot, ".runtime");
|
|
2154
|
-
const codexHome = process.env.CODEX_HOME ?? path.join(runtimeDir, "codex-home");
|
|
2155
|
-
const codexRoot = path.join(runtimeDir, "codex");
|
|
2156
|
-
const versionsDir = path.join(codexRoot, "versions");
|
|
2157
|
-
const currentLink = path.join(codexRoot, "current");
|
|
2158
|
-
const managedCodexBin = path.join(runtimeDir, "bin", "codex");
|
|
2159
|
-
const stagingDir = path.join(codexRoot, `.staging-${job.id}`);
|
|
2160
|
-
const npmEnvironment = codexNpmEnvironment();
|
|
2161
|
-
await fs.mkdir(path.join(runtimeDir, "bin"), { recursive: true });
|
|
2162
|
-
await fs.mkdir(codexHome, { recursive: true });
|
|
2163
|
-
await fs.mkdir(versionsDir, { recursive: true });
|
|
2164
|
-
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
2165
|
-
let installedDir = null;
|
|
2166
|
-
try {
|
|
2167
|
-
appendCodexUpdateLog(job, "system", `Staging prefix: ${stagingDir}`);
|
|
2168
|
-
appendCodexUpdateLog(job, "system", `Registry: ${CODEX_NPM_REGISTRY}`);
|
|
2169
|
-
await runLoggedProcess(job, "npm", [
|
|
2170
|
-
"install",
|
|
2171
|
-
"--global",
|
|
2172
|
-
"--prefix",
|
|
2173
|
-
stagingDir,
|
|
2174
|
-
"--no-audit",
|
|
2175
|
-
"--no-fund",
|
|
2176
|
-
`--registry=${CODEX_NPM_REGISTRY}`,
|
|
2177
|
-
"--fetch-retries=1",
|
|
2178
|
-
"--fetch-timeout=30000",
|
|
2179
|
-
"@openai/codex@latest",
|
|
2180
|
-
], {
|
|
2181
|
-
env: {
|
|
2182
|
-
...npmEnvironment,
|
|
2183
|
-
CODEX_HOME: codexHome,
|
|
2184
|
-
PATH: `${path.join(stagingDir, "bin")}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
2185
|
-
},
|
|
2186
|
-
});
|
|
2187
|
-
const installedVersion = await readCodexPackageVersion(stagingDir);
|
|
2188
|
-
if (!installedVersion) {
|
|
2189
|
-
throw new Error("Installed Codex package version could not be read from staging");
|
|
2190
|
-
}
|
|
2191
|
-
const stagingBin = path.join(stagingDir, "bin", "codex");
|
|
2192
|
-
const stagingVersionOutput = await executeCodexVersion(stagingBin, CODEX_VERSION_VERIFY_TIMEOUT_MS);
|
|
2193
|
-
if (normalizeCodexVersion(stagingVersionOutput ?? "") !== installedVersion) {
|
|
2194
|
-
throw new Error(`Staged Codex CLI verification failed: expected ${installedVersion}, got ${stagingVersionOutput ?? "unavailable"}`);
|
|
2195
|
-
}
|
|
2196
|
-
appendCodexUpdateLog(job, "system", `Staged version verified: ${installedVersion}`);
|
|
2197
|
-
appendCodexUpdateLog(job, "system", "Validating staged Codex app-server.");
|
|
2198
|
-
const stagedCatalog = await runtime.listModels({ ...config, codexBin: stagingBin });
|
|
2199
|
-
if (normalizeCodexVersion(stagedCatalog.codexVersion) !== installedVersion) {
|
|
2200
|
-
throw new Error(`Staged Codex app-server reported ${stagedCatalog.codexVersion}; expected ${installedVersion}`);
|
|
2201
|
-
}
|
|
2202
|
-
appendCodexUpdateLog(job, "system", "Staged Codex app-server validation passed.");
|
|
2203
|
-
const versionName = codexVersionDirectoryName(installedVersion);
|
|
2204
|
-
const versionDir = path.join(versionsDir, versionName);
|
|
2205
|
-
try {
|
|
2206
|
-
await fs.rename(stagingDir, versionDir);
|
|
2207
|
-
installedDir = versionDir;
|
|
2208
|
-
}
|
|
2209
|
-
catch (error) {
|
|
2210
|
-
if (!isExistingPathError(error)) {
|
|
2211
|
-
throw error;
|
|
2212
|
-
}
|
|
2213
|
-
const existingVersion = await readCodexPackageVersion(versionDir);
|
|
2214
|
-
const existingVersionOutput = await readCodexVersion(path.join(versionDir, "bin", "codex"));
|
|
2215
|
-
if (existingVersion !== installedVersion ||
|
|
2216
|
-
normalizeCodexVersion(existingVersionOutput ?? "") !== installedVersion) {
|
|
2217
|
-
throw new Error(`Existing Codex version directory is invalid: ${versionDir}`);
|
|
2218
|
-
}
|
|
2219
|
-
installedDir = versionDir;
|
|
2220
|
-
appendCodexUpdateLog(job, "system", `Reusing verified version directory: ${versionDir}`);
|
|
2221
|
-
}
|
|
2222
|
-
const previousTarget = await prepareVersionedCodexLauncher({
|
|
2223
|
-
runtimeDir,
|
|
2224
|
-
codexRoot,
|
|
2225
|
-
versionsDir,
|
|
2226
|
-
currentLink,
|
|
2227
|
-
managedCodexBin,
|
|
2228
|
-
job,
|
|
2229
|
-
});
|
|
2230
|
-
const nextTarget = path.relative(codexRoot, installedDir);
|
|
2231
|
-
appendCodexUpdateLog(job, "system", `Activating Codex CLI ${installedVersion}`);
|
|
2232
|
-
await replaceSymlinkAtomically(currentLink, nextTarget);
|
|
2233
|
-
await replaceManagedCodexLauncher(managedCodexBin);
|
|
2234
|
-
let activeVersionOutput;
|
|
2235
|
-
try {
|
|
2236
|
-
activeVersionOutput = await executeCodexVersion(managedCodexBin, CODEX_VERSION_VERIFY_TIMEOUT_MS);
|
|
2237
|
-
}
|
|
2238
|
-
catch (error) {
|
|
2239
|
-
if (previousTarget) {
|
|
2240
|
-
await replaceSymlinkAtomically(currentLink, previousTarget);
|
|
2241
|
-
}
|
|
2242
|
-
throw error;
|
|
2243
|
-
}
|
|
2244
|
-
if (normalizeCodexVersion(activeVersionOutput ?? "") !== installedVersion) {
|
|
2245
|
-
if (previousTarget) {
|
|
2246
|
-
await replaceSymlinkAtomically(currentLink, previousTarget);
|
|
2247
|
-
}
|
|
2248
|
-
throw new Error(`Activated Codex CLI verification failed: expected ${installedVersion}, got ${activeVersionOutput ?? "unavailable"}`);
|
|
2249
|
-
}
|
|
2250
|
-
appendCodexUpdateLog(job, "system", `Active launcher: ${managedCodexBin}`);
|
|
2251
|
-
config.codexBin = managedCodexBin;
|
|
2252
|
-
return stagedCatalog;
|
|
2253
|
-
}
|
|
2254
|
-
finally {
|
|
2255
|
-
await fs.rm(stagingDir, { recursive: true, force: true });
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
async function readCodexPackageVersion(prefix) {
|
|
2259
|
-
const packageJsonPath = path.join(prefix, "lib", "node_modules", "@openai", "codex", "package.json");
|
|
2260
|
-
try {
|
|
2261
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
2262
|
-
return typeof packageJson.version === "string" ? packageJson.version : null;
|
|
2263
|
-
}
|
|
2264
|
-
catch {
|
|
2265
|
-
return null;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
function sanitizeCodexVersionDirectory(version) {
|
|
2269
|
-
return version.replace(/[^0-9A-Za-z._-]/g, "_");
|
|
2270
|
-
}
|
|
2271
|
-
function codexVersionDirectoryName(version) {
|
|
2272
|
-
return [
|
|
2273
|
-
sanitizeCodexVersionDirectory(version),
|
|
2274
|
-
process.platform,
|
|
2275
|
-
process.arch,
|
|
2276
|
-
].join("-");
|
|
2277
|
-
}
|
|
2278
|
-
function isExistingPathError(error) {
|
|
2279
|
-
const code = error?.code;
|
|
2280
|
-
return code === "EEXIST" || code === "ENOTEMPTY";
|
|
2281
|
-
}
|
|
2282
|
-
async function prepareVersionedCodexLauncher({ runtimeDir, codexRoot, versionsDir, currentLink, managedCodexBin, job, }) {
|
|
2283
|
-
const currentTarget = await readSymlink(currentLink);
|
|
2284
|
-
if (await isManagedCodexLauncher(managedCodexBin)) {
|
|
2285
|
-
return currentTarget;
|
|
2286
|
-
}
|
|
2287
|
-
try {
|
|
2288
|
-
await fs.access(managedCodexBin, fsConstants.X_OK);
|
|
2289
|
-
}
|
|
2290
|
-
catch {
|
|
2291
|
-
return currentTarget;
|
|
2292
|
-
}
|
|
2293
|
-
const legacyVersionOutput = await readCodexVersion(managedCodexBin);
|
|
2294
|
-
const legacyVersion = normalizeCodexVersion(legacyVersionOutput ?? "") ?? "unknown";
|
|
2295
|
-
const legacyName = `legacy-${sanitizeCodexVersionDirectory(legacyVersion)}-${job.id.slice(0, 8)}`;
|
|
2296
|
-
const legacyDir = path.join(versionsDir, legacyName);
|
|
2297
|
-
const legacyBinDir = path.join(legacyDir, "bin");
|
|
2298
|
-
await fs.mkdir(legacyBinDir, { recursive: true });
|
|
2299
|
-
const oldNpmLib = path.join(runtimeDir, "lib");
|
|
2300
|
-
const oldNpmVersion = await readCodexPackageVersion(runtimeDir);
|
|
2301
|
-
if (oldNpmVersion) {
|
|
2302
|
-
await fs.cp(oldNpmLib, path.join(legacyDir, "lib"), {
|
|
2303
|
-
recursive: true,
|
|
2304
|
-
verbatimSymlinks: true,
|
|
2305
|
-
});
|
|
2306
|
-
const oldLinkTarget = await readSymlink(managedCodexBin);
|
|
2307
|
-
if (oldLinkTarget) {
|
|
2308
|
-
await fs.symlink(oldLinkTarget, path.join(legacyBinDir, "codex"));
|
|
2309
|
-
}
|
|
2310
|
-
else {
|
|
2311
|
-
await fs.copyFile(managedCodexBin, path.join(legacyBinDir, "codex"));
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
else {
|
|
2315
|
-
await fs.copyFile(managedCodexBin, path.join(legacyBinDir, "codex"));
|
|
2316
|
-
}
|
|
2317
|
-
await fs.chmod(path.join(legacyBinDir, "codex"), 0o755).catch(() => undefined);
|
|
2318
|
-
const legacyTarget = path.relative(codexRoot, legacyDir);
|
|
2319
|
-
await replaceSymlinkAtomically(currentLink, legacyTarget);
|
|
2320
|
-
appendCodexUpdateLog(job, "system", `Preserved previous Codex CLI as ${legacyTarget}`);
|
|
2321
|
-
return legacyTarget;
|
|
2322
|
-
}
|
|
2323
|
-
async function replaceManagedCodexLauncher(managedCodexBin) {
|
|
2324
|
-
if (await isManagedCodexLauncher(managedCodexBin)) {
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
await replaceSymlinkAtomically(managedCodexBin, "../codex/current/bin/codex");
|
|
2328
|
-
}
|
|
2329
|
-
async function isManagedCodexLauncher(managedCodexBin) {
|
|
2330
|
-
return (await readSymlink(managedCodexBin)) === "../codex/current/bin/codex";
|
|
2331
|
-
}
|
|
2332
|
-
async function readSymlink(linkPath) {
|
|
2333
|
-
try {
|
|
2334
|
-
return await fs.readlink(linkPath);
|
|
2335
|
-
}
|
|
2336
|
-
catch {
|
|
2337
|
-
return null;
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
async function replaceSymlinkAtomically(linkPath, target) {
|
|
2341
|
-
const temporaryLink = `${linkPath}.next-${randomUUID()}`;
|
|
2342
|
-
await fs.mkdir(path.dirname(linkPath), { recursive: true });
|
|
2343
|
-
try {
|
|
2344
|
-
await fs.symlink(target, temporaryLink);
|
|
2345
|
-
await fs.rename(temporaryLink, linkPath);
|
|
2346
|
-
}
|
|
2347
|
-
finally {
|
|
2348
|
-
await fs.rm(temporaryLink, { force: true });
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
function runLoggedProcess(job, command, args, options) {
|
|
2352
|
-
appendCodexUpdateLog(job, "system", `$ ${command} ${args.join(" ")}`);
|
|
2353
|
-
return new Promise((resolve, reject) => {
|
|
2354
|
-
const child = spawn(command, args, {
|
|
2355
|
-
env: options.env,
|
|
2356
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2357
|
-
});
|
|
2358
|
-
const timeout = setTimeout(() => {
|
|
2359
|
-
child.kill("SIGTERM");
|
|
2360
|
-
reject(new Error(`Codex CLI update timed out after ${CODEX_UPDATE_TIMEOUT_MS}ms`));
|
|
2361
|
-
}, CODEX_UPDATE_TIMEOUT_MS);
|
|
2362
|
-
child.stdout.on("data", (chunk) => {
|
|
2363
|
-
appendCodexUpdateLog(job, "stdout", chunk.toString("utf8"));
|
|
2364
|
-
});
|
|
2365
|
-
child.stderr.on("data", (chunk) => {
|
|
2366
|
-
appendCodexUpdateLog(job, "stderr", chunk.toString("utf8"));
|
|
2367
|
-
});
|
|
2368
|
-
child.once("error", (error) => {
|
|
2369
|
-
clearTimeout(timeout);
|
|
2370
|
-
reject(error);
|
|
2371
|
-
});
|
|
2372
|
-
child.once("close", (code, signal) => {
|
|
2373
|
-
clearTimeout(timeout);
|
|
2374
|
-
if (code === 0) {
|
|
2375
|
-
resolve();
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
reject(new Error(`Codex CLI installer exited with ${code ?? "no code"}${signal ? ` (${signal})` : ""}`));
|
|
2379
|
-
});
|
|
2380
|
-
});
|
|
2381
|
-
}
|
|
2382
|
-
function appendCodexUpdateLog(job, stream, value) {
|
|
2383
|
-
const lines = value
|
|
2384
|
-
.split(/\r?\n/)
|
|
2385
|
-
.map((line) => line.trimEnd())
|
|
2386
|
-
.filter(Boolean);
|
|
2387
|
-
for (const text of lines) {
|
|
2388
|
-
const effectiveStream = stream === "stderr" && /^npm\s+warn\b/i.test(text) ? "warning" : stream;
|
|
2389
|
-
job.logs.push({ at: new Date().toISOString(), stream: effectiveStream, text });
|
|
2390
|
-
}
|
|
2391
|
-
if (job.logs.length > 500) {
|
|
2392
|
-
job.logs.splice(0, job.logs.length - 500);
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
async function readCodexVersion(codexBin) {
|
|
2396
|
-
try {
|
|
2397
|
-
return await executeCodexVersion(codexBin, CODEX_VERSION_READ_TIMEOUT_MS);
|
|
2398
|
-
}
|
|
2399
|
-
catch {
|
|
2400
|
-
return null;
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
async function executeCodexVersion(codexBin, timeout) {
|
|
2404
|
-
const { stdout, stderr } = await execFileAsync(codexBin, ["--version"], {
|
|
2405
|
-
env: process.env,
|
|
2406
|
-
timeout,
|
|
2407
|
-
});
|
|
2408
|
-
const output = stdout.trim() || stderr.trim();
|
|
2409
|
-
if (!output) {
|
|
2410
|
-
throw new Error(`Codex CLI returned no version output: ${codexBin}`);
|
|
2411
|
-
}
|
|
2412
|
-
return output;
|
|
2413
|
-
}
|
|
2414
|
-
function formatCodexUpdateError(error) {
|
|
2415
|
-
if (!(error instanceof Error)) {
|
|
2416
|
-
return String(error);
|
|
2417
|
-
}
|
|
2418
|
-
const processError = error;
|
|
2419
|
-
const output = processError.stderr?.trim() || processError.stdout?.trim();
|
|
2420
|
-
return output ? `${error.message}\n${output}` : error.message;
|
|
2421
|
-
}
|
|
2422
|
-
function codexNetworkEnvironment() {
|
|
2423
|
-
if (!config.proxy) {
|
|
2424
|
-
return process.env;
|
|
2425
|
-
}
|
|
2426
|
-
return {
|
|
2427
|
-
...process.env,
|
|
2428
|
-
ALL_PROXY: config.proxy,
|
|
2429
|
-
HTTP_PROXY: config.proxy,
|
|
2430
|
-
HTTPS_PROXY: config.proxy,
|
|
2431
|
-
all_proxy: config.proxy,
|
|
2432
|
-
http_proxy: config.proxy,
|
|
2433
|
-
https_proxy: config.proxy,
|
|
2434
|
-
};
|
|
2435
|
-
}
|
|
2436
|
-
function codexNpmEnvironment() {
|
|
2437
|
-
const environment = { ...process.env };
|
|
2438
|
-
for (const key of [
|
|
2439
|
-
"ALL_PROXY",
|
|
2440
|
-
"HTTP_PROXY",
|
|
2441
|
-
"HTTPS_PROXY",
|
|
2442
|
-
"all_proxy",
|
|
2443
|
-
"http_proxy",
|
|
2444
|
-
"https_proxy",
|
|
2445
|
-
"npm_config_proxy",
|
|
2446
|
-
"npm_config_https_proxy",
|
|
2447
|
-
"npm_config_recursive",
|
|
2448
|
-
"npm_config_store_dir",
|
|
2449
|
-
]) {
|
|
2450
|
-
delete environment[key];
|
|
2451
|
-
}
|
|
2452
|
-
const registryHost = new URL(CODEX_NPM_REGISTRY).hostname;
|
|
2453
|
-
const noProxy = [environment.NO_PROXY, environment.no_proxy, registryHost]
|
|
2454
|
-
.filter(Boolean)
|
|
2455
|
-
.join(",");
|
|
2456
|
-
return {
|
|
2457
|
-
...environment,
|
|
2458
|
-
NO_PROXY: noProxy,
|
|
2459
|
-
no_proxy: noProxy,
|
|
2460
|
-
};
|
|
2461
|
-
}
|
|
2462
|
-
function steerConversationTurn(conversation, turnId, message, body = {}) {
|
|
2463
|
-
runtime.steerTurn(turnId, message);
|
|
2464
|
-
const persistUserMessage = body.persistUserMessage !== false;
|
|
2465
|
-
const userMessageRecord = persistUserMessage
|
|
2466
|
-
? appendMessage(conversation, "user", message, turnId)
|
|
2467
|
-
: null;
|
|
2468
|
-
appendAndBroadcastTurnEvent(turnId, "server.steer", {
|
|
2469
|
-
message,
|
|
2470
|
-
messageId: userMessageRecord?.id ?? null,
|
|
2471
|
-
source: typeof body.source === "string" ? body.source : "web",
|
|
2472
|
-
persistedUserMessage: persistUserMessage,
|
|
2473
|
-
});
|
|
2474
|
-
return userMessageRecord;
|
|
2475
|
-
}
|
|
2476
|
-
function createQueuedConversationTurn(conversation, body, userMessage) {
|
|
2477
|
-
const turnId = randomUUID();
|
|
2478
|
-
const persistUserMessage = body.persistUserMessage !== false;
|
|
2479
|
-
const userMessageRecord = persistUserMessage
|
|
2480
|
-
? appendMessage(conversation, "user", userMessage, turnId)
|
|
2481
|
-
: null;
|
|
2482
|
-
createConversationTurn(conversation, {
|
|
2483
|
-
id: turnId,
|
|
2484
|
-
userMessageId: userMessageRecord?.id ?? null,
|
|
2485
|
-
status: "queued",
|
|
2486
|
-
});
|
|
2487
|
-
turnConversations.set(turnId, conversation.id);
|
|
2488
|
-
turnEventBacklog.set(turnId, []);
|
|
2489
|
-
appendEvent(conversation, "server.turn", {
|
|
2490
|
-
source: typeof body.source === "string" ? body.source : "web",
|
|
2491
|
-
turnId,
|
|
2492
|
-
userMessageId: userMessageRecord?.id ?? null,
|
|
2493
|
-
wakeupId: typeof body.wakeupId === "string" ? body.wakeupId : null,
|
|
2494
|
-
persistedUserMessage: persistUserMessage,
|
|
2495
|
-
}, { turnId });
|
|
2496
|
-
if (typeof body.wakeupId === "string") {
|
|
2497
|
-
turnWakeups.set(turnId, body.wakeupId);
|
|
2498
|
-
}
|
|
2499
|
-
return { turnId, message: userMessageRecord };
|
|
2500
|
-
}
|
|
2501
|
-
function startConversationTurn(conversation, body) {
|
|
2502
|
-
const userMessage = String(body.message ?? "");
|
|
2503
|
-
const { turnId } = createQueuedConversationTurn(conversation, body, userMessage);
|
|
2504
|
-
startQueuedConversationTurn(conversation, { turnId, body, userMessage });
|
|
2505
|
-
return turnId;
|
|
2506
|
-
}
|
|
2507
|
-
function startQueuedConversationTurn(conversation, queuedTurn) {
|
|
2508
|
-
void startQueuedConversationTurnAsync(conversation, queuedTurn);
|
|
2509
|
-
}
|
|
2510
|
-
async function startQueuedConversationTurnAsync(conversation, queuedTurn) {
|
|
2511
|
-
const { turnId, body, userMessage } = queuedTurn;
|
|
2512
|
-
setConversationStatus(conversation, "running");
|
|
2513
|
-
activeConversationTurns.set(conversation.id, turnId);
|
|
2514
|
-
updateConversationTurn(conversation, turnId, { status: "running" });
|
|
2515
|
-
appendEvent(conversation, "server.turn.started", {
|
|
2516
|
-
turnId,
|
|
2517
|
-
source: typeof body.source === "string" ? body.source : "web",
|
|
2518
|
-
}, { turnId });
|
|
2519
|
-
try {
|
|
2520
|
-
ensureWorkspaceSkillGuides(conversation.workspacePath);
|
|
2521
|
-
const runtimeConfig = withWorkspaceRuntimeSettings(config, conversation);
|
|
2522
|
-
const injectionContext = buildRuntimeInjectionContext(conversation);
|
|
2523
|
-
runtime.startTurn(turnId, runtimeConfig, {
|
|
2524
|
-
...body,
|
|
2525
|
-
message: userMessage,
|
|
2526
|
-
threadId: conversation.codexThreadId,
|
|
2527
|
-
cwd: conversation.workspacePath,
|
|
2528
|
-
...injectionContext,
|
|
2529
|
-
sandbox: runtimeConfig.defaultSandbox,
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2532
|
-
catch (error) {
|
|
2533
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2534
|
-
failTurn(turnId, message);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
function buildRuntimeInjectionContext(conversation) {
|
|
2538
|
-
return {
|
|
2539
|
-
workspaceId: conversation.id,
|
|
2540
|
-
workspaceTitle: conversation.title,
|
|
2541
|
-
workspaceTranscriptApiBase: getAgentTranscriptApiBase(conversation),
|
|
2542
|
-
workspaceKnowledgeApiBase: getAgentKnowledgeApiBase(conversation),
|
|
2543
|
-
workspaceResponsibility: conversation.responsibility,
|
|
2544
|
-
workspaceKnowledge: summarizeWorkspaceKnowledge(conversation),
|
|
2545
|
-
globalMemory: getGlobalMemory(),
|
|
2546
|
-
workspaceMemory: conversation.memory,
|
|
2547
|
-
globalMemoryItems: getGlobalMemoryItems(),
|
|
2548
|
-
workspaceMemoryItems: conversation.memoryItems,
|
|
2549
|
-
};
|
|
2550
|
-
}
|
|
2551
|
-
async function resetConversationRuntimeThread(conversation, options, plan = buildRuntimeResetPlan(conversation)) {
|
|
2552
|
-
if (activeConversationTurns.has(conversation.id)) {
|
|
2553
|
-
throw new Error("workspace has an active turn");
|
|
2554
|
-
}
|
|
2555
|
-
const queuedTurns = workspaceTurnQueues.get(conversation.id);
|
|
2556
|
-
if (queuedTurns && queuedTurns.length > 0) {
|
|
2557
|
-
throw new Error("workspace has queued turns");
|
|
2558
|
-
}
|
|
2559
|
-
const resetStartedAt = new Date().toISOString();
|
|
2560
|
-
const oldThreadId = conversation.codexThreadId;
|
|
2561
|
-
const { visibleMessages, injectedItems, injectedUserMessageCount, injectedAssistantMessageCount, } = plan;
|
|
2562
|
-
let newThreadId = null;
|
|
2563
|
-
appendEvent(conversation, "server.runtime_reset.started", {
|
|
2564
|
-
oldThreadId,
|
|
2565
|
-
mode: "lossless-visible-history",
|
|
2566
|
-
visibleMessageCount: visibleMessages.length,
|
|
2567
|
-
});
|
|
2568
|
-
try {
|
|
2569
|
-
ensureWorkspaceSkillGuides(conversation.workspacePath);
|
|
2570
|
-
const runtimeConfig = withWorkspaceRuntimeSettings(config, conversation);
|
|
2571
|
-
const injectionContext = buildRuntimeInjectionContext(conversation);
|
|
2572
|
-
const result = await runtime.rebuildThread(runtimeConfig, {
|
|
2573
|
-
cwd: conversation.workspacePath,
|
|
2574
|
-
...injectionContext,
|
|
2575
|
-
sandbox: runtimeConfig.defaultSandbox,
|
|
2576
|
-
}, injectedItems);
|
|
2577
|
-
newThreadId = result.threadId;
|
|
2578
|
-
setConversationThread(conversation, newThreadId);
|
|
2579
|
-
recordRuntimeReset(conversation, {
|
|
2580
|
-
oldCodexThreadId: oldThreadId,
|
|
2581
|
-
newCodexThreadId: newThreadId,
|
|
2582
|
-
status: "completed",
|
|
2583
|
-
injectedMessageCount: visibleMessages.length,
|
|
2584
|
-
injectedUserMessageCount,
|
|
2585
|
-
injectedAssistantMessageCount,
|
|
2586
|
-
injectedMessages: visibleMessages,
|
|
2587
|
-
createdAt: resetStartedAt,
|
|
2588
|
-
});
|
|
2589
|
-
appendEvent(conversation, "server.runtime_reset.completed", {
|
|
2590
|
-
oldThreadId,
|
|
2591
|
-
newThreadId,
|
|
2592
|
-
mode: "lossless-visible-history",
|
|
2593
|
-
injectedMessageCount: visibleMessages.length,
|
|
2594
|
-
injectedUserMessageCount,
|
|
2595
|
-
injectedAssistantMessageCount,
|
|
2596
|
-
});
|
|
2597
|
-
const warmupTurnId = options.warmup
|
|
2598
|
-
? startConversationTurn(conversation, {
|
|
2599
|
-
source: "runtime-reset",
|
|
2600
|
-
persistUserMessage: false,
|
|
2601
|
-
message: buildRuntimeResetWarmupMessage({
|
|
2602
|
-
workspaceId: conversation.id,
|
|
2603
|
-
workspaceTitle: conversation.title,
|
|
2604
|
-
oldThreadId,
|
|
2605
|
-
newThreadId,
|
|
2606
|
-
injectedMessageCount: visibleMessages.length,
|
|
2607
|
-
injectedUserMessageCount,
|
|
2608
|
-
injectedAssistantMessageCount,
|
|
2609
|
-
}),
|
|
2610
|
-
})
|
|
2611
|
-
: null;
|
|
2612
|
-
return {
|
|
2613
|
-
oldThreadId,
|
|
2614
|
-
newThreadId,
|
|
2615
|
-
injectedMessageCount: visibleMessages.length,
|
|
2616
|
-
injectedUserMessageCount,
|
|
2617
|
-
injectedAssistantMessageCount,
|
|
2618
|
-
warmupTurnId,
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
catch (error) {
|
|
2622
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2623
|
-
recordRuntimeReset(conversation, {
|
|
2624
|
-
oldCodexThreadId: oldThreadId,
|
|
2625
|
-
newCodexThreadId: newThreadId,
|
|
2626
|
-
status: "failed",
|
|
2627
|
-
injectedMessageCount: visibleMessages.length,
|
|
2628
|
-
injectedUserMessageCount,
|
|
2629
|
-
injectedAssistantMessageCount,
|
|
2630
|
-
injectedMessages: visibleMessages,
|
|
2631
|
-
error: message,
|
|
2632
|
-
createdAt: resetStartedAt,
|
|
2633
|
-
});
|
|
2634
|
-
appendEvent(conversation, "server.runtime_reset.failed", {
|
|
2635
|
-
oldThreadId,
|
|
2636
|
-
newThreadId,
|
|
2637
|
-
mode: "lossless-visible-history",
|
|
2638
|
-
injectedMessageCount: visibleMessages.length,
|
|
2639
|
-
error: message,
|
|
2640
|
-
});
|
|
2641
|
-
throw error;
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
function buildRuntimeResetPlan(conversation) {
|
|
2645
|
-
const visibleMessages = listAllConversationVisibleMessages(conversation);
|
|
2646
|
-
const injectedItems = visibleMessages.map(toResponseHistoryItem);
|
|
2647
|
-
return {
|
|
2648
|
-
visibleMessages,
|
|
2649
|
-
injectedItems,
|
|
2650
|
-
injectedUserMessageCount: visibleMessages.filter((message) => message.role === "user").length,
|
|
2651
|
-
injectedAssistantMessageCount: visibleMessages.filter((message) => message.role === "assistant").length,
|
|
2652
|
-
fingerprint: createHash("sha256")
|
|
2653
|
-
.update(JSON.stringify(injectedItems))
|
|
2654
|
-
.digest("hex"),
|
|
2655
|
-
};
|
|
2656
|
-
}
|
|
2657
|
-
function toResponseHistoryItem(message) {
|
|
2658
|
-
if (message.role === "assistant") {
|
|
2659
|
-
return {
|
|
2660
|
-
type: "message",
|
|
2661
|
-
role: "assistant",
|
|
2662
|
-
content: [
|
|
2663
|
-
{
|
|
2664
|
-
type: "output_text",
|
|
2665
|
-
text: message.content,
|
|
2666
|
-
},
|
|
2667
|
-
],
|
|
2668
|
-
};
|
|
2669
|
-
}
|
|
2670
|
-
return {
|
|
2671
|
-
type: "message",
|
|
2672
|
-
role: "user",
|
|
2673
|
-
content: [
|
|
2674
|
-
{
|
|
2675
|
-
type: "input_text",
|
|
2676
|
-
text: message.content,
|
|
2677
|
-
},
|
|
2678
|
-
],
|
|
2679
|
-
};
|
|
2680
|
-
}
|
|
2681
|
-
function buildRuntimeResetWarmupMessage(input) {
|
|
2682
|
-
return [
|
|
2683
|
-
"这是 Seele 系统触发的 runtime reset warmup,不是用户的新需求。",
|
|
2684
|
-
"",
|
|
2685
|
-
"当前 workspace 的 Codex runtime thread 已经完成无损可见历史重建:",
|
|
2686
|
-
`- workspace: ${input.workspaceTitle} (${input.workspaceId})`,
|
|
2687
|
-
`- old thread: ${input.oldThreadId ?? "none"}`,
|
|
2688
|
-
`- new thread: ${input.newThreadId}`,
|
|
2689
|
-
`- injected visible messages: ${input.injectedMessageCount}`,
|
|
2690
|
-
`- injected user messages: ${input.injectedUserMessageCount}`,
|
|
2691
|
-
`- injected assistant messages: ${input.injectedAssistantMessageCount}`,
|
|
2692
|
-
"",
|
|
2693
|
-
"请做一次轻量恢复:",
|
|
2694
|
-
"1. 基于已经注入的新 thread 可见历史,恢复你对当前 workspace 的状态感知。",
|
|
2695
|
-
"2. 必要时只通过 Seele Transcript / Knowledge API 定向回看,不要执行重操作。",
|
|
2696
|
-
"3. 用简短中文输出:我是谁、当前职责、最近上下文、接下来等待用户什么或建议继续什么。",
|
|
2697
|
-
].join("\n");
|
|
2698
|
-
}
|
|
2699
|
-
function ensureWorkspaceSkillGuides(workspacePath) {
|
|
2700
|
-
const guidePath = path.join(workspacePath, RESPONSIBILITY_UPDATE_GUIDE_RELATIVE_PATH);
|
|
2701
|
-
mkdirSync(path.dirname(guidePath), { recursive: true });
|
|
2702
|
-
writeFileSync(guidePath, RESPONSIBILITY_UPDATE_GUIDE, "utf8");
|
|
2703
|
-
const knowledgeGuidePath = path.join(workspacePath, KNOWLEDGE_SOURCE_GUIDE_RELATIVE_PATH);
|
|
2704
|
-
mkdirSync(path.dirname(knowledgeGuidePath), { recursive: true });
|
|
2705
|
-
writeFileSync(knowledgeGuidePath, KNOWLEDGE_SOURCE_GUIDE, "utf8");
|
|
2706
|
-
}
|
|
2707
|
-
function startWakeupScheduler() {
|
|
2708
|
-
void processDueWakeups();
|
|
2709
|
-
const timer = setInterval(() => {
|
|
2710
|
-
void processDueWakeups();
|
|
2711
|
-
}, WAKEUP_POLL_INTERVAL_MS);
|
|
2712
|
-
timer.unref?.();
|
|
2713
|
-
}
|
|
2714
|
-
function startRuntimeWatchdog() {
|
|
2715
|
-
const timer = setInterval(() => {
|
|
2716
|
-
checkRuntimeWorkerHealth();
|
|
2717
|
-
}, RUNTIME_WATCHDOG_INTERVAL_MS);
|
|
2718
|
-
timer.unref?.();
|
|
2719
|
-
}
|
|
2720
|
-
function checkRuntimeWorkerHealth() {
|
|
2721
|
-
const diagnostics = runtime.getDiagnostics();
|
|
2722
|
-
const activeWorkerTurnIds = new Set(diagnostics.workers.map((worker) => worker.turnId));
|
|
2723
|
-
for (const worker of diagnostics.workers) {
|
|
2724
|
-
if (worker.uptimeMs >= TURN_LONG_RUNNING_AFTER_MS &&
|
|
2725
|
-
!longRunningAdvisedTurns.has(worker.turnId)) {
|
|
2726
|
-
longRunningAdvisedTurns.add(worker.turnId);
|
|
2727
|
-
recordTurnEvent(worker.turnId, "server.turn.long_running", {
|
|
2728
|
-
uptimeMs: worker.uptimeMs,
|
|
2729
|
-
thresholdMs: TURN_LONG_RUNNING_AFTER_MS,
|
|
2730
|
-
recommendation: "Consider finishing the current phase and scheduling a wakeup for the next phase if the remaining work can resume from a self-contained instruction.",
|
|
2731
|
-
});
|
|
2732
|
-
}
|
|
2733
|
-
if (worker.stale && !staleRuntimeTurns.has(worker.turnId)) {
|
|
2734
|
-
staleRuntimeTurns.add(worker.turnId);
|
|
2735
|
-
recordTurnEvent(worker.turnId, "runtime.worker_stale", {
|
|
2736
|
-
pid: worker.pid,
|
|
2737
|
-
state: worker.state,
|
|
2738
|
-
uptimeMs: worker.uptimeMs,
|
|
2739
|
-
lastMessageAt: worker.lastMessageAt,
|
|
2740
|
-
lastEventAt: worker.lastEventAt,
|
|
2741
|
-
lastHeartbeatAt: worker.lastHeartbeatAt,
|
|
2742
|
-
});
|
|
2743
|
-
continue;
|
|
2744
|
-
}
|
|
2745
|
-
if (!worker.stale && staleRuntimeTurns.delete(worker.turnId)) {
|
|
2746
|
-
recordTurnEvent(worker.turnId, "runtime.worker_recovered", {
|
|
2747
|
-
pid: worker.pid,
|
|
2748
|
-
state: worker.state,
|
|
2749
|
-
uptimeMs: worker.uptimeMs,
|
|
2750
|
-
lastMessageAt: worker.lastMessageAt,
|
|
2751
|
-
lastEventAt: worker.lastEventAt,
|
|
2752
|
-
lastHeartbeatAt: worker.lastHeartbeatAt,
|
|
2753
|
-
});
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
for (const turnId of [...staleRuntimeTurns]) {
|
|
2757
|
-
if (!activeWorkerTurnIds.has(turnId)) {
|
|
2758
|
-
staleRuntimeTurns.delete(turnId);
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
async function processDueWakeups() {
|
|
2763
|
-
const wakeups = listDueWorkspaceWakeups(new Date().toISOString());
|
|
2764
|
-
for (const wakeup of wakeups) {
|
|
2765
|
-
if (wakeupsInFlight.has(wakeup.id)) {
|
|
2766
|
-
continue;
|
|
2767
|
-
}
|
|
2768
|
-
const conversation = getConversation(wakeup.workspaceId);
|
|
2769
|
-
if (!conversation) {
|
|
2770
|
-
updateWorkspaceWakeup(wakeup.id, {
|
|
2771
|
-
status: "failed",
|
|
2772
|
-
error: "workspace_not_found",
|
|
2773
|
-
});
|
|
2774
|
-
continue;
|
|
2775
|
-
}
|
|
2776
|
-
if (activeConversationTurns.has(conversation.id) || conversation.status === "running") {
|
|
2777
|
-
continue;
|
|
2778
|
-
}
|
|
2779
|
-
if (wakeup.attemptCount >= wakeup.maxAttempts) {
|
|
2780
|
-
updateWorkspaceWakeup(wakeup.id, {
|
|
2781
|
-
status: "failed",
|
|
2782
|
-
error: "max_attempts_exceeded",
|
|
2783
|
-
});
|
|
2784
|
-
appendEvent(conversation, "server.wakeup.failed", {
|
|
2785
|
-
wakeupId: wakeup.id,
|
|
2786
|
-
reason: "max_attempts_exceeded",
|
|
2787
|
-
attemptCount: wakeup.attemptCount,
|
|
2788
|
-
maxAttempts: wakeup.maxAttempts,
|
|
2789
|
-
});
|
|
2790
|
-
continue;
|
|
2791
|
-
}
|
|
2792
|
-
wakeupsInFlight.add(wakeup.id);
|
|
2793
|
-
try {
|
|
2794
|
-
runWorkspaceWakeup(conversation, wakeup);
|
|
2795
|
-
}
|
|
2796
|
-
finally {
|
|
2797
|
-
wakeupsInFlight.delete(wakeup.id);
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
function runWorkspaceWakeup(conversation, wakeup) {
|
|
2802
|
-
const message = [
|
|
2803
|
-
"定时唤醒:请继续处理之前设置的延后检查任务。",
|
|
2804
|
-
"",
|
|
2805
|
-
`Wakeup ID: ${wakeup.id}`,
|
|
2806
|
-
`Scheduled at: ${wakeup.wakeAt}`,
|
|
2807
|
-
"",
|
|
2808
|
-
wakeup.instruction,
|
|
2809
|
-
"",
|
|
2810
|
-
"如果外部状态还没有变化,请简短说明当前仍需等待;如果已经变化,请继续推进并说明结果。",
|
|
2811
|
-
].join("\n");
|
|
2812
|
-
try {
|
|
2813
|
-
const attemptCount = wakeup.attemptCount + 1;
|
|
2814
|
-
updateWorkspaceWakeup(wakeup.id, {
|
|
2815
|
-
status: "running",
|
|
2816
|
-
attemptCount,
|
|
2817
|
-
error: null,
|
|
2818
|
-
});
|
|
2819
|
-
const turnId = startConversationTurn(conversation, {
|
|
2820
|
-
message,
|
|
2821
|
-
persistUserMessage: false,
|
|
2822
|
-
source: "wakeup",
|
|
2823
|
-
wakeupId: wakeup.id,
|
|
2824
|
-
});
|
|
2825
|
-
appendEvent(conversation, "server.wakeup.started", {
|
|
2826
|
-
wakeupId: wakeup.id,
|
|
2827
|
-
wakeAt: wakeup.wakeAt,
|
|
2828
|
-
instruction: wakeup.instruction,
|
|
2829
|
-
attemptCount,
|
|
2830
|
-
maxAttempts: wakeup.maxAttempts,
|
|
2831
|
-
turnId,
|
|
2832
|
-
}, { turnId });
|
|
2833
|
-
updateWorkspaceWakeup(wakeup.id, {
|
|
2834
|
-
status: "running",
|
|
2835
|
-
resultTurnId: turnId,
|
|
2836
|
-
error: null,
|
|
2837
|
-
});
|
|
2838
|
-
}
|
|
2839
|
-
catch (error) {
|
|
2840
|
-
const messageText = error instanceof Error ? error.message : String(error);
|
|
2841
|
-
updateWorkspaceWakeup(wakeup.id, {
|
|
2842
|
-
status: "failed",
|
|
2843
|
-
error: messageText,
|
|
2844
|
-
});
|
|
2845
|
-
appendEvent(conversation, "server.wakeup.failed", {
|
|
2846
|
-
wakeupId: wakeup.id,
|
|
2847
|
-
message: messageText,
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
function subscribeTurnEvents(turnId, response) {
|
|
2852
|
-
const subscribers = turnSubscribers.get(turnId) ?? new Set();
|
|
2853
|
-
subscribers.add(response);
|
|
2854
|
-
turnSubscribers.set(turnId, subscribers);
|
|
2855
|
-
for (const event of getTurnReplayEvents(turnId)) {
|
|
2856
|
-
sendBufferedTurnEvent(response, event);
|
|
2857
|
-
}
|
|
2858
|
-
if (turnTerminalEvents.has(turnId) && !response.writableEnded) {
|
|
2859
|
-
response.end();
|
|
2860
|
-
return;
|
|
2861
|
-
}
|
|
2862
|
-
response.on("close", () => {
|
|
2863
|
-
subscribers.delete(response);
|
|
2864
|
-
if (subscribers.size === 0) {
|
|
2865
|
-
turnSubscribers.delete(turnId);
|
|
2866
|
-
}
|
|
2867
|
-
});
|
|
2868
|
-
}
|
|
2869
|
-
function getTurnReplayEvents(turnId) {
|
|
2870
|
-
const conversation = getConversationForTurn(turnId);
|
|
2871
|
-
const persistedEvents = conversation
|
|
2872
|
-
? listConversationTurnEvents(conversation, turnId)
|
|
2873
|
-
: [];
|
|
2874
|
-
const replayEvents = persistedEvents.map((event, index) => ({
|
|
2875
|
-
id: event.id,
|
|
2876
|
-
turnId,
|
|
2877
|
-
event: event.event,
|
|
2878
|
-
data: event.data,
|
|
2879
|
-
createdAt: event.createdAt,
|
|
2880
|
-
seq: index + 1,
|
|
2881
|
-
}));
|
|
2882
|
-
const seenEventIds = new Set(replayEvents.map((event) => event.id));
|
|
2883
|
-
const backlogOnlyEvents = (turnEventBacklog.get(turnId) ?? []).filter((event) => !seenEventIds.has(event.id));
|
|
2884
|
-
const combined = [...replayEvents, ...backlogOnlyEvents].sort((left, right) => {
|
|
2885
|
-
const leftTime = Date.parse(left.createdAt);
|
|
2886
|
-
const rightTime = Date.parse(right.createdAt);
|
|
2887
|
-
if (!Number.isNaN(leftTime) && !Number.isNaN(rightTime) && leftTime !== rightTime) {
|
|
2888
|
-
return leftTime - rightTime;
|
|
2889
|
-
}
|
|
2890
|
-
return String(left.id).localeCompare(String(right.id));
|
|
2891
|
-
});
|
|
2892
|
-
return combined.map((event, index) => ({
|
|
2893
|
-
...event,
|
|
2894
|
-
seq: index + 1,
|
|
2895
|
-
}));
|
|
2896
|
-
}
|
|
2897
|
-
function recordTurnEvent(turnId, event, data) {
|
|
2898
|
-
if (isTurnTerminal(turnId)) {
|
|
2899
|
-
return;
|
|
2900
|
-
}
|
|
2901
|
-
maybePersistRuntimeThread(turnId, event, data);
|
|
2902
|
-
appendAndBroadcastTurnEvent(turnId, event, data);
|
|
2903
|
-
for (const derived of deriveTurnEvents(turnId, event, data)) {
|
|
2904
|
-
appendAndBroadcastTurnEvent(turnId, derived.event, derived.data);
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
function maybePersistRuntimeThread(turnId, event, data) {
|
|
2908
|
-
if (event !== "server.thread") {
|
|
2909
|
-
return;
|
|
2910
|
-
}
|
|
2911
|
-
const conversation = getConversationForTurn(turnId);
|
|
2912
|
-
if (!conversation || conversation.codexThreadId) {
|
|
2913
|
-
return;
|
|
2914
|
-
}
|
|
2915
|
-
const threadId = readRuntimeThreadId(data);
|
|
2916
|
-
if (!threadId) {
|
|
2917
|
-
return;
|
|
2918
|
-
}
|
|
2919
|
-
setConversationThread(conversation, threadId);
|
|
2920
|
-
appendEvent(conversation, "server.thread.created", { threadId }, { turnId });
|
|
2921
|
-
}
|
|
2922
|
-
function readRuntimeThreadId(data) {
|
|
2923
|
-
const record = readRecord(data);
|
|
2924
|
-
return typeof record.id === "string" ? record.id : null;
|
|
2925
|
-
}
|
|
2926
|
-
function appendAndBroadcastTurnEvent(turnId, event, data) {
|
|
2927
|
-
const backlog = turnEventBacklog.get(turnId) ?? [];
|
|
2928
|
-
const buffered = {
|
|
2929
|
-
id: `${turnId}:${backlog.length + 1}`,
|
|
2930
|
-
turnId,
|
|
2931
|
-
event,
|
|
2932
|
-
data,
|
|
2933
|
-
createdAt: new Date().toISOString(),
|
|
2934
|
-
seq: backlog.length + 1,
|
|
2935
|
-
};
|
|
2936
|
-
const conversation = getConversationForTurn(turnId);
|
|
2937
|
-
if (conversation) {
|
|
2938
|
-
const record = appendEvent(conversation, event, data, { turnId, createdAt: buffered.createdAt });
|
|
2939
|
-
buffered.id = record.id;
|
|
2940
|
-
updateTurnStatusFromEvent(conversation, turnId, event, data);
|
|
2941
|
-
}
|
|
2942
|
-
persistRateLimitsFromEvent(event, data, buffered.createdAt);
|
|
2943
|
-
backlog.push(buffered);
|
|
2944
|
-
turnEventBacklog.set(turnId, backlog);
|
|
2945
|
-
broadcastTurnEvent(turnId, buffered);
|
|
2946
|
-
maybeRecordPhaseBoundaryAdvisory(turnId, event, data);
|
|
2947
|
-
}
|
|
2948
|
-
function maybeRecordPhaseBoundaryAdvisory(turnId, event, data) {
|
|
2949
|
-
if (phaseBoundaryAdvisedTurns.has(turnId) ||
|
|
2950
|
-
isTurnTerminal(turnId) ||
|
|
2951
|
-
!isToolOperationStartEvent(event, data)) {
|
|
2952
|
-
return;
|
|
2953
|
-
}
|
|
2954
|
-
const nextCount = (turnOperationCounts.get(turnId) ?? 0) + 1;
|
|
2955
|
-
turnOperationCounts.set(turnId, nextCount);
|
|
2956
|
-
if (nextCount < TURN_PHASE_BOUNDARY_OPERATION_COUNT) {
|
|
2957
|
-
return;
|
|
2958
|
-
}
|
|
2959
|
-
phaseBoundaryAdvisedTurns.add(turnId);
|
|
2960
|
-
appendAndBroadcastTurnEvent(turnId, "server.turn.phase_boundary_recommended", {
|
|
2961
|
-
operationCount: nextCount,
|
|
2962
|
-
threshold: TURN_PHASE_BOUNDARY_OPERATION_COUNT,
|
|
2963
|
-
recommendation: "This turn has accumulated many tool operations. At the next coherent boundary, summarize the current phase and schedule a wakeup so the following phase starts in a fresh turn bubble.",
|
|
2964
|
-
});
|
|
2965
|
-
}
|
|
2966
|
-
function isToolOperationStartEvent(event, data) {
|
|
2967
|
-
if (event !== "codex.notification") {
|
|
2968
|
-
return false;
|
|
2969
|
-
}
|
|
2970
|
-
const notification = readRecord(data);
|
|
2971
|
-
if (notification.method !== "item/started") {
|
|
2972
|
-
return false;
|
|
2973
|
-
}
|
|
2974
|
-
const item = readRecord(readRecord(notification.params).item);
|
|
2975
|
-
return item.type !== "agentMessage";
|
|
2976
|
-
}
|
|
2977
|
-
function deriveTurnEvents(turnId, event, data) {
|
|
2978
|
-
if (event === "codex.server_request") {
|
|
2979
|
-
const request = readRecord(data);
|
|
2980
|
-
if (request.method === "item/commandExecution/requestApproval") {
|
|
2981
|
-
const params = readRecord(request.params);
|
|
2982
|
-
return [
|
|
2983
|
-
{
|
|
2984
|
-
event: "runtime.command.approval_requested",
|
|
2985
|
-
data: {
|
|
2986
|
-
requestId: typeof request.requestId === "number" ? request.requestId : null,
|
|
2987
|
-
itemId: typeof params.itemId === "string" ? params.itemId : null,
|
|
2988
|
-
command: typeof params.command === "string" ? params.command : null,
|
|
2989
|
-
reason: typeof params.reason === "string" ? params.reason : null,
|
|
2990
|
-
},
|
|
2991
|
-
},
|
|
2992
|
-
];
|
|
2993
|
-
}
|
|
2994
|
-
return [];
|
|
2995
|
-
}
|
|
2996
|
-
if (event !== "codex.notification") {
|
|
2997
|
-
return [];
|
|
2998
|
-
}
|
|
2999
|
-
const notification = readRecord(data);
|
|
3000
|
-
const method = notification.method;
|
|
3001
|
-
const params = readRecord(notification.params);
|
|
3002
|
-
if (method === "thread/tokenUsage/updated") {
|
|
3003
|
-
const usage = readContextUsage(params.tokenUsage);
|
|
3004
|
-
if (!usage) {
|
|
3005
|
-
return [];
|
|
3006
|
-
}
|
|
3007
|
-
latestTurnContextUsage.set(turnId, usage);
|
|
3008
|
-
const observed = [];
|
|
3009
|
-
for (const [key, completed] of completedContextCompactionsAwaitingUsage) {
|
|
3010
|
-
if (!key.startsWith(`${turnId}:`)) {
|
|
3011
|
-
continue;
|
|
3012
|
-
}
|
|
3013
|
-
completedContextCompactionsAwaitingUsage.delete(key);
|
|
3014
|
-
observed.push({
|
|
3015
|
-
event: "runtime.compaction.usage_observed",
|
|
3016
|
-
data: {
|
|
3017
|
-
itemId: completed.itemId,
|
|
3018
|
-
type: "contextCompaction",
|
|
3019
|
-
trigger: "codex",
|
|
3020
|
-
status: "completed",
|
|
3021
|
-
durationMs: completed.durationMs,
|
|
3022
|
-
before: completed.before,
|
|
3023
|
-
after: usage,
|
|
3024
|
-
},
|
|
3025
|
-
});
|
|
3026
|
-
}
|
|
3027
|
-
return observed;
|
|
3028
|
-
}
|
|
3029
|
-
if (method === "item/started") {
|
|
3030
|
-
const item = readRecord(params.item);
|
|
3031
|
-
if (item.type === "commandExecution") {
|
|
3032
|
-
return [
|
|
3033
|
-
{
|
|
3034
|
-
event: "runtime.command.started",
|
|
3035
|
-
data: normalizeCommandItem(item),
|
|
3036
|
-
},
|
|
3037
|
-
];
|
|
3038
|
-
}
|
|
3039
|
-
if (item.type === "contextCompaction") {
|
|
3040
|
-
const itemId = typeof item.id === "string" ? item.id : null;
|
|
3041
|
-
activeContextCompactions.set(contextCompactionKey(turnId, itemId), {
|
|
3042
|
-
startedAt: Date.now(),
|
|
3043
|
-
before: latestTurnContextUsage.get(turnId) ?? null,
|
|
3044
|
-
});
|
|
3045
|
-
return [
|
|
3046
|
-
{
|
|
3047
|
-
event: "runtime.compaction.started",
|
|
3048
|
-
data: {
|
|
3049
|
-
itemId,
|
|
3050
|
-
type: item.type,
|
|
3051
|
-
trigger: "codex",
|
|
3052
|
-
before: latestTurnContextUsage.get(turnId) ?? null,
|
|
3053
|
-
},
|
|
3054
|
-
},
|
|
3055
|
-
];
|
|
3056
|
-
}
|
|
3057
|
-
return [];
|
|
3058
|
-
}
|
|
3059
|
-
if (method === "item/completed") {
|
|
3060
|
-
const item = readRecord(params.item);
|
|
3061
|
-
if (item.type === "commandExecution") {
|
|
3062
|
-
return [
|
|
3063
|
-
{
|
|
3064
|
-
event: "runtime.command.completed",
|
|
3065
|
-
data: normalizeCommandItem(item),
|
|
3066
|
-
},
|
|
3067
|
-
];
|
|
3068
|
-
}
|
|
3069
|
-
if (item.type === "contextCompaction") {
|
|
3070
|
-
const itemId = typeof item.id === "string" ? item.id : null;
|
|
3071
|
-
const key = contextCompactionKey(turnId, itemId);
|
|
3072
|
-
const active = activeContextCompactions.get(key);
|
|
3073
|
-
const before = active?.before ?? null;
|
|
3074
|
-
const latestUsage = latestTurnContextUsage.get(turnId) ?? null;
|
|
3075
|
-
const after = hasContextUsageChanged(before, latestUsage) ? latestUsage : null;
|
|
3076
|
-
const durationMs = active ? Math.max(0, Date.now() - active.startedAt) : null;
|
|
3077
|
-
activeContextCompactions.delete(key);
|
|
3078
|
-
if (!after) {
|
|
3079
|
-
completedContextCompactionsAwaitingUsage.set(key, {
|
|
3080
|
-
itemId,
|
|
3081
|
-
startedAt: active?.startedAt ?? Date.now(),
|
|
3082
|
-
durationMs,
|
|
3083
|
-
before,
|
|
3084
|
-
});
|
|
3085
|
-
}
|
|
3086
|
-
return [
|
|
3087
|
-
{
|
|
3088
|
-
event: "runtime.compaction.completed",
|
|
3089
|
-
data: {
|
|
3090
|
-
itemId,
|
|
3091
|
-
type: item.type,
|
|
3092
|
-
trigger: "codex",
|
|
3093
|
-
status: "completed",
|
|
3094
|
-
durationMs,
|
|
3095
|
-
before,
|
|
3096
|
-
after,
|
|
3097
|
-
},
|
|
3098
|
-
},
|
|
3099
|
-
];
|
|
3100
|
-
}
|
|
3101
|
-
return [];
|
|
3102
|
-
}
|
|
3103
|
-
return [];
|
|
3104
|
-
}
|
|
3105
|
-
function contextCompactionKey(turnId, itemId) {
|
|
3106
|
-
return `${turnId}:${itemId ?? "active"}`;
|
|
3107
|
-
}
|
|
3108
|
-
function hasContextUsageChanged(before, after) {
|
|
3109
|
-
if (!after) {
|
|
3110
|
-
return false;
|
|
3111
|
-
}
|
|
3112
|
-
return (!before ||
|
|
3113
|
-
before.totalTokens !== after.totalTokens ||
|
|
3114
|
-
before.modelContextWindow !== after.modelContextWindow);
|
|
3115
|
-
}
|
|
3116
|
-
function readContextUsage(value) {
|
|
3117
|
-
const tokenUsage = readRecord(value);
|
|
3118
|
-
const last = readRecord(tokenUsage.last);
|
|
3119
|
-
const total = readRecord(tokenUsage.total);
|
|
3120
|
-
const usage = typeof last.totalTokens === "number" ? last : total;
|
|
3121
|
-
return typeof usage.totalTokens === "number"
|
|
3122
|
-
? {
|
|
3123
|
-
totalTokens: usage.totalTokens,
|
|
3124
|
-
modelContextWindow: typeof tokenUsage.modelContextWindow === "number"
|
|
3125
|
-
? tokenUsage.modelContextWindow
|
|
3126
|
-
: null,
|
|
3127
|
-
}
|
|
3128
|
-
: null;
|
|
3129
|
-
}
|
|
3130
|
-
function normalizeCommandItem(item) {
|
|
3131
|
-
return {
|
|
3132
|
-
itemId: typeof item.id === "string" ? item.id : null,
|
|
3133
|
-
command: item.command,
|
|
3134
|
-
status: typeof item.status === "string" ? item.status : null,
|
|
3135
|
-
processId: typeof item.processId === "number" || typeof item.processId === "string"
|
|
3136
|
-
? item.processId
|
|
3137
|
-
: null,
|
|
3138
|
-
exitCode: typeof item.exitCode === "number" ? item.exitCode : null,
|
|
3139
|
-
output: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : null,
|
|
3140
|
-
};
|
|
3141
|
-
}
|
|
3142
|
-
function updateTurnStatusFromEvent(conversation, turnId, event, data) {
|
|
3143
|
-
if (event === "server.done") {
|
|
3144
|
-
updateConversationTurn(conversation, turnId, {
|
|
3145
|
-
status: "completed",
|
|
3146
|
-
finishedAt: new Date().toISOString(),
|
|
3147
|
-
error: null,
|
|
3148
|
-
});
|
|
3149
|
-
return;
|
|
3150
|
-
}
|
|
3151
|
-
if (event === "server.error") {
|
|
3152
|
-
const message = readRecord(data).message;
|
|
3153
|
-
updateConversationTurn(conversation, turnId, {
|
|
3154
|
-
status: "failed",
|
|
3155
|
-
finishedAt: new Date().toISOString(),
|
|
3156
|
-
error: typeof message === "string" ? message : "turn_failed",
|
|
3157
|
-
});
|
|
3158
|
-
return;
|
|
3159
|
-
}
|
|
3160
|
-
if (event === "server.stopped") {
|
|
3161
|
-
updateConversationTurn(conversation, turnId, {
|
|
3162
|
-
status: "cancelled",
|
|
3163
|
-
finishedAt: new Date().toISOString(),
|
|
3164
|
-
error: null,
|
|
3165
|
-
});
|
|
3166
|
-
return;
|
|
3167
|
-
}
|
|
3168
|
-
if (event === "codex.server_request") {
|
|
3169
|
-
const request = readRecord(data);
|
|
3170
|
-
if (request.method === "item/commandExecution/requestApproval") {
|
|
3171
|
-
updateConversationTurn(conversation, turnId, {
|
|
3172
|
-
status: "waiting_approval",
|
|
3173
|
-
});
|
|
3174
|
-
}
|
|
3175
|
-
return;
|
|
3176
|
-
}
|
|
3177
|
-
if (event === "server.approval.resolved") {
|
|
3178
|
-
updateConversationTurn(conversation, turnId, {
|
|
3179
|
-
status: "running",
|
|
3180
|
-
});
|
|
3181
|
-
return;
|
|
3182
|
-
}
|
|
3183
|
-
if (event !== "codex.notification") {
|
|
3184
|
-
return;
|
|
3185
|
-
}
|
|
3186
|
-
const notification = readRecord(data);
|
|
3187
|
-
if (notification.method === "turn/started") {
|
|
3188
|
-
const params = readRecord(notification.params);
|
|
3189
|
-
const turn = readRecord(params.turn);
|
|
3190
|
-
updateConversationTurn(conversation, turnId, {
|
|
3191
|
-
codexTurnId: typeof turn.id === "string" ? turn.id : null,
|
|
3192
|
-
status: "running",
|
|
3193
|
-
});
|
|
3194
|
-
return;
|
|
3195
|
-
}
|
|
3196
|
-
if (notification.method === "turn/completed") {
|
|
3197
|
-
const params = readRecord(notification.params);
|
|
3198
|
-
const turn = readRecord(params.turn);
|
|
3199
|
-
updateConversationTurn(conversation, turnId, {
|
|
3200
|
-
codexTurnId: typeof turn.id === "string" ? turn.id : null,
|
|
3201
|
-
status: turn.error ? "failed" : "completed",
|
|
3202
|
-
finishedAt: new Date().toISOString(),
|
|
3203
|
-
error: turn.error ? JSON.stringify(turn.error) : null,
|
|
3204
|
-
});
|
|
3205
|
-
return;
|
|
3206
|
-
}
|
|
3207
|
-
if (notification.method === "thread/status/changed") {
|
|
3208
|
-
const params = readRecord(notification.params);
|
|
3209
|
-
const status = readRecord(params.status);
|
|
3210
|
-
const flags = Array.isArray(status.activeFlags) ? status.activeFlags : [];
|
|
3211
|
-
if (flags.includes("waitingOnApproval")) {
|
|
3212
|
-
updateConversationTurn(conversation, turnId, {
|
|
3213
|
-
status: "waiting_approval",
|
|
3214
|
-
});
|
|
3215
|
-
}
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
function completeTurn(turnId, result) {
|
|
3219
|
-
if (isTurnTerminal(turnId)) {
|
|
3220
|
-
cleanupTurn(turnId);
|
|
3221
|
-
return;
|
|
3222
|
-
}
|
|
3223
|
-
if (locallyStoppedTurns.delete(turnId)) {
|
|
3224
|
-
cleanupTurn(turnId);
|
|
3225
|
-
return;
|
|
3226
|
-
}
|
|
3227
|
-
const conversation = getConversationForTurn(turnId);
|
|
3228
|
-
if (conversation) {
|
|
3229
|
-
const assistantOutput = extractAgentRequests(getTurnAssistantText(turnId, result));
|
|
3230
|
-
if (assistantOutput.visibleText) {
|
|
3231
|
-
appendMessage(conversation, "assistant", assistantOutput.visibleText, turnId);
|
|
3232
|
-
}
|
|
3233
|
-
scheduleAgentWakeups(conversation, turnId, assistantOutput.wakeups);
|
|
3234
|
-
recordResponsibilityProposals(turnId, assistantOutput.responsibilityProposals);
|
|
3235
|
-
recordKnowledgeProposals(turnId, assistantOutput.knowledgeProposals);
|
|
3236
|
-
recordTranscriptSummaries(conversation, turnId, assistantOutput.transcriptSummaries);
|
|
3237
|
-
updateConversationTurn(conversation, turnId, {
|
|
3238
|
-
status: "completed",
|
|
3239
|
-
finishedAt: new Date().toISOString(),
|
|
3240
|
-
error: null,
|
|
3241
|
-
});
|
|
3242
|
-
completeWakeupForTurn(turnId, "completed", null);
|
|
3243
|
-
setConversationStatus(conversation, "done");
|
|
3244
|
-
if (assistantOutput.visibleText &&
|
|
3245
|
-
getWorkspaceRuntimeSettings(conversation).effective.notifyEnabled) {
|
|
3246
|
-
void notifyProfuse(turnId, conversation, assistantOutput.visibleText);
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3249
|
-
finishTurn(turnId, "server.done", { ok: true });
|
|
3250
|
-
}
|
|
3251
|
-
function failTurn(turnId, message) {
|
|
3252
|
-
if (isTurnTerminal(turnId)) {
|
|
3253
|
-
cleanupTurn(turnId);
|
|
3254
|
-
return;
|
|
3255
|
-
}
|
|
3256
|
-
if (locallyStoppedTurns.delete(turnId)) {
|
|
3257
|
-
cleanupTurn(turnId);
|
|
3258
|
-
return;
|
|
3259
|
-
}
|
|
3260
|
-
const conversation = getConversationForTurn(turnId);
|
|
3261
|
-
if (conversation) {
|
|
3262
|
-
updateConversationTurn(conversation, turnId, {
|
|
3263
|
-
status: "failed",
|
|
3264
|
-
finishedAt: new Date().toISOString(),
|
|
3265
|
-
error: message,
|
|
3266
|
-
});
|
|
3267
|
-
completeWakeupForTurn(turnId, "failed", message);
|
|
3268
|
-
setConversationStatus(conversation, "error");
|
|
3269
|
-
if (getWorkspaceRuntimeSettings(conversation).effective.notifyEnabled) {
|
|
3270
|
-
void notifyProfuse(turnId, conversation, ["**任务失败**", "", message].join("\n"));
|
|
3271
|
-
}
|
|
3272
|
-
finishTurn(turnId, "server.error", { message });
|
|
3273
|
-
return;
|
|
3274
|
-
}
|
|
3275
|
-
finishTurn(turnId, "server.error", { message });
|
|
3276
|
-
}
|
|
3277
|
-
function stopTurnLocally(turnId, conversation) {
|
|
3278
|
-
if (isTurnTerminal(turnId)) {
|
|
3279
|
-
cleanupTurn(turnId);
|
|
3280
|
-
return;
|
|
3281
|
-
}
|
|
3282
|
-
locallyStoppedTurns.add(turnId);
|
|
3283
|
-
try {
|
|
3284
|
-
runtime.stopTurn(turnId);
|
|
3285
|
-
}
|
|
3286
|
-
catch (error) {
|
|
3287
|
-
appendEvent(conversation, "runtime.stop.error", { message: error instanceof Error ? error.message : String(error) }, { turnId });
|
|
3288
|
-
}
|
|
3289
|
-
appendEvent(conversation, "server.stop", {}, { turnId });
|
|
3290
|
-
appendEvent(conversation, "server.stopped", {}, { turnId });
|
|
3291
|
-
updateConversationTurn(conversation, turnId, {
|
|
3292
|
-
status: "cancelled",
|
|
3293
|
-
finishedAt: new Date().toISOString(),
|
|
3294
|
-
error: null,
|
|
3295
|
-
});
|
|
3296
|
-
completeWakeupForTurn(turnId, "cancelled", null);
|
|
3297
|
-
setConversationStatus(conversation, "done");
|
|
3298
|
-
finishTurn(turnId, "server.stopped", { ok: true });
|
|
3299
|
-
}
|
|
3300
|
-
function completeWakeupForTurn(turnId, status, error) {
|
|
3301
|
-
const wakeupId = turnWakeups.get(turnId);
|
|
3302
|
-
if (!wakeupId) {
|
|
3303
|
-
return;
|
|
3304
|
-
}
|
|
3305
|
-
updateWorkspaceWakeup(wakeupId, {
|
|
3306
|
-
status,
|
|
3307
|
-
resultTurnId: turnId,
|
|
3308
|
-
error,
|
|
3309
|
-
});
|
|
3310
|
-
}
|
|
3311
|
-
function finishTurn(turnId, event, data) {
|
|
3312
|
-
if (isTurnTerminal(turnId)) {
|
|
3313
|
-
cleanupTurn(turnId);
|
|
3314
|
-
return;
|
|
3315
|
-
}
|
|
3316
|
-
const backlog = turnEventBacklog.get(turnId) ?? [];
|
|
3317
|
-
const lastEvent = backlog[backlog.length - 1];
|
|
3318
|
-
if (lastEvent?.event === event && isSameEventData(lastEvent.data, data)) {
|
|
3319
|
-
turnTerminalEvents.set(turnId, { event, data });
|
|
3320
|
-
cleanupTurn(turnId);
|
|
3321
|
-
return;
|
|
3322
|
-
}
|
|
3323
|
-
appendAndBroadcastTurnEvent(turnId, event, data);
|
|
3324
|
-
turnTerminalEvents.set(turnId, { event, data });
|
|
3325
|
-
cleanupTurn(turnId);
|
|
3326
|
-
}
|
|
3327
|
-
function isTurnTerminal(turnId) {
|
|
3328
|
-
return turnTerminalEvents.has(turnId);
|
|
3329
|
-
}
|
|
3330
|
-
function isSameEventData(left, right) {
|
|
3331
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
3332
|
-
}
|
|
3333
|
-
function summarizeRuntimeCompactions(events) {
|
|
3334
|
-
const records = new Map();
|
|
3335
|
-
for (const event of [...events].reverse()) {
|
|
3336
|
-
const data = readRecord(event.data);
|
|
3337
|
-
const recordId = typeof data.itemId === "string"
|
|
3338
|
-
? data.itemId
|
|
3339
|
-
: `${event.turnId ?? "unknown"}:contextCompaction`;
|
|
3340
|
-
const existing = records.get(recordId) ?? {
|
|
3341
|
-
id: recordId,
|
|
3342
|
-
turnId: event.turnId,
|
|
3343
|
-
status: "running",
|
|
3344
|
-
startedAt: null,
|
|
3345
|
-
completedAt: null,
|
|
3346
|
-
usageObservedAt: null,
|
|
3347
|
-
durationMs: null,
|
|
3348
|
-
trigger: null,
|
|
3349
|
-
before: null,
|
|
3350
|
-
after: null,
|
|
3351
|
-
};
|
|
3352
|
-
if (event.event === "runtime.compaction.started") {
|
|
3353
|
-
existing.startedAt = event.createdAt;
|
|
3354
|
-
existing.trigger = typeof data.trigger === "string" ? data.trigger : null;
|
|
3355
|
-
existing.before = data.before ?? null;
|
|
3356
|
-
}
|
|
3357
|
-
else if (event.event === "runtime.compaction.completed") {
|
|
3358
|
-
existing.status = "completed";
|
|
3359
|
-
existing.completedAt = event.createdAt;
|
|
3360
|
-
existing.durationMs = typeof data.durationMs === "number" ? data.durationMs : null;
|
|
3361
|
-
existing.before = data.before ?? existing.before;
|
|
3362
|
-
existing.after = data.after ?? existing.after;
|
|
3363
|
-
}
|
|
3364
|
-
else {
|
|
3365
|
-
existing.status = "completed";
|
|
3366
|
-
existing.usageObservedAt = event.createdAt;
|
|
3367
|
-
existing.durationMs =
|
|
3368
|
-
typeof data.durationMs === "number" ? data.durationMs : existing.durationMs;
|
|
3369
|
-
existing.before = data.before ?? existing.before;
|
|
3370
|
-
existing.after = data.after ?? existing.after;
|
|
3371
|
-
}
|
|
3372
|
-
records.set(recordId, existing);
|
|
3373
|
-
}
|
|
3374
|
-
return [...records.values()].sort((left, right) => {
|
|
3375
|
-
const leftAt = left.completedAt ?? left.usageObservedAt ?? left.startedAt ?? "";
|
|
3376
|
-
const rightAt = right.completedAt ?? right.usageObservedAt ?? right.startedAt ?? "";
|
|
3377
|
-
return rightAt.localeCompare(leftAt);
|
|
3378
|
-
});
|
|
3379
|
-
}
|
|
3380
|
-
function getTurnAssistantText(turnId, result) {
|
|
3381
|
-
const resultText = result.assistantText.trim();
|
|
3382
|
-
if (resultText) {
|
|
3383
|
-
return resultText;
|
|
3384
|
-
}
|
|
3385
|
-
return extractAssistantTextFromEvents(turnEventBacklog.get(turnId) ?? []);
|
|
3386
|
-
}
|
|
3387
|
-
function extractAssistantTextFromEvents(events) {
|
|
3388
|
-
const phases = new Map();
|
|
3389
|
-
const texts = new Map();
|
|
3390
|
-
let anonymousFinalText = "";
|
|
3391
|
-
let anonymousAssistantText = "";
|
|
3392
|
-
let lastAssistantItemId = null;
|
|
3393
|
-
for (const event of events) {
|
|
3394
|
-
if (event.event !== "codex.notification") {
|
|
3395
|
-
continue;
|
|
3396
|
-
}
|
|
3397
|
-
const notification = readRecord(event.data);
|
|
3398
|
-
const method = notification.method;
|
|
3399
|
-
const params = readRecord(notification.params);
|
|
3400
|
-
if (method === "item/started") {
|
|
3401
|
-
const item = readRecord(params.item);
|
|
3402
|
-
if (item.type === "agentMessage" && typeof item.id === "string") {
|
|
3403
|
-
phases.set(item.id, readAgentMessagePhase(item.phase));
|
|
3404
|
-
texts.set(item.id, "");
|
|
3405
|
-
lastAssistantItemId = item.id;
|
|
3406
|
-
}
|
|
3407
|
-
continue;
|
|
3408
|
-
}
|
|
3409
|
-
if (method === "item/agentMessage/delta") {
|
|
3410
|
-
const delta = params.delta;
|
|
3411
|
-
if (typeof delta === "string") {
|
|
3412
|
-
const itemId = typeof params.itemId === "string" ? params.itemId : lastAssistantItemId;
|
|
3413
|
-
if (itemId) {
|
|
3414
|
-
texts.set(itemId, `${texts.get(itemId) ?? ""}${delta}`);
|
|
3415
|
-
}
|
|
3416
|
-
else if (readAgentMessagePhase(params.phase) === "final_answer") {
|
|
3417
|
-
anonymousFinalText += delta;
|
|
3418
|
-
}
|
|
3419
|
-
else {
|
|
3420
|
-
anonymousAssistantText += delta;
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
continue;
|
|
3424
|
-
}
|
|
3425
|
-
if (method === "item/completed") {
|
|
3426
|
-
const item = readRecord(params.item);
|
|
3427
|
-
if (item.type === "agentMessage" && typeof item.id === "string") {
|
|
3428
|
-
phases.set(item.id, readAgentMessagePhase(item.phase));
|
|
3429
|
-
if (typeof item.text === "string") {
|
|
3430
|
-
texts.set(item.id, item.text);
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
const finalTexts = [...texts.entries()]
|
|
3436
|
-
.filter(([itemId]) => phases.get(itemId) === "final_answer")
|
|
3437
|
-
.map(([, value]) => value)
|
|
3438
|
-
.join("")
|
|
3439
|
-
.trim();
|
|
3440
|
-
if (finalTexts) {
|
|
3441
|
-
return finalTexts;
|
|
3442
|
-
}
|
|
3443
|
-
const latestNonCommentary = [...texts.entries()]
|
|
3444
|
-
.filter(([itemId]) => phases.get(itemId) !== "commentary")
|
|
3445
|
-
.map(([, value]) => value.trim())
|
|
3446
|
-
.filter(Boolean)
|
|
3447
|
-
.at(-1);
|
|
3448
|
-
if (latestNonCommentary) {
|
|
3449
|
-
return latestNonCommentary;
|
|
3450
|
-
}
|
|
3451
|
-
return (anonymousFinalText || anonymousAssistantText).trim();
|
|
3452
|
-
}
|
|
3453
|
-
function readAgentMessagePhase(value) {
|
|
3454
|
-
return value === "commentary" || value === "final_answer" ? value : "unknown";
|
|
3455
|
-
}
|
|
3456
|
-
function extractAgentRequests(content) {
|
|
3457
|
-
const wakeups = [];
|
|
3458
|
-
const responsibilityProposals = [];
|
|
3459
|
-
const knowledgeProposals = [];
|
|
3460
|
-
const transcriptSummaries = [];
|
|
3461
|
-
const controlBlockPattern = /```seele-(wakeup|responsibility-proposal|knowledge-proposal|transcript-summary)\s*([\s\S]*?)```/gi;
|
|
3462
|
-
let cursor = 0;
|
|
3463
|
-
let rawVisibleText = "";
|
|
3464
|
-
for (const match of content.matchAll(controlBlockPattern)) {
|
|
3465
|
-
const index = match.index ?? 0;
|
|
3466
|
-
rawVisibleText += content.slice(cursor, index);
|
|
3467
|
-
const kind = match[1];
|
|
3468
|
-
const rawJson = match[2] ?? "";
|
|
3469
|
-
if (kind === "wakeup") {
|
|
3470
|
-
for (const request of parseWakeupRequestBlock(rawJson)) {
|
|
3471
|
-
wakeups.push(request);
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
else if (kind === "responsibility-proposal") {
|
|
3475
|
-
const proposal = parseResponsibilityProposalBlock(rawJson);
|
|
3476
|
-
if (proposal) {
|
|
3477
|
-
responsibilityProposals.push({
|
|
3478
|
-
...proposal,
|
|
3479
|
-
anchorOffset: rawVisibleText.length,
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
else if (kind === "knowledge-proposal") {
|
|
3484
|
-
const proposal = parseKnowledgeProposalBlock(rawJson);
|
|
3485
|
-
if (proposal) {
|
|
3486
|
-
knowledgeProposals.push({
|
|
3487
|
-
...proposal,
|
|
3488
|
-
anchorOffset: rawVisibleText.length,
|
|
3489
|
-
});
|
|
3490
|
-
}
|
|
3491
|
-
}
|
|
3492
|
-
else {
|
|
3493
|
-
const summary = parseTranscriptSummaryBlock(rawJson);
|
|
3494
|
-
if (summary) {
|
|
3495
|
-
transcriptSummaries.push(summary);
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
cursor = index + match[0].length;
|
|
3499
|
-
}
|
|
3500
|
-
rawVisibleText += content.slice(cursor);
|
|
3501
|
-
const leadingWhitespaceLength = rawVisibleText.match(/^\s*/)?.[0].length ?? 0;
|
|
3502
|
-
const visibleText = rawVisibleText.trim();
|
|
3503
|
-
for (const proposal of responsibilityProposals) {
|
|
3504
|
-
proposal.anchorOffset = Math.max(0, Math.min(visibleText.length, proposal.anchorOffset - leadingWhitespaceLength));
|
|
3505
|
-
}
|
|
3506
|
-
for (const proposal of knowledgeProposals) {
|
|
3507
|
-
proposal.anchorOffset = Math.max(0, Math.min(visibleText.length, proposal.anchorOffset - leadingWhitespaceLength));
|
|
3508
|
-
}
|
|
3509
|
-
return {
|
|
3510
|
-
visibleText,
|
|
3511
|
-
wakeups,
|
|
3512
|
-
responsibilityProposals,
|
|
3513
|
-
knowledgeProposals,
|
|
3514
|
-
transcriptSummaries,
|
|
3515
|
-
};
|
|
3516
|
-
}
|
|
3517
|
-
function parseWakeupRequestBlock(rawJson) {
|
|
3518
|
-
try {
|
|
3519
|
-
const parsed = JSON.parse(rawJson.trim());
|
|
3520
|
-
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
3521
|
-
return items
|
|
3522
|
-
.filter((item) => Boolean(item && typeof item === "object"))
|
|
3523
|
-
.map((item) => ({
|
|
3524
|
-
delaySeconds: typeof item.delaySeconds === "number" ? item.delaySeconds : undefined,
|
|
3525
|
-
instruction: typeof item.instruction === "string" ? item.instruction : undefined,
|
|
3526
|
-
}));
|
|
3527
|
-
}
|
|
3528
|
-
catch {
|
|
3529
|
-
return [
|
|
3530
|
-
{
|
|
3531
|
-
instruction: "",
|
|
3532
|
-
},
|
|
3533
|
-
];
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
function parseResponsibilityProposalBlock(rawJson) {
|
|
3537
|
-
try {
|
|
3538
|
-
const parsed = JSON.parse(rawJson.trim());
|
|
3539
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3540
|
-
return null;
|
|
3541
|
-
}
|
|
3542
|
-
const record = parsed;
|
|
3543
|
-
const responsibility = typeof record.responsibility === "string"
|
|
3544
|
-
? record.responsibility.trim()
|
|
3545
|
-
: "";
|
|
3546
|
-
if (!responsibility) {
|
|
3547
|
-
return null;
|
|
3548
|
-
}
|
|
3549
|
-
return {
|
|
3550
|
-
responsibility,
|
|
3551
|
-
reason: typeof record.reason === "string" && record.reason.trim()
|
|
3552
|
-
? record.reason.trim()
|
|
3553
|
-
: undefined,
|
|
3554
|
-
};
|
|
3555
|
-
}
|
|
3556
|
-
catch {
|
|
3557
|
-
return null;
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
function recordResponsibilityProposals(turnId, proposals) {
|
|
3561
|
-
for (const proposal of proposals) {
|
|
3562
|
-
appendAndBroadcastTurnEvent(turnId, "server.responsibility.proposed", {
|
|
3563
|
-
responsibility: proposal.responsibility,
|
|
3564
|
-
reason: proposal.reason ?? null,
|
|
3565
|
-
anchorOffset: proposal.anchorOffset,
|
|
3566
|
-
status: "pending",
|
|
3567
|
-
});
|
|
3568
|
-
}
|
|
3569
|
-
}
|
|
3570
|
-
function parseKnowledgeProposalBlock(rawJson) {
|
|
3571
|
-
try {
|
|
3572
|
-
const parsed = JSON.parse(rawJson.trim());
|
|
3573
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3574
|
-
return null;
|
|
3575
|
-
}
|
|
3576
|
-
const record = parsed;
|
|
3577
|
-
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
3578
|
-
const content = typeof record.content === "string" ? record.content.trim() : "";
|
|
3579
|
-
const purpose = typeof record.purpose === "string" ? record.purpose.trim() : "";
|
|
3580
|
-
const action = record.action === "update" ? "update" : "create";
|
|
3581
|
-
const knowledgeId = typeof record.knowledgeId === "string" && record.knowledgeId.trim()
|
|
3582
|
-
? record.knowledgeId.trim()
|
|
3583
|
-
: undefined;
|
|
3584
|
-
if (!name || !content || !purpose || (action === "update" && !knowledgeId)) {
|
|
3585
|
-
return null;
|
|
3586
|
-
}
|
|
3587
|
-
const knowledge = {
|
|
3588
|
-
action,
|
|
3589
|
-
name,
|
|
3590
|
-
purpose,
|
|
3591
|
-
content,
|
|
3592
|
-
};
|
|
3593
|
-
if (action === "update") {
|
|
3594
|
-
knowledge.knowledgeId = knowledgeId;
|
|
3595
|
-
}
|
|
3596
|
-
return {
|
|
3597
|
-
action,
|
|
3598
|
-
knowledgeId,
|
|
3599
|
-
knowledge,
|
|
3600
|
-
};
|
|
3601
|
-
}
|
|
3602
|
-
catch {
|
|
3603
|
-
return null;
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
function recordKnowledgeProposals(turnId, proposals) {
|
|
3607
|
-
for (const proposal of proposals) {
|
|
3608
|
-
appendAndBroadcastTurnEvent(turnId, "server.knowledge.proposed", {
|
|
3609
|
-
action: proposal.action,
|
|
3610
|
-
knowledgeId: proposal.knowledgeId ?? null,
|
|
3611
|
-
knowledge: proposal.knowledge,
|
|
3612
|
-
anchorOffset: proposal.anchorOffset,
|
|
3613
|
-
status: "pending",
|
|
3614
|
-
});
|
|
3615
|
-
}
|
|
3616
|
-
}
|
|
3617
|
-
function parseTranscriptSummaryBlock(rawJson) {
|
|
3618
|
-
try {
|
|
3619
|
-
const parsed = JSON.parse(rawJson.trim());
|
|
3620
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3621
|
-
return null;
|
|
3622
|
-
}
|
|
3623
|
-
const record = parsed;
|
|
3624
|
-
const startMessageId = typeof record.startMessageId === "string"
|
|
3625
|
-
? record.startMessageId.trim()
|
|
3626
|
-
: "";
|
|
3627
|
-
const endMessageId = typeof record.endMessageId === "string" ? record.endMessageId.trim() : "";
|
|
3628
|
-
const title = typeof record.title === "string" ? record.title.trim() : "";
|
|
3629
|
-
const summary = typeof record.summary === "string" ? record.summary.trim() : "";
|
|
3630
|
-
if (!isTranscriptMessageId(startMessageId) ||
|
|
3631
|
-
!isTranscriptMessageId(endMessageId) ||
|
|
3632
|
-
!title ||
|
|
3633
|
-
summary.length < 24) {
|
|
3634
|
-
return null;
|
|
3635
|
-
}
|
|
3636
|
-
return {
|
|
3637
|
-
startMessageId,
|
|
3638
|
-
endMessageId,
|
|
3639
|
-
title: title.slice(0, 120),
|
|
3640
|
-
summary,
|
|
3641
|
-
};
|
|
3642
|
-
}
|
|
3643
|
-
catch {
|
|
3644
|
-
return null;
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
function isTranscriptMessageId(value) {
|
|
3648
|
-
return (/^msg_\d{8}_\d{6}_[0-9a-f]{6}$/.test(value) ||
|
|
3649
|
-
/^[0-9a-fA-F-]{8,}$/.test(value));
|
|
3650
|
-
}
|
|
3651
|
-
function recordTranscriptSummaries(conversation, turnId, summaries) {
|
|
3652
|
-
for (const summary of summaries) {
|
|
3653
|
-
const saved = upsertTranscriptRangeSummary(conversation, summary);
|
|
3654
|
-
appendAndBroadcastTurnEvent(turnId, "server.transcript_summary.saved", {
|
|
3655
|
-
id: saved.id,
|
|
3656
|
-
startMessageId: saved.startMessageId,
|
|
3657
|
-
endMessageId: saved.endMessageId,
|
|
3658
|
-
title: saved.title,
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
3661
|
-
}
|
|
3662
|
-
function knowledgeProposalToInput(knowledge) {
|
|
3663
|
-
const content = typeof knowledge.content === "string" ? knowledge.content : knowledge.description;
|
|
3664
|
-
return {
|
|
3665
|
-
name: knowledge.name,
|
|
3666
|
-
description: content,
|
|
3667
|
-
purpose: knowledge.purpose,
|
|
3668
|
-
};
|
|
3669
|
-
}
|
|
3670
|
-
function validateKnowledgeProposalForPersistence(knowledge) {
|
|
3671
|
-
const clues = normalizeKnowledgeProposalSourceClues(knowledge.sourceClues);
|
|
3672
|
-
const hasClues = clues.length > 0;
|
|
3673
|
-
const chatOnly = hasClues &&
|
|
3674
|
-
clues.every((clue) => clue.type.trim().toLowerCase() === "chat_range");
|
|
3675
|
-
if (!chatOnly) {
|
|
3676
|
-
return null;
|
|
3677
|
-
}
|
|
3678
|
-
const description = typeof knowledge.content === "string"
|
|
3679
|
-
? knowledge.content.trim()
|
|
3680
|
-
: typeof knowledge.description === "string"
|
|
3681
|
-
? knowledge.description.trim()
|
|
3682
|
-
: "";
|
|
3683
|
-
const purpose = typeof knowledge.purpose === "string"
|
|
3684
|
-
? knowledge.purpose.trim()
|
|
3685
|
-
: "";
|
|
3686
|
-
const durableText = `${description}\n${purpose}`.trim();
|
|
3687
|
-
if (description.length < 24 || purpose.length < 16 || durableText.length < 80) {
|
|
3688
|
-
return "knowledge_chat_source_too_ephemeral";
|
|
3689
|
-
}
|
|
3690
|
-
return null;
|
|
3691
|
-
}
|
|
3692
|
-
function normalizeKnowledgeProposalSourceClues(sourceClues) {
|
|
3693
|
-
if (!Array.isArray(sourceClues)) {
|
|
3694
|
-
return [];
|
|
3695
|
-
}
|
|
3696
|
-
return sourceClues
|
|
3697
|
-
.map((item) => readRecord(item))
|
|
3698
|
-
.map((item) => normalizeKnowledgeProposalSourceClue(item))
|
|
3699
|
-
.filter((item) => item.value.trim());
|
|
3700
|
-
}
|
|
3701
|
-
function normalizeKnowledgeProposalSourceClue(item) {
|
|
3702
|
-
const type = typeof item.type === "string" ? item.type.trim().toLowerCase() : "other";
|
|
3703
|
-
const value = typeof item.value === "string" ? item.value.trim() : "";
|
|
3704
|
-
if (type === "chat" || type === "chat_range") {
|
|
3705
|
-
return {
|
|
3706
|
-
type: "chat_range",
|
|
3707
|
-
value: normalizeChatRangeSourceValue(value),
|
|
3708
|
-
providedBy: "agent",
|
|
3709
|
-
};
|
|
3710
|
-
}
|
|
3711
|
-
return {
|
|
3712
|
-
type: type || "other",
|
|
3713
|
-
value,
|
|
3714
|
-
providedBy: "agent",
|
|
3715
|
-
};
|
|
3716
|
-
}
|
|
3717
|
-
function normalizeChatRangeSourceValue(value) {
|
|
3718
|
-
const compact = value.trim();
|
|
3719
|
-
const messageIdPattern = "msg_\\d{8}_\\d{6}_[0-9a-f]{6}|[0-9a-fA-F-]{8,}";
|
|
3720
|
-
const rangeMatch = compact.match(new RegExp(`^message:(${messageIdPattern})\\.\\.message:(${messageIdPattern})$`));
|
|
3721
|
-
if (rangeMatch) {
|
|
3722
|
-
return `message:${rangeMatch[1]}..message:${rangeMatch[2]}`;
|
|
3723
|
-
}
|
|
3724
|
-
const singleMatch = compact.match(new RegExp(`^message:(${messageIdPattern})$`));
|
|
3725
|
-
if (singleMatch) {
|
|
3726
|
-
return `message:${singleMatch[1]}..message:${singleMatch[1]}`;
|
|
3727
|
-
}
|
|
3728
|
-
return "chat_range_unspecified";
|
|
3729
|
-
}
|
|
3730
|
-
function scheduleAgentWakeups(conversation, turnId, wakeups) {
|
|
3731
|
-
for (const wakeupRequest of wakeups) {
|
|
3732
|
-
const instruction = typeof wakeupRequest.instruction === "string"
|
|
3733
|
-
? wakeupRequest.instruction.trim()
|
|
3734
|
-
: "";
|
|
3735
|
-
const wakeAt = resolveWakeAt(wakeupRequest);
|
|
3736
|
-
if (!instruction || !wakeAt) {
|
|
3737
|
-
appendEvent(conversation, "server.wakeup.request_failed", {
|
|
3738
|
-
turnId,
|
|
3739
|
-
reason: !instruction ? "instruction_required" : "wake_at_required",
|
|
3740
|
-
}, { turnId });
|
|
3741
|
-
continue;
|
|
3742
|
-
}
|
|
3743
|
-
const wakeup = createWorkspaceWakeup({
|
|
3744
|
-
workspaceId: conversation.id,
|
|
3745
|
-
wakeAt,
|
|
3746
|
-
instruction,
|
|
3747
|
-
createdBy: "agent",
|
|
3748
|
-
createdTurnId: turnId,
|
|
3749
|
-
});
|
|
3750
|
-
appendEvent(conversation, "server.wakeup.scheduled", {
|
|
3751
|
-
wakeupId: wakeup.id,
|
|
3752
|
-
wakeAt: wakeup.wakeAt,
|
|
3753
|
-
instruction: wakeup.instruction,
|
|
3754
|
-
createdBy: "agent",
|
|
3755
|
-
}, { turnId });
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
async function notifyProfuse(turnId, conversation, content) {
|
|
3759
|
-
const controller = new AbortController();
|
|
3760
|
-
const timeout = setTimeout(() => controller.abort(), PROFUSE_NOTIFY_TIMEOUT_MS);
|
|
3761
|
-
try {
|
|
3762
|
-
const response = await fetch(PROFUSE_NOTIFY_URL, {
|
|
3763
|
-
method: "POST",
|
|
3764
|
-
headers: {
|
|
3765
|
-
Authorization: `Bearer ${PROFUSE_AGENT_TOKEN}`,
|
|
3766
|
-
"Content-Type": "application/json",
|
|
3767
|
-
},
|
|
3768
|
-
body: JSON.stringify({
|
|
3769
|
-
platform: PROFUSE_PLATFORM,
|
|
3770
|
-
instance_id: conversation.id,
|
|
3771
|
-
name: conversation.title,
|
|
3772
|
-
content,
|
|
3773
|
-
}),
|
|
3774
|
-
signal: controller.signal,
|
|
3775
|
-
});
|
|
3776
|
-
if (!response.ok) {
|
|
3777
|
-
throw new Error(`Profuse notify failed with HTTP ${response.status}`);
|
|
3778
|
-
}
|
|
3779
|
-
recordNotificationEvent(turnId, conversation, "server.notification.sent", {
|
|
3780
|
-
platform: "profuse",
|
|
3781
|
-
});
|
|
3782
|
-
}
|
|
3783
|
-
catch (error) {
|
|
3784
|
-
recordNotificationEvent(turnId, conversation, "server.notification.failed", {
|
|
3785
|
-
platform: "profuse",
|
|
3786
|
-
message: error instanceof Error ? error.message : String(error),
|
|
3787
|
-
});
|
|
3788
|
-
}
|
|
3789
|
-
finally {
|
|
3790
|
-
clearTimeout(timeout);
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
function recordNotificationEvent(turnId, conversation, event, data) {
|
|
3794
|
-
if (isTurnTerminal(turnId)) {
|
|
3795
|
-
appendEvent(conversation, event, data, { turnId });
|
|
3796
|
-
return;
|
|
3797
|
-
}
|
|
3798
|
-
recordTurnEvent(turnId, event, data);
|
|
3799
|
-
}
|
|
3800
|
-
function broadcastTurnEvent(turnId, event) {
|
|
3801
|
-
for (const subscriber of turnSubscribers.get(turnId) ?? []) {
|
|
3802
|
-
sendBufferedTurnEvent(subscriber, event);
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
function sendBufferedTurnEvent(response, event) {
|
|
3806
|
-
sendSse(response, event.event, {
|
|
3807
|
-
__seq: event.seq,
|
|
3808
|
-
__eventId: event.id,
|
|
3809
|
-
__createdAt: event.createdAt,
|
|
3810
|
-
__turnId: event.turnId,
|
|
3811
|
-
payload: event.data,
|
|
3812
|
-
});
|
|
3813
|
-
}
|
|
3814
|
-
function cleanupTurn(turnId) {
|
|
3815
|
-
locallyStoppedTurns.delete(turnId);
|
|
3816
|
-
staleRuntimeTurns.delete(turnId);
|
|
3817
|
-
longRunningAdvisedTurns.delete(turnId);
|
|
3818
|
-
phaseBoundaryAdvisedTurns.delete(turnId);
|
|
3819
|
-
turnOperationCounts.delete(turnId);
|
|
3820
|
-
latestTurnContextUsage.delete(turnId);
|
|
3821
|
-
for (const key of activeContextCompactions.keys()) {
|
|
3822
|
-
if (key.startsWith(`${turnId}:`)) {
|
|
3823
|
-
activeContextCompactions.delete(key);
|
|
3824
|
-
}
|
|
3825
|
-
}
|
|
3826
|
-
for (const key of completedContextCompactionsAwaitingUsage.keys()) {
|
|
3827
|
-
if (key.startsWith(`${turnId}:`)) {
|
|
3828
|
-
completedContextCompactionsAwaitingUsage.delete(key);
|
|
3829
|
-
}
|
|
3830
|
-
}
|
|
3831
|
-
const conversationId = turnConversations.get(turnId);
|
|
3832
|
-
if (conversationId && activeConversationTurns.get(conversationId) === turnId) {
|
|
3833
|
-
activeConversationTurns.delete(conversationId);
|
|
3834
|
-
queueMicrotask(() => startNextQueuedTurn(conversationId));
|
|
3835
|
-
}
|
|
3836
|
-
const subscribers = turnSubscribers.get(turnId);
|
|
3837
|
-
if (subscribers) {
|
|
3838
|
-
for (const subscriber of subscribers) {
|
|
3839
|
-
if (!subscriber.writableEnded) {
|
|
3840
|
-
subscriber.end();
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
turnSubscribers.delete(turnId);
|
|
3844
|
-
}
|
|
3845
|
-
setTimeout(() => {
|
|
3846
|
-
turnEventBacklog.delete(turnId);
|
|
3847
|
-
turnTerminalEvents.delete(turnId);
|
|
3848
|
-
turnConversations.delete(turnId);
|
|
3849
|
-
turnWakeups.delete(turnId);
|
|
3850
|
-
}, 60_000).unref?.();
|
|
3851
|
-
}
|
|
3852
|
-
function startNextQueuedTurn(conversationId) {
|
|
3853
|
-
if (activeConversationTurns.has(conversationId)) {
|
|
3854
|
-
return;
|
|
3855
|
-
}
|
|
3856
|
-
const conversation = getConversation(conversationId);
|
|
3857
|
-
if (!conversation) {
|
|
3858
|
-
workspaceTurnQueues.delete(conversationId);
|
|
3859
|
-
return;
|
|
3860
|
-
}
|
|
3861
|
-
const queue = workspaceTurnQueues.get(conversationId) ?? [];
|
|
3862
|
-
const next = queue.shift();
|
|
3863
|
-
if (!next) {
|
|
3864
|
-
workspaceTurnQueues.delete(conversationId);
|
|
3865
|
-
return;
|
|
3866
|
-
}
|
|
3867
|
-
if (queue.length > 0) {
|
|
3868
|
-
workspaceTurnQueues.set(conversationId, queue);
|
|
3869
|
-
}
|
|
3870
|
-
else {
|
|
3871
|
-
workspaceTurnQueues.delete(conversationId);
|
|
3872
|
-
}
|
|
3873
|
-
try {
|
|
3874
|
-
startQueuedConversationTurn(conversation, next);
|
|
3875
|
-
}
|
|
3876
|
-
catch (error) {
|
|
3877
|
-
appendEvent(conversation, "server.turn.queue_start_failed", {
|
|
3878
|
-
turnId: next.turnId,
|
|
3879
|
-
message: error instanceof Error ? error.message : String(error),
|
|
3880
|
-
}, { turnId: next.turnId });
|
|
3881
|
-
queueMicrotask(() => startNextQueuedTurn(conversationId));
|
|
3882
|
-
}
|
|
3883
|
-
}
|
|
3884
|
-
function getConversationForTurn(turnId) {
|
|
3885
|
-
const conversationId = turnConversations.get(turnId);
|
|
3886
|
-
return conversationId ? getConversation(conversationId) : null;
|
|
3887
|
-
}
|
|
3888
|
-
function withRuntimeSettings(input) {
|
|
3889
|
-
const globalRuntime = getGlobalRuntimeSettings();
|
|
3890
|
-
return {
|
|
3891
|
-
...input,
|
|
3892
|
-
defaultModel: globalRuntime.model || input.defaultModel,
|
|
3893
|
-
defaultReasoningEffort: globalRuntime.reasoningEffort,
|
|
3894
|
-
defaultFastMode: globalRuntime.fastMode,
|
|
3895
|
-
defaultSandbox: globalRuntime.sandbox,
|
|
3896
|
-
globalEnvironment: getGlobalEnvironmentMap(),
|
|
3897
|
-
globalEnvironmentDescriptions: getGlobalEnvironmentDescriptions(),
|
|
3898
|
-
globalMemory: getGlobalMemory(),
|
|
3899
|
-
};
|
|
3900
|
-
}
|
|
3901
|
-
function withWorkspaceRuntimeSettings(input, conversation) {
|
|
3902
|
-
const effective = getWorkspaceRuntimeSettings(conversation).effective;
|
|
3903
|
-
const globalEnvironment = getGlobalEnvironmentMap();
|
|
3904
|
-
const workspaceEnvironment = getConversationEnvironmentMap(conversation);
|
|
3905
|
-
const globalEnvironmentDescriptions = getGlobalEnvironmentDescriptions();
|
|
3906
|
-
const workspaceEnvironmentDescriptions = getConversationEnvironmentDescriptions(conversation);
|
|
3907
|
-
return {
|
|
3908
|
-
...withRuntimeSettings(input),
|
|
3909
|
-
defaultModel: effective.model || input.defaultModel,
|
|
3910
|
-
defaultReasoningEffort: effective.reasoningEffort,
|
|
3911
|
-
defaultFastMode: effective.fastMode,
|
|
3912
|
-
defaultSandbox: effective.sandbox,
|
|
3913
|
-
globalEnvironment: {
|
|
3914
|
-
...globalEnvironment,
|
|
3915
|
-
...workspaceEnvironment,
|
|
3916
|
-
},
|
|
3917
|
-
globalEnvironmentDescriptions: {
|
|
3918
|
-
...globalEnvironmentDescriptions,
|
|
3919
|
-
...workspaceEnvironmentDescriptions,
|
|
3920
|
-
},
|
|
3921
|
-
};
|
|
3922
|
-
}
|
|
3923
|
-
function getGlobalMemory() {
|
|
3924
|
-
return formatMemoryItems(getGlobalMemoryItems());
|
|
3925
|
-
}
|
|
3926
|
-
function getGlobalMemoryItems() {
|
|
3927
|
-
return parseMemoryItems(getAppSettingValue(GLOBAL_MEMORY_ITEMS_SETTING_KEY));
|
|
3928
|
-
}
|
|
3929
|
-
function describeRuntimeEnvironment(runtimeConfig) {
|
|
3930
|
-
const descriptions = runtimeConfig.globalEnvironmentDescriptions ?? {};
|
|
3931
|
-
return Object.keys(runtimeConfig.globalEnvironment ?? {}).map((key) => ({
|
|
3932
|
-
key,
|
|
3933
|
-
description: descriptions[key] ?? "",
|
|
3934
|
-
}));
|
|
3935
|
-
}
|
|
3936
|
-
function normalizeMemoryRequestItems(value) {
|
|
3937
|
-
if (!Array.isArray(value)) {
|
|
3938
|
-
return [];
|
|
3939
|
-
}
|
|
3940
|
-
const now = new Date().toISOString();
|
|
3941
|
-
return normalizeMemoryItems(value.map((item) => {
|
|
3942
|
-
const record = item && typeof item === "object" ? item : {};
|
|
3943
|
-
return {
|
|
3944
|
-
key: "key" in record && typeof record.key === "string"
|
|
3945
|
-
? record.key
|
|
3946
|
-
: "",
|
|
3947
|
-
value: "value" in record && typeof record.value === "string"
|
|
3948
|
-
? record.value
|
|
3949
|
-
: "",
|
|
3950
|
-
updatedAt: "updatedAt" in record && typeof record.updatedAt === "string"
|
|
3951
|
-
? record.updatedAt
|
|
3952
|
-
: now,
|
|
3953
|
-
};
|
|
3954
|
-
}));
|
|
3955
|
-
}
|
|
3956
|
-
function validateMemoryRequestItems(value) {
|
|
3957
|
-
const keys = new Set();
|
|
3958
|
-
for (const [index, item] of value.entries()) {
|
|
3959
|
-
const record = item && typeof item === "object" ? item : {};
|
|
3960
|
-
const key = "key" in record && typeof record.key === "string" ? record.key.trim() : "";
|
|
3961
|
-
const itemValue = "value" in record && typeof record.value === "string" ? record.value : "";
|
|
3962
|
-
if (!isValidMemoryRequestKey(key)) {
|
|
3963
|
-
return {
|
|
3964
|
-
error: "invalid_memory_key",
|
|
3965
|
-
detail: `Memory item ${index + 1} has an invalid key. ` +
|
|
3966
|
-
"Use lowercase letters, numbers, dots, underscores, and hyphens; start with a lowercase letter or underscore.",
|
|
3967
|
-
};
|
|
3968
|
-
}
|
|
3969
|
-
if (!itemValue.trim()) {
|
|
3970
|
-
return {
|
|
3971
|
-
error: "invalid_memory_value",
|
|
3972
|
-
detail: `Memory item ${index + 1} has an empty value.`,
|
|
3973
|
-
};
|
|
3974
|
-
}
|
|
3975
|
-
if (keys.has(key)) {
|
|
3976
|
-
return {
|
|
3977
|
-
error: "duplicate_memory_key",
|
|
3978
|
-
detail: `Memory key "${key}" is duplicated.`,
|
|
3979
|
-
};
|
|
3980
|
-
}
|
|
3981
|
-
keys.add(key);
|
|
3982
|
-
}
|
|
3983
|
-
return null;
|
|
3984
|
-
}
|
|
3985
|
-
function isValidMemoryRequestKey(key) {
|
|
3986
|
-
return /^[a-z_][a-z0-9_.-]*$/.test(key);
|
|
3987
|
-
}
|
|
3988
|
-
function getRuntimeSettings() {
|
|
3989
|
-
return getGlobalRuntimeSettings();
|
|
3990
|
-
}
|
|
3991
|
-
function getGlobalRuntimeSettings() {
|
|
3992
|
-
return {
|
|
3993
|
-
model: getRuntimeModel(config) ?? "",
|
|
3994
|
-
reasoningEffort: getRuntimeReasoningEffort(config),
|
|
3995
|
-
fastMode: getRuntimeFastMode(config),
|
|
3996
|
-
sandbox: getRuntimeSandbox(config),
|
|
3997
|
-
notifyEnabled: getRuntimeNotifyEnabled(),
|
|
3998
|
-
};
|
|
3999
|
-
}
|
|
4000
|
-
function getWorkspaceRuntimeSettings(conversation) {
|
|
4001
|
-
const global = getGlobalRuntimeSettings();
|
|
4002
|
-
const settings = getConversationWorkspaceSettings(conversation);
|
|
4003
|
-
const overrides = {};
|
|
4004
|
-
const sources = {
|
|
4005
|
-
model: "global",
|
|
4006
|
-
reasoningEffort: "global",
|
|
4007
|
-
fastMode: "global",
|
|
4008
|
-
sandbox: "global",
|
|
4009
|
-
notifyEnabled: "global",
|
|
4010
|
-
};
|
|
4011
|
-
if (Object.prototype.hasOwnProperty.call(settings, RUNTIME_MODEL_SETTING_KEY)) {
|
|
4012
|
-
overrides.model = settings[RUNTIME_MODEL_SETTING_KEY] ?? "";
|
|
4013
|
-
sources.model = "workspace";
|
|
4014
|
-
}
|
|
4015
|
-
if (Object.prototype.hasOwnProperty.call(settings, RUNTIME_REASONING_EFFORT_SETTING_KEY)) {
|
|
4016
|
-
overrides.reasoningEffort = normalizeRuntimeReasoningEffort(settings[RUNTIME_REASONING_EFFORT_SETTING_KEY]);
|
|
4017
|
-
sources.reasoningEffort = "workspace";
|
|
4018
|
-
}
|
|
4019
|
-
if (Object.prototype.hasOwnProperty.call(settings, RUNTIME_FAST_MODE_SETTING_KEY)) {
|
|
4020
|
-
overrides.fastMode = settings[RUNTIME_FAST_MODE_SETTING_KEY] === "true";
|
|
4021
|
-
sources.fastMode = "workspace";
|
|
4022
|
-
}
|
|
4023
|
-
if (Object.prototype.hasOwnProperty.call(settings, RUNTIME_SANDBOX_SETTING_KEY)) {
|
|
4024
|
-
const sandbox = settings[RUNTIME_SANDBOX_SETTING_KEY];
|
|
4025
|
-
if (isConversationSandbox(sandbox)) {
|
|
4026
|
-
overrides.sandbox = sandbox;
|
|
4027
|
-
sources.sandbox = "workspace";
|
|
4028
|
-
}
|
|
4029
|
-
}
|
|
4030
|
-
if (Object.prototype.hasOwnProperty.call(settings, RUNTIME_NOTIFY_ENABLED_SETTING_KEY)) {
|
|
4031
|
-
overrides.notifyEnabled =
|
|
4032
|
-
settings[RUNTIME_NOTIFY_ENABLED_SETTING_KEY] !== "false";
|
|
4033
|
-
sources.notifyEnabled = "workspace";
|
|
4034
|
-
}
|
|
4035
|
-
return {
|
|
4036
|
-
global,
|
|
4037
|
-
overrides,
|
|
4038
|
-
effective: {
|
|
4039
|
-
...global,
|
|
4040
|
-
...overrides,
|
|
4041
|
-
},
|
|
4042
|
-
sources,
|
|
4043
|
-
};
|
|
4044
|
-
}
|
|
4045
|
-
function applyWorkspaceRuntimeSettingUpdate(conversation, body) {
|
|
4046
|
-
const updateStringOverride = (property, key, normalize) => {
|
|
4047
|
-
if (!Object.prototype.hasOwnProperty.call(body, property)) {
|
|
4048
|
-
return;
|
|
4049
|
-
}
|
|
4050
|
-
if (body[property] === null) {
|
|
4051
|
-
deleteConversationWorkspaceSetting(conversation, key);
|
|
4052
|
-
return;
|
|
4053
|
-
}
|
|
4054
|
-
setConversationWorkspaceSetting(conversation, key, normalize(body[property]));
|
|
4055
|
-
};
|
|
4056
|
-
updateStringOverride("model", RUNTIME_MODEL_SETTING_KEY, (value) => typeof value === "string" ? value.trim() : "");
|
|
4057
|
-
updateStringOverride("reasoningEffort", RUNTIME_REASONING_EFFORT_SETTING_KEY, normalizeRuntimeReasoningEffort);
|
|
4058
|
-
if (Object.prototype.hasOwnProperty.call(body, "fastMode")) {
|
|
4059
|
-
if (body.fastMode === null) {
|
|
4060
|
-
deleteConversationWorkspaceSetting(conversation, RUNTIME_FAST_MODE_SETTING_KEY);
|
|
4061
|
-
}
|
|
4062
|
-
else {
|
|
4063
|
-
setConversationWorkspaceSetting(conversation, RUNTIME_FAST_MODE_SETTING_KEY, body.fastMode === true ? "true" : "false");
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
if (Object.prototype.hasOwnProperty.call(body, "sandbox")) {
|
|
4067
|
-
if (body.sandbox === null) {
|
|
4068
|
-
deleteConversationWorkspaceSetting(conversation, RUNTIME_SANDBOX_SETTING_KEY);
|
|
4069
|
-
setConversationSandbox(conversation, getGlobalRuntimeSettings().sandbox);
|
|
4070
|
-
}
|
|
4071
|
-
else if (isConversationSandbox(body.sandbox)) {
|
|
4072
|
-
setConversationWorkspaceSetting(conversation, RUNTIME_SANDBOX_SETTING_KEY, body.sandbox);
|
|
4073
|
-
setConversationSandbox(conversation, body.sandbox);
|
|
4074
|
-
}
|
|
4075
|
-
else {
|
|
4076
|
-
throw new Error("invalid sandbox");
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
|
-
if (Object.prototype.hasOwnProperty.call(body, "notifyEnabled")) {
|
|
4080
|
-
if (body.notifyEnabled === null) {
|
|
4081
|
-
deleteConversationWorkspaceSetting(conversation, RUNTIME_NOTIFY_ENABLED_SETTING_KEY);
|
|
4082
|
-
}
|
|
4083
|
-
else {
|
|
4084
|
-
setConversationWorkspaceSetting(conversation, RUNTIME_NOTIFY_ENABLED_SETTING_KEY, body.notifyEnabled === false ? "false" : "true");
|
|
4085
|
-
}
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4088
|
-
function getRuntimeModel(input) {
|
|
4089
|
-
return getAppSettingValue(RUNTIME_MODEL_SETTING_KEY)?.trim() || input.defaultModel;
|
|
4090
|
-
}
|
|
4091
|
-
function getRuntimeReasoningEffort(input) {
|
|
4092
|
-
return normalizeRuntimeReasoningEffort(getAppSettingValue(RUNTIME_REASONING_EFFORT_SETTING_KEY) ??
|
|
4093
|
-
input.defaultReasoningEffort);
|
|
4094
|
-
}
|
|
4095
|
-
function getRuntimeFastMode(input) {
|
|
4096
|
-
const stored = getAppSettingValue(RUNTIME_FAST_MODE_SETTING_KEY);
|
|
4097
|
-
if (stored === "true") {
|
|
4098
|
-
return true;
|
|
4099
|
-
}
|
|
4100
|
-
if (stored === "false") {
|
|
4101
|
-
return false;
|
|
4102
|
-
}
|
|
4103
|
-
return input.defaultFastMode === true;
|
|
4104
|
-
}
|
|
4105
|
-
function getRuntimeSandbox(input) {
|
|
4106
|
-
const stored = getAppSettingValue(RUNTIME_SANDBOX_SETTING_KEY);
|
|
4107
|
-
return isConversationSandbox(stored) ? stored : input.defaultSandbox;
|
|
4108
|
-
}
|
|
4109
|
-
function getRuntimeNotifyEnabled() {
|
|
4110
|
-
return getAppSettingValue(RUNTIME_NOTIFY_ENABLED_SETTING_KEY) !== "false";
|
|
4111
|
-
}
|
|
4112
|
-
function normalizeRuntimeReasoningEffort(value) {
|
|
4113
|
-
if (value === "minimal" ||
|
|
4114
|
-
value === "low" ||
|
|
4115
|
-
value === "medium" ||
|
|
4116
|
-
value === "high" ||
|
|
4117
|
-
value === "xhigh") {
|
|
4118
|
-
return value;
|
|
4119
|
-
}
|
|
4120
|
-
return "medium";
|
|
4121
|
-
}
|
|
4122
|
-
function isValidEnvironmentVariableKey(key) {
|
|
4123
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
4124
|
-
}
|
|
4125
|
-
async function readJsonBody(request) {
|
|
4126
|
-
const chunks = [];
|
|
4127
|
-
for await (const chunk of request) {
|
|
4128
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
4129
|
-
}
|
|
4130
|
-
if (chunks.length === 0) {
|
|
4131
|
-
return {};
|
|
4132
|
-
}
|
|
4133
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
4134
|
-
}
|
|
4135
|
-
function setCorsHeaders(response) {
|
|
4136
|
-
response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
|
4137
|
-
response.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
|
|
4138
|
-
}
|
|
4139
|
-
function sendJson(response, statusCode, data) {
|
|
4140
|
-
response.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
4141
|
-
response.end(JSON.stringify(data));
|
|
4142
|
-
}
|
|
4143
|
-
function getAgentTranscriptApiBase(conversation) {
|
|
4144
|
-
const host = config.host === "0.0.0.0" || config.host === "::"
|
|
4145
|
-
? "127.0.0.1"
|
|
4146
|
-
: config.host;
|
|
4147
|
-
return `http://${host}:${config.port}/agent/workspaces/${encodeURIComponent(conversation.id)}/transcript`;
|
|
4148
|
-
}
|
|
4149
|
-
function getAgentKnowledgeApiBase(conversation) {
|
|
4150
|
-
const host = config.host === "0.0.0.0" || config.host === "::"
|
|
4151
|
-
? "127.0.0.1"
|
|
4152
|
-
: config.host;
|
|
4153
|
-
return `http://${host}:${config.port}/agent/workspaces/${encodeURIComponent(conversation.id)}/knowledge`;
|
|
4154
|
-
}
|
|
4155
|
-
function readPositiveInteger(value) {
|
|
4156
|
-
if (typeof value !== "string" && typeof value !== "number") {
|
|
4157
|
-
return undefined;
|
|
4158
|
-
}
|
|
4159
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(value, 10);
|
|
4160
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
4161
|
-
}
|
|
4162
|
-
function resolveWakeAt(body) {
|
|
4163
|
-
if (typeof body.delaySeconds === "number" &&
|
|
4164
|
-
Number.isFinite(body.delaySeconds)) {
|
|
4165
|
-
const delaySeconds = Math.max(1, Math.floor(body.delaySeconds));
|
|
4166
|
-
return new Date(Date.now() + delaySeconds * 1000).toISOString();
|
|
4167
|
-
}
|
|
4168
|
-
return null;
|
|
4169
|
-
}
|
|
4170
|
-
async function getAppMeta() {
|
|
4171
|
-
if (cachedAppMeta) {
|
|
4172
|
-
return cachedAppMeta;
|
|
4173
|
-
}
|
|
4174
|
-
const packageJsonPath = path.join(config.defaultCwd ?? process.cwd(), "package.json");
|
|
4175
|
-
try {
|
|
4176
|
-
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
4177
|
-
const data = JSON.parse(raw);
|
|
4178
|
-
cachedAppMeta = {
|
|
4179
|
-
version: typeof data.version === "string" ? data.version : "0.0.0",
|
|
4180
|
-
};
|
|
4181
|
-
}
|
|
4182
|
-
catch {
|
|
4183
|
-
cachedAppMeta = { version: "0.0.0" };
|
|
4184
|
-
}
|
|
4185
|
-
return cachedAppMeta;
|
|
4186
|
-
}
|
|
4187
|
-
async function buildHealthSnapshot() {
|
|
4188
|
-
const diagnostics = await buildDiagnosticsSnapshot();
|
|
4189
|
-
const store = diagnostics.store;
|
|
4190
|
-
const checks = {
|
|
4191
|
-
centerDb: {
|
|
4192
|
-
ok: store.centerSchemaVersion === store.expectedCenterSchemaVersion,
|
|
4193
|
-
detail: {
|
|
4194
|
-
schemaVersion: store.centerSchemaVersion,
|
|
4195
|
-
expected: store.expectedCenterSchemaVersion,
|
|
4196
|
-
},
|
|
4197
|
-
},
|
|
4198
|
-
workspaceSchemas: {
|
|
4199
|
-
ok: store.workspaceDatabases.every((workspace) => workspace.schemaVersion === store.expectedWorkspaceSchemaVersion),
|
|
4200
|
-
detail: {
|
|
4201
|
-
expected: store.expectedWorkspaceSchemaVersion,
|
|
4202
|
-
workspaces: store.workspaceDatabases.map((workspace) => ({
|
|
4203
|
-
id: workspace.id,
|
|
4204
|
-
schemaVersion: workspace.schemaVersion,
|
|
4205
|
-
})),
|
|
4206
|
-
},
|
|
4207
|
-
},
|
|
4208
|
-
workspaceRoot: diagnostics.paths.workspaceRoot,
|
|
4209
|
-
seeleRoot: diagnostics.paths.seeleRoot,
|
|
4210
|
-
runtimeWorkers: {
|
|
4211
|
-
ok: diagnostics.runtime.staleWorkerCount === 0 &&
|
|
4212
|
-
diagnostics.runtime.orphanedActiveTurnCount === 0,
|
|
4213
|
-
detail: {
|
|
4214
|
-
workerCount: diagnostics.runtime.workerCount,
|
|
4215
|
-
staleWorkerCount: diagnostics.runtime.staleWorkerCount,
|
|
4216
|
-
activeTurnCount: diagnostics.runtime.activeTurnCount,
|
|
4217
|
-
orphanedActiveTurnCount: diagnostics.runtime.orphanedActiveTurnCount,
|
|
4218
|
-
},
|
|
4219
|
-
},
|
|
4220
|
-
};
|
|
4221
|
-
return {
|
|
4222
|
-
ok: Object.values(checks).every((check) => check.ok),
|
|
4223
|
-
checks,
|
|
4224
|
-
};
|
|
4225
|
-
}
|
|
4226
|
-
async function buildDiagnosticsSnapshot() {
|
|
4227
|
-
const runtimeDiagnostics = runtime.getDiagnostics();
|
|
4228
|
-
const effectiveRuntimeConfig = withRuntimeSettings(config);
|
|
4229
|
-
const activeTurns = [...activeConversationTurns.entries()]
|
|
4230
|
-
.map(([conversationId, turnId]) => {
|
|
4231
|
-
const worker = runtimeDiagnostics.workers.find((candidate) => candidate.turnId === turnId);
|
|
4232
|
-
return {
|
|
4233
|
-
conversationId,
|
|
4234
|
-
turnId,
|
|
4235
|
-
backlogSize: turnEventBacklog.get(turnId)?.length ?? 0,
|
|
4236
|
-
subscriberCount: turnSubscribers.get(turnId)?.size ?? 0,
|
|
4237
|
-
terminal: turnTerminalEvents.has(turnId),
|
|
4238
|
-
worker: worker
|
|
4239
|
-
? {
|
|
4240
|
-
state: worker.state,
|
|
4241
|
-
pid: worker.pid,
|
|
4242
|
-
uptimeMs: worker.uptimeMs,
|
|
4243
|
-
lastEventAt: worker.lastEventAt,
|
|
4244
|
-
lastHeartbeatAt: worker.lastHeartbeatAt,
|
|
4245
|
-
stale: worker.stale,
|
|
4246
|
-
}
|
|
4247
|
-
: null,
|
|
4248
|
-
};
|
|
4249
|
-
})
|
|
4250
|
-
.sort((left, right) => left.conversationId.localeCompare(right.conversationId));
|
|
4251
|
-
const queuedTurns = [...workspaceTurnQueues.entries()]
|
|
4252
|
-
.map(([conversationId, queue]) => ({
|
|
4253
|
-
conversationId,
|
|
4254
|
-
turnIds: queue.map((item) => item.turnId),
|
|
4255
|
-
queueLength: queue.length,
|
|
4256
|
-
}))
|
|
4257
|
-
.sort((left, right) => left.conversationId.localeCompare(right.conversationId));
|
|
4258
|
-
return {
|
|
4259
|
-
app: await getAppMeta(),
|
|
4260
|
-
config: {
|
|
4261
|
-
host: config.host,
|
|
4262
|
-
port: config.port,
|
|
4263
|
-
defaultSandbox: config.defaultSandbox,
|
|
4264
|
-
defaultModel: effectiveRuntimeConfig.defaultModel ?? null,
|
|
4265
|
-
defaultReasoningEffort: effectiveRuntimeConfig.defaultReasoningEffort ?? "medium",
|
|
4266
|
-
fastMode: effectiveRuntimeConfig.defaultFastMode === true,
|
|
4267
|
-
codexBin: config.codexBin ?? null,
|
|
4268
|
-
},
|
|
4269
|
-
paths: {
|
|
4270
|
-
seeleRoot: await checkPathHealth(config.seeleRoot),
|
|
4271
|
-
workspaceRoot: await checkPathHealth(config.workspaceRoot),
|
|
4272
|
-
centerDbPath: config.centerDbPath,
|
|
4273
|
-
webDistPath: await checkPathHealth(config.webDistPath, { requireWrite: false }),
|
|
4274
|
-
},
|
|
4275
|
-
store: getConversationStoreDiagnostics(),
|
|
4276
|
-
runtime: {
|
|
4277
|
-
...runtimeDiagnostics,
|
|
4278
|
-
activeTurnCount: activeConversationTurns.size,
|
|
4279
|
-
activeConversationIds: [...activeConversationTurns.keys()].sort(),
|
|
4280
|
-
activeTurns,
|
|
4281
|
-
queuedTurns,
|
|
4282
|
-
queuedTurnCount: queuedTurns.reduce((total, item) => total + item.queueLength, 0),
|
|
4283
|
-
orphanedActiveTurnCount: activeTurns.filter((turn) => !turn.worker).length,
|
|
4284
|
-
backloggedTurnCount: turnEventBacklog.size,
|
|
4285
|
-
subscriberTurnCount: turnSubscribers.size,
|
|
4286
|
-
terminalTurnCount: turnTerminalEvents.size,
|
|
4287
|
-
locallyStoppedTurnCount: locallyStoppedTurns.size,
|
|
4288
|
-
staleNotifiedTurnCount: staleRuntimeTurns.size,
|
|
4289
|
-
},
|
|
4290
|
-
startupRecovery: {
|
|
4291
|
-
turns: startupRecovery,
|
|
4292
|
-
wakeups: wakeupStartupRecovery,
|
|
4293
|
-
},
|
|
4294
|
-
};
|
|
4295
|
-
}
|
|
4296
|
-
async function checkPathHealth(targetPath, options = {}) {
|
|
4297
|
-
const requireWrite = options.requireWrite ?? true;
|
|
4298
|
-
let readable = false;
|
|
4299
|
-
let writable = requireWrite ? false : null;
|
|
4300
|
-
let error = null;
|
|
4301
|
-
try {
|
|
4302
|
-
await fs.access(targetPath, fsConstants.R_OK);
|
|
4303
|
-
readable = true;
|
|
4304
|
-
}
|
|
4305
|
-
catch (accessError) {
|
|
4306
|
-
error = accessError instanceof Error ? accessError.message : String(accessError);
|
|
4307
|
-
}
|
|
4308
|
-
if (requireWrite) {
|
|
4309
|
-
try {
|
|
4310
|
-
await fs.access(targetPath, fsConstants.W_OK);
|
|
4311
|
-
writable = true;
|
|
4312
|
-
}
|
|
4313
|
-
catch (accessError) {
|
|
4314
|
-
error = accessError instanceof Error ? accessError.message : String(accessError);
|
|
4315
|
-
}
|
|
4316
|
-
}
|
|
4317
|
-
return {
|
|
4318
|
-
ok: readable && (writable ?? true),
|
|
4319
|
-
path: targetPath,
|
|
4320
|
-
readable,
|
|
4321
|
-
writable,
|
|
4322
|
-
error,
|
|
4323
|
-
};
|
|
4324
|
-
}
|
|
4325
|
-
async function serveWebAsset(request, response, pathname) {
|
|
4326
|
-
if (pathname.startsWith("/api/")) {
|
|
4327
|
-
return false;
|
|
4328
|
-
}
|
|
4329
|
-
const requestedPath = pathname === "/" ? "/index.html" : pathname;
|
|
4330
|
-
const relativePath = decodeURIComponent(requestedPath).replace(/^\/+/, "");
|
|
4331
|
-
const webRoot = path.resolve(config.webDistPath);
|
|
4332
|
-
const assetPath = path.resolve(webRoot, relativePath);
|
|
4333
|
-
if (!assetPath.startsWith(`${webRoot}${path.sep}`) && assetPath !== webRoot) {
|
|
4334
|
-
sendJson(response, 403, { error: "forbidden" });
|
|
4335
|
-
return true;
|
|
4336
|
-
}
|
|
4337
|
-
const filePath = await resolveStaticFile(assetPath, webRoot);
|
|
4338
|
-
if (!filePath) {
|
|
4339
|
-
return false;
|
|
4340
|
-
}
|
|
4341
|
-
const stat = await fs.stat(filePath);
|
|
4342
|
-
if (path.basename(filePath) === "index.html") {
|
|
4343
|
-
const html = injectRuntimeWebConfig(await fs.readFile(filePath, "utf8"));
|
|
4344
|
-
response.writeHead(200, {
|
|
4345
|
-
"Cache-Control": "no-store",
|
|
4346
|
-
"Content-Length": Buffer.byteLength(html),
|
|
4347
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
4348
|
-
});
|
|
4349
|
-
response.end(request.method === "HEAD" ? undefined : html);
|
|
4350
|
-
return true;
|
|
4351
|
-
}
|
|
4352
|
-
response.writeHead(200, {
|
|
4353
|
-
"Content-Length": stat.size,
|
|
4354
|
-
"Content-Type": contentTypeFor(filePath),
|
|
4355
|
-
});
|
|
4356
|
-
if (request.method === "HEAD") {
|
|
4357
|
-
response.end();
|
|
4358
|
-
return true;
|
|
4359
|
-
}
|
|
4360
|
-
createReadStream(filePath).pipe(response);
|
|
4361
|
-
return true;
|
|
4362
|
-
}
|
|
4363
|
-
function injectRuntimeWebConfig(html) {
|
|
4364
|
-
const basePath = escapeHtmlAttribute(config.publicBasePath);
|
|
4365
|
-
const runtimeHead = [
|
|
4366
|
-
`<base href="${basePath}" />`,
|
|
4367
|
-
`<meta name="seele-public-base-path" content="${basePath}" />`,
|
|
4368
|
-
].join("\n ");
|
|
4369
|
-
return html.includes("<head>")
|
|
4370
|
-
? html.replace("<head>", `<head>\n ${runtimeHead}`)
|
|
4371
|
-
: `${runtimeHead}\n${html}`;
|
|
4372
|
-
}
|
|
4373
|
-
function escapeHtmlAttribute(value) {
|
|
4374
|
-
return value
|
|
4375
|
-
.replaceAll("&", "&")
|
|
4376
|
-
.replaceAll('"', """)
|
|
4377
|
-
.replaceAll("<", "<")
|
|
4378
|
-
.replaceAll(">", ">");
|
|
4379
|
-
}
|
|
4380
|
-
async function serveConversationFile(request, response, workspacePath, requestedFilePath) {
|
|
4381
|
-
const workspaceRoot = path.resolve(workspacePath);
|
|
4382
|
-
const filePath = path.isAbsolute(requestedFilePath)
|
|
4383
|
-
? path.resolve(requestedFilePath)
|
|
4384
|
-
: path.resolve(workspaceRoot, requestedFilePath);
|
|
4385
|
-
if (!isInsidePath(filePath, workspaceRoot)) {
|
|
4386
|
-
sendJson(response, 403, { error: "forbidden" });
|
|
4387
|
-
return;
|
|
4388
|
-
}
|
|
4389
|
-
const contentType = contentTypeFor(filePath);
|
|
4390
|
-
if (!isServableConversationFileContentType(contentType)) {
|
|
4391
|
-
sendJson(response, 415, { error: "unsupported_file_type" });
|
|
4392
|
-
return;
|
|
4393
|
-
}
|
|
4394
|
-
let stat;
|
|
4395
|
-
try {
|
|
4396
|
-
stat = await fs.stat(filePath);
|
|
4397
|
-
}
|
|
4398
|
-
catch {
|
|
4399
|
-
sendJson(response, 404, { error: "file_not_found" });
|
|
4400
|
-
return;
|
|
4401
|
-
}
|
|
4402
|
-
if (!stat.isFile()) {
|
|
4403
|
-
sendJson(response, 404, { error: "file_not_found" });
|
|
4404
|
-
return;
|
|
4405
|
-
}
|
|
4406
|
-
response.writeHead(200, {
|
|
4407
|
-
"Cache-Control": "no-store",
|
|
4408
|
-
"Content-Length": stat.size,
|
|
4409
|
-
"Content-Type": contentType,
|
|
4410
|
-
});
|
|
4411
|
-
if (request.method === "HEAD") {
|
|
4412
|
-
response.end();
|
|
4413
|
-
return;
|
|
4414
|
-
}
|
|
4415
|
-
createReadStream(filePath).pipe(response);
|
|
4416
|
-
}
|
|
4417
|
-
async function resolveStaticFile(assetPath, webRoot) {
|
|
4418
|
-
try {
|
|
4419
|
-
const stat = await fs.stat(assetPath);
|
|
4420
|
-
if (stat.isFile()) {
|
|
4421
|
-
return assetPath;
|
|
4422
|
-
}
|
|
4423
|
-
}
|
|
4424
|
-
catch {
|
|
4425
|
-
// Fall back to the SPA entrypoint below.
|
|
4426
|
-
}
|
|
4427
|
-
const indexPath = path.join(webRoot, "index.html");
|
|
4428
|
-
try {
|
|
4429
|
-
const stat = await fs.stat(indexPath);
|
|
4430
|
-
return stat.isFile() ? indexPath : null;
|
|
4431
|
-
}
|
|
4432
|
-
catch {
|
|
4433
|
-
return null;
|
|
4434
|
-
}
|
|
4435
|
-
}
|
|
4436
|
-
function isInsidePath(value, root) {
|
|
4437
|
-
const relative = path.relative(root, value);
|
|
4438
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
4439
|
-
}
|
|
4440
|
-
function isCommandApprovalDecision(value) {
|
|
4441
|
-
return (value === "accept" ||
|
|
4442
|
-
value === "acceptForSession" ||
|
|
4443
|
-
value === "decline" ||
|
|
4444
|
-
value === "cancel");
|
|
4445
|
-
}
|
|
4446
|
-
function readRecord(value) {
|
|
4447
|
-
return value && typeof value === "object" ? value : {};
|
|
4448
|
-
}
|
|
4449
|
-
function isAuthorizedProfuseRequest(request) {
|
|
4450
|
-
return request.headers.authorization === `Bearer ${PROFUSE_AGENT_TOKEN}`;
|
|
4451
|
-
}
|
|
4452
|
-
function readAuthenticatedUser(request) {
|
|
4453
|
-
if (!userStore.enabled) {
|
|
4454
|
-
return {
|
|
4455
|
-
id: "local-admin",
|
|
4456
|
-
username: config.authUsername,
|
|
4457
|
-
role: "admin",
|
|
4458
|
-
active: true,
|
|
4459
|
-
sessionVersion: 1,
|
|
4460
|
-
createdAt: "",
|
|
4461
|
-
updatedAt: "",
|
|
4462
|
-
};
|
|
4463
|
-
}
|
|
4464
|
-
const payload = readSessionPayload(request, userStore.sessionSecret);
|
|
4465
|
-
if (!payload) {
|
|
4466
|
-
return null;
|
|
4467
|
-
}
|
|
4468
|
-
const user = userStore.getUser(payload.userId);
|
|
4469
|
-
if (!user ||
|
|
4470
|
-
!user.active ||
|
|
4471
|
-
user.sessionVersion !== payload.sessionVersion) {
|
|
4472
|
-
return null;
|
|
4473
|
-
}
|
|
4474
|
-
return user;
|
|
4475
|
-
}
|
|
4476
|
-
function publicUser(user) {
|
|
4477
|
-
const { sessionVersion: _sessionVersion, ...result } = user;
|
|
4478
|
-
return result;
|
|
4479
|
-
}
|
|
4480
|
-
function isLoopbackHost(host) {
|
|
4481
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
4482
|
-
}
|
|
4483
|
-
function workspaceIdFromApiPath(pathname) {
|
|
4484
|
-
const match = pathname.match(/^\/api\/conversations\/([^/]+)/);
|
|
4485
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
4486
|
-
}
|
|
4487
|
-
function workspaceIdFromAgentPath(pathname) {
|
|
4488
|
-
const match = pathname.match(/^\/agent\/workspaces\/([^/]+)/);
|
|
4489
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
4490
|
-
}
|
|
4491
|
-
function workspaceGroupIdFromApiPath(pathname) {
|
|
4492
|
-
const match = pathname.match(/^\/api\/workspace-groups\/([^/]+)/);
|
|
4493
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
4494
|
-
}
|
|
4495
|
-
function canAccessWorkspace(user, workspaceId) {
|
|
4496
|
-
const conversation = getConversation(workspaceId);
|
|
4497
|
-
return Boolean(conversation && (user.role === "admin" || conversation.ownerId === user.id));
|
|
4498
|
-
}
|
|
4499
|
-
function canAccessWorkspaceGroup(user, groupId) {
|
|
4500
|
-
const group = listWorkspaceGroups().find((item) => item.id === groupId);
|
|
4501
|
-
return Boolean(group &&
|
|
4502
|
-
(group.ownerId === null || user.role === "admin" || group.ownerId === user.id));
|
|
4503
|
-
}
|
|
4504
|
-
function canManageWorkspaceGroup(user, groupId) {
|
|
4505
|
-
const group = listWorkspaceGroups().find((item) => item.id === groupId);
|
|
4506
|
-
return Boolean(group && (user.role === "admin" || group.ownerId === user.id));
|
|
4507
|
-
}
|
|
4508
|
-
function visibleConversations(user) {
|
|
4509
|
-
const items = listConversations();
|
|
4510
|
-
const visible = user.role === "admin"
|
|
4511
|
-
? items
|
|
4512
|
-
: items.filter((conversation) => conversation.ownerId === user.id);
|
|
4513
|
-
return visible.map((conversation) => ({
|
|
4514
|
-
...conversation,
|
|
4515
|
-
unreadTurnCount: countConversationUnreadTurns(conversation, user.id),
|
|
4516
|
-
}));
|
|
4517
|
-
}
|
|
4518
|
-
function visibleWorkspaceGroups(user) {
|
|
4519
|
-
const groups = listWorkspaceGroups();
|
|
4520
|
-
return user.role === "admin"
|
|
4521
|
-
? groups
|
|
4522
|
-
: groups.filter((group) => group.ownerId === null || group.ownerId === user.id);
|
|
4523
|
-
}
|
|
4524
|
-
function requiresAdminAccess(method, pathname) {
|
|
4525
|
-
if (pathname.startsWith("/api/admin/") || pathname === "/api/diagnostics") {
|
|
4526
|
-
return true;
|
|
4527
|
-
}
|
|
4528
|
-
if (!pathname.startsWith("/api/settings/")) {
|
|
4529
|
-
return false;
|
|
4530
|
-
}
|
|
4531
|
-
if (method !== "GET") {
|
|
4532
|
-
return true;
|
|
4533
|
-
}
|
|
4534
|
-
return pathname.endsWith("/reveal");
|
|
4535
|
-
}
|
|
4536
|
-
function normalizeUserRole(value) {
|
|
4537
|
-
return value === "admin" ? "admin" : "user";
|
|
4538
|
-
}
|
|
4539
|
-
function activeAdminCount() {
|
|
4540
|
-
return userStore
|
|
4541
|
-
.listUsers()
|
|
4542
|
-
.filter((user) => user.role === "admin" && user.active).length;
|
|
4543
|
-
}
|
|
4544
|
-
function userMutationError(error) {
|
|
4545
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4546
|
-
if (message.includes("UNIQUE constraint failed")) {
|
|
4547
|
-
return "username_exists";
|
|
4548
|
-
}
|
|
4549
|
-
return message === "invalid_username" || message === "invalid_password"
|
|
4550
|
-
? message
|
|
4551
|
-
: "user_update_failed";
|
|
4552
|
-
}
|
|
4553
|
-
function loginAttemptBuckets(request, username) {
|
|
4554
|
-
const source = request.socket.remoteAddress ?? "unknown";
|
|
4555
|
-
const usernameDigest = createHash("sha256")
|
|
4556
|
-
.update(username.trim().toLowerCase())
|
|
4557
|
-
.digest("hex");
|
|
4558
|
-
const sourceDigest = createHash("sha256").update(source).digest("hex");
|
|
4559
|
-
return [
|
|
4560
|
-
{
|
|
4561
|
-
key: `pair:${sourceDigest}:${usernameDigest}`,
|
|
4562
|
-
maxFailures: LOGIN_MAX_FAILURES,
|
|
4563
|
-
},
|
|
4564
|
-
{ key: `user:${usernameDigest}`, maxFailures: LOGIN_USER_MAX_FAILURES },
|
|
4565
|
-
{ key: `source:${sourceDigest}`, maxFailures: LOGIN_SOURCE_MAX_FAILURES },
|
|
4566
|
-
];
|
|
4567
|
-
}
|
|
4568
|
-
function isServableConversationFileContentType(value) {
|
|
4569
|
-
return (value.startsWith("image/") ||
|
|
4570
|
-
value.startsWith("text/") ||
|
|
4571
|
-
value.startsWith("application/json"));
|
|
4572
|
-
}
|
|
4573
|
-
function contentTypeFor(filePath) {
|
|
4574
|
-
switch (path.extname(filePath)) {
|
|
4575
|
-
case ".csv":
|
|
4576
|
-
return "text/csv; charset=utf-8";
|
|
4577
|
-
case ".css":
|
|
4578
|
-
return "text/css; charset=utf-8";
|
|
4579
|
-
case ".html":
|
|
4580
|
-
return "text/html; charset=utf-8";
|
|
4581
|
-
case ".js":
|
|
4582
|
-
return "text/javascript; charset=utf-8";
|
|
4583
|
-
case ".json":
|
|
4584
|
-
return "application/json; charset=utf-8";
|
|
4585
|
-
case ".map":
|
|
4586
|
-
return "application/json; charset=utf-8";
|
|
4587
|
-
case ".md":
|
|
4588
|
-
case ".markdown":
|
|
4589
|
-
return "text/markdown; charset=utf-8";
|
|
4590
|
-
case ".svg":
|
|
4591
|
-
return "image/svg+xml";
|
|
4592
|
-
case ".txt":
|
|
4593
|
-
case ".log":
|
|
4594
|
-
return "text/plain; charset=utf-8";
|
|
4595
|
-
case ".png":
|
|
4596
|
-
return "image/png";
|
|
4597
|
-
case ".jpg":
|
|
4598
|
-
case ".jpeg":
|
|
4599
|
-
return "image/jpeg";
|
|
4600
|
-
case ".webp":
|
|
4601
|
-
return "image/webp";
|
|
4602
|
-
default:
|
|
4603
|
-
return "application/octet-stream";
|
|
4604
|
-
}
|
|
4605
|
-
}
|