helloagents 3.0.35 → 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.
@@ -10,10 +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 { looksLikeAutoCreatedState, readStateDocument } from './state-document.mjs'
13
14
 
14
15
  export const PROJECT_SESSION_CLEANUP_COOLDOWN_MS = 10 * 60 * 1000
15
16
  export const PROJECT_SESSION_MAX_AGE_MS = LONG_RUNNING_TTL_MS
16
-
17
17
  function removePath(filePath, result, bucket) {
18
18
  try {
19
19
  rmSync(filePath, { recursive: true, force: true })
@@ -23,6 +23,10 @@ function removePath(filePath, result, bucket) {
23
23
  }
24
24
  }
25
25
 
26
+ function isDebugLog(entryName = '') {
27
+ return /\.log$/i.test(entryName)
28
+ }
29
+
26
30
  function isDirectoryEmptyRecursive(dirPath) {
27
31
  const entries = readdirSync(dirPath, { withFileTypes: true })
28
32
  if (entries.length === 0) return true
@@ -32,9 +36,10 @@ function isDirectoryEmptyRecursive(dirPath) {
32
36
  })
33
37
  }
34
38
 
35
- function shouldKeepSession(active, workspace, session) {
39
+ function shouldKeepNestedSession(active, workspace, sessionName) {
36
40
  const activeWorkspace = active.workspace || active.branch || ''
37
- return activeWorkspace === workspace && active.session === session
41
+ const activeSession = active.session || ''
42
+ return activeWorkspace === workspace && activeSession === sessionName
38
43
  }
39
44
 
40
45
  function readCleanupCheckedAt(active) {
@@ -55,6 +60,14 @@ function hasStateSnapshot(sessionDir) {
55
60
  return existsSync(join(sessionDir, 'STATE.md'))
56
61
  }
57
62
 
63
+ function isAutoCreatedSeedSession(sessionDir) {
64
+ const statePath = join(sessionDir, 'STATE.md')
65
+ if (!existsSync(statePath)) return false
66
+
67
+ const { body } = readStateDocument(statePath)
68
+ return looksLikeAutoCreatedState(body)
69
+ }
70
+
58
71
  function readSessionStateMtimeMs(sessionDir) {
59
72
  try {
60
73
  return statSync(join(sessionDir, 'STATE.md')).mtimeMs
@@ -79,6 +92,32 @@ function cleanupTransientSessionTemps(sessionsDir, result) {
79
92
  }
80
93
  }
81
94
 
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
+
82
121
  export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs = 0, maxAgeMs = PROJECT_SESSION_MAX_AGE_MS } = {}) {
83
122
  const projectRoot = getProjectRoot(cwd)
84
123
  const activationDir = getProjectActivationDir(projectRoot)
@@ -90,7 +129,9 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
90
129
  removedEmptyDirs: [],
91
130
  removedInactiveDirs: [],
92
131
  removedNoStateDirs: [],
132
+ removedSeedDirs: [],
93
133
  removedTempFiles: [],
134
+ removedLegacyArtifacts: [],
94
135
  errors: [],
95
136
  skipped: false,
96
137
  }
@@ -109,30 +150,33 @@ export function cleanupProjectSessions(cwd, { now = Date.now(), minIntervalMs =
109
150
  } catch (error) {
110
151
  result.errors.push(`${sessionsDir}: ${error.message}`)
111
152
  }
153
+ cleanupLegacyProjectArtifacts(activationDir, result)
112
154
 
113
155
  for (const workspaceEntry of readdirSync(sessionsDir, { withFileTypes: true })) {
114
156
  if (!workspaceEntry.isDirectory()) continue
115
157
  const workspaceDir = join(sessionsDir, workspaceEntry.name)
116
-
117
- for (const sessionEntry of readdirSync(workspaceDir, { withFileTypes: true })) {
118
- if (!sessionEntry.isDirectory()) continue
119
- const sessionDir = join(workspaceDir, sessionEntry.name)
120
- if (shouldKeepSession(active, workspaceEntry.name, sessionEntry.name)) continue
121
-
122
- 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
123
163
  if (isDirectoryEmptyRecursive(sessionDir)) {
124
164
  removePath(sessionDir, result, 'removedEmptyDirs')
125
- } else if (!hasStateSnapshot(sessionDir)) {
165
+ continue
166
+ }
167
+ if (!hasStateSnapshot(sessionDir)) {
126
168
  removePath(sessionDir, result, 'removedNoStateDirs')
127
- } else if (isStaleStateSession(sessionDir, now, maxAgeMs)) {
169
+ continue
170
+ }
171
+ if (isAutoCreatedSeedSession(sessionDir)) {
172
+ removePath(sessionDir, result, 'removedSeedDirs')
173
+ continue
174
+ }
175
+ if (isStaleStateSession(sessionDir, now, maxAgeMs)) {
128
176
  removePath(sessionDir, result, 'removedInactiveDirs')
129
177
  }
130
- } catch (error) {
131
- result.errors.push(`${sessionDir}: ${error.message}`)
132
178
  }
133
- }
134
179
 
135
- try {
136
180
  if (isDirectoryEmptyRecursive(workspaceDir)) {
137
181
  removePath(workspaceDir, result, 'removedEmptyDirs')
138
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 {
@@ -7,7 +7,7 @@ import {
7
7
  readSessionArtifact,
8
8
  writeSessionArtifact,
9
9
  } from './session-capsule.mjs'
10
- import { EVIDENCE_MAX_AGE_MS, LONG_RUNNING_TTL_HOURS } from './runtime-ttl.mjs'
10
+ import { EVIDENCE_MAX_AGE_MS, LONG_RUNNING_TTL_HOURS, STANDARD_RUNTIME_TTL_HOURS } from './runtime-ttl.mjs'
11
11
 
12
12
  export { EVIDENCE_MAX_AGE_MS }
13
13
 
@@ -87,7 +87,7 @@ export function validateEvidenceTimestamp(evidence, now, label) {
87
87
  required: true,
88
88
  status: 'stale-time',
89
89
  evidence,
90
- details: [`${label}超过 ${LONG_RUNNING_TTL_HOURS} 小时`],
90
+ details: [`${label}超过 ${STANDARD_RUNTIME_TTL_HOURS} 小时(长任务上限:${LONG_RUNNING_TTL_HOURS} 小时)`],
91
91
  }
92
92
  }
93
93
  return null
@@ -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 { 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
  }
@@ -323,21 +363,18 @@ function findProjectActivationDir(cwd) {
323
363
 
324
364
  function resolvePayloadSessionToken(payload = {}) {
325
365
  if (payload?._helloagentsSessionAlias) return ''
326
- return resolveSessionToken({
366
+ return resolveProjectSessionToken({
327
367
  payload,
328
368
  env: {},
329
- ppid: 0,
330
- allowPpidFallback: false,
331
369
  })
332
370
  }
333
371
 
334
372
  function resolveEnvSessionToken(env = process.env) {
335
- return resolveSessionToken({
336
- payload: {},
337
- env,
338
- ppid: 0,
339
- allowPpidFallback: false,
340
- })
373
+ return resolveProjectSessionToken({ payload: {}, env })
374
+ }
375
+
376
+ function resolveEnvSessionAliasToken(env = process.env) {
377
+ return resolveProjectSessionAliasToken({ env })
341
378
  }
342
379
 
343
380
  function resolveTransientSessionToken({ payload = {}, env = process.env, ppid = process.ppid } = {}) {
@@ -349,10 +386,24 @@ function resolveTransientSessionToken({ payload = {}, env = process.env, ppid =
349
386
  })
350
387
  }
351
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
+
352
396
  function getActiveSessionPath(activationDir) {
353
397
  return join(activationDir, PROJECT_SESSIONS_DIR_NAME, ACTIVE_SESSION_FILE_NAME)
354
398
  }
355
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
+
356
407
  function resolveActiveSessionToken({ activationDir, projectRoot, workspace, now = Date.now() } = {}) {
357
408
  const active = readJsonFile(getActiveSessionPath(activationDir), null)
358
409
  if (!active || typeof active !== 'object') return ''
@@ -384,14 +435,15 @@ function resolveActiveAliasSession({ activationDir, projectRoot, workspace, alia
384
435
  }
385
436
 
386
437
  export function writeActiveProjectSession(scope, { host = '', source = '', env = process.env } = {}) {
387
- if (!scope?.active || !scope.activationDir || !scope.session) return ''
438
+ if (!scope?.active || !scope.activationDir || !scope.workspace) return ''
388
439
 
389
440
  const activePath = getActiveSessionPath(scope.activationDir)
390
441
  const current = readJsonFile(activePath, null) || {}
391
442
  const aliases = current.aliases && typeof current.aliases === 'object' ? current.aliases : {}
392
443
  const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
393
444
  if (envToken && envToken !== scope.session) aliases[envToken] = scope.session
394
-
445
+ const aliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
446
+ if (aliasToken && aliasToken !== scope.session) aliases[aliasToken] = scope.session
395
447
  writeJsonFileAtomic(activePath, {
396
448
  version: 1,
397
449
  cwd: scope.cwd,
@@ -409,9 +461,14 @@ export function writeActiveProjectSession(scope, { host = '', source = '', env =
409
461
 
410
462
  function chooseProjectSession({ payload, env, activationDir, projectRoot, workspace }) {
411
463
  const payloadToken = sanitizeRuntimeSegment(resolvePayloadSessionToken(payload), '')
412
- if (payloadToken) return { session: payloadToken, sessionMode: 'host-session' }
413
-
414
464
  const payloadAlias = sanitizeRuntimeSegment(payload?._helloagentsSessionAlias, '')
465
+ if (payloadToken) {
466
+ return {
467
+ session: buildScopedSessionToken('host', payloadToken),
468
+ sessionMode: 'host-session',
469
+ }
470
+ }
471
+
415
472
  const payloadAliasToken = resolveActiveAliasSession({
416
473
  activationDir,
417
474
  projectRoot,
@@ -419,8 +476,15 @@ function chooseProjectSession({ payload, env, activationDir, projectRoot, worksp
419
476
  alias: payloadAlias,
420
477
  })
421
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
+ }
422
485
 
423
486
  const envToken = sanitizeRuntimeSegment(resolveEnvSessionToken(env), '')
487
+ const envAliasToken = sanitizeRuntimeSegment(resolveEnvSessionAliasToken(env), '')
424
488
  const aliasToken = resolveActiveAliasSession({
425
489
  activationDir,
426
490
  projectRoot,
@@ -429,12 +493,37 @@ function chooseProjectSession({ payload, env, activationDir, projectRoot, worksp
429
493
  })
430
494
  if (aliasToken) return { session: aliasToken, sessionMode: 'active-session' }
431
495
 
432
- if (envToken) return { session: envToken, sessionMode: 'host-session' }
496
+ if (envToken) {
497
+ return {
498
+ session: buildScopedSessionToken('host', envToken),
499
+ sessionMode: 'host-session',
500
+ }
501
+ }
433
502
 
434
- const activeToken = resolveActiveSessionToken({ activationDir, projectRoot, workspace })
435
- 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
+ }
516
+
517
+ return { session: '', sessionMode: 'unidentified' }
518
+ }
436
519
 
437
- return { session: DEFAULT_STATE_SESSION_TOKEN, sessionMode: 'default' }
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 {}
438
527
  }
439
528
 
440
529
  export function getProjectSessionScope(cwd, options = {}) {
@@ -442,6 +531,8 @@ export function getProjectSessionScope(cwd, options = {}) {
442
531
  const projectRoot = getProjectRoot(normalizedCwd)
443
532
  const { payload = {}, env = process.env } = normalizeRuntimeOptions(options)
444
533
  const activationDir = getProjectActivationDir(projectRoot)
534
+ removeLegacyProjectArtifacts(activationDir)
535
+ removeLegacySessionPointersFile(activationDir)
445
536
  const workspace = resolveWorkspaceName(projectRoot)
446
537
  const { session, sessionMode } = chooseProjectSession({
447
538
  payload,
@@ -450,7 +541,8 @@ export function getProjectSessionScope(cwd, options = {}) {
450
541
  projectRoot,
451
542
  workspace,
452
543
  })
453
- 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)
454
546
 
455
547
  return {
456
548
  cwd: projectRoot,
@@ -461,10 +553,12 @@ export function getProjectSessionScope(cwd, options = {}) {
461
553
  sessionMode,
462
554
  activationDir,
463
555
  sessionDir,
556
+ workspaceDir,
464
557
  statePath: join(sessionDir, 'STATE.md'),
465
558
  eventsPath: join(sessionDir, EVENTS_FILE_NAME),
466
559
  artifactsDir: join(sessionDir, PROJECT_ARTIFACTS_DIR_NAME),
467
- key: `${projectRoot}::${workspace}::${session}`,
560
+ runtimePath: join(sessionDir, PROJECT_RUNTIME_FILE_NAME),
561
+ key: `${projectRoot}::${workspace}::${session || DEFAULT_STATE_SESSION_TOKEN}`,
468
562
  }
469
563
  }
470
564
 
@@ -494,6 +588,7 @@ function buildTransientRuntimeDir(cwd, options = {}) {
494
588
  statePath: join(getUserRuntimeRoot(), hash, 'STATE.md'),
495
589
  eventsPath: join(getUserRuntimeRoot(), hash, EVENTS_FILE_NAME),
496
590
  artifactsDir: join(getUserRuntimeRoot(), hash, PROJECT_ARTIFACTS_DIR_NAME),
591
+ runtimePath: join(getUserRuntimeRoot(), hash, PROJECT_RUNTIME_FILE_NAME),
497
592
  key: `${normalizedCwd}::transient::${token}`,
498
593
  }
499
594
  }
@@ -1,7 +1,10 @@
1
1
  export const LONG_RUNNING_TTL_HOURS = 720
2
2
  export const LONG_RUNNING_TTL_MS = LONG_RUNNING_TTL_HOURS * 60 * 60 * 1000
3
3
 
4
- export const ROUTE_CONTEXT_TTL_MS = LONG_RUNNING_TTL_MS
5
- export const TURN_STATE_TTL_MS = LONG_RUNNING_TTL_MS
6
- export const EVIDENCE_MAX_AGE_MS = LONG_RUNNING_TTL_MS
7
- export const USER_RUNTIME_MAX_AGE_MS = LONG_RUNNING_TTL_MS
4
+ export const STANDARD_RUNTIME_TTL_HOURS = 72
5
+ export const STANDARD_RUNTIME_TTL_MS = STANDARD_RUNTIME_TTL_HOURS * 60 * 60 * 1000
6
+
7
+ export const ROUTE_CONTEXT_TTL_MS = STANDARD_RUNTIME_TTL_MS
8
+ export const TURN_STATE_TTL_MS = STANDARD_RUNTIME_TTL_MS
9
+ export const EVIDENCE_MAX_AGE_MS = STANDARD_RUNTIME_TTL_MS
10
+ export const USER_RUNTIME_MAX_AGE_MS = STANDARD_RUNTIME_TTL_MS
@@ -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
@@ -68,8 +141,23 @@ function getScope(cwd, options = {}) {
68
141
  return getRuntimeScope(cwd, normalizedOptions)
69
142
  }
70
143
 
144
+ function shouldMaterializeSessionState(options = {}) {
145
+ const normalizedOptions = normalizeOptions(options)
146
+ if (normalizedOptions.ensureProjectLocal === true) return true
147
+ if (normalizedOptions.project === true) return true
148
+ if (normalizedOptions.traceEvents === true) return true
149
+
150
+ const payload = normalizedOptions.payload || {}
151
+ if (payload.traceEvents === true || payload._helloagentsTraceEvents === true) return true
152
+
153
+ const raw = String(normalizedOptions.env?.HELLOAGENTS_TRACE_EVENTS || process.env.HELLOAGENTS_TRACE_EVENTS || '')
154
+ .trim()
155
+ .toLowerCase()
156
+ return raw === '1' || raw === 'true' || raw === 'yes'
157
+ }
158
+
71
159
  export function getSessionCapsulePath(cwd = process.cwd(), options = {}) {
72
- return getScope(cwd, options).statePath
160
+ return getScope(cwd, options).runtimePath
73
161
  }
74
162
 
75
163
  export function getSessionEventsPath(cwd = process.cwd(), options = {}) {
@@ -87,15 +175,15 @@ export function getSessionArtifactPath(cwd, fileName, options = {}) {
87
175
  export function getSessionArtifactRelativePath(cwd, fileName, options = {}) {
88
176
  const scope = getScope(cwd, options)
89
177
  if (scope.scope === 'project-session') {
90
- return `.helloagents/sessions/${scope.workspace || scope.branch}/${scope.session}/artifacts/${fileName}`
178
+ return `.helloagents/sessions/${scope.workspace || scope.branch}/${scope.session || 'default'}/artifacts/${fileName}`
91
179
  }
92
180
  return `~/.helloagents/runtime/${basename(scope.sessionDir)}/artifacts/${fileName}`
93
181
  }
94
182
 
95
183
  export function readSessionCapsule(cwd = process.cwd(), options = {}) {
96
184
  const scope = getScope(cwd, options)
97
- const { metadata } = readStateDocument(scope.statePath)
98
- const capsule = metadata && typeof metadata === 'object' ? metadata : null
185
+ migrateLegacyProjectScope(scope)
186
+ const capsule = readRuntimeDocument(scope.runtimePath)
99
187
  if (!capsule || Array.isArray(capsule)) return buildEmptyCapsule(scope)
100
188
  return {
101
189
  ...buildEmptyCapsule(scope),
@@ -113,9 +201,11 @@ export function readSessionCapsule(cwd = process.cwd(), options = {}) {
113
201
  export function writeSessionCapsule(cwd, capsule, options = {}) {
114
202
  const normalizedOptions = normalizeOptions(options)
115
203
  const scope = getScope(cwd, normalizedOptions)
204
+ migrateLegacyProjectScope(scope)
205
+ const shouldMaterialize = shouldMaterializeSessionState(normalizedOptions)
116
206
  const currentDocument = readStateDocument(scope.statePath)
117
207
  const hasBody = Boolean(currentDocument.body && currentDocument.body.trim())
118
- if (!hasBody && normalizedOptions.ensureProjectLocal !== true && !existsSync(scope.statePath)) {
208
+ if (!hasBody && !shouldMaterialize && !existsSync(scope.statePath)) {
119
209
  return {
120
210
  ...buildEmptyCapsule(scope),
121
211
  ...capsule,
@@ -129,6 +219,14 @@ export function writeSessionCapsule(cwd, capsule, options = {}) {
129
219
  updatedAt: new Date().toISOString(),
130
220
  }
131
221
  }
222
+ if (!hasBody && shouldMaterialize && !existsSync(scope.statePath)) {
223
+ ensureProjectLocalRuntime(cwd, {
224
+ ...normalizedOptions,
225
+ stateSeed: normalizedOptions.stateSeed && typeof normalizedOptions.stateSeed === 'object'
226
+ ? normalizedOptions.stateSeed
227
+ : {},
228
+ })
229
+ }
132
230
  const nextCapsule = {
133
231
  ...buildEmptyCapsule(scope),
134
232
  ...capsule,
@@ -141,10 +239,12 @@ export function writeSessionCapsule(cwd, capsule, options = {}) {
141
239
  sessionMode: scope.sessionMode,
142
240
  updatedAt: new Date().toISOString(),
143
241
  }
144
- writeStateDocument(scope.statePath, {
145
- metadata: nextCapsule,
146
- body: currentDocument.body,
147
- })
242
+ writeRuntimeDocument(scope.runtimePath, nextCapsule)
243
+ if (hasBody) {
244
+ writeStateDocument(scope.statePath, {
245
+ body: currentDocument.body,
246
+ })
247
+ }
148
248
  writeActiveProjectSession(scope, {
149
249
  env: normalizedOptions.env,
150
250
  })
@@ -173,8 +273,8 @@ export function writeCapsuleSection(cwd, section, value, options = {}) {
173
273
  }
174
274
 
175
275
  export function clearCapsuleSection(cwd, section, options = {}) {
176
- const statePath = getSessionCapsulePath(cwd, options)
177
- if (!existsSync(statePath)) return false
276
+ const runtimePath = getSessionCapsulePath(cwd, options)
277
+ if (!existsSync(runtimePath)) return false
178
278
 
179
279
  const capsule = readSessionCapsule(cwd, options)
180
280
  if (!Object.prototype.hasOwnProperty.call(capsule, section)) return false
@@ -265,7 +365,11 @@ export function clearSessionArtifact(cwd, fileName, options = {}) {
265
365
  }
266
366
 
267
367
  export function removeSessionCapsule(cwd, options = {}) {
268
- 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
+ }
269
373
  }
270
374
 
271
375
  function shouldRecordSessionEvents(options = {}) {