File size: 3,709 Bytes
e249c6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Covers command-session resolution for a yielded parent whose children complete
// after its transcript was admitted: the parent generation must survive.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
  appendTranscriptEvent,
  loadSessionEntry,
  upsertSessionEntryCore,
} from "../../config/sessions/session-accessor.js";
import { deriveGatewaySessionLifecycleSnapshot } from "../../gateway/session-lifecycle-state.js";
import {
  closeOpenClawAgentDatabasesForTest,
  resolveOpenClawAgentSqlitePath,
} from "../../state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import { resolveSession } from "./session.js";

describe("resolveSession with a yielded running parent", () => {
  let stateDir: string;
  let storePath: string;

  beforeEach(() => {
    stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-yielded-parent-"));
    storePath = path.join(stateDir, "agents", "main", "sessions", "sessions.json");
  });

  afterEach(() => {
    closeOpenClawAgentDatabasesForTest();
    closeOpenClawStateDatabaseForTest();
    fs.rmSync(stateDir, { recursive: true, force: true });
  });

  it("keeps the parent session generation across sibling completions", async () => {
    const agentId = "main";
    const sessionKey = "agent:main:main";
    const parentSessionId = "parent-session-0001";
    const cfg = { session: { store: storePath } } as OpenClawConfig;
    const startedAt = Date.now() - 60_000;
    const yieldedAt = startedAt + 20_000;

    // The parent run starts, then yields; the Gateway projection keeps it running
    // while recording the settled run's endedAt.
    const running = deriveGatewaySessionLifecycleSnapshot({
      session: { updatedAt: startedAt },
      event: {
        ts: startedAt,
        sessionId: parentSessionId,
        runId: "parent-run",
        data: { phase: "start", startedAt },
      },
    });
    const yielded = deriveGatewaySessionLifecycleSnapshot({
      session: running,
      event: {
        ts: yieldedAt,
        sessionId: parentSessionId,
        runId: "parent-run",
        data: {
          phase: "end",
          endedAt: yieldedAt,
          yielded: true,
          livenessState: "paused",
          stopReason: "end_turn",
        },
      },
    });
    expect(yielded).toMatchObject({ status: "running", endedAt: yieldedAt });
    await upsertSessionEntryCore(
      { agentId, sessionKey, storePath },
      {
        sessionId: parentSessionId,
        sessionFile: `sqlite:main:${parentSessionId}:${resolveOpenClawAgentSqlitePath({
          agentId,
          env: { OPENCLAW_STATE_DIR: stateDir },
        })}`,
        updatedAt: yieldedAt,
        startedAt,
        status: yielded.status,
        endedAt: yielded.endedAt,
      },
    );

    const first = resolveSession({ cfg, sessionKey, agentId });
    expect(first.sessionId).toBe(parentSessionId);

    // The first completion's prompt admission lands after the registry row.
    await appendTranscriptEvent(
      { agentId, sessionId: parentSessionId, sessionKey, storePath },
      { type: "custom", timestamp: new Date().toISOString() },
    );
    expect(loadSessionEntry({ agentId, sessionKey, storePath })).toMatchObject({
      status: "running",
      endedAt: yieldedAt,
    });

    const second = resolveSession({ cfg, sessionKey, agentId });
    expect(second.sessionId).toBe(parentSessionId);
    expect(second.isNewSession).toBe(false);
    expect(second.previousSessionId).toBeUndefined();
  });
});