helloagents 3.0.38 → 3.0.39

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.38",
3
+ "version": "3.0.39",
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.38",
3
+ "version": "3.0.39",
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.38-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.0.39-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)
@@ -189,7 +189,7 @@ HelloAGENTS now resolves the current state file from `state_path`:
189
189
 
190
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 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.
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. If a session starts before a stable host identifier is available, HelloAGENTS can begin in `default` and keep reusing that same active directory after the same CLI session later exposes a stable identifier, instead of splitting into a second session directory.
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
 
@@ -210,7 +210,7 @@ Runtime state now stays intentionally small:
210
210
  - `~/.codex/.helloagents/notify-state.json` for Codex-native closeout de-duplication only
211
211
 
212
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.
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.
213
+ Project-local `STATE.md` is now materialized more lazily.
214
214
 
215
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.
216
216
 
@@ -660,7 +660,7 @@ Codex is rules-file driven by default.
660
660
  - standby creates `~/.codex/helloagents -> ~/.helloagents/helloagents`
661
661
  - global mode installs the native local-plugin chain, but keeps `~/.helloagents/helloagents` as the single managed runtime source by linking plugin roots, plugin cache, and `~/.codex/helloagents` back to it
662
662
  - for Codex app/plugin discovery, `global` is the native path; `standby` remains the lighter default for explicit project work
663
- - cleanup removes only the HelloAGENTS-managed hook trust entries and legacy managed notify residues, while keeping user-owned hook state untouched
663
+ - cleanup removes only the HelloAGENTS-managed hook trust entries, while keeping user-owned hook state untouched
664
664
  - Codex hooks only synchronize runtime state and enforce Stop gates; they do not inject HelloAGENTS rules or route text through hook output
665
665
  - Codex closeout de-duplicates Stop hooks and native `codex-notify`, so one turn does not notify twice, and clientless delegated child-completion events stay silent when the managed Stop hook is active
666
666
  - `/goal` remains Codex-native. Enable it explicitly with `helloagents codex goals enable` when long-running plan execution is needed
@@ -681,7 +681,7 @@ The current suite covers:
681
681
  - one-shot shell and PowerShell lifecycle dispatch, plus wrapper env cleanup and mode-routing rules for install, update, cleanup, uninstall, and branch switching
682
682
  - Claude, Gemini, and Codex host integration behavior, including global-to-standby cleanup and failed native cleanup tracking
683
683
  - Codex managed `model_instructions_file`, `notify`, `hooks.json`, hook trust state, local plugin, marketplace, and cache behavior
684
- - Codex cleanup of legacy managed notify variants on Windows and canonical managed notify restoration rules
684
+ - Codex cleanup and canonical managed notify restoration rules
685
685
  - Codex `/goal` feature toggles, long-running route context, and goal-aware command contracts
686
686
  - `helloagents doctor`
687
687
  - project storage and `repo-shared` behavior
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.38-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.0.39-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)
@@ -189,7 +189,7 @@ HelloAGENTS 现在只从 `state_path` 解析当前状态文件:
189
189
 
190
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` 这类窗口或终端标识,HelloAGENTS 只把它们当作轻量 alias 桥接,并优先复用已映射的会话目录,而不是继续分裂出重复目录。
192
+ 对于项目本地会话目录,HelloAGENTS 会优先使用稳定宿主标识,如 `sessionId`、`conversationId`、`threadId` 或 `HELLOAGENTS_NOTIFY_SESSION_ID`。如果宿主只能提供 `WT_SESSION`、`TERM_SESSION_ID`、`WINDOWID` 这类窗口或终端标识,HelloAGENTS 只把它们当作轻量 alias 桥接,并优先复用已映射的会话目录,而不是继续分裂出重复目录。如果一个会话启动时还拿不到稳定宿主标识,HelloAGENTS 可以先落到 `default`,等同一个 CLI 会话后续拿到稳定标识时,仍继续复用这个活动目录,而不是再拆出第二个会话目录。
193
193
 
194
194
  `STATE.md` 只记录当前工作流做到哪里,不承担所有对话的统一记忆。Codex `/goal` 也不替代 `state_path`、`turn-state` 或本地证据文件;它只负责 Codex 侧的长程续跑。
195
195
 
@@ -210,7 +210,7 @@ HelloAGENTS 不把“命令通过”和“任务完成”简单画等号。交
210
210
  - 仅用于 Codex 原生收尾去重的 `~/.codex/.helloagents/notify-state.json`
211
211
 
212
212
  `STATE.md` 只保留给人看的恢复快照。`runtime.json` 只给机器用,只保存极少量运行态。`artifacts/*.json` 只保留结构化收据。`events.jsonl` 仍是可选 trace 输出,默认不写。
213
- 项目本地 `STATE.md` 现在会更晚创建;旧版残留的项目根 `.helloagents/artifacts/*.log` 也会自动清理,不再继续充当第二套历史系统。
213
+ 项目本地 `STATE.md` 现在会更晚创建。
214
214
 
215
215
  标准运行态证据和临时运行态现在默认 72 小时过期。只有工作流明确需要的长程 Codex goal 链路,才继续保留 720 小时上限。
216
216
 
@@ -664,7 +664,7 @@ Codex 默认走规则文件驱动。
664
664
  - 标准模式创建 `~/.codex/helloagents -> ~/.helloagents/helloagents`
665
665
  - 全局模式安装原生本地插件流程,但仍把 `~/.helloagents/helloagents` 作为唯一受管运行时源;插件根目录、插件缓存和 `~/.codex/helloagents` 都会回链到它
666
666
  - 如果你主要看重 Codex app / 插件发现链路,优先使用 `global`;如果你主要看重更轻量、更显式的项目工作流,保留 `standby`
667
- - 清理时只删除 HelloAGENTS 自己写入的 hook trust 条目和旧式受管 notify 残留,不影响用户已有的 hook 状态
667
+ - 清理时只删除 HelloAGENTS 自己写入的 hook trust 条目,不影响用户已有的 hook 状态
668
668
  - Codex hooks 只做静默运行态同步和 Stop 门禁,不通过 hook 注入 HelloAGENTS 规则或路由说明
669
669
  - Codex 收尾会对 Stop hook 和原生 `codex-notify` 去重,避免同一轮重复通知;受管 Stop hook 生效时,client 为空的委派子任务完成事件也会保持静默
670
670
  - `/goal` 保持 Codex 原生能力;需要长程执行时,用 `helloagents codex goals enable` 显式启用
@@ -685,7 +685,7 @@ npm test
685
685
  - shell 与 PowerShell 一键脚本分发链路,以及包装脚本在安装、更新、清理、卸载和分支切换中的环境清理与模式传递规则
686
686
  - Claude、Gemini、Codex 的宿主集成行为,包括全局切回标准模式的清理和原生清理失败时的模式保留
687
687
  - Codex 受管 `model_instructions_file`、`notify`、`hooks.json`、hook trust 状态、本地插件、marketplace 和缓存行为
688
- - Windows 下 Codex 旧式受管 notify 变体的清理,以及受管 notify 恢复规则
688
+ - Codex 清理链路,以及受管 notify 恢复规则
689
689
  - Codex `/goal` 功能开关、长程路由上下文和 goal 感知命令契约
690
690
  - `helloagents doctor`
691
691
  - 项目存储和 `repo-shared`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.38",
3
+ "version": "3.0.39",
4
4
  "description": "Quality-driven orchestration kernel for AI CLIs",
5
5
  "contextFileName": "bootstrap.md",
6
6
  "author": "HelloWind",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.38",
3
+ "version": "3.0.39",
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",
@@ -13,13 +13,8 @@ export const CODEX_MANAGED_TOML_COMMENT = '# helloagents-managed'
13
13
  export const CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH = '~/.codex/AGENTS.md'
14
14
  export const CODEX_MANAGED_NOTIFY_COMMAND = 'helloagents-js'
15
15
  export const CODEX_MANAGED_NOTIFY_VALUE = `["${CODEX_MANAGED_NOTIFY_COMMAND}", "codex-notify"]`
16
- const CODEX_MANAGED_NOTIFY_LEGACY_VALUES = [
17
- `["${CODEX_MANAGED_NOTIFY_COMMAND}.cmd", "codex-notify"]`,
18
- `["${CODEX_MANAGED_NOTIFY_COMMAND}.exe", "codex-notify"]`,
19
- ]
20
16
  export const CODEX_MANAGED_TUI_NOTIFICATIONS_VALUE = '["plan-mode-prompt"]'
21
17
  export const CODEX_HOOKS_FEATURE_KEY = 'hooks'
22
- export const CODEX_LEGACY_HOOKS_FEATURE_KEY = 'codex_hooks'
23
18
  export const CODEX_GOALS_FEATURE_KEY = 'goals'
24
19
  export const CODEX_MANAGED_GOALS_FEATURE_LINE = `${CODEX_GOALS_FEATURE_KEY} = true ${CODEX_MANAGED_TOML_COMMENT}`
25
20
  export const CODEX_MANAGED_GOALS_DISABLED_LINE = `${CODEX_GOALS_FEATURE_KEY} = false ${CODEX_MANAGED_TOML_COMMENT}`
@@ -105,10 +100,6 @@ export function readCodexHooksFeatureLine(text) {
105
100
  return readCodexFeatureLine(text, CODEX_HOOKS_FEATURE_KEY)
106
101
  }
107
102
 
108
- export function readLegacyCodexHooksFeatureLine(text) {
109
- return readCodexFeatureLine(text, CODEX_LEGACY_HOOKS_FEATURE_KEY)
110
- }
111
-
112
103
  export function readCodexGoalsFeatureLine(text) {
113
104
  return readCodexFeatureLine(text, CODEX_GOALS_FEATURE_KEY)
114
105
  }
@@ -146,15 +137,6 @@ export function removeCodexGoalsFeatureConfig(text) {
146
137
  )
147
138
  }
148
139
 
149
- export function removeLegacyManagedCodexHooksFeatureConfig(text) {
150
- return removeTomlSectionLine(
151
- text,
152
- CODEX_FEATURES_HEADER,
153
- CODEX_LEGACY_HOOKS_FEATURE_KEY,
154
- isManagedLegacyCodexHooksFeature,
155
- )
156
- }
157
-
158
140
  export function restoreCodexGoalsFeatureConfig(text, { codexGoalsLine = '' } = {}) {
159
141
  if (!codexGoalsLine) return normalizeToml(text)
160
142
  return upsertTomlSectionLine(
@@ -173,10 +155,7 @@ export function isManagedCodexModelInstruction(line = '') {
173
155
  export function isManagedCodexNotify(line = '') {
174
156
  const value = String(line || '').replace(/\\/g, '/')
175
157
  return value.includes(CODEX_MANAGED_TOML_COMMENT)
176
- && (
177
- value.includes(CODEX_MANAGED_NOTIFY_VALUE)
178
- || CODEX_MANAGED_NOTIFY_LEGACY_VALUES.some((entry) => value.includes(entry))
179
- )
158
+ && value.includes(CODEX_MANAGED_NOTIFY_VALUE)
180
159
  }
181
160
 
182
161
  export function isManagedCodexTuiNotifications(line = '') {
@@ -193,10 +172,6 @@ export function isManagedCodexHooksFeature(line = '') {
193
172
  return isManagedFeatureLine(line, CODEX_HOOKS_FEATURE_KEY)
194
173
  }
195
174
 
196
- export function isManagedLegacyCodexHooksFeature(line = '') {
197
- return isManagedFeatureLine(line, CODEX_LEGACY_HOOKS_FEATURE_KEY)
198
- }
199
-
200
175
  export function isManagedCodexGoalsFeature(line = '') {
201
176
  return isManagedFeatureLine(line, CODEX_GOALS_FEATURE_KEY)
202
177
  }
@@ -17,12 +17,9 @@ import {
17
17
  isManagedCodexGoalsFeature,
18
18
  isManagedCodexModelInstruction,
19
19
  isManagedCodexNotify,
20
- isManagedLegacyCodexHooksFeature,
21
20
  readCodexGoalsFeatureLine,
22
- readLegacyCodexHooksFeatureLine,
23
21
  removeCodexGoalsFeatureConfig,
24
22
  removeCodexManagedTuiConfig,
25
- removeLegacyManagedCodexHooksFeatureConfig,
26
23
  removeCodexPluginConfig,
27
24
  restoreCodexGoalsFeatureConfig,
28
25
  restoreCodexTopLevelConfig,
@@ -156,12 +153,10 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
156
153
  const currentModelInstructions = readTopLevelTomlLine(toml, 'model_instructions_file');
157
154
  const currentNotify = readTopLevelTomlBlock(toml, 'notify');
158
155
  const currentCodexGoalsFeature = readCodexGoalsFeatureLine(toml);
159
- const currentLegacyCodexHooksFeature = readLegacyCodexHooksFeatureLine(toml);
160
156
 
161
157
  const shouldRestoreModelInstructions = isManagedCodexModelInstruction(currentModelInstructions);
162
158
  const shouldRestoreNotify = isManagedCodexNotify(currentNotify);
163
159
  const shouldRestoreCodexGoalsFeature = isManagedCodexGoalsFeature(currentCodexGoalsFeature);
164
- const shouldRemoveLegacyCodexHooksFeature = isManagedLegacyCodexHooksFeature(currentLegacyCodexHooksFeature);
165
160
 
166
161
  if (removePluginConfig) {
167
162
  toml = removeCodexPluginConfig(toml);
@@ -170,9 +165,6 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
170
165
  toml = removeCodexGoalsFeatureConfig(toml);
171
166
  }
172
167
  toml = removeCodexManagedTuiConfig(toml);
173
- if (shouldRemoveLegacyCodexHooksFeature) {
174
- toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
175
- }
176
168
  if (shouldRestoreModelInstructions) {
177
169
  toml = removeTopLevelTomlLines(toml, (line) =>
178
170
  line.startsWith('model_instructions_file =') && isManagedCodexModelInstruction(line)).text;
@@ -220,7 +212,6 @@ export function installCodexStandby(home, pkgRoot) {
220
212
  modelInstructionsPath: CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
221
213
  });
222
214
  toml = installCodexManagedTuiConfig(toml);
223
- toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
224
215
  safeWrite(configPath, toml);
225
216
  installCodexStandaloneHooks(home, pkgRoot);
226
217
 
@@ -312,7 +303,6 @@ export function installCodexGlobal(home, pkgRoot) {
312
303
  modelInstructionsPath: CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
313
304
  });
314
305
  toml = installCodexManagedTuiConfig(toml);
315
- toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
316
306
  toml = upsertCodexPluginConfig(toml);
317
307
  safeWrite(configPath, toml);
318
308
  installCodexStandaloneHooks(home, pkgRoot);
@@ -9,7 +9,6 @@ import {
9
9
  CODEX_MANAGED_NOTIFY_VALUE,
10
10
  readCodexGoalsFeatureLine,
11
11
  readCodexHooksFeatureLine,
12
- readLegacyCodexHooksFeatureLine,
13
12
  } from './cli-codex-config.mjs'
14
13
  import {
15
14
  buildManagedCodexHookTrustEntries,
@@ -301,8 +300,6 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
301
300
  )
302
301
  const hooksFeatureLine = readCodexHooksFeatureLine(codexConfig)
303
302
  const goalsFeatureLine = readCodexGoalsFeatureLine(codexConfig)
304
- const legacyHooksFeatureLine = readLegacyCodexHooksFeatureLine(codexConfig)
305
-
306
303
  return {
307
304
  checks: {
308
305
  carrierMarker: (safeRead(join(codexDir, 'AGENTS.md')) || '').includes('HELLOAGENTS_START'),
@@ -319,10 +316,8 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
319
316
  modelInstructionsPathMatch: !!modelInstructionsLine && normalizePath(modelInstructionsLine).includes(`"${CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH}"`),
320
317
  codexNotify: codexConfig.includes('codex-notify'),
321
318
  notifyPathMatch: codexConfig.includes(CODEX_MANAGED_NOTIFY_VALUE),
322
- codexHooksFeature: !/^\s*hooks\s*=\s*false\b/.test(hooksFeatureLine)
323
- && !/^\s*codex_hooks\s*=\s*false\b/.test(legacyHooksFeatureLine),
319
+ codexHooksFeature: !/^\s*hooks\s*=\s*false\b/.test(hooksFeatureLine),
324
320
  codexGoalsFeature: /^\s*goals\s*=\s*true\b/.test(goalsFeatureLine),
325
- legacyCodexHooksFeature: Boolean(legacyHooksFeatureLine),
326
321
  standaloneHooks: JSON.stringify(codexHooks.hooks || {}).includes('helloagents'),
327
322
  standaloneHooksMatch: managedHooksMatch(codexHooks.hooks || {}, expectedHooks),
328
323
  managedHookTrust: expectedHookTrust.every((entry) => managedHookTrust.has(entry.key)),
@@ -369,7 +364,6 @@ export function inspectCodexDoctor(runtime, settings) {
369
364
  if (!checks.pluginVersionMatch && !pluginVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件根目录版本信息', 'Global plugin root version was not readable'))
370
365
  if (!checks.pluginCacheVersionMatch && !cacheVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件缓存版本信息', 'Global plugin cache version was not readable'))
371
366
  if (detectedMode !== 'none' && !checks.codexGoalsFeature) notes.push(runtime.msg('Codex /goal 未启用;如需长程执行,可运行 `helloagents codex goals enable`。', 'Codex /goal is not enabled; run `helloagents codex goals enable` if you need long-running goals.'))
372
- if (detectedMode !== 'none' && checks.legacyCodexHooksFeature) notes.push(runtime.msg('检测到旧版 `codex_hooks`;HelloAGENTS 只兼容 Codex 最新版,请移除旧 key。', 'Legacy `codex_hooks` was detected; HelloAGENTS targets latest Codex only, so remove the old key.'))
373
367
  if (!nativeDoctor.available) notes.push(runtime.msg('未检测到原生 `codex doctor`;当前仅检查 HelloAGENTS 受管覆盖层。', 'Native `codex doctor` was not available; only the HelloAGENTS managed overlay was checked.'))
374
368
 
375
369
  const status = summarizeDoctorStatus(issues, { trackedMode, detectedMode })
@@ -23,10 +23,6 @@ function removePath(filePath, result, bucket) {
23
23
  }
24
24
  }
25
25
 
26
- function isDebugLog(entryName = '') {
27
- return /\.log$/i.test(entryName)
28
- }
29
-
30
26
  function isDirectoryEmptyRecursive(dirPath) {
31
27
  const entries = readdirSync(dirPath, { withFileTypes: true })
32
28
  if (entries.length === 0) return true
@@ -92,32 +88,6 @@ function cleanupTransientSessionTemps(sessionsDir, result) {
92
88
  }
93
89
  }
94
90
 
95
- function cleanupLegacyProjectArtifacts(activationDir, result) {
96
- const artifactsDir = join(activationDir, 'artifacts')
97
- if (!existsSync(artifactsDir)) return
98
-
99
- let removableEntries = []
100
- try {
101
- removableEntries = readdirSync(artifactsDir, { withFileTypes: true })
102
- } catch (error) {
103
- result.errors.push(`${artifactsDir}: ${error.message}`)
104
- return
105
- }
106
-
107
- for (const entry of removableEntries) {
108
- if (!entry.isFile() || !isDebugLog(entry.name)) continue
109
- removePath(join(artifactsDir, entry.name), result, 'removedLegacyArtifacts')
110
- }
111
-
112
- try {
113
- if (isDirectoryEmptyRecursive(artifactsDir)) {
114
- removePath(artifactsDir, result, 'removedLegacyArtifacts')
115
- }
116
- } catch (error) {
117
- result.errors.push(`${artifactsDir}: ${error.message}`)
118
- }
119
- }
120
-
121
91
  export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs = 0, maxAgeMs = PROJECT_SESSION_MAX_AGE_MS } = {}) {
122
92
  const projectRoot = getProjectRoot(cwd)
123
93
  const activationDir = getProjectActivationDir(projectRoot)
@@ -131,7 +101,6 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
131
101
  removedNoStateDirs: [],
132
102
  removedSeedDirs: [],
133
103
  removedTempFiles: [],
134
- removedLegacyArtifacts: [],
135
104
  errors: [],
136
105
  skipped: false,
137
106
  }
@@ -150,7 +119,6 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
150
119
  } catch (error) {
151
120
  result.errors.push(`${sessionsDir}: ${error.message}`)
152
121
  }
153
- cleanupLegacyProjectArtifacts(activationDir, result)
154
122
 
155
123
  for (const workspaceEntry of readdirSync(sessionsDir, { withFileTypes: true })) {
156
124
  if (!workspaceEntry.isDirectory()) continue
@@ -5,14 +5,17 @@ import { dirname, join, normalize, resolve } from 'node:path'
5
5
  import { homedir } from 'node:os'
6
6
 
7
7
  import {
8
+ PROJECT_CONVERSATION_PAYLOAD_KEYS,
9
+ PROJECT_SESSION_PAYLOAD_KEYS,
10
+ PROJECT_THREAD_PAYLOAD_KEYS,
8
11
  resolveProjectSessionAliasToken,
9
12
  resolveProjectSessionToken,
10
13
  resolveSessionToken,
14
+ sanitizeSessionToken,
11
15
  } from './session-token.mjs'
12
16
  import { USER_RUNTIME_MAX_AGE_MS } from './runtime-ttl.mjs'
13
17
  import { cleanupUserRuntimeRoot, getUserRuntimeRoot } from './runtime-user-cleanup.mjs'
14
18
  import { FULL_CARRIER_PROFILE_MARKER } from './cli-utils.mjs'
15
- import { readStateDocument, writeStateDocument } from './state-document.mjs'
16
19
 
17
20
  export const PROJECT_DIR_NAME = '.helloagents'
18
21
  export const PROJECT_SESSIONS_DIR_NAME = 'sessions'
@@ -21,7 +24,6 @@ export const EVENTS_FILE_NAME = 'events.jsonl'
21
24
  export const ACTIVE_SESSION_FILE_NAME = 'active.json'
22
25
  export const PROJECT_RUNTIME_FILE_NAME = 'runtime.json'
23
26
  export const DEFAULT_STATE_SESSION_TOKEN = 'default'
24
- export const LEGACY_SESSION_POINTERS_FILE_NAME = 'session-pointers.json'
25
27
  export const USER_RUNTIME_DIR_NAME = 'runtime'
26
28
  export { cleanupUserRuntimeRoot, getUserRuntimeRoot, USER_RUNTIME_MAX_AGE_MS }
27
29
 
@@ -267,38 +269,6 @@ function buildInitialStateSnapshot({
267
269
  ].join('\n')
268
270
  }
269
271
 
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
-
302
272
  export function ensureProjectLocalRuntime(cwd, options = {}) {
303
273
  const normalizedCwd = normalizePath(cwd || process.cwd())
304
274
  const localDir = getProjectLocalDir(normalizedCwd)
@@ -310,7 +280,6 @@ export function ensureProjectLocalRuntime(cwd, options = {}) {
310
280
  if (!existsSync(scope.statePath)) {
311
281
  writeFileSync(scope.statePath, `${buildInitialStateSnapshot(options.stateSeed || {})}\n`, 'utf-8')
312
282
  }
313
- normalizeProjectSessionState(scope)
314
283
 
315
284
  return scope
316
285
  }
@@ -369,6 +338,31 @@ function resolvePayloadSessionToken(payload = {}) {
369
338
  })
370
339
  }
371
340
 
341
+ function readRawPayloadValue(payload = {}, key = '') {
342
+ if (!payload || typeof payload !== 'object') return ''
343
+ const value = payload[key]
344
+ if (typeof value === 'string') return value.trim()
345
+ if (typeof value === 'number') return String(value)
346
+ return ''
347
+ }
348
+
349
+ function readPayloadSessionIdentity(payload = {}) {
350
+ const groups = [
351
+ ['session', PROJECT_SESSION_PAYLOAD_KEYS],
352
+ ['conversation', PROJECT_CONVERSATION_PAYLOAD_KEYS],
353
+ ['thread', PROJECT_THREAD_PAYLOAD_KEYS],
354
+ ]
355
+
356
+ for (const [kind, keys] of groups) {
357
+ for (const key of keys) {
358
+ const value = sanitizeRuntimeSegment(sanitizeSessionToken(readRawPayloadValue(payload, key)), '')
359
+ if (value) return { kind, token: value }
360
+ }
361
+ }
362
+
363
+ return { kind: '', token: '' }
364
+ }
365
+
372
366
  function resolveEnvSessionToken(env = process.env) {
373
367
  return resolveProjectSessionToken({ payload: {}, env })
374
368
  }
@@ -377,6 +371,17 @@ function resolveEnvSessionAliasToken(env = process.env) {
377
371
  return resolveProjectSessionAliasToken({ env })
378
372
  }
379
373
 
374
+ function resolveProjectSessionHostHint({ env = process.env, ppid = process.ppid } = {}) {
375
+ const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
376
+ if (envToken) return `host:${envToken}`
377
+
378
+ const envAliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
379
+ if (envAliasToken) return `alias:${envAliasToken}`
380
+
381
+ const parentToken = sanitizeRuntimeSegment(String(ppid || '').trim(), '')
382
+ return parentToken ? `ppid:${parentToken}` : ''
383
+ }
384
+
380
385
  function resolveTransientSessionToken({ payload = {}, env = process.env, ppid = process.ppid } = {}) {
381
386
  return resolveSessionToken({
382
387
  payload,
@@ -393,18 +398,33 @@ function buildScopedSessionToken(kind = '', raw = '') {
393
398
  return `${normalizedKind}-${value}`
394
399
  }
395
400
 
396
- function getActiveSessionPath(activationDir) {
397
- return join(activationDir, PROJECT_SESSIONS_DIR_NAME, ACTIVE_SESSION_FILE_NAME)
401
+ function buildSessionAliasKeys({ payload = {}, env = process.env } = {}) {
402
+ const keys = []
403
+ const payloadIdentity = readPayloadSessionIdentity(payload)
404
+ if (payloadIdentity.token) {
405
+ keys.push(`${payloadIdentity.kind}:${payloadIdentity.token}`)
406
+ }
407
+
408
+ const envSession = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
409
+ if (envSession && !payloadIdentity.token) keys.push(`host:${envSession}`)
410
+
411
+ const payloadAlias = sanitizeRuntimeSegment(sanitizeSessionToken(payload?._helloagentsSessionAlias), '')
412
+ if (payloadAlias) keys.push(`alias:${payloadAlias}`)
413
+
414
+ const envAlias = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
415
+ if (envAlias && envSession && envAlias === envSession) {
416
+ return [...new Set(keys.filter(Boolean))]
417
+ }
418
+ if (envAlias) keys.push(`alias:${envAlias}`)
419
+
420
+ return [...new Set(keys.filter(Boolean))]
398
421
  }
399
422
 
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 {}
423
+ function getActiveSessionPath(activationDir) {
424
+ return join(activationDir, PROJECT_SESSIONS_DIR_NAME, ACTIVE_SESSION_FILE_NAME)
405
425
  }
406
426
 
407
- function resolveActiveSessionToken({ activationDir, projectRoot, workspace, now = Date.now() } = {}) {
427
+ function readActiveProjectSession({ activationDir, projectRoot, workspace, now = Date.now() } = {}) {
408
428
  const active = readJsonFile(getActiveSessionPath(activationDir), null)
409
429
  if (!active || typeof active !== 'object') return ''
410
430
  if (active.cwd && !samePath(active.cwd, projectRoot)) return ''
@@ -415,43 +435,77 @@ function resolveActiveSessionToken({ activationDir, projectRoot, workspace, now
415
435
  const updatedAt = Date.parse(active.updatedAt || '')
416
436
  if (!Number.isFinite(updatedAt) || now - updatedAt > USER_RUNTIME_MAX_AGE_MS) return ''
417
437
 
418
- return sanitizeRuntimeSegment(active.session, '')
438
+ return active
419
439
  }
420
440
 
421
441
  function resolveActiveAliasSession({ activationDir, projectRoot, workspace, alias, now = Date.now() } = {}) {
422
442
  if (!alias) return ''
423
- const active = readJsonFile(getActiveSessionPath(activationDir), null)
443
+ const active = readActiveProjectSession({
444
+ activationDir,
445
+ projectRoot,
446
+ workspace,
447
+ now,
448
+ })
424
449
  if (!active || typeof active !== 'object') return ''
425
- if (active.cwd && !samePath(active.cwd, projectRoot)) return ''
426
450
 
427
- const activeWorkspace = sanitizeRuntimeSegment(active.workspace || active.branch || '', '')
428
- if (activeWorkspace && activeWorkspace !== workspace) return ''
451
+ const aliases = active.aliases && typeof active.aliases === 'object' ? active.aliases : {}
452
+ const mapped = sanitizeRuntimeSegment(aliases[alias], '')
453
+ if (mapped) return mapped
454
+ if (
455
+ Object.prototype.hasOwnProperty.call(aliases, alias)
456
+ && resolveActiveSessionToken(active, join(activationDir, PROJECT_SESSIONS_DIR_NAME, workspace)) === DEFAULT_STATE_SESSION_TOKEN
457
+ ) {
458
+ return DEFAULT_STATE_SESSION_TOKEN
459
+ }
460
+ return ''
461
+ }
429
462
 
430
- const updatedAt = Date.parse(active.updatedAt || '')
431
- if (!Number.isFinite(updatedAt) || now - updatedAt > USER_RUNTIME_MAX_AGE_MS) return ''
463
+ function choosePreferredProjectSession(activeSession = '', candidates = []) {
464
+ for (const candidate of candidates) {
465
+ if (!candidate) continue
466
+ if (activeSession && candidate === activeSession) return candidate
467
+ }
468
+ return candidates.find(Boolean) || ''
469
+ }
432
470
 
433
- const aliases = active.aliases && typeof active.aliases === 'object' ? active.aliases : {}
434
- return sanitizeRuntimeSegment(aliases[alias], '')
471
+ function resolveActiveSessionToken(active = {}, workspaceDir = '') {
472
+ const session = sanitizeRuntimeSegment(active?.session || '', '')
473
+ if (session) return session
474
+
475
+ const defaultStatePath = workspaceDir
476
+ ? join(workspaceDir, DEFAULT_STATE_SESSION_TOKEN, 'STATE.md')
477
+ : ''
478
+ return defaultStatePath && existsSync(defaultStatePath)
479
+ ? DEFAULT_STATE_SESSION_TOKEN
480
+ : ''
435
481
  }
436
482
 
437
- export function writeActiveProjectSession(scope, { host = '', source = '', env = process.env } = {}) {
483
+ export function writeActiveProjectSession(scope, { host = '', source = '', payload = {}, env = process.env, ppid = process.ppid } = {}) {
438
484
  if (!scope?.active || !scope.activationDir || !scope.workspace) return ''
439
485
 
440
486
  const activePath = getActiveSessionPath(scope.activationDir)
441
487
  const current = readJsonFile(activePath, null) || {}
442
- const aliases = current.aliases && typeof current.aliases === 'object' ? current.aliases : {}
443
- const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
444
- if (envToken && envToken !== scope.session) aliases[envToken] = scope.session
445
- const aliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
446
- if (aliasToken && aliasToken !== scope.session) aliases[aliasToken] = scope.session
488
+ const aliases = current.aliases && typeof current.aliases === 'object' ? { ...current.aliases } : {}
489
+ const session = scope.session || DEFAULT_STATE_SESSION_TOKEN
490
+ const sessionMode = scope.session
491
+ ? scope.sessionMode
492
+ : 'default'
493
+ const hostHint = resolveProjectSessionHostHint({ env, ppid }) || current.hostHint || ''
494
+ const aliasKeys = buildSessionAliasKeys({ payload, env })
495
+ for (const aliasKey of aliasKeys) {
496
+ aliases[aliasKey] = session
497
+ const [, aliasValue = ''] = String(aliasKey).split(':')
498
+ if (aliasValue) aliases[aliasValue] = session
499
+ }
447
500
  writeJsonFileAtomic(activePath, {
448
501
  version: 1,
449
502
  cwd: scope.cwd,
450
503
  workspace: scope.workspace || scope.branch,
451
- session: scope.session,
452
- sessionMode: scope.sessionMode,
504
+ session,
505
+ sessionMode,
453
506
  host,
454
507
  source,
508
+ ...(hostHint ? { hostHint } : {}),
455
509
  aliases,
456
510
  ...(current.cleanupCheckedAt ? { cleanupCheckedAt: current.cleanupCheckedAt } : {}),
457
511
  updatedAt: new Date().toISOString(),
@@ -459,23 +513,73 @@ export function writeActiveProjectSession(scope, { host = '', source = '', env =
459
513
  return activePath
460
514
  }
461
515
 
462
- function chooseProjectSession({ payload, env, activationDir, projectRoot, workspace }) {
463
- const payloadToken = sanitizeRuntimeSegment(resolvePayloadSessionToken(payload), '')
464
- const payloadAlias = sanitizeRuntimeSegment(payload?._helloagentsSessionAlias, '')
516
+ function chooseProjectSession({ payload, env, ppid, activationDir, projectRoot, workspace }) {
517
+ const workspaceDir = join(activationDir, PROJECT_SESSIONS_DIR_NAME, workspace)
518
+ const active = readActiveProjectSession({
519
+ activationDir,
520
+ projectRoot,
521
+ workspace,
522
+ })
523
+ const activeSession = resolveActiveSessionToken(active, workspaceDir)
524
+ const payloadIdentity = readPayloadSessionIdentity(payload)
525
+ const payloadToken = payloadIdentity.token
526
+ const payloadAlias = sanitizeRuntimeSegment(sanitizeSessionToken(payload?._helloagentsSessionAlias), '')
527
+ const payloadAliases = [
528
+ payloadIdentity.token ? `${payloadIdentity.kind}:${payloadIdentity.token}` : '',
529
+ payloadAlias ? `alias:${payloadAlias}` : '',
530
+ ].filter(Boolean)
531
+ const payloadMappedSession = choosePreferredProjectSession(
532
+ activeSession,
533
+ payloadAliases.map((alias) => resolveActiveAliasSession({
534
+ activationDir,
535
+ projectRoot,
536
+ workspace,
537
+ alias,
538
+ })),
539
+ )
540
+ if (payloadMappedSession) {
541
+ return { session: payloadMappedSession, sessionMode: 'active-session' }
542
+ }
543
+
544
+ const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
545
+ const envAliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
546
+ const envMappedSession = choosePreferredProjectSession(
547
+ activeSession,
548
+ [
549
+ envToken ? resolveActiveAliasSession({
550
+ activationDir,
551
+ projectRoot,
552
+ workspace,
553
+ alias: `host:${envToken}`,
554
+ }) : '',
555
+ envAliasToken ? resolveActiveAliasSession({
556
+ activationDir,
557
+ projectRoot,
558
+ workspace,
559
+ alias: `alias:${envAliasToken}`,
560
+ }) : '',
561
+ ],
562
+ )
563
+ if (envMappedSession) return { session: envMappedSession, sessionMode: 'active-session' }
564
+
565
+ if (
566
+ activeSession === DEFAULT_STATE_SESSION_TOKEN
567
+ && active?.hostHint
568
+ && active.hostHint === resolveProjectSessionHostHint({ env, ppid })
569
+ && (payloadToken || payloadAlias || envToken || envAliasToken)
570
+ ) {
571
+ return {
572
+ session: activeSession,
573
+ sessionMode: 'active-session',
574
+ }
575
+ }
576
+
465
577
  if (payloadToken) {
466
578
  return {
467
579
  session: buildScopedSessionToken('host', payloadToken),
468
580
  sessionMode: 'host-session',
469
581
  }
470
582
  }
471
-
472
- const payloadAliasToken = resolveActiveAliasSession({
473
- activationDir,
474
- projectRoot,
475
- workspace,
476
- alias: payloadAlias,
477
- })
478
- if (payloadAliasToken) return { session: payloadAliasToken, sessionMode: 'active-session' }
479
583
  if (payloadAlias) {
480
584
  return {
481
585
  session: buildScopedSessionToken('alias', payloadAlias),
@@ -483,16 +587,6 @@ function chooseProjectSession({ payload, env, activationDir, projectRoot, worksp
483
587
  }
484
588
  }
485
589
 
486
- const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
487
- const envAliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
488
- const aliasToken = resolveActiveAliasSession({
489
- activationDir,
490
- projectRoot,
491
- workspace,
492
- alias: envToken,
493
- })
494
- if (aliasToken) return { session: aliasToken, sessionMode: 'active-session' }
495
-
496
590
  if (envToken) {
497
591
  return {
498
592
  session: buildScopedSessionToken('host', envToken),
@@ -501,42 +595,33 @@ function chooseProjectSession({ payload, env, activationDir, projectRoot, worksp
501
595
  }
502
596
 
503
597
  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
598
  return {
512
599
  session: buildScopedSessionToken('alias', envAliasToken),
513
600
  sessionMode: 'alias-session',
514
601
  }
515
602
  }
516
603
 
517
- return { session: '', sessionMode: 'unidentified' }
518
- }
604
+ const source = String(payload?.source || '').trim().toLowerCase()
605
+ if ((source === 'resume' || source === 'compact') && activeSession) {
606
+ return {
607
+ session: activeSession,
608
+ sessionMode: 'active-session',
609
+ }
610
+ }
519
611
 
520
- function removeLegacyProjectArtifacts(activationDir) {
521
- if (!activationDir) return
522
- const artifactsDir = join(activationDir, PROJECT_ARTIFACTS_DIR_NAME)
523
- if (!existsSync(artifactsDir)) return
524
- try {
525
- rmSync(artifactsDir, { recursive: true, force: true })
526
- } catch {}
612
+ return { session: '', sessionMode: 'unidentified' }
527
613
  }
528
614
 
529
615
  export function getProjectSessionScope(cwd, options = {}) {
530
616
  const normalizedCwd = normalizePath(cwd || process.cwd())
531
617
  const projectRoot = getProjectRoot(normalizedCwd)
532
- const { payload = {}, env = process.env } = normalizeRuntimeOptions(options)
618
+ const { payload = {}, env = process.env, ppid = process.ppid } = normalizeRuntimeOptions(options)
533
619
  const activationDir = getProjectActivationDir(projectRoot)
534
- removeLegacyProjectArtifacts(activationDir)
535
- removeLegacySessionPointersFile(activationDir)
536
620
  const workspace = resolveWorkspaceName(projectRoot)
537
621
  const { session, sessionMode } = chooseProjectSession({
538
622
  payload,
539
623
  env,
624
+ ppid,
540
625
  activationDir,
541
626
  projectRoot,
542
627
  workspace,
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import { basename, dirname, join } from 'node:path'
3
3
 
4
4
  import {
@@ -43,67 +43,6 @@ function writeRuntimeDocument(filePath, payload) {
43
43
  writeJsonFileAtomic(filePath, payload)
44
44
  }
45
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
-
107
46
  function normalizeOptions(options = {}) {
108
47
  if (!options || typeof options !== 'object') return {}
109
48
  if (options.payload && typeof options.payload === 'object') return options
@@ -114,7 +53,7 @@ function normalizeOptions(options = {}) {
114
53
  }
115
54
 
116
55
  function getEventSessionAlias(eventPayload = {}) {
117
- return eventPayload.sessionId || eventPayload.session_id || eventPayload['session-id'] || ''
56
+ return eventPayload.sessionAlias || eventPayload.session_alias || eventPayload['session-alias'] || eventPayload._helloagentsSessionAlias || ''
118
57
  }
119
58
 
120
59
  function getScope(cwd, options = {}) {
@@ -182,7 +121,6 @@ export function getSessionArtifactRelativePath(cwd, fileName, options = {}) {
182
121
 
183
122
  export function readSessionCapsule(cwd = process.cwd(), options = {}) {
184
123
  const scope = getScope(cwd, options)
185
- migrateLegacyProjectScope(scope)
186
124
  const capsule = readRuntimeDocument(scope.runtimePath)
187
125
  if (!capsule || Array.isArray(capsule)) return buildEmptyCapsule(scope)
188
126
  return {
@@ -201,7 +139,6 @@ export function readSessionCapsule(cwd = process.cwd(), options = {}) {
201
139
  export function writeSessionCapsule(cwd, capsule, options = {}) {
202
140
  const normalizedOptions = normalizeOptions(options)
203
141
  const scope = getScope(cwd, normalizedOptions)
204
- migrateLegacyProjectScope(scope)
205
142
  const shouldMaterialize = shouldMaterializeSessionState(normalizedOptions)
206
143
  const currentDocument = readStateDocument(scope.statePath)
207
144
  const hasBody = Boolean(currentDocument.body && currentDocument.body.trim())
@@ -246,7 +183,9 @@ export function writeSessionCapsule(cwd, capsule, options = {}) {
246
183
  })
247
184
  }
248
185
  writeActiveProjectSession(scope, {
186
+ payload: normalizedOptions.payload,
249
187
  env: normalizedOptions.env,
188
+ ppid: normalizedOptions.ppid,
250
189
  })
251
190
  return nextCapsule
252
191
  }
@@ -305,7 +244,9 @@ export function appendSessionEvent(cwd, eventPayload, options = {}) {
305
244
  writeActiveProjectSession(scope, {
306
245
  host: eventPayload.host || '',
307
246
  source: eventPayload.source || eventName,
247
+ payload: scopedOptions.payload,
308
248
  env: scopedOptions.env,
249
+ ppid: scopedOptions.ppid,
309
250
  })
310
251
  if (!shouldRecordSessionEvents(scopedOptions)) return ''
311
252
 
@@ -17,19 +17,31 @@ const PAYLOAD_SESSION_KEYS = [
17
17
  'tab',
18
18
  ]
19
19
 
20
- const PROJECT_PAYLOAD_SESSION_KEYS = [
20
+ const PROJECT_SESSION_PAYLOAD_KEYS = [
21
21
  'sessionId',
22
22
  'session_id',
23
23
  'session',
24
+ ]
25
+
26
+ const PROJECT_CONVERSATION_PAYLOAD_KEYS = [
24
27
  'conversationId',
25
28
  'conversation_id',
26
29
  'conversation',
30
+ ]
31
+
32
+ const PROJECT_THREAD_PAYLOAD_KEYS = [
27
33
  'threadId',
28
34
  'thread_id',
29
35
  'thread-id',
30
36
  'thread',
31
37
  ]
32
38
 
39
+ const PROJECT_PAYLOAD_SESSION_KEYS = [
40
+ ...PROJECT_SESSION_PAYLOAD_KEYS,
41
+ ...PROJECT_CONVERSATION_PAYLOAD_KEYS,
42
+ ...PROJECT_THREAD_PAYLOAD_KEYS,
43
+ ]
44
+
33
45
  const ENV_SESSION_KEYS = [
34
46
  'HELLOAGENTS_NOTIFY_SESSION_ID',
35
47
  'WT_SESSION',
@@ -46,7 +58,6 @@ const PROJECT_ENV_SESSION_KEYS = [
46
58
  ]
47
59
 
48
60
  const PROJECT_ALIAS_ENV_SESSION_KEYS = [
49
- 'HELLOAGENTS_NOTIFY_SESSION_ID',
50
61
  'WT_SESSION',
51
62
  'TERM_SESSION_ID',
52
63
  'KITTY_WINDOW_ID',
@@ -120,6 +131,9 @@ export {
120
131
  ENV_SESSION_KEYS,
121
132
  PAYLOAD_SESSION_KEYS,
122
133
  PROJECT_ALIAS_ENV_SESSION_KEYS,
134
+ PROJECT_CONVERSATION_PAYLOAD_KEYS,
123
135
  PROJECT_ENV_SESSION_KEYS,
124
136
  PROJECT_PAYLOAD_SESSION_KEYS,
137
+ PROJECT_SESSION_PAYLOAD_KEYS,
138
+ PROJECT_THREAD_PAYLOAD_KEYS,
125
139
  }
@@ -1,67 +1,23 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import { dirname } from 'node:path'
3
3
 
4
- const STATE_META_BEGIN = '<!-- HELLOAGENTS_STATE_META'
5
- const STATE_META_END = 'HELLOAGENTS_STATE_META -->'
6
4
  export const AUTO_CREATED_STATE_MARKER = '由运行时自动创建;后续按实际任务重写'
7
5
 
8
6
  function normalizeText(content = '') {
9
7
  return String(content || '').replace(/^\uFEFF/, '')
10
8
  }
11
9
 
12
- function splitLines(content = '') {
13
- return normalizeText(content).replace(/\r\n/g, '\n').split('\n')
14
- }
15
-
16
- export function parseStateDocument(content = '') {
17
- const lines = splitLines(content)
18
- if (lines[0]?.trim() !== STATE_META_BEGIN) {
19
- return {
20
- hasMetadata: false,
21
- metadata: null,
22
- body: normalizeText(content),
23
- }
24
- }
25
-
26
- const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === STATE_META_END)
27
- if (endIndex < 0) {
28
- return {
29
- hasMetadata: false,
30
- metadata: null,
31
- body: normalizeText(content),
32
- }
33
- }
34
-
35
- const metadataText = lines.slice(1, endIndex).join('\n').trim()
36
- const body = lines.slice(endIndex + 1).join('\n').replace(/^\n+/, '')
37
- try {
38
- return {
39
- hasMetadata: true,
40
- metadata: JSON.parse(metadataText),
41
- body,
42
- }
43
- } catch {
44
- return {
45
- hasMetadata: false,
46
- metadata: null,
47
- body,
48
- }
49
- }
50
- }
51
-
52
10
  export function readStateDocument(filePath) {
53
11
  if (!filePath || !existsSync(filePath)) {
54
- return {
55
- hasMetadata: false,
56
- metadata: null,
57
- body: '',
58
- }
12
+ return { body: '' }
59
13
  }
60
14
 
61
- return parseStateDocument(readFileSync(filePath, 'utf-8'))
15
+ return {
16
+ body: normalizeText(readFileSync(filePath, 'utf-8')),
17
+ }
62
18
  }
63
19
 
64
- export function composeStateDocument({ metadata = {}, body = '' } = {}) {
20
+ export function composeStateDocument({ body = '' } = {}) {
65
21
  const normalizedBody = normalizeText(body).replace(/^\n+/, '')
66
22
  return normalizedBody ? `${normalizedBody.replace(/\n+$/, '')}\n` : ''
67
23
  }
@@ -70,7 +26,7 @@ export function looksLikeAutoCreatedState(body = '') {
70
26
  return normalizeText(body).includes(AUTO_CREATED_STATE_MARKER)
71
27
  }
72
28
 
73
- export function writeStateDocument(filePath, { metadata = {}, body = '' } = {}) {
29
+ export function writeStateDocument(filePath, { body = '' } = {}) {
74
30
  mkdirSync(dirname(filePath), { recursive: true })
75
- writeFileSync(filePath, composeStateDocument({ metadata, body }), 'utf-8')
31
+ writeFileSync(filePath, composeStateDocument({ body }), 'utf-8')
76
32
  }