@meistrari/agent-core 0.3.0 → 0.4.0-experimental.0

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
@@ -160,3 +160,9 @@ using it. Commit generated files with generator changes; CI checks reproducibili
160
160
  `agent.change-settings` carries a full `settings: { model, reasoningEffort }` pair through the supervisor wire and the agent protocol. Claude and Codex apply it only at an idle turn boundary, without submitting input, and emit `session.settings.changed` with the command ID and `appliesFrom: next_turn`. Product catalog, provider compatibility and admission ordering remain the caller's responsibility.
161
161
 
162
162
  The supervisor advertises `capabilities["model-change"] = "v1"` in health. It commits the settings command outcome and inbox status in one SQLite transaction. After a process restart, an interrupted settings command can be reapplied idempotently; prompt commands retain their uncertain-delivery policy and are never retried by this path. Confirmed settings override the immutable bootstrap defaults when resuming the provider. Prompt and user-input-answer commands may carry resolved `settings` so their execution does not rely solely on mutable defaults.
163
+
164
+ ## Reconfiguration before the first command
165
+
166
+ A bound supervisor accepts a `session.bootstrap` whose body differs from its binding only while its journal has received no command and the bootstrap carries none. The new body replaces the persisted binding after the same admission as the first bootstrap, the agent runtime started for the previous configuration is closed, and the next one opens a fresh provider session (`fresh` on the provider factory) instead of resuming the bound one, so it reads the agent's files again. The supervisor store keeps that reset until a provider session starts from the new body, so a restart or a reconnect in between does not resume the previous one. Once a command arrived, a different body is rejected with `agent-core.bootstrap-configuration-conflict`, as before.
167
+
168
+ The supervisor advertises `capabilities["reconfigure-before-commands"] = "v1"` in health. A control plane uses it only on sandboxes that report it: an older sandbox keeps rejecting any other configuration after its first bootstrap.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/agent-core",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.4.0-experimental.0",
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",
@@ -57,6 +57,9 @@ export interface CodexEventMapperState {
57
57
  readonly endedSubagentIds: Set<string>
58
58
  readonly subagentByThreadId: Map<string, CodexSubagentRef>
59
59
  readonly subagentById: Map<string, CodexSubagentRef>
60
+ readonly ownerTurnIdBySubagentId: Map<string, string>
61
+ readonly draftsAwaitingOwnerByActorId: Map<string, AgentEventDraft[]>
62
+ readonly unreportedEndedSubagentsByParent: Map<string, Map<string, string>>
60
63
  readonly subagentNameByThreadId: Map<string, string>
61
64
  readonly pendingDraftsByThreadId: Map<string, CodexUnownedTurnEventDraft[]>
62
65
  readonly turnErrorsById: Map<string, AgentError>
@@ -80,6 +83,9 @@ export function createCodexEventMapperState(): CodexEventMapperState {
80
83
  endedSubagentIds: new Set(),
81
84
  subagentByThreadId: new Map(),
82
85
  subagentById: new Map(),
86
+ ownerTurnIdBySubagentId: new Map(),
87
+ draftsAwaitingOwnerByActorId: new Map(),
88
+ unreportedEndedSubagentsByParent: new Map(),
83
89
  subagentNameByThreadId: new Map(),
84
90
  pendingDraftsByThreadId: new Map(),
85
91
  turnErrorsById: new Map(),
@@ -106,6 +112,52 @@ export function cacheCodexThreadMetadata(thread: CodexThreadMetadata, state: Cod
106
112
  }
107
113
 
108
114
  export function mapCodexNotification(notification: CodexNotification, state: CodexEventMapperState): AgentEventDraft[] {
115
+ return attributeSubagentDraftsToOwnerTurn(mapNotificationDrafts(notification, state), state)
116
+ }
117
+
118
+ function attributeSubagentDraftsToOwnerTurn(drafts: AgentEventDraft[], state: CodexEventMapperState): AgentEventDraft[] {
119
+ return drafts.flatMap(draft => attributeSubagentDraft(draft, state))
120
+ }
121
+
122
+ function attributeSubagentDraft(draft: AgentEventDraft, state: CodexEventMapperState): AgentEventDraft[] {
123
+ if (!('actor' in draft) || draft.actor.type !== 'subagent')
124
+ return [draft]
125
+ const { subagentId, parentActorId } = draft.actor
126
+ if (draft.type === 'turn.started' || draft.type === 'turn.ended')
127
+ return []
128
+ let ownerTurnId = state.ownerTurnIdBySubagentId.get(subagentId)
129
+ if (!ownerTurnId && draft.type === 'subagent.started') {
130
+ ownerTurnId = parentActorId === mainActor.actorId ? draft.turnId : state.ownerTurnIdBySubagentId.get(parentActorId)
131
+ if (ownerTurnId)
132
+ state.ownerTurnIdBySubagentId.set(subagentId, ownerTurnId)
133
+ else
134
+ return awaitOwner(parentActorId, draft, state)
135
+ }
136
+ if (!ownerTurnId && parentActorId === mainActor.actorId)
137
+ ownerTurnId = state.currentMainTurn.status === 'open' ? state.currentMainTurn.turnId : draft.turnId
138
+ if (!ownerTurnId)
139
+ return awaitOwner(subagentId, draft, state)
140
+ if (draft.type === 'subagent.ended') {
141
+ const pending = state.unreportedEndedSubagentsByParent.get(parentActorId) ?? new Map<string, string>()
142
+ pending.set(subagentId, draft.payload.status)
143
+ state.unreportedEndedSubagentsByParent.set(parentActorId, pending)
144
+ }
145
+ const attributed: AgentEventDraft = { ...draft, turnId: ownerTurnId }
146
+ if (draft.type !== 'subagent.started')
147
+ return [attributed]
148
+ const awaiting = state.draftsAwaitingOwnerByActorId.get(subagentId) ?? []
149
+ state.draftsAwaitingOwnerByActorId.delete(subagentId)
150
+ return [attributed, ...attributeSubagentDraftsToOwnerTurn(awaiting, state)]
151
+ }
152
+
153
+ function awaitOwner(actorId: string, draft: AgentEventDraft, state: CodexEventMapperState): AgentEventDraft[] {
154
+ const awaiting = state.draftsAwaitingOwnerByActorId.get(actorId) ?? []
155
+ awaiting.push(draft)
156
+ state.draftsAwaitingOwnerByActorId.set(actorId, awaiting)
157
+ return []
158
+ }
159
+
160
+ function mapNotificationDrafts(notification: CodexNotification, state: CodexEventMapperState): AgentEventDraft[] {
109
161
  // Codex has many app-server notifications; the runtime maps the subset that has a provider-neutral AgentEvent.
110
162
 
111
163
  switch (notification.method) {
@@ -265,11 +317,11 @@ function toolCallStart(item: CodexStartedItem): { toolName: string, input: JsonV
265
317
  return undefined
266
318
 
267
319
  if (item.type === 'commandExecution')
268
- return { toolName, input: toJsonObject({ command: Array.isArray(item.command) ? item.command.join(' ') : String(item.command ?? ''), cwd: item.cwd }) }
320
+ return { toolName, input: toJsonObject({ command: unwrapShellCommand(Array.isArray(item.command) ? item.command.join(' ') : String(item.command ?? '')), cwd: item.cwd }) }
269
321
  if (item.type === 'webSearch')
270
322
  return { toolName, input: toJsonObject({ query: optionalNonEmpty(item.query) }) }
271
323
  if (item.type === 'fileChange')
272
- return { toolName, input: jsonInput({ files: item.changes.map(change => removeUndefined({ path: change.path, kind: change.kind?.type })) }) }
324
+ return { toolName, input: jsonInput({ file_path: item.changes.map(change => change.path).join(', '), files: item.changes.map(change => removeUndefined({ path: change.path, kind: change.kind?.type })) }) }
273
325
  if (item.type === 'imageView')
274
326
  return { toolName, input: toJsonObject({ path: item.path }) }
275
327
  if (item.type === 'imageGeneration')
@@ -283,6 +335,17 @@ function toolCallStart(item: CodexStartedItem): { toolName: string, input: JsonV
283
335
  return undefined
284
336
  }
285
337
 
338
+ const SHELL_WRAPPER = /^(?:\/(?:usr\/)?bin\/)?(?:ba|z)?sh -l?c (?:'((?:[^']|'\\'')*)'|"((?:[^"\\]|\\.)*)")$/s
339
+
340
+ export function unwrapShellCommand(command: string): string {
341
+ const match = SHELL_WRAPPER.exec(command.trim())
342
+ if (!match)
343
+ return command
344
+ if (match[1] !== undefined)
345
+ return match[1].replaceAll(`'\\''`, `'`)
346
+ return match[2]!.replace(/\\([\\"$`])/g, '$1')
347
+ }
348
+
286
349
  function toolNameForItem(item: CodexStartedItem | CodexCompletedItem): string | undefined {
287
350
  if (item.type === 'commandExecution')
288
351
  return CANONICAL_TOOL.bash
@@ -586,6 +649,7 @@ function mapCollabAgentToolCompleted({ turnId, threadId, item, mapperState }: {
586
649
  toolCallId: item.id,
587
650
  toolName: item.tool,
588
651
  status: item.status === 'failed' ? 'failed' as const : 'completed' as const,
652
+ output: item.status === 'failed' ? undefined : collabToolOutput(collabToolSummary(item, mapperState)),
589
653
  error: item.status === 'failed' ? { message: `Codex ${item.tool} failed.` } : undefined,
590
654
  }),
591
655
  },
@@ -666,7 +730,7 @@ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string,
666
730
  state.subagentById.set(item.agentThreadId, actor)
667
731
  return [
668
732
  ...emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: 'spawnAgent' } }, threadId, state }),
669
- ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'spawnAgent', status: 'completed' } }, threadId, state }),
733
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'spawnAgent', status: 'completed', output: collabToolOutput(item.agentPath) } }, threadId, state }),
670
734
  {
671
735
  type: 'subagent.started',
672
736
  turnId,
@@ -680,7 +744,7 @@ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string,
680
744
  if (item.kind === 'interacted') {
681
745
  return [
682
746
  ...emitOrBufferByThread({ draft: { type: 'tool.call.started', turnId, payload: { toolCallId: item.id, toolName: 'sendInput' } }, threadId, state }),
683
- ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'sendInput', status: 'completed' } }, threadId, state }),
747
+ ...emitOrBufferByThread({ draft: { type: 'tool.call.completed', turnId, payload: { toolCallId: item.id, toolName: 'sendInput', status: 'completed', output: collabToolOutput(item.agentPath) } }, threadId, state }),
684
748
  ]
685
749
  }
686
750
 
@@ -703,6 +767,26 @@ function mapSubAgentActivityCompleted(input: { turnId: string, threadId: string,
703
767
  }]
704
768
  }
705
769
 
770
+ function collabToolSummary(item: CodexCollabAgentToolCompletedItem, state: CodexEventMapperState): string {
771
+ const name = (threadId: string) => state.subagentByThreadId.get(threadId)?.name ?? state.subagentNameByThreadId.get(threadId) ?? threadId
772
+ const parentActorId = item.senderThreadId === state.mainThreadId ? mainActor.actorId : item.senderThreadId
773
+ const unreported = state.unreportedEndedSubagentsByParent.get(parentActorId)
774
+ const states = Object.entries(item.agentsStates).map(([threadId, agentState]) => {
775
+ unreported?.delete(threadId)
776
+ return `${name(threadId)}: ${agentState.status}`
777
+ })
778
+ if (states.length)
779
+ return states.join('\n')
780
+ if (item.receiverThreadIds.length)
781
+ return item.receiverThreadIds.map(name).join('\n')
782
+ state.unreportedEndedSubagentsByParent.delete(parentActorId)
783
+ return [...unreported ?? []].map(([threadId, status]) => `${name(threadId)}: ${status}`).join('\n') || item.tool
784
+ }
785
+
786
+ function collabToolOutput(text: string): AgentToolResult {
787
+ return { success: true, content: [{ type: 'text', text }] }
788
+ }
789
+
706
790
  function mapChildTurnCompleted(input: {
707
791
  threadId: string
708
792
  turnId: string
@@ -853,10 +937,9 @@ function subagentPayloadRef(actor: CodexSubagentRef): Pick<CodexSubagentRef, 'ac
853
937
 
854
938
  function collabToolInput(item: CodexCollabAgentToolStartedItem): JsonObject {
855
939
  return toJsonObject({
856
- senderThreadId: item.senderThreadId,
857
- receiverThreadIds: item.receiverThreadIds.join(','),
858
- prompt: item.prompt,
859
- model: item.model,
940
+ prompt: optionalNonEmpty(item.prompt),
941
+ receiverThreadIds: optionalNonEmpty(item.receiverThreadIds.join(',')),
942
+ model: optionalNonEmpty(item.model),
860
943
  reasoningEffort: typeof item.reasoningEffort === 'string' ? item.reasoningEffort : undefined,
861
944
  })
862
945
  }
@@ -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.3.0",
4
- sha: "8cc96eb90e17628946dfd6f7cc0bac298722a4c1",
5
- buildTime: "2026-10-20T14:46:27.024Z",
3
+ version: "0.4.0-experimental.0",
4
+ sha: "91be1614dad53cf32cfbdceccd8c1209b21686e9",
5
+ buildTime: "2026-10-29T14:15:16.414Z",
6
6
  } as const
@@ -152,14 +152,16 @@ export class DurableStepProjector {
152
152
  : event.type.startsWith('reasoning.') ? 'reasoning' : undefined
153
153
  if (!kind || !('turnId' in event)) {
154
154
  const frames: WireEventBody[] = []
155
- if (event.type === 'turn.ended' || event.type === 'session.ended') {
155
+ if (event.type === 'turn.ended' || event.type === 'session.ended' || event.type === 'subagent.ended') {
156
156
  for (const row of this.db.query<{ body: string, last_event_id: string | null }, []>('select body, last_event_id from steps').all()) {
157
157
  const step = JSON.parse(row.body) as StepDetailV1
158
158
  if (step.sessionId !== event.sessionId || step.providerSessionId !== event.providerSessionId)
159
159
  continue
160
160
  if ((step.status !== 'running' && row.last_event_id !== event.eventId) || (event.type === 'turn.ended' && (step.turnId !== event.turnId || step.actor.actorId !== event.actor.actorId)))
161
161
  continue
162
- step.status = event.type === 'turn.ended' && event.payload.status === 'failed' ? 'failed' : 'interrupted'
162
+ if (event.type === 'subagent.ended' && step.actor.actorId !== event.actor.actorId)
163
+ continue
164
+ step.status = event.type !== 'session.ended' && event.payload.status === 'failed' ? 'failed' : 'interrupted'
163
165
  step.incomplete = true
164
166
  step.endedAt = event.timestamp
165
167
  frames.push(...await this.finish(step))
@@ -86,10 +86,12 @@ export function createAgentSupervisorProviderFactory(
86
86
  if (provider.id !== input.bootstrap.agent.provider)
87
87
  throw new Error(`Provider factory returned ${provider.id} for ${input.bootstrap.agent.provider}.`)
88
88
 
89
- const previousProviderSessionId = await options.bindingStore.load({
90
- sessionId: input.bootstrap.sessionId,
91
- provider: provider.id,
92
- })
89
+ const previousProviderSessionId = input.fresh
90
+ ? undefined
91
+ : await options.bindingStore.load({
92
+ sessionId: input.bootstrap.sessionId,
93
+ provider: provider.id,
94
+ })
93
95
  const sessionInput = {
94
96
  sessionId: input.bootstrap.sessionId,
95
97
  cwd: input.bootstrap.agent.cwd,
@@ -111,11 +111,27 @@ export function createBootstrapBinder(input: {
111
111
  }
112
112
  const persisted = binding.store.binding()
113
113
  if (!persisted || persisted.bodyHash !== sha256CanonicalJson(request.envelope.body)) {
114
- throw new SupervisorConfigurationError({
115
- code: 'agent-core.bootstrap-configuration-conflict',
116
- message: 'Supervisor reconnect bootstrap differs from its immutable binding.',
117
- publicMessage: 'The sandbox configuration cannot change after bootstrap.',
118
- })
114
+ // Before any command the configuration has run nothing, so a session may still
115
+ // replace it; once a command arrived it is the configuration of what ran.
116
+ if (!persisted || request.envelope.body.initialCommand || binding.store.lastReceivedCommandSeq() !== 0) {
117
+ throw new SupervisorConfigurationError({
118
+ code: 'agent-core.bootstrap-configuration-conflict',
119
+ message: 'Supervisor reconnect bootstrap differs from its immutable binding.',
120
+ publicMessage: 'The sandbox configuration cannot change after bootstrap.',
121
+ })
122
+ }
123
+ try {
124
+ await input.admission?.assertAllowed(request.envelope.body)
125
+ }
126
+ catch (cause) {
127
+ throw new SupervisorConfigurationError({
128
+ code: 'agent-core.bootstrap-admission-rejected',
129
+ message: 'Supervisor bootstrap admission rejected the replacement configuration.',
130
+ publicMessage: 'The sandbox configuration is not allowed.',
131
+ cause,
132
+ })
133
+ }
134
+ binding.store.reconfigure(request.envelope.body)
119
135
  }
120
136
  }
121
137
  const acceptedBinding = binding
@@ -158,6 +158,37 @@ export class SupervisorStore {
158
158
  }
159
159
  }
160
160
 
161
+ /**
162
+ * Replaces the bootstrap body of a binding that has not received a command yet: nothing has
163
+ * run under the previous configuration, so the next agent runtime starts from this one.
164
+ */
165
+ reconfigure(body: SessionBootstrapBody): PersistedSupervisorBinding {
166
+ const transaction = this.database.transaction(() => {
167
+ const existing = this.bootstrapRow()
168
+ if (!existing)
169
+ throw new Error('Supervisor is not bound.')
170
+ const binding = bindingFromRow(existing)
171
+ if (binding.body.sessionId !== body.sessionId)
172
+ throw new Error('Supervisor is already bound to another session.')
173
+ if (body.initialCommand || this.lastReceivedCommandSeq() !== 0)
174
+ throw new Error('Supervisor configuration cannot change after a command.')
175
+ const bodyJson = canonicalJson(body)
176
+ const bodyHash = sha256CanonicalJson(body)
177
+ this.database.query(`
178
+ UPDATE bootstrap SET body_json = ?, body_hash = ?, updated_at = ? WHERE id = ?
179
+ `).run(bodyJson, bodyHash, new Date().toISOString(), bootstrapId)
180
+ this.database.query(`INSERT INTO metadata (key, value) VALUES ('provider_session_reset', ?)
181
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(bodyHash)
182
+ return { ...binding, body, bodyHash }
183
+ })
184
+ try {
185
+ return transaction()
186
+ }
187
+ catch (cause) {
188
+ throw new SupervisorPersistenceError({ message: 'Failed to reconfigure supervisor bootstrap.', cause })
189
+ }
190
+ }
191
+
161
192
  acceptConnectionAttempt(input: {
162
193
  connectionGeneration: number
163
194
  runtimeConnectionAttemptId: string
@@ -217,6 +248,18 @@ export class SupervisorStore {
217
248
  })()
218
249
  }
219
250
 
251
+ /**
252
+ * A reconfiguration left the provider session bound before it on disk: the next agent runtime
253
+ * opens a new one, across restarts, until one is started from the replaced configuration.
254
+ */
255
+ providerSessionResetPending(): boolean {
256
+ return Boolean(this.database.query('SELECT 1 FROM metadata WHERE key = \'provider_session_reset\'').get())
257
+ }
258
+
259
+ clearProviderSessionReset(bodyHash: string): void {
260
+ this.database.query('DELETE FROM metadata WHERE key = \'provider_session_reset\' AND value = ?').run(bodyHash)
261
+ }
262
+
220
263
  /** Restore accepted defaults in application order; the bootstrap remains immutable. */
221
264
  appliedSettings(): AgentSettingsChange | undefined {
222
265
  const row = this.database.query<{ value: string }, []>('SELECT value FROM metadata WHERE key = \'applied_settings\'').get()
@@ -31,6 +31,8 @@ export interface SupervisorProviderFactory {
31
31
  signal: AbortSignal
32
32
  emit?: (body: WireEventBody) => void
33
33
  onFatal?: (error: unknown) => void
34
+ /** Open a new provider session rather than resume the one bound before a reconfiguration. */
35
+ fresh?: boolean
34
36
  }) => Promise<SupervisorAgentRuntime>
35
37
  }
36
38
 
@@ -75,13 +75,14 @@ export async function createResidentSupervisor(input: {
75
75
  credentials: activation.envelope.credentials,
76
76
  gitToken: activation.envelope.gitToken,
77
77
  logger: input.logger,
78
- onFatal: error => requestShutdown(
79
- error instanceof SupervisorConfigurationError ? TERMINAL_ERROR_CODE : RECOVERABLE_ERROR_CODE,
80
- ),
78
+ onFatal: (error) => {
79
+ input.logger.error({ err: error }, 'Supervisor agent runtime failed')
80
+ requestShutdown(error instanceof SupervisorConfigurationError ? TERMINAL_ERROR_CODE : RECOVERABLE_ERROR_CODE)
81
+ },
81
82
  })
82
83
  }
83
84
  else {
84
- await handler.refreshSecrets({
85
+ await handler.rebind({
85
86
  credentials: activation.envelope.credentials,
86
87
  gitToken: activation.envelope.gitToken,
87
88
  })
@@ -105,7 +106,7 @@ export async function createResidentSupervisor(input: {
105
106
  binding: binding.status,
106
107
  profile: input.profile,
107
108
  agentCore: agentCoreProvenance(),
108
- capabilities: { 'model-change': 'v1' },
109
+ capabilities: { 'model-change': 'v1', 'reconfigure-before-commands': 'v1' },
109
110
  }),
110
111
  getBoundSessionId: () => binding.status === 'bound' ? binding.sessionId : undefined,
111
112
  getLocalEventMax: () => binding.status === 'bound' ? binding.store.localEventMax() : 0,
@@ -39,6 +39,7 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
39
39
  private processing = Promise.resolve()
40
40
  private eventAckFloor = 0
41
41
  private stopped = false
42
+ private agentBodyHash: string | undefined
42
43
  private readonly retainedFrames: string[] = []
43
44
  private readonly ephemeralFrames = new EphemeralFrameBuffer()
44
45
  private readonly heartbeatTimer: ReturnType<typeof setInterval>
@@ -164,6 +165,32 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
164
165
  await this.agent?.refreshSecrets?.(input)
165
166
  }
166
167
 
168
+ /**
169
+ * A bootstrap was accepted. When the binding took another configuration before any command,
170
+ * the agent started from the previous one is closed and the next starts from the new one;
171
+ * otherwise the agent keeps running with the bootstrap's secrets.
172
+ */
173
+ async rebind(input: {
174
+ credentials?: EphemeralCredentials
175
+ gitToken?: SessionBootstrapGitToken
176
+ }): Promise<void> {
177
+ if (!this.agentBodyHash || this.agentBodyHash === this.dependencies.store.binding()?.bodyHash) {
178
+ if (this.agent && this.agentBodyHash)
179
+ this.dependencies.store.clearProviderSessionReset(this.agentBodyHash)
180
+ await this.refreshSecrets(input)
181
+ return
182
+ }
183
+ this.dependencies.credentials = input.credentials
184
+ this.dependencies.gitToken = input.gitToken
185
+ await this.processing.catch(() => undefined)
186
+ const agent = await this.createAgentTask?.catch(() => undefined) ?? this.agent
187
+ this.agent = undefined
188
+ this.createAgentTask = undefined
189
+ this.agentBodyHash = undefined
190
+ if (agent)
191
+ await agent.close({ signal: new AbortController().signal }).catch(error => this.dependencies.logger.warn({ err: error }, 'agent runtime close failed'))
192
+ }
193
+
167
194
  async stop(reason: 'sigterm' | 'sigint' | 'fatal_error' = 'sigterm'): Promise<void> {
168
195
  if (this.stopped)
169
196
  return
@@ -206,14 +233,18 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
206
233
  credentials: this.dependencies.credentials,
207
234
  gitToken: this.dependencies.gitToken,
208
235
  }
236
+ const fresh = this.dependencies.store.providerSessionResetPending()
237
+ this.agentBodyHash = binding.bodyHash
209
238
  this.createAgentTask = this.dependencies.providerFactory.create({
210
239
  bootstrap: { ...binding.body, agent: { ...binding.body.agent, ...this.dependencies.store.appliedSettings() } },
211
240
  ...initialSecrets,
241
+ ...(fresh && { fresh }),
212
242
  rpc: this.rpc,
213
243
  signal: this.lifecycleController.signal,
214
244
  emit: body => this.emit(body),
215
245
  onFatal: error => this.dependencies.onFatal(error),
216
246
  }).then(async (agent) => {
247
+ this.dependencies.store.clearProviderSessionReset(binding.bodyHash)
217
248
  await agent.ready?.()
218
249
  const previousEventMax = this.dependencies.store.localEventMax()
219
250
  this.dependencies.store.recoverCommandDispatches()