@parall/codex-agent 1.45.0 → 1.46.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.
@@ -5,6 +5,22 @@ import type { ForkSessionHandle } from '@parall/agent-core';
5
5
  type PersistedThread = {
6
6
  runtimeKey: string;
7
7
  threadId: string;
8
+ /**
9
+ * sha256 (hex) of the platform instructions last known EFFECTIVE on the
10
+ * main thread — i.e. present in the thread's model-visible initial context:
11
+ * recorded when the thread is started (thread/start bakes them in) or after
12
+ * a compaction rebuilt the context from the resumed configuration. NOT
13
+ * recorded on resume alone — resume updates the canonical configuration
14
+ * but leaves the model-visible context untouched (see
15
+ * instructions-refresh.ts). ONE exception weakens the "proven visible"
16
+ * reading: a legacy state file (no sha) ADOPTS the current prompt as
17
+ * baseline without a compaction — there the sha means "assumed baseline,
18
+ * converges at the next change or organic compaction", not verified
19
+ * delivery. Absent on state files from before this tracking existed.
20
+ * Dropped whenever the thread id changes (an effectiveness fact is
21
+ * per-thread).
22
+ */
23
+ effectiveInstructionsSha?: string;
8
24
  };
9
25
 
10
26
  type Logger = { warn(message: string): void };
@@ -17,6 +33,7 @@ type Logger = { warn(message: string): void };
17
33
  */
18
34
  export class CodexSessionManager {
19
35
  private readonly threadIds = new Map<string, string>();
36
+ private readonly effectiveInstructionsShas = new Map<string, string>();
20
37
 
21
38
  constructor(
22
39
  readonly mainSessionKey: string,
@@ -35,9 +52,47 @@ export class CodexSessionManager {
35
52
  }
36
53
 
37
54
  recordThreadId(sessionKey: string, threadId: string) {
55
+ // An effective-instructions sha is a fact about one specific thread.
56
+ // Recording a DIFFERENT thread id under the same session invalidates it;
57
+ // re-recording the same id (the resume path) must preserve it, or every
58
+ // bridge restart would trigger a redundant refresh.
59
+ if (this.threadIds.get(sessionKey) !== threadId) {
60
+ this.effectiveInstructionsShas.delete(sessionKey);
61
+ }
38
62
  this.threadIds.set(sessionKey, threadId);
39
63
  if (sessionKey === this.mainSessionKey) {
40
- this.persist(threadId);
64
+ this.persist();
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Record a freshly-STARTED thread together with the sha of the
70
+ * instructions baked into it — one atomic state-file write. Two separate
71
+ * writes (thread id, then sha) would leave a crash window whose survivor
72
+ * looks exactly like a pre-tracking legacy file, and the reconcile path
73
+ * deliberately does NOT compact legacy state — a stale thread would then
74
+ * be recorded as carrying instructions it never saw.
75
+ */
76
+ recordStartedThread(sessionKey: string, threadId: string, effectiveSha: string | undefined) {
77
+ this.threadIds.set(sessionKey, threadId);
78
+ if (effectiveSha) {
79
+ this.effectiveInstructionsShas.set(sessionKey, effectiveSha);
80
+ } else {
81
+ this.effectiveInstructionsShas.delete(sessionKey);
82
+ }
83
+ if (sessionKey === this.mainSessionKey) {
84
+ this.persist();
85
+ }
86
+ }
87
+
88
+ getEffectiveInstructionsSha(sessionKey: string): string | undefined {
89
+ return this.effectiveInstructionsShas.get(sessionKey);
90
+ }
91
+
92
+ recordEffectiveInstructionsSha(sessionKey: string, sha: string) {
93
+ this.effectiveInstructionsShas.set(sessionKey, sha);
94
+ if (sessionKey === this.mainSessionKey) {
95
+ this.persist();
41
96
  }
42
97
  }
43
98
 
@@ -50,11 +105,13 @@ export class CodexSessionManager {
50
105
 
51
106
  cleanupFork(sessionKey: string) {
52
107
  this.threadIds.delete(sessionKey);
108
+ this.effectiveInstructionsShas.delete(sessionKey);
53
109
  }
54
110
 
55
111
  /** Clear the main thread id when app-server rejects it (stale / deleted). */
56
112
  clearMainThread() {
57
113
  this.threadIds.delete(this.mainSessionKey);
114
+ this.effectiveInstructionsShas.delete(this.mainSessionKey);
58
115
  try {
59
116
  fs.rmSync(this.stateFilePath, { force: true });
60
117
  } catch (error) {
@@ -74,6 +131,15 @@ export class CodexSessionManager {
74
131
  parsed.threadId.trim()
75
132
  ) {
76
133
  this.threadIds.set(this.mainSessionKey, parsed.threadId.trim());
134
+ if (
135
+ typeof parsed.effectiveInstructionsSha === 'string' &&
136
+ parsed.effectiveInstructionsSha.trim()
137
+ ) {
138
+ this.effectiveInstructionsShas.set(
139
+ this.mainSessionKey,
140
+ parsed.effectiveInstructionsSha.trim(),
141
+ );
142
+ }
77
143
  }
78
144
  } catch (error) {
79
145
  if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
@@ -84,7 +150,9 @@ export class CodexSessionManager {
84
150
  }
85
151
  }
86
152
 
87
- private persist(threadId: string) {
153
+ private persist() {
154
+ const threadId = this.threadIds.get(this.mainSessionKey);
155
+ if (!threadId) return;
88
156
  // Atomic write: truncate-in-place risks leaving a half-written / zero-byte
89
157
  // state file if the process dies between `open(O_TRUNC)` and the final
90
158
  // fsync. Since this file is the sole source of cross-restart `resume`
@@ -94,10 +162,10 @@ export class CodexSessionManager {
94
162
  try {
95
163
  fs.mkdirSync(path.dirname(this.stateFilePath), { recursive: true });
96
164
  const tmpPath = `${this.stateFilePath}.tmp`;
97
- fs.writeFileSync(
98
- tmpPath,
99
- JSON.stringify({ runtimeKey: this.mainSessionKey, threadId }, null, 2),
100
- );
165
+ const state: PersistedThread = { runtimeKey: this.mainSessionKey, threadId };
166
+ const effectiveSha = this.effectiveInstructionsShas.get(this.mainSessionKey);
167
+ if (effectiveSha) state.effectiveInstructionsSha = effectiveSha;
168
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
101
169
  fs.renameSync(tmpPath, this.stateFilePath);
102
170
  } catch (error) {
103
171
  this.logger?.warn(
package/src/turn-sink.ts CHANGED
@@ -45,4 +45,10 @@ export class TurnSink {
45
45
  this.resolver = null;
46
46
  r?.({ kind: 'turn_end' });
47
47
  }
48
+
49
+ /** True once close() ran — the sink's terminal state (queued envelopes may
50
+ * still drain via next()). */
51
+ get isClosed(): boolean {
52
+ return this.closed;
53
+ }
48
54
  }
package/src/workspace.ts CHANGED
@@ -563,12 +563,11 @@ function systemPromptCopyPath(workspaceDir: string): string {
563
563
  // guide, before skill references; empty/absent = no capability section.
564
564
  // Split out from ensureCodexWorkspace so the bridge can rebuild the prompt on
565
565
  // a platform-config heat-update and hand the new value to
566
- // adapter.updateConfig, which carries it into the NEXT thread/start. A thread
567
- // that is already open keeps the instructions it was started with — codex
568
- // bakes them into the thread at start and neither thread/resume nor
569
- // thread/fork replaces them (verified against a live 0.144.1 app-server; the
570
- // retired workspace-config channel behaved identically). See
571
- // docs/tech-debt/codex-persisted-thread-prompt-refresh.md.
566
+ // adapter.updateConfig. Delivery to threads is the adapter's job: baked into
567
+ // new threads at thread/start; converged onto the persisted main thread via
568
+ // the lazy restart's fresh-process thread/resume (canonical configuration)
569
+ // plus an explicit compaction (model-visible context) see
570
+ // src/instructions-refresh.ts for the two-plane semantics.
572
571
  // Throws on failure BY DESIGN — at boot the prompt is mandatory, so a write
573
572
  // failure must fail startup loudly; the refresh hot path wraps this in
574
573
  // try/catch and retries next refresh.