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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.37",
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.37",
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
- [![Version](https://img.shields.io/badge/version-3.0.37-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.0.38-orange.svg)](./package.json)
12
12
  [![npm](https://img.shields.io/npm/v/helloagents.svg)](https://www.npmjs.com/package/helloagents)
13
13
  [![Node](https://img.shields.io/badge/node-%3E%3D18-339933.svg)](./package.json)
14
14
  [![Skills](https://img.shields.io/badge/skills-14-6366f1.svg)](./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
- - without a stable session id: `.helloagents/sessions/<workspace>/default/STATE.md`
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 records the active session index.
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 now only uses stable host identifiers such as `sessionId`, `conversationId`, `threadId`, or `HELLOAGENTS_NOTIFY_SESSION_ID`. It no longer uses terminal/window ids such as `WT_SESSION`, `TERM_SESSION_ID`, or `WINDOWID` to create extra project session directories.
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
- `turn-state`, route context, and the artifact index are stored inside `STATE.md` metadata instead of a separate `capsule.json`. `events.jsonl` is opt-in trace output, not a default runtime file.
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/.../STATE.md` automatically instead of keeping that task only in the user-level transient runtime.
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-scoped `state_path`, runtime signals, and evidence
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
- [![Version](https://img.shields.io/badge/version-3.0.37-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.0.38-orange.svg)](./package.json)
12
12
  [![npm](https://img.shields.io/npm/v/helloagents.svg)](https://www.npmjs.com/package/helloagents)
13
13
  [![Node](https://img.shields.io/badge/node-%3E%3D18-339933.svg)](./package.json)
14
14
  [![Skills](https://img.shields.io/badge/skills-14-6366f1.svg)](./skills)
@@ -184,12 +184,12 @@ HelloAGENTS 可以在 `.helloagents/` 下创建和维护项目知识库。
184
184
 
185
185
  HelloAGENTS 现在只从 `state_path` 解析当前状态文件:
186
186
 
187
- - 宿主提供稳定会话标识时:`.helloagents/sessions/<workspace>/<session>/STATE.md`
188
- - 宿主未提供稳定会话标识时:`.helloagents/sessions/<workspace>/default/STATE.md`
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`。`.helloagents/sessions/active.json` 只记录当前活跃会话索引。
190
+ `<workspace>` 是当前 Git 分支、detached HEAD 的 `detached-<sha>`,或非 Git 项目的 `workspace`。`<session>` 是当前项目本地会话标识。`.helloagents/sessions/active.json` 只保留最近一次活跃的工作区/会话映射和 alias 桥接,这样同一个 CLI 会话会稳定落在同一个目录里,`/resume` 也能复用它。
191
191
 
192
- 对于项目本地会话目录,HelloAGENTS 现在只使用稳定宿主标识,如 `sessionId`、`conversationId`、`threadId` 或 `HELLOAGENTS_NOTIFY_SESSION_ID`。它不再使用 `WT_SESSION`、`TERM_SESSION_ID`、`WINDOWID` 这类终端或窗口标识去额外生成项目会话目录。
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
- `turn-state`、路由上下文和 artifact 索引都写进 `STATE.md` 的元数据,不再单独生成 `capsule.json`。`events.jsonl` 改为可选 trace 输出,默认不写。
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/.../STATE.md`,而不是只停留在用户级临时运行态。
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
- - 会话级 `state_path`、运行态信号和证据
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 索引写入这个文件的元数据,`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` 只记录当前活跃会话索引,避免同一会话被拆成多个目录
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 索引写入这个文件的元数据,`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` 只记录当前活跃会话索引,避免同一会话被拆成多个目录
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
-
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.37",
3
+ "version": "3.0.38",
4
4
  "description": "Quality-driven orchestration kernel for AI CLIs",
5
5
  "contextFileName": "bootstrap.md",
6
6
  "author": "HelloWind",
package/install.ps1 CHANGED
@@ -124,7 +124,9 @@ switch ($Action) {
124
124
  Invoke-Npm -NpmArgs @("install", "-g", "helloagents")
125
125
  }
126
126
  }
127
- Sync-Hosts
127
+ if ($HasExplicitTarget) {
128
+ Sync-Hosts
129
+ }
128
130
  }
129
131
  "cleanup" {
130
132
  Cleanup-Hosts
package/install.sh CHANGED
@@ -144,7 +144,9 @@ case "$ACTION" in
144
144
  else
145
145
  npm update -g helloagents || npm install -g helloagents
146
146
  fi
147
- sync_hosts
147
+ if [ "$HAS_EXPLICIT_TARGET" -eq 1 ]; then
148
+ sync_hosts
149
+ fi
148
150
  ;;
149
151
  cleanup)
150
152
  cleanup_hosts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.37",
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",
@@ -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 shouldKeepSession(active, workspace, session) {
39
+ function shouldKeepNestedSession(active, workspace, sessionName) {
42
40
  const activeWorkspace = active.workspace || active.branch || ''
43
- return activeWorkspace === workspace && active.session === session
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 { metadata, body } = readStateDocument(statePath)
69
- if (metadata && typeof metadata === 'object' && Object.keys(metadata).length > 0) return false
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
- for (const sessionEntry of readdirSync(workspaceDir, { withFileTypes: true })) {
162
- if (!sessionEntry.isDirectory()) continue
163
- const sessionDir = join(workspaceDir, sessionEntry.name)
164
- if (shouldKeepSession(active, workspaceEntry.name, sessionEntry.name)) continue
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
- } else if (!hasStateSnapshot(sessionDir)) {
165
+ continue
166
+ }
167
+ if (!hasStateSnapshot(sessionDir)) {
170
168
  removePath(sessionDir, result, 'removedNoStateDirs')
171
- } else if (isAutoCreatedSeedSession(sessionDir)) {
169
+ continue
170
+ }
171
+ if (isAutoCreatedSeedSession(sessionDir)) {
172
172
  removePath(sessionDir, result, 'removedSeedDirs')
173
- } else if (isStaleStateSession(sessionDir, now, maxAgeMs)) {
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 { resolveProjectSessionToken, resolveSessionToken } from './session-token.mjs'
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.session) return ''
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) return { session: envToken, sessionMode: 'host-session' }
496
+ if (envToken) {
497
+ return {
498
+ session: buildScopedSessionToken('host', envToken),
499
+ sessionMode: 'host-session',
500
+ }
501
+ }
426
502
 
427
- const activeToken = resolveActiveSessionToken({ activationDir, projectRoot, workspace })
428
- if (activeToken) return { session: activeToken, sessionMode: 'active-session' }
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: DEFAULT_STATE_SESSION_TOKEN, sessionMode: 'default' }
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 sessionDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME, workspace, session)
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
- key: `${projectRoot}::${workspace}::${session}`,
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).statePath
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
- const { metadata } = readStateDocument(scope.statePath)
113
- const capsule = metadata && typeof metadata === 'object' ? metadata : null
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
- writeStateDocument(scope.statePath, {
169
- metadata: nextCapsule,
170
- body: currentDocument.body,
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 statePath = getSessionCapsulePath(cwd, options)
201
- if (!existsSync(statePath)) return false
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
- removeRuntimeFile(getSessionCapsulePath(cwd, options))
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
- STATE_META_BEGIN,
67
- JSON.stringify(metadata, null, 2),
68
- STATE_META_END,
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 = '' } = {}) {
@@ -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: getSessionCapsulePath(cwd, options),
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: getSessionCapsulePath(cwd, input),
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
- if (state.stateSessionMode === 'default') {
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: stateScope.stateScope === 'session',
191
+ sessionScoped: true,
192
192
  exists,
193
193
  content,
194
194
  sections,