@atolis-hq/wake 0.2.93 → 0.2.95

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.
@@ -268,6 +268,10 @@ async function applyEvent(current, event, ctx, config) {
268
268
  config !== undefined &&
269
269
  isCustomCommandAction(payload.action, config);
270
270
  const shouldClearSession = isForwardProgression || isFailed;
271
+ const hasPendingChangesRequested = (typeof current.context.changesRequestedCount === 'number' &&
272
+ current.context.changesRequestedCount > 0) ||
273
+ current.context.changesRequestedFeedback !== undefined;
274
+ const resolvesChangesRequested = sentinel === doneRunnerSentinel && (!hasPendingChangesRequested || !approvalGated);
271
275
  const currentFailureCount = typeof current.context.failureCount === 'number' &&
272
276
  Number.isInteger(current.context.failureCount)
273
277
  ? current.context.failureCount
@@ -327,9 +331,11 @@ async function applyEvent(current, event, ctx, config) {
327
331
  approvalGated,
328
332
  }),
329
333
  }),
330
- // A fresh DONE cycle (gated or not) resolves whatever changes were
331
- // previously requested reset the loop counter and stored feedback.
332
- ...(sentinel === doneRunnerSentinel
334
+ // An approval-gated DONE after changes-requested only resubmits work for
335
+ // review; the next reviewer verdict decides whether the request was
336
+ // actually resolved. Keep the retry counter through that gate so
337
+ // repeated rejections can hit the configured escalation cap.
338
+ ...(resolvesChangesRequested
333
339
  ? { changesRequestedCount: 0, changesRequestedFeedback: undefined }
334
340
  : {}),
335
341
  };
@@ -194,7 +194,7 @@ export function createTickRunner(deps) {
194
194
  isRunningRecordActive,
195
195
  deliverOutboundEvent,
196
196
  });
197
- const { cleanupClosedIssueWorkspaces } = createWorkspaceCleanup({
197
+ const { cleanupClosedIssueWorkspaces, sweepExpiredTranscriptDirs } = createWorkspaceCleanup({
198
198
  clock: deps.clock,
199
199
  config: deps.config,
200
200
  stateStore: deps.stateStore,
@@ -920,6 +920,7 @@ export function createTickRunner(deps) {
920
920
  }
921
921
  const projections = await deps.stateStore.listIssueStates();
922
922
  await cleanupClosedIssueWorkspaces(projections);
923
+ await sweepExpiredTranscriptDirs();
923
924
  // Runs every tick, not only when this tick happened to poll a fresh
924
925
  // inbound event — a stale label on a parked item (nothing else ever
925
926
  // re-checks it once there's no next action) otherwise never self-heals.
@@ -1,8 +1,9 @@
1
- import { rm } from 'node:fs/promises';
1
+ import { readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
2
  import { isAbsolute, join, relative } from 'node:path';
3
3
  import { WORKSPACE_CLEANED_EVENT, WORKSPACE_CLEANUP_FAILED_EVENT } from '../domain/event-types.js';
4
4
  import { createEventEnvelope } from '../lib/event-log.js';
5
- // Cleans up per-issue workspaces (and, unless retained, transcripts) once the
5
+ const TRANSCRIPT_CLEANED_AT_MARKER = '.cleaned-at';
6
+ // Cleans up per-issue workspaces and applies transcript retention once the
6
7
  // originating issue is closed. A cleanup failure is recorded as an event and
7
8
  // skipped rather than aborting the sweep.
8
9
  export function createWorkspaceCleanup(deps) {
@@ -14,6 +15,49 @@ export function createWorkspaceCleanup(deps) {
14
15
  const rel = relative(workspacesRoot, workspacePath);
15
16
  return !rel.startsWith('..') && !isAbsolute(rel) && rel.length > 0;
16
17
  }
18
+ async function markTranscriptDirectoryCleaned(workItemKey, cleanedAt) {
19
+ const transcriptDir = deps.stateStore.paths.transcriptWorkDir(workItemKey);
20
+ const transcriptDirStat = await stat(transcriptDir).catch(() => undefined);
21
+ if (transcriptDirStat === undefined || !transcriptDirStat.isDirectory()) {
22
+ return;
23
+ }
24
+ await writeFile(join(transcriptDir, TRANSCRIPT_CLEANED_AT_MARKER), `${cleanedAt}\n`, 'utf8');
25
+ }
26
+ async function applyTranscriptCleanupRetention(workItemKey, cleanedAt) {
27
+ if (deps.config.transcripts.retentionMs === 0) {
28
+ await rm(deps.stateStore.paths.transcriptWorkDir(workItemKey), {
29
+ recursive: true,
30
+ force: true,
31
+ });
32
+ return;
33
+ }
34
+ await markTranscriptDirectoryCleaned(workItemKey, cleanedAt);
35
+ }
36
+ async function sweepExpiredTranscriptDirs() {
37
+ const retentionMs = deps.config.transcripts.retentionMs;
38
+ const transcriptDirs = await readdir(deps.stateStore.paths.transcriptsRoot, {
39
+ withFileTypes: true,
40
+ }).catch(() => []);
41
+ const nowMs = deps.clock.now().getTime();
42
+ for (const entry of transcriptDirs) {
43
+ if (!entry.isDirectory()) {
44
+ continue;
45
+ }
46
+ const transcriptDir = join(deps.stateStore.paths.transcriptsRoot, entry.name);
47
+ const markerPath = join(transcriptDir, TRANSCRIPT_CLEANED_AT_MARKER);
48
+ const marker = await readFile(markerPath, 'utf8').catch(() => undefined);
49
+ if (marker === undefined) {
50
+ continue;
51
+ }
52
+ const cleanedAtMs = Date.parse(marker.trim());
53
+ if (!Number.isFinite(cleanedAtMs)) {
54
+ continue;
55
+ }
56
+ if (nowMs - cleanedAtMs >= retentionMs) {
57
+ await rm(transcriptDir, { recursive: true, force: true });
58
+ }
59
+ }
60
+ }
17
61
  async function cleanupClosedIssueWorkspaces(projections) {
18
62
  for (const projection of projections) {
19
63
  const { workspacePath } = projection.wake;
@@ -22,12 +66,7 @@ export function createWorkspaceCleanup(deps) {
22
66
  isPerIssueWorkspacePath(workspacePath)) {
23
67
  try {
24
68
  await deps.workspaceManager.cleanupWorkspace({ workspacePath });
25
- if (!deps.config.transcripts.retainAfterWorkspaceCleanup) {
26
- await rm(deps.stateStore.paths.transcriptWorkDir(projection.workItemKey), {
27
- recursive: true,
28
- force: true,
29
- });
30
- }
69
+ await applyTranscriptCleanupRetention(projection.workItemKey, eventStampNow());
31
70
  }
32
71
  catch (error) {
33
72
  const failedAt = eventStampNow();
@@ -74,5 +113,5 @@ export function createWorkspaceCleanup(deps) {
74
113
  }
75
114
  }
76
115
  }
77
- return { cleanupClosedIssueWorkspaces };
116
+ return { cleanupClosedIssueWorkspaces, sweepExpiredTranscriptDirs };
78
117
  }
@@ -691,15 +691,19 @@ const wakeConfigBaseSchema = z.object({
691
691
  transcripts: z
692
692
  .object({
693
693
  enabled: z.boolean().default(false),
694
- retainAfterWorkspaceCleanup: z.boolean().default(false),
694
+ retentionMs: z
695
+ .number()
696
+ .int()
697
+ .nonnegative()
698
+ .default(3 * 24 * 60 * 60 * 1000),
695
699
  })
696
- .default({ enabled: false, retainAfterWorkspaceCleanup: false }),
700
+ .default({ enabled: false, retentionMs: 3 * 24 * 60 * 60 * 1000 }),
697
701
  retry: z
698
702
  .object({
699
703
  maxFailureRetries: z.number().int().positive().default(5),
700
- maxChangesRequestedRetries: z.number().int().positive().default(5),
704
+ maxChangesRequestedRetries: z.number().int().positive().default(3),
701
705
  })
702
- .default({ maxFailureRetries: 5, maxChangesRequestedRetries: 5 }),
706
+ .default({ maxFailureRetries: 5, maxChangesRequestedRetries: 3 }),
703
707
  runners: z.record(z.string(), runnerEntrySchema).default({
704
708
  fake: { kind: 'fake', cli: 'Fake' },
705
709
  'claude-haiku': {
@@ -1090,8 +1094,16 @@ function attachDerivedPromptContext(config) {
1090
1094
  return config;
1091
1095
  }
1092
1096
  export function parseWakeConfig(input) {
1097
+ if (isRecord(input) &&
1098
+ isRecord(input.transcripts) &&
1099
+ 'retainAfterWorkspaceCleanup' in input.transcripts) {
1100
+ throw new Error('transcripts.retainAfterWorkspaceCleanup is no longer supported; use transcripts.retentionMs instead. Set transcripts.retentionMs to 0 to delete transcripts immediately on workspace cleanup.');
1101
+ }
1093
1102
  return structuredClone(attachDerivedPromptContext(wakeConfigSchema.parse(input)));
1094
1103
  }
1104
+ function isRecord(value) {
1105
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1106
+ }
1095
1107
  export function parseSourceStateRecord(input) {
1096
1108
  return sourceStateRecordSchema.parse(input);
1097
1109
  }
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g5c69d41";
127
+ export const wakeVersion = "g803f372";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.93",
3
+ "version": "0.2.95",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {