@mindot/will 0.6.0 → 0.7.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.
@@ -1,5 +1,5 @@
1
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
- import { E as EffectorHandler, W as Will } from '../will-Bikuk4s2.js';
2
+ import { E as EffectorHandler, W as Will } from '../will-DAW0l-lY.js';
3
3
 
4
4
  /** Where the tools live: spawn a local server, reach a remote one, or bring a connected client. */
5
5
  type McpToolsSource = {
@@ -6026,6 +6026,9 @@ declare class SchemaRepertoire {
6026
6026
  private _skills;
6027
6027
  /** Tracks which templates were learned at runtime (vs innate) so decay can forget them. */
6028
6028
  private _learned;
6029
+ /** Availability layer (P2): schema → { value 0..1, lastRefusedTick }. Empty until
6030
+ * a refusal lands — a never-refused Will writes nothing here (byte-identical). */
6031
+ private _availability;
6029
6032
  constructor(seed?: MotorSchema[]);
6030
6033
  schemas(): MotorSchema[];
6031
6034
  getSchema(id: string): MotorSchema | undefined;
@@ -6040,6 +6043,23 @@ declare class SchemaRepertoire {
6040
6043
  registerExternal(schema: MotorSchema): void;
6041
6044
  skills(): ReadonlyMap<string, LearnedSkill>;
6042
6045
  getSkill(id: string): LearnedSkill | undefined;
6046
+ availability(): ReadonlyMap<string, {
6047
+ value: number;
6048
+ lastRefusedTick: number;
6049
+ }>;
6050
+ /**
6051
+ * How available a schema is right now, 0..1. Absent from the ledger ⇒ 1
6052
+ * (fully available — the common case). This is the ONLY value the
6053
+ * AffordanceSynthesizer reads; it never touches competence.
6054
+ */
6055
+ availabilityOf(schema: string): number;
6056
+ /**
6057
+ * Fold a policy refusal into the availability layer (NOT competence). A
6058
+ * `class` refusal cuts availability hard; an `instance` refusal dents it
6059
+ * lightly. Multiplicative so repeated refusals compound toward — but never
6060
+ * reach — zero, keeping re-probe alive.
6061
+ */
6062
+ recordRefusal(schema: string, finality: 'class' | 'instance', tick: number): number;
6043
6063
  /**
6044
6064
  * Fold one outcome into the schema's learned skill. Returns the updated skill
6045
6065
  * and whether it just crossed the proceduralization threshold this update.
@@ -6049,11 +6069,16 @@ declare class SchemaRepertoire {
6049
6069
  proceduralized: boolean;
6050
6070
  };
6051
6071
  /**
6052
- * Forgetting curve over the competence layer. Skills unused for IDLE_TICKS
6053
- * lose habit; learned composites that fall below DROP_HABIT are dropped
6054
- * entirely (template + skill). Returns the schema ids that were forgotten.
6072
+ * Forgetting curve over the competence layer, plus availability recovery.
6073
+ * Skills unused for IDLE_TICKS lose habit; learned composites below DROP_HABIT
6074
+ * are dropped entirely (template + skill). Availability entries climb back
6075
+ * toward 1 and are dropped once fully recovered. Returns the ids that were
6076
+ * removed from each layer so their mirrored state entities can be deleted.
6055
6077
  */
6056
- decay(tick: number): string[];
6078
+ decay(tick: number): {
6079
+ skills: string[];
6080
+ availability: string[];
6081
+ };
6057
6082
  /** Learned composite templates + all skills above a confidence floor. */
6058
6083
  export(minHabit?: number): {
6059
6084
  composites: MotorSchema[];
@@ -6073,6 +6098,12 @@ declare class SchemaRepertoire {
6073
6098
  * Mirrors GoalManager._syncFromStateGoals.
6074
6099
  */
6075
6100
  restoreComposites(entities: ReadonlySimulationState['entities']): void;
6101
+ /** Availability ledger encoded as `agency.availability` state entities (P2).
6102
+ * Empty until a refusal lands, so the quiet path writes nothing. */
6103
+ availabilityEntities(): EntityInput[];
6104
+ /** Rehydrate the availability ledger from state after a restore. Idempotent;
6105
+ * keeps whichever value is more restrictive so a concurrent refusal isn't lost. */
6106
+ restoreAvailability(entities: ReadonlySimulationState['entities']): void;
6076
6107
  }
6077
6108
 
6078
6109
  type SkillAccessor = () => ReadonlyMap<string, LearnedSkill>;
@@ -7695,6 +7726,13 @@ declare class WillStem {
7695
7726
  description: string;
7696
7727
  metrics?: Record<string, number>;
7697
7728
  }): void;
7729
+ /**
7730
+ * Resolve a policy escalation the Will raised (POLICY_REAFFERENCE P4).
7731
+ * `approved` dispatches the held invocation to the world; otherwise it is
7732
+ * refused. Applied at the next tick boundary. `invocationId` is the awaiting
7733
+ * `agency.intent` id the escalation ask referenced.
7734
+ */
7735
+ resolveEscalation(id: string, invocationId: string, approved: boolean): void;
7698
7736
  /**
7699
7737
  * Confirm a message was received by the target entity. Writes a
7700
7738
  * message.delivery percept ("ear hears the word you spoke") and updates the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindot/will",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "author": "Fabrice <fabrice8@github.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -294,19 +294,26 @@ export class ActionSelector implements CognitiveEngine {
294
294
  }
295
295
  })
296
296
 
297
- // ── Commitment revocation (EXAFFERENCE P4) ───────────────────
298
- // A hard rupture doesn't just soften the switch cost it lets go of a
299
- // commitment still being weighed. We can't delete the `deliberating` intent
300
- // here (Deliberation runs after us and would resurrect it set-after-delete),
301
- // so we drop a tombstone the Deliberation engine + Executor honor next tick.
302
- // No successor is committed the field re-forms and the next tick selects.
303
- if( deliberating && rupture >= RUPTURE_REVOKE_GATE ){
297
+ // ── Commitment revocation (EXAFFERENCE P4 · POLICY_REAFFERENCE P3) ──
298
+ // A commitment still being weighed is let go for either of two DISTINCT
299
+ // reasons: a hard exafferent rupture (the world surprised us), or a class
300
+ // policy refusal of the very schema we're deliberating (the boundary just
301
+ // declared it forbidden deliberating our way into it is wasted). The two
302
+ // never mix: the refusal is an outcome, not a percept, so it contributes
303
+ // ZERO to the exafferent scalar. Either way we can't delete the
304
+ // `deliberating` intent here (Deliberation runs after us and would resurrect
305
+ // it set-after-delete), so we drop a tombstone the Deliberation engine +
306
+ // Executor honor next tick. No successor is committed — the field re-forms.
307
+ const policyRevoke = !!deliberating && refusedClassSchemas( state ).has( deliberating.schema )
308
+ if( deliberating && ( rupture >= RUPTURE_REVOKE_GATE || policyRevoke ) ){
309
+ const reason = policyRevoke ? 'policy-refusal' : 'exafferent-rupture'
310
+ const revRupture = policyRevoke ? Math.max( rupture, RUPTURE_REVOKE_GATE ) : rupture
304
311
  if( this._bus ){
305
312
  try {
306
313
  this._bus.publish({
307
314
  type: 'agency.commitment.revoked', version: 1, sourceEngine: this.name,
308
315
  salience: 0.85,
309
- payload: { from: deliberating.schema, reason: 'exafferent-rupture', rupture, tick },
316
+ payload: { from: deliberating.schema, reason, rupture: revRupture, tick },
310
317
  })
311
318
  }
312
319
  catch( err ){ logger.warn(`[selector] revoked publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
@@ -314,11 +321,12 @@ export class ActionSelector implements CognitiveEngine {
314
321
  this._lastRevoked = { schema: deliberating.schema, tick } // Channel-B: flavor the next deliberation
315
322
  return {
316
323
  commands: {
317
- set: [ revocationEntity( deliberating.id, deliberating.schema, rupture, tick ) ],
324
+ set: [ revocationEntity( deliberating.id, deliberating.schema, revRupture, tick ) ],
318
325
  metrics: [
319
326
  [ 'agency.field.eligible', eligible.length ],
320
327
  [ 'agency.selection.busy', 1 ],
321
328
  [ 'agency.commitment.revoked', 1 ],
329
+ ...( policyRevoke ? [ [ 'agency.policy.revoked', 1 ] as [ string, number ] ] : [] ),
322
330
  ...stabMetrics,
323
331
  ],
324
332
  },
@@ -632,6 +640,30 @@ function computeRupture(
632
640
  return clamp01( ( maxSalience - RUPTURE_SALIENCE_GATE ) / ( 1 - RUPTURE_SALIENCE_GATE ) )
633
641
  }
634
642
 
643
+ /**
644
+ * POLICY_REAFFERENCE P3 — schemas hit by a CLASS-final policy refusal visible in
645
+ * frozen state this tick (a refused `agency.outcome` lives exactly one tick).
646
+ *
647
+ * A refusal is an `agency.outcome`, never a `percept`, so it can NEVER feed
648
+ * computeRupture: the mind cannot rupture itself with its own boundary. This is
649
+ * a SEPARATE, explicit trigger — "the boundary just declared this forbidden, let
650
+ * go of any commitment I'm still weighing toward it" — distinct from a
651
+ * world-surprise exafferent rupture, and it carries its own revocation reason.
652
+ * Only `class` finality qualifies: an `instance` refusal means "not with those
653
+ * parameters", not "never", so a still-deliberating attempt may yet succeed.
654
+ */
655
+ function refusedClassSchemas( state: ReadonlySimulationState ): Set<string> {
656
+ const out = new Set<string>()
657
+ for( const e of state.entities.values() ){
658
+ if( e.type !== 'agency.outcome') continue
659
+ const m = e.metadata
660
+ if( m?.['refused'] !== true || str( m?.['finality'] ) !== 'class') continue
661
+ const schema = str( m?.['schema'] )
662
+ if( schema ) out.add( schema )
663
+ }
664
+ return out
665
+ }
666
+
635
667
  /**
636
668
  * Competition weights = DEFAULT_WEIGHTS with the two trait-owned ones (risk, novelty)
637
669
  * overridden by their developed values (base ⊕ prior). Negative weights are clamped to
@@ -103,6 +103,9 @@ export class AffordanceSynthesizer implements CognitiveEngine {
103
103
  // means the executor, which ticks later in the SAME tick, sees a whole
104
104
  // repertoire and can expand a restored mid-flight macro. Idempotent.
105
105
  this._repertoire?.restoreComposites( state.entities )
106
+ // Availability entries (P2) rehydrate the same way, so a restored Will keeps
107
+ // its learned suppressions instead of re-probing forbidden abilities.
108
+ this._repertoire?.restoreAvailability( state.entities )
106
109
 
107
110
  const schemas = this._repertoire?.schemas() ?? this._schemas
108
111
  const skills = this._skills?.() ?? this._repertoire?.skills() ?? null
@@ -306,6 +309,10 @@ export class AffordanceSynthesizer implements CognitiveEngine {
306
309
  ): Affordance {
307
310
  const skill = skills?.get( schema.id )
308
311
 
312
+ // Policy availability (P2): omitted when fully available (1), so a never-refused
313
+ // Will's affordance field is byte-identical. Present only once a refusal dented it.
314
+ const availability = this._repertoire?.availabilityOf( schema.id ) ?? 1
315
+
309
316
  // Learned value if known, else the schema's intrinsic prior mapped to 0..1.
310
317
  const expectedReward = skill?.valueEstimate ?? clamp01( ( ( schema.baseValence ?? 0 ) + 1 ) / 2 )
311
318
  const expectedValence = schema.baseValence ?? valence
@@ -333,6 +340,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
333
340
  available: this._available( schema.preconditions, ( k ) => metric( state, k, 0 ) ),
334
341
  tags: schema.tags ?? [],
335
342
  ...( schema.description ? { description: schema.description } : {} ),
343
+ ...( availability < 1 ? { availability } : {} ),
336
344
  planBias: ctx.planBias,
337
345
  planId: ctx.planId,
338
346
  stepId: ctx.stepId,
@@ -357,6 +365,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
357
365
  available: a.available,
358
366
  tags: a.tags,
359
367
  description: a.description,
368
+ ...( a.availability !== undefined ? { availability: a.availability } : {} ),
360
369
  planBias: a.planBias,
361
370
  planId: a.planId,
362
371
  stepId: a.stepId,
@@ -179,6 +179,10 @@ export class MotorSchemaExecutor implements CognitiveEngine {
179
179
  // outcome (which also teaches reafference the action is unreliable here).
180
180
  for( const [ id, e ] of state.entities ){
181
181
  if( e.type !== 'agency.intent' || str( e.metadata?.['status'] ) !== 'awaiting') continue
182
+ // POLICY_REAFFERENCE P4 — an escalated intent is HELD: the stem owns its
183
+ // lifecycle (extended TTL → approve/deny/expire), so the executor must not
184
+ // time it out at AWAIT_TIMEOUT and reconcile it as a phantom failure.
185
+ if( e.metadata?.['escalated'] === true ) continue
182
186
  const dispatchedAt = num( e.metadata?.['dispatchedAt'], tick )
183
187
  if( tick - dispatchedAt < AWAIT_TIMEOUT ) continue
184
188
 
@@ -28,7 +28,7 @@ import type { CognitiveBus, CognitiveEvent } from '#cognition/bus'
28
28
  import type { CognitiveEngine, EngineResult } from '#cognition/types'
29
29
  import type { CognitiveEventSchema } from '#cognition/schema.registry'
30
30
  import type { SchemaRepertoire } from '#agency/schemas/repertoire'
31
- import { schemaEntityId } from '#agency/schemas/repertoire'
31
+ import { schemaEntityId, availabilityEntityId } from '#agency/schemas/repertoire'
32
32
  import { AWAIT_TIMEOUT } from '#agency/engines/motor.schema.executor'
33
33
 
34
34
  const PROC_THRESHOLD = 0.60 // mirror of repertoire's threshold for the habitual-count metric
@@ -190,10 +190,28 @@ export class ReafferenceEngine implements CognitiveEngine {
190
190
  // ── 1. Fold each outcome into its skill ──────────────────────
191
191
  let updates = 0
192
192
  let discovered = 0
193
+ let refused = 0
193
194
  for( const { id, meta: m, fromState } of outcomes ){
194
195
  const schema = str( m['schema'] )
195
196
  if( !schema ){ if( fromState ) del.push( id ); continue }
196
197
 
198
+ // POLICY_REAFFERENCE P2 — a refusal is NOT a failure. Route it to the
199
+ // availability layer and stop: it must never touch LearnedSkill (value,
200
+ // habit, param priors), or the Will learns it is unskilled at something it
201
+ // is merely forbidden to do. The awaiting intent is still freed, and a
202
+ // refused plan step is signalled unsuccessful so the plan doesn't hang.
203
+ if( m['refused'] === true ){
204
+ const finality = str( m['finality'] ) === 'class' ? 'class' : 'instance'
205
+ this._repertoire.recordRefusal( schema, finality, tick )
206
+ if( fromState ) del.push( id )
207
+ const refusedIntent = str( m['intentId'] )
208
+ if( refusedIntent ) del.push( refusedIntent )
209
+ const refusedPlan = str( m['planId'] )
210
+ if( refusedPlan ) this._emitPlanOutcome( refusedPlan, str( m['stepId'] ), schema, false, 0, 0, tick )
211
+ refused++
212
+ continue
213
+ }
214
+
197
215
  const { skill, proceduralized } = this._repertoire.recordOutcome({
198
216
  schema,
199
217
  success: m['success'] === true,
@@ -235,19 +253,23 @@ export class ReafferenceEngine implements CognitiveEngine {
235
253
  }
236
254
  }
237
255
 
238
- // ── 2. Forgetting curve over the competence layer ────────────
256
+ // ── 2. Forgetting curve over the competence layer + availability recovery ──
239
257
  const dropped = this._repertoire.decay( tick )
240
- for( const id of dropped ){
258
+ for( const id of dropped.skills ){
241
259
  del.push(`agency-skill-${ id }`)
242
260
  del.push( schemaEntityId( id ) ) // composite mirror (harmless no-op for primitives)
243
261
  }
262
+ for( const id of dropped.availability )
263
+ del.push( availabilityEntityId( id ) ) // fully-recovered ⇒ remove the mirror
244
264
 
245
- // ── 3. Mirror learned composite templates ────────────────────
265
+ // ── 3. Mirror learned composite templates + availability ─────
246
266
  // Skills become `agency.skill` (above); the invented composite *definitions*
247
267
  // must travel too, or a snapshot/restore brings back a skill whose schema is
248
268
  // gone and the executor can't expand it. Idempotent re-write each tick, like
249
269
  // GoalManager._persistGoals. Empty until a composite is actually learned.
250
- for( const e of this._repertoire.compositeEntities() ) set.push( e )
270
+ for( const e of this._repertoire.compositeEntities() ) set.push( e )
271
+ // Availability entries (P2) mirror the same way — empty until a refusal lands.
272
+ for( const e of this._repertoire.availabilityEntities() ) set.push( e )
251
273
 
252
274
  // ── 4. Telemetry ─────────────────────────────────────────────
253
275
  const skills = this._repertoire.skills()
@@ -259,6 +281,9 @@ export class ReafferenceEngine implements CognitiveEngine {
259
281
  [ 'agency.habitual.count', habitual ],
260
282
  [ 'agency.sensory.confirmed', sensory ],
261
283
  )
284
+ // Only emit the refusal metric when it fired — a never-refused Will writes
285
+ // nothing here, preserving the byte-identical quiet path (cf. EXAFFERENCE P3).
286
+ if( refused > 0 ) metrics.push([ 'agency.refused.count', refused ])
262
287
 
263
288
  return { commands: { set, delete: del, metrics } }
264
289
  }
@@ -24,6 +24,14 @@ export interface HostAckResult {
24
24
  /** −1..1 felt valence of the outcome. */
25
25
  valence?: number
26
26
  description?: string
27
+ /**
28
+ * POLICY_REAFFERENCE P2 — this ack is a policy REFUSAL, not a world failure.
29
+ * The ReafferenceEngine routes it to the availability layer (NOT competence):
30
+ * a refusal must never teach the Will it is unskilled at something it is merely
31
+ * forbidden to do. `finality` decides how hard availability is cut.
32
+ */
33
+ refused?: boolean
34
+ finality?: 'class' | 'instance'
27
35
  }
28
36
 
29
37
  /**
@@ -71,6 +79,7 @@ export function reconcileInvocation(
71
79
  mode: 'external',
72
80
  tick,
73
81
  reconciled: true,
82
+ ...( result.refused ? { refused: true, finality: result.finality ?? 'instance' } : {} ),
74
83
  ...( provenance.planId ? { planId: provenance.planId } : {} ),
75
84
  ...( provenance.stepId ? { stepId: provenance.stepId } : {} ),
76
85
  },
@@ -39,6 +39,23 @@ const IDLE_TICKS = 200 // ticks of disuse before forgetting starts
39
39
  const DECAY_RATE = 0.02 // habit lost per decay application
40
40
  const DROP_HABIT = 0.05 // below this (and learned) the skill is forgotten
41
41
 
42
+ // ── availability layer (POLICY_REAFFERENCE P2) ────────────────
43
+ // Availability is NOT competence. It answers "may I use this schema", learned
44
+ // from policy refusals, and is kept strictly apart from LearnedSkill so a
45
+ // refusal never teaches the Will it is *unskilled* at something it is merely
46
+ // *forbidden* to do. A `class` refusal ("never, under this policy") drives
47
+ // availability down hard; an `instance` refusal ("not with those parameters")
48
+ // dents it lightly — the Will should keep reaching for the ability, just not
49
+ // that way. Recovery is slow but real, so a policy change is re-discoverable:
50
+ // availability never floors at zero, and it climbs back toward 1 with disuse of
51
+ // the refusal. P2 keys availability by SCHEMA; per-(schema, params) envelope
52
+ // narrowing is a follow-up that belongs at selection time, not fielding time.
53
+ const AVAIL_DROP_CLASS = 0.50 // multiplicative cut on a class-final refusal
54
+ const AVAIL_DROP_INSTANCE = 0.12 // lighter cut on an instance-final refusal
55
+ const AVAIL_FLOOR = 0.05 // never zero — re-probe must always be possible
56
+ const AVAIL_RECOVERY = 0.02 // per-decay climb back toward 1
57
+ const AVAIL_RECOVERED = 0.999 // at/above this the entry is dropped (quiet path)
58
+
42
59
  export interface OutcomeObservation {
43
60
  schema: string
44
61
  success: boolean
@@ -54,6 +71,9 @@ export class SchemaRepertoire {
54
71
  private _skills = new Map<string, LearnedSkill>()
55
72
  /** Tracks which templates were learned at runtime (vs innate) so decay can forget them. */
56
73
  private _learned = new Set<string>()
74
+ /** Availability layer (P2): schema → { value 0..1, lastRefusedTick }. Empty until
75
+ * a refusal lands — a never-refused Will writes nothing here (byte-identical). */
76
+ private _availability = new Map<string, { value: number; lastRefusedTick: number }>()
57
77
 
58
78
  constructor( seed: MotorSchema[] = INNATE_SCHEMAS ){
59
79
  for( const s of seed ) this._templates.set( s.id, s )
@@ -85,6 +105,32 @@ export class SchemaRepertoire {
85
105
  skills(): ReadonlyMap<string, LearnedSkill> { return this._skills }
86
106
  getSkill( id: string ): LearnedSkill | undefined { return this._skills.get( id ) }
87
107
 
108
+ // ── availability (P2) ─────────────────────────────────────────
109
+ availability(): ReadonlyMap<string, { value: number; lastRefusedTick: number }> { return this._availability }
110
+
111
+ /**
112
+ * How available a schema is right now, 0..1. Absent from the ledger ⇒ 1
113
+ * (fully available — the common case). This is the ONLY value the
114
+ * AffordanceSynthesizer reads; it never touches competence.
115
+ */
116
+ availabilityOf( schema: string ): number {
117
+ return this._availability.get( schema )?.value ?? 1
118
+ }
119
+
120
+ /**
121
+ * Fold a policy refusal into the availability layer (NOT competence). A
122
+ * `class` refusal cuts availability hard; an `instance` refusal dents it
123
+ * lightly. Multiplicative so repeated refusals compound toward — but never
124
+ * reach — zero, keeping re-probe alive.
125
+ */
126
+ recordRefusal( schema: string, finality: 'class' | 'instance', tick: number ): number {
127
+ const prev = this._availability.get( schema )?.value ?? 1
128
+ const drop = finality === 'class' ? AVAIL_DROP_CLASS : AVAIL_DROP_INSTANCE
129
+ const value = Math.max( AVAIL_FLOOR, prev * ( 1 - drop ) )
130
+ this._availability.set( schema, { value, lastRefusedTick: tick } )
131
+ return value
132
+ }
133
+
88
134
  /**
89
135
  * Fold one outcome into the schema's learned skill. Returns the updated skill
90
136
  * and whether it just crossed the proceduralization threshold this update.
@@ -118,12 +164,14 @@ export class SchemaRepertoire {
118
164
  }
119
165
 
120
166
  /**
121
- * Forgetting curve over the competence layer. Skills unused for IDLE_TICKS
122
- * lose habit; learned composites that fall below DROP_HABIT are dropped
123
- * entirely (template + skill). Returns the schema ids that were forgotten.
167
+ * Forgetting curve over the competence layer, plus availability recovery.
168
+ * Skills unused for IDLE_TICKS lose habit; learned composites below DROP_HABIT
169
+ * are dropped entirely (template + skill). Availability entries climb back
170
+ * toward 1 and are dropped once fully recovered. Returns the ids that were
171
+ * removed from each layer so their mirrored state entities can be deleted.
124
172
  */
125
- decay( tick: number ): string[] {
126
- const dropped: string[] = []
173
+ decay( tick: number ): { skills: string[]; availability: string[] } {
174
+ const skills: string[] = []
127
175
  for( const [ id, skill ] of this._skills ){
128
176
  if( tick - skill.lastEnactedTick <= IDLE_TICKS ) continue
129
177
 
@@ -132,12 +180,23 @@ export class SchemaRepertoire {
132
180
  this._skills.delete( id )
133
181
  this._templates.delete( id )
134
182
  this._learned.delete( id )
135
- dropped.push( id )
183
+ skills.push( id )
136
184
  continue
137
185
  }
138
186
  this._skills.set( id, { ...skill, habitStrength } )
139
187
  }
140
- return dropped
188
+
189
+ // Availability recovery (P2): each entry climbs slowly back toward 1, so a
190
+ // policy change is re-discoverable. A fully-recovered entry is dropped, so a
191
+ // Will that was refused long ago returns to the byte-identical quiet path.
192
+ const availability: string[] = []
193
+ for( const [ id, avail ] of this._availability ){
194
+ const value = avail.value + AVAIL_RECOVERY * ( 1 - avail.value )
195
+ if( value >= AVAIL_RECOVERED ){ this._availability.delete( id ); availability.push( id ) }
196
+ else this._availability.set( id, { ...avail, value } )
197
+ }
198
+
199
+ return { skills, availability }
141
200
  }
142
201
 
143
202
  // ── PMA portability (Phase 6 reads these) ─────────────────────
@@ -188,6 +247,30 @@ export class SchemaRepertoire {
188
247
  this._learned.add( s.id )
189
248
  }
190
249
  }
250
+
251
+ /** Availability ledger encoded as `agency.availability` state entities (P2).
252
+ * Empty until a refusal lands, so the quiet path writes nothing. */
253
+ availabilityEntities(): EntityInput[] {
254
+ const out: EntityInput[] = []
255
+ for( const [ schema, a ] of this._availability )
256
+ out.push( availabilityEntity( schema, a.value, a.lastRefusedTick ) )
257
+ return out
258
+ }
259
+
260
+ /** Rehydrate the availability ledger from state after a restore. Idempotent;
261
+ * keeps whichever value is more restrictive so a concurrent refusal isn't lost. */
262
+ restoreAvailability( entities: ReadonlySimulationState['entities'] ): void {
263
+ for( const e of entities.values() ){
264
+ if( e.type !== AVAILABILITY_ENTITY_TYPE ) continue
265
+ const m = ( e.metadata ?? {} ) as Record<string, unknown>
266
+ const schema = typeof m['schema'] === 'string' ? m['schema'] : ''
267
+ if( !schema ) continue
268
+ const value = typeof m['value'] === 'number' ? m['value'] : 1
269
+ const tick = typeof m['lastRefusedTick'] === 'number' ? m['lastRefusedTick'] : 0
270
+ const prev = this._availability.get( schema )
271
+ if( !prev || value < prev.value ) this._availability.set( schema, { value, lastRefusedTick: tick } )
272
+ }
273
+ }
191
274
  }
192
275
 
193
276
  // ─── helpers ─────────────────────────────────────────────────────────────────
@@ -209,6 +292,23 @@ function clamp01( n: number ): number {
209
292
  return n < 0 ? 0 : n > 1 ? 1 : n
210
293
  }
211
294
 
295
+ // ─── availability ⇄ state-entity codec (P2) ──────────────────────────────────
296
+
297
+ /** State-entity type for a mirrored availability entry. */
298
+ export const AVAILABILITY_ENTITY_TYPE = 'agency.availability'
299
+
300
+ /** Stable entity id for a schema's availability mirror (idempotent re-writes). */
301
+ export function availabilityEntityId( schema: string ): string { return `agency-availability-${ schema }` }
302
+
303
+ /** Encode one availability entry as a state entity. */
304
+ function availabilityEntity( schema: string, value: number, lastRefusedTick: number ): EntityInput {
305
+ return {
306
+ id: availabilityEntityId( schema ),
307
+ type: AVAILABILITY_ENTITY_TYPE,
308
+ metadata: { schema, value, lastRefusedTick },
309
+ }
310
+ }
311
+
212
312
  // ─── composite ⇄ state-entity codec (snapshot/replay) ────────────────────────
213
313
 
214
314
  /** State-entity type for a mirrored composite template. */
@@ -147,7 +147,7 @@ export function scoreAffordance(
147
147
  bias: BiasContext,
148
148
  w: ScoreWeights = DEFAULT_WEIGHTS,
149
149
  ): number {
150
- return (
150
+ const raw = (
151
151
  w.goal * goalRelevance( a, bias )
152
152
  + w.reward * a.expectedReward
153
153
  + w.novelty * novelty( a )
@@ -158,6 +158,12 @@ export function scoreAffordance(
158
158
  - w.inhib * bias.inhibition
159
159
  - w.risk * risk( a, bias )
160
160
  )
161
+ // POLICY_REAFFERENCE P2 — policy availability damps a POSITIVE activation only,
162
+ // never flipping its sign: a refused ability competes weakly (so it is rarely
163
+ // chosen) yet is never removed from the field, keeping re-probe alive as
164
+ // availability recovers. Absent ⇒ 1 ⇒ no change (byte-identical quiet path).
165
+ const availability = a.availability ?? 1
166
+ return raw > 0 ? raw * availability : raw
161
167
  }
162
168
 
163
169
  /**
@@ -140,6 +140,15 @@ export interface Affordance {
140
140
  * competition WITHOUT bypassing it (the plan biases; the field still decides).
141
141
  */
142
142
  planBias?: number
143
+ /**
144
+ * Policy availability 0..1 (POLICY_REAFFERENCE P2) — learned from refusals,
145
+ * distinct from `available` (precondition satisfaction) and from competence.
146
+ * Present only when < 1 (a refusal has dented it); absent ⇒ fully available,
147
+ * so a never-refused Will's field is byte-identical. Damps positive activation
148
+ * in the competition without flipping its sign, so a suppressed ability still
149
+ * gets an occasional re-probe and can climb back as availability recovers.
150
+ */
151
+ availability?: number
143
152
  /** Provenance: the plan whose frontier step projected this affordance. */
144
153
  planId?: string
145
154
  /** Provenance: the frontier step id — flows through to action.outcome so the plan advances. */
package/src/stem/index.ts CHANGED
@@ -807,6 +807,16 @@ export class WillStem {
807
807
  this._effector.confirmExecution( this._get( id ), invocationId, result )
808
808
  }
809
809
 
810
+ /**
811
+ * Resolve a policy escalation the Will raised (POLICY_REAFFERENCE P4).
812
+ * `approved` dispatches the held invocation to the world; otherwise it is
813
+ * refused. Applied at the next tick boundary. `invocationId` is the awaiting
814
+ * `agency.intent` id the escalation ask referenced.
815
+ */
816
+ resolveEscalation( id: string, invocationId: string, approved: boolean ): void {
817
+ this._effector.resolveEscalation( this._get( id ), invocationId, approved )
818
+ }
819
+
810
820
  // ── Messaging / outbox (11.1) ────────────────────────────────────────────
811
821
  // Delegates to OutboxController (R5-c). `_get(id)` validates the Will exists
812
822
  // and supplies the WillInstance; the outbox ops touch only instance fields.
@@ -963,6 +973,11 @@ export class WillStem {
963
973
  sensory: this._sensory,
964
974
  } )
965
975
 
976
+ // Apply policy refusals queued during the previous step's flush, at the
977
+ // same boundary and for the same reason (POLICY_REAFFERENCE P1): a denial
978
+ // reconciles as a host-rejection-shaped failure ack the step then sees.
979
+ this._effector.applyPolicyOutcomes( instance )
980
+
966
981
  await instance.simulation.step( 1 )
967
982
 
968
983
  instance.tickCount++