@principal-ai/subsystems-studio 0.6.8 → 0.6.9

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.
@@ -10655,7 +10655,7 @@ import {
10655
10655
  import { homedir as homedir2 } from "os";
10656
10656
  import { join as join3 } from "path";
10657
10657
  var CACHE_ROOT = join3(homedir2(), ".principal", "session-events");
10658
- var PROCESSING_VERSION = 1;
10658
+ var PROCESSING_VERSION = 2;
10659
10659
  var KEEP_DAYS = 14;
10660
10660
  function dayKeyOf(date) {
10661
10661
  const y = date.getFullYear();
@@ -10745,6 +10745,24 @@ function trimSessionEventRows(events) {
10745
10745
 
10746
10746
  // src/bun/session-pipeline.ts
10747
10747
  import { statSync as statSync3 } from "fs";
10748
+
10749
+ // src/bun/maintain-sessions.ts
10750
+ function isMaintainSessionTitle(title) {
10751
+ return /^Maintain\s*[\u2014\u2013-]/.test(title.trim());
10752
+ }
10753
+ function isMaintainAgentName(agent) {
10754
+ return agent === "issue-fixer" || agent === "gap-filler";
10755
+ }
10756
+ function isMaintainSession(opts) {
10757
+ if (isMaintainAgentName(opts.agent))
10758
+ return true;
10759
+ if (opts.title && isMaintainSessionTitle(opts.title))
10760
+ return true;
10761
+ return false;
10762
+ }
10763
+
10764
+ // src/bun/opencode-v2-messages.ts
10765
+ import { Database } from "bun:sqlite";
10748
10766
  function openCodeDBPath() {
10749
10767
  const env = process.env;
10750
10768
  if (env["OPENCODE_DATA_DIR"])
@@ -10753,6 +10771,222 @@ function openCodeDBPath() {
10753
10771
  const xdgData = env["XDG_DATA_HOME"] || `${home}/.local/share`;
10754
10772
  return `${xdgData}/opencode/opencode.db`;
10755
10773
  }
10774
+ function tableExists(db, name) {
10775
+ const row = db.prepare(`SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?`).get(name);
10776
+ return row != null;
10777
+ }
10778
+ function openReadonlyDb() {
10779
+ return new Database(openCodeDBPath(), { readonly: true });
10780
+ }
10781
+ function resolveOpencodeSessionKind(sessionId, db) {
10782
+ const owned = !db;
10783
+ let conn = db ?? null;
10784
+ try {
10785
+ if (!conn)
10786
+ conn = openReadonlyDb();
10787
+ if (tableExists(conn, "session_v2")) {
10788
+ const v2 = conn.prepare(`SELECT 1 AS ok FROM session_v2 WHERE id = ?`).get(sessionId);
10789
+ if (v2)
10790
+ return "v2";
10791
+ }
10792
+ if (tableExists(conn, "session")) {
10793
+ const v1 = conn.prepare(`SELECT 1 AS ok FROM session WHERE id = ?`).get(sessionId);
10794
+ if (v1)
10795
+ return "v1";
10796
+ }
10797
+ return null;
10798
+ } catch {
10799
+ return null;
10800
+ } finally {
10801
+ if (owned) {
10802
+ try {
10803
+ conn?.close();
10804
+ } catch {}
10805
+ }
10806
+ }
10807
+ }
10808
+ function isOpencodeV2Session(sessionId) {
10809
+ return resolveOpencodeSessionKind(sessionId) === "v2";
10810
+ }
10811
+ function readOpencodeV2Session(sessionId) {
10812
+ let db = null;
10813
+ try {
10814
+ db = openReadonlyDb();
10815
+ if (!tableExists(db, "session_v2"))
10816
+ return null;
10817
+ const metaRow = db.prepare(`SELECT id, title, slug, agent, directory, version
10818
+ FROM session_v2 WHERE id = ?`).get(sessionId);
10819
+ if (!metaRow)
10820
+ return null;
10821
+ const messages = tableExists(db, "session_message") ? db.prepare(`SELECT id, session_id, type, seq, time_created, data
10822
+ FROM session_message
10823
+ WHERE session_id = ?
10824
+ ORDER BY seq ASC`).all(sessionId) : [];
10825
+ return {
10826
+ meta: {
10827
+ id: metaRow.id,
10828
+ title: (metaRow.title ?? "").trim() || metaRow.id.slice(0, 12),
10829
+ slug: metaRow.slug ?? "",
10830
+ agent: metaRow.agent ?? undefined,
10831
+ directory: metaRow.directory ?? undefined,
10832
+ version: metaRow.version ?? undefined
10833
+ },
10834
+ messages
10835
+ };
10836
+ } catch {
10837
+ return null;
10838
+ } finally {
10839
+ try {
10840
+ db?.close();
10841
+ } catch {}
10842
+ }
10843
+ }
10844
+ function mapToolName(name) {
10845
+ const n = name.trim().toLowerCase();
10846
+ if (n === "shell" || n === "bash")
10847
+ return "Bash";
10848
+ if (n === "read")
10849
+ return "Read";
10850
+ if (n === "write")
10851
+ return "Write";
10852
+ if (n === "edit" || n === "multiedit")
10853
+ return "Edit";
10854
+ if (n === "glob")
10855
+ return "Glob";
10856
+ if (n === "grep")
10857
+ return "Grep";
10858
+ if (n === "list" || n === "ls")
10859
+ return "Bash";
10860
+ if (n === "webfetch")
10861
+ return "WebFetch";
10862
+ if (n === "websearch")
10863
+ return "WebSearch";
10864
+ if (n === "task")
10865
+ return "Task";
10866
+ return name;
10867
+ }
10868
+ function pathsFromToolInput(input) {
10869
+ if (!input || typeof input !== "object")
10870
+ return [];
10871
+ const obj = input;
10872
+ const paths = [];
10873
+ for (const key of ["filePath", "file_path", "path", "pattern", "glob"]) {
10874
+ if (typeof obj[key] === "string" && obj[key].trim()) {
10875
+ paths.push(obj[key]);
10876
+ }
10877
+ }
10878
+ return paths;
10879
+ }
10880
+ function applyFileContext(event, toolName, toolInput) {
10881
+ const paths = pathsFromToolInput(toolInput);
10882
+ if (paths.length === 0)
10883
+ return;
10884
+ event.rawFilePaths = [...new Set(paths)];
10885
+ const op = getFileOperation(toolName);
10886
+ if (op)
10887
+ event.operation = op;
10888
+ }
10889
+ function sessionMessagesToUniversalEvents(sessionId, messages, opts) {
10890
+ const workingDirectory = opts?.workingDirectory?.trim() || "";
10891
+ const out = [];
10892
+ for (const row of messages) {
10893
+ let parsed;
10894
+ try {
10895
+ parsed = JSON.parse(row.data);
10896
+ } catch {
10897
+ continue;
10898
+ }
10899
+ const time = parsed["time"];
10900
+ const created = typeof time?.["created"] === "number" ? time["created"] : typeof row.time_created === "number" ? row.time_created : Date.now();
10901
+ const base = {
10902
+ sessionId,
10903
+ workingDirectory,
10904
+ timestamp: created,
10905
+ provider: SupportedAgent.OPENCODE,
10906
+ raw: { id: row.id, type: row.type, seq: row.seq, data: parsed }
10907
+ };
10908
+ if (row.type === "user") {
10909
+ const text = typeof parsed["text"] === "string" ? parsed["text"] : "";
10910
+ out.push({
10911
+ ...base,
10912
+ eventType: "user-prompt-submit",
10913
+ data: { prompt: text, messageID: row.id }
10914
+ });
10915
+ continue;
10916
+ }
10917
+ if (row.type !== "assistant")
10918
+ continue;
10919
+ const content = Array.isArray(parsed["content"]) ? parsed["content"] : [];
10920
+ for (const part of content) {
10921
+ const partType = typeof part["type"] === "string" ? part["type"] : "";
10922
+ const partTime = part["time"];
10923
+ const partCreated = typeof partTime?.["created"] === "number" ? partTime["created"] : typeof partTime?.["completed"] === "number" ? partTime["completed"] : created;
10924
+ const partBase = {
10925
+ ...base,
10926
+ timestamp: partCreated,
10927
+ raw: { ...base.raw, part }
10928
+ };
10929
+ if (partType === "reasoning") {
10930
+ const text = typeof part["text"] === "string" ? part["text"] : "";
10931
+ out.push({
10932
+ ...partBase,
10933
+ eventType: "model-reasoning",
10934
+ data: { messageID: row.id, message: text }
10935
+ });
10936
+ continue;
10937
+ }
10938
+ if (partType === "text") {
10939
+ const text = typeof part["text"] === "string" ? part["text"] : "";
10940
+ out.push({
10941
+ ...partBase,
10942
+ eventType: "notification",
10943
+ data: { messageID: row.id, message: text }
10944
+ });
10945
+ continue;
10946
+ }
10947
+ if (partType !== "tool")
10948
+ continue;
10949
+ const rawName = typeof part["name"] === "string" ? part["name"] : typeof part["tool"] === "string" ? part["tool"] : "unknown";
10950
+ const toolName = mapToolName(rawName);
10951
+ const callID = typeof part["id"] === "string" ? part["id"] : `${row.id}-${partCreated}`;
10952
+ const state = typeof part["state"] === "object" && part["state"] !== null ? part["state"] : {};
10953
+ const input = state["input"];
10954
+ const status = typeof state["status"] === "string" ? state["status"].toLowerCase() : "";
10955
+ const failed = status.includes("fail") || status.includes("error") || part["error"] != null;
10956
+ const pre = {
10957
+ ...partBase,
10958
+ eventType: "pre-tool-use",
10959
+ toolName,
10960
+ toolInput: input,
10961
+ data: { callID, tool: rawName }
10962
+ };
10963
+ applyFileContext(pre, rawName, input);
10964
+ out.push(pre);
10965
+ const post = {
10966
+ ...partBase,
10967
+ timestamp: typeof partTime?.["completed"] === "number" ? partTime["completed"] : partCreated,
10968
+ eventType: failed ? "post-tool-use-failure" : "post-tool-use",
10969
+ toolName,
10970
+ toolInput: input,
10971
+ toolOutput: state["content"] ?? state["output"] ?? state["metadata"],
10972
+ data: { callID, tool: rawName, status: state["status"] }
10973
+ };
10974
+ applyFileContext(post, rawName, input);
10975
+ out.push(post);
10976
+ }
10977
+ }
10978
+ return out;
10979
+ }
10980
+
10981
+ // src/bun/session-pipeline.ts
10982
+ function openCodeDBPath2() {
10983
+ const env = process.env;
10984
+ if (env["OPENCODE_DATA_DIR"])
10985
+ return `${env["OPENCODE_DATA_DIR"]}/opencode/opencode.db`;
10986
+ const home = env["HOME"] || env["USERPROFILE"] || "/root";
10987
+ const xdgData = env["XDG_DATA_HOME"] || `${home}/.local/share`;
10988
+ return `${xdgData}/opencode/opencode.db`;
10989
+ }
10756
10990
  var INCLUDE_RAW_EVENT_PAYLOADS = (process.env["TRAIL_INCLUDE_RAW"] ?? "") === "1";
10757
10991
  function wantRawPayloads(requestIncludeRaw) {
10758
10992
  return INCLUDE_RAW_EVENT_PAYLOADS || requestIncludeRaw;
@@ -10778,6 +11012,104 @@ var cursorReader = new CursorSessionReader;
10778
11012
  function isCursorSession(sessionId) {
10779
11013
  return cursorReader.readSession(sessionId) !== null;
10780
11014
  }
11015
+ async function runOpencodeV2MessagePipeline(sessionId) {
11016
+ const record = readOpencodeV2Session(sessionId);
11017
+ if (!record)
11018
+ return null;
11019
+ const sessionTitle = record.meta.title || "OpenCode V2 session";
11020
+ const sessionSlug = record.meta.slug || "";
11021
+ const workingDirectory = record.meta.directory ?? "";
11022
+ const rawEvents = sessionMessagesToUniversalEvents(sessionId, record.messages, {
11023
+ workingDirectory
11024
+ });
11025
+ if (rawEvents.length === 0)
11026
+ return null;
11027
+ const alexandriaRepos = loadAlexandriaRepos();
11028
+ const knownRoots = new Map;
11029
+ for (const [path, repo] of alexandriaRepos) {
11030
+ knownRoots.set(path, {
11031
+ root: repo.root,
11032
+ remoteUrl: repo.remoteUrl,
11033
+ owner: repo.owner,
11034
+ repo: repo.repo
11035
+ });
11036
+ }
11037
+ const adapter = new BunNormalizationAdapter(knownRoots);
11038
+ const normalizationService = new PathNormalizationService(adapter);
11039
+ const normalizedEvents = await normalizationService.normalizePathsBatch(rawEvents, workingDirectory);
11040
+ for (const discovered of adapter.newlyDiscovered) {
11041
+ registerProjectInAlexandria(discovered.root, discovered.remoteUrl);
11042
+ }
11043
+ const accState = createAccumulatedState(sessionTitle);
11044
+ const events = [];
11045
+ const repoSet = new Map;
11046
+ for (let i = 0;i < normalizedEvents.length; i++) {
11047
+ const normalizedEvent = normalizedEvents[i];
11048
+ const accResult = eventOp(accState, normalizedEvent);
11049
+ events.push({
11050
+ seq: i,
11051
+ type: normalizedEvent.eventType,
11052
+ raw: INCLUDE_RAW_EVENT_PAYLOADS ? normalizedEvent.raw : undefined,
11053
+ normalized: INCLUDE_RAW_EVENT_PAYLOADS ? normalizedEvent : { timestamp: normalizedEvent.timestamp },
11054
+ accumulated: accResult
11055
+ });
11056
+ if (normalizedEvent.files) {
11057
+ for (const f of normalizedEvent.files) {
11058
+ const root = f.repository?.gitRoot;
11059
+ if (root) {
11060
+ const entry = repoSet.get(root) ?? { root, fileCount: 0 };
11061
+ entry.fileCount++;
11062
+ repoSet.set(root, entry);
11063
+ }
11064
+ }
11065
+ }
11066
+ }
11067
+ const lastEvent = events[events.length - 1];
11068
+ if (lastEvent) {
11069
+ const lastTimestamp = lastEvent.normalized["timestamp"] ?? 0;
11070
+ events.push({
11071
+ seq: lastEvent.seq + 1,
11072
+ type: "finished",
11073
+ raw: null,
11074
+ normalized: { timestamp: lastTimestamp },
11075
+ accumulated: {
11076
+ id: "",
11077
+ timestamp: lastTimestamp,
11078
+ sessionId: sessionSlug || sessionId,
11079
+ sessionName: accState.sessionName,
11080
+ sessionColor: accState.sessionColor,
11081
+ operation: "finished",
11082
+ files: [],
11083
+ dependencies: [],
11084
+ description: `${accState.sessionName} finished`,
11085
+ layers: [],
11086
+ contextTokens: accState.contextTokens
11087
+ }
11088
+ });
11089
+ }
11090
+ const repos = Array.from(repoSet.values()).sort((a, b) => b.fileCount - a.fileCount).map((r) => {
11091
+ const parts = r.root.replace(/\/+$/, "").split("/");
11092
+ const known = knownRoots.get(r.root);
11093
+ return {
11094
+ root: r.root,
11095
+ fileCount: r.fileCount,
11096
+ owner: known?.owner ?? null,
11097
+ name: parts[parts.length - 1] ?? null,
11098
+ editing: false
11099
+ };
11100
+ });
11101
+ const repoRoot = repos.length > 0 ? repos[0].root : undefined;
11102
+ return {
11103
+ rawEvents,
11104
+ normalizedEvents,
11105
+ accState,
11106
+ events,
11107
+ repos,
11108
+ repoRoot,
11109
+ sessionTitle,
11110
+ sessionSlug
11111
+ };
11112
+ }
10781
11113
  async function runClinePipeline(sessionId) {
10782
11114
  const record = clineReader.readSession(sessionId);
10783
11115
  if (!record)
@@ -11241,7 +11573,7 @@ function dbFingerprint(dbPath) {
11241
11573
  async function buildSessionIndex({ days }) {
11242
11574
  const dayCount = Math.max(1, Math.floor(days ?? 7));
11243
11575
  const cutoff = Date.now() - dayCount * 86400000;
11244
- const dbPath = openCodeDBPath();
11576
+ const dbPath = openCodeDBPath2();
11245
11577
  const cacheKey = `${dayCount}|${dbFingerprint(dbPath)}`;
11246
11578
  const cached = indexCache.get(cacheKey);
11247
11579
  if (cached)
@@ -11249,8 +11581,8 @@ async function buildSessionIndex({ days }) {
11249
11581
  let result;
11250
11582
  let db = null;
11251
11583
  try {
11252
- const { Database } = await import("bun:sqlite");
11253
- db = new Database(dbPath, { readonly: true });
11584
+ const { Database: Database2 } = await import("bun:sqlite");
11585
+ db = new Database2(dbPath, { readonly: true });
11254
11586
  const firstEvents = db.prepare(`SELECT
11255
11587
  e.aggregate_id,
11256
11588
  e.data
@@ -11263,6 +11595,7 @@ async function buildSessionIndex({ days }) {
11263
11595
  let title = row.aggregate_id.slice(0, 12);
11264
11596
  let slug = "";
11265
11597
  let createdAtStr = "";
11598
+ let agentFromInfo;
11266
11599
  const durationMs = 0;
11267
11600
  try {
11268
11601
  const parsed = JSON.parse(row.data);
@@ -11275,12 +11608,18 @@ async function buildSessionIndex({ days }) {
11275
11608
  if (typeof rawSlug === "string") {
11276
11609
  slug = rawSlug;
11277
11610
  }
11611
+ const rawAgent = info?.["agent"];
11612
+ if (typeof rawAgent === "string") {
11613
+ agentFromInfo = rawAgent;
11614
+ }
11278
11615
  const rawTime = info?.["time"];
11279
11616
  const rawCreated = rawTime?.["created"];
11280
11617
  if (typeof rawCreated === "number") {
11281
11618
  createdAtStr = new Date(rawCreated).toISOString();
11282
11619
  }
11283
11620
  } catch {}
11621
+ if (isMaintainSession({ title, agent: agentFromInfo }))
11622
+ continue;
11284
11623
  const createdMs = createdAtStr ? new Date(createdAtStr).getTime() : 0;
11285
11624
  if (!createdMs)
11286
11625
  continue;
@@ -11606,6 +11945,17 @@ async function processSessionEvents(sessionId, opts) {
11606
11945
  return { ok: false, error: err.message };
11607
11946
  }
11608
11947
  }
11948
+ if (isOpencodeV2Session(sessionId)) {
11949
+ try {
11950
+ const result = await runOpencodeV2MessagePipeline(sessionId);
11951
+ if (!result) {
11952
+ return { ok: false, error: "OpenCode V2 session not found or empty" };
11953
+ }
11954
+ return respondWithCachedPipeline(sessionId, "opencode-v2", result);
11955
+ } catch (err) {
11956
+ return { ok: false, error: err.message };
11957
+ }
11958
+ }
11609
11959
  const cacheKey = `opencode:${sessionId}:${includeRaw ? "raw" : "min"}`;
11610
11960
  let events = sessionEventsCache.get(cacheKey);
11611
11961
  let sessionSlug = "";
@@ -11613,11 +11963,11 @@ async function processSessionEvents(sessionId, opts) {
11613
11963
  let repoRoot;
11614
11964
  let repos = [];
11615
11965
  if (!events) {
11616
- const dbPath = openCodeDBPath();
11966
+ const dbPath = openCodeDBPath2();
11617
11967
  let db = null;
11618
11968
  try {
11619
- const { Database } = await import("bun:sqlite");
11620
- db = new Database(dbPath, { readonly: true });
11969
+ const { Database: Database2 } = await import("bun:sqlite");
11970
+ db = new Database2(dbPath, { readonly: true });
11621
11971
  const rows = db.prepare(`SELECT id, aggregate_id, seq, type, data FROM event WHERE aggregate_id = ? ORDER BY seq ASC`).all(sessionId);
11622
11972
  if (rows.length > 0) {
11623
11973
  try {
@@ -11711,13 +12061,15 @@ async function processSessionEvents(sessionId, opts) {
11711
12061
  }).sort((a, b) => b.fileCount - a.fileCount);
11712
12062
  repoRoot = repos.length > 0 ? repos[0].root : undefined;
11713
12063
  events = built;
11714
- writeCachedSessionEvents(sessionId, {
11715
- agent: "opencode",
11716
- session: { slug: sessionSlug, title: sessionTitle, agent: "opencode" },
11717
- repoRoot,
11718
- repos,
11719
- events: trimSessionEventRows(built)
11720
- });
12064
+ if (built.length > 0) {
12065
+ writeCachedSessionEvents(sessionId, {
12066
+ agent: "opencode",
12067
+ session: { slug: sessionSlug, title: sessionTitle, agent: "opencode" },
12068
+ repoRoot,
12069
+ repos,
12070
+ events: trimSessionEventRows(built)
12071
+ });
12072
+ }
11721
12073
  sessionEventsCache.set(cacheKey, events);
11722
12074
  while (sessionEventsCache.size > 4) {
11723
12075
  const oldestKey = sessionEventsCache.keys().next().value;
@@ -2,5 +2,5 @@
2
2
  "identifier": "ai.principal.subsystems-studio",
3
3
  "name": "Subsystems Studio",
4
4
  "channel": "stable",
5
- "hash": "ct2o0q8id5eq"
5
+ "hash": "13kyfkdd9mdni"
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@principal-ai/subsystems-studio",
3
- "version": "0.6.8",
4
- "description": "Subsystems Studio — desktop host for subsystem models, throughlines, trails, and agent sessions. Pairs with the principal-ai CLI.",
3
+ "version": "0.6.9",
4
+ "description": "Subsystems Studio — desktop host for subsystem models, walkthroughs, trails, and agent sessions. Pairs with the principal-ai CLI.",
5
5
  "type": "module",
6
6
  "os": [
7
7
  "darwin"
@@ -44,8 +44,8 @@
44
44
  "@principal-ai/file-city-react": "^0.5.91",
45
45
  "@principal-ai/logo-component": "^0.1.23",
46
46
  "@principal-ai/repository-abstraction": "^0.5.7",
47
- "@principal-ai/subsystems-core": "0.31.4",
48
- "@principal-ai/subsystems-react": "0.20.6",
47
+ "@principal-ai/subsystems-core": "0.32.0",
48
+ "@principal-ai/subsystems-react": "0.21.1",
49
49
  "@types/bun": "latest",
50
50
  "@types/react": "^19.2.17",
51
51
  "@types/react-dom": "^19.2.3",