Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// references become '(circular)', and class instances collapse to a '(ClassName)'
// marker — the wire shape of an entry is the JSON projection of the type here.
//
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 83 keys)
// Index (App: 0 keys · Workspace: 6 keys · Session: 9 keys · Agent: 82 keys)
// App
// Workspace
// workspaceDirs.ephemeralDirs src/workspace/workspaceDirs/workspaceDirsService.ts
Expand Down Expand Up @@ -55,7 +55,6 @@
// agentPlugin.sessionStartRefreshPending src/agent/plugin/agentPluginService.ts
// agentsMdReminder.cwd src/agent/agentsMdReminder/agentsMdReminderService.ts
// agentsMdReminder.known src/agent/agentsMdReminder/agentsMdReminderService.ts
// agentsMdReminder.pending src/agent/agentsMdReminder/agentsMdReminderService.ts
// agentsMdReminder.seeded src/agent/agentsMdReminder/agentsMdReminderService.ts
// contextMemory src/agent/contextMemory/contextOps.ts
// contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts
Expand Down Expand Up @@ -1041,7 +1040,6 @@ export interface AgentStateSnapshot {
// src/agent/agentsMdReminder/agentsMdReminderService.ts
'agentsMdReminder.cwd': string | undefined;
'agentsMdReminder.known': Set<string>;
'agentsMdReminder.pending': Set<string>;
'agentsMdReminder.seeded': boolean;
// src/agent/contextMemory/contextOps.ts
// replayable · durable · undoable — folds: ContextAppendMessage, ContextAppendLoopEvent, ContextClear, ContextApplyCompaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,12 @@ const AGENTS_MD_BASENAMES: ReadonlySet<string> = new Set<string>(AGENTS_MD_PLAIN

const BASH_PARSE_OPTIONS = { timeoutMs: 20, maxNodes: 10_000 } as const;

const DISCOVERY_REMINDER_VARIANT = 'agents_md';

export const agentsMdReminderKnownKey = defineState<Set<string>>(
'agentsMdReminder.known',
() => new Set(),
);
export const agentsMdReminderPendingKey = defineState<Set<string>>(
'agentsMdReminder.pending',
() => new Set(),
);
export const agentsMdReminderCwdKey = defineState<string | undefined>(
'agentsMdReminder.cwd',
() => undefined as string | undefined,
Expand All @@ -65,6 +63,10 @@ export class AgentAgentsMdReminderService
{
declare readonly _serviceBrand: undefined;

private readonly remindQueue = new Set<string>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deliver discoveries through the one-off reminder path

When a session is restored after an access tool result is persisted but before the next step head, this in-memory queue is reconstructed empty, so the applicable AGENTS.md reminder is lost and that access proceeds without its instructions. The package guide explicitly forbids deferred-delivery queues and classifies AGENTS.md discovery as a one-off notify; route the discovery through that safe event path so delivery follows the persisted context lifecycle rather than service-local memory.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L82-L82

Useful? React with 👍 / 👎.

private readonly readRecently = new Set<string>();
private readonly telemetryFired = new Set<string>();

constructor(
@IAgentToolExecutorService toolExecutor: IAgentToolExecutorService,
@IAgentReminderService private readonly reminder: IAgentReminderService,
Expand All @@ -80,11 +82,10 @@ export class AgentAgentsMdReminderService
) {
super();
this.states.contributeState(agentsMdReminderKnownKey);
this.states.contributeState(agentsMdReminderPendingKey);
this.states.contributeState(agentsMdReminderCwdKey);
this.states.contributeState(agentsMdReminderSeededKey);
this._register(
this.reminder.register<readonly string[]>('agents_md', (context) =>
this.reminder.register<readonly string[]>(DISCOVERY_REMINDER_VARIANT, (context) =>
this.injectReminder(context),
),
);
Expand Down Expand Up @@ -113,9 +114,7 @@ export class AgentAgentsMdReminderService
const known = new Set(this.known);
for (const path of paths) known.add(normalize(path));
this.states.set(agentsMdReminderKnownKey, known);
const pending = new Set(this.pending);
for (const path of paths) pending.delete(normalize(path));
this.states.set(agentsMdReminderPendingKey, pending);
for (const path of paths) this.remindQueue.delete(normalize(path));
this.states.set(agentsMdReminderCwdKey, cwd);
this.states.set(agentsMdReminderSeededKey, true);
}
Expand All @@ -135,42 +134,34 @@ export class AgentAgentsMdReminderService
this.markKnown(
list.filter((change) => change.action === 'modified').map((change) => change.path),
);
this.markPending(
list.filter((change) => change.action === 'created').map((change) => change.path),
);
this.markDeleted(
list.filter((change) => change.action === 'deleted').map((change) => change.path),
);
}

private readonly claimed = new Set<string>();

private get known(): Set<string> {
return this.states.get(agentsMdReminderKnownKey);
}

private get pending(): Set<string> {
return this.states.get(agentsMdReminderPendingKey);
}

private get agentCwd(): string {
return this.states.get(agentsMdReminderCwdKey) ?? this.sessionContext.cwd;
}

private hasPath(path: string): boolean {
return this.known.has(path) || this.pending.has(path);
}

private injectReminder(
context: ContextInjectionContext<readonly string[]>,
): ContextInjectionResult<readonly string[]> | undefined {
const readRecently = new Set(this.readRecently);
this.readRecently.clear();
const known = this.known;
const pending = [...this.pending].filter((path) => !known.has(path));
if (pending.length === 0) return undefined;
const queued = [...this.remindQueue].filter(
(path) => !known.has(path) && !readRecently.has(path),
);
this.remindQueue.clear();
if (queued.length === 0) return undefined;
const covered = context.lastDisclosure ?? [];
const fresh = pending.filter((path) => !covered.includes(path));
const fresh = queued.filter((path) => !covered.includes(path));
if (fresh.length === 0) return undefined;
return { content: reminderText(fresh), disclosure: pending };
return { content: reminderText(fresh), disclosure: [...covered, ...fresh] };
}

private async ensureSeeded(): Promise<void> {
Expand All @@ -190,68 +181,55 @@ export class AgentAgentsMdReminderService

private async probeAndRemind(ctx: ToolDidExecuteContext): Promise<void> {
if (ctx.outcome !== 'executed') return;
const discovered: string[] = [];
try {
await this.ensureSeeded();
const { dirs, selfKnown } = this.targetDirs(ctx);
const selfKnownSet = new Set(selfKnown);
const discovered: string[] = [];
for (const dir of dirs) {
for (const path of await this.probeDir(dir)) {
if (this.hasPath(path) || this.claimed.has(path) || selfKnownSet.has(path)) continue;
this.claimed.add(path);
if (this.known.has(path) || this.remindQueue.has(path) || selfKnownSet.has(path)) {
continue;
}
discovered.push(path);
}
}
if (discovered.length === 0) {
this.markKnown(selfKnown);
return;
for (const path of selfKnown) {
this.remindQueue.delete(path);
this.readRecently.add(path);
}
const properties: AgentsMdReminderShownEvent = {
turn_id: ctx.turnId,
tool_name: ctx.toolCall.name,
reminded_count: discovered.length,
trace_id: ctx.trace?.traceId,
};
this.telemetry.track2('agents_md_reminder_shown', properties);
this.markKnown(selfKnown);
this.markPending(discovered);
} catch {} finally {
for (const path of discovered) this.claimed.delete(path);
}
if (discovered.length === 0) return;
const untracked = discovered.filter((path) => !this.telemetryFired.has(path));
if (untracked.length > 0) {
const properties: AgentsMdReminderShownEvent = {
turn_id: ctx.turnId,
tool_name: ctx.toolCall.name,
reminded_count: untracked.length,
trace_id: ctx.trace?.traceId,
};
this.telemetry.track2('agents_md_reminder_shown', properties);
for (const path of untracked) this.telemetryFired.add(path);
}
for (const path of discovered) this.remindQueue.add(path);
} catch {}
}

private markKnown(paths: readonly string[]): void {
if (paths.length === 0) return;
const known = new Set(this.known);
const pending = new Set(this.pending);
for (const path of paths) {
known.add(path);
pending.delete(path);
}
for (const path of paths) known.add(path);
this.states.set(agentsMdReminderKnownKey, known);
this.states.set(agentsMdReminderPendingKey, pending);
}

private markPending(paths: readonly string[]): void {
if (paths.length === 0) return;
const known = this.known;
const pending = new Set(this.pending);
for (const path of paths) {
if (!known.has(path)) pending.add(path);
}
this.states.set(agentsMdReminderPendingKey, pending);
}

private markDeleted(paths: readonly string[]): void {
if (paths.length === 0) return;
const known = new Set(this.known);
const pending = new Set(this.pending);
for (const path of paths) {
known.delete(path);
pending.delete(path);
this.remindQueue.delete(path);
this.telemetryFired.delete(path);
}
this.states.set(agentsMdReminderKnownKey, known);
Comment on lines 226 to 232

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove deleted paths from the reminder queue

When a discovered AGENTS.md is deleted after a tool hook queues it but before the next step head—for example, while a slower sibling tool is still running—markDeleted now removes it only from known. The path remains in remindQueue, so injectReminder subsequently tells the model to read a file that no longer exists; remove the normalized deleted paths from the queue here as well.

Useful? React with 👍 / 👎.

this.states.set(agentsMdReminderPendingKey, pending);
}

private targetDirs(ctx: ToolDidExecuteContext): { dirs: string[]; selfKnown: string[] } {
Expand Down Expand Up @@ -339,7 +317,7 @@ export class AgentAgentsMdReminderService
const found: string[] = [];
for (const chainDir of chain) {
const candidates = agentsMdCandidatePaths(chainDir);
if (candidates.every((candidate) => this.hasPath(normalize(candidate)))) continue;
if (candidates.every((candidate) => this.known.has(normalize(candidate)))) continue;
for (const path of await findAgentsMdInDir(deps, chainDir)) {
found.push(normalize(path));
}
Expand Down
Loading
Loading