@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 +6 -0
- package/package.json +1 -1
- package/src/agents/codex/codex-event-mapper.ts +91 -8
- package/src/provenance.gen.ts +3 -3
- package/src/steps/projector.ts +4 -2
- package/src/supervisor/agent-provider-factory.ts +6 -4
- package/src/supervisor/bootstrap-binder.ts +21 -5
- package/src/supervisor/persistence/supervisor-store.ts +43 -0
- package/src/supervisor/provider-factory.ts +2 -0
- package/src/supervisor/resident.ts +6 -5
- package/src/supervisor/runtime-handler.ts +31 -0
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.
|
|
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
|
-
|
|
857
|
-
receiverThreadIds: item.receiverThreadIds.join(','),
|
|
858
|
-
|
|
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
|
}
|
package/src/provenance.gen.ts
CHANGED
|
@@ -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.
|
|
4
|
-
sha: "
|
|
5
|
-
buildTime: "2026-10-
|
|
3
|
+
version: "0.4.0-experimental.0",
|
|
4
|
+
sha: "91be1614dad53cf32cfbdceccd8c1209b21686e9",
|
|
5
|
+
buildTime: "2026-10-29T14:15:16.414Z",
|
|
6
6
|
} as const
|
package/src/steps/projector.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 =>
|
|
79
|
-
error
|
|
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.
|
|
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()
|