@wrongstack/core 0.309.0 → 0.310.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.
Files changed (129) hide show
  1. package/dist/chronicle/index.js +113 -41
  2. package/dist/chronicle/metrics-ingest.d.ts +7 -0
  3. package/dist/chronicle/metrics-schema.d.ts +4 -1
  4. package/dist/chronicle/project-server.js +94 -38
  5. package/dist/chronicle/query.d.ts +1 -0
  6. package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
  7. package/dist/chronicle/types.d.ts +2 -0
  8. package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
  9. package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
  10. package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
  11. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
  12. package/dist/coordination/director.d.ts +7 -0
  13. package/dist/coordination/explore-companion.d.ts +9 -6
  14. package/dist/coordination/fleet-status-tool.d.ts +1 -1
  15. package/dist/coordination/index.d.ts +5 -1
  16. package/dist/coordination/index.js +2392 -405
  17. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  18. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  19. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  20. package/dist/coordination/mailbox-project-server.js +14 -9
  21. package/dist/coordination/mutation-engine.d.ts +5 -3
  22. package/dist/core/context.d.ts +35 -44
  23. package/dist/core/conversation-state.d.ts +16 -52
  24. package/dist/core/index.js +92 -25
  25. package/dist/core/provider-runner.d.ts +2 -2
  26. package/dist/core/run-env.d.ts +7 -28
  27. package/dist/core/streaming-response-builder.d.ts +2 -2
  28. package/dist/execution/index.js +138 -162
  29. package/dist/goal/index.js +100 -22
  30. package/dist/hq/auth-store.d.ts +9 -0
  31. package/dist/hq/cost-bridge.d.ts +1 -1
  32. package/dist/hq/index.js +69 -7
  33. package/dist/index.d.ts +12 -5
  34. package/dist/index.js +21185 -26807
  35. package/dist/infrastructure/index.js +232 -135
  36. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  37. package/dist/infrastructure/token-counter.d.ts +5 -1
  38. package/dist/kernel/events/file-events.d.ts +6 -0
  39. package/dist/kernel/events/provider-events.d.ts +10 -7
  40. package/dist/kernel/events/session-events.d.ts +4 -4
  41. package/dist/kernel/events/tool-events.d.ts +12 -2
  42. package/dist/observability/index.js +1 -1
  43. package/dist/plugin/index.js +218 -47
  44. package/dist/prompts/index.js +360 -3
  45. package/dist/security/auto-approve-policy.d.ts +2 -2
  46. package/dist/security/index.d.ts +1 -0
  47. package/dist/security/index.js +224 -68
  48. package/dist/security/kanban-boundary.d.ts +1 -1
  49. package/dist/security/kanban-governance-port.d.ts +28 -0
  50. package/dist/security/permission-helpers.d.ts +11 -0
  51. package/dist/security/permission-policy.d.ts +10 -1
  52. package/dist/security/yolo-risk.d.ts +17 -0
  53. package/dist/session-catalog/index.js +34 -5
  54. package/dist/session-catalog/project-server.js +34 -5
  55. package/dist/session-catalog/protocol.d.ts +1 -1
  56. package/dist/session-catalog/registry.d.ts +1 -1
  57. package/dist/session-catalog/store-schema.d.ts +1 -1
  58. package/dist/session-catalog/store.d.ts +1 -1
  59. package/dist/skills/index.js +39 -6
  60. package/dist/storage/annotations-store.d.ts +1 -1
  61. package/dist/storage/board-store-port.d.ts +45 -0
  62. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  63. package/dist/storage/config-loader/types.d.ts +1 -1
  64. package/dist/storage/event-bus-port.d.ts +27 -0
  65. package/dist/storage/file-session-writer.d.ts +47 -5
  66. package/dist/storage/goal-coordination.d.ts +1 -1
  67. package/dist/storage/goal-store.d.ts +1 -1
  68. package/dist/storage/index.d.ts +3 -4
  69. package/dist/storage/index.js +1107 -1462
  70. package/dist/storage/plan-store.d.ts +1 -1
  71. package/dist/storage/queue-store.d.ts +1 -1
  72. package/dist/storage/replay-log-store.d.ts +1 -1
  73. package/dist/storage/session-recovery.d.ts +10 -0
  74. package/dist/storage/session-resume-validation.d.ts +13 -1
  75. package/dist/storage/session-store/events.d.ts +1 -1
  76. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  77. package/dist/storage/session-store/rename-session.d.ts +1 -1
  78. package/dist/storage/session-store/resume-session.d.ts +3 -1
  79. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  80. package/dist/storage/session-store/types.d.ts +1 -1
  81. package/dist/storage/session-store.d.ts +70 -0
  82. package/dist/storage/session-summary-tracker.d.ts +8 -0
  83. package/dist/storage/session-write-buffer.d.ts +31 -2
  84. package/dist/storage/task-store.d.ts +1 -1
  85. package/dist/storage/todos-checkpoint.d.ts +1 -1
  86. package/dist/storage/tool-audit-log.d.ts +1 -1
  87. package/dist/tasking/index.js +100 -22
  88. package/dist/tasking/task-tracker.d.ts +24 -1
  89. package/dist/types/compactor.d.ts +2 -2
  90. package/dist/types/context.d.ts +212 -0
  91. package/dist/types/conversation-state.d.ts +109 -0
  92. package/dist/types/error-handler.d.ts +2 -2
  93. package/dist/types/file-event-record.d.ts +6 -0
  94. package/dist/types/index.d.ts +3 -3
  95. package/dist/types/index.js +17 -1
  96. package/dist/types/permission.d.ts +3 -3
  97. package/dist/types/plugin.d.ts +3 -3
  98. package/dist/types/provider-runner.d.ts +2 -2
  99. package/dist/types/provider.d.ts +10 -0
  100. package/dist/types/run-env.d.ts +32 -0
  101. package/dist/types/session.d.ts +3 -0
  102. package/dist/types/slash-command.d.ts +2 -2
  103. package/dist/types/token-counter.d.ts +30 -1
  104. package/dist/types/tool-executor.d.ts +2 -2
  105. package/dist/types/tool.d.ts +20 -5
  106. package/dist/utils/context-breakdown.d.ts +2 -2
  107. package/dist/utils/context-evidence.d.ts +12 -12
  108. package/dist/utils/crash-shield.d.ts +9 -0
  109. package/dist/utils/heap-watchdog.js +11 -3
  110. package/dist/utils/index.d.ts +2 -1
  111. package/dist/utils/index.js +126 -160
  112. package/dist/utils/regex-guard.d.ts +7 -30
  113. package/dist/utils/terminal-sanitize.d.ts +41 -0
  114. package/dist/utils/todos-format.d.ts +1 -1
  115. package/dist/utils/tool-subject.d.ts +1 -1
  116. package/dist/utils/tree-kill.d.ts +2 -0
  117. package/dist/utils/tree-kill.js +1 -0
  118. package/instructions/agents/chaos-monkey.md +5 -1
  119. package/instructions/coordination/subagent-baseline.md +10 -0
  120. package/instructions/system-lite.md +2 -0
  121. package/instructions/system-pro.md +40 -0
  122. package/instructions/system.md +15 -0
  123. package/package.json +7 -10
  124. package/dist/defaults/index.d.ts +0 -69
  125. package/dist/defaults/index.js +0 -37755
  126. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  127. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  128. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  129. /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { ConversationState } from '../core/conversation-state.js';
2
- import type { EventBus } from '../kernel/events.js';
2
+ import type { EventBus } from './event-bus-port.js';
3
3
  /**
4
4
  * Plan items are the strategic counterpart to todos. Where `ctx.todos`
5
5
  * is the moment-to-moment task board the LLM mutates per-turn, a plan
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../kernel/events.js';
1
+ import type { EventBus } from './event-bus-port.js';
2
2
  import type { ContentBlock } from '../types/blocks.js';
3
3
  import type { Logger } from '../types/logger.js';
4
4
  /**
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../kernel/events.js';
1
+ import type { EventBus } from './event-bus-port.js';
2
2
  import type { Request, Response } from '../types/provider.js';
3
3
  /**
4
4
  * ReplayLogStore — sidecar store for deterministic-replay support
@@ -43,6 +43,8 @@ export interface RecoveryPlan {
43
43
  context: string | null;
44
44
  }
45
45
  export interface InterruptedToolDetail {
46
+ /** Tool_use id — lets callers synthesize a matching error tool_result. */
47
+ id?: string | undefined;
46
48
  name: string;
47
49
  argsSummary?: string | undefined;
48
50
  ts?: string | undefined;
@@ -64,6 +66,14 @@ export declare class SessionRecovery {
64
66
  private readonly dir;
65
67
  private static readonly MAX_PENDING_EVENTS;
66
68
  private static readonly MAX_PENDING_BYTES;
69
+ /**
70
+ * Build a recovery plan from ALREADY-LOADED events without touching disk.
71
+ * executeResumeSession uses this because load() has already paid for the
72
+ * transcript read; recover()'s file scan would duplicate it.
73
+ */
74
+ static buildRecoveryPlan(events: readonly SessionEvent[],
75
+ /** Canonical session id — threaded through so RecoveryPlan stays complete. */
76
+ sessionId: string): RecoveryPlan;
67
77
  /**
68
78
  * Scan a session log and return a `StaleSession` if and only if the newest
69
79
  * lifecycle boundary is an `in_flight_start` without a later
@@ -13,7 +13,19 @@ export declare function validateResumeFileObservations(events: readonly SessionE
13
13
  * strips previous notices from the replayed conversation before appending the
14
14
  * current ones; these prefixes are how it recognizes them.
15
15
  */
16
- export declare const RESUME_NOTICE_HEADERS: readonly ['[SESSION RESUME FILE VALIDATION]', '[SESSION RESUME INTERRUPTED WORK]'];
16
+ export declare const RESUME_NOTICE_HEADERS: readonly ['[SESSION RESUME FILE VALIDATION]', '[SESSION RESUME INTERRUPTED WORK]', '[SESSION RESUME CRASH RECOVERY]'];
17
+ /**
18
+ * Build the ephemeral system message shown when the resumed session's newest
19
+ * lifecycle boundary was a dangling `in_flight_start` — i.e. the previous
20
+ * process died mid-iteration. Lists the interrupted tool calls that were
21
+ * stripped from the restored conversation (adjacency repair) so the model
22
+ * can decide whether to retry.
23
+ * Returns null when the session did not crash.
24
+ */
25
+ export declare function formatCrashRecoveryNotice(interruptedTools: ReadonlyArray<{
26
+ name: string;
27
+ argsSummary?: string | undefined;
28
+ }>, lastContext: string | null): string | null;
17
29
  /**
18
30
  * True for a system message this module produced on an earlier resume.
19
31
  *
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../../kernel/events.js';
1
+ import type { EventBus } from '../event-bus-port.js';
2
2
  export type SessionStoreReadOperation = 'load' | 'load_events_only' | 'list' | 'summary' | 'index_read';
3
3
  export type SessionStoreWriteOperation = 'create' | 'resume' | 'rename' | 'append' | 'flush' | 'close' | 'index_append' | 'compact' | 'checkpoint';
4
4
  export type SessionStoreOutcome = 'success' | 'failure';
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../../kernel/events.js';
1
+ import type { EventBus } from '../event-bus-port.js';
2
2
  import type { SecretScrubber } from '../../types/secret-scrubber.js';
3
3
  import type { SessionData } from '../../types/session.js';
4
4
  /**
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../../kernel/events.js';
1
+ import type { EventBus } from '../event-bus-port.js';
2
2
  import type { SecretScrubber } from '../../types/secret-scrubber.js';
3
3
  import type { SessionSummary } from '../../types/session.js';
4
4
  export interface RenameSessionParams {
@@ -1,5 +1,5 @@
1
1
  import * as fsp from 'node:fs/promises';
2
- import type { EventBus } from '../../kernel/events.js';
2
+ import type { EventBus } from '../event-bus-port.js';
3
3
  import type { SecretScrubber } from '../../types/secret-scrubber.js';
4
4
  import type { ResumedSession, SessionData, SessionEvent, SessionSummary } from '../../types/session.js';
5
5
  import type { SessionCheckpointCas } from '../session-checkpoint-cas.js';
@@ -20,6 +20,8 @@ export interface ResumeSessionParams {
20
20
  }>>;
21
21
  persistCatalogSummary: (summary: SessionSummary) => Promise<void>;
22
22
  logWarn: (msg: string, ctx?: Record<string, unknown>) => void;
23
+ /** Store root used to rescan the full transcript when load() dropped events. */
24
+ sessionsDir?: string | undefined;
23
25
  }
24
26
  export declare function executeResumeSession(params: ResumeSessionParams): Promise<ResumedSession>;
25
27
  export declare function openSessionForAppend(file: string): Promise<fsp.FileHandle>;
@@ -7,7 +7,8 @@ export declare function appendToIndexStrict(dir: string, indexFile: string, summ
7
7
  export declare function writeTombstone(dir: string, indexFile: string, id: string, invalidateShard: (id: string) => Promise<void>, onTombstone: () => void): Promise<void>;
8
8
  export declare function readIndexFile(indexFile: string, currentCache: IndexCacheEntry | null): Promise<{
9
9
  summaries: readonly SessionSummary[];
10
+ deletedIds: ReadonlySet<string>;
10
11
  cache: IndexCacheEntry | null;
11
12
  }>;
12
- export declare function compactIndexInner(indexFile: string, entries: readonly SessionSummary[]): Promise<void>;
13
+ export declare function compactIndexInner(indexFile: string, entries: readonly SessionSummary[], deletedIds?: ReadonlySet<string>): Promise<void>;
13
14
  //# sourceMappingURL=session-store-index.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../../kernel/events.js';
1
+ import type { EventBus } from '../event-bus-port.js';
2
2
  import type { Logger } from '../../types/logger.js';
3
3
  import type { SecretScrubber } from '../../types/secret-scrubber.js';
4
4
  import type { SessionData, SessionEvent, SessionSummary } from '../../types/session.js';
@@ -16,6 +16,28 @@ export declare class DefaultSessionStore implements SessionStore {
16
16
  private readonly _loadCache;
17
17
  private readonly loadCache;
18
18
  private _indexCache;
19
+ /**
20
+ * Tombstoned ids — hidden even if their JSONL remains on disk.
21
+ * Convention: readIndex() REASSIGNS this set from the parsed index file
22
+ * MERGED with _manualTombstones; writeTombstone() adds in-place immediately
23
+ * so an incremental cache rebuild can never resurrect a just-deleted
24
+ * session.
25
+ */
26
+ private _indexDeletedIds;
27
+ /**
28
+ * Tombstones added by THIS store between reads. Merged into every fresh
29
+ * snapshot so a read racing writeTombstone cannot drop an in-flight
30
+ * deletion; entries are pruned once the parsed index file itself carries
31
+ * them.
32
+ */
33
+ private readonly _manualTombstones;
34
+ /**
35
+ * File-truth tombstones from the last readIndex() parse (EXCLUDES
36
+ * _manualTombstones additions). compactIndexInner persists THIS snapshot so
37
+ * concurrent writeTombstones that landed after the parse are not written
38
+ * prematurely — they persist through their own append path instead.
39
+ */
40
+ private _indexFileDeletedIds;
19
41
  private readonly shardManifestCache;
20
42
  private static readonly LIST_SCAN_CONCURRENCY;
21
43
  private indexAppendCount;
@@ -30,6 +52,17 @@ export declare class DefaultSessionStore implements SessionStore {
30
52
  private shardKeyForSessionId;
31
53
  private invalidateShardManifestBySessionId;
32
54
  private ensureShardDir;
55
+ /**
56
+ * Create a fresh session writer.
57
+ *
58
+ * @threadSafety Failure-prone steps (manifest invalidation, sidecar
59
+ * removal, catalog upsert, the durable `{action:'create'}` index row)
60
+ * run BEFORE the truncating `'w'` open, so no rejection path can destroy
61
+ * prior bytes. Ordinary index summary rows never undelete a tombstone
62
+ * (the parser only honors `{action:'create'}`), so a swallowed create-row
63
+ * would leave a live writer whose id stays hidden forever — that append
64
+ * is therefore required, not best-effort.
65
+ */
33
66
  create(meta: Omit<SessionMetadata, 'startedAt'>): Promise<SessionWriter>;
34
67
  fork(id: string, opts?: SessionForkOptions): Promise<ForkedSession>;
35
68
  readRawEvents(id: string): Promise<SessionEvent[]>;
@@ -63,8 +96,45 @@ export declare class DefaultSessionStore implements SessionStore {
63
96
  private persistCatalogSummary;
64
97
  private writeTombstone;
65
98
  private compactIndex;
99
+ /**
100
+ * Compact the local index in place.
101
+ *
102
+ * Contract carried into the shared compactIndexInner helper
103
+ * (session-store-index.ts): `entries` MUST already exclude tombstoned ids,
104
+ * and the deleted-set argument is persisted VERBATIM — neither the helper
105
+ * nor its callers may resurrect filtered rows or invent deletions.
106
+ * Locking: callers MUST already hold the indexFile lock (both do:
107
+ * compactIndex() below and the appendToIndexStrict compaction hook);
108
+ * readIndex() inside reads that same locked file, so no second lock may
109
+ * be taken here (non-reentrant → deadlock).
110
+ *
111
+ * That same lock is what makes the _indexFileDeletedIds snapshot safe to
112
+ * pass across the await below: writeTombstone() appends under the identical
113
+ * non-reentrant indexFile lock, so no tombstone can land between our
114
+ * readIndex() and the snapshot handed to the helper. Compaction would
115
+ * otherwise be racing a delete it cannot see.
116
+ */
66
117
  private compactIndexInner;
67
118
  private readIndex;
119
+ /**
120
+ * Merge close-time index rows with directory-scan results, keyed by id.
121
+ * Scanned entries win — their metadata is re-derived from the transcript,
122
+ * so it reflects mid-session activity that index rows (written on close)
123
+ * cannot know about. Indexed-only ids fill gaps; duplicates within the
124
+ * index resolve last-wins, matching append order.
125
+ */
126
+ private mergeIndexWithScan;
127
+ /**
128
+ * Rebuild the index from what is actually on disk.
129
+ *
130
+ * @returns the number of healthy, live entries in the rebuilt index. Both
131
+ * backends report that same quantity: ids whose summary could not be derived
132
+ * are excluded (the catalog counts them as `damaged`; the local scan drops
133
+ * them when `summaryFor` rejects), and ids carrying a surviving tombstone are
134
+ * excluded (the catalog rebuilds only from live files; the local branch skips
135
+ * them explicitly). It is NOT a count of rows written to the file — tombstone
136
+ * rows are persisted but never counted.
137
+ */
68
138
  rebuildIndex(): Promise<number>;
69
139
  private listFromDirectoryScan;
70
140
  private collectShardKeys;
@@ -30,6 +30,14 @@ export declare class SessionSummaryTracker {
30
30
  private openToolUses;
31
31
  constructor(opts: SessionSummaryTrackerOptions);
32
32
  get currentSummary(): Readonly<SessionSummary>;
33
+ /**
34
+ * Non-destructive materialization of every live counter into a summary
35
+ * snapshot. Unlike finalize(), it stamps no endedAt, applies no final
36
+ * outcome, and resolves no name — mid-session metadata checkpoints use it
37
+ * so a killed process leaves accurate listing metadata behind without
38
+ * pretending the session ended cleanly.
39
+ */
40
+ snapshot(): SessionSummary;
33
41
  get pendingToolUses(): string[];
34
42
  observe(event: SessionEvent): void;
35
43
  recomputeFromDisk(filePath: string): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import * as fsp from 'node:fs/promises';
2
- import type { EventBus } from '../kernel/events.js';
2
+ import type { EventBus } from './event-bus-port.js';
3
3
  import type { SessionEvent } from '../types/session.js';
4
4
  export interface SessionWriteBufferOptions {
5
5
  sessionId: string;
@@ -9,6 +9,17 @@ export interface SessionWriteBufferOptions {
9
9
  events?: EventBus | undefined;
10
10
  getTraceId?: () => string | undefined;
11
11
  }
12
+ /**
13
+ * Options for explicit (immediate-path) flushes.
14
+ */
15
+ export interface FlushBufferOptions {
16
+ /**
17
+ * fsync-level durability after the append lands: disk-durable rather than
18
+ * only page-cache durable (SIGKILL survives page cache; power loss does
19
+ * not). Best-effort — a failed datasync never fails the flush itself.
20
+ */
21
+ datasync?: boolean;
22
+ }
12
23
  export declare class SessionWriteBuffer {
13
24
  private readonly opts;
14
25
  private static readonly FLUSH_INTERVAL_MS;
@@ -24,18 +35,36 @@ export declare class SessionWriteBuffer {
24
35
  private lastAppendWarnAt;
25
36
  private writeChain;
26
37
  private flushPromise;
38
+ /**
39
+ * Batch currently inside enqueueWrite. `flushSync` may steal it if the
40
+ * async append has not started, so a dying process writes in-flight +
41
+ * remaining buffer as one ordered append instead of racing a second fd.
42
+ */
43
+ private inFlight;
27
44
  constructor(opts: SessionWriteBufferOptions);
28
45
  get isClosed(): boolean;
29
46
  get length(): number;
30
47
  private eventBytes;
31
48
  push(event: SessionEvent): boolean;
32
49
  enqueueWrite(data: string): Promise<void>;
50
+ private enqueueFlight;
33
51
  scheduleFlush(isClosed?: boolean): void;
34
52
  cancelTimer(): void;
35
- flushBuffer(isClosed?: boolean): Promise<void>;
53
+ flushBuffer(isClosed?: boolean, opts?: FlushBufferOptions): Promise<void>;
36
54
  private flushBufferOnce;
37
55
  drainWriteChain(): Promise<void>;
38
56
  drainFlushPromise(): Promise<void>;
57
+ /**
58
+ * Last-gasp synchronous append (SIGKILL/SIGTERM traps, `process.on('exit')`).
59
+ *
60
+ * Failure contract: nothing is discarded before the write is known to have
61
+ * landed. Buffered events stay in `writeBuffer` — it is cleared only after
62
+ * `fsyncSync` returns — and a stolen in-flight batch is handed back to the
63
+ * async write chain, so a survivable failure (EACCES, ENOSPC, EMFILE) loses
64
+ * nothing. Failure is never silent: a structured `session.flush_sync_failed`
65
+ * warning names exactly what was still pending, which is what SIGKILL-trap
66
+ * tests assert against.
67
+ */
39
68
  flushSync(): void;
40
69
  clear(): void;
41
70
  shouldFlushNow(): boolean;
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../kernel/events.js';
1
+ import type { EventBus } from './event-bus-port.js';
2
2
  import type { TaskItem } from '../utils/task-format.js';
3
3
  export interface TaskFile {
4
4
  version: 1;
@@ -1,6 +1,6 @@
1
1
  import type { TodoItem } from '../core/context.js';
2
2
  import type { ConversationState } from '../core/conversation-state.js';
3
- import type { EventBus } from '../kernel/events.js';
3
+ import type { EventBus } from './event-bus-port.js';
4
4
  /**
5
5
  * On-disk checkpoint for `ctx.todos`. Written atomically every time the
6
6
  * todo list changes, read once on session resume. This is the missing
@@ -1,4 +1,4 @@
1
- import type { EventBus } from '../kernel/events.js';
1
+ import type { EventBus } from './event-bus-port.js';
2
2
  /**
3
3
  * ToolAuditLog — idea #9 from IDEAS.md.
4
4
  *
@@ -232,6 +232,13 @@ var TaskTracker = class _TaskTracker {
232
232
  graph = null;
233
233
  transitions = [];
234
234
  listeners = [];
235
+ /**
236
+ * Re-entrancy guard for status cascades: true while an auto-block /
237
+ * auto-unblock cascade is running. Listener-driven mutations during a
238
+ * cascade still record their own primary transition — only their
239
+ * cascading is skipped (prevents unbounded recursion).
240
+ */
241
+ cascading = false;
235
242
  static MAX_TRANSITIONS = 1e4;
236
243
  /**
237
244
  * Subscribe to live task mutations (add / update / status change). Returns an
@@ -307,6 +314,28 @@ var TaskTracker = class _TaskTracker {
307
314
  message: "No graph loaded",
308
315
  code: ERROR_CODES.SDD_INVALID_STATE
309
316
  });
317
+ if (!this.graph.nodes.has(from) || !this.graph.nodes.has(to)) {
318
+ throw new SddError({
319
+ message: `Cannot add ${type} edge ${from} -> ${to}: endpoint task is not in the graph`,
320
+ code: ERROR_CODES.SDD_NOT_READY,
321
+ context: { from, to, type }
322
+ });
323
+ }
324
+ if (from === to) {
325
+ throw new SddError({
326
+ message: `Cannot add ${type} edge: self-loop on task ${from}`,
327
+ code: ERROR_CODES.SDD_INVALID_STATE,
328
+ context: { nodeId: from, type }
329
+ });
330
+ }
331
+ if (this.graph.edges.some((e) => e.from === from && e.to === to && e.type === type)) return;
332
+ if (type === "depends_on" && this.dependsOnTransitively(from, to, /* @__PURE__ */ new Set())) {
333
+ throw new SddError({
334
+ message: `Cannot add depends_on edge ${from} -> ${to}: it would create a dependency cycle`,
335
+ code: ERROR_CODES.SDD_INVALID_STATE,
336
+ context: { from, to }
337
+ });
338
+ }
310
339
  this.graph.edges.push({
311
340
  id: crypto.randomUUID(),
312
341
  from,
@@ -407,7 +436,7 @@ var TaskTracker = class _TaskTracker {
407
436
  if (status === "in_progress") {
408
437
  node.startedAt = now;
409
438
  }
410
- this.transitions.push({ from, to: status, timestamp: now, reason });
439
+ this.transitions.push({ nodeId: id, from, to: status, timestamp: now, reason });
411
440
  if (this.transitions.length > _TaskTracker.MAX_TRANSITIONS) {
412
441
  this.transitions.splice(0, this.transitions.length - _TaskTracker.MAX_TRANSITIONS);
413
442
  }
@@ -423,7 +452,7 @@ var TaskTracker = class _TaskTracker {
423
452
  type: "status_changed",
424
453
  nodeId: id,
425
454
  node,
426
- transition: { from, to: status, timestamp: now, reason }
455
+ transition: { nodeId: id, from, to: status, timestamp: now, reason }
427
456
  });
428
457
  }
429
458
  updateNode(id, patch) {
@@ -514,40 +543,89 @@ var TaskTracker = class _TaskTracker {
514
543
  }
515
544
  return computeTaskProgress(this.graph);
516
545
  }
517
- getTransitions(_taskId) {
518
- return [...this.transitions];
546
+ /**
547
+ * Recorded transitions in insertion order (oldest first — the array is
548
+ * trimmed from the front). With `taskId`, only that task's transitions are
549
+ * returned, still oldest-first; a consumer wanting newest-first reverses
550
+ * the result. Per-task filtering is possible because every TaskTransition
551
+ * carries `nodeId` (BIZ-005).
552
+ */
553
+ getTransitions(taskId) {
554
+ return taskId === void 0 ? [...this.transitions] : this.transitions.filter((t) => t.nodeId === taskId);
555
+ }
556
+ /**
557
+ * Record a cascade-driven status change (BIZ-003): mutate the node, push a
558
+ * TaskTransition, persist, and notify subscribers — the same effects an
559
+ * explicit updateNodeStatus produces — so the board projector's live
560
+ * snapshot and the transition log observe auto-block/auto-unblock.
561
+ */
562
+ recordCascadeStatusChange(nodeId, node, to, reason) {
563
+ const from = node.status;
564
+ if (from === to) return;
565
+ const now = Date.now();
566
+ node.status = to;
567
+ node.updatedAt = now;
568
+ if (this.graph) this.graph.updatedAt = now;
569
+ this.transitions.push({ nodeId, from, to, timestamp: now, reason });
570
+ if (this.transitions.length > _TaskTracker.MAX_TRANSITIONS) {
571
+ this.transitions.splice(0, this.transitions.length - _TaskTracker.MAX_TRANSITIONS);
572
+ }
573
+ this.persist();
574
+ this.notifyChange({
575
+ type: "status_changed",
576
+ nodeId,
577
+ node,
578
+ transition: { nodeId, from, to, timestamp: now, reason }
579
+ });
519
580
  }
520
581
  unblockDependents(completedId) {
521
- if (!this.graph) return;
522
- const dependents = this.getDependents(completedId);
523
- for (const depId of dependents) {
524
- const dep = this.graph.nodes.get(depId);
525
- if (dep?.status === "blocked") {
582
+ if (!this.graph || this.cascading) return;
583
+ this.cascading = true;
584
+ try {
585
+ const dependents = this.getDependents(completedId);
586
+ for (const depId of dependents) {
587
+ const dep = this.graph.nodes.get(depId);
588
+ if (dep?.status !== "blocked") continue;
526
589
  const remainingBlockers = this.getBlockers(depId);
527
590
  const allUnblocked = remainingBlockers.every((id) => {
528
591
  const blocker = this.graph?.nodes.get(id);
529
592
  return blocker?.status === "completed" || blocker?.status === "failed";
530
593
  });
531
594
  if (allUnblocked) {
532
- dep.status = "pending";
533
- dep.updatedAt = Date.now();
595
+ this.recordCascadeStatusChange(
596
+ depId,
597
+ dep,
598
+ "pending",
599
+ `auto-unblocked: blocker ${completedId} reached a terminal state`
600
+ );
534
601
  }
535
602
  }
603
+ } finally {
604
+ this.cascading = false;
536
605
  }
537
606
  }
538
607
  checkAndBlockIfNeeded(taskId) {
539
- if (!this.graph) return;
540
- const blockers = this.getBlockers(taskId);
541
- const someBlocked = blockers.some((id) => {
542
- const blocker = this.graph?.nodes.get(id);
543
- return blocker?.status !== "completed" && blocker?.status !== "failed";
544
- });
545
- if (someBlocked) {
546
- const node = this.graph.nodes.get(taskId);
547
- if (node) {
548
- node.status = "blocked";
549
- node.updatedAt = Date.now();
608
+ if (!this.graph || this.cascading) return;
609
+ this.cascading = true;
610
+ try {
611
+ const blockers = this.getBlockers(taskId);
612
+ const incomplete = blockers.filter((id) => {
613
+ const blocker = this.graph?.nodes.get(id);
614
+ return blocker?.status !== "completed" && blocker?.status !== "failed";
615
+ });
616
+ if (incomplete.length > 0) {
617
+ const node = this.graph.nodes.get(taskId);
618
+ if (node) {
619
+ this.recordCascadeStatusChange(
620
+ taskId,
621
+ node,
622
+ "blocked",
623
+ `auto-blocked: incomplete blocker(s) ${incomplete.join(", ")}`
624
+ );
625
+ }
550
626
  }
627
+ } finally {
628
+ this.cascading = false;
551
629
  }
552
630
  }
553
631
  /**
@@ -20,6 +20,8 @@ export interface TaskTrackerOptions {
20
20
  onPersistError?: ((err: unknown) => void) | undefined;
21
21
  }
22
22
  export interface TaskTransition {
23
+ /** The task the transition applies to — required for per-task filtering. */
24
+ nodeId: string;
23
25
  from: TaskNode['status'];
24
26
  to: TaskNode['status'];
25
27
  timestamp: number;
@@ -39,6 +41,13 @@ export declare class TaskTracker {
39
41
  private graph;
40
42
  private transitions;
41
43
  private listeners;
44
+ /**
45
+ * Re-entrancy guard for status cascades: true while an auto-block /
46
+ * auto-unblock cascade is running. Listener-driven mutations during a
47
+ * cascade still record their own primary transition — only their
48
+ * cascading is skipped (prevents unbounded recursion).
49
+ */
50
+ private cascading;
42
51
  private static readonly MAX_TRANSITIONS;
43
52
  constructor(opts: TaskTrackerOptions);
44
53
  /**
@@ -89,7 +98,21 @@ export declare class TaskTracker {
89
98
  getBlockers(taskId: string): string[];
90
99
  canStart(taskId: string): boolean;
91
100
  getProgress(): TaskProgress;
92
- getTransitions(_taskId?: string): TaskTransition[];
101
+ /**
102
+ * Recorded transitions in insertion order (oldest first — the array is
103
+ * trimmed from the front). With `taskId`, only that task's transitions are
104
+ * returned, still oldest-first; a consumer wanting newest-first reverses
105
+ * the result. Per-task filtering is possible because every TaskTransition
106
+ * carries `nodeId` (BIZ-005).
107
+ */
108
+ getTransitions(taskId?: string): TaskTransition[];
109
+ /**
110
+ * Record a cascade-driven status change (BIZ-003): mutate the node, push a
111
+ * TaskTransition, persist, and notify subscribers — the same effects an
112
+ * explicit updateNodeStatus produces — so the board projector's live
113
+ * snapshot and the transition log observe auto-block/auto-unblock.
114
+ */
115
+ private recordCascadeStatusChange;
93
116
  private unblockDependents;
94
117
  private checkAndBlockIfNeeded;
95
118
  /**
@@ -1,4 +1,4 @@
1
- import type { Context } from '../core/context.js';
1
+ import type { AgentContext } from './context.js';
2
2
  export interface CompactRepairReport {
3
3
  removedToolUses: string[];
4
4
  removedToolResults: string[];
@@ -58,7 +58,7 @@ export interface CompactReport {
58
58
  } | undefined;
59
59
  }
60
60
  export interface Compactor {
61
- compact(ctx: Context, opts?: {
61
+ compact(ctx: AgentContext, opts?: {
62
62
  aggressive?: boolean | undefined;
63
63
  }): Promise<CompactReport>;
64
64
  }