dsh-llm-fallbacks 0.6.5 → 0.6.7
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 +60 -231
- package/README.zh-CN.md +77 -248
- package/dist/authorized-route.d.ts +1 -1
- package/dist/index.d.ts +23 -3
- package/dist/index.js +170 -106
- package/dist/role-persona.d.ts +71 -0
- package/dist/role-resolution.d.ts +25 -12
- package/dist/roles.d.ts +6 -4
- package/dist/subagents-seam.d.ts +83 -70
- package/package.json +26 -25
- package/scripts/repair-session-logs.ts +68 -34
- package/scripts/session-logs/catalog.ts +103 -12
- package/scripts/session-logs/publish.ts +23 -9
package/dist/subagents-seam.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dispatch-seam role resolution + per-child record (plan
|
|
3
|
-
* role-based-subagent-adoption Task 1
|
|
3
|
+
* role-based-subagent-adoption Task 1; rc.1 `startActivation` migration, plan
|
|
4
|
+
* dsh-021-subagent-seam-repair Task 2).
|
|
4
5
|
*
|
|
5
6
|
* The seam is the documented cordis service-read waterfall
|
|
6
7
|
* (`Events['internal/get']`, `@deepseek-ai/cordis`
|
|
7
8
|
* `lib/types/events.d.ts`): a listener wraps the `subagents` service VALUE on
|
|
8
|
-
* read and returns a prototype-delegating wrapper whose `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* read and returns a prototype-delegating wrapper whose `startActivation`
|
|
10
|
+
* resolves the dispatch role from the incoming activation spec BEFORE
|
|
11
|
+
* delegating. The real service object is never mutated; readers of the
|
|
11
12
|
* `subagents` service transparently receive the wrapper.
|
|
12
13
|
*
|
|
13
14
|
* Role source (plan Global Constraints): the child request's prompt carries
|
|
@@ -29,34 +30,36 @@
|
|
|
29
30
|
* ambiguity rule stands unchanged: the DISTINCT DECLARED ids must collapse to
|
|
30
31
|
* exactly one, else the dispatch is a no-op — never a first-occurrence pick.
|
|
31
32
|
*
|
|
32
|
-
* Per-child record
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
33
|
+
* Per-child record + the timing rule (rc.1 `startActivation`): the record
|
|
34
|
+
* `{ role, at, firstNoticePending }` is keyed by the child id (Task 3 reads it
|
|
35
|
+
* to emit the one role notice row). The child is created INSIDE the native
|
|
36
|
+
* call — `manager.startLocal` mints/uses `spec.childId` and only then resolves
|
|
37
|
+
* the activation — so `agent/created` fires BEFORE any `.then` on this side
|
|
38
|
+
* could key a record off the resolved `SubagentActivation.childId`. The
|
|
39
|
+
* wrapper therefore reserves the child id itself for a LOCAL backend (the
|
|
40
|
+
* provider exposes `prepareContinuable`; a reserved id on any other backend is
|
|
41
|
+
* rejected with `UNSUPPORTED_CAPABILITY`) and writes the record SYNCHRONOUSLY
|
|
42
|
+
* before delegating, deleting it when the native call fails. An EXTERNAL
|
|
43
|
+
* backend allocates its own child id, receives the caller's OWN spec object
|
|
44
|
+
* untouched, and has its record keyed off the resolved activation's `childId`.
|
|
40
45
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* absent), and every skip path returns the caller's OWN request object, so the
|
|
59
|
-
* native call stays byte-identical.
|
|
46
|
+
* A result carrying no child id — a `jobId` is a registry id, NOT a session id
|
|
47
|
+
* — is never keyed: the child is correlated instead through the parent-owned
|
|
48
|
+
* `subagent/catalog` session event `{ childId, label }` joined on the
|
|
49
|
+
* delegating parent + the activation `label` (the same string the cataloged
|
|
50
|
+
* child membership carries).
|
|
51
|
+
*
|
|
52
|
+
* Persona delivery is NO LONGER this module's job (plan Task 1): rc.1 removed
|
|
53
|
+
* `SubagentStartRequest.persona`, so the resolved role's persona is registered
|
|
54
|
+
* as a scoped `deployment:persona-prefix` section on the CHILD's own context
|
|
55
|
+
* by `./role-persona.ts`, over the SAME ROOT-shared record map this module
|
|
56
|
+
* writes. That listener owns the `personaNotApplied` verdict the notice row
|
|
57
|
+
* reads — EXCEPT when a reservation failure means it can never see the child
|
|
58
|
+
* (the runtime mints its own child id and `agent/created` has already run), in
|
|
59
|
+
* which case this module resolves the same question against the live roles
|
|
60
|
+
* source and carries the verdict onto the record keyed off the RESOLVED id,
|
|
61
|
+
* before the child's first pre-step. This module writes no request field,
|
|
62
|
+
* reads no provider persona capability, and injects no agent preset.
|
|
60
63
|
*
|
|
61
64
|
* In-session notice row (plan Task 3): the SAME install point registers the
|
|
62
65
|
* `agent/pre-step` emitter (`./role-notice.ts`) over the record map it owns, so
|
|
@@ -103,14 +106,14 @@ export interface SubagentSeamRecord {
|
|
|
103
106
|
* The DECLARED RAW role id resolved at the dispatch seam.
|
|
104
107
|
*
|
|
105
108
|
* Authoritative-role precedence (CF-10, documented rather than resolved by
|
|
106
|
-
* re-branding): a
|
|
107
|
-
*
|
|
109
|
+
* re-branding): a resume that reuses a reserved `childId` may resolve a
|
|
110
|
+
* DIFFERENT declared role, and this field is then rewritten while the
|
|
108
111
|
* durable notice row the child's session already carries keeps the FIRST role.
|
|
109
112
|
* The projection deliberately reads the notice row, so the durable role is the
|
|
110
113
|
* child's announced identity and this record is only the LIVE dispatch hint
|
|
111
|
-
* (what the next
|
|
112
|
-
* disagree after a re-dispatch until the child is disposed; no
|
|
113
|
-
* treat the record as the durable role.
|
|
114
|
+
* (what the next activation / the resume fallback would use). The two can
|
|
115
|
+
* therefore disagree after a re-dispatch until the child is disposed; no
|
|
116
|
+
* consumer may treat the record as the durable role.
|
|
114
117
|
*/
|
|
115
118
|
role: string;
|
|
116
119
|
/** When the record was written (epoch ms). */
|
|
@@ -126,20 +129,24 @@ export interface SubagentSeamRecord {
|
|
|
126
129
|
firstNoticePending: boolean;
|
|
127
130
|
/**
|
|
128
131
|
* `true` when the role DECLARES a persona and that persona was NOT delivered
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
132
|
+
* onto the child. Written by the `agent/created` role-persona listener
|
|
133
|
+
* (`./role-persona.ts`) over this same map — except when a reservation
|
|
134
|
+
* failure means that listener can never see the child (the runtime mints its
|
|
135
|
+
* own child id), where the seam itself carries the verdict onto the record
|
|
136
|
+
* keyed off the RESOLVED id. The seam never delivers or reads a persona
|
|
137
|
+
* other than for that one question. Absent when there was nothing to report:
|
|
138
|
+
* the section was registered, a same-name section was already on the child's
|
|
139
|
+
* scope (whoever registered it), the listener never saw the child (an
|
|
140
|
+
* EXTERNAL backend creates no local agent), the role declares none, or no
|
|
141
|
+
* persona source is wired. Task 3 appends ` (persona not applied)` on `true`
|
|
142
|
+
* only.
|
|
134
143
|
*
|
|
135
|
-
* STICKY (Task 3 review Minor 4): a record rewrite
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* child's one notice row is a claim about that dispatch, so the first `true`
|
|
142
|
-
* survives; a later `false` is simply not recorded.
|
|
144
|
+
* STICKY (Task 3 review Minor 4): a record rewrite — a resume reusing the
|
|
145
|
+
* reserved `childId`, or the resolved activation re-keying the same child
|
|
146
|
+
* after `agent/created` already ran — can only ADD this verdict, never clear
|
|
147
|
+
* it. A rewrite carries no verdict of its own, and the child's one notice row
|
|
148
|
+
* is a claim about the dispatch that already reported it missing, so the first
|
|
149
|
+
* `true` survives.
|
|
143
150
|
*/
|
|
144
151
|
personaNotApplied?: boolean;
|
|
145
152
|
}
|
|
@@ -149,29 +156,29 @@ export interface SubagentPromptBlockView {
|
|
|
149
156
|
readonly text?: string;
|
|
150
157
|
}
|
|
151
158
|
/**
|
|
152
|
-
* Structural view of the
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* other field reaches the service unchanged.
|
|
159
|
+
* Structural view of the activation request (consumed fields only): `prompt` is
|
|
160
|
+
* the role-extraction source and `parent` is the catalog-correlation join half.
|
|
161
|
+
* The wrapper forwards the request OBJECT itself, so every other field (cwd,
|
|
162
|
+
* agentOptions, outputSchema, maxDepth, the signal) reaches the service
|
|
163
|
+
* unchanged.
|
|
158
164
|
*/
|
|
159
165
|
export interface SubagentStartRequestView {
|
|
160
166
|
readonly prompt?: readonly SubagentPromptBlockView[];
|
|
161
|
-
readonly label?: string;
|
|
162
167
|
readonly parent?: {
|
|
163
168
|
readonly session?: {
|
|
164
169
|
readonly id?: string;
|
|
165
170
|
};
|
|
166
171
|
};
|
|
167
|
-
readonly persona?: string;
|
|
168
172
|
}
|
|
169
173
|
/**
|
|
170
|
-
* Structural view of the
|
|
171
|
-
* `childId` is the caller-reserved identity a resume
|
|
172
|
-
* the record fallback when the
|
|
174
|
+
* Structural view of the activation spec (rc.1 `SubagentActivationSpec`,
|
|
175
|
+
* consumed fields only): `childId` is the caller-reserved identity a resume
|
|
176
|
+
* already carries — also the record fallback when the request carries no
|
|
177
|
+
* Assignment header — and `label` is the catalog join's other half (the
|
|
178
|
+
* cataloged child membership carries this exact string). `signal`, `delivery`
|
|
179
|
+
* and every future field travel with the caller's spec object.
|
|
173
180
|
*/
|
|
174
|
-
export interface
|
|
181
|
+
export interface SubagentActivationSpecView {
|
|
175
182
|
readonly provider: string;
|
|
176
183
|
readonly label?: string;
|
|
177
184
|
readonly childId?: string;
|
|
@@ -180,14 +187,16 @@ export interface ContinuableStartSpecView {
|
|
|
180
187
|
/** Options for {@link installSubagentSeam}. */
|
|
181
188
|
export interface SubagentSeamOptions {
|
|
182
189
|
/**
|
|
183
|
-
* Live declared-role id map (trimmed id → DECLARED RAW id), read per
|
|
184
|
-
* so a settings change is observed without a re-install.
|
|
190
|
+
* Live declared-role id map (trimmed id → DECLARED RAW id), read per
|
|
191
|
+
* activation so a settings change is observed without a re-install.
|
|
185
192
|
*/
|
|
186
193
|
roleIds: () => ReadonlyMap<string, string>;
|
|
187
194
|
/**
|
|
188
|
-
* Live declared roles (`roles.list`) — the
|
|
189
|
-
*
|
|
190
|
-
* seam only
|
|
195
|
+
* Live declared roles (`roles.list`) — the SAME source `./role-persona.ts`
|
|
196
|
+
* reads. Consulted ONLY when a reservation failure means that listener can
|
|
197
|
+
* never see the child: the seam is then the only place left that can carry
|
|
198
|
+
* the persona verdict to the child's notice row. Absent (no persona source is
|
|
199
|
+
* wired) ⇒ no verdict, exactly like a listener that never saw the child.
|
|
191
200
|
*/
|
|
192
201
|
roles?: () => readonly FallbacksRole[];
|
|
193
202
|
/** Contained debug sink (at most one line per contained degrade). */
|
|
@@ -196,10 +205,12 @@ export interface SubagentSeamOptions {
|
|
|
196
205
|
/** The installed seam (Task 3 surface + explicit teardown). */
|
|
197
206
|
export interface SubagentSeam {
|
|
198
207
|
/**
|
|
199
|
-
* Per-child records keyed by the child
|
|
200
|
-
*
|
|
208
|
+
* Per-child records keyed by the child id — the reserved id this wrapper
|
|
209
|
+
* wrote before a local activation, or the `childId` a resolved activation
|
|
210
|
+
* reported. ROOT-shared (CF-5): every fiber applied over the same root reads
|
|
201
211
|
* the same map. Mutable by design: Task 3 clears `firstNoticePending` after
|
|
202
|
-
* emitting the notice
|
|
212
|
+
* emitting the notice, and the role-persona listener writes
|
|
213
|
+
* `personaNotApplied`.
|
|
203
214
|
*/
|
|
204
215
|
readonly records: Map<string, SubagentSeamRecord>;
|
|
205
216
|
/**
|
|
@@ -253,7 +264,9 @@ export declare const PENDING_CORRELATION_LIMIT = 64;
|
|
|
253
264
|
*/
|
|
254
265
|
export declare function resolveDeclaredRoleFromAssignment(prompt: string, roleIds: ReadonlyMap<string, string>): string | undefined;
|
|
255
266
|
/**
|
|
256
|
-
* PURE, chain-independent persona lookup
|
|
267
|
+
* PURE, chain-independent persona lookup for the `agent/created` role-persona
|
|
268
|
+
* listener (`./role-persona.ts`; rc.1 delivers the persona as a scoped prompt
|
|
269
|
+
* section on the child, never as a request field):
|
|
257
270
|
* the DECLARED `persona` of `roleId`, trimmed, and only when it is still
|
|
258
271
|
* non-empty after the trim — a blank persona is NO persona. `roleId` is the
|
|
259
272
|
* DECLARED RAW id the seam already resolved, so this is an exact id match, not
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"dsh",
|
|
6
6
|
"dsh-plugin"
|
|
7
7
|
],
|
|
8
|
-
"version": "0.6.
|
|
8
|
+
"version": "0.6.7",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
@@ -64,30 +64,31 @@
|
|
|
64
64
|
"use-sync-external-store": "^1.6.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
68
|
-
"@deepseek-ai/dsh-agent": "^0.2.
|
|
69
|
-
"@deepseek-ai/dsh-api-gateway": "^0.2.
|
|
70
|
-
"@deepseek-ai/dsh-api-remotes": "^0.2.
|
|
71
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.2.
|
|
72
|
-
"@deepseek-ai/dsh-client-connection": "^0.2.
|
|
73
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.
|
|
74
|
-
"@deepseek-ai/dsh-client-store": "^0.2.
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-chat": "^0.2.
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.
|
|
81
|
-
"@deepseek-ai/dsh-commands": "^0.2.
|
|
82
|
-
"@deepseek-ai/dsh-llm": "^0.2.
|
|
83
|
-
"@deepseek-ai/dsh-llm-retry": "^0.2.
|
|
84
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
85
|
-
"@deepseek-ai/dsh-session-projection": "^0.2.
|
|
86
|
-
"@deepseek-ai/dsh-settings": "^0.2.
|
|
87
|
-
"@deepseek-ai/dsh-
|
|
88
|
-
"@deepseek-ai/dsh-
|
|
89
|
-
"@deepseek-ai/dsh-typert-
|
|
90
|
-
"@deepseek-ai/
|
|
67
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
68
|
+
"@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-api-gateway": "^0.2.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-store": "^0.2.1-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-chat": "^0.2.1-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.1-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-commands": "^0.2.1-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-llm-retry": "^0.2.1-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-session": "^0.2.1-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-session-projection": "^0.2.1-rc.2",
|
|
86
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
|
|
87
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-tool-subagent": "^0.2.1-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.2.1-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-typert-registry": "^0.2.1-rc.2",
|
|
91
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
91
92
|
"react": "^18.2.0"
|
|
92
93
|
},
|
|
93
94
|
"devDependencies": {
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* and — only for the classes whose normalization is provably shape-preserving
|
|
10
10
|
* (the rule registry in `./session-logs/rules.ts`) and only with a resolved
|
|
11
11
|
* released catalog as the oracle (`./session-logs/catalog.ts`) — publishes a
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* REPAIR-FLOOR (v3) successor beside the original (`./session-logs/publish.ts`).
|
|
13
|
+
* The original generation is never modified. A newer installed catalog supplies
|
|
14
|
+
* that floor through its own `historicalSessionFormatCatalog`; the host migrates
|
|
15
|
+
* the published v3 generation onward when it opens the session, with its own
|
|
16
|
+
* historical child evidence (the V3→V4 edge needs evidence no single-log repair
|
|
17
|
+
* can produce — see `CatalogHandle.floor`).
|
|
14
18
|
*
|
|
15
19
|
* This module CONSUMES the frozen Task 1/2 modules; it re-implements none of
|
|
16
20
|
* them. The refusal vocabulary, the rule registry, the oracle resolution and
|
|
@@ -134,8 +138,8 @@
|
|
|
134
138
|
* at all (`skipped`), or a stale `session.repair.*.jsonl.zstd.tmp` was found under
|
|
135
139
|
* the root; 2 = fatal (bad arguments, missing/unreadable
|
|
136
140
|
* `--root`, `--apply` without a resolved catalog, a catalog below the required
|
|
137
|
-
* format version
|
|
138
|
-
* without zstd).
|
|
141
|
+
* format version or a newer catalog without a usable repair floor,
|
|
142
|
+
* `--apply --drop-legacy-events` without `--backup`, or a runtime without zstd).
|
|
139
143
|
*/
|
|
140
144
|
import { createHash } from 'node:crypto'
|
|
141
145
|
import { constants as fsConstants } from 'node:fs'
|
|
@@ -149,6 +153,7 @@ import {
|
|
|
149
153
|
REQUIRED_CATALOG_VERSION,
|
|
150
154
|
catalogVersionRefusal,
|
|
151
155
|
classifyWithCatalog,
|
|
156
|
+
repairCatalog,
|
|
152
157
|
resolveCatalog,
|
|
153
158
|
restoreRows,
|
|
154
159
|
} from './session-logs/catalog.ts'
|
|
@@ -733,6 +738,12 @@ export interface RunResult {
|
|
|
733
738
|
packageVersion: string | null
|
|
734
739
|
/** The format version that catalog declares as current. */
|
|
735
740
|
currentVersion: number
|
|
741
|
+
/**
|
|
742
|
+
* The format version this tool REPAIRS TO (the floor): the declared
|
|
743
|
+
* `currentVersion` on a build at the floor, otherwise that build's
|
|
744
|
+
* historical V0–V3 catalog. Successors are published at this version.
|
|
745
|
+
*/
|
|
746
|
+
repairVersion: number
|
|
736
747
|
}
|
|
737
748
|
| { resolved: false }
|
|
738
749
|
logs: LogOutcome[]
|
|
@@ -801,8 +812,11 @@ function strictRefusalOf(rows: readonly ParsedRow[], catalog: CatalogHandle | nu
|
|
|
801
812
|
// differs from this one exactly by `recovery`, so a refusal found here can only
|
|
802
813
|
// mean the rows after it are dropped — not merely that the session is not
|
|
803
814
|
// current-shaped. Using the publisher's `validation: 'current'` axis here would
|
|
804
|
-
// over-claim truncation for a log that loses no rows at all.
|
|
805
|
-
|
|
815
|
+
// over-claim truncation for a log that loses no rows at all. The restore goes
|
|
816
|
+
// through the REPAIR FLOOR (the pre-V3 chain this tool repairs along and the
|
|
817
|
+
// generation it publishes); the host's floor→current migration needs
|
|
818
|
+
// corpus-level child evidence and is outside a single log's verdict.
|
|
819
|
+
restoreRows(repairCatalog(catalog), structuredClone(rows), { recovery: 'strict', validation: 'transformed' })
|
|
806
820
|
return null
|
|
807
821
|
} catch (error) {
|
|
808
822
|
const cause = error instanceof Error ? error.cause : undefined
|
|
@@ -1004,7 +1018,10 @@ function lossyDropRefusal(
|
|
|
1004
1018
|
try {
|
|
1005
1019
|
// A defensive copy: the released restore normalizes a packed Assistant run's
|
|
1006
1020
|
// stream IN PLACE, and `repaired` is what the publisher is about to write.
|
|
1007
|
-
restoreRows(catalog
|
|
1021
|
+
restoreRows(repairCatalog(catalog), structuredClone(repaired), {
|
|
1022
|
+
recovery: 'strict',
|
|
1023
|
+
validation: 'transformed',
|
|
1024
|
+
})
|
|
1008
1025
|
} catch (error) {
|
|
1009
1026
|
return {
|
|
1010
1027
|
ok: false,
|
|
@@ -1228,23 +1245,25 @@ async function analyzeLog(candidate: LogGeneration, context: InspectContext): Pr
|
|
|
1228
1245
|
}
|
|
1229
1246
|
const renumbered = verdict === null ? 0 : verdict.renumberedEventCount
|
|
1230
1247
|
|
|
1231
|
-
// The successor NAME needs the
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1248
|
+
// The successor NAME needs the repair floor's version, so it is only known with
|
|
1249
|
+
// a resolved oracle: in report mode without one nothing is probed (the log is
|
|
1250
|
+
// still reported `repairable` from the rule registry alone), and `--apply` was
|
|
1251
|
+
// already refused as fatal by the caller — which is why the null case is checked
|
|
1252
|
+
// here rather than assumed away.
|
|
1236
1253
|
//
|
|
1237
1254
|
// The name comes from the publisher's own exported rule (no local copy). Note
|
|
1238
|
-
// the two INPUTS differ: this probe uses the
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
// settles at the
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1255
|
+
// the two INPUTS differ: this probe uses the handle's REPAIR FLOOR version,
|
|
1256
|
+
// while `publishSuccessor` derives its target from the version the RESTORED
|
|
1257
|
+
// header settles on. They agree for every released catalog (the floor catalog's
|
|
1258
|
+
// restore settles at the floor it declares, and the publisher pins the
|
|
1259
|
+
// successor to it before publication); a catalog whose restore settled at a
|
|
1260
|
+
// different version would leave this probe looking for a name the publication
|
|
1261
|
+
// never used, so report mode alone could call such a log unpublished while
|
|
1262
|
+
// `--apply` accepts the existing successor as byte-identical.
|
|
1246
1263
|
const catalog = context.catalog
|
|
1247
|
-
const expected = catalog === null
|
|
1264
|
+
const expected = catalog === null
|
|
1265
|
+
? null
|
|
1266
|
+
: context.successorFilename(repairCatalog(catalog).currentVersion)
|
|
1248
1267
|
const successorPath = expected === null ? null : join(candidate.sessionDir, expected)
|
|
1249
1268
|
const existingSuccessor =
|
|
1250
1269
|
successorPath !== null && (await fileExists(successorPath)) ? successorPath : null
|
|
@@ -1493,10 +1512,11 @@ export async function runRepair(
|
|
|
1493
1512
|
throw new FatalError(messageOf(error))
|
|
1494
1513
|
}
|
|
1495
1514
|
}
|
|
1496
|
-
// The oracle is pinned to a RELEASE, in every mode: the
|
|
1497
|
-
// successor and reads it back, so "it accepts its own output" is only
|
|
1498
|
-
// when the
|
|
1499
|
-
// floor
|
|
1515
|
+
// The oracle is pinned to a RELEASE, in every mode: the floor catalog encodes
|
|
1516
|
+
// the successor and reads it back, so "it accepts its own output" is only
|
|
1517
|
+
// evidence when the floor comes from the build that will read the session. A
|
|
1518
|
+
// catalog below the floor, or a newer build with no usable floor, is a fatal
|
|
1519
|
+
// (exit 2), not a quieter classification.
|
|
1500
1520
|
const catalogProblem = catalog === null ? null : catalogVersionRefusal(catalog)
|
|
1501
1521
|
if (catalogProblem !== null) throw new FatalError(catalogProblem)
|
|
1502
1522
|
|
|
@@ -1510,6 +1530,7 @@ export async function runRepair(
|
|
|
1510
1530
|
resolvedBy: catalog.resolvedBy,
|
|
1511
1531
|
packageVersion: catalog.packageVersion,
|
|
1512
1532
|
currentVersion: catalog.catalog.currentVersion,
|
|
1533
|
+
repairVersion: repairCatalog(catalog).currentVersion,
|
|
1513
1534
|
}
|
|
1514
1535
|
|
|
1515
1536
|
const reportRules = policyRules(options.dropLegacyEvents)
|
|
@@ -1741,6 +1762,9 @@ function reportHead(head: ReportHead, io: CliIO): void {
|
|
|
1741
1762
|
io.out(
|
|
1742
1763
|
head.catalog.resolved
|
|
1743
1764
|
? `catalog: ${head.catalog.modulePath} (v${head.catalog.currentVersion},`
|
|
1765
|
+
+ `${head.catalog.repairVersion === head.catalog.currentVersion
|
|
1766
|
+
? ''
|
|
1767
|
+
: ` repair target v${head.catalog.repairVersion},`}`
|
|
1744
1768
|
+ ` package ${head.catalog.packageVersion ?? 'version unknown'}, resolved by ${head.catalog.resolvedBy})`
|
|
1745
1769
|
: `catalog: none resolved — classification is STRUCTURAL ONLY and --apply would refuse `
|
|
1746
1770
|
+ `(no --catalog, no ${CATALOG_ENV_VAR}, no dsh on PATH, no npx install)`,
|
|
@@ -1884,15 +1908,19 @@ export function usage(): string {
|
|
|
1884
1908
|
Triage (default, read-only) and repair (--apply) pre-V3 dsh session logs. A log is
|
|
1885
1909
|
refused when the FROZEN released migration chain cannot classify it; this tool names
|
|
1886
1910
|
the refusal class and, for the classes whose normalization is provably
|
|
1887
|
-
shape-preserving, publishes a
|
|
1888
|
-
original generation is never modified and never truncated.
|
|
1911
|
+
shape-preserving, publishes a REPAIR-FLOOR (v${CURRENT_VERSION_FLOOR}) successor generation
|
|
1912
|
+
beside the original. The original generation is never modified and never truncated.
|
|
1913
|
+
The floor stays v${CURRENT_VERSION_FLOOR} even when the installed catalog is newer: the next
|
|
1914
|
+
released edge (v${CURRENT_VERSION_FLOOR}->v${CURRENT_VERSION_FLOOR + 1}) needs historical child evidence a
|
|
1915
|
+
single-log repair cannot produce, so the host migrates the published v${CURRENT_VERSION_FLOOR}
|
|
1916
|
+
generation onward when it opens the session.
|
|
1889
1917
|
|
|
1890
1918
|
--root DIR session root to walk (default: $DSH_HOME or ~/.dsh, then /sessions).
|
|
1891
1919
|
Every <namespace>/<session>/ directory's newest canonical generation
|
|
1892
1920
|
below format v${CURRENT_VERSION_FLOOR} is considered; noncanonical names (staged
|
|
1893
|
-
.tmp files, .bak copies) are ignored, and a
|
|
1894
|
-
present beside it is reported as an already published
|
|
1895
|
-
by a read-back) instead of being a repair target.
|
|
1921
|
+
.tmp files, .bak copies) are ignored, and a repair-floor generation
|
|
1922
|
+
already present beside it is reported as an already published
|
|
1923
|
+
successor (proven by a read-back) instead of being a repair target.
|
|
1896
1924
|
A namespace/session directory that cannot be read, a canonical
|
|
1897
1925
|
generation BELOW the format floor that is a symlink or not a regular
|
|
1898
1926
|
file, and a stale session.repair.*.jsonl.zstd.tmp are REPORTED (a
|
|
@@ -1903,7 +1931,8 @@ original generation is never modified and never truncated.
|
|
|
1903
1931
|
Symlinks are reported, never followed: a repair writes beside the
|
|
1904
1932
|
generation it repairs, which must stay inside --root.
|
|
1905
1933
|
--apply run the rules' proofs and publish a successor generation per repaired
|
|
1906
|
-
log. Requires a resolved catalog.
|
|
1934
|
+
log, at the repair floor (v${CURRENT_VERSION_FLOOR}). Requires a resolved catalog.
|
|
1935
|
+
Default: read-only report.
|
|
1907
1936
|
The revision this run DECODED is digest-checked against the file before
|
|
1908
1937
|
anything is staged, so a concurrent append is refused before the first
|
|
1909
1938
|
write; if the source moves after a publication this run CREATED, that
|
|
@@ -1924,9 +1953,14 @@ original generation is never modified and never truncated.
|
|
|
1924
1953
|
resolves, then the newest ~/.npm/_npx install. The resolved module is
|
|
1925
1954
|
EXECUTED, not parsed (the same privilege as running this tool), and it
|
|
1926
1955
|
must declare currentVersion >= ${CURRENT_VERSION_FLOOR}: a below-floor catalog is
|
|
1927
|
-
refused with exit 2 rather than trusted to verify its own output.
|
|
1928
|
-
|
|
1929
|
-
|
|
1956
|
+
refused with exit 2 rather than trusted to verify its own output. When
|
|
1957
|
+
currentVersion is ABOVE the floor, the module must also export
|
|
1958
|
+
historicalSessionFormatCatalog (that build's V0-V${CURRENT_VERSION_FLOOR} reader/encoder):
|
|
1959
|
+
the floor catalog restores and verifies the published v${CURRENT_VERSION_FLOOR} successor
|
|
1960
|
+
and the host migrates it onward with its own child evidence; a build
|
|
1961
|
+
without a usable floor is refused with exit 2. A file candidate whose
|
|
1962
|
+
owning package.json names a different package is refused too. The
|
|
1963
|
+
report names the module path, its format version, the repair target and
|
|
1930
1964
|
its package version.
|
|
1931
1965
|
--backup copy the original generation to <name>.bak before publishing. A run
|
|
1932
1966
|
that ends up publishing nothing removes the copy IT created again,
|