build-review round 1: add durable time refresh scheduling
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# @deepseek-ai/dsh-time-context
|
||||
|
||||
Opt-in durable context with the current zoned time and elapsed time at every model step. `dsh-agent-spine-demo` and shipped examples do not mount it. Decision record: [the durable time-context RFC](../../../docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md).
|
||||
Opt-in durable context with the current zoned time and elapsed time sampled during model-request preparation. `dsh-agent-spine-demo` and shipped examples do not mount it. Decision record: [the durable time-context RFC](../../../docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md).
|
||||
|
||||
## Config
|
||||
|
||||
@@ -9,38 +9,45 @@ Opt-in durable context with the current zoned time and elapsed time at every mod
|
||||
name: '@deepseek-ai/dsh-time-context'
|
||||
config:
|
||||
timeZone: Asia/Shanghai # optional IANA override; omit for the process zone
|
||||
refreshIntervalMs: 60000 # optional; omit or set to 0 for every eligible attempt
|
||||
```
|
||||
|
||||
When `timeZone` is omitted, the plugin resolves the Node process's system zone once at plugin load. Node honors `TZ`; without that override, the host or container supplies the zone. An explicit `timeZone` must be an IANA identifier and is validated at plugin load.
|
||||
|
||||
`refreshIntervalMs` must be a non-negative safe integer. Omission or `0` appends on every pre-step attempt whose signal is not already aborted. A positive value appends only when the session has no earlier time-context injection, wall time moved backward, or at least that many milliseconds have elapsed since the latest injection.
|
||||
|
||||
## Timing semantics
|
||||
|
||||
The plugin prepends an `agent/pre-step` listener. Every non-aborted step appends one `context/message` through `agent.inject()` before `step/start` and ordinary automatic compaction, with source `{ kind: 'plugin', plugin: 'time-context' }`.
|
||||
The plugin prepends an `agent/pre-step` listener. When an injection is due, it appends one `context/message` through `agent.inject()` before `step/start` and ordinary automatic compaction, with source `{ kind: 'plugin', plugin: 'time-context' }`. A suppressed attempt appends nothing.
|
||||
|
||||
Step 1 measures from the latest preceding model-visible message, including the prompt that opened the turn. Later steps measure from the preceding time-context event. Both baselines use durable session-event timestamps; backward wall-clock movement clamps elapsed time to zero. A missing first-step baseline reports `unavailable`.
|
||||
Positive-interval scheduling scans the raw durable session events for the latest `context/message` with that source, including a reading shadowed by compaction. The schedule therefore applies across turns and resumed processes without process-local cache state. It reduces append frequency and history growth but never removes an existing reading, and sessions schedule independently.
|
||||
|
||||
The time reading stays in derived conversation history until a later compaction shadows it. Request headers and header deltas contain no time-context state, so the durable message plus the matching `step/start` reconstruct each request's reading.
|
||||
Step 1 measures from the latest preceding model-visible message, including the prompt that opened the turn. Later steps measure from the preceding time-context event in the same turn. Both baselines use durable session-event timestamps; backward wall-clock movement clamps elapsed time to zero. A missing first-step baseline, or a later step with no earlier same-turn reading because interval suppression skipped it, reports `unavailable`.
|
||||
|
||||
A time reading records a request-preparation attempt, not a committed step or transmitted request. Because the listener runs first, its append may remain when a later pre-step listener cancels or fails the attempt; the log is append-only and the plugin performs no rollback.
|
||||
|
||||
The time reading stays in derived conversation history until a later compaction shadows it. Request headers and header deltas contain no time-context state. Request reconstruction uses the complete durable surface prefix at each `step/start`, so transmitted requests need not map one-to-one to readings: a failed preparation can leave an extra reading, while interval suppression can let a request reuse existing history without adding one.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Per-step temporal context
|
||||
### Preparation-time temporal context
|
||||
|
||||
**What the model sees**: Before each step, one source-tagged context message containing the two lines below. `<timestamp>` is an ISO-shaped local timestamp with numeric offset and IANA zone; durations use compact whole-second units.
|
||||
**What the model sees**: On each preparation attempt that injects, one source-tagged context message containing the two lines below. `<timestamp>` is an ISO-shaped local timestamp with numeric offset and IANA zone; durations use compact whole-second units. Positive intervals can leave an attempted step without a new reading.
|
||||
|
||||
**Token effect**: One two-line message accumulates per step until compaction shadows older history.
|
||||
**Token effect**: Each injected two-line message accumulates until compaction shadows it. A positive interval reduces additions; omission or `0` adds one for every eligible preparation attempt.
|
||||
|
||||
#### First step
|
||||
|
||||
```markdown
|
||||
Time recorded before turn <turn>, step 1: <timestamp>
|
||||
Time sampled while preparing turn <turn>, step 1: <timestamp>
|
||||
Elapsed since the preceding model-visible message: <duration-or-unavailable>.
|
||||
```
|
||||
|
||||
#### Later steps
|
||||
|
||||
```markdown
|
||||
Time recorded before turn <turn>, step <step>: <timestamp>
|
||||
Elapsed since the preceding step context: <duration>.
|
||||
Time sampled while preparing turn <turn>, step <step>: <timestamp>
|
||||
Elapsed since the preceding step context: <duration-or-unavailable>.
|
||||
```
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
@@ -48,4 +55,4 @@ Elapsed since the preceding step context: <duration>.
|
||||
- **Whole-second display** — timestamps and durations omit sub-second precision even though durable event times retain milliseconds.
|
||||
- **Session-event baseline** — elapsed time starts from durable append timestamps, not a client transport's original send timestamp.
|
||||
- **Process-local default zone** — omission uses the Node process's `TZ`, host, or container zone captured at plugin load, not a remote user's zone; configure an explicit IANA zone when those differ.
|
||||
- **History cost between compactions** — one reading remains model-visible for every unshadowed step so prior timing claims stay historically truthful.
|
||||
- **History cost between compactions** — omission or `0` retains one reading for every eligible preparation attempt, including attempts later cancelled or failed; a positive interval reduces but does not eliminate this cost.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Opt-in per-step clock context. Every pending model request receives a
|
||||
* durable, source-attributed time reading in conversation history.
|
||||
* Opt-in request-preparation clock context. Eligible pre-step attempts append
|
||||
* durable, source-attributed time readings to conversation history.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-time-context
|
||||
*/
|
||||
@@ -16,15 +16,18 @@ export const name = 'time-context'
|
||||
/** The agent registry that owns the pre-step lifecycle seam. */
|
||||
export const inject = ['agents']
|
||||
|
||||
/** Request-time clock formatting. Invalid values fail plugin load. */
|
||||
/** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */
|
||||
export interface Config {
|
||||
/** IANA time zone used for the rendered timestamp. Omit to resolve the Node process's system zone at plugin load. */
|
||||
timeZone?: string
|
||||
/** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject on every eligible pre-step attempt. */
|
||||
refreshIntervalMs?: number
|
||||
}
|
||||
|
||||
/** Schemastery validation for {@link Config}. */
|
||||
export const Config: z<Config> = z.object({
|
||||
timeZone: z.string(),
|
||||
refreshIntervalMs: z.number(),
|
||||
})
|
||||
|
||||
type TimestampPart = 'day' | 'hour' | 'minute' | 'month' | 'second' | 'timeZoneName' | 'year'
|
||||
@@ -86,6 +89,18 @@ function precedingStepContextTime(agent: Agent, turn: number): number | undefine
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Find this plugin's latest durable injection, including a shadowed surface event. */
|
||||
function latestInjectionTime(agent: Agent): number | undefined {
|
||||
for (const event of [...agent.session.events].reverse()) {
|
||||
if (event.type === 'context/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === name) {
|
||||
return event.time
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function renderText(
|
||||
now: number,
|
||||
turn: number,
|
||||
@@ -96,18 +111,32 @@ function renderText(
|
||||
): string {
|
||||
const elapsed = previous === undefined ? 'unavailable' : formatDuration(now - previous)
|
||||
const baseline = step === 1 ? 'model-visible message' : 'step context'
|
||||
return `Time recorded before turn ${turn}, step ${step}: ${formatTimestamp(now, formatter, timeZone)}\n`
|
||||
return `Time sampled while preparing turn ${turn}, step ${step}: ${formatTimestamp(now, formatter, timeZone)}\n`
|
||||
+ `Elapsed since the preceding ${baseline}: ${elapsed}.`
|
||||
}
|
||||
|
||||
/** Reject refresh intervals that cannot represent an exact elapsed-millisecond threshold. */
|
||||
function validateRefreshInterval(refreshIntervalMs: number | undefined): void {
|
||||
if (refreshIntervalMs !== undefined && (
|
||||
!Number.isSafeInteger(refreshIntervalMs)
|
||||
|| refreshIntervalMs < 0
|
||||
)) {
|
||||
throw new TypeError(
|
||||
`time-context: refreshIntervalMs must be a non-negative safe integer, got ${String(refreshIntervalMs)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a prepended pre-step listener for the lifetime of `ctx`.
|
||||
* @param ctx - plugin context; the listener is disposed with it.
|
||||
* @param config - validated time zone configuration.
|
||||
* @throws when the configured or process time zone cannot be resolved.
|
||||
* @param config - time zone and durable refresh scheduling configuration.
|
||||
* @throws when the refresh interval is invalid or the configured or process time zone cannot be resolved.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const timeZone = config.timeZone
|
||||
const refreshIntervalMs = config.refreshIntervalMs
|
||||
validateRefreshInterval(refreshIntervalMs)
|
||||
let formatter: Intl.DateTimeFormat
|
||||
try {
|
||||
formatter = new Intl.DateTimeFormat('en-US', {
|
||||
@@ -139,6 +168,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
) => {
|
||||
if (signal.aborted) return
|
||||
const now = Date.now()
|
||||
if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) {
|
||||
const lastInjection = latestInjectionTime(agent)
|
||||
if (lastInjection !== undefined
|
||||
&& now >= lastInjection
|
||||
&& now - lastInjection < refreshIntervalMs) return
|
||||
}
|
||||
const previous = step === 1
|
||||
? precedingMessageTime(agent)
|
||||
: precedingStepContextTime(agent, turn)
|
||||
|
||||
@@ -12,8 +12,8 @@ const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.m
|
||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||
const PROCESS_TIMEOUT_MS = 30_000
|
||||
const TEST_TIMEOUT_MS = PROCESS_TIMEOUT_MS + 15_000
|
||||
const FIRST_REPLY = '[main turn 1] You said: "Time recorded before turn 1, step 1:'
|
||||
const SECOND_REPLY = '[main turn 2] You said: "Time recorded before turn 2, step 1:'
|
||||
const FIRST_REPLY = '[main turn 1] You said: "Time sampled while preparing turn 1, step 1:'
|
||||
const SECOND_REPLY = '[main turn 2] You said: "Time sampled while preparing turn 2, step 1:'
|
||||
|
||||
let child: ChildProcessWithoutNullStreams | undefined
|
||||
let workdir: string | undefined
|
||||
@@ -112,15 +112,15 @@ describe('time-context through a real cordis.yml and stdio process', () => {
|
||||
.map(block => block.text)
|
||||
.join('\n'))
|
||||
expect(contextText[0]).toMatch(
|
||||
/Time recorded before turn 1, step 1: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00\[Asia\/Shanghai\]/,
|
||||
/Time sampled while preparing turn 1, step 1: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00\[Asia\/Shanghai\]/,
|
||||
)
|
||||
expect(contextText[0]).toMatch(
|
||||
/Elapsed since the preceding model-visible message: (?:\d+d )?(?:\d+h )?(?:\d+m )?\d+s\./,
|
||||
)
|
||||
expect(contextText[1]).toMatch(/Time recorded before turn 2, step 1:/)
|
||||
expect(contextText[1]).toMatch(/Time sampled while preparing turn 2, step 1:/)
|
||||
|
||||
const headers = events.filter(event => event.type === 'request/header'
|
||||
|| event.type === 'request/header-delta')
|
||||
expect(JSON.stringify(headers)).not.toContain('Time recorded before')
|
||||
expect(JSON.stringify(headers)).not.toContain('Time sampled while preparing')
|
||||
}, TEST_TIMEOUT_MS)
|
||||
})
|
||||
|
||||
@@ -65,9 +65,15 @@ function openMessageTurn(session: Session, turn: number): void {
|
||||
}
|
||||
|
||||
function contextTexts(session: Session): string[] {
|
||||
return session.events
|
||||
.filter(event => event.type === 'context/message')
|
||||
.map(event => event.data.content.find(block => block.type === 'text')?.text ?? '')
|
||||
const texts: string[] = []
|
||||
for (const event of session.events) {
|
||||
if (event.type === 'context/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === 'time-context') {
|
||||
texts.push(event.data.content.find(block => block.type === 'text')?.text ?? '')
|
||||
}
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
async function fire(
|
||||
@@ -146,7 +152,7 @@ describe('durable step context', () => {
|
||||
await fire(ctx, sessionAgent(session), 1, 1)
|
||||
|
||||
expect(contextTexts(session)).toEqual([
|
||||
'Time recorded before turn 1, step 1: 2026-07-15T09:01:01+08:00[Asia/Shanghai]\n'
|
||||
'Time sampled while preparing turn 1, step 1: 2026-07-15T09:01:01+08:00[Asia/Shanghai]\n'
|
||||
+ 'Elapsed since the preceding model-visible message: 1d 1h 1m 1s.',
|
||||
])
|
||||
const event = session.events.at(-1)
|
||||
@@ -168,8 +174,11 @@ describe('durable step context', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the preceding durable step-context timestamp after step one', async () => {
|
||||
const { ctx } = await mount()
|
||||
it.each([
|
||||
['omitted interval', {}],
|
||||
['zero interval', { refreshIntervalMs: 0 }],
|
||||
] as const)('uses the preceding durable step-context timestamp after step one with %s', async (_label, config) => {
|
||||
const { ctx } = await mount(config)
|
||||
const session = new Session(SessionId('later-step'))
|
||||
const agent = sessionAgent(session)
|
||||
openMessageTurn(session, 3)
|
||||
@@ -179,7 +188,7 @@ describe('durable step context', () => {
|
||||
await fire(ctx, agent, 3, 2)
|
||||
|
||||
expect(contextTexts(session)[1]).toBe(
|
||||
'Time recorded before turn 3, step 2: 2026-07-14T00:01:01+00:00[UTC]\n'
|
||||
'Time sampled while preparing turn 3, step 2: 2026-07-14T00:01:01+00:00[UTC]\n'
|
||||
+ 'Elapsed since the preceding step context: 1m 1s.',
|
||||
)
|
||||
})
|
||||
@@ -207,8 +216,8 @@ describe('durable step context', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('clamps backward wall-clock movement against the preceding context to zero', async () => {
|
||||
const { ctx } = await mount()
|
||||
it('injects after backward wall-clock movement and clamps elapsed time to zero', async () => {
|
||||
const { ctx } = await mount({ refreshIntervalMs: 60_000 })
|
||||
const session = new Session(SessionId('backward'))
|
||||
const agent = sessionAgent(session)
|
||||
openMessageTurn(session, 1)
|
||||
@@ -217,9 +226,70 @@ describe('durable step context', () => {
|
||||
|
||||
await fire(ctx, agent, 1, 2)
|
||||
|
||||
expect(contextTexts(session)).toHaveLength(2)
|
||||
expect(contextTexts(session)[1]).toContain('Elapsed since the preceding step context: 0s.')
|
||||
})
|
||||
|
||||
it('uses a shadowed durable injection after resume and injects at the exact threshold', async () => {
|
||||
const { ctx } = await mount({ refreshIntervalMs: 1_000 })
|
||||
const original = new Session(SessionId('seed-source'))
|
||||
openMessageTurn(original, 1)
|
||||
await fire(ctx, sessionAgent(original), 1, 1)
|
||||
const user = original.events.find(event => event.type === 'user/message')
|
||||
const reading = original.events.find(event => event.type === 'context/message')
|
||||
if (user === undefined || reading === undefined) throw new Error('missing source surface events')
|
||||
original.append('context/message', {
|
||||
content: [{ type: 'text', text: 'compacted history' }],
|
||||
source: { kind: 'plugin', plugin: 'compact-basic' },
|
||||
}, {
|
||||
surfaceOp: { op: 'replace', start: user.seq, end: reading.seq },
|
||||
sourceEventSeqs: [user.seq, reading.seq],
|
||||
})
|
||||
original.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
expect(JSON.stringify(original.deriveMessages())).not.toContain('Time sampled while preparing')
|
||||
|
||||
const resumed = new Session(SessionId('resumed'), [...original.events])
|
||||
const resumedAgent = sessionAgent(resumed)
|
||||
vi.setSystemTime(BASE + 999)
|
||||
openMessageTurn(resumed, 2)
|
||||
const beforeSkip = resumed.events.length
|
||||
|
||||
await fire(ctx, resumedAgent, 2, 1)
|
||||
|
||||
expect(resumed.events).toHaveLength(beforeSkip)
|
||||
expect(contextTexts(resumed)).toHaveLength(1)
|
||||
|
||||
vi.setSystemTime(BASE + 1_000)
|
||||
await fire(ctx, resumedAgent, 2, 2)
|
||||
|
||||
expect(contextTexts(resumed)).toHaveLength(2)
|
||||
expect(contextTexts(resumed)[1]).toContain(
|
||||
'Elapsed since the preceding step context: unavailable.',
|
||||
)
|
||||
})
|
||||
|
||||
it('applies a positive interval across turns without sharing state between sessions', async () => {
|
||||
const { ctx } = await mount({ refreshIntervalMs: 1_000 })
|
||||
const first = new Session(SessionId('interval-first'))
|
||||
const firstAgent = sessionAgent(first, 'first-agent')
|
||||
openMessageTurn(first, 1)
|
||||
await fire(ctx, firstAgent, 1, 1)
|
||||
first.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
|
||||
vi.setSystemTime(BASE + 500)
|
||||
openMessageTurn(first, 2)
|
||||
const beforeSkip = first.events.length
|
||||
await fire(ctx, firstAgent, 2, 1)
|
||||
|
||||
const independent = new Session(SessionId('interval-independent'))
|
||||
openMessageTurn(independent, 1)
|
||||
await fire(ctx, sessionAgent(independent, 'independent-agent'), 1, 1)
|
||||
|
||||
expect(first.events).toHaveLength(beforeSkip)
|
||||
expect(contextTexts(first)).toHaveLength(1)
|
||||
expect(contextTexts(independent)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('runs before ordinary pre-step listeners and skips an already-aborted step', async () => {
|
||||
const { ctx } = await mount()
|
||||
const session = new Session(SessionId('ordering'))
|
||||
@@ -268,6 +338,15 @@ describe('configuration and lifecycle', () => {
|
||||
await expect(unresolved.plugin(timeContext, {})).rejects.toThrow(/failed to resolve the system time zone/)
|
||||
})
|
||||
|
||||
it('rejects invalid refresh intervals at plugin load with one diagnostic', async () => {
|
||||
const invalid = [-1, 0.5, Number.MAX_SAFE_INTEGER + 1, Number.POSITIVE_INFINITY, Number.NaN]
|
||||
for (const refreshIntervalMs of invalid) {
|
||||
await expect(mount({ refreshIntervalMs })).rejects.toThrow(
|
||||
'time-context: refreshIntervalMs must be a non-negative safe integer',
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('removes its listener when the plugin fiber disposes', async () => {
|
||||
const { ctx, fiber } = await mount()
|
||||
const session = new Session(SessionId('dispose'))
|
||||
@@ -283,6 +362,32 @@ describe('configuration and lifecycle', () => {
|
||||
})
|
||||
|
||||
describe('real agent-loop request history', () => {
|
||||
it.each([
|
||||
['throws', 'error'],
|
||||
['cancels', 'aborted'],
|
||||
] as const)('retains the preparation reading when a later pre-step listener %s', async (mode, reasonKind) => {
|
||||
const adapter = new ScriptedAdapter([textResponse('unused')])
|
||||
const ctx = await loopHarness(adapter)
|
||||
let laterSawReading = false
|
||||
ctx.on('agent/pre-step', (subject) => {
|
||||
laterSawReading = contextTexts(subject.session).length === 1
|
||||
if (mode === 'throws') throw new Error('later pre-step failure')
|
||||
subject.cancel('later pre-step cancellation')
|
||||
})
|
||||
const agent = ctx.agentLoop.create(AgentId(`late-${mode}`), { model: 'mock' })
|
||||
|
||||
agent.send([{ type: 'text', text: 'start' }])
|
||||
await agent.whenIdle()
|
||||
|
||||
expect(laterSawReading).toBe(true)
|
||||
expect(contextTexts(agent.session)).toHaveLength(1)
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(agent.session.events.some(event => event.type === 'step/start')).toBe(false)
|
||||
const turnEnd = agent.session.events.findLast(event => event.type === 'turn/end')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind).toBe(reasonKind)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('persists one ordered context per request, accumulates readings, and leaves system headers unchanged', async () => {
|
||||
const adapter = new ScriptedAdapter([toolCallResponse(), textResponse('done')])
|
||||
const ctx = await loopHarness(adapter)
|
||||
@@ -314,17 +419,17 @@ describe('real agent-loop request history', () => {
|
||||
|
||||
const firstRequestText = requestText(adapter.requests[0]!)
|
||||
const secondRequestText = requestText(adapter.requests[1]!)
|
||||
expect(firstRequestText).toContain('Time recorded before turn 1, step 1:')
|
||||
expect(firstRequestText).toContain('Time sampled while preparing turn 1, step 1:')
|
||||
expect(firstRequestText).toContain('Elapsed since the preceding model-visible message: 0s.')
|
||||
expect(firstRequestText).not.toContain('Time recorded before turn 1, step 2:')
|
||||
expect(secondRequestText).toContain('Time recorded before turn 1, step 1:')
|
||||
expect(secondRequestText).toContain('Time recorded before turn 1, step 2:')
|
||||
expect(firstRequestText).not.toContain('Time sampled while preparing turn 1, step 2:')
|
||||
expect(secondRequestText).toContain('Time sampled while preparing turn 1, step 1:')
|
||||
expect(secondRequestText).toContain('Time sampled while preparing turn 1, step 2:')
|
||||
expect(secondRequestText).toContain('Elapsed since the preceding step context: 1m 1s.')
|
||||
|
||||
for (const request of adapter.requests) expect(request.system).not.toContain('Time recorded before')
|
||||
for (const request of adapter.requests) expect(request.system).not.toContain('Time sampled while preparing')
|
||||
const headers = agent.session.events.filter(event => event.type === 'request/header'
|
||||
|| event.type === 'request/header-delta')
|
||||
expect(JSON.stringify(headers)).not.toContain('Time recorded before')
|
||||
expect(JSON.stringify(headers)).not.toContain('Time sampled while preparing')
|
||||
expect(agent.session.events.filter(event => event.type === 'request/header-delta')).toHaveLength(0)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
@@ -348,6 +453,6 @@ describe('real Loader export path', () => {
|
||||
const session = new Session(SessionId('loader'))
|
||||
openMessageTurn(session, 1)
|
||||
await fire(ctx, sessionAgent(session), 1, 1)
|
||||
expect(contextTexts(session)[0]).toContain('Time recorded before turn 1, step 1:')
|
||||
expect(contextTexts(session)[0]).toContain('Time sampled while preparing turn 1, step 1:')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user