@meistrari/agent-core 0.1.0 → 0.1.1

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.
package/README.md CHANGED
@@ -45,3 +45,15 @@ bunx agent-core-supervisor \
45
45
  Use `--loopback-tcp` for local and image-build probes. Production images default to the protected Unix socket. The extension module exports `default`, `extensions`, or `createSupervisorExtensions()` matching `SupervisorExtensions`.
46
46
 
47
47
  Publishing runs from `main` through `.github/workflows/publish.yml` (conventional commits decide the bump).
48
+
49
+ ## Durable command outcomes
50
+
51
+ A supervisor `received` ACK advances the command delivery cursor only. Implementations of
52
+ `DurableEventSink.record` must atomically store each durable event, project
53
+ `supervisor.command.applied` or `supervisor.command.failed` into command state, and advance the
54
+ ACK floor. Duplicate events must not repeat projections. This keeps outcomes consistent when
55
+ the worker disconnects after persistence but before sending its event ACK.
56
+
57
+ The former `ConnectionAuthorityStore.projectAppliedCommand` callback has been removed.
58
+ Move that projection into the durable-event transaction; receiving a command does not prove
59
+ that the provider executed it. See `InMemoryRuntimeControlPlane.record` for the test fixture.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/agent-core",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "packageManager": "bun@1.3.12",
6
6
  "description": "Shared contracts and runtime modules for Tela coding-agent sandboxes: agent protocol, supervisor wire protocol, resident supervisor, worker runtime client, and Claude/Codex harness adapters.",
7
7
  "license": "UNLICENSED",
@@ -89,9 +89,9 @@
89
89
  }
90
90
  },
91
91
  "dependencies": {
92
- "jose": "^6.1.3",
92
+ "jose": "6.2.11",
93
93
  "pino": "^9.7.0",
94
- "ulid": "^3.0.1",
94
+ "ulid": "3.0.2",
95
95
  "zod": "^4.1.12"
96
96
  },
97
97
  "devDependencies": {
@@ -100,9 +100,9 @@
100
100
  "@anthropic-ai/sdk": "0.93.0",
101
101
  "@modelcontextprotocol/sdk": "1.29.0",
102
102
  "@types/bun": "1.3.12",
103
- "@types/pg": "^8.15.5",
103
+ "@types/pg": "8.23.1",
104
104
  "eslint": "^9.30.0",
105
- "pg": "^8.16.3",
105
+ "pg": "8.23.0",
106
106
  "typescript": "5.9.3"
107
107
  }
108
108
  }
@@ -191,8 +191,20 @@ export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperSt
191
191
  // Claude Agent SDK result usage is per completed ask/turn for the stream-json path we use,
192
192
  // not a monotonic counter across the long-lived query. Emit it directly so summing
193
193
  // normalized usage events gives the run total; do not diff against prior results.
194
- if (usage)
195
- drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage } })
194
+ if (usage) {
195
+ drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage, accounting: {
196
+ observationId: message.uuid,
197
+ mode: 'increment',
198
+ scopeId: turnId,
199
+ includesChildren: true,
200
+ models: Object.fromEntries(Object.entries(message.modelUsage ?? {}).map(([model, counts]) => [model, {
201
+ inputTokens: counts.inputTokens,
202
+ outputTokens: counts.outputTokens,
203
+ cacheReadTokens: counts.cacheReadInputTokens,
204
+ cacheWriteTokens: counts.cacheCreationInputTokens,
205
+ }])),
206
+ } } })
207
+ }
196
208
  endOpenReasoning(drafts, state, turnId)
197
209
  const interruptAccepted = state.interrupt.status === 'accepted' && state.interrupt.turnId === turnId
198
210
  drafts.push({ type: 'turn.ended', turnId, actor: mainActor, payload: turnEndedPayload(message, interruptAccepted) })
@@ -285,8 +285,8 @@ export class ClaudeRun implements AgentRun {
285
285
  // session identity mismatch are provider protocol drift and fail the run closed. `cancelled`
286
286
  // (a steered-over/aborted command) and `discarded` (the session ended with the command still
287
287
  // queued) are valid observed states but NEGATIVE terminals — never a positive acceptance, so
288
- // they are validated and no-op even when a waiter is pending; the pending prompt's real outcome
289
- // is settled by run close/error as delivery-unknown. Valid positive frames without a pending
288
+ // they reject any pending waiter as delivery-unknown so command processing can advance.
289
+ // Valid positive frames without a pending
290
290
  // waiter are internal lifecycle for already-settled or non-prompt inputs and are ignored.
291
291
  private settleAcceptance(frame: ClaudeCommandLifecycleMessage): void {
292
292
  const parsed = commandLifecycleFrameSchema.safeParse(frame)
@@ -302,8 +302,12 @@ export class ClaudeRun implements AgentRun {
302
302
  details: { expected: this.metadata.providerSessionId, actual: parsed.data.session_id },
303
303
  })
304
304
  }
305
- if (parsed.data.state === 'cancelled' || parsed.data.state === 'discarded')
305
+ if (parsed.data.state === 'cancelled' || parsed.data.state === 'discarded') {
306
+ this.acceptanceWaiters.get(parsed.data.command_uuid)?.reject(new AgentDeliveryUnknownError({
307
+ message: `Claude command ended as ${parsed.data.state} before acceptance was observed.`,
308
+ }))
306
309
  return
310
+ }
307
311
  this.acceptanceWaiters.get(parsed.data.command_uuid)?.resolve()
308
312
  }
309
313
 
@@ -3,6 +3,7 @@ import type { AgentWorkStatus } from '../../protocol/agent-work'
3
3
  import type { AgentEventDraft } from '../agent-event-stream'
4
4
  import type { CodexJsonRpcMessage, CodexServerNotification } from './codex-json-rpc-client'
5
5
  import type { ServerNotificationParamsByMethod } from './codex-protocol'
6
+ import { createHash } from 'node:crypto'
6
7
  import { jsonObjectSchema, jsonValueSchema } from '../../protocol'
7
8
  import { CANONICAL_TOOL } from '../../protocol/agent-tool-name'
8
9
  import { createRuntimeWorkItemId } from '../agent-id'
@@ -96,7 +97,19 @@ export function mapCodexNotification(notification: CodexNotification, state: Cod
96
97
  const usage = usageFromTokenUsage(notification.params.tokenUsage)
97
98
  if (!usage)
98
99
  return []
99
- return emitOrBufferByThread({ draft: { type: 'usage', turnId: notification.params.turnId, payload: { usage } }, threadId: notification.params.threadId, state })
100
+ return emitOrBufferByThread({ draft: { type: 'usage', turnId: notification.params.turnId, payload: {
101
+ usage,
102
+ accounting: {
103
+ observationId: createHash('sha256').update(notification.params.threadId).update(JSON.stringify(notification.params.tokenUsage.total)).digest('hex'),
104
+ mode: 'cumulative',
105
+ scopeId: notification.params.threadId,
106
+ includesChildren: false,
107
+ usage: {
108
+ ...usageFromCounts(notification.params.tokenUsage.total),
109
+ inputTokens: Math.max(0, notification.params.tokenUsage.total.inputTokens - notification.params.tokenUsage.total.cachedInputTokens),
110
+ },
111
+ },
112
+ } }, threadId: notification.params.threadId, state })
100
113
  }
101
114
  case 'turn/plan/updated':
102
115
  if (!state.startedTurnIds.has(notification.params.turnId) || notification.params.threadId !== state.mainThreadId)
@@ -4,7 +4,7 @@ import { agentRunMetadataSchema } from './agent-metadata'
4
4
  import { agentPayloadOverflowSchema } from './agent-overflow'
5
5
  import { agentProviderIdSchema } from './agent-provider'
6
6
  import { agentToolCallStatusSchema, agentToolResultSchema } from './agent-tool'
7
- import { agentUsageSchema } from './agent-usage'
7
+ import { agentUsageAccountingSchema, agentUsageSchema } from './agent-usage'
8
8
  import { agentUserInputAnswersSchema, agentUserInputQuestionSchema } from './agent-user-input'
9
9
  import { agentWorkObservationSchema } from './agent-work'
10
10
 
@@ -265,6 +265,7 @@ export const usageAgentEventSchema = agentTurnEventBaseSchema.extend({
265
265
  type: z.literal('usage'),
266
266
  payload: z.object({
267
267
  usage: agentUsageSchema,
268
+ accounting: agentUsageAccountingSchema.optional(),
268
269
  }).strict(),
269
270
  }).strict()
270
271
 
@@ -12,3 +12,17 @@ export const agentUsageSchema = z.object({
12
12
  }).strict()
13
13
 
14
14
  export type AgentUsage = z.infer<typeof agentUsageSchema>
15
+
16
+ /**
17
+ * Accounting semantics are separate from display counters. Consumers persist
18
+ * observations/checkpoints before acknowledging events, including across runs.
19
+ */
20
+ export const agentUsageAccountingSchema = z.object({
21
+ observationId: z.string().min(1),
22
+ mode: z.enum(['increment', 'cumulative']),
23
+ scopeId: z.string().min(1),
24
+ includesChildren: z.boolean(),
25
+ usage: agentUsageSchema.optional(),
26
+ model: z.string().min(1).optional(),
27
+ models: z.record(z.string(), agentUsageSchema).optional(),
28
+ }).strict()
@@ -1,6 +1,6 @@
1
1
  // Generated by scripts/write-provenance.ts. Do not edit by hand.
2
2
  export const generatedProvenance = {
3
- version: "0.1.0",
4
- sha: "9cd95365c6d39d1f44098fdd8006087c3adabfb4",
5
- buildTime: "2026-09-04T20:01:49.852Z",
3
+ version: "0.1.1",
4
+ sha: "00ac4425fb84e86e0adabba1067485b94f45c100",
5
+ buildTime: "2026-09-08T17:04:18.818Z",
6
6
  } as const
@@ -34,7 +34,12 @@ export function createBootstrapBinder(input: {
34
34
  shutdown: (code: number) => void
35
35
  logger: Logger
36
36
  }): BootstrapBinder {
37
+ let pending = Promise.resolve()
37
38
  return async (request) => {
39
+ const previous = pending
40
+ const released = Promise.withResolvers<void>()
41
+ pending = released.promise
42
+ await previous
38
43
  try {
39
44
  return await bind(request)
40
45
  }
@@ -55,6 +60,9 @@ export function createBootstrapBinder(input: {
55
60
  terminate: () => input.shutdown(TERMINAL_ERROR_CODE),
56
61
  }
57
62
  }
63
+ finally {
64
+ released.resolve()
65
+ }
58
66
  }
59
67
 
60
68
  async function bind(request: {
@@ -381,6 +381,7 @@ export class ResidentSupervisorWebSocketServer {
381
381
  return
382
382
  input.socket.data.terminalRejection = undefined
383
383
  input.socket.close(internalErrorCloseCode, 'Supervisor rejection receipt was not observed.')
384
+ input.terminate()
384
385
  }, terminalRejectionReceiptBudgetMs)
385
386
  timeout.unref?.()
386
387
  input.socket.data.terminalRejection = { terminate: input.terminate, timeout }
@@ -26,6 +26,7 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
26
26
  readonly ephemeralEventLog: Array<Parameters<EphemeralEventSink['publish']>[0]> = []
27
27
  readonly readySnapshots: SupervisorAgentRunSnapshot[] = []
28
28
  readonly projectedCommandSequences: number[] = []
29
+ readonly failedCommandSequences: number[] = []
29
30
  readonly failureLog: Array<{ kind: string, code?: string, detail?: string }> = []
30
31
  readonly rpcCalls: SupervisorRpcRequestEnvelope[] = []
31
32
  onDurableRecord: ((event: DurableEventEnvelope) => void | Promise<void>) | undefined
@@ -105,7 +106,7 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
105
106
  return {
106
107
  renewed,
107
108
  ...(renewed ? { leaseExpiresAt: new Date(Date.now() + 30_000) } : {}),
108
- hasOutstandingCommand: this.projectedCommandSequences.length < this.commandLog.length - 1,
109
+ hasOutstandingCommand: this.projectedCommandSequences.length + this.failedCommandSequences.length < this.commandLog.length,
109
110
  }
110
111
  }
111
112
 
@@ -131,10 +132,6 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
131
132
  return this.claimed && input.generation === this.generation
132
133
  }
133
134
 
134
- async projectAppliedCommand(input: { commandSeq: number }): Promise<void> {
135
- this.projectedCommandSequences.push(input.commandSeq)
136
- }
137
-
138
135
  async initial(): Promise<CommandEnvelope | undefined> {
139
136
  return this.commandLog[0]
140
137
  }
@@ -157,6 +154,10 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
157
154
  return { status: 'duplicate', highWaterMark: this.ackFloor }
158
155
  if (event.seq !== this.ackFloor + 1)
159
156
  return { status: 'gap' }
157
+ if (event.body.type === 'supervisor.command.applied')
158
+ this.projectedCommandSequences.push(event.body.payload.commandSeq)
159
+ if (event.body.type === 'supervisor.command.failed')
160
+ this.failedCommandSequences.push(event.body.payload.commandSeq)
160
161
  this.durableEventLog.push(event)
161
162
  this.ackFloor = event.seq
162
163
  await this.onDurableRecord?.(event)
@@ -98,11 +98,6 @@ export class SessionCommandPump {
98
98
  return
99
99
  }
100
100
  this.afterSequence = ack.commandSeq
101
- await this.dependencies.authorityStore.projectAppliedCommand({
102
- sessionSandboxId: this.dependencies.sessionSandboxId,
103
- runtimeConnectionId: this.dependencies.runtimeConnectionId,
104
- commandSeq: ack.commandSeq,
105
- })
106
101
  }
107
102
  }
108
103
  }
@@ -31,6 +31,7 @@ export class SessionSandboxConnectionAttempt {
31
31
 
32
32
  constructor(private readonly dependencies: {
33
33
  claim: ConnectionClaim
34
+ establishmentDeadline: number
34
35
  replicaId: string
35
36
  trafficTokenEncryptionKey: string
36
37
  signControlAuthority: ControlAuthoritySigner
@@ -96,7 +97,7 @@ export class SessionSandboxConnectionAttempt {
96
97
  signal: this.controller.signal,
97
98
  onMessage: frame => this.processor ? this.processor.receive(frame) : queuedFrames.push(frame),
98
99
  onClose: close => closed.resolve(close),
99
- }), performance.now() + this.dependencies.timings.dialTimeoutMs, this.controller.signal, () => new ConnectionClosedError({
100
+ }), Math.min(this.dependencies.establishmentDeadline, performance.now() + this.dependencies.timings.dialTimeoutMs), this.controller.signal, () => new ConnectionClosedError({
100
101
  ready: false,
101
102
  detail: 'Supervisor connection dial timed out.',
102
103
  }))
@@ -160,7 +161,15 @@ export class SessionSandboxConnectionAttempt {
160
161
  ...secrets,
161
162
  }))
162
163
 
163
- const readiness = await processor.waitForReadiness({ signal: this.controller.signal }).catch(async (error: unknown) => {
164
+ const readiness = await beforeDeadline(Promise.race([
165
+ processor.waitForReadiness({ signal: this.controller.signal }),
166
+ closed.promise.then((close) => {
167
+ throw new ConnectionClosedError({ ready: false, detail: `${close.code}:${close.reason}` })
168
+ }),
169
+ ]), Math.min(this.dependencies.establishmentDeadline, performance.now() + 10_000), this.controller.signal, () => new ConnectionClosedError({
170
+ ready: false,
171
+ detail: 'Supervisor bootstrap timed out.',
172
+ })).catch(async (error: unknown) => {
164
173
  if (error instanceof BootstrapRejectedAckError) {
165
174
  await processor.acknowledgeBootstrapRejection(error.ack)
166
175
  throw new DeterministicEstablishmentError(error.ack.errorCode, error.ack.detail, true)
@@ -44,7 +44,6 @@ export interface ConnectionAuthorityStore {
44
44
  markConnectionReconnecting: (input: { sessionSandboxId: string, generation: number }) => Promise<void>
45
45
  releaseConnection: (input: { sessionSandboxId: string, generation: number, reason: string }) => Promise<void>
46
46
  markConnectionRetryExhausted: (input: { sessionSandboxId: string, generation: number, replicaId: string }) => Promise<boolean>
47
- projectAppliedCommand: (input: { sessionSandboxId: string, runtimeConnectionId: string, commandSeq: number }) => Promise<void>
48
47
  }
49
48
 
50
49
  export interface CommandSource {
@@ -58,6 +57,9 @@ export type DurableEventResult
58
57
 
59
58
  export interface DurableEventSink {
60
59
  getAckFloor: (input: { sessionSandboxId: string }) => Promise<number>
60
+ // Atomically persist the event, advance the ACK floor, and project its command outcome.
61
+ // Only supervisor.command.applied marks a command applied; received ACKs never do.
62
+ // Replayed events must not duplicate projections, including after reconnect.
61
63
  record: (input: SessionSandboxRef & { runtimeConnectionId: string, event: DurableEventEnvelope }) => Promise<DurableEventResult>
62
64
  }
63
65
 
@@ -154,6 +154,7 @@ class OwnedConnection {
154
154
  while (!this.controller.signal.aborted) {
155
155
  const attempt = new SessionSandboxConnectionAttempt({
156
156
  claim,
157
+ establishmentDeadline,
157
158
  replicaId: this.dependencies.replicaId,
158
159
  trafficTokenEncryptionKey: this.dependencies.trafficTokenEncryptionKey,
159
160
  signControlAuthority: this.dependencies.signControlAuthority,