@wrongstack/core 0.8.6 → 0.9.0

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,6 +1,6 @@
1
1
  export { B as BindOptions, C as Container, D as Decorator, F as Factory, M as Middleware, b as MiddlewareHandler, N as NextFn, P as Pipeline, d as PipelineOptions, T as Token } from '../system-prompt-CWA6ml-d.js';
2
2
  export { E as EventBus, a as EventLogger, b as EventMap, c as EventName, L as Listener, S as ScopedEventBus } from '../events-yCpJD4wa.js';
3
- export { R as RunController, b as RunControllerOptions, T as TOKENS } from '../index-BvZld4Dr.js';
3
+ export { R as RunController, b as RunControllerOptions, T as TOKENS } from '../index-Dl3PaD7f.js';
4
4
  import '../context-z2x5gv_V.js';
5
5
  import '../compactor-Mw7-rNyb.js';
6
6
  import '../config-Bi4Q0fnz.js';
@@ -331,9 +331,14 @@ declare class RecoveryLock {
331
331
  */
332
332
  checkAbandoned(): Promise<AbandonedSession | null>;
333
333
  /**
334
- * Claim the lock for the given session. Overwrites any existing lock
335
- * the caller should have already handled abandonment (via
336
- * `checkAbandoned`) before calling this.
334
+ * Claim the lock for the given session. Uses exclusive-create (`O_EXCL`)
335
+ * to detect whether another process acquired the lock between our
336
+ * `checkAbandoned()` call and now. If the file already exists, it means
337
+ * another process won the race and we throw instead of silently
338
+ * overwriting their recovery record.
339
+ *
340
+ * The caller MUST have already called `checkAbandoned()` and handled its
341
+ * null return before calling this.
337
342
  */
338
343
  write(sessionId: string): Promise<void>;
339
344
  /**
@@ -1,4 +1,4 @@
1
- export { A as AbandonedSession, a as AttachmentStoreOptions, C as ConfigLoaderOptions, b as ConfigMigration, c as ConfigMigrationError, d as ConfigSource, D as DEFAULT_CONFIG_MIGRATIONS, e as DefaultAttachmentStore, f as DefaultConfigLoader, g as DefaultConfigStore, h as DefaultMemoryStore, i as DefaultSessionStore, M as MemoryStoreOptions, j as MigrationContext, k as MigrationResult, P as PersistedQueueItem, l as PlanFile, m as PlanItem, n as PlanTemplate, Q as QueueStore, R as RecoveryLock, o as RecoveryLockOptions, S as SessionAnalyzer, p as SessionStoreOptions, T as TodosCheckpointFile, q as addPlanItem, r as attachPlanCheckpoint, s as attachTodosCheckpoint, t as clearPlan, u as deriveTodosFromPlanItem, v as emptyPlan, w as formatPlan, x as formatPlanTemplates, y as getPlanTemplate, z as listPlanTemplates, B as loadPlan, E as loadTodosCheckpoint, F as removePlanItem, G as runConfigMigrations, H as savePlan, I as saveTodosCheckpoint, J as setPlanItemStatus } from '../plan-templates-Cqdy6rtM.js';
1
+ export { A as AbandonedSession, a as AttachmentStoreOptions, C as ConfigLoaderOptions, b as ConfigMigration, c as ConfigMigrationError, d as ConfigSource, D as DEFAULT_CONFIG_MIGRATIONS, e as DefaultAttachmentStore, f as DefaultConfigLoader, g as DefaultConfigStore, h as DefaultMemoryStore, i as DefaultSessionStore, M as MemoryStoreOptions, j as MigrationContext, k as MigrationResult, P as PersistedQueueItem, l as PlanFile, m as PlanItem, n as PlanTemplate, Q as QueueStore, R as RecoveryLock, o as RecoveryLockOptions, S as SessionAnalyzer, p as SessionStoreOptions, T as TodosCheckpointFile, q as addPlanItem, r as attachPlanCheckpoint, s as attachTodosCheckpoint, t as clearPlan, u as deriveTodosFromPlanItem, v as emptyPlan, w as formatPlan, x as formatPlanTemplates, y as getPlanTemplate, z as listPlanTemplates, B as loadPlan, E as loadTodosCheckpoint, F as removePlanItem, G as runConfigMigrations, H as savePlan, I as saveTodosCheckpoint, J as setPlanItemStatus } from '../plan-templates-Cc-04Z8o.js';
2
2
  export { D as DefaultSessionReader } from '../session-reader-DsadjyF9.js';
3
3
  import { S as SessionRewinder, C as CheckpointInfo, a as RewindResultExtended } from '../session-rewinder-C9HnMkhP.js';
4
4
  export { D as DirectorStateCheckpoint, a as DirectorStateSnapshot, b as DirectorSubagentState, c as DirectorTaskState, l as loadDirectorState } from '../director-state-BmYi3DGA.js';
@@ -13,6 +13,8 @@ import '../secret-vault-DoISxaKO.js';
13
13
 
14
14
  interface SessionRewinderOptions {
15
15
  sessionsDir: string;
16
+ /** The project root directory; used to validate rewind targets stay inside it. */
17
+ projectRoot: string;
16
18
  }
17
19
  /**
18
20
  * Rewind engine that reads session JSONL files and reverts file system
@@ -20,7 +22,8 @@ interface SessionRewinderOptions {
20
22
  */
21
23
  declare class DefaultSessionRewinder implements SessionRewinder {
22
24
  private readonly sessionsDir;
23
- constructor(sessionsDir: string);
25
+ private readonly projectRoot;
26
+ constructor(sessionsDir: string, projectRoot: string);
24
27
  listCheckpoints(sessionId: string): Promise<CheckpointInfo[]>;
25
28
  rewindToCheckpoint(sessionId: string, checkpointIndex: number): Promise<RewindResultExtended>;
26
29
  rewindLastN(sessionId: string, n: number): Promise<RewindResultExtended>;
@@ -68,7 +68,7 @@ async function renameWithRetry(from, to) {
68
68
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
69
69
  throw err;
70
70
  }
71
- await new Promise((resolve2) => setTimeout(resolve2, delays[i]));
71
+ await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
72
72
  }
73
73
  }
74
74
  throw lastErr;
@@ -1488,9 +1488,14 @@ var RecoveryLock = class {
1488
1488
  };
1489
1489
  }
1490
1490
  /**
1491
- * Claim the lock for the given session. Overwrites any existing lock
1492
- * the caller should have already handled abandonment (via
1493
- * `checkAbandoned`) before calling this.
1491
+ * Claim the lock for the given session. Uses exclusive-create (`O_EXCL`)
1492
+ * to detect whether another process acquired the lock between our
1493
+ * `checkAbandoned()` call and now. If the file already exists, it means
1494
+ * another process won the race and we throw instead of silently
1495
+ * overwriting their recovery record.
1496
+ *
1497
+ * The caller MUST have already called `checkAbandoned()` and handled its
1498
+ * null return before calling this.
1494
1499
  */
1495
1500
  async write(sessionId) {
1496
1501
  await ensureDir(path2.dirname(this.file));
@@ -1501,7 +1506,15 @@ var RecoveryLock = class {
1501
1506
  hostname: this.hostname,
1502
1507
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1503
1508
  };
1504
- await atomicWrite(this.file, JSON.stringify(lock), { mode: 384 });
1509
+ try {
1510
+ await fsp.writeFile(this.file, JSON.stringify(lock), { flag: "wx", mode: 384 });
1511
+ } catch (err) {
1512
+ const code = err.code;
1513
+ if (code === "EEXIST") {
1514
+ throw new Error(`Recovery lock already held by another process`);
1515
+ }
1516
+ throw err;
1517
+ }
1505
1518
  }
1506
1519
  /**
1507
1520
  * Release the lock. Idempotent — silently succeeds if the file is
@@ -1945,10 +1958,12 @@ var SessionAnalyzer = class {
1945
1958
  }
1946
1959
  };
1947
1960
  var DefaultSessionRewinder = class {
1948
- constructor(sessionsDir) {
1961
+ constructor(sessionsDir, projectRoot) {
1949
1962
  this.sessionsDir = sessionsDir;
1963
+ this.projectRoot = projectRoot;
1950
1964
  }
1951
1965
  sessionsDir;
1966
+ projectRoot;
1952
1967
  async listCheckpoints(sessionId) {
1953
1968
  const file = path2.join(this.sessionsDir, `${sessionId}.jsonl`);
1954
1969
  const raw = await fsp.readFile(file, "utf8");
@@ -2005,7 +2020,7 @@ var DefaultSessionRewinder = class {
2005
2020
  }
2006
2021
  }
2007
2022
  }
2008
- const result = await revertSnapshots(snapshotsToRevert);
2023
+ const result = await revertSnapshots(snapshotsToRevert, this.projectRoot);
2009
2024
  const removedEvents = events.length - targetIdx - 1;
2010
2025
  return { ...result, toPromptIndex: checkpointIndex, removedEvents };
2011
2026
  }
@@ -2035,7 +2050,7 @@ var DefaultSessionRewinder = class {
2035
2050
  snapshotsToRevert.push({ promptIndex: event.promptIndex, files: event.files });
2036
2051
  }
2037
2052
  }
2038
- const result = await revertSnapshots(snapshotsToRevert.reverse());
2053
+ const result = await revertSnapshots(snapshotsToRevert.reverse(), this.projectRoot);
2039
2054
  return { ...result, toPromptIndex: targetIndex, removedEvents: snapshotsToRevert.length };
2040
2055
  }
2041
2056
  async rewindToStart(sessionId) {
@@ -2051,7 +2066,7 @@ var DefaultSessionRewinder = class {
2051
2066
  if (allSnapshots.length === 0) {
2052
2067
  return { revertedFiles: [], errors: [], toPromptIndex: 0, removedEvents: 0 };
2053
2068
  }
2054
- const result = await revertSnapshots(allSnapshots.reverse());
2069
+ const result = await revertSnapshots(allSnapshots.reverse(), this.projectRoot);
2055
2070
  return { ...result, toPromptIndex: 0, removedEvents: allSnapshots.length };
2056
2071
  }
2057
2072
  };
@@ -2069,12 +2084,19 @@ function parseEvents(raw) {
2069
2084
  }
2070
2085
  return events;
2071
2086
  }
2072
- async function revertSnapshots(snapshots) {
2087
+ async function revertSnapshots(snapshots, projectRoot) {
2073
2088
  const revertedFiles = [];
2074
2089
  const errors = [];
2075
2090
  for (const snapshot of snapshots) {
2076
2091
  for (const file of snapshot.files) {
2077
2092
  try {
2093
+ const absPath = path2.resolve(file.path);
2094
+ const root = path2.resolve(projectRoot);
2095
+ const rel = path2.relative(root, absPath);
2096
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
2097
+ errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
2098
+ continue;
2099
+ }
2078
2100
  if (file.action === "deleted") {
2079
2101
  if (file.before !== null) {
2080
2102
  await atomicWrite(file.path, file.before, { mode: 420 });