helloagents 3.0.37 → 3.0.38
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +12 -9
- package/README_CN.md +12 -9
- package/bootstrap-lite.md +3 -4
- package/bootstrap.md +3 -4
- package/gemini-extension.json +1 -1
- package/install.ps1 +3 -1
- package/install.sh +3 -1
- package/package.json +1 -1
- package/scripts/notify.mjs +4 -0
- package/scripts/project-session-cleanup.mjs +20 -22
- package/scripts/project-storage.mjs +2 -8
- package/scripts/runtime-scope.mjs +103 -11
- package/scripts/session-capsule.mjs +92 -12
- package/scripts/session-token.mjs +18 -0
- package/scripts/state-document.mjs +6 -7
- package/scripts/turn-state.mjs +2 -3
- package/scripts/workflow-core.mjs +1 -4
- package/scripts/workflow-plan-files.mjs +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.38",
|
|
4
4
|
"description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, unified QA gates, safety guards, and notifications.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HelloWind",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.38",
|
|
4
4
|
"description": "HelloAGENTS — Quality-driven orchestration kernel for AI CLIs with intelligent routing, unified QA gates, safety guards, and notifications.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HelloWind",
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**A workflow layer for AI coding CLIs: skills, project knowledge, delivery checks, safer config writes, and resumable execution.**
|
|
10
10
|
|
|
11
|
-
[](./package.json)
|
|
12
12
|
[](https://www.npmjs.com/package/helloagents)
|
|
13
13
|
[](./package.json)
|
|
14
14
|
[](./skills)
|
|
@@ -184,12 +184,12 @@ Long tasks need a small recovery snapshot, but one shared state file is not safe
|
|
|
184
184
|
|
|
185
185
|
HelloAGENTS now resolves the current state file from `state_path`:
|
|
186
186
|
|
|
187
|
-
- with a stable session id: `.helloagents/sessions/<workspace>/<session>/STATE.md`
|
|
188
|
-
-
|
|
187
|
+
- with a stable or reusable session id: `.helloagents/sessions/<workspace>/<session>/STATE.md`
|
|
188
|
+
- before a reusable session id is available: `.helloagents/sessions/<workspace>/default/STATE.md`
|
|
189
189
|
|
|
190
|
-
`<workspace>` is the current Git branch, `detached-<sha>` for a detached HEAD, or `workspace` for non-Git projects. `.helloagents/sessions/active.json` only
|
|
190
|
+
`<workspace>` is the current Git branch, `detached-<sha>` for a detached HEAD, or `workspace` for non-Git projects. `<session>` is the current project-local session token. `.helloagents/sessions/active.json` only keeps the latest active workspace/session mapping plus alias bridges, so the same CLI session stays in one directory and `/resume` can reuse it.
|
|
191
191
|
|
|
192
|
-
For project-local sessions, HelloAGENTS
|
|
192
|
+
For project-local sessions, HelloAGENTS first uses stable host identifiers such as `sessionId`, `conversationId`, `threadId`, or `HELLOAGENTS_NOTIFY_SESSION_ID`. If the host only exposes a window or terminal id such as `WT_SESSION`, `TERM_SESSION_ID`, or `WINDOWID`, HelloAGENTS uses it only as a lightweight alias bridge and reuses the mapped session first instead of fanning out duplicate directories.
|
|
193
193
|
|
|
194
194
|
`STATE.md` records where the current workflow stopped. It is not a universal memory file for every conversation. Codex `/goal` does not replace `state_path`, `turn-state`, or local evidence files; it only handles long-running continuation on the Codex side.
|
|
195
195
|
|
|
@@ -200,14 +200,16 @@ HelloAGENTS does not treat “tests passed” and “task complete” as the sam
|
|
|
200
200
|
Runtime state now stays intentionally small:
|
|
201
201
|
|
|
202
202
|
- `.helloagents/sessions/<workspace>/<session>/STATE.md`
|
|
203
|
+
- `.helloagents/sessions/<workspace>/<session>/runtime.json`
|
|
203
204
|
- `.helloagents/sessions/active.json`
|
|
204
205
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/qa-review.json`
|
|
205
206
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/advisor.json`
|
|
206
207
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/visual.json`
|
|
207
208
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/closeout.json`
|
|
209
|
+
- optional `.helloagents/sessions/<workspace>/<session>/events.jsonl`
|
|
208
210
|
- `~/.codex/.helloagents/notify-state.json` for Codex-native closeout de-duplication only
|
|
209
211
|
|
|
210
|
-
`
|
|
212
|
+
`STATE.md` only keeps the human-readable recovery snapshot. `runtime.json` is machine-only and keeps the minimal runtime state. `artifacts/*.json` stays limited to structured receipts. `events.jsonl` remains opt-in trace output and stays off by default.
|
|
211
213
|
Project-local `STATE.md` is now materialized more lazily, and legacy root-level `.helloagents/artifacts/*.log` files are cleaned up automatically instead of growing as a second history system.
|
|
212
214
|
|
|
213
215
|
Standard runtime evidence and transient runtime state now expire after 72 hours. Long-running Codex goal flows still keep their 720-hour upper bound where the workflow explicitly needs it.
|
|
@@ -507,6 +509,7 @@ Runtime state and evidence remain local to the working project:
|
|
|
507
509
|
|
|
508
510
|
- `state_path`
|
|
509
511
|
- `.helloagents/sessions/active.json`
|
|
512
|
+
- `.helloagents/sessions/<workspace>/<session>/runtime.json`
|
|
510
513
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/*.json`
|
|
511
514
|
|
|
512
515
|
### Temporary sessions outside project-local storage
|
|
@@ -517,9 +520,9 @@ For read-only work with no local output, if neither the current directory nor it
|
|
|
517
520
|
~/.helloagents/runtime/<scope-key>/
|
|
518
521
|
```
|
|
519
522
|
|
|
520
|
-
This only stores short-lived `STATE.md` and `artifacts/`. Optional `events.jsonl` trace files are only written when trace mode is enabled. It is not project knowledge. Expired transient sessions are removed by TTL cleanup.
|
|
523
|
+
This only stores short-lived `STATE.md`, `runtime.json`, and `artifacts/`. Optional `events.jsonl` trace files are only written when trace mode is enabled. It is not project knowledge. Expired transient sessions are removed by TTL cleanup.
|
|
521
524
|
|
|
522
|
-
Once the task creates or modifies local files, or otherwise leaves local output in the current project, HelloAGENTS creates the project-local `.helloagents/sessions
|
|
525
|
+
Once the task creates or modifies local files, or otherwise leaves local output in the current project, HelloAGENTS creates the project-local `.helloagents/sessions/<workspace>/<session>/STATE.md` automatically instead of keeping that task only in the user-level transient runtime.
|
|
523
526
|
|
|
524
527
|
### Knowledge creation rules
|
|
525
528
|
|
|
@@ -682,7 +685,7 @@ The current suite covers:
|
|
|
682
685
|
- Codex `/goal` feature toggles, long-running route context, and goal-aware command contracts
|
|
683
686
|
- `helloagents doctor`
|
|
684
687
|
- project storage and `repo-shared` behavior
|
|
685
|
-
- session
|
|
688
|
+
- workspace-session scoped `state_path`, runtime signals, and evidence
|
|
686
689
|
- runtime injection, routing, guard, verification, visual evidence, delivery gates, closeout de-duplication, sub-agent wrapper and notification suppression, and successful-mode tracking after native install failures
|
|
687
690
|
- README and skill contract alignment
|
|
688
691
|
|
package/README_CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**面向 AI 编码 CLI 的工作流层:技能、知识库、交付检查、更安全的配置写入,以及可恢复的执行流程。**
|
|
10
10
|
|
|
11
|
-
[](./package.json)
|
|
12
12
|
[](https://www.npmjs.com/package/helloagents)
|
|
13
13
|
[](./package.json)
|
|
14
14
|
[](./skills)
|
|
@@ -184,12 +184,12 @@ HelloAGENTS 可以在 `.helloagents/` 下创建和维护项目知识库。
|
|
|
184
184
|
|
|
185
185
|
HelloAGENTS 现在只从 `state_path` 解析当前状态文件:
|
|
186
186
|
|
|
187
|
-
-
|
|
188
|
-
-
|
|
187
|
+
- 宿主提供稳定会话标识或可复用会话标识时:`.helloagents/sessions/<workspace>/<session>/STATE.md`
|
|
188
|
+
- 暂时还拿不到可复用会话标识时:`.helloagents/sessions/<workspace>/default/STATE.md`
|
|
189
189
|
|
|
190
|
-
`<workspace>` 是当前 Git 分支、detached HEAD 的 `detached-<sha>`,或非 Git 项目的 `workspace
|
|
190
|
+
`<workspace>` 是当前 Git 分支、detached HEAD 的 `detached-<sha>`,或非 Git 项目的 `workspace`。`<session>` 是当前项目本地会话标识。`.helloagents/sessions/active.json` 只保留最近一次活跃的工作区/会话映射和 alias 桥接,这样同一个 CLI 会话会稳定落在同一个目录里,`/resume` 也能复用它。
|
|
191
191
|
|
|
192
|
-
对于项目本地会话目录,HelloAGENTS
|
|
192
|
+
对于项目本地会话目录,HelloAGENTS 会优先使用稳定宿主标识,如 `sessionId`、`conversationId`、`threadId` 或 `HELLOAGENTS_NOTIFY_SESSION_ID`。如果宿主只能提供 `WT_SESSION`、`TERM_SESSION_ID`、`WINDOWID` 这类窗口或终端标识,HelloAGENTS 只把它们当作轻量 alias 桥接,并优先复用已映射的会话目录,而不是继续分裂出重复目录。
|
|
193
193
|
|
|
194
194
|
`STATE.md` 只记录当前工作流做到哪里,不承担所有对话的统一记忆。Codex `/goal` 也不替代 `state_path`、`turn-state` 或本地证据文件;它只负责 Codex 侧的长程续跑。
|
|
195
195
|
|
|
@@ -200,14 +200,16 @@ HelloAGENTS 不把“命令通过”和“任务完成”简单画等号。交
|
|
|
200
200
|
运行态现在尽量收敛,只保留真正有用的文件:
|
|
201
201
|
|
|
202
202
|
- `.helloagents/sessions/<workspace>/<session>/STATE.md`
|
|
203
|
+
- `.helloagents/sessions/<workspace>/<session>/runtime.json`
|
|
203
204
|
- `.helloagents/sessions/active.json`
|
|
204
205
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/qa-review.json`
|
|
205
206
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/advisor.json`
|
|
206
207
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/visual.json`
|
|
207
208
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/closeout.json`
|
|
209
|
+
- 可选 `.helloagents/sessions/<workspace>/<session>/events.jsonl`
|
|
208
210
|
- 仅用于 Codex 原生收尾去重的 `~/.codex/.helloagents/notify-state.json`
|
|
209
211
|
|
|
210
|
-
`
|
|
212
|
+
`STATE.md` 只保留给人看的恢复快照。`runtime.json` 只给机器用,只保存极少量运行态。`artifacts/*.json` 只保留结构化收据。`events.jsonl` 仍是可选 trace 输出,默认不写。
|
|
211
213
|
项目本地 `STATE.md` 现在会更晚创建;旧版残留的项目根 `.helloagents/artifacts/*.log` 也会自动清理,不再继续充当第二套历史系统。
|
|
212
214
|
|
|
213
215
|
标准运行态证据和临时运行态现在默认 72 小时过期。只有工作流明确需要的长程 Codex goal 链路,才继续保留 720 小时上限。
|
|
@@ -511,6 +513,7 @@ Codex 全局模式由 HelloAGENTS 通过本地插件路径自动安装。
|
|
|
511
513
|
|
|
512
514
|
- `state_path`
|
|
513
515
|
- `.helloagents/sessions/active.json`
|
|
516
|
+
- `.helloagents/sessions/<workspace>/<session>/runtime.json`
|
|
514
517
|
- `.helloagents/sessions/<workspace>/<session>/artifacts/*.json`
|
|
515
518
|
|
|
516
519
|
### 项目本地存储之外的临时会话
|
|
@@ -521,9 +524,9 @@ Codex 全局模式由 HelloAGENTS 通过本地插件路径自动安装。
|
|
|
521
524
|
~/.helloagents/runtime/<scope-key>/
|
|
522
525
|
```
|
|
523
526
|
|
|
524
|
-
这里仅保存短期的 `STATE.md` 和 `artifacts/`。`events.jsonl` 只有在启用 trace 时才会写入,不作为默认运行态文件。它也不属于项目知识库。过期临时会话会按 TTL 清理。
|
|
527
|
+
这里仅保存短期的 `STATE.md`、`runtime.json` 和 `artifacts/`。`events.jsonl` 只有在启用 trace 时才会写入,不作为默认运行态文件。它也不属于项目知识库。过期临时会话会按 TTL 清理。
|
|
525
528
|
|
|
526
|
-
一旦任务会创建或修改本地文件,或会在当前项目留下本地输出,HelloAGENTS 就会自动创建项目本地 `.helloagents/sessions
|
|
529
|
+
一旦任务会创建或修改本地文件,或会在当前项目留下本地输出,HelloAGENTS 就会自动创建项目本地 `.helloagents/sessions/<workspace>/<session>/STATE.md`,而不是只停留在用户级临时运行态。
|
|
527
530
|
|
|
528
531
|
### 知识创建规则
|
|
529
532
|
|
|
@@ -686,7 +689,7 @@ npm test
|
|
|
686
689
|
- Codex `/goal` 功能开关、长程路由上下文和 goal 感知命令契约
|
|
687
690
|
- `helloagents doctor`
|
|
688
691
|
- 项目存储和 `repo-shared`
|
|
689
|
-
-
|
|
692
|
+
- 工作区+会话级 `state_path`、运行态信号和证据
|
|
690
693
|
- 运行时注入、选路、Guard、验证、视觉证据、交付门控、收尾去重、子代理外层格式与通知静默保护,以及原生安装失败后的模式记录
|
|
691
694
|
- README 与 skill 契约一致性
|
|
692
695
|
|
package/bootstrap-lite.md
CHANGED
|
@@ -221,9 +221,9 @@
|
|
|
221
221
|
路径: {CWD}/.helloagents/
|
|
222
222
|
所有文件的创建和更新必须按 templates/ 目录中对应模板的格式执行,不可自由发挥格式。
|
|
223
223
|
- `.helloagents/` 表示项目本地存储路径,负责知识、方案、状态与运行态;它不再作为项目是否已初始化的判定信号
|
|
224
|
-
- `state_path` 指向的状态文件始终保留在项目本地 `.helloagents/sessions/{workspace}/{session}/STATE.md`;当前会话的 `turn-state`、路由上下文和 artifact
|
|
225
|
-
- `state_path`
|
|
226
|
-
- `{workspace}` 为当前 Git 分支、`detached-{sha}` 或非 Git 项目的 `workspace`;`.helloagents/sessions/active.json`
|
|
224
|
+
- `state_path` 指向的状态文件始终保留在项目本地 `.helloagents/sessions/{workspace}/{session}/STATE.md`;当前会话的 `turn-state`、路由上下文和 artifact 索引写入同目录 `runtime.json`,`artifacts/*.json` 仅在需要结构化证据时按需生成,`events.jsonl` 仅在显式 trace 模式下写入
|
|
225
|
+
- `state_path` 是状态文件的唯一位置。宿主提供稳定会话标识时,写入 `.helloagents/sessions/{workspace}/{session}/STATE.md`;没有稳定或可复用会话标识时,写入 `.helloagents/sessions/{workspace}/default/STATE.md`
|
|
226
|
+
- `{workspace}` 为当前 Git 分支、`detached-{sha}` 或非 Git 项目的 `workspace`;`.helloagents/sessions/active.json` 只记录最近一次活跃的工作区/会话映射与 alias 桥接,避免同一 CLI 会话被拆成多个目录
|
|
227
227
|
- 若 helloagents.json 中 `project_store_mode = "repo-shared"`,`context.md`、`guidelines.md`、`CHANGELOG.md`、`verify.yaml`、`DESIGN.md`、`modules/`、`plans/`、`archive/` 改按当前上下文中已注入的“当前项目存储”/“项目知识/方案目录”解析;未注入具体路径时,按当前存储模式自行解析,不要假定这些文件一定实际位于当前工作树中
|
|
228
228
|
templates/ 查找路径(按优先级;首次确定模板根目录后,本会话复用):
|
|
229
229
|
按上文 `~command` 路由中的相同技能根目录规则确定;确定根目录后读取其中的 `templates/`。
|
|
@@ -292,4 +292,3 @@ Tier 3 — 深入特定模块时读取:
|
|
|
292
292
|
|
|
293
293
|
### 项目文件
|
|
294
294
|
根据知识库中的架构描述和模块索引,结合当前任务需求,按需读取相关的项目源码、配置和资源文件。不要一次性读取整个项目,先通过知识库了解项目结构,再有针对性地读取需要的文件。不要把项目级规则文件(`AGENTS.md`、`CLAUDE.md`、`.gemini/GEMINI.md`)当作普通项目文件重复读取。
|
|
295
|
-
|
package/bootstrap.md
CHANGED
|
@@ -290,9 +290,9 @@ hello-* 技能读取路径:`{HELLOAGENTS_READ_ROOT}/skills/{技能名}/SKILL.m
|
|
|
290
290
|
路径: {CWD}/.helloagents/
|
|
291
291
|
所有文件的创建和更新必须按 templates/ 目录中对应模板的格式执行,不可自由发挥格式。
|
|
292
292
|
- `.helloagents/` 表示项目本地存储路径,负责知识、方案、状态与运行态;它不再作为项目是否已初始化的判定信号
|
|
293
|
-
- `state_path` 指向的状态文件始终保留在项目本地 `.helloagents/sessions/{workspace}/{session}/STATE.md`;当前会话的 `turn-state`、路由上下文和 artifact
|
|
294
|
-
- `state_path`
|
|
295
|
-
- `{workspace}` 为当前 Git 分支、`detached-{sha}` 或非 Git 项目的 `workspace`;`.helloagents/sessions/active.json`
|
|
293
|
+
- `state_path` 指向的状态文件始终保留在项目本地 `.helloagents/sessions/{workspace}/{session}/STATE.md`;当前会话的 `turn-state`、路由上下文和 artifact 索引写入同目录 `runtime.json`,`artifacts/*.json` 仅在需要结构化证据时按需生成,`events.jsonl` 仅在显式 trace 模式下写入
|
|
294
|
+
- `state_path` 是状态文件的唯一位置。宿主提供稳定会话标识时,写入 `.helloagents/sessions/{workspace}/{session}/STATE.md`;没有稳定或可复用会话标识时,写入 `.helloagents/sessions/{workspace}/default/STATE.md`
|
|
295
|
+
- `{workspace}` 为当前 Git 分支、`detached-{sha}` 或非 Git 项目的 `workspace`;`.helloagents/sessions/active.json` 只记录最近一次活跃的工作区/会话映射与 alias 桥接,避免同一 CLI 会话被拆成多个目录
|
|
296
296
|
- 若 helloagents.json 中 `project_store_mode = "repo-shared"`,`context.md`、`guidelines.md`、`CHANGELOG.md`、`verify.yaml`、`DESIGN.md`、`modules/`、`plans/`、`archive/` 改按当前上下文中已注入的“当前项目存储”/“项目知识/方案目录”解析;未注入具体路径时,按当前存储模式自行解析,不要假定这些文件一定实际位于当前工作树中
|
|
297
297
|
templates/ 查找路径(按优先级;首次确定模板根目录后,本会话复用):
|
|
298
298
|
按上文相同的技能根目录规则确定;确定根目录后读取其中的 `templates/`。
|
|
@@ -361,4 +361,3 @@ Tier 3 — 深入特定模块时读取:
|
|
|
361
361
|
|
|
362
362
|
### 项目文件
|
|
363
363
|
根据知识库中的架构描述和模块索引,结合当前任务需求,按需读取相关的项目源码、配置和资源文件。不要一次性读取整个项目,先通过知识库了解项目结构,再有针对性地读取需要的文件。不要把项目级规则文件(`AGENTS.md`、`CLAUDE.md`、`.gemini/GEMINI.md`)当作普通项目文件重复读取。
|
|
364
|
-
|
package/gemini-extension.json
CHANGED
package/install.ps1
CHANGED
package/install.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloagents",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, unified QA gates, safety guards, and notifications.",
|
|
6
6
|
"author": "HelloWind",
|
package/scripts/notify.mjs
CHANGED
|
@@ -349,6 +349,10 @@ function attachTurnSession(payload = {}, cwd = payload.cwd || process.cwd()) {
|
|
|
349
349
|
allowPpidFallback: !isProjectRuntimeActive(cwd),
|
|
350
350
|
});
|
|
351
351
|
if (!sessionId || payload.sessionId) return payload;
|
|
352
|
+
const aliasSource = String(process.env.HELLOAGENTS_NOTIFY_SESSION_ID || process.env.WT_SESSION || process.env.TERM_SESSION_ID || process.env.WINDOWID || '').trim();
|
|
353
|
+
if (aliasSource) {
|
|
354
|
+
return { ...payload, sessionId, _helloagentsSessionAlias: aliasSource };
|
|
355
|
+
}
|
|
352
356
|
return { ...payload, sessionId };
|
|
353
357
|
}
|
|
354
358
|
|
|
@@ -10,12 +10,10 @@ import {
|
|
|
10
10
|
writeJsonFileAtomic,
|
|
11
11
|
} from './runtime-scope.mjs'
|
|
12
12
|
import { LONG_RUNNING_TTL_MS } from './runtime-ttl.mjs'
|
|
13
|
-
import { readStateDocument } from './state-document.mjs'
|
|
13
|
+
import { looksLikeAutoCreatedState, readStateDocument } from './state-document.mjs'
|
|
14
14
|
|
|
15
15
|
export const PROJECT_SESSION_CLEANUP_COOLDOWN_MS = 10 * 60 * 1000
|
|
16
16
|
export const PROJECT_SESSION_MAX_AGE_MS = LONG_RUNNING_TTL_MS
|
|
17
|
-
const AUTO_CREATED_STATE_MARKER = '由运行时自动创建;后续按实际任务重写'
|
|
18
|
-
|
|
19
17
|
function removePath(filePath, result, bucket) {
|
|
20
18
|
try {
|
|
21
19
|
rmSync(filePath, { recursive: true, force: true })
|
|
@@ -38,9 +36,10 @@ function isDirectoryEmptyRecursive(dirPath) {
|
|
|
38
36
|
})
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
function
|
|
39
|
+
function shouldKeepNestedSession(active, workspace, sessionName) {
|
|
42
40
|
const activeWorkspace = active.workspace || active.branch || ''
|
|
43
|
-
|
|
41
|
+
const activeSession = active.session || ''
|
|
42
|
+
return activeWorkspace === workspace && activeSession === sessionName
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
function readCleanupCheckedAt(active) {
|
|
@@ -65,9 +64,8 @@ function isAutoCreatedSeedSession(sessionDir) {
|
|
|
65
64
|
const statePath = join(sessionDir, 'STATE.md')
|
|
66
65
|
if (!existsSync(statePath)) return false
|
|
67
66
|
|
|
68
|
-
const {
|
|
69
|
-
|
|
70
|
-
return String(body || '').includes(AUTO_CREATED_STATE_MARKER)
|
|
67
|
+
const { body } = readStateDocument(statePath)
|
|
68
|
+
return looksLikeAutoCreatedState(body)
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
function readSessionStateMtimeMs(sessionDir) {
|
|
@@ -157,28 +155,28 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
|
|
|
157
155
|
for (const workspaceEntry of readdirSync(sessionsDir, { withFileTypes: true })) {
|
|
158
156
|
if (!workspaceEntry.isDirectory()) continue
|
|
159
157
|
const workspaceDir = join(sessionsDir, workspaceEntry.name)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
try {
|
|
158
|
+
try {
|
|
159
|
+
const nestedEntries = readdirSync(workspaceDir, { withFileTypes: true }).filter((entry) => entry.isDirectory())
|
|
160
|
+
for (const nestedEntry of nestedEntries) {
|
|
161
|
+
const sessionDir = join(workspaceDir, nestedEntry.name)
|
|
162
|
+
if (shouldKeepNestedSession(active, workspaceEntry.name, nestedEntry.name)) continue
|
|
167
163
|
if (isDirectoryEmptyRecursive(sessionDir)) {
|
|
168
164
|
removePath(sessionDir, result, 'removedEmptyDirs')
|
|
169
|
-
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (!hasStateSnapshot(sessionDir)) {
|
|
170
168
|
removePath(sessionDir, result, 'removedNoStateDirs')
|
|
171
|
-
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (isAutoCreatedSeedSession(sessionDir)) {
|
|
172
172
|
removePath(sessionDir, result, 'removedSeedDirs')
|
|
173
|
-
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (isStaleStateSession(sessionDir, now, maxAgeMs)) {
|
|
174
176
|
removePath(sessionDir, result, 'removedInactiveDirs')
|
|
175
177
|
}
|
|
176
|
-
} catch (error) {
|
|
177
|
-
result.errors.push(`${sessionDir}: ${error.message}`)
|
|
178
178
|
}
|
|
179
|
-
}
|
|
180
179
|
|
|
181
|
-
try {
|
|
182
180
|
if (isDirectoryEmptyRecursive(workspaceDir)) {
|
|
183
181
|
removePath(workspaceDir, result, 'removedEmptyDirs')
|
|
184
182
|
}
|
|
@@ -114,8 +114,8 @@ export function getProjectSessionStateScope(cwd, options = {}) {
|
|
|
114
114
|
const scope = getProjectSessionScope(cwd, normalizeRuntimeOptions(options))
|
|
115
115
|
|
|
116
116
|
return {
|
|
117
|
-
stateScope: 'session',
|
|
118
|
-
stateSessionToken: scope.session,
|
|
117
|
+
stateScope: 'workspace-session',
|
|
118
|
+
stateSessionToken: scope.session || '',
|
|
119
119
|
stateSessionMode: scope.sessionMode,
|
|
120
120
|
stateWorkspace: scope.workspace || scope.branch,
|
|
121
121
|
sessionDir: scope.sessionDir,
|
|
@@ -246,9 +246,6 @@ export function buildProjectStorageHint(cwd, options = {}) {
|
|
|
246
246
|
const summary = getProjectStoreSummary(cwd, options)
|
|
247
247
|
const hints = []
|
|
248
248
|
hints.push(`当前状态文件写入 \`${summary.promptStatePath}\``)
|
|
249
|
-
if (summary.stateSessionMode === 'default') {
|
|
250
|
-
hints.push(`当前宿主未提供稳定会话标识,因此使用工作区默认位置 \`${summary.stateSessionToken}\``)
|
|
251
|
-
}
|
|
252
249
|
if (summary.usesSharedStore) {
|
|
253
250
|
hints.push(`项目存储:\`project_store_mode=repo-shared\`;项目本地存储/会话运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\``)
|
|
254
251
|
}
|
|
@@ -277,9 +274,6 @@ export function buildProjectStorageBlock(cwd, options = {}) {
|
|
|
277
274
|
|
|
278
275
|
const explanations = []
|
|
279
276
|
explanations.push('说明:状态文件只使用 `state_path`。')
|
|
280
|
-
if (summary.stateSessionMode === 'default') {
|
|
281
|
-
explanations.push('说明:当前宿主未提供稳定会话标识,因此使用工作区默认位置。')
|
|
282
|
-
}
|
|
283
277
|
if (summary.usesSharedStore) {
|
|
284
278
|
explanations.push('说明:状态文件与会话产物写项目本地存储目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。')
|
|
285
279
|
} else {
|
|
@@ -4,17 +4,24 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync,
|
|
|
4
4
|
import { dirname, join, normalize, resolve } from 'node:path'
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
resolveProjectSessionAliasToken,
|
|
9
|
+
resolveProjectSessionToken,
|
|
10
|
+
resolveSessionToken,
|
|
11
|
+
} from './session-token.mjs'
|
|
8
12
|
import { USER_RUNTIME_MAX_AGE_MS } from './runtime-ttl.mjs'
|
|
9
13
|
import { cleanupUserRuntimeRoot, getUserRuntimeRoot } from './runtime-user-cleanup.mjs'
|
|
10
14
|
import { FULL_CARRIER_PROFILE_MARKER } from './cli-utils.mjs'
|
|
15
|
+
import { readStateDocument, writeStateDocument } from './state-document.mjs'
|
|
11
16
|
|
|
12
17
|
export const PROJECT_DIR_NAME = '.helloagents'
|
|
13
18
|
export const PROJECT_SESSIONS_DIR_NAME = 'sessions'
|
|
14
19
|
export const PROJECT_ARTIFACTS_DIR_NAME = 'artifacts'
|
|
15
20
|
export const EVENTS_FILE_NAME = 'events.jsonl'
|
|
16
21
|
export const ACTIVE_SESSION_FILE_NAME = 'active.json'
|
|
22
|
+
export const PROJECT_RUNTIME_FILE_NAME = 'runtime.json'
|
|
17
23
|
export const DEFAULT_STATE_SESSION_TOKEN = 'default'
|
|
24
|
+
export const LEGACY_SESSION_POINTERS_FILE_NAME = 'session-pointers.json'
|
|
18
25
|
export const USER_RUNTIME_DIR_NAME = 'runtime'
|
|
19
26
|
export { cleanupUserRuntimeRoot, getUserRuntimeRoot, USER_RUNTIME_MAX_AGE_MS }
|
|
20
27
|
|
|
@@ -260,6 +267,38 @@ function buildInitialStateSnapshot({
|
|
|
260
267
|
].join('\n')
|
|
261
268
|
}
|
|
262
269
|
|
|
270
|
+
function normalizeProjectSessionState(scope) {
|
|
271
|
+
if (!scope?.statePath) return
|
|
272
|
+
|
|
273
|
+
const currentDocument = readStateDocument(scope.statePath)
|
|
274
|
+
if (currentDocument.hasMetadata) {
|
|
275
|
+
if (currentDocument.metadata && typeof currentDocument.metadata === 'object' && !existsSync(scope.runtimePath)) {
|
|
276
|
+
writeJsonFileAtomic(scope.runtimePath, currentDocument.metadata)
|
|
277
|
+
}
|
|
278
|
+
writeStateDocument(scope.statePath, {
|
|
279
|
+
body: currentDocument.body,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const workspaceStatePath = scope.workspaceDir ? join(scope.workspaceDir, 'STATE.md') : ''
|
|
284
|
+
if (!workspaceStatePath || samePath(workspaceStatePath, scope.statePath) || !existsSync(workspaceStatePath)) return
|
|
285
|
+
|
|
286
|
+
const legacyDocument = readStateDocument(workspaceStatePath)
|
|
287
|
+
if (!existsSync(scope.statePath) && legacyDocument.body.trim()) {
|
|
288
|
+
writeStateDocument(scope.statePath, {
|
|
289
|
+
body: legacyDocument.body,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
if (legacyDocument.metadata && typeof legacyDocument.metadata === 'object' && !existsSync(scope.runtimePath)) {
|
|
293
|
+
writeJsonFileAtomic(scope.runtimePath, legacyDocument.metadata)
|
|
294
|
+
}
|
|
295
|
+
if (legacyDocument.hasMetadata) {
|
|
296
|
+
writeStateDocument(workspaceStatePath, {
|
|
297
|
+
body: legacyDocument.body,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
263
302
|
export function ensureProjectLocalRuntime(cwd, options = {}) {
|
|
264
303
|
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
265
304
|
const localDir = getProjectLocalDir(normalizedCwd)
|
|
@@ -271,6 +310,7 @@ export function ensureProjectLocalRuntime(cwd, options = {}) {
|
|
|
271
310
|
if (!existsSync(scope.statePath)) {
|
|
272
311
|
writeFileSync(scope.statePath, `${buildInitialStateSnapshot(options.stateSeed || {})}\n`, 'utf-8')
|
|
273
312
|
}
|
|
313
|
+
normalizeProjectSessionState(scope)
|
|
274
314
|
|
|
275
315
|
return scope
|
|
276
316
|
}
|
|
@@ -333,6 +373,10 @@ function resolveEnvSessionToken(env = process.env) {
|
|
|
333
373
|
return resolveProjectSessionToken({ payload: {}, env })
|
|
334
374
|
}
|
|
335
375
|
|
|
376
|
+
function resolveEnvSessionAliasToken(env = process.env) {
|
|
377
|
+
return resolveProjectSessionAliasToken({ env })
|
|
378
|
+
}
|
|
379
|
+
|
|
336
380
|
function resolveTransientSessionToken({ payload = {}, env = process.env, ppid = process.ppid } = {}) {
|
|
337
381
|
return resolveSessionToken({
|
|
338
382
|
payload,
|
|
@@ -342,10 +386,24 @@ function resolveTransientSessionToken({ payload = {}, env = process.env, ppid =
|
|
|
342
386
|
})
|
|
343
387
|
}
|
|
344
388
|
|
|
389
|
+
function buildScopedSessionToken(kind = '', raw = '') {
|
|
390
|
+
const normalizedKind = sanitizeRuntimeSegment(kind, 'session')
|
|
391
|
+
const value = sanitizeRuntimeSegment(String(raw || '').trim(), '')
|
|
392
|
+
if (!value) return ''
|
|
393
|
+
return `${normalizedKind}-${value}`
|
|
394
|
+
}
|
|
395
|
+
|
|
345
396
|
function getActiveSessionPath(activationDir) {
|
|
346
397
|
return join(activationDir, PROJECT_SESSIONS_DIR_NAME, ACTIVE_SESSION_FILE_NAME)
|
|
347
398
|
}
|
|
348
399
|
|
|
400
|
+
function removeLegacySessionPointersFile(activationDir) {
|
|
401
|
+
if (!activationDir) return
|
|
402
|
+
try {
|
|
403
|
+
rmSync(join(activationDir, PROJECT_SESSIONS_DIR_NAME, LEGACY_SESSION_POINTERS_FILE_NAME), { force: true })
|
|
404
|
+
} catch {}
|
|
405
|
+
}
|
|
406
|
+
|
|
349
407
|
function resolveActiveSessionToken({ activationDir, projectRoot, workspace, now = Date.now() } = {}) {
|
|
350
408
|
const active = readJsonFile(getActiveSessionPath(activationDir), null)
|
|
351
409
|
if (!active || typeof active !== 'object') return ''
|
|
@@ -377,14 +435,15 @@ function resolveActiveAliasSession({ activationDir, projectRoot, workspace, alia
|
|
|
377
435
|
}
|
|
378
436
|
|
|
379
437
|
export function writeActiveProjectSession(scope, { host = '', source = '', env = process.env } = {}) {
|
|
380
|
-
if (!scope?.active || !scope.activationDir || !scope.
|
|
438
|
+
if (!scope?.active || !scope.activationDir || !scope.workspace) return ''
|
|
381
439
|
|
|
382
440
|
const activePath = getActiveSessionPath(scope.activationDir)
|
|
383
441
|
const current = readJsonFile(activePath, null) || {}
|
|
384
442
|
const aliases = current.aliases && typeof current.aliases === 'object' ? current.aliases : {}
|
|
385
443
|
const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
|
|
386
444
|
if (envToken && envToken !== scope.session) aliases[envToken] = scope.session
|
|
387
|
-
|
|
445
|
+
const aliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
|
|
446
|
+
if (aliasToken && aliasToken !== scope.session) aliases[aliasToken] = scope.session
|
|
388
447
|
writeJsonFileAtomic(activePath, {
|
|
389
448
|
version: 1,
|
|
390
449
|
cwd: scope.cwd,
|
|
@@ -402,9 +461,14 @@ export function writeActiveProjectSession(scope, { host = '', source = '', env =
|
|
|
402
461
|
|
|
403
462
|
function chooseProjectSession({ payload, env, activationDir, projectRoot, workspace }) {
|
|
404
463
|
const payloadToken = sanitizeRuntimeSegment(resolvePayloadSessionToken(payload), '')
|
|
405
|
-
if (payloadToken) return { session: payloadToken, sessionMode: 'host-session' }
|
|
406
|
-
|
|
407
464
|
const payloadAlias = sanitizeRuntimeSegment(payload?._helloagentsSessionAlias, '')
|
|
465
|
+
if (payloadToken) {
|
|
466
|
+
return {
|
|
467
|
+
session: buildScopedSessionToken('host', payloadToken),
|
|
468
|
+
sessionMode: 'host-session',
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
408
472
|
const payloadAliasToken = resolveActiveAliasSession({
|
|
409
473
|
activationDir,
|
|
410
474
|
projectRoot,
|
|
@@ -412,8 +476,15 @@ function chooseProjectSession({ payload, env, activationDir, projectRoot, worksp
|
|
|
412
476
|
alias: payloadAlias,
|
|
413
477
|
})
|
|
414
478
|
if (payloadAliasToken) return { session: payloadAliasToken, sessionMode: 'active-session' }
|
|
479
|
+
if (payloadAlias) {
|
|
480
|
+
return {
|
|
481
|
+
session: buildScopedSessionToken('alias', payloadAlias),
|
|
482
|
+
sessionMode: 'alias-session',
|
|
483
|
+
}
|
|
484
|
+
}
|
|
415
485
|
|
|
416
486
|
const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
|
|
487
|
+
const envAliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
|
|
417
488
|
const aliasToken = resolveActiveAliasSession({
|
|
418
489
|
activationDir,
|
|
419
490
|
projectRoot,
|
|
@@ -422,12 +493,28 @@ function chooseProjectSession({ payload, env, activationDir, projectRoot, worksp
|
|
|
422
493
|
})
|
|
423
494
|
if (aliasToken) return { session: aliasToken, sessionMode: 'active-session' }
|
|
424
495
|
|
|
425
|
-
if (envToken)
|
|
496
|
+
if (envToken) {
|
|
497
|
+
return {
|
|
498
|
+
session: buildScopedSessionToken('host', envToken),
|
|
499
|
+
sessionMode: 'host-session',
|
|
500
|
+
}
|
|
501
|
+
}
|
|
426
502
|
|
|
427
|
-
|
|
428
|
-
|
|
503
|
+
if (envAliasToken) {
|
|
504
|
+
const activeAliasToken = resolveActiveAliasSession({
|
|
505
|
+
activationDir,
|
|
506
|
+
projectRoot,
|
|
507
|
+
workspace,
|
|
508
|
+
alias: envAliasToken,
|
|
509
|
+
})
|
|
510
|
+
if (activeAliasToken) return { session: activeAliasToken, sessionMode: 'active-session' }
|
|
511
|
+
return {
|
|
512
|
+
session: buildScopedSessionToken('alias', envAliasToken),
|
|
513
|
+
sessionMode: 'alias-session',
|
|
514
|
+
}
|
|
515
|
+
}
|
|
429
516
|
|
|
430
|
-
return { session:
|
|
517
|
+
return { session: '', sessionMode: 'unidentified' }
|
|
431
518
|
}
|
|
432
519
|
|
|
433
520
|
function removeLegacyProjectArtifacts(activationDir) {
|
|
@@ -445,6 +532,7 @@ export function getProjectSessionScope(cwd, options = {}) {
|
|
|
445
532
|
const { payload = {}, env = process.env } = normalizeRuntimeOptions(options)
|
|
446
533
|
const activationDir = getProjectActivationDir(projectRoot)
|
|
447
534
|
removeLegacyProjectArtifacts(activationDir)
|
|
535
|
+
removeLegacySessionPointersFile(activationDir)
|
|
448
536
|
const workspace = resolveWorkspaceName(projectRoot)
|
|
449
537
|
const { session, sessionMode } = chooseProjectSession({
|
|
450
538
|
payload,
|
|
@@ -453,7 +541,8 @@ export function getProjectSessionScope(cwd, options = {}) {
|
|
|
453
541
|
projectRoot,
|
|
454
542
|
workspace,
|
|
455
543
|
})
|
|
456
|
-
const
|
|
544
|
+
const workspaceDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME, workspace)
|
|
545
|
+
const sessionDir = session ? join(workspaceDir, session) : join(workspaceDir, DEFAULT_STATE_SESSION_TOKEN)
|
|
457
546
|
|
|
458
547
|
return {
|
|
459
548
|
cwd: projectRoot,
|
|
@@ -464,10 +553,12 @@ export function getProjectSessionScope(cwd, options = {}) {
|
|
|
464
553
|
sessionMode,
|
|
465
554
|
activationDir,
|
|
466
555
|
sessionDir,
|
|
556
|
+
workspaceDir,
|
|
467
557
|
statePath: join(sessionDir, 'STATE.md'),
|
|
468
558
|
eventsPath: join(sessionDir, EVENTS_FILE_NAME),
|
|
469
559
|
artifactsDir: join(sessionDir, PROJECT_ARTIFACTS_DIR_NAME),
|
|
470
|
-
|
|
560
|
+
runtimePath: join(sessionDir, PROJECT_RUNTIME_FILE_NAME),
|
|
561
|
+
key: `${projectRoot}::${workspace}::${session || DEFAULT_STATE_SESSION_TOKEN}`,
|
|
471
562
|
}
|
|
472
563
|
}
|
|
473
564
|
|
|
@@ -497,6 +588,7 @@ function buildTransientRuntimeDir(cwd, options = {}) {
|
|
|
497
588
|
statePath: join(getUserRuntimeRoot(), hash, 'STATE.md'),
|
|
498
589
|
eventsPath: join(getUserRuntimeRoot(), hash, EVENTS_FILE_NAME),
|
|
499
590
|
artifactsDir: join(getUserRuntimeRoot(), hash, PROJECT_ARTIFACTS_DIR_NAME),
|
|
591
|
+
runtimePath: join(getUserRuntimeRoot(), hash, PROJECT_RUNTIME_FILE_NAME),
|
|
500
592
|
key: `${normalizedCwd}::transient::${token}`,
|
|
501
593
|
}
|
|
502
594
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { basename, dirname, join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -31,6 +31,79 @@ function buildEmptyCapsule(scope) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function readRuntimeDocument(filePath) {
|
|
35
|
+
const payload = readJsonFile(filePath, null)
|
|
36
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
return payload
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeRuntimeDocument(filePath, payload) {
|
|
43
|
+
writeJsonFileAtomic(filePath, payload)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isSamePath(left = '', right = '') {
|
|
47
|
+
if (process.platform === 'win32') {
|
|
48
|
+
return left.toLowerCase() === right.toLowerCase()
|
|
49
|
+
}
|
|
50
|
+
return left === right
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isSeedOnlyState(body = '') {
|
|
54
|
+
return String(body || '').includes('由运行时自动创建;后续按实际任务重写')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function looksLikeLegacyFlattenedSessionDir(entryName = '') {
|
|
58
|
+
return /^[a-z0-9]{8}$/i.test(String(entryName || '').trim())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function migrateLegacyProjectScope(scope) {
|
|
62
|
+
if (scope.scope !== 'project-session') return
|
|
63
|
+
const workspaceDir = scope.workspaceDir || join(scope.activationDir, 'sessions', scope.workspace || scope.branch)
|
|
64
|
+
const legacyStatePath = join(workspaceDir, 'STATE.md')
|
|
65
|
+
const legacyRuntimePath = join(workspaceDir, 'runtime.json')
|
|
66
|
+
if (isSamePath(workspaceDir, scope.sessionDir)) return
|
|
67
|
+
|
|
68
|
+
const currentDocument = readStateDocument(scope.statePath)
|
|
69
|
+
const currentCapsule = currentDocument.metadata && typeof currentDocument.metadata === 'object'
|
|
70
|
+
? currentDocument.metadata
|
|
71
|
+
: null
|
|
72
|
+
const legacyDocument = readStateDocument(legacyStatePath)
|
|
73
|
+
const legacyCapsule = readRuntimeDocument(legacyRuntimePath)
|
|
74
|
+
const shouldNormalizeCurrentBody = currentDocument.hasMetadata
|
|
75
|
+
const shouldWriteBody = (!currentDocument.body.trim() && legacyDocument.body.trim()) || shouldNormalizeCurrentBody
|
|
76
|
+
const shouldWriteRuntime = (legacyCapsule || currentCapsule) && !readRuntimeDocument(scope.runtimePath)
|
|
77
|
+
|
|
78
|
+
if (shouldWriteBody) {
|
|
79
|
+
writeStateDocument(scope.statePath, {
|
|
80
|
+
body: currentDocument.body.trim() ? currentDocument.body : legacyDocument.body,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
if (shouldWriteRuntime) {
|
|
84
|
+
writeRuntimeDocument(scope.runtimePath, legacyCapsule || currentCapsule)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (existsSync(legacyStatePath) && shouldWriteBody) {
|
|
88
|
+
const legacyCurrent = readStateDocument(legacyStatePath)
|
|
89
|
+
if (legacyCurrent.hasMetadata) {
|
|
90
|
+
writeStateDocument(legacyStatePath, {
|
|
91
|
+
body: legacyCurrent.body,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (existsSync(legacyRuntimePath) && shouldWriteRuntime) {
|
|
96
|
+
rmSync(legacyRuntimePath, { force: true })
|
|
97
|
+
}
|
|
98
|
+
if (existsSync(workspaceDir)) {
|
|
99
|
+
for (const entry of readdirSync(workspaceDir, { withFileTypes: true })) {
|
|
100
|
+
if (!entry.isDirectory()) continue
|
|
101
|
+
if (!looksLikeLegacyFlattenedSessionDir(entry.name)) continue
|
|
102
|
+
rmSync(join(workspaceDir, entry.name), { recursive: true, force: true })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
34
107
|
function normalizeOptions(options = {}) {
|
|
35
108
|
if (!options || typeof options !== 'object') return {}
|
|
36
109
|
if (options.payload && typeof options.payload === 'object') return options
|
|
@@ -84,7 +157,7 @@ function shouldMaterializeSessionState(options = {}) {
|
|
|
84
157
|
}
|
|
85
158
|
|
|
86
159
|
export function getSessionCapsulePath(cwd = process.cwd(), options = {}) {
|
|
87
|
-
return getScope(cwd, options).
|
|
160
|
+
return getScope(cwd, options).runtimePath
|
|
88
161
|
}
|
|
89
162
|
|
|
90
163
|
export function getSessionEventsPath(cwd = process.cwd(), options = {}) {
|
|
@@ -102,15 +175,15 @@ export function getSessionArtifactPath(cwd, fileName, options = {}) {
|
|
|
102
175
|
export function getSessionArtifactRelativePath(cwd, fileName, options = {}) {
|
|
103
176
|
const scope = getScope(cwd, options)
|
|
104
177
|
if (scope.scope === 'project-session') {
|
|
105
|
-
return `.helloagents/sessions/${scope.workspace || scope.branch}/${scope.session}/artifacts/${fileName}`
|
|
178
|
+
return `.helloagents/sessions/${scope.workspace || scope.branch}/${scope.session || 'default'}/artifacts/${fileName}`
|
|
106
179
|
}
|
|
107
180
|
return `~/.helloagents/runtime/${basename(scope.sessionDir)}/artifacts/${fileName}`
|
|
108
181
|
}
|
|
109
182
|
|
|
110
183
|
export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
111
184
|
const scope = getScope(cwd, options)
|
|
112
|
-
|
|
113
|
-
const capsule =
|
|
185
|
+
migrateLegacyProjectScope(scope)
|
|
186
|
+
const capsule = readRuntimeDocument(scope.runtimePath)
|
|
114
187
|
if (!capsule || Array.isArray(capsule)) return buildEmptyCapsule(scope)
|
|
115
188
|
return {
|
|
116
189
|
...buildEmptyCapsule(scope),
|
|
@@ -128,6 +201,7 @@ export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
|
128
201
|
export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
129
202
|
const normalizedOptions = normalizeOptions(options)
|
|
130
203
|
const scope = getScope(cwd, normalizedOptions)
|
|
204
|
+
migrateLegacyProjectScope(scope)
|
|
131
205
|
const shouldMaterialize = shouldMaterializeSessionState(normalizedOptions)
|
|
132
206
|
const currentDocument = readStateDocument(scope.statePath)
|
|
133
207
|
const hasBody = Boolean(currentDocument.body && currentDocument.body.trim())
|
|
@@ -165,10 +239,12 @@ export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
|
165
239
|
sessionMode: scope.sessionMode,
|
|
166
240
|
updatedAt: new Date().toISOString(),
|
|
167
241
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
242
|
+
writeRuntimeDocument(scope.runtimePath, nextCapsule)
|
|
243
|
+
if (hasBody) {
|
|
244
|
+
writeStateDocument(scope.statePath, {
|
|
245
|
+
body: currentDocument.body,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
172
248
|
writeActiveProjectSession(scope, {
|
|
173
249
|
env: normalizedOptions.env,
|
|
174
250
|
})
|
|
@@ -197,8 +273,8 @@ export function writeCapsuleSection(cwd, section, value, options = {}) {
|
|
|
197
273
|
}
|
|
198
274
|
|
|
199
275
|
export function clearCapsuleSection(cwd, section, options = {}) {
|
|
200
|
-
const
|
|
201
|
-
if (!existsSync(
|
|
276
|
+
const runtimePath = getSessionCapsulePath(cwd, options)
|
|
277
|
+
if (!existsSync(runtimePath)) return false
|
|
202
278
|
|
|
203
279
|
const capsule = readSessionCapsule(cwd, options)
|
|
204
280
|
if (!Object.prototype.hasOwnProperty.call(capsule, section)) return false
|
|
@@ -289,7 +365,11 @@ export function clearSessionArtifact(cwd, fileName, options = {}) {
|
|
|
289
365
|
}
|
|
290
366
|
|
|
291
367
|
export function removeSessionCapsule(cwd, options = {}) {
|
|
292
|
-
|
|
368
|
+
const scope = getScope(cwd, options)
|
|
369
|
+
removeRuntimeFile(scope.runtimePath)
|
|
370
|
+
if (scope.scope !== 'project-session') {
|
|
371
|
+
removeRuntimeFile(scope.statePath)
|
|
372
|
+
}
|
|
293
373
|
}
|
|
294
374
|
|
|
295
375
|
function shouldRecordSessionEvents(options = {}) {
|
|
@@ -45,6 +45,17 @@ const PROJECT_ENV_SESSION_KEYS = [
|
|
|
45
45
|
'HELLOAGENTS_NOTIFY_SESSION_ID',
|
|
46
46
|
]
|
|
47
47
|
|
|
48
|
+
const PROJECT_ALIAS_ENV_SESSION_KEYS = [
|
|
49
|
+
'HELLOAGENTS_NOTIFY_SESSION_ID',
|
|
50
|
+
'WT_SESSION',
|
|
51
|
+
'TERM_SESSION_ID',
|
|
52
|
+
'KITTY_WINDOW_ID',
|
|
53
|
+
'ALACRITTY_WINDOW_ID',
|
|
54
|
+
'WINDOWID',
|
|
55
|
+
'WEZTERM_PANE',
|
|
56
|
+
'TAB_ID',
|
|
57
|
+
]
|
|
58
|
+
|
|
48
59
|
function readStringCandidate(input, key) {
|
|
49
60
|
if (!input || typeof input !== 'object') return ''
|
|
50
61
|
const value = input[key]
|
|
@@ -99,9 +110,16 @@ export function resolveProjectSessionToken({
|
|
|
99
110
|
return resolveTokenFromKeys(env, PROJECT_ENV_SESSION_KEYS)
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
export function resolveProjectSessionAliasToken({
|
|
114
|
+
env = process.env,
|
|
115
|
+
} = {}) {
|
|
116
|
+
return resolveTokenFromKeys(env, PROJECT_ALIAS_ENV_SESSION_KEYS)
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
export {
|
|
103
120
|
ENV_SESSION_KEYS,
|
|
104
121
|
PAYLOAD_SESSION_KEYS,
|
|
122
|
+
PROJECT_ALIAS_ENV_SESSION_KEYS,
|
|
105
123
|
PROJECT_ENV_SESSION_KEYS,
|
|
106
124
|
PROJECT_PAYLOAD_SESSION_KEYS,
|
|
107
125
|
}
|
|
@@ -3,6 +3,7 @@ import { dirname } from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
const STATE_META_BEGIN = '<!-- HELLOAGENTS_STATE_META'
|
|
5
5
|
const STATE_META_END = 'HELLOAGENTS_STATE_META -->'
|
|
6
|
+
export const AUTO_CREATED_STATE_MARKER = '由运行时自动创建;后续按实际任务重写'
|
|
6
7
|
|
|
7
8
|
function normalizeText(content = '') {
|
|
8
9
|
return String(content || '').replace(/^\uFEFF/, '')
|
|
@@ -62,13 +63,11 @@ export function readStateDocument(filePath) {
|
|
|
62
63
|
|
|
63
64
|
export function composeStateDocument({ metadata = {}, body = '' } = {}) {
|
|
64
65
|
const normalizedBody = normalizeText(body).replace(/^\n+/, '')
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
normalizedBody,
|
|
71
|
-
].join('\n').replace(/\n+$/, '\n')
|
|
66
|
+
return normalizedBody ? `${normalizedBody.replace(/\n+$/, '')}\n` : ''
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function looksLikeAutoCreatedState(body = '') {
|
|
70
|
+
return normalizeText(body).includes(AUTO_CREATED_STATE_MARKER)
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
export function writeStateDocument(filePath, { metadata = {}, body = '' } = {}) {
|
package/scripts/turn-state.mjs
CHANGED
|
@@ -5,7 +5,6 @@ import { fileURLToPath } from 'node:url'
|
|
|
5
5
|
import {
|
|
6
6
|
appendSessionEvent,
|
|
7
7
|
clearCapsuleSection,
|
|
8
|
-
getSessionCapsulePath,
|
|
9
8
|
getRuntimeScope,
|
|
10
9
|
readCapsuleSection,
|
|
11
10
|
writeCapsuleSection,
|
|
@@ -108,7 +107,7 @@ export function readTurnState(cwd = process.cwd(), { now = Date.now(), ...option
|
|
|
108
107
|
return {
|
|
109
108
|
cwd: normalizePath(entry.cwd),
|
|
110
109
|
key: entry.key || '',
|
|
111
|
-
path:
|
|
110
|
+
path: getRuntimeScope(cwd, options).statePath,
|
|
112
111
|
updatedAt: entry.updatedAt,
|
|
113
112
|
...normalized,
|
|
114
113
|
}
|
|
@@ -288,7 +287,7 @@ function main() {
|
|
|
288
287
|
const payload = writeTurnState(cwd, input)
|
|
289
288
|
process.stdout.write(JSON.stringify({
|
|
290
289
|
suppressOutput: true,
|
|
291
|
-
path:
|
|
290
|
+
path: getRuntimeScope(cwd, input).statePath,
|
|
292
291
|
payload,
|
|
293
292
|
}))
|
|
294
293
|
return
|
|
@@ -15,10 +15,7 @@ export function getTargetPlans(snapshot) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function describeStateLabel(state) {
|
|
18
|
-
|
|
19
|
-
return '当前工作区默认位置的状态文件'
|
|
20
|
-
}
|
|
21
|
-
return '当前会话的状态文件'
|
|
18
|
+
return '当前工作区状态文件'
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
export function classifyPlan(plan) {
|
|
@@ -188,7 +188,7 @@ export function readStateSnapshot(cwd, options = {}) {
|
|
|
188
188
|
stateSessionToken: stateScope.stateSessionToken,
|
|
189
189
|
stateSessionMode: stateScope.stateSessionMode,
|
|
190
190
|
stateWorkspace: stateScope.stateWorkspace,
|
|
191
|
-
sessionScoped:
|
|
191
|
+
sessionScoped: true,
|
|
192
192
|
exists,
|
|
193
193
|
content,
|
|
194
194
|
sections,
|