helloagents 3.0.38 → 3.1.1

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,7 +1,7 @@
1
1
  import { copyFileSync, existsSync, mkdtempSync, realpathSync, renameSync } from 'node:fs'
2
2
  import { dirname, join, resolve } from 'node:path'
3
3
 
4
- import { copyEntries, ensureDir, removeIfExists } from './cli-utils.mjs'
4
+ import { copyEntries, createLink, ensureDir, removeIfExists } from './cli-utils.mjs'
5
5
 
6
6
  export const RUNTIME_ROOT_ENTRIES = [
7
7
  '.claude-plugin',
@@ -28,6 +28,16 @@ export function getStableRuntimeRoot(home) {
28
28
  return join(home, '.helloagents', 'helloagents')
29
29
  }
30
30
 
31
+ /** Return the Claude local marketplace projection root derived from the shared runtime copy. */
32
+ export function getClaudeMarketplaceRoot(home) {
33
+ return join(home, '.helloagents', 'host-projections', 'claude-marketplace')
34
+ }
35
+
36
+ /** Return the Gemini extension projection root derived from the shared runtime copy. */
37
+ export function getGeminiExtensionRoot(home) {
38
+ return join(home, '.helloagents', 'host-projections', 'gemini')
39
+ }
40
+
31
41
  function normalizePath(path) {
32
42
  const resolved = resolve(path)
33
43
  try {
@@ -63,17 +73,9 @@ function retryTransientFs(operation) {
63
73
  throw lastError
64
74
  }
65
75
 
66
- function materializeGeminiHooks(root) {
67
- const source = join(root, 'hooks', 'hooks-gemini.json')
68
- const target = join(root, 'hooks', 'hooks.json')
69
- if (!existsSync(source)) return
70
- copyFileSync(source, target)
71
- }
72
-
73
- /** Sync package runtime files into the stable root without copying repo-only files. */
74
- export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
76
+ function syncRuntimeTree(sourceRoot, targetRoot, { materializeGeminiHooks = false } = {}) {
75
77
  const source = resolve(sourceRoot)
76
- const target = resolve(runtimeRoot)
78
+ const target = resolve(targetRoot)
77
79
  if (samePath(source, target)) {
78
80
  return { synced: false, root: target }
79
81
  }
@@ -84,7 +86,13 @@ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
84
86
 
85
87
  try {
86
88
  copyEntries(source, staging, RUNTIME_ROOT_ENTRIES)
87
- materializeGeminiHooks(staging)
89
+ if (materializeGeminiHooks) {
90
+ const sourceHooks = join(staging, 'hooks', 'hooks-gemini.json')
91
+ const targetHooks = join(staging, 'hooks', 'hooks.json')
92
+ if (existsSync(sourceHooks)) {
93
+ copyFileSync(sourceHooks, targetHooks)
94
+ }
95
+ }
88
96
  retryTransientFs(() => {
89
97
  removeIfExists(target)
90
98
  renameSync(staging, target)
@@ -96,7 +104,42 @@ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
96
104
  }
97
105
  }
98
106
 
107
+ /** Sync package runtime files into the stable root without copying repo-only files. */
108
+ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
109
+ return syncRuntimeTree(sourceRoot, runtimeRoot)
110
+ }
111
+
112
+ /** Sync a Claude local marketplace root that resolves to the stable runtime copy. */
113
+ export function syncClaudeMarketplaceRoot(sourceRoot, marketplaceRoot) {
114
+ const source = resolve(sourceRoot)
115
+ const target = resolve(marketplaceRoot)
116
+ if (samePath(source, target)) {
117
+ return { synced: false, root: target }
118
+ }
119
+
120
+ removeIfExists(target)
121
+ if (createLink(source, target)) {
122
+ return { synced: true, root: target }
123
+ }
124
+ return syncRuntimeTree(source, target)
125
+ }
126
+
127
+ /** Sync a host-specific extension root derived from the stable runtime copy. */
128
+ export function syncGeminiExtensionRoot(sourceRoot, extensionRoot) {
129
+ return syncRuntimeTree(sourceRoot, extensionRoot, { materializeGeminiHooks: true })
130
+ }
131
+
99
132
  /** Remove the stable runtime copy while leaving user settings under ~/.helloagents intact. */
100
133
  export function removeRuntimeRoot(runtimeRoot) {
101
134
  removeIfExists(runtimeRoot)
102
135
  }
136
+
137
+ /** Remove the Claude marketplace projection root. */
138
+ export function removeClaudeMarketplaceRoot(home) {
139
+ removeIfExists(getClaudeMarketplaceRoot(home))
140
+ }
141
+
142
+ /** Remove the Gemini extension projection root. */
143
+ export function removeGeminiExtensionRoot(home) {
144
+ removeIfExists(getGeminiExtensionRoot(home))
145
+ }
@@ -117,6 +117,10 @@ export function removeTopLevelTomlBlock(text, key) {
117
117
  return normalizeToml(`${normalized.slice(0, existing.start)}${normalized.slice(existing.end)}`);
118
118
  }
119
119
 
120
+ export function hasTopLevelTomlBlock(text, key) {
121
+ return Boolean(findTopLevelTomlBlock(text, key));
122
+ }
123
+
120
124
  export function prependTopLevelTomlBlocks(text, blocks) {
121
125
  const normalizedBlocks = blocks
122
126
  .map((block) => String(block || '').trim())
@@ -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,