sneakoscope 4.6.3 → 4.6.4

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.
package/README.md CHANGED
@@ -49,7 +49,7 @@ sks seo-geo-optimizer apply latest --mode seo --apply --json
49
49
  sks seo-geo-optimizer audit --mode geo --target package --offline --json
50
50
  ```
51
51
 
52
- > 📋 **Current release: `v4.6.3`** — full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
52
+ > 📋 **Current release: `v4.6.4`** — full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
53
53
 
54
54
  ## 🍥 Parallelism, UX, And Integrations
55
55
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "4.6.3"
79
+ version = "4.6.4"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "4.6.3"
3
+ version = "4.6.4"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 4.6.3"),
7
+ Some("--version") => println!("sks-rs 4.6.4"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '4.6.3';
2
+ const FAST_PACKAGE_VERSION = '4.6.4';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -92,6 +92,11 @@ export async function postinstall({ bootstrap, args = [] }) {
92
92
  console.log('SKS update migration: global Doctor ran; project receipt will be finalized on first normal command.');
93
93
  else
94
94
  console.log(`SKS update migration: global Doctor did not complete; first normal command will retry. ${(postinstallDoctor.blockers || []).join(', ')}`.trim());
95
+ const postinstallRetention = await runPostinstallProjectRetentionCleanup(installRoot);
96
+ if (postinstallRetention.status === 'completed' && postinstallRetention.action_count > 0)
97
+ console.log(`SKS mission cleanup: removed ${postinstallRetention.action_count} disposable runtime artifact(s) from closed missions.`);
98
+ else if (postinstallRetention.status === 'failed')
99
+ console.log(`SKS mission cleanup: skipped (${postinstallRetention.error || 'cleanup failed'}).`);
95
100
  // Terminating a third-party app's processes during `npm i` is unsafe by default; opt-in only.
96
101
  const appProcessRepair = process.env.SKS_POSTINSTALL_RECONCILE_APP_PROCESSES === '1'
97
102
  ? await reconcileCodexAppUpgradeProcesses()
@@ -150,6 +155,36 @@ export async function postinstall({ bootstrap, args = [] }) {
150
155
  await reportPostinstallCodexLbAuth().catch(() => { });
151
156
  }
152
157
  }
158
+ async function runPostinstallProjectRetentionCleanup(root) {
159
+ const projectRoot = path.resolve(root || process.cwd());
160
+ if (process.env.SKS_POSTINSTALL_RETENTION_CLEANUP === '0') {
161
+ return { status: 'skipped', reason: 'disabled_by_env', action_count: 0 };
162
+ }
163
+ if (!(await exists(path.join(projectRoot, '.sneakoscope', 'missions')))) {
164
+ return { status: 'skipped', reason: 'missions_missing', action_count: 0 };
165
+ }
166
+ try {
167
+ const { enforceRetention } = await import('../core/retention.js');
168
+ const result = await enforceRetention(projectRoot, {
169
+ mode: 'postinstall_update',
170
+ pruneReportLogs: true,
171
+ policy: { max_tmp_age_hours: 0 }
172
+ });
173
+ return {
174
+ status: 'completed',
175
+ root: projectRoot,
176
+ action_count: Array.isArray(result.actions) ? result.actions.length : 0
177
+ };
178
+ }
179
+ catch (err) {
180
+ return {
181
+ status: 'failed',
182
+ root: projectRoot,
183
+ action_count: 0,
184
+ error: err?.message || String(err)
185
+ };
186
+ }
187
+ }
153
188
  async function reportPostinstallCodexLbAuth() {
154
189
  const codexLbAuth = await ensureCodexLbAuthDuringInstall();
155
190
  if (codexLbAuth.legacy_auth_migrated)
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '4.6.3';
8
+ export const PACKAGE_VERSION = '4.6.4';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -14,6 +14,7 @@ export const DEFAULT_RETENTION_POLICY = Object.freeze({
14
14
  run_gc_after_each_cycle: true,
15
15
  compact_oversize_missions: false,
16
16
  compact_closed_mission_workdirs: true,
17
+ compact_terminal_session_runtime_homes: true,
17
18
  prune_disposable_report_logs: false,
18
19
  max_wiki_artifacts: 40,
19
20
  max_wiki_artifact_age_days: 30,
@@ -46,7 +47,11 @@ const DISPOSABLE_MISSION_DIRS = Object.freeze([
46
47
  'tmp',
47
48
  'cycles',
48
49
  'arenas',
50
+ 'sessions',
51
+ 'codex-sdk-workers',
49
52
  'agents/lanes',
53
+ 'agents/sessions',
54
+ 'agents/codex-sdk-workers',
50
55
  'agents/tmp',
51
56
  'agents/worktrees',
52
57
  'research/cycles',
@@ -56,6 +61,10 @@ const DISPOSABLE_MISSION_FILES = Object.freeze([
56
61
  'agents/agent-intelligent-work-graph.json',
57
62
  'agents/agent-intelligent-work-graph-v2.json'
58
63
  ]);
64
+ const DISPOSABLE_RUNTIME_HOME_DIR_NAMES = Object.freeze([
65
+ 'codex-sdk-home',
66
+ 'codex-sdk-workers'
67
+ ]);
59
68
  const MISSION_CLOSE_GATES = Object.freeze([
60
69
  'team-gate.json',
61
70
  'reflection-gate.json',
@@ -175,6 +184,15 @@ function proofClosed(proof) {
175
184
  return false;
176
185
  return ['verified', 'verified_partial', 'pass', 'passed'].includes(status);
177
186
  }
187
+ function sessionsTerminal(cleanup) {
188
+ if (!cleanup || typeof cleanup !== 'object')
189
+ return false;
190
+ if (cleanup.all_sessions_terminal === true || cleanup.all_sessions_closed === true)
191
+ return true;
192
+ const terminal = Number(cleanup.terminal_session_count);
193
+ const total = Number(cleanup.total_sessions);
194
+ return Number.isFinite(terminal) && Number.isFinite(total) && total > 0 && terminal >= total;
195
+ }
178
196
  async function missionClosed(mission, opts = {}) {
179
197
  const proof = await readJson(path.join(mission.path, 'completion-proof.json'), null).catch(() => null);
180
198
  if (opts.completedMissionId && mission.id === opts.completedMissionId)
@@ -195,6 +213,19 @@ async function missionClosed(mission, opts = {}) {
195
213
  }
196
214
  return false;
197
215
  }
216
+ async function missionSessionsTerminal(mission) {
217
+ const cleanupFiles = [
218
+ 'agents/agent-session-cleanup.json',
219
+ 'team-session-cleanup.json',
220
+ 'agent-session-cleanup.json'
221
+ ];
222
+ for (const rel of cleanupFiles) {
223
+ const cleanup = await readJson(path.join(mission.path, rel), null).catch(() => null);
224
+ if (sessionsTerminal(cleanup))
225
+ return true;
226
+ }
227
+ return false;
228
+ }
198
229
  async function removePath(action, target, dryRun, actions, extra = {}) {
199
230
  const st = await fs.stat(target).catch(() => null);
200
231
  if (!st)
@@ -208,20 +239,58 @@ async function removePath(action, target, dryRun, actions, extra = {}) {
208
239
  function missionRelative(mission, file) {
209
240
  return path.relative(mission.path, file).split(path.sep).join('/');
210
241
  }
211
- function isPreservedSessionPath(rel) {
212
- return rel.startsWith('sessions/') || rel.startsWith('agents/sessions/');
213
- }
214
242
  async function pruneMissionDisposableLogs(mission, dryRun, actions) {
215
243
  const files = await listFilesRecursive(mission.path, { ignore: [], maxFiles: 10000, maxDepth: 8 }).catch(() => []);
216
244
  for (const file of files) {
217
245
  const rel = missionRelative(mission, file);
218
- if (isPreservedSessionPath(rel))
219
- continue;
220
246
  if (!DISPOSABLE_LOG_RE.test(rel))
221
247
  continue;
222
248
  await removePath('remove_closed_mission_raw_log', file, dryRun, actions, { mission: mission.id, reason: 'closed_mission_disposable_log' });
223
249
  }
224
250
  }
251
+ async function collectRuntimeHomeDirs(dir, depth = 10) {
252
+ if (depth < 0 || !(await exists(dir)))
253
+ return [];
254
+ const out = [];
255
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
256
+ for (const entry of entries) {
257
+ if (!entry.isDirectory())
258
+ continue;
259
+ const child = path.join(dir, entry.name);
260
+ if (DISPOSABLE_RUNTIME_HOME_DIR_NAMES.includes(entry.name)) {
261
+ out.push(child);
262
+ continue;
263
+ }
264
+ out.push(...await collectRuntimeHomeDirs(child, depth - 1));
265
+ }
266
+ return out;
267
+ }
268
+ async function pruneMissionRuntimeHomeDirs(mission, action, dryRun, actions, reason) {
269
+ const dirs = await collectRuntimeHomeDirs(mission.path);
270
+ for (const dir of dirs) {
271
+ await removePath(action, dir, dryRun, actions, { mission: mission.id, rel: missionRelative(mission, dir), reason });
272
+ }
273
+ }
274
+ async function pruneTerminalSessionRuntimeHomes(root, policy, dryRun, actions, opts = {}) {
275
+ if (policy.compact_terminal_session_runtime_homes === false && opts.compactTerminalSessionRuntimeHomes !== true)
276
+ return;
277
+ const activeId = await activeMissionId(root);
278
+ const targetOnly = Boolean(opts.afterRoute && opts.completedMissionId && opts.sweepClosedMissions !== true);
279
+ const missions = targetOnly
280
+ ? [await missionDirById(root, opts.completedMissionId)]
281
+ : await listMissionDirs(root);
282
+ for (const mission of missions.filter(Boolean)) {
283
+ if (await missionClosed(mission, opts))
284
+ continue;
285
+ const active = activeId && mission.id === activeId;
286
+ const activeRouteTarget = Boolean(opts.afterRoute && opts.completedMissionId === mission.id);
287
+ if (active && !activeRouteTarget && opts.allowActiveMissionCleanup !== true)
288
+ continue;
289
+ if (!(await missionSessionsTerminal(mission)))
290
+ continue;
291
+ await pruneMissionRuntimeHomeDirs(mission, 'remove_terminal_session_runtime_home', dryRun, actions, 'terminal_agent_session_runtime_home');
292
+ }
293
+ }
225
294
  async function compactClosedMissionWorkdirs(root, policy, dryRun, actions, opts = {}) {
226
295
  if (policy.compact_closed_mission_workdirs === false || opts.compactClosedMissionWorkdirs === false)
227
296
  return;
@@ -539,6 +608,8 @@ export async function enforceRetention(root, opts = {}) {
539
608
  }
540
609
  if (shouldCompactClosedMissions)
541
610
  await compactClosedMissionWorkdirs(root, policy, dryRun, actions, opts);
611
+ if (fullMissionSweep || opts.compactTerminalSessionRuntimeHomes === true || Boolean(opts.afterRoute && opts.completedMissionId))
612
+ await pruneTerminalSessionRuntimeHomes(root, policy, dryRun, actions, opts);
542
613
  if (fullMissionSweep || opts.rotateLargeJsonl === true)
543
614
  await rotateLargeJsonl(root, policy, dryRun, actions);
544
615
  await pruneDisposableReportLogs(root, policy, dryRun, actions, opts);
@@ -556,6 +627,7 @@ export async function enforceRetention(root, opts = {}) {
556
627
  protected_durable_context: DURABLE_RETENTION_CLASSES,
557
628
  disposable_mission_dirs: DISPOSABLE_MISSION_DIRS,
558
629
  disposable_mission_files: DISPOSABLE_MISSION_FILES,
630
+ disposable_runtime_home_dir_names: DISPOSABLE_RUNTIME_HOME_DIR_NAMES,
559
631
  prune_report_logs: Boolean(opts.pruneReportLogs || policy.prune_disposable_report_logs),
560
632
  completed_mission_id: opts.completedMissionId || null,
561
633
  actions
@@ -2,6 +2,7 @@ import fsp from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { ensureDir, exists, globalSksRoot, nowIso, packageRoot, PACKAGE_VERSION, projectRoot, readJson, runProcess, sha256, which, writeJsonAtomic } from '../fsx.js';
4
4
  import { MANAGED_ASSET_VERSION } from '../managed-assets/managed-assets-manifest.js';
5
+ import { enforceRetention } from '../retention.js';
5
6
  export const UPDATE_MIGRATION_SCHEMA = 'sks.project-migration-receipt.v2';
6
7
  export const INSTALLATION_EPOCH_SCHEMA = 'sks.installation-epoch.v1';
7
8
  const ALLOWLIST_COMMANDS = new Set([
@@ -98,6 +99,7 @@ export async function clearPendingUpdateMigration() {
98
99
  export async function writeProjectUpdateMigrationReceipt(input) {
99
100
  const receiptPath = projectUpdateMigrationReceiptPath(input.root);
100
101
  const epoch = await ensureInstallationEpoch(input.source);
102
+ const retentionCleanup = await runUpdateRetentionCleanup(input.root, input.source);
101
103
  const requiredBlockers = input.blockers || [];
102
104
  const optionalWarnings = input.warnings || [];
103
105
  const receipt = {
@@ -113,6 +115,7 @@ export async function writeProjectUpdateMigrationReceipt(input) {
113
115
  pending_marker_path: installationEpochPath(),
114
116
  installation_epoch_path: installationEpochPath(),
115
117
  doctor: input.doctor || null,
118
+ retention_cleanup: retentionCleanup,
116
119
  update_stages: input.updateStages || [],
117
120
  required_blockers: requiredBlockers,
118
121
  optional_warnings: optionalWarnings,
@@ -122,6 +125,71 @@ export async function writeProjectUpdateMigrationReceipt(input) {
122
125
  await writeJsonAtomic(receiptPath, receipt);
123
126
  return receipt;
124
127
  }
128
+ export async function runUpdateRetentionCleanup(root, source = 'update-migration') {
129
+ const missionsPath = path.join(root, '.sneakoscope', 'missions');
130
+ const cleanupPath = path.join(root, '.sneakoscope', 'reports', 'retention-cleanup.json');
131
+ const storagePath = path.join(root, '.sneakoscope', 'reports', 'storage.json');
132
+ if (process.env.SKS_UPDATE_RETENTION_CLEANUP === '0') {
133
+ return {
134
+ schema: 'sks.update-retention-cleanup.v1',
135
+ ok: true,
136
+ status: 'skipped',
137
+ root,
138
+ source,
139
+ generated_at: nowIso(),
140
+ action_count: 0,
141
+ cleanup_report_path: null,
142
+ storage_report_path: null,
143
+ reason: 'disabled_by_env'
144
+ };
145
+ }
146
+ if (!(await exists(missionsPath))) {
147
+ return {
148
+ schema: 'sks.update-retention-cleanup.v1',
149
+ ok: true,
150
+ status: 'skipped',
151
+ root,
152
+ source,
153
+ generated_at: nowIso(),
154
+ action_count: 0,
155
+ cleanup_report_path: null,
156
+ storage_report_path: null,
157
+ reason: 'missions_missing'
158
+ };
159
+ }
160
+ try {
161
+ const result = await enforceRetention(root, {
162
+ mode: 'update_migration',
163
+ pruneReportLogs: true,
164
+ policy: { max_tmp_age_hours: 0 }
165
+ });
166
+ return {
167
+ schema: 'sks.update-retention-cleanup.v1',
168
+ ok: true,
169
+ status: 'completed',
170
+ root,
171
+ source,
172
+ generated_at: nowIso(),
173
+ action_count: Array.isArray(result.actions) ? result.actions.length : 0,
174
+ cleanup_report_path: cleanupPath,
175
+ storage_report_path: storagePath
176
+ };
177
+ }
178
+ catch (err) {
179
+ return {
180
+ schema: 'sks.update-retention-cleanup.v1',
181
+ ok: false,
182
+ status: 'failed',
183
+ root,
184
+ source,
185
+ generated_at: nowIso(),
186
+ action_count: 0,
187
+ cleanup_report_path: null,
188
+ storage_report_path: null,
189
+ error: err?.message || String(err)
190
+ };
191
+ }
192
+ }
125
193
  export async function ensureCurrentMigrationBeforeCommand(input) {
126
194
  const env = input.env || process.env;
127
195
  const command = input.command;
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '4.6.3';
1
+ export const PACKAGE_VERSION = '4.6.4';
2
2
  //# sourceMappingURL=version.js.map
@@ -11,6 +11,7 @@ const helpers = readText('src/cli/install-helpers.ts');
11
11
  assertGate(helpers.includes('SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS'), 'postinstall cli tool install must be gated behind SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS');
12
12
  // Killing a third-party app's processes is opt-in only.
13
13
  assertGate(helpers.includes("SKS_POSTINSTALL_RECONCILE_APP_PROCESSES === '1'"), 'postinstall process reconciliation must be gated behind SKS_POSTINSTALL_RECONCILE_APP_PROCESSES');
14
+ assertGate(helpers.includes('runPostinstallProjectRetentionCleanup') && helpers.includes('SKS_POSTINSTALL_RETENTION_CLEANUP'), 'postinstall mission retention cleanup must be project-scoped and disableable');
14
15
  // Config writes are gated (unparseable preserved / unsafe rewrite skipped) and backed up.
15
16
  assertGate(helpers.includes('unparseable_config_preserved'), 'postinstall must preserve unparseable config');
16
17
  assertGate(helpers.includes('skipped_unsafe_rewrite'), 'postinstall must skip unsafe config rewrites');
@@ -38,11 +38,12 @@ try {
38
38
  '.sneakoscope/missions/M-old/completion-proof.json',
39
39
  '.sneakoscope/missions/M-old/trust-report.json',
40
40
  '.sneakoscope/missions/M-old/reflection.md',
41
- '.sneakoscope/missions/M-done/sessions/terminal-transcript.log',
42
- '.sneakoscope/missions/M-done/agents/sessions/session-1/terminal-transcript.log',
43
41
  '.sneakoscope/missions/M-active/team-inbox/active.md',
42
+ '.sneakoscope/missions/M-active/agents/sessions/session-1/gen-1/worker/codex-sdk-home/codex/cache.bin',
44
43
  '.sneakoscope/missions/M-blocked/team-inbox/blocked.md',
45
- '.sneakoscope/missions/M-blocked/scout.stderr.log'
44
+ '.sneakoscope/missions/M-blocked/scout.stderr.log',
45
+ '.sneakoscope/missions/M-blocked-terminal/team-inbox/blocked.md',
46
+ '.sneakoscope/missions/M-blocked-terminal/agents/sessions/session-1/gen-1/worker/worker-result.json'
46
47
  ]) {
47
48
  assertGate(exists(path.join(applyRoot, rel)), `durable or active artifact was removed: ${rel}`, { rel, actions: applied.actions });
48
49
  }
@@ -51,10 +52,15 @@ try {
51
52
  '.sneakoscope/missions/M-done/team-inbox/worker.md',
52
53
  '.sneakoscope/missions/M-done/bus/event.jsonl',
53
54
  '.sneakoscope/missions/M-done/agents/lanes/lane-1.json',
55
+ '.sneakoscope/missions/M-done/sessions/terminal-transcript.log',
56
+ '.sneakoscope/missions/M-done/agents/sessions/session-1/terminal-transcript.log',
57
+ '.sneakoscope/missions/M-done/agents/sessions/session-1/gen-1/worker/codex-sdk-home/codex/cache.bin',
54
58
  '.sneakoscope/missions/M-done/scout.stdout.log',
55
59
  '.sneakoscope/missions/M-done/scout.stderr.log',
56
60
  '.sneakoscope/missions/M-old/team-inbox/worker.md',
61
+ '.sneakoscope/missions/M-old/agents/sessions/session-1/gen-1/worker/codex-sdk-home/codex/cache.bin',
57
62
  '.sneakoscope/missions/M-old/scout.stdout.log',
63
+ '.sneakoscope/missions/M-blocked-terminal/agents/sessions/session-1/gen-1/worker/codex-sdk-home/codex/cache.bin',
58
64
  '.sneakoscope/reports/release-parallel-logs/build.stdout.log'
59
65
  ]) {
60
66
  assertGate(!exists(path.join(applyRoot, rel)), `disposable artifact survived cleanup: ${rel}`, { rel, actions: applied.actions });
@@ -64,6 +70,7 @@ try {
64
70
  for (const kind of ['remove_tmp', 'remove_closed_mission_raw_log', 'remove_disposable_report_log_dir']) {
65
71
  assertGate(actionKinds.has(kind), `retention cleanup did not report action kind: ${kind}`, { actions: applied.actions });
66
72
  }
73
+ assertGate(actionKinds.has('remove_terminal_session_runtime_home'), 'retention cleanup did not remove terminal session runtime homes', { actions: applied.actions });
67
74
  assertGate(actionKinds.has('remove_closed_mission_workdir') || actionKinds.has('remove_old_mission_workdir'), 'retention cleanup did not report mission workdir cleanup', { actions: applied.actions });
68
75
  assertGate(actionKinds.has('retain_mission_durable_context'), 'retention cleanup did not preserve old durable mission context', { actions: applied.actions });
69
76
  assertGate(dry.actions.length >= applied.actions.length, 'dry-run should plan cleanup actions without deleting files', { applied: applied.actions.length, dry: dry.actions.length });
@@ -95,6 +102,7 @@ async function writeFixture(projectRoot) {
95
102
  await writeMission(projectRoot, 'M-active', false);
96
103
  await writeOldDurableMission(projectRoot);
97
104
  await writeBlockedMission(projectRoot);
105
+ await writeTerminalBlockedMission(projectRoot);
98
106
  await writeText(path.join(projectRoot, '.sneakoscope', 'reports', 'release-parallel-logs', 'build.stdout.log'), 'summarized release log\n');
99
107
  }
100
108
  async function writeMission(projectRoot, missionId, closed) {
@@ -110,14 +118,16 @@ async function writeMission(projectRoot, missionId, closed) {
110
118
  await write(path.join(dir, 'team-gate.json'), { passed: true });
111
119
  await write(path.join(dir, 'team-session-cleanup.json'), { passed: true, all_sessions_closed: true });
112
120
  await write(path.join(dir, 'agents', 'agent-proof-evidence.json'), { ok: true, all_sessions_closed: true });
113
- await writeText(path.join(dir, 'sessions', 'terminal-transcript.log'), 'transcript stays\n');
114
- await writeText(path.join(dir, 'agents', 'sessions', 'session-1', 'terminal-transcript.log'), 'agent transcript stays\n');
121
+ await writeText(path.join(dir, 'sessions', 'terminal-transcript.log'), 'transcript is disposable after close\n');
122
+ await writeText(path.join(dir, 'agents', 'sessions', 'session-1', 'terminal-transcript.log'), 'agent transcript is disposable after close\n');
123
+ await writeText(path.join(dir, 'agents', 'sessions', 'session-1', 'gen-1', 'worker', 'codex-sdk-home', 'codex', 'cache.bin'), 'large sdk cache\n');
115
124
  await writeText(path.join(dir, 'scout.stdout.log'), 'raw stdout\n');
116
125
  await writeText(path.join(dir, 'scout.stderr.log'), 'raw stderr\n');
117
126
  await writeText(path.join(dir, 'team-inbox', 'worker.md'), 'temporary inbox\n');
118
127
  }
119
128
  else {
120
129
  await writeText(path.join(dir, 'team-inbox', 'active.md'), 'active mission scratch stays\n');
130
+ await writeText(path.join(dir, 'agents', 'sessions', 'session-1', 'gen-1', 'worker', 'codex-sdk-home', 'codex', 'cache.bin'), 'active sdk cache stays\n');
121
131
  }
122
132
  await writeText(path.join(dir, 'bus', 'event.jsonl'), '{"event":"temporary"}\n');
123
133
  await writeText(path.join(dir, 'agents', 'lanes', 'lane-1.json'), '{"lane":"temporary"}\n');
@@ -129,6 +139,7 @@ async function writeOldDurableMission(projectRoot) {
129
139
  await write(path.join(dir, 'evidence-index.json'), { evidence: [] });
130
140
  await writeText(path.join(dir, 'reflection.md'), '# old retained reflection\n');
131
141
  await writeText(path.join(dir, 'team-inbox', 'worker.md'), 'old scratch\n');
142
+ await writeText(path.join(dir, 'agents', 'sessions', 'session-1', 'gen-1', 'worker', 'codex-sdk-home', 'codex', 'cache.bin'), 'old sdk cache\n');
132
143
  await writeText(path.join(dir, 'scout.stdout.log'), 'old raw log\n');
133
144
  await old(dir);
134
145
  }
@@ -138,6 +149,14 @@ async function writeBlockedMission(projectRoot) {
138
149
  await writeText(path.join(dir, 'team-inbox', 'blocked.md'), 'diagnostic scratch\n');
139
150
  await writeText(path.join(dir, 'scout.stderr.log'), 'diagnostic raw log\n');
140
151
  }
152
+ async function writeTerminalBlockedMission(projectRoot) {
153
+ const dir = path.join(projectRoot, '.sneakoscope', 'missions', 'M-blocked-terminal');
154
+ await write(path.join(dir, 'completion-proof.json'), { schema: 'sks.completion-proof.v1', status: 'blocked', blockers: ['fixture_blocker'] });
155
+ await write(path.join(dir, 'agents', 'agent-session-cleanup.json'), { all_sessions_terminal: true, terminal_session_count: 1, total_sessions: 1 });
156
+ await writeText(path.join(dir, 'team-inbox', 'blocked.md'), 'diagnostic scratch\n');
157
+ await writeText(path.join(dir, 'agents', 'sessions', 'session-1', 'gen-1', 'worker', 'worker-result.json'), '{"status":"blocked"}\n');
158
+ await writeText(path.join(dir, 'agents', 'sessions', 'session-1', 'gen-1', 'worker', 'codex-sdk-home', 'codex', 'cache.bin'), 'terminal sdk cache\n');
159
+ }
141
160
  async function write(file, data) {
142
161
  await writeText(file, `${JSON.stringify(data, null, 2)}\n`);
143
162
  }
@@ -8,6 +8,7 @@ assertGate(helper.includes('INSTALLATION_EPOCH_SCHEMA') && helper.includes('inst
8
8
  assertGate(helper.includes('projectUpdateMigrationReceiptPath'), 'migration helper must keep a project receipt');
9
9
  assertGate(helper.includes("'--profile', 'migration'") && helper.includes("'--machine-only'") && helper.includes("'--report-file'"), 'first normal command must repair through package-local migration Doctor machine report before continuing');
10
10
  assertGate(helper.includes('isProjectReceiptCurrentForEpoch'), 'project receipts must be compared against the current installation epoch');
11
+ assertGate(helper.includes('runUpdateRetentionCleanup') && helper.includes('retention_cleanup'), 'project update migration receipt must run mission retention cleanup and record its receipt');
11
12
  assertGate(helper.includes('clearPendingUpdateMigration') && helper.includes('one project must not consume global migration state'), 'legacy clear helper must preserve the persistent epoch contract');
12
13
  assertGate(scriptContains('update:first-command-migration', 'update-first-command-migration-check.js'), 'package script must expose first command migration gate');
13
14
  emitGate('update:first-command-migration');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "4.6.3",
4
+ "version": "4.6.4",
5
5
  "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",