@linxin666/dsh-pet 0.1.11 → 0.1.13
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.i18n.yaml +6 -0
- package/README.md +57 -56
- package/README.zh.md +109 -0
- package/lib/client.js +72 -54
- package/lib/client.js.map +1 -1
- package/lib/index.js +56 -35
- package/lib/invariant.js +1 -1
- package/lib/{state-IyVnKymD.js → state-C9EyycwI.js} +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/WhalePet.d.ts.map +1 -1
- package/lib/types/client/WhalePet.js +6 -0
- package/lib/types/client/index.d.ts +13 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +2 -1
- package/lib/types/service.d.ts +12 -8
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +48 -32
- package/lib/types/state.d.ts +7 -7
- package/lib/types/state.d.ts.map +1 -1
- package/lib/types/state.js +5 -5
- package/lib/types/treats.d.ts +6 -2
- package/lib/types/treats.d.ts.map +1 -1
- package/lib/types/treats.js +14 -3
- package/package.json +2 -12
- package/src/client/PluginSettingsCard.tsx +30 -5
- package/src/client/WhalePet.test.tsx +138 -0
- package/src/client/WhalePet.tsx +5 -0
- package/src/client/index.ts +15 -2
- package/src/client/settings-card.module.css +141 -127
- package/src/service.ts +48 -36
- package/src/state.ts +7 -7
- package/src/treats.test.ts +25 -2
- package/src/treats.ts +15 -3
package/src/service.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pet host service — the `pet.*` RPC domain. Owns the state machine wiring
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
5
|
-
* this service's methods onto
|
|
6
|
-
* `pet.
|
|
3
|
+
* (maps core rc.6 session events — turn/step/tool boundaries — and the
|
|
4
|
+
* session lifecycle onto the pet phases), the affinity ledger, and the
|
|
5
|
+
* persisted display config. The API gateway maps this service's methods onto
|
|
6
|
+
* `pet.state` / `pet.interact` / `pet.setVisible` / `pet.setConfig`
|
|
7
|
+
* for browser consumers.
|
|
7
8
|
* @module @linxin666/dsh-pet/service
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
11
|
-
import type { Session } from '@deepseek-ai/dsh-session'
|
|
12
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
12
13
|
import {
|
|
13
14
|
applyInteraction,
|
|
14
15
|
applyTurnReward,
|
|
@@ -127,13 +128,6 @@ declare module '@deepseek-ai/cordis' {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
/** One session/event guard: only the latest activity snapshot matters. */
|
|
131
|
-
interface ActivityStatusEventLike {
|
|
132
|
-
phase?: string
|
|
133
|
-
line?: string
|
|
134
|
-
phrase?: string
|
|
135
|
-
}
|
|
136
|
-
|
|
137
131
|
/**
|
|
138
132
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
139
133
|
* written until a query or interaction arrives; event listeners update only
|
|
@@ -148,7 +142,8 @@ export class PetService extends Service {
|
|
|
148
142
|
private readonly treatConfig: TreatConfig
|
|
149
143
|
private readonly persistDir: string
|
|
150
144
|
private persist: PetPersist
|
|
151
|
-
|
|
145
|
+
/** Completed turns already rewarded, per session (turn numbers are per-session). */
|
|
146
|
+
private rewardedTurns = new Map<string, number>()
|
|
152
147
|
private enabled: boolean
|
|
153
148
|
private disposeActivity: (() => void) | undefined
|
|
154
149
|
|
|
@@ -201,20 +196,36 @@ export class PetService extends Service {
|
|
|
201
196
|
if (!this.enabled) return
|
|
202
197
|
this.disposeActivity = (() => {
|
|
203
198
|
const disposers = [
|
|
204
|
-
this.ctx.on('session/event', (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
199
|
+
this.ctx.on('session/event', (session: Session, event: SessionEvent) => {
|
|
200
|
+
// rc.6 publishes no 'activity/status' event (the working-activity
|
|
201
|
+
// tracker is gone), so the pet derives its phases from the core
|
|
202
|
+
// session vocabulary instead.
|
|
203
|
+
switch (event.type) {
|
|
204
|
+
case 'turn/start':
|
|
205
|
+
this.machine.onSessionActive()
|
|
206
|
+
break
|
|
207
|
+
case 'step/start':
|
|
208
|
+
this.machine.onSessionActive()
|
|
209
|
+
this.machine.onActivityStatus({ phase: 'thinking' })
|
|
210
|
+
break
|
|
211
|
+
case 'tool/call':
|
|
212
|
+
this.machine.onSessionActive()
|
|
213
|
+
this.machine.onActivityStatus({ phase: 'tool', line: 'tool: ' + event.data.name })
|
|
214
|
+
break
|
|
215
|
+
case 'turn/end':
|
|
216
|
+
this.machine.onSessionActive()
|
|
217
|
+
if (event.data.reason.kind === 'completed') {
|
|
218
|
+
this.machine.onActivityStatus({ phase: 'done' })
|
|
219
|
+
this.rewardTurn(String(session.id), event.data.turn)
|
|
220
|
+
} else {
|
|
221
|
+
// Aborted / failed turns clear the working pose instead of
|
|
222
|
+
// freezing the pet on its last phase.
|
|
223
|
+
this.machine.onActivityStatus({ phase: 'idle' })
|
|
224
|
+
}
|
|
225
|
+
break
|
|
226
|
+
default:
|
|
227
|
+
break
|
|
228
|
+
}
|
|
218
229
|
}),
|
|
219
230
|
this.ctx.on('session/disposed', () => {
|
|
220
231
|
this.machine.onSessionDisposed()
|
|
@@ -318,19 +329,20 @@ export class PetService extends Service {
|
|
|
318
329
|
})
|
|
319
330
|
}
|
|
320
331
|
|
|
321
|
-
/** Award the turn reward once per
|
|
322
|
-
private rewardTurn(): void {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
this.lastTurnRewardAt = nowMs
|
|
332
|
+
/** Award the turn reward once per completed turn (idempotent per session + turn). */
|
|
333
|
+
private rewardTurn(sessionId: string, turn: number): void {
|
|
334
|
+
const last = this.rewardedTurns.get(sessionId) ?? 0
|
|
335
|
+
if (turn <= last) return
|
|
336
|
+
this.rewardedTurns.set(sessionId, turn)
|
|
327
337
|
this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) }
|
|
328
338
|
this.flush()
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
/**
|
|
332
|
-
* Settle the treat economy (work + time output since the last
|
|
333
|
-
*
|
|
342
|
+
* Settle the treat economy (work + time output since the last settlement)
|
|
343
|
+
* and persist whenever the ledger changed. A zero-gain first settlement
|
|
344
|
+
* still starts the time clock (anchor write), which is what lets the
|
|
345
|
+
* 30-minute time output ever accrue.
|
|
334
346
|
*/
|
|
335
347
|
private settleTreats(nowMs: number): void {
|
|
336
348
|
const settlement = settleTreatGrants(
|
|
@@ -339,7 +351,7 @@ export class PetService extends Service {
|
|
|
339
351
|
nowMs,
|
|
340
352
|
this.treatConfig,
|
|
341
353
|
)
|
|
342
|
-
if (settlement.
|
|
354
|
+
if (settlement.ledger !== this.persist.treats) {
|
|
343
355
|
this.persist = { ...this.persist, treats: settlement.ledger }
|
|
344
356
|
this.flush()
|
|
345
357
|
}
|
package/src/state.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet state machine — pure, clock-injected. Maps the
|
|
3
|
-
*
|
|
4
|
-
* animation contract, plus the session lifecycle
|
|
5
|
-
* exposes (turn end celebration, no-session idle).
|
|
2
|
+
* Pet state machine — pure, clock-injected. Maps the pet's working-phase
|
|
3
|
+
* vocabulary (the service derives it from core session events) onto the
|
|
4
|
+
* 9-state Codex pet animation contract, plus the session lifecycle
|
|
5
|
+
* transitions the web UI exposes (turn end celebration, no-session idle).
|
|
6
6
|
*
|
|
7
7
|
* The machine is deliberately dumb: it holds the last input phase, the
|
|
8
8
|
* animation decision, and a one-shot "celebration" window after `done` so the
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module @linxin666/dsh-pet/state
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/** The
|
|
14
|
+
/** The pet's working-phase vocabulary (derived from core session events by the service). */
|
|
15
15
|
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'done'
|
|
16
16
|
|
|
17
17
|
/** The Codex-compatible 9-state animation contract (spritesheet rows). */
|
|
@@ -28,7 +28,7 @@ export type PetAnimation =
|
|
|
28
28
|
|
|
29
29
|
/** One input snapshot consumed by the machine. */
|
|
30
30
|
export interface PetStateInput {
|
|
31
|
-
/** Current
|
|
31
|
+
/** Current working phase of the active session. */
|
|
32
32
|
phase: ActivityPhase
|
|
33
33
|
/** Human-readable status line (plain text). */
|
|
34
34
|
line?: string
|
|
@@ -110,7 +110,7 @@ export class PetStateMachine {
|
|
|
110
110
|
private readonly now: () => number = Date.now,
|
|
111
111
|
) {}
|
|
112
112
|
|
|
113
|
-
/** Consume one
|
|
113
|
+
/** Consume one phase snapshot (fed by the service from session events). */
|
|
114
114
|
onActivityStatus(input: PetStateInput): void {
|
|
115
115
|
this.phase = input.phase
|
|
116
116
|
this.line = input.line
|
package/src/treats.test.ts
CHANGED
|
@@ -23,11 +23,34 @@ describe('settleTreatGrants', () => {
|
|
|
23
23
|
expect(s.ledger.lastTreatGrantAt).toBe(1_000 + 90 * 60_000)
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it('does not backfill time output before the first settlement', () => {
|
|
26
|
+
it('does not backfill time output before the first settlement, but starts the clock', () => {
|
|
27
27
|
const ledger = emptyTreatLedger() // lastTreatGrantAt === 0
|
|
28
28
|
const s = settleTreatGrants(ledger, 0, 1_000 + 10 * 60 * 60_000, defaultTreatConfig)
|
|
29
29
|
expect(s.gained).toBe(0)
|
|
30
|
-
expect(s.ledger).toBe(
|
|
30
|
+
expect(s.ledger.treats).toBe(0)
|
|
31
|
+
expect(s.ledger.lastTreatGrantAt).toBe(1_000 + 10 * 60 * 60_000)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('starts the time clock on a zero-gain settlement so later time output accrues (anchor deadlock)', () => {
|
|
35
|
+
// Regression for issue #99: the old code returned the ledger untouched
|
|
36
|
+
// when nothing was due, so lastTreatGrantAt stayed 0 forever and the
|
|
37
|
+
// 30-minute time output could never begin.
|
|
38
|
+
let ledger = emptyTreatLedger()
|
|
39
|
+
const first = settleTreatGrants(ledger, 0, 1_000, defaultTreatConfig)
|
|
40
|
+
expect(first.gained).toBe(0)
|
|
41
|
+
expect(first.ledger.lastTreatGrantAt).toBe(1_000)
|
|
42
|
+
expect(first.ledger.treats).toBe(0)
|
|
43
|
+
ledger = first.ledger
|
|
44
|
+
// Anchored and nothing due: the same object comes back (no persistence
|
|
45
|
+
// churn) and the anchor must not move.
|
|
46
|
+
const same = settleTreatGrants(ledger, 0, 1_000 + 10_000, defaultTreatConfig)
|
|
47
|
+
expect(same.gained).toBe(0)
|
|
48
|
+
expect(same.ledger).toBe(ledger)
|
|
49
|
+
// One full period after the anchor: the time treat finally lands.
|
|
50
|
+
const grant = settleTreatGrants(ledger, 0, 1_000 + defaultTreatConfig.timeTreatMs, defaultTreatConfig)
|
|
51
|
+
expect(grant.gained).toBe(1)
|
|
52
|
+
expect(grant.ledger.treats).toBe(1)
|
|
53
|
+
expect(grant.ledger.lastTreatGrantAt).toBe(1_000 + defaultTreatConfig.timeTreatMs)
|
|
31
54
|
})
|
|
32
55
|
|
|
33
56
|
it('caps stocked treats at maxTreats', () => {
|
package/src/treats.ts
CHANGED
|
@@ -58,8 +58,12 @@ function cap(treats: number, max: number): number {
|
|
|
58
58
|
* time output counts whole periods since the time anchor
|
|
59
59
|
* (`lastTreatGrantAt`) and advances only the time anchor. The two sources
|
|
60
60
|
* are independent so a continuously working user still earns time treats.
|
|
61
|
-
* 0 time history never backfills — the clock starts at the first settlement
|
|
62
|
-
*
|
|
61
|
+
* 0 time history never backfills — the clock starts at the first settlement,
|
|
62
|
+
* and even a zero-gain first settlement writes the time anchor so the next
|
|
63
|
+
* elapsed period can accrue (anchor deadlock fix). Both sources are clamped
|
|
64
|
+
* by the stock cap. When the anchor is already set and nothing is due, the
|
|
65
|
+
* input ledger is returned unchanged (same object), so callers can skip
|
|
66
|
+
* persistence cheaply.
|
|
63
67
|
*/
|
|
64
68
|
export function settleTreatGrants(
|
|
65
69
|
ledger: TreatLedger,
|
|
@@ -74,7 +78,15 @@ export function settleTreatGrants(
|
|
|
74
78
|
const timeAnchor = ledger.lastTreatGrantAt === 0 ? nowMs : ledger.lastTreatGrantAt
|
|
75
79
|
const timeGrants = Math.floor(Math.max(0, nowMs - timeAnchor) / config.timeTreatMs)
|
|
76
80
|
const gained = workGrants + timeGrants
|
|
77
|
-
if (gained <= 0)
|
|
81
|
+
if (gained <= 0) {
|
|
82
|
+
if (ledger.lastTreatGrantAt === 0) {
|
|
83
|
+
// Zero-gain first settlement: persist the clock start anyway, so the
|
|
84
|
+
// 30-minute time output can begin. Before this fix the anchor stayed 0
|
|
85
|
+
// forever (the deadlock: no grant means no anchor write means no grant).
|
|
86
|
+
return { ledger: { ...ledger, lastTreatGrantAt: nowMs }, gained: 0 }
|
|
87
|
+
}
|
|
88
|
+
return { ledger, gained: 0 }
|
|
89
|
+
}
|
|
78
90
|
return {
|
|
79
91
|
ledger: {
|
|
80
92
|
treats: cap(ledger.treats + gained, config.maxTreats),
|