@swarmclawai/swarmclaw 1.7.0 → 1.7.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.
@@ -435,18 +435,25 @@ export async function finalizeChatTurn(params: {
435
435
  ;(current as unknown as Record<string, unknown>)[key] = normalized
436
436
  }
437
437
  }
438
+ const preferRunValue = (runValue: unknown, fallbackValue: unknown) => (
439
+ runValue !== undefined ? runValue : fallbackValue
440
+ )
438
441
 
439
- persistField('claudeSessionId', session.claudeSessionId)
440
- persistField('codexThreadId', session.codexThreadId)
441
- persistField('opencodeSessionId', session.opencodeSessionId)
442
- persistField('geminiSessionId', session.geminiSessionId)
443
- persistField('copilotSessionId', session.copilotSessionId)
444
- persistField('droidSessionId', session.droidSessionId)
445
- persistField('cursorSessionId', session.cursorSessionId)
446
- persistField('qwenSessionId', session.qwenSessionId)
447
- persistField('acpSessionId', session.acpSessionId)
448
-
449
- const sourceResume = session.delegateResumeIds
442
+ // Provider handlers receive `sessionForRun` and may mutate CLI resume IDs there.
443
+ // Persist from run-session first, allowing null to intentionally clear IDs.
444
+ persistField('claudeSessionId', preferRunValue(sessionForRun.claudeSessionId, session.claudeSessionId))
445
+ persistField('codexThreadId', preferRunValue(sessionForRun.codexThreadId, session.codexThreadId))
446
+ persistField('opencodeSessionId', preferRunValue(sessionForRun.opencodeSessionId, session.opencodeSessionId))
447
+ persistField('geminiSessionId', preferRunValue(sessionForRun.geminiSessionId, session.geminiSessionId))
448
+ persistField('copilotSessionId', preferRunValue(sessionForRun.copilotSessionId, session.copilotSessionId))
449
+ persistField('droidSessionId', preferRunValue(sessionForRun.droidSessionId, session.droidSessionId))
450
+ persistField('cursorSessionId', preferRunValue(sessionForRun.cursorSessionId, session.cursorSessionId))
451
+ persistField('qwenSessionId', preferRunValue(sessionForRun.qwenSessionId, session.qwenSessionId))
452
+ persistField('acpSessionId', preferRunValue(sessionForRun.acpSessionId, session.acpSessionId))
453
+
454
+ const sourceResume = (sessionForRun.delegateResumeIds && typeof sessionForRun.delegateResumeIds === 'object')
455
+ ? sessionForRun.delegateResumeIds
456
+ : session.delegateResumeIds
450
457
  if (sourceResume && typeof sourceResume === 'object') {
451
458
  const currentResume = (current.delegateResumeIds && typeof current.delegateResumeIds === 'object')
452
459
  ? current.delegateResumeIds
@@ -454,14 +461,14 @@ export async function finalizeChatTurn(params: {
454
461
  const sr = sourceResume as Record<string, unknown>
455
462
  const cr = currentResume as Record<string, unknown>
456
463
  const nextResume = {
457
- claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
458
- codex: normalizeResumeId(sr.codex ?? cr.codex),
459
- opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
460
- gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
461
- copilot: normalizeResumeId(sr.copilot ?? cr.copilot),
462
- droid: normalizeResumeId(sr.droid ?? cr.droid),
463
- cursor: normalizeResumeId(sr.cursor ?? cr.cursor),
464
- qwen: normalizeResumeId(sr.qwen ?? cr.qwen),
464
+ claudeCode: normalizeResumeId(preferRunValue(sr.claudeCode, cr.claudeCode)),
465
+ codex: normalizeResumeId(preferRunValue(sr.codex, cr.codex)),
466
+ opencode: normalizeResumeId(preferRunValue(sr.opencode, cr.opencode)),
467
+ gemini: normalizeResumeId(preferRunValue(sr.gemini, cr.gemini)),
468
+ copilot: normalizeResumeId(preferRunValue(sr.copilot, cr.copilot)),
469
+ droid: normalizeResumeId(preferRunValue(sr.droid, cr.droid)),
470
+ cursor: normalizeResumeId(preferRunValue(sr.cursor, cr.cursor)),
471
+ qwen: normalizeResumeId(preferRunValue(sr.qwen, cr.qwen)),
465
472
  }
466
473
  if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
467
474
  current.delegateResumeIds = nextResume
@@ -32,11 +32,49 @@ test('selectActiveSessionId prefers override when present', () => {
32
32
  assert.equal(selectActiveSessionId(state), 'task-1')
33
33
  })
34
34
 
35
- test('selectActiveSessionId falls back to agent thread session', () => {
35
+ test('selectActiveSessionId chooses most recently active session for current agent', () => {
36
36
  const state = makeState({
37
37
  currentAgentId: 'agent-1',
38
38
  agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
39
- sessions: { 'thread-1': makeSession('thread-1') },
39
+ sessions: {
40
+ 'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 100 } as unknown as Session,
41
+ 'old-1': { ...makeSession('old-1'), agentId: 'agent-1', lastActiveAt: 90 } as unknown as Session,
42
+ 'latest-1': { ...makeSession('latest-1'), agentId: 'agent-1', lastActiveAt: 200, messageCount: 1 } as unknown as Session,
43
+ 'other-agent': { ...makeSession('other-agent'), agentId: 'agent-2', lastActiveAt: 999 } as unknown as Session,
44
+ },
45
+ })
46
+ assert.equal(selectActiveSessionId(state), 'latest-1')
47
+ })
48
+
49
+ test('selectActiveSessionId prefers most recent session with content over newer empty thread session', () => {
50
+ const state = makeState({
51
+ currentAgentId: 'agent-1',
52
+ agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
53
+ sessions: {
54
+ 'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 300 } as unknown as Session,
55
+ 'work-1': { ...makeSession('work-1'), agentId: 'agent-1', lastActiveAt: 200, messageCount: 2 } as unknown as Session,
56
+ },
57
+ })
58
+ assert.equal(selectActiveSessionId(state), 'work-1')
59
+ })
60
+
61
+ test('selectActiveSessionId falls back to thread session when agent has no loaded sessions', () => {
62
+ const state = makeState({
63
+ currentAgentId: 'agent-1',
64
+ agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
65
+ sessions: { 'unrelated': { ...makeSession('unrelated'), agentId: 'agent-2' } as unknown as Session },
66
+ })
67
+ assert.equal(selectActiveSessionId(state), 'thread-1')
68
+ })
69
+
70
+ test('selectActiveSessionId falls back to thread session when all loaded sessions are empty', () => {
71
+ const state = makeState({
72
+ currentAgentId: 'agent-1',
73
+ agents: { 'agent-1': makeAgent('agent-1', 'thread-1') },
74
+ sessions: {
75
+ 'thread-1': { ...makeSession('thread-1'), agentId: 'agent-1', lastActiveAt: 120 } as unknown as Session,
76
+ 'empty-newer': { ...makeSession('empty-newer'), agentId: 'agent-1', lastActiveAt: 220 } as unknown as Session,
77
+ },
40
78
  })
41
79
  assert.equal(selectActiveSessionId(state), 'thread-1')
42
80
  })
@@ -8,6 +8,46 @@ import { createLoader, createInflightDeduplicator } from '../store-utils'
8
8
 
9
9
  const sessionRefreshDedup = createInflightDeduplicator('sessionSlice_inflightRefreshes')
10
10
 
11
+ function getSessionSortScore(session: Session): number {
12
+ return session.lastAssistantAt
13
+ || session.lastActiveAt
14
+ || session.updatedAt
15
+ || session.createdAt
16
+ || 0
17
+ }
18
+
19
+ function hasSessionContent(session: Session): boolean {
20
+ if (typeof session.messageCount === 'number' && Number.isFinite(session.messageCount) && session.messageCount > 0) {
21
+ return true
22
+ }
23
+ if (session.lastMessageSummary) return true
24
+ return Array.isArray(session.messages) && session.messages.length > 0
25
+ }
26
+
27
+ function getLatestAgentSessionId(s: AppState, agentId: string, threadSessionId?: string | null): string | null {
28
+ let bestAnyId: string | null = null
29
+ let bestAnyScore = Number.NEGATIVE_INFINITY
30
+ let bestWithContentId: string | null = null
31
+ let bestWithContentScore = Number.NEGATIVE_INFINITY
32
+
33
+ for (const [sessionId, session] of Object.entries(s.sessions)) {
34
+ if (session.agentId !== agentId) continue
35
+ const score = getSessionSortScore(session)
36
+ if (score > bestAnyScore) {
37
+ bestAnyScore = score
38
+ bestAnyId = sessionId
39
+ }
40
+ if (hasSessionContent(session) && score > bestWithContentScore) {
41
+ bestWithContentScore = score
42
+ bestWithContentId = sessionId
43
+ }
44
+ }
45
+
46
+ if (bestWithContentId) return bestWithContentId
47
+ if (threadSessionId && s.sessions[threadSessionId]?.agentId === agentId) return threadSessionId
48
+ return bestAnyId
49
+ }
50
+
11
51
  /** Derive the active session ID from the current agent — no stored `currentSessionId`. */
12
52
  export function selectActiveSessionId(s: AppState): string | null {
13
53
  if (s.activeSessionIdOverride && s.sessions[s.activeSessionIdOverride]) {
@@ -15,7 +55,7 @@ export function selectActiveSessionId(s: AppState): string | null {
15
55
  }
16
56
  if (!s.currentAgentId) return null
17
57
  const agent = s.agents[s.currentAgentId]
18
- return agent?.threadSessionId ?? null
58
+ return getLatestAgentSessionId(s, s.currentAgentId, agent?.threadSessionId) || agent?.threadSessionId || null
19
59
  }
20
60
 
21
61
  export interface SessionSlice {