Skip to content

Commit 4072796

Browse files
committed
feat(daemon): add correlated prompt lifecycles
Closes #3
1 parent 81faafa commit 4072796

39 files changed

Lines changed: 6543 additions & 330 deletions

.pylon/features.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ decisions:
7676
revisit_when:
7777
- Prime publishes list, correlation, cancellation, and terminal lifecycle events.
7878

79+
80+
correlated-prompt-lifecycle:
81+
area: prompt-ownership
82+
state: candidate
83+
owner: shared
84+
decision: hybridize
85+
pylon_refs:
86+
- https://github.com/pylon-code/prime-agent/issues/3
87+
- https://github.com/pylon-code/prime-agent/pull/9
88+
- https://github.com/pylon-code/pylon/issues/114
89+
upstream_refs:
90+
- https://github.com/PrimeIntellect-ai/prime-agent/pull/800
91+
- https://github.com/PrimeIntellect-ai/prime-agent/pull/1239
92+
- https://github.com/PrimeIntellect-ai/prime-agent/pull/1859
93+
- https://github.com/PrimeIntellect-ai/prime-agent/pull/1861
94+
fork_change: negotiated-correlated-prompt-lifecycle-v1
95+
upstream_support: Prime has queue, background, cancellation, reconnect, and event primitives, but no optional contract proving per-prompt ownership, delivery, provenance, terminal usage, and durable generation-scoped recovery together.
96+
revisit_when:
97+
- Prime upstream ships an equivalent negotiated lifecycle with exact delivery, provenance, scoped cancellation, recovery, and per-prompt usage guarantees.
98+
- Pylon can remove the fork contract without weakening stock-Prime fallback behavior.
99+
79100
heartbeat-projection:
80101
area: autonomous-work
81102
state: blocked

.pylon/upstream-review.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,14 @@ This ledger records Prime upstream evidence and the decision taken for each over
3434
- `kernel-protocol-test-fixtures`: **adopt** the exact three-line repair from open Prime PR #1886 at `a97f6995d39f2cccbd33123ab246f9786fc8ac13`. Preserve that upstream commit as a merge parent rather than creating Pylon-specific behavior.
3535
- Validation: the focused Prime Inference catalog test passed all 12 cases; the two protocol fixture suites passed all 39 cases; `npm run check` passed Biome over 937 files, the TypeScript check, installer rendering, and the browser smoke check; `git diff --check` passed.
3636
- Revisit when Prime adopts deterministic catalog fixtures or repo-owned structural invariants, or stops regenerating vendor pricing before tests and releases.
37+
38+
## 2026-08-29 — correlated prompt lifecycle candidate
39+
40+
- Upstream baseline: `PrimeIntellect-ai/prime-agent@d60fab8a76d9c169f945341f0ee3bde21903bb55`; latest audited release remains `v0.8.1`.
41+
- Reviewed Prime PRs #800, #1239, #1859, and #1861 plus the current daemon prompt, queue, cancellation, compact stream, worker recovery, update restart, and agent-connection paths. These changes provide useful primitives but no single optional contract proves exact per-prompt ownership, delivery, provenance, scoped cancellation, reconnect/recovery, generation fencing, request identity, and terminal usage.
42+
- `correlated-prompt-lifecycle`: **hybridize**. Retain Prime's existing session and queue machinery behind Pylon's optional `correlated_prompt_lifecycle_v1` daemon/client capability. Keep protocol version 7, use additive schema revision 24, keep the capability out of default and required client capability sets, and shape lifecycle/provenance only for clients that explicitly negotiate it.
43+
- The fork candidate adds dedicated correlated submit, cancel, and read-only lifecycle commands; exact session-generation fencing; canonical request fingerprints and conflicting retry rejection; durable command and worker recovery; delivery-aware cancellation; bounded lifecycle records/tombstones; prompt or session event provenance; per-prompt terminal usage; and fail-closed reconnect, replacement, and recovery validation. Stock clients and daemons retain their legacy prompt behavior.
44+
- Replacement and reload are fenced at the session runtime boundary while any correlated lifecycle is nonterminal. Worker recovery replays only queued or selected actions; preparation that may already have run arbitrary extension hooks is terminalized as failed instead of being replayed.
45+
- Fork candidate: [pylon-code/prime-agent#9](https://github.com/pylon-code/prime-agent/pull/9).
46+
- Validation: the post-rebase lifecycle, connection, supervisor, runtime, recovery, and ledger checkpoint passed 564 tests across 17 files. The final changed-boundary checkpoint passed 202 connection and supervisor-monitor tests; the clean supervisor process checkpoint passed 10 tests with 8 fixture-gated skips. `npm run check` passed Biome over 940 files, TypeScript, installer rendering, and browser smoke; `git diff --check` passed. Independent final and follow-up audits found no remaining P0 or P1 findings.
47+
- Revisit when Prime upstream ships an explicitly negotiated equivalent with the same delivery, provenance, scoped cancellation, recovery, generation, retry-integrity, privacy, and per-prompt usage guarantees.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Added optional correlated prompt lifecycle tracking and event provenance for capable daemon clients.

packages/coding-agent/src/core/agent-session-runtime.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
6868
private beforeSessionInvalidate?: () => void;
6969
private subagentRuntimeHost?: SubagentRuntimeHost;
7070
private subagentRuntimes = new Map<string, AgentSessionRuntime>();
71+
private sessionReplacementInProgress = false;
7172
private disposePromise?: Promise<void>;
7273

7374
constructor(
@@ -208,6 +209,7 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
208209

209210
private bindRuntimeHost(): void {
210211
this._session.setSubagentRuntimeHost(this.subagentRuntimeHost ?? this);
212+
this._session.setSessionReplacementAdmissionGuard(() => this.sessionReplacementInProgress);
211213
}
212214

213215
private apply(result: CreateAgentSessionRuntimeResult): void {
@@ -389,6 +391,20 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
389391
}
390392
}
391393

394+
private async withSessionReplacementFence<T>(operation: () => Promise<T>): Promise<T> {
395+
if (this.sessionReplacementInProgress) {
396+
throw new Error("A session replacement or reload is already in progress");
397+
}
398+
const release = this.session.acquireSessionReplacementFence();
399+
this.sessionReplacementInProgress = true;
400+
try {
401+
return await operation();
402+
} finally {
403+
this.sessionReplacementInProgress = false;
404+
release();
405+
}
406+
}
407+
392408
private async finishSessionReplacement(withSession?: (ctx: ReplacedSessionContext) => Promise<void>): Promise<void> {
393409
if (this.rebindSession) {
394410
await this.rebindSession(this.session);
@@ -407,6 +423,16 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
407423
cwdOverride?: string;
408424
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
409425
},
426+
): Promise<{ cancelled: boolean }> {
427+
return this.withSessionReplacementFence(() => this.switchSessionUnfenced(sessionPath, options));
428+
}
429+
430+
private async switchSessionUnfenced(
431+
sessionPath: string,
432+
options?: {
433+
cwdOverride?: string;
434+
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
435+
},
410436
): Promise<{ cancelled: boolean }> {
411437
const beforeResult = await this.emitBeforeSwitch("resume", sessionPath);
412438
if (beforeResult.cancelled) {
@@ -449,6 +475,14 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
449475
parentSession?: string;
450476
setup?: (sessionManager: SessionManager) => Promise<void>;
451477
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
478+
}): Promise<{ cancelled: boolean }> {
479+
return this.withSessionReplacementFence(() => this.newSessionUnfenced(options));
480+
}
481+
482+
private async newSessionUnfenced(options?: {
483+
parentSession?: string;
484+
setup?: (sessionManager: SessionManager) => Promise<void>;
485+
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
452486
}): Promise<{ cancelled: boolean }> {
453487
const beforeResult = await this.emitBeforeSwitch("new");
454488
if (beforeResult.cancelled) {
@@ -498,6 +532,16 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
498532
position?: "before" | "at";
499533
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
500534
},
535+
): Promise<{ cancelled: boolean; selectedText?: string }> {
536+
return this.withSessionReplacementFence(() => this.forkUnfenced(entryId, options));
537+
}
538+
539+
private async forkUnfenced(
540+
entryId: string,
541+
options?: {
542+
position?: "before" | "at";
543+
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
544+
},
501545
): Promise<{ cancelled: boolean; selectedText?: string }> {
502546
const position = options?.position ?? "before";
503547
const beforeResult = await this.emitBeforeFork(entryId, { position });
@@ -629,6 +673,10 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
629673
* @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided.
630674
*/
631675
async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
676+
return this.withSessionReplacementFence(() => this.importFromJsonlUnfenced(inputPath, cwdOverride));
677+
}
678+
679+
private async importFromJsonlUnfenced(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
632680
const resolvedPath = resolve(inputPath);
633681
if (!existsSync(resolvedPath)) {
634682
throw new SessionImportFileNotFoundError(resolvedPath);

0 commit comments

Comments
 (0)