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.
- package/.claude-plugin/marketplace.json +1 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +21 -13
- package/README_CN.md +21 -13
- package/bootstrap-lite.md +3 -4
- package/bootstrap.md +3 -4
- package/gemini-extension.json +1 -1
- package/install.ps1 +9 -2
- package/install.sh +11 -2
- package/package.json +1 -1
- package/scripts/cli-doctor-codex.mjs +4 -1
- package/scripts/cli-doctor.mjs +3 -3
- package/scripts/cli-hosts.mjs +1 -1
- package/scripts/cli-lifecycle-hosts.mjs +48 -20
- package/scripts/cli-messages.mjs +2 -2
- package/scripts/cli-runtime-root.mjs +9 -1
- package/scripts/notify-sound.mjs +2 -1
- package/scripts/notify.mjs +4 -0
- package/scripts/project-session-cleanup.mjs +60 -16
- package/scripts/project-storage.mjs +2 -8
- package/scripts/runtime-artifacts.mjs +2 -2
- package/scripts/runtime-scope.mjs +115 -20
- package/scripts/runtime-ttl.mjs +7 -4
- package/scripts/session-capsule.mjs +117 -13
- package/scripts/session-token.mjs +62 -9
- package/scripts/state-document.mjs +6 -7
- package/scripts/turn-state.mjs +2 -3
- package/scripts/workflow-core.mjs +1 -4
- package/scripts/workflow-plan-files.mjs +1 -1
- /package/hooks/{hooks.json → hooks-gemini.json} +0 -0
|
@@ -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
|
|
39
|
+
function shouldKeepNestedSession(active, workspace, sessionName) {
|
|
36
40
|
const activeWorkspace = active.workspace || active.branch || ''
|
|
37
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (!hasStateSnapshot(sessionDir)) {
|
|
126
168
|
removePath(sessionDir, result, 'removedNoStateDirs')
|
|
127
|
-
|
|
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 {
|
|
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
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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.
|
|
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)
|
|
496
|
+
if (envToken) {
|
|
497
|
+
return {
|
|
498
|
+
session: buildScopedSessionToken('host', envToken),
|
|
499
|
+
sessionMode: 'host-session',
|
|
500
|
+
}
|
|
501
|
+
}
|
|
433
502
|
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/scripts/runtime-ttl.mjs
CHANGED
|
@@ -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
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
export const
|
|
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).
|
|
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
|
-
|
|
98
|
-
const capsule =
|
|
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 &&
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
177
|
-
if (!existsSync(
|
|
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
|
-
|
|
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 = {}) {
|