@mindot/will 0.6.0 → 0.8.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.
Files changed (40) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +1 -1
  3. package/dist/channels/whatsapp.d.ts +1 -1
  4. package/dist/cli.js +11104 -10312
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +972 -213
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp/effectors.d.ts +1 -1
  10. package/dist/{will-Bikuk4s2.d.ts → will-cS6k4uiJ.d.ts} +510 -86
  11. package/package.json +1 -1
  12. package/src/cognition/agency/engines/action.selector.ts +42 -9
  13. package/src/cognition/agency/engines/affordance.synthesizer.ts +9 -0
  14. package/src/cognition/agency/engines/motor.schema.executor.ts +4 -0
  15. package/src/cognition/agency/engines/reafference.engine.ts +40 -5
  16. package/src/cognition/agency/reconcile.learning.ts +23 -0
  17. package/src/cognition/agency/schemas/repertoire.ts +114 -7
  18. package/src/cognition/agency/selection.scoring.ts +7 -1
  19. package/src/cognition/agency/types.ts +9 -0
  20. package/src/cognition/config.mirror.entities.ts +1 -1
  21. package/src/cognition/faculties/executive.engine/engine.ts +136 -58
  22. package/src/cognition/faculties/executive.engine/facet.ts +10 -2
  23. package/src/cognition/faculties/executive.engine/prompt.factory.ts +2 -1
  24. package/src/cognition/index.ts +4 -0
  25. package/src/cognition/memory/vector.embedder.ts +9 -5
  26. package/src/cognition/utilities/token.tracker.ts +191 -96
  27. package/src/host/boot.ts +78 -22
  28. package/src/index.ts +35 -0
  29. package/src/llm/index.ts +397 -96
  30. package/src/llm/routing.ts +198 -0
  31. package/src/llm/summarizer.ts +5 -1
  32. package/src/runners/thin-shim.runner.ts +18 -6
  33. package/src/sdk/will.ts +82 -16
  34. package/src/stem/guards/identity.coherence.ts +17 -6
  35. package/src/stem/index.ts +18 -3
  36. package/src/stem/mind.ts +155 -24
  37. package/src/stem/policy/arbiter.ts +171 -0
  38. package/src/stem/policy/rule.table.ts +172 -0
  39. package/src/stem/policy/verdict.recorder.ts +0 -0
  40. package/src/stem/tracts/effector.controller.ts +426 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindot/will",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "author": "Fabrice <fabrice8@github.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,6 +38,7 @@ import {
38
38
  import { readEffectiveParams, readPersonaPrior } from '#cognition/persona.prior'
39
39
  import { RUPTURE_REVOKE_GATE, revocationEntity } from '#agency/revocation'
40
40
  import { liveConsequences, matchConsequenceText } from '#agency/consequence'
41
+ import { asFinality } from '#stem/policy/arbiter'
41
42
 
42
43
  /**
43
44
  * Activation margin (winner − runner-up) below which the choice is "contested" —
@@ -294,19 +295,26 @@ export class ActionSelector implements CognitiveEngine {
294
295
  }
295
296
  })
296
297
 
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 ){
298
+ // ── Commitment revocation (EXAFFERENCE P4 · POLICY_REAFFERENCE P3) ──
299
+ // A commitment still being weighed is let go for either of two DISTINCT
300
+ // reasons: a hard exafferent rupture (the world surprised us), or a class
301
+ // policy refusal of the very schema we're deliberating (the boundary just
302
+ // declared it forbidden deliberating our way into it is wasted). The two
303
+ // never mix: the refusal is an outcome, not a percept, so it contributes
304
+ // ZERO to the exafferent scalar. Either way we can't delete the
305
+ // `deliberating` intent here (Deliberation runs after us and would resurrect
306
+ // it set-after-delete), so we drop a tombstone the Deliberation engine +
307
+ // Executor honor next tick. No successor is committed — the field re-forms.
308
+ const policyRevoke = !!deliberating && refusedClassSchemas( state ).has( deliberating.schema )
309
+ if( deliberating && ( rupture >= RUPTURE_REVOKE_GATE || policyRevoke ) ){
310
+ const reason = policyRevoke ? 'policy-refusal' : 'exafferent-rupture'
311
+ const revRupture = policyRevoke ? Math.max( rupture, RUPTURE_REVOKE_GATE ) : rupture
304
312
  if( this._bus ){
305
313
  try {
306
314
  this._bus.publish({
307
315
  type: 'agency.commitment.revoked', version: 1, sourceEngine: this.name,
308
316
  salience: 0.85,
309
- payload: { from: deliberating.schema, reason: 'exafferent-rupture', rupture, tick },
317
+ payload: { from: deliberating.schema, reason, rupture: revRupture, tick },
310
318
  })
311
319
  }
312
320
  catch( err ){ logger.warn(`[selector] revoked publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
@@ -314,11 +322,12 @@ export class ActionSelector implements CognitiveEngine {
314
322
  this._lastRevoked = { schema: deliberating.schema, tick } // Channel-B: flavor the next deliberation
315
323
  return {
316
324
  commands: {
317
- set: [ revocationEntity( deliberating.id, deliberating.schema, rupture, tick ) ],
325
+ set: [ revocationEntity( deliberating.id, deliberating.schema, revRupture, tick ) ],
318
326
  metrics: [
319
327
  [ 'agency.field.eligible', eligible.length ],
320
328
  [ 'agency.selection.busy', 1 ],
321
329
  [ 'agency.commitment.revoked', 1 ],
330
+ ...( policyRevoke ? [ [ 'agency.policy.revoked', 1 ] as [ string, number ] ] : [] ),
322
331
  ...stabMetrics,
323
332
  ],
324
333
  },
@@ -632,6 +641,30 @@ function computeRupture(
632
641
  return clamp01( ( maxSalience - RUPTURE_SALIENCE_GATE ) / ( 1 - RUPTURE_SALIENCE_GATE ) )
633
642
  }
634
643
 
644
+ /**
645
+ * POLICY_REAFFERENCE P3 — schemas hit by a CLASS-final policy refusal visible in
646
+ * frozen state this tick (a refused `agency.outcome` lives exactly one tick).
647
+ *
648
+ * A refusal is an `agency.outcome`, never a `percept`, so it can NEVER feed
649
+ * computeRupture: the mind cannot rupture itself with its own boundary. This is
650
+ * a SEPARATE, explicit trigger — "the boundary just declared this forbidden, let
651
+ * go of any commitment I'm still weighing toward it" — distinct from a
652
+ * world-surprise exafferent rupture, and it carries its own revocation reason.
653
+ * Only `class` finality qualifies: an `instance` refusal means "not with those
654
+ * parameters", not "never", so a still-deliberating attempt may yet succeed.
655
+ */
656
+ function refusedClassSchemas( state: ReadonlySimulationState ): Set<string> {
657
+ const out = new Set<string>()
658
+ for( const e of state.entities.values() ){
659
+ if( e.type !== 'agency.outcome') continue
660
+ const m = e.metadata
661
+ if( m?.['refused'] !== true || asFinality( m?.['finality'] ) !== 'class') continue
662
+ const schema = str( m?.['schema'] )
663
+ if( schema ) out.add( schema )
664
+ }
665
+ return out
666
+ }
667
+
635
668
  /**
636
669
  * Competition weights = DEFAULT_WEIGHTS with the two trait-owned ones (risk, novelty)
637
670
  * 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,8 @@ 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
+ import { asFinality } from '#stem/policy/arbiter'
32
33
  import { AWAIT_TIMEOUT } from '#agency/engines/motor.schema.executor'
33
34
 
34
35
  const PROC_THRESHOLD = 0.60 // mirror of repertoire's threshold for the habitual-count metric
@@ -190,10 +191,37 @@ export class ReafferenceEngine implements CognitiveEngine {
190
191
  // ── 1. Fold each outcome into its skill ──────────────────────
191
192
  let updates = 0
192
193
  let discovered = 0
194
+ let refused = 0
193
195
  for( const { id, meta: m, fromState } of outcomes ){
194
196
  const schema = str( m['schema'] )
195
197
  if( !schema ){ if( fromState ) del.push( id ); continue }
196
198
 
199
+ // POLICY_REAFFERENCE P2 — a refusal is NOT a failure. Route it to the
200
+ // availability layer and stop: it must never touch LearnedSkill (value,
201
+ // habit, param priors), or the Will learns it is unskilled at something it
202
+ // is merely forbidden to do. The awaiting intent is still freed, and a
203
+ // refused plan step is signalled unsuccessful so the plan doesn't hang.
204
+ if( m['refused'] === true ){
205
+ const finality = asFinality( m['finality'] )
206
+
207
+ // P5 — 'context' means the refusal was NOT ABOUT THE ACTION (tainted
208
+ // context, an unavailable dependency, the arbiter itself down). Denting
209
+ // availability here would teach a lesson the policy never taught:
210
+ // "reach for this less", when nothing about the ability was the problem.
211
+ // So the intent is freed and the plan step signalled — the mind must not
212
+ // hang — and NOTHING else is written. Not competence, not availability.
213
+ if( finality !== 'context')
214
+ this._repertoire.recordRefusal( schema, finality, tick )
215
+
216
+ if( fromState ) del.push( id )
217
+ const refusedIntent = str( m['intentId'] )
218
+ if( refusedIntent ) del.push( refusedIntent )
219
+ const refusedPlan = str( m['planId'] )
220
+ if( refusedPlan ) this._emitPlanOutcome( refusedPlan, str( m['stepId'] ), schema, false, 0, 0, tick )
221
+ refused++
222
+ continue
223
+ }
224
+
197
225
  const { skill, proceduralized } = this._repertoire.recordOutcome({
198
226
  schema,
199
227
  success: m['success'] === true,
@@ -235,19 +263,23 @@ export class ReafferenceEngine implements CognitiveEngine {
235
263
  }
236
264
  }
237
265
 
238
- // ── 2. Forgetting curve over the competence layer ────────────
266
+ // ── 2. Forgetting curve over the competence layer + availability recovery ──
239
267
  const dropped = this._repertoire.decay( tick )
240
- for( const id of dropped ){
268
+ for( const id of dropped.skills ){
241
269
  del.push(`agency-skill-${ id }`)
242
270
  del.push( schemaEntityId( id ) ) // composite mirror (harmless no-op for primitives)
243
271
  }
272
+ for( const id of dropped.availability )
273
+ del.push( availabilityEntityId( id ) ) // fully-recovered ⇒ remove the mirror
244
274
 
245
- // ── 3. Mirror learned composite templates ────────────────────
275
+ // ── 3. Mirror learned composite templates + availability ─────
246
276
  // Skills become `agency.skill` (above); the invented composite *definitions*
247
277
  // must travel too, or a snapshot/restore brings back a skill whose schema is
248
278
  // gone and the executor can't expand it. Idempotent re-write each tick, like
249
279
  // GoalManager._persistGoals. Empty until a composite is actually learned.
250
- for( const e of this._repertoire.compositeEntities() ) set.push( e )
280
+ for( const e of this._repertoire.compositeEntities() ) set.push( e )
281
+ // Availability entries (P2) mirror the same way — empty until a refusal lands.
282
+ for( const e of this._repertoire.availabilityEntities() ) set.push( e )
251
283
 
252
284
  // ── 4. Telemetry ─────────────────────────────────────────────
253
285
  const skills = this._repertoire.skills()
@@ -259,6 +291,9 @@ export class ReafferenceEngine implements CognitiveEngine {
259
291
  [ 'agency.habitual.count', habitual ],
260
292
  [ 'agency.sensory.confirmed', sensory ],
261
293
  )
294
+ // Only emit the refusal metric when it fired — a never-refused Will writes
295
+ // nothing here, preserving the byte-identical quiet path (cf. EXAFFERENCE P3).
296
+ if( refused > 0 ) metrics.push([ 'agency.refused.count', refused ])
262
297
 
263
298
  return { commands: { set, delete: del, metrics } }
264
299
  }
@@ -16,6 +16,7 @@
16
16
  // ─────────────────────────────────────────────────────────────
17
17
 
18
18
  import type { EntityInput, Tick } from '#core/types'
19
+ import type { DenialFinality, PolicyCounterfactual } from '#stem/policy/arbiter'
19
20
 
20
21
  export interface HostAckResult {
21
22
  success: boolean
@@ -24,6 +25,24 @@ export interface HostAckResult {
24
25
  /** −1..1 felt valence of the outcome. */
25
26
  valence?: number
26
27
  description?: string
28
+ /**
29
+ * POLICY_REAFFERENCE P2 — this ack is a policy REFUSAL, not a world failure.
30
+ * The ReafferenceEngine routes it to the availability layer (NOT competence):
31
+ * a refusal must never teach the Will it is unskilled at something it is merely
32
+ * forbidden to do. `finality` decides how hard availability is cut.
33
+ */
34
+ refused?: boolean
35
+ finality?: DenialFinality
36
+ /**
37
+ * ENVELOPE_NARROWING P0 — what WOULD have been permitted, e.g.
38
+ * `{ field: 'amount', requested: 500, allowed: 100 }`.
39
+ *
40
+ * Carried onto the outcome so the tape and the thing the mind actually learns
41
+ * from agree about what happened. Until ENVELOPE_NARROWING P1 lands nothing
42
+ * READS it — that fold is blocked upstream on whether a scalar `allowed` is a
43
+ * ceiling or a floor — but dropping it here is what made the two disagree.
44
+ */
45
+ counterfactual?: PolicyCounterfactual
27
46
  }
28
47
 
29
48
  /**
@@ -71,6 +90,10 @@ export function reconcileInvocation(
71
90
  mode: 'external',
72
91
  tick,
73
92
  reconciled: true,
93
+ ...( result.refused ? { refused: true, finality: result.finality ?? 'parameter' } : {} ),
94
+ // Only when the arbiter actually reported a bound — a refusal without one
95
+ // writes no key at all, so the quiet path is unchanged.
96
+ ...( result.refused && result.counterfactual ? { counterfactual: result.counterfactual } : {} ),
74
97
  ...( provenance.planId ? { planId: provenance.planId } : {} ),
75
98
  ...( provenance.stepId ? { stepId: provenance.stepId } : {} ),
76
99
  },
@@ -26,6 +26,7 @@
26
26
 
27
27
  import type { MotorSchema, LearnedSkill } from '#agency/types'
28
28
  import type { EntityInput, ReadonlySimulationState } from '#core/types'
29
+ import type { DenialFinality } from '#stem/policy/arbiter'
29
30
  import { INNATE_SCHEMAS } from '#agency/schemas/innate'
30
31
 
31
32
  const VALUE_ALPHA = 0.2 // value EMA rate
@@ -39,6 +40,23 @@ const IDLE_TICKS = 200 // ticks of disuse before forgetting starts
39
40
  const DECAY_RATE = 0.02 // habit lost per decay application
40
41
  const DROP_HABIT = 0.05 // below this (and learned) the skill is forgotten
41
42
 
43
+ // ── availability layer (POLICY_REAFFERENCE P2) ────────────────
44
+ // Availability is NOT competence. It answers "may I use this schema", learned
45
+ // from policy refusals, and is kept strictly apart from LearnedSkill so a
46
+ // refusal never teaches the Will it is *unskilled* at something it is merely
47
+ // *forbidden* to do. A `class` refusal ("never, under this policy") drives
48
+ // availability down hard; an `instance` refusal ("not with those parameters")
49
+ // dents it lightly — the Will should keep reaching for the ability, just not
50
+ // that way. Recovery is slow but real, so a policy change is re-discoverable:
51
+ // availability never floors at zero, and it climbs back toward 1 with disuse of
52
+ // the refusal. P2 keys availability by SCHEMA; per-(schema, params) envelope
53
+ // narrowing is a follow-up that belongs at selection time, not fielding time.
54
+ const AVAIL_DROP_CLASS = 0.50 // multiplicative cut on a class-final refusal
55
+ const AVAIL_DROP_PARAMETER = 0.12 // lighter cut when only the arguments were refused
56
+ const AVAIL_FLOOR = 0.05 // never zero — re-probe must always be possible
57
+ const AVAIL_RECOVERY = 0.02 // per-decay climb back toward 1
58
+ const AVAIL_RECOVERED = 0.999 // at/above this the entry is dropped (quiet path)
59
+
42
60
  export interface OutcomeObservation {
43
61
  schema: string
44
62
  success: boolean
@@ -54,6 +72,9 @@ export class SchemaRepertoire {
54
72
  private _skills = new Map<string, LearnedSkill>()
55
73
  /** Tracks which templates were learned at runtime (vs innate) so decay can forget them. */
56
74
  private _learned = new Set<string>()
75
+ /** Availability layer (P2): schema → { value 0..1, lastRefusedTick }. Empty until
76
+ * a refusal lands — a never-refused Will writes nothing here (byte-identical). */
77
+ private _availability = new Map<string, { value: number; lastRefusedTick: number }>()
57
78
 
58
79
  constructor( seed: MotorSchema[] = INNATE_SCHEMAS ){
59
80
  for( const s of seed ) this._templates.set( s.id, s )
@@ -85,6 +106,38 @@ export class SchemaRepertoire {
85
106
  skills(): ReadonlyMap<string, LearnedSkill> { return this._skills }
86
107
  getSkill( id: string ): LearnedSkill | undefined { return this._skills.get( id ) }
87
108
 
109
+ // ── availability (P2) ─────────────────────────────────────────
110
+ availability(): ReadonlyMap<string, { value: number; lastRefusedTick: number }> { return this._availability }
111
+
112
+ /**
113
+ * How available a schema is right now, 0..1. Absent from the ledger ⇒ 1
114
+ * (fully available — the common case). This is the ONLY value the
115
+ * AffordanceSynthesizer reads; it never touches competence.
116
+ */
117
+ availabilityOf( schema: string ): number {
118
+ return this._availability.get( schema )?.value ?? 1
119
+ }
120
+
121
+ /**
122
+ * Fold a policy refusal into the availability layer (NOT competence). A
123
+ * `class` refusal cuts availability hard; a `parameter` refusal dents it
124
+ * lightly. Multiplicative so repeated refusals compound toward — but never
125
+ * reach — zero, keeping re-probe alive.
126
+ *
127
+ * `context` is EXCLUDED FROM THE SIGNATURE, not handled inside: a refusal
128
+ * that was not about the action must never reach the availability layer at
129
+ * all, and making that a type error rather than a convention means a future
130
+ * caller cannot quietly re-introduce the dent. The routing decision lives in
131
+ * the ReafferenceEngine's refused branch (P5).
132
+ */
133
+ recordRefusal( schema: string, finality: Exclude<DenialFinality, 'context'>, tick: number ): number {
134
+ const prev = this._availability.get( schema )?.value ?? 1
135
+ const drop = finality === 'class' ? AVAIL_DROP_CLASS : AVAIL_DROP_PARAMETER
136
+ const value = Math.max( AVAIL_FLOOR, prev * ( 1 - drop ) )
137
+ this._availability.set( schema, { value, lastRefusedTick: tick } )
138
+ return value
139
+ }
140
+
88
141
  /**
89
142
  * Fold one outcome into the schema's learned skill. Returns the updated skill
90
143
  * and whether it just crossed the proceduralization threshold this update.
@@ -118,12 +171,14 @@ export class SchemaRepertoire {
118
171
  }
119
172
 
120
173
  /**
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.
174
+ * Forgetting curve over the competence layer, plus availability recovery.
175
+ * Skills unused for IDLE_TICKS lose habit; learned composites below DROP_HABIT
176
+ * are dropped entirely (template + skill). Availability entries climb back
177
+ * toward 1 and are dropped once fully recovered. Returns the ids that were
178
+ * removed from each layer so their mirrored state entities can be deleted.
124
179
  */
125
- decay( tick: number ): string[] {
126
- const dropped: string[] = []
180
+ decay( tick: number ): { skills: string[]; availability: string[] } {
181
+ const skills: string[] = []
127
182
  for( const [ id, skill ] of this._skills ){
128
183
  if( tick - skill.lastEnactedTick <= IDLE_TICKS ) continue
129
184
 
@@ -132,12 +187,23 @@ export class SchemaRepertoire {
132
187
  this._skills.delete( id )
133
188
  this._templates.delete( id )
134
189
  this._learned.delete( id )
135
- dropped.push( id )
190
+ skills.push( id )
136
191
  continue
137
192
  }
138
193
  this._skills.set( id, { ...skill, habitStrength } )
139
194
  }
140
- return dropped
195
+
196
+ // Availability recovery (P2): each entry climbs slowly back toward 1, so a
197
+ // policy change is re-discoverable. A fully-recovered entry is dropped, so a
198
+ // Will that was refused long ago returns to the byte-identical quiet path.
199
+ const availability: string[] = []
200
+ for( const [ id, avail ] of this._availability ){
201
+ const value = avail.value + AVAIL_RECOVERY * ( 1 - avail.value )
202
+ if( value >= AVAIL_RECOVERED ){ this._availability.delete( id ); availability.push( id ) }
203
+ else this._availability.set( id, { ...avail, value } )
204
+ }
205
+
206
+ return { skills, availability }
141
207
  }
142
208
 
143
209
  // ── PMA portability (Phase 6 reads these) ─────────────────────
@@ -188,6 +254,30 @@ export class SchemaRepertoire {
188
254
  this._learned.add( s.id )
189
255
  }
190
256
  }
257
+
258
+ /** Availability ledger encoded as `agency.availability` state entities (P2).
259
+ * Empty until a refusal lands, so the quiet path writes nothing. */
260
+ availabilityEntities(): EntityInput[] {
261
+ const out: EntityInput[] = []
262
+ for( const [ schema, a ] of this._availability )
263
+ out.push( availabilityEntity( schema, a.value, a.lastRefusedTick ) )
264
+ return out
265
+ }
266
+
267
+ /** Rehydrate the availability ledger from state after a restore. Idempotent;
268
+ * keeps whichever value is more restrictive so a concurrent refusal isn't lost. */
269
+ restoreAvailability( entities: ReadonlySimulationState['entities'] ): void {
270
+ for( const e of entities.values() ){
271
+ if( e.type !== AVAILABILITY_ENTITY_TYPE ) continue
272
+ const m = ( e.metadata ?? {} ) as Record<string, unknown>
273
+ const schema = typeof m['schema'] === 'string' ? m['schema'] : ''
274
+ if( !schema ) continue
275
+ const value = typeof m['value'] === 'number' ? m['value'] : 1
276
+ const tick = typeof m['lastRefusedTick'] === 'number' ? m['lastRefusedTick'] : 0
277
+ const prev = this._availability.get( schema )
278
+ if( !prev || value < prev.value ) this._availability.set( schema, { value, lastRefusedTick: tick } )
279
+ }
280
+ }
191
281
  }
192
282
 
193
283
  // ─── helpers ─────────────────────────────────────────────────────────────────
@@ -209,6 +299,23 @@ function clamp01( n: number ): number {
209
299
  return n < 0 ? 0 : n > 1 ? 1 : n
210
300
  }
211
301
 
302
+ // ─── availability ⇄ state-entity codec (P2) ──────────────────────────────────
303
+
304
+ /** State-entity type for a mirrored availability entry. */
305
+ export const AVAILABILITY_ENTITY_TYPE = 'agency.availability'
306
+
307
+ /** Stable entity id for a schema's availability mirror (idempotent re-writes). */
308
+ export function availabilityEntityId( schema: string ): string { return `agency-availability-${ schema }` }
309
+
310
+ /** Encode one availability entry as a state entity. */
311
+ function availabilityEntity( schema: string, value: number, lastRefusedTick: number ): EntityInput {
312
+ return {
313
+ id: availabilityEntityId( schema ),
314
+ type: AVAILABILITY_ENTITY_TYPE,
315
+ metadata: { schema, value, lastRefusedTick },
316
+ }
317
+ }
318
+
212
319
  // ─── composite ⇄ state-entity codec (snapshot/replay) ────────────────────────
213
320
 
214
321
  /** 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. */
@@ -34,7 +34,7 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
34
34
  engine: 'system',
35
35
  params: {
36
36
  anatomy: config.anatomy ?? 'mind',
37
- model: config.model ?? '',
37
+ model: config.llm?.model ?? '',
38
38
  tickIntervalMs: config.tickIntervalMs ?? 1000
39
39
  }
40
40
  },