Skip to content

Commit 40d9c4e

Browse files
committed
fix: address PR review findings
Preserve terminal prompt states during replay. Tighten review guards and regression coverage for retry delays, abort cleanup, and JavaScript comment scanning.\n\nRefs PR #188 cleanup C3.
1 parent 9d56da3 commit 40d9c4e

9 files changed

Lines changed: 58 additions & 10 deletions

File tree

.agents/skills/agent-core-dev/service-authoring.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ What belongs here:
164164
- **Helper classes / functions** used only by this impl (e.g. a built-in writer, an `extractError` helper) — co-located in the same file.
165165
- **Top-level `registerScopedService(...)`** — one per Service the file owns; importing the impl file runs the registration.
166166

167-
Base class: extend `Service` (from `#/_base/di/service`) when the unit needs capability calls on `this``provide` / `effect` / `on` / `get` / `ref` (e.g. contributing a record to a `collection` token). `Service` extends `Disposable`, so `_register` keeps working; constructor-time `provide` / `on` / `effect` calls are buffered and flushed by the kernel after construction, while `get` / `ref` throw inside the constructor (dependencies stay constructor parameters). Otherwise extend `Disposable` — both are full DI units; a service whose own members collide with the `Service` vocabulary (`name` / `state` / `config` / `get`) must stay on `Disposable` (leave a NOTE comment saying so).
167+
Base class: extend `Service` (from `#/_base/di/service`) when the unit needs capability calls on `this``provide` / `effect` / `on` / `get` / `ref` (e.g. contributing a record to a `collection` token). `Service` extends `Disposable`, so `_register` keeps working; constructor-time `provide` / `on` / `effect` calls are buffered and flushed by the kernel after construction, while `get` / `ref` throw inside the constructor (dependencies stay constructor parameters). Otherwise extend `Disposable` — both are full DI units; a service whose own members collide with the `Service` vocabulary (`name` / `state` / `config` / `get`) must stay on `Disposable`.
168168

169169
## Constructor conventions
170170

@@ -298,7 +298,6 @@ Importing the package therefore fires every `register*` side effect, exactly as
298298

299299
- **No comments** (orient.md): no file headers, no statement-level narration, no JSDoc; the only exception is a load-bearing lint-suppression directive.
300300
- **Methods and fields carry no comments by default.** Well-named identifiers and types say *what*; the code is the source of truth for *how*.
301-
- Write an inline comment only when the *why* is non-obvious (a hidden constraint, a subtle invariant, a workaround). One short line.
302301
- For unimplemented stubs, throw `NotImplementedError('feature')` rather than `throw new Error('TODO: …')` (errors.md).
303302

304303
## Complete minimal example

.agents/skills/agent-core-dev/test.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Resolving by interface is what makes `registerScopedService(ISut, Sut, …)` par
2121

2222
Pure functions, value objects, and services with **no** `@IService` dependencies may be constructed directly.
2323

24-
The only other exception is a test that genuinely needs **two independent instances** of the same service with different dependencies (e.g. constructing two `TurnService`s with different `ILoopRunner`s). A singleton-per-container resolution cannot produce both, so `ix.createInstance(Impl)` is acceptable there — annotate it with a comment explaining why.
24+
The only other exception is a test that genuinely needs **two independent instances** of the same service with different dependencies (e.g. constructing two `TurnService`s with different `ILoopRunner`s). A singleton-per-container resolution cannot produce both, so `ix.createInstance(Impl)` is acceptable there — state the reason in the test name and local identifiers.
2525

2626
## Two harnesses
2727

packages/agent-core-v2/test/agent/llmRequester/llmRequesterService.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,17 +390,20 @@ describe('AgentLLMRequesterService infinite retry', () => {
390390
});
391391

392392
it('honors the provider retry-after delay while retrying indefinitely', async () => {
393+
vi.useFakeTimers();
393394
const calls = { value: 0 };
394395
const requester = createRequester(calls, new APIProviderRateLimitError('slow down', null, 1));
395396
const { service } = createService(requester, undefined, {
396397
env: { [PYTHINKER_CODE_INFINITE_RETRY_ENV]: '1' },
397398
});
398399

399-
const startedAt = Date.now();
400-
await service.request();
400+
const promise = service.request();
401+
await vi.advanceTimersByTimeAsync(0);
402+
expect(calls.value).toBe(1);
403+
await vi.advanceTimersByTimeAsync(1);
404+
await promise;
401405

402406
expect(calls.value).toBe(2);
403-
expect(Date.now() - startedAt).toBeLessThan(500);
404407
});
405408

406409
it('stops retrying when the caller aborts during the backoff wait', async () => {

packages/agent-core-v2/test/workspace/workspaceFs/fsProcess.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,20 @@ describe('runCommand', () => {
8383
await runCommand(fakeRunner(fakeProcess()), ['echo'], { signal: controller.signal });
8484
expect(listenerCount(controller.signal as unknown as EventEmitter, 'abort')).toBe(0);
8585
});
86+
87+
it('removes the abort listener when stream collection fails', async () => {
88+
const controller = new AbortController();
89+
const proc = {
90+
...fakeProcess(),
91+
stdout: new Readable({
92+
read() {
93+
this.destroy(new Error('stream failed'));
94+
},
95+
}),
96+
};
97+
await expect(
98+
runCommand(fakeRunner(proc), ['echo'], { signal: controller.signal }),
99+
).rejects.toThrow('stream failed');
100+
expect(listenerCount(controller.signal as unknown as EventEmitter, 'abort')).toBe(0);
101+
});
86102
});

packages/agent-gateway/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The Pythinker Code server, backed by the DI × Scope agent engine (`@pymodel/age
44

55
## Comment conventions
66

7-
No comments — no file headers, no section banners, no statement-level narration; the code is the source of truth. The only exception is JSDoc attached to exported symbols (it flows into the generated `.d.ts` and IDE hover). Lint-suppression directives (`oxlint-disable` / `eslint-disable`) are allowed where they suppress an active rule for a deliberate pattern; other tooling directives (`@ts-expect-error`, `@ts-ignore`, …) stay banned — fix the underlying type problem instead. Enforced by `scripts/check-no-comments.mjs` (part of `pnpm lint`).
7+
No comments — no file headers, no section banners, no statement-level narration, and no JSDoc. The only exception is a load-bearing lint-suppression directive (`oxlint-disable` / `eslint-disable`) for a deliberate pattern; other tooling directives (`@ts-expect-error`, `@ts-ignore`, …) stay banned — fix the underlying type problem instead. Enforced by `scripts/check-no-comments.mjs` (part of `pnpm lint`).
88

99
## Routes
1010

packages/agent-gateway/src/services/transcript/coreEventMap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1257,7 +1257,7 @@ export class AgentTranscriptProjector {
12571257
private onPromptStarted(event: PromptStartedEvent): TranscriptOperation[] {
12581258
const prompt = this.upsertPrompt(event.promptId, (prev) => ({
12591259
promptId: event.promptId,
1260-
status: 'running',
1260+
status: prev !== undefined && isTerminalPromptStatus(prev.status) ? prev.status : 'running',
12611261
userMessageId: prev?.userMessageId,
12621262
content: prev?.content,
12631263
createdAt: prev?.createdAt ?? new Date().toISOString(),

packages/agent-gateway/test/services/transcript.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,36 @@ describe('AgentTranscriptProjector', () => {
17751775
});
17761776
});
17771777

1778+
it('preserves terminal prompt status when prompt.started is replayed', () => {
1779+
const projector = new AgentTranscriptProjector('main');
1780+
const tx = new AgentTranscript('main');
1781+
const feed = (event: ProjectorBusEvent): void => void tx.apply(projector.map(event));
1782+
1783+
for (const status of ['completed', 'failed', 'blocked'] as const) {
1784+
const promptId = `p-${status}`;
1785+
feed(
1786+
ev({
1787+
type: 'prompt.completed',
1788+
promptId,
1789+
finishedAt: '2026-01-01T00:00:00.000Z',
1790+
reason: status,
1791+
}),
1792+
);
1793+
feed(ev({ type: 'prompt.started', promptId }));
1794+
expect(tx.getPrompt(promptId)?.status).toBe(status);
1795+
}
1796+
1797+
feed(
1798+
ev({
1799+
type: 'prompt.aborted',
1800+
promptId: 'p-aborted',
1801+
abortedAt: '2026-01-01T00:00:00.000Z',
1802+
}),
1803+
);
1804+
feed(ev({ type: 'prompt.started', promptId: 'p-aborted' }));
1805+
expect(tx.getPrompt('p-aborted')?.status).toBe('aborted');
1806+
});
1807+
17781808
it('projects prompt.steered media content to the wire shape (no daemon ref or path leak)', () => {
17791809
const projector = new AgentTranscriptProjector('main');
17801810
const tx = new AgentTranscript('main');

packages/transcript/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The isomorphic transcript rendering data layer — agent-granular L1 store, idem
44

55
## Comment conventions
66

7-
No comments — no file headers, no section banners, no statement-level narration; the code is the source of truth. The only exception is JSDoc attached to exported symbols (it flows into the generated `.d.ts` and IDE hover). Lint-suppression directives (`oxlint-disable` / `eslint-disable`) are allowed where they suppress an active rule for a deliberate pattern; other tooling directives (`@ts-expect-error`, `@ts-ignore`, …) stay banned — fix the underlying type problem instead. Enforced by `scripts/check-no-comments.mjs` (part of `pnpm lint`).
7+
No comments — no file headers, no section banners, no statement-level narration, and no JSDoc. The only exception is a load-bearing lint-suppression directive (`oxlint-disable` / `eslint-disable`) for a deliberate pattern; other tooling directives (`@ts-expect-error`, `@ts-ignore`, …) stay banned — fix the underlying type problem instead. Enforced by `scripts/check-no-comments.mjs` (part of `pnpm lint`).
88

99
## Cold rebuild
1010

scripts/check-no-comments.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ for (const pkg of PACKAGES) {
9999
const p = path.join(d, e.name);
100100
if (e.isDirectory()) {
101101
if (e.name !== 'node_modules') stack.push(p);
102-
} else if (/\.(ts|tsx|mts|mjs)$/.test(e.name)) {
102+
} else if (/\.(ts|tsx|js|jsx|mts|mjs)$/.test(e.name)) {
103103
files.push(p);
104104
}
105105
}

0 commit comments

Comments
 (0)