File size: 2,747 Bytes
5b5da0e
 
cae0460
5b5da0e
 
 
 
 
 
 
 
 
 
 
 
 
ece21ed
 
 
 
 
 
5b5da0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd0a8b6
 
5b5da0e
 
 
 
 
 
 
 
 
 
fd0a8b6
5b5da0e
 
fd0a8b6
 
 
 
 
5b5da0e
 
 
 
cae0460
5b5da0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import fs from 'node:fs';
import crypto from 'node:crypto';
import { SESSIONS_FILE } from './config.js';

let sessions = [];

function load() {
  try {
    sessions = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
    if (!Array.isArray(sessions)) sessions = [];
  } catch {
    sessions = [];
  }
}

function persist() {
  // A FUSE write hiccup must not throw out of a timer/handler and crash us.
  try {
    const tmp = `${SESSIONS_FILE}.tmp`;
    fs.writeFileSync(tmp, JSON.stringify(sessions, null, 2));
    fs.renameSync(tmp, SESSIONS_FILE);
  } catch (e) { console.error('[sessions.persist]', e && e.message); }
}

function slugify(name) {
  return (
    name
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '')
      .slice(0, 32) || 'session'
  );
}

export function init() {
  load();
  let changed = false;
  for (const s of sessions) {
    if (!s.sessionUuid) { s.sessionUuid = crypto.randomUUID(); changed = true; }
  }
  if (changed) persist();
  // NOTE: `path` migration from the old folder/group-folder model happens in
  // index.js (it needs the groups store).
}

export function list() {
  return sessions.slice();
}

export function get(id) {
  return sessions.find((s) => s.id === id) || null;
}

export function create({ name, cli, path }) {
  const cleanName = (name || '').trim() || 'session';
  const id = `${slugify(cleanName)}-${crypto.randomBytes(3).toString('hex')}`;
  // `path` is the workspace-relative folder the agent runs in, chosen at
  // creation (or defaulted by the caller). It is a plain recorded value — the
  // display name can change freely without touching disk, and nothing tracks
  // the folder afterwards (deleted/moved folders are simply re-created empty on
  // next start). Files agents may use null = browse the whole workspace root.
  const session = {
    id,
    name: cleanName,
    cli,
    path: path ?? (cli === 'files' ? null : ''),
    // Stable per-session conversation id. Lets agents that share a folder (a
    // group) each resume their OWN conversation instead of all latching onto
    // the most-recent one in that directory.
    sessionUuid: crypto.randomUUID(),
    createdAt: new Date().toISOString(),
    everStarted: false,
  };
  sessions.push(session);
  persist();
  return session;
}

export function update(id, patch) {
  const s = get(id);
  if (!s) return null;
  Object.assign(s, patch);
  persist();
  return s;
}

export function remove(id) {
  const before = sessions.length;
  sessions = sessions.filter((s) => s.id !== id);
  if (sessions.length !== before) persist();
  // NOTE: the working directory under DATA_DIR/workspaces/<id> is intentionally
  // left on disk so a delete never destroys the user's files.
}