dsh-ptc-plus 0.4.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +61 -123
- package/README.md +61 -123
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.cjs +42336 -0
- package/cordis.patch.yml +13 -2
- package/docs/README.md +7 -2
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
- package/docs/adr/0008-define-agent-operating-contract.md +5 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
- package/docs/adr/0023-global-user-bindings.md +3 -1
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
- package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
- package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
- package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
- package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
- package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
- package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
- package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
- package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
- package/docs/adr/0041-record-cell-execution-directories.md +65 -0
- package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
- package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture.md +25 -27
- package/docs/binding-review-placement-design.md +29 -21
- package/docs/capability-planning-components.md +93 -0
- package/docs/capability-projection.md +7 -0
- package/docs/client-ui.md +88 -29
- package/docs/client-visual-contract.md +65 -0
- package/docs/context-management.md +22 -0
- package/docs/context-optimization-plan.md +353 -0
- package/docs/context-public-interface-coverage.md +25 -0
- package/docs/durability-design.md +31 -14
- package/docs/evaluation.md +41 -6
- package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
- package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
- package/docs/installation.md +50 -8
- package/docs/program-data-plane.md +5 -3
- package/docs/publishing.md +5 -5
- package/docs/release-coverage.md +73 -0
- package/docs/release-notes.md +1 -1
- package/docs/review-procedure.md +56 -0
- package/docs/runtime-reference.md +48 -16
- package/docs/semantic-validation.md +14 -2
- package/docs/stateful-binding-updates-proposal.md +2 -2
- package/docs/user-bindings.en.md +2 -2
- package/docs/user-bindings.md +3 -3
- package/docs/value-wire.md +11 -1
- package/docs/verification-optimization-work.md +52 -15
- package/docs/verification.md +30 -86
- package/index.js +147 -23
- package/internal/binding-toggle-receipt.js +24 -0
- package/internal/capability-command-receipt.js +29 -0
- package/internal/capability-environment.js +1247 -0
- package/internal/capability-plan.js +285 -0
- package/internal/capability-policy.js +96 -0
- package/internal/capability-statistics-store.js +180 -0
- package/internal/capability-statistics.js +210 -0
- package/internal/compiler-platform.js +5 -3
- package/internal/compiler-service.js +2 -9
- package/internal/config-spec.js +79 -9
- package/internal/cordis-tools-owner.js +22 -19
- package/internal/direct-surface-owner.js +29 -29
- package/internal/dynamic-environment-runtime.js +9 -9
- package/internal/edit-transport-owner.js +183 -139
- package/internal/execution-provider-owner.js +160 -0
- package/internal/execution-seam-compat.js +76 -120
- package/internal/isolated-worker.js +12 -8
- package/internal/kernel-worker.js +104 -32
- package/internal/native-root-dynamic.js +12 -13
- package/internal/program-bindings.js +190 -0
- package/internal/recovery-tips.js +53 -23
- package/internal/rejected-cell-editor.js +8 -8
- package/internal/repl-observation-interest.js +11 -3
- package/internal/rpc-contract.js +1 -0
- package/internal/runtime-bridge-owner.js +351 -82
- package/internal/runtime-config.js +58 -0
- package/internal/runtime-contexts.js +85 -16
- package/internal/runtime-intrinsics.js +1 -0
- package/internal/runtime-messages.js +41 -0
- package/internal/sdk-projection.js +10 -82
- package/internal/session-cell-executor.js +42 -19
- package/internal/session-events.js +74 -4
- package/internal/session-journal-recovery.js +904 -184
- package/internal/session-journal-schema.js +6 -3
- package/internal/session-journal.js +31 -2
- package/internal/session-log-view.js +189 -43
- package/internal/session-runtime.js +175 -23
- package/internal/settings-compat.js +23 -6
- package/internal/stateful-module-compiler.js +3 -2
- package/internal/stateful-module-runtime.js +7 -3
- package/internal/stateful-root-runtime.js +9 -4
- package/internal/user-binding-authoring.js +2 -7
- package/internal/user-bindings-owner.js +47 -7
- package/internal/validated-parse-repair.js +1 -1
- package/internal/value-wire.js +68 -21
- package/internal/worker-cwd-virtualization.js +20 -18
- package/internal/worker-realm-surfaces.js +3 -3
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +55 -38
- package/src/client-authoring-view.js +286 -168
- package/src/client-binding-review.js +4 -1
- package/src/client-binding-toggle.js +18 -0
- package/src/client-capabilities-view.js +1341 -0
- package/src/client-capability-sort.js +29 -0
- package/src/client-code-editor.js +6 -0
- package/src/client-copy.js +881 -8
- package/src/client-feature-gates.js +15 -5
- package/src/client-host-compat.js +23 -2
- package/src/client-indicator-view.js +4 -6
- package/src/client-primitives.js +280 -0
- package/src/client-repl-view.js +23 -16
- package/src/client-settings-view.js +412 -69
- package/src/client-styles.js +193 -41
- package/src/client-tool-hover.js +185 -0
- package/src/client-tool-view.js +45 -11
- package/src/client-transport.js +44 -9
- package/src/client-workbench.js +9 -10
- package/src/client.js +148 -68
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
- package/client.js +0 -31857
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { capabilityRule, validateCapabilityPolicy, effectiveCapabilitySettings } from './capability-policy.js'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
|
+
|
|
4
|
+
const MANAGED_CATEGORIES = new Set(['tool', 'skill', 'resource', 'resource-template', 'task'])
|
|
5
|
+
const EXCLUDED_CATEGORIES = new Set(['language', 'node', 'repl', 'ptc-plus', 'global-binding'])
|
|
6
|
+
const SELECTIONS = new Set(['full', 'on-demand', 'closed'])
|
|
7
|
+
const CATEGORY_NAVIGATION = new Map([
|
|
8
|
+
['tool', 'Tools: search by task or tools.<name> for parameters and results.'],
|
|
9
|
+
['skill', 'Skills: search by task or skills.<name> for when to use and how to load instructions.'],
|
|
10
|
+
['resource', 'MCP resources: search by server or resource purpose.'],
|
|
11
|
+
['resource-template', 'MCP templates: search by server or resource purpose.'],
|
|
12
|
+
['task', 'Tasks: search by the operation you need.'],
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
16
|
+
|
|
17
|
+
function record(value, label) {
|
|
18
|
+
if (!isRecord(value)) throw new TypeError(`ptc-plus: ${label} must be an object`)
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function text(value, label, empty = false) {
|
|
23
|
+
if (typeof value !== 'string' || (!empty && !value.trim())) throw new TypeError(`ptc-plus: ${label} must be a non-empty string`)
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function integer(value, label) {
|
|
28
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`ptc-plus: ${label} must be a non-negative integer`)
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function freeze(value) {
|
|
33
|
+
if (value && typeof value === 'object') {
|
|
34
|
+
for (const member of Object.values(value)) freeze(member)
|
|
35
|
+
Object.freeze(value)
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compareIds(a, b) { return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 }
|
|
41
|
+
|
|
42
|
+
function catalog(entries, priority = () => 0) {
|
|
43
|
+
const dependencies = new Map()
|
|
44
|
+
const blocks = []
|
|
45
|
+
const groups = Map.groupBy([...entries].sort(compareIds), priority)
|
|
46
|
+
for (const [, group] of [...groups].sort(([a], [b]) => a - b)) {
|
|
47
|
+
const additions = new Map()
|
|
48
|
+
for (const entry of group) {
|
|
49
|
+
for (const dependency of entry.dependencies) {
|
|
50
|
+
const previous = dependencies.get(dependency.id) ?? additions.get(dependency.id)
|
|
51
|
+
if (previous !== undefined && previous !== dependency.text) throw new TypeError(`ptc-plus: conflicting shared contract ${dependency.id}`)
|
|
52
|
+
if (previous === undefined) additions.set(dependency.id, dependency.text)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const [id, body] of [...additions].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
56
|
+
dependencies.set(id, body)
|
|
57
|
+
blocks.push(`### dependency:${id}\n${body}`)
|
|
58
|
+
}
|
|
59
|
+
blocks.push(...group.map(entry => `### ${entry.id}\n${entry.contract}`))
|
|
60
|
+
}
|
|
61
|
+
return blocks.join('\n\n')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function statisticsSettings(value) {
|
|
65
|
+
if (!value || value.enabled === false) return { usable: false, reason: 'statistics-disabled' }
|
|
66
|
+
if (value.enabled !== true || !Number.isSafeInteger(value.windowSize) || value.windowSize < 1
|
|
67
|
+
|| !Number.isSafeInteger(value.minimumSamples) || value.minimumSamples < 1 || value.minimumSamples > value.windowSize
|
|
68
|
+
|| !Number.isFinite(value.fullThreshold) || value.fullThreshold < 0 || value.fullThreshold > 1) {
|
|
69
|
+
return { usable: false, reason: 'statistics-settings-invalid' }
|
|
70
|
+
}
|
|
71
|
+
return { ...value, usable: true }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function coverage(value, settings) {
|
|
75
|
+
if (!isRecord(value) || value.status !== 'usable' || value.windowSize !== settings.windowSize
|
|
76
|
+
|| !Number.isSafeInteger(value.eligible) || value.eligible < settings.minimumSamples || value.eligible > settings.windowSize
|
|
77
|
+
|| !Number.isSafeInteger(value.used) || value.used < 0 || value.used > value.eligible) return null
|
|
78
|
+
return { used: value.used, eligible: value.eligible, coverage: value.used / value.eligible }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A frozen disclosure read model. It neither registers nor removes executable capabilities. */
|
|
82
|
+
function createLegacyCapabilityPlan(input) {
|
|
83
|
+
const options = record(input, 'capability plan input')
|
|
84
|
+
const schema = 'ptc-capability-plan/v1'
|
|
85
|
+
if (typeof options.enabled !== 'boolean') throw new TypeError('ptc-plus: capability management enabled must be boolean')
|
|
86
|
+
const configurationRevision = text(options.configurationRevision, 'configuration revision')
|
|
87
|
+
if (!Array.isArray(options.capabilities)) throw new TypeError('ptc-plus: capabilities must be an array')
|
|
88
|
+
const ids = new Set()
|
|
89
|
+
const excluded = []
|
|
90
|
+
const capabilities = options.capabilities.map(raw => {
|
|
91
|
+
const value = record(raw, 'capability')
|
|
92
|
+
const id = text(value.id, 'capability id')
|
|
93
|
+
if (ids.has(id)) throw new TypeError(`ptc-plus: duplicate capability identity ${id}`)
|
|
94
|
+
ids.add(id)
|
|
95
|
+
const revision = text(value.revision, 'capability revision')
|
|
96
|
+
if (EXCLUDED_CATEGORIES.has(value.category)) {
|
|
97
|
+
excluded.push({ id, revision, category: value.category })
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
if (!MANAGED_CATEGORIES.has(value.category)) throw new TypeError(`ptc-plus: unknown capability category ${value.category}`)
|
|
101
|
+
const dependencies = value.dependencies ?? []
|
|
102
|
+
if (!Array.isArray(dependencies)) throw new TypeError('ptc-plus: contract dependencies must be an array')
|
|
103
|
+
return {
|
|
104
|
+
id, revision, category: value.category,
|
|
105
|
+
...(value.mcpServer === undefined ? {} : { mcpServer: text(value.mcpServer, 'MCP server identity') }),
|
|
106
|
+
...(value.mcpServerCandidates === undefined ? {} : { mcpServerCandidates: value.mcpServerCandidates.map(server => text(server, 'MCP server candidate')) }),
|
|
107
|
+
contract: text(value.contract, 'complete capability contract'),
|
|
108
|
+
dependencies: dependencies.map(rawDependency => {
|
|
109
|
+
const dependency = record(rawDependency, 'contract dependency')
|
|
110
|
+
return { id: text(dependency.id, 'dependency id'), text: text(dependency.text, 'dependency contract') }
|
|
111
|
+
}),
|
|
112
|
+
}
|
|
113
|
+
}).filter(Boolean).sort(compareIds)
|
|
114
|
+
const bytes = value => Buffer.byteLength(value, 'utf8')
|
|
115
|
+
if (!options.enabled) return freeze({
|
|
116
|
+
schema, enabled: false, configurationRevision,
|
|
117
|
+
entries: capabilities.map(({ id, revision, category }) => ({ id, revision, category, selection: 'host', reason: 'management-disabled' })),
|
|
118
|
+
excluded: excluded.sort(compareIds), catalogText: '', directoryText: '',
|
|
119
|
+
budgets: { unit: 'utf8-bytes', contractBytes: 0, directoryBytes: 0, overageBytes: 0 },
|
|
120
|
+
})
|
|
121
|
+
const budgetBytes = integer(options.budgetBytes, 'contract budget bytes')
|
|
122
|
+
const directoryBudgetBytes = integer(options.directoryBudgetBytes, 'directory budget bytes')
|
|
123
|
+
const navigationText = text(options.navigationText, 'bounded discovery guidance')
|
|
124
|
+
if (bytes(navigationText) > directoryBudgetBytes) throw new RangeError('ptc-plus: discovery guidance exceeds directory byte budget')
|
|
125
|
+
const manual = record(options.manual ?? {}, 'manual capability settings')
|
|
126
|
+
const overrides = record(manual.overrides ?? {}, 'capability overrides')
|
|
127
|
+
const baseline = manual.baseline ?? []
|
|
128
|
+
if (!Array.isArray(baseline)) throw new TypeError('ptc-plus: initial common set must be an array')
|
|
129
|
+
baseline.forEach(id => text(id, 'baseline capability identity'))
|
|
130
|
+
const common = new Set(baseline)
|
|
131
|
+
for (const [id, selection] of Object.entries(overrides)) {
|
|
132
|
+
text(id, 'override capability identity')
|
|
133
|
+
if (!SELECTIONS.has(selection)) throw new TypeError(`ptc-plus: invalid capability selection ${selection}`)
|
|
134
|
+
}
|
|
135
|
+
const settings = statisticsSettings(options.statistics)
|
|
136
|
+
// Statistical damage cannot invalidate the independently valid manual configuration.
|
|
137
|
+
const statisticsInput = settings.usable ? options.coverage : undefined
|
|
138
|
+
const statistics = isRecord(statisticsInput) ? statisticsInput : null
|
|
139
|
+
const entries = []
|
|
140
|
+
const selections = new Map()
|
|
141
|
+
const full = []
|
|
142
|
+
const candidates = []
|
|
143
|
+
for (const capability of capabilities) {
|
|
144
|
+
if (capability.mcpServerCandidates?.some(server => overrides[`mcpServers.${server}`] === 'closed')) {
|
|
145
|
+
throw new TypeError(`ptc-plus: cannot determine the MCP server owner of ${capability.id}; conflicting namespaces: ${capability.mcpServerCandidates.join(', ')}`)
|
|
146
|
+
}
|
|
147
|
+
const serverClosed = capability.mcpServer !== undefined && overrides[`mcpServers.${capability.mcpServer}`] === 'closed'
|
|
148
|
+
const explicit = serverClosed ? 'closed' : Object.hasOwn(overrides, capability.id) ? overrides[capability.id] : undefined
|
|
149
|
+
const entry = { id: capability.id, revision: capability.revision, category: capability.category, selection: explicit ?? 'on-demand', reason: serverClosed ? 'mcp-server-disabled' : explicit ? 'manual-override' : 'baseline-not-selected' }
|
|
150
|
+
entries.push(entry)
|
|
151
|
+
selections.set(capability.id, entry)
|
|
152
|
+
if (explicit === 'full') full.push(capability)
|
|
153
|
+
if (explicit) continue
|
|
154
|
+
const evidence = statistics && Object.hasOwn(statistics, capability.id) ? statistics[capability.id] : undefined
|
|
155
|
+
let selectedCoverage = null
|
|
156
|
+
let source = settings.reason
|
|
157
|
+
if (settings.usable) {
|
|
158
|
+
if (isRecord(evidence) && evidence.revision === capability.revision) {
|
|
159
|
+
selectedCoverage = coverage(evidence.workspace, settings)
|
|
160
|
+
source = 'workspace'
|
|
161
|
+
if (!selectedCoverage) {
|
|
162
|
+
selectedCoverage = coverage(evidence.global, settings)
|
|
163
|
+
source = 'global'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!selectedCoverage) source = statisticsInput !== undefined && !statistics ? 'statistics-data-invalid' : 'statistics-unavailable'
|
|
167
|
+
}
|
|
168
|
+
entry.statisticsReason = source
|
|
169
|
+
if (selectedCoverage) {
|
|
170
|
+
entry.evidence = { scope: source, ...selectedCoverage }
|
|
171
|
+
entry.reason = 'coverage-below-threshold'
|
|
172
|
+
if (selectedCoverage.coverage >= settings.fullThreshold) candidates.push({ capability, entry, weight: selectedCoverage.coverage })
|
|
173
|
+
} else if (common.has(capability.id)) {
|
|
174
|
+
entry.reason = 'baseline'
|
|
175
|
+
candidates.push({ capability, entry, weight: 1 })
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Whole blocks and shared type identities reproduce the final renderer's exact byte cost.
|
|
179
|
+
const includedDependencies = new Map()
|
|
180
|
+
let blockCount = 0
|
|
181
|
+
let blockBytes = 0
|
|
182
|
+
function addCost(capability, commit) {
|
|
183
|
+
let count = blockCount + 1
|
|
184
|
+
let size = blockBytes + bytes(`### ${capability.id}\n${capability.contract}`)
|
|
185
|
+
const additions = new Map()
|
|
186
|
+
for (const dependency of capability.dependencies) {
|
|
187
|
+
const previous = additions.get(dependency.id) ?? includedDependencies.get(dependency.id)
|
|
188
|
+
if (previous !== undefined && previous !== dependency.text) throw new TypeError(`ptc-plus: conflicting shared contract ${dependency.id}`)
|
|
189
|
+
if (previous === undefined) {
|
|
190
|
+
additions.set(dependency.id, dependency.text)
|
|
191
|
+
size += bytes(`### dependency:${dependency.id}\n${dependency.text}`)
|
|
192
|
+
count++
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (commit) {
|
|
196
|
+
for (const [id, body] of additions) includedDependencies.set(id, body)
|
|
197
|
+
blockCount = count
|
|
198
|
+
blockBytes = size
|
|
199
|
+
}
|
|
200
|
+
return size + Math.max(0, count - 1) * 2
|
|
201
|
+
}
|
|
202
|
+
for (const capability of full) addCost(capability, true)
|
|
203
|
+
// Ranking includes actual UTF-8 wrappers and dependencies, not capability counts.
|
|
204
|
+
for (const candidate of candidates) candidate.cost = Math.max(1, bytes(catalog([candidate.capability])))
|
|
205
|
+
candidates.sort((a, b) => {
|
|
206
|
+
return b.weight / b.cost - a.weight / a.cost || compareIds(a.capability, b.capability)
|
|
207
|
+
})
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
if (addCost(candidate.capability, false) <= budgetBytes) {
|
|
210
|
+
addCost(candidate.capability, true)
|
|
211
|
+
full.push(candidate.capability)
|
|
212
|
+
candidate.entry.selection = 'full'
|
|
213
|
+
candidate.entry.reason = candidate.entry.evidence ? 'statistical-coverage' : 'baseline'
|
|
214
|
+
} else candidate.entry.reason = 'contract-budget'
|
|
215
|
+
}
|
|
216
|
+
// Statistics choose membership, never the order of unchanged contract groups.
|
|
217
|
+
const catalogText = catalog(full,
|
|
218
|
+
capability => overrides[capability.id] === 'full' ? 0 : common.has(capability.id) ? 1 : 2)
|
|
219
|
+
let directoryText = navigationText
|
|
220
|
+
const directoryCategories = []
|
|
221
|
+
const availableCategories = new Set(capabilities.filter(capability => selections.get(capability.id).selection !== 'closed')
|
|
222
|
+
.map(capability => capability.category))
|
|
223
|
+
for (const [category, guidance] of CATEGORY_NAVIGATION) {
|
|
224
|
+
if (!availableCategories.has(category)) continue
|
|
225
|
+
const proposed = `${directoryText}\n${guidance}`
|
|
226
|
+
if (bytes(proposed) <= directoryBudgetBytes) {
|
|
227
|
+
directoryText = proposed
|
|
228
|
+
directoryCategories.push(category)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return freeze({
|
|
232
|
+
schema, enabled: true, configurationRevision, entries,
|
|
233
|
+
excluded: excluded.sort(compareIds), catalogText, directoryText, directoryCategories,
|
|
234
|
+
budgets: { unit: 'utf8-bytes', budgetBytes, directoryBudgetBytes, contractBytes: bytes(catalogText), directoryBytes: bytes(directoryText), overageBytes: Math.max(0, bytes(catalogText) - budgetBytes) },
|
|
235
|
+
unresolvedConfigurationIds: [...new Set([...baseline, ...Object.keys(overrides)])].filter(id => !capabilities.some(entry => entry.id === id)).sort(),
|
|
236
|
+
statisticsStatus: settings.usable ? 'enabled' : settings.reason,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The old planner remains the replay oracle for environment v1/v2. */
|
|
241
|
+
export function createCapabilityPlan(input) {
|
|
242
|
+
if (input.contextEnabled !== undefined) {
|
|
243
|
+
if (typeof input.contextEnabled !== 'boolean') throw new TypeError('ptc-plus: capability context enabled must be boolean')
|
|
244
|
+
const effective = effectiveCapabilitySettings(input)
|
|
245
|
+
const { contextEnabled, ...historicalInput } = effective
|
|
246
|
+
return freeze({ ...createCapabilityPlan(historicalInput), schema: 'ptc-capability-plan/v3', contextEnabled })
|
|
247
|
+
}
|
|
248
|
+
if (input.policy === undefined) return createLegacyCapabilityPlan(input)
|
|
249
|
+
const policy = validateCapabilityPolicy(input.policy)
|
|
250
|
+
const rules = new Map(input.capabilities.filter(item => MANAGED_CATEGORIES.has(item.category))
|
|
251
|
+
.map(item => {
|
|
252
|
+
const rule = input.enabled ? capabilityRule(policy, item) : { availability: 'enabled', disclosure: 'host', availabilitySource: 'host', disclosureSource: 'host' }
|
|
253
|
+
return [item.id, item.effectiveAvailability === 'closed'
|
|
254
|
+
? { ...rule, availability: 'closed', availabilitySource: 'invocation-path' } : rule]
|
|
255
|
+
}))
|
|
256
|
+
const mixedSkills = input.enabled && input.capabilities.some(item => item.category === 'skill'
|
|
257
|
+
&& rules.get(item.id)?.availability !== 'closed' && (rules.get(item.id)?.disclosure !== 'host' || item.nativeInvocation !== undefined))
|
|
258
|
+
const overrides = Object.fromEntries([...rules].flatMap(([id, rule]) => rule.availability === 'closed'
|
|
259
|
+
? [[id, 'closed']] : rule.disclosure === 'adaptive' ? []
|
|
260
|
+
: [[id, rule.disclosure === 'host' ? mixedSkills && input.capabilities.find(item => item.id === id)?.category === 'skill' ? 'full' : 'on-demand' : rule.disclosure]]))
|
|
261
|
+
const planned = createLegacyCapabilityPlan({ ...input,
|
|
262
|
+
budgetBytes: policy.allocation.budget ? input.budgetBytes : Number.MAX_SAFE_INTEGER,
|
|
263
|
+
manual: { baseline: policy.allocation.baseline ? input.manual?.baseline ?? [] : [], overrides },
|
|
264
|
+
statistics: policy.allocation.statistics ? { ...input.statistics, enabled: true } : { enabled: false },
|
|
265
|
+
})
|
|
266
|
+
const entries = planned.entries.map(entry => {
|
|
267
|
+
const rule = rules.get(entry.id)
|
|
268
|
+
const selection = rule.availability === 'closed' ? 'closed' : !input.enabled || rule.disclosure === 'host' ? 'host' : entry.selection
|
|
269
|
+
return { ...entry, ...rule, selection,
|
|
270
|
+
availability: rule.availability,
|
|
271
|
+
disclosure: !input.enabled ? 'host' : rule.disclosure,
|
|
272
|
+
...((selection === 'host' && input.enabled) ? { reason: 'host-disclosure' }
|
|
273
|
+
: rule.availability === 'closed' ? { reason: rule.availabilitySource.startsWith('mcp:') ? 'mcp-server-disabled' : 'availability-rule' }
|
|
274
|
+
: rule.disclosure !== 'adaptive' && input.enabled ? { reason: 'disclosure-rule' } : {}) }
|
|
275
|
+
})
|
|
276
|
+
// Navigation is needed only for deliberately deferred explanations.
|
|
277
|
+
const onDemand = entries.some(entry => entry.selection === 'on-demand')
|
|
278
|
+
return freeze({ ...planned, schema: 'ptc-capability-plan/v2', entries,
|
|
279
|
+
directoryText: onDemand ? planned.directoryText : '', directoryCategories: onDemand ? planned.directoryCategories : [],
|
|
280
|
+
budgets: { ...planned.budgets, ...(input.enabled ? { budgetBytes: input.budgetBytes } : {}),
|
|
281
|
+
directoryBytes: onDemand ? planned.budgets.directoryBytes : 0,
|
|
282
|
+
overageBytes: input.enabled ? Math.max(0, planned.budgets.contractBytes - input.budgetBytes) : 0 },
|
|
283
|
+
...(input.enabled ? { unresolvedConfigurationIds: [...new Set([...(input.manual?.baseline ?? []), ...Object.keys(policy.items)])]
|
|
284
|
+
.filter(id => !input.capabilities.some(item => item.id === id)).sort(), allocation: policy.allocation } : {}) })
|
|
285
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Reusable user intent; no inventory, observations, authority or session state. */
|
|
2
|
+
export const CAPABILITY_POLICY_DEFAULT = JSON.stringify({
|
|
3
|
+
schema: 'ptc-capability-policy/v1',
|
|
4
|
+
defaults: { availability: 'enabled', disclosure: 'host' },
|
|
5
|
+
categories: {}, mcpServers: {}, items: {},
|
|
6
|
+
allocation: { baseline: true, statistics: false, budget: true },
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
/** Categories produced by the current Host integration; resources and templates share a server entry. */
|
|
10
|
+
export const CAPABILITY_INVENTORY_CATEGORIES = Object.freeze({ tool: 'tool', skill: 'skill', resource: 'resource' })
|
|
11
|
+
// The configuration format also accepts independent template/task entries supplied to the pure planner.
|
|
12
|
+
export const CAPABILITY_CATEGORIES = [...Object.values(CAPABILITY_INVENTORY_CATEGORIES), 'resource-template', 'task']
|
|
13
|
+
export const CAPABILITY_DISCLOSURES = ['host', 'full', 'on-demand', 'adaptive']
|
|
14
|
+
const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
15
|
+
const own = (value, key) => Object.hasOwn(value, key) ? value[key] : undefined
|
|
16
|
+
|
|
17
|
+
/** The group pauses application, never the retained independent choices. Missing denotes historical behavior. */
|
|
18
|
+
export function effectiveCapabilitySettings(settings) {
|
|
19
|
+
if (settings.contextEnabled !== false) return settings
|
|
20
|
+
return { ...settings, enabled: false, recoveryEnabled: false, previewEnabled: false,
|
|
21
|
+
statistics: { ...settings.statistics, enabled: false } }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateCapabilityPolicy(value) {
|
|
25
|
+
const fail = () => { throw new TypeError('ptc-plus: invalid capability policy') }
|
|
26
|
+
if (!isRecord(value) || value.schema !== 'ptc-capability-policy/v1'
|
|
27
|
+
|| Object.keys(value).some(key => !['schema', 'defaults', 'categories', 'mcpServers', 'items', 'allocation'].includes(key))) fail()
|
|
28
|
+
function rule(value, defaults = false) {
|
|
29
|
+
if (!isRecord(value) || Object.keys(value).some(key => !['availability', 'disclosure'].includes(key))
|
|
30
|
+
|| (own(value, 'availability') !== undefined && !['enabled', 'closed'].includes(value.availability))
|
|
31
|
+
|| (own(value, 'disclosure') !== undefined && !CAPABILITY_DISCLOSURES.includes(value.disclosure))
|
|
32
|
+
|| defaults && (value.availability === undefined || value.disclosure === undefined)) fail()
|
|
33
|
+
return { ...(value.availability === undefined ? {} : { availability: value.availability }),
|
|
34
|
+
...(value.disclosure === undefined ? {} : { disclosure: value.disclosure }) }
|
|
35
|
+
}
|
|
36
|
+
const rules = (values, valid) => {
|
|
37
|
+
if (!isRecord(values)) fail()
|
|
38
|
+
return Object.fromEntries(Object.entries(values).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
39
|
+
.map(([id, value]) => { if (!valid(id)) fail(); return [id, rule(value)] }))
|
|
40
|
+
}
|
|
41
|
+
if (!isRecord(value.allocation) || Object.keys(value.allocation).length !== 3
|
|
42
|
+
|| ['baseline', 'statistics', 'budget'].some(key => typeof value.allocation[key] !== 'boolean')) fail()
|
|
43
|
+
return { schema: value.schema, defaults: rule(value.defaults, true),
|
|
44
|
+
categories: rules(value.categories, id => CAPABILITY_CATEGORIES.includes(id)),
|
|
45
|
+
mcpServers: rules(value.mcpServers, id => Boolean(id.trim())),
|
|
46
|
+
items: rules(value.items, id => Boolean(id.trim()) && !id.startsWith('mcpServers.')),
|
|
47
|
+
allocation: { baseline: value.allocation.baseline, statistics: value.allocation.statistics, budget: value.allocation.budget } }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Missing policy denotes the previous configuration format, not a new default. */
|
|
51
|
+
export function capabilityPolicyForConfig(config) {
|
|
52
|
+
if (config.capabilityPolicy !== undefined) return validateCapabilityPolicy(JSON.parse(config.capabilityPolicy))
|
|
53
|
+
const policy = JSON.parse(CAPABILITY_POLICY_DEFAULT)
|
|
54
|
+
if (config.capabilityManagementEnabled === true) policy.defaults.disclosure = 'adaptive'
|
|
55
|
+
try { policy.allocation.statistics = JSON.parse(config.capabilityStatistics ?? '{}')?.enabled === true } catch {}
|
|
56
|
+
const overrides = JSON.parse(config.capabilityOverrides ?? '{}')
|
|
57
|
+
for (const [id, choice] of Object.entries(overrides)) {
|
|
58
|
+
const rule = choice === 'closed' ? { availability: 'closed' } : { disclosure: choice }
|
|
59
|
+
const rules = id.startsWith('mcpServers.') ? policy.mcpServers : policy.items
|
|
60
|
+
const key = id.startsWith('mcpServers.') ? id.slice(11) : id
|
|
61
|
+
Object.defineProperty(rules, key, { value: rule, enumerable: true, writable: true, configurable: true })
|
|
62
|
+
}
|
|
63
|
+
return validateCapabilityPolicy(policy)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Independent dimensions preserve the rule that supplied each decision. */
|
|
67
|
+
export function capabilityRule(policy, capability) {
|
|
68
|
+
let availability = policy.defaults.availability, disclosure = policy.defaults.disclosure
|
|
69
|
+
let availabilitySource = 'default', disclosureSource = 'default'
|
|
70
|
+
const server = capability.mcpServer === undefined ? undefined : own(policy.mcpServers, capability.mcpServer)
|
|
71
|
+
const apply = (rule, source) => {
|
|
72
|
+
if (rule?.availability !== undefined) { availability = rule.availability; availabilitySource = source }
|
|
73
|
+
if (rule?.disclosure !== undefined) { disclosure = rule.disclosure; disclosureSource = source }
|
|
74
|
+
}
|
|
75
|
+
apply(own(policy.categories, capability.category), `category:${capability.category}`)
|
|
76
|
+
apply(server, `mcp:${capability.mcpServer}`)
|
|
77
|
+
apply(own(policy.items, capability.id), `item:${capability.id}`)
|
|
78
|
+
if (server?.availability === 'closed') { availability = 'closed'; availabilitySource = `mcp:${capability.mcpServer}` }
|
|
79
|
+
if (capability.mcpServerCandidates?.some(id => own(policy.mcpServers, id)?.availability === 'closed')) {
|
|
80
|
+
throw new TypeError(`ptc-plus: cannot determine the MCP server owner of ${capability.id}; conflicting namespaces: ${capability.mcpServerCandidates.join(', ')}`)
|
|
81
|
+
}
|
|
82
|
+
return { availability, disclosure, availabilitySource, disclosureSource }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Change one reusable dimension, removing only that dimension for automatic. */
|
|
86
|
+
export function changeCapabilityRule(policy, id, dimension, choice) {
|
|
87
|
+
const next = structuredClone(policy)
|
|
88
|
+
const rules = id.startsWith('mcpServers.') ? next.mcpServers : next.items
|
|
89
|
+
const key = id.startsWith('mcpServers.') ? id.slice(11) : id
|
|
90
|
+
const rule = { ...own(rules, key) }
|
|
91
|
+
if (choice === 'automatic') delete rule[dimension]
|
|
92
|
+
else rule[dimension] = choice
|
|
93
|
+
if (Object.keys(rule).length) Object.defineProperty(rules, key, { value: rule, enumerable: true, writable: true, configurable: true })
|
|
94
|
+
else delete rules[key]
|
|
95
|
+
return validateCapabilityPolicy(next)
|
|
96
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { effectiveCapabilitySettings } from './capability-policy.js'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { mkdir, readFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join, resolve } from 'node:path'
|
|
5
|
+
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
|
|
6
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
7
|
+
import { capabilityCoverage, createCapabilityStatistics, reduceCapabilityStatistics, validateCapabilityStatistics } from './capability-statistics.js'
|
|
8
|
+
|
|
9
|
+
const hash = value => createHash('sha256').update(value).digest('hex')
|
|
10
|
+
const evidence = { tool: 'host-execution-result', task: 'host-execution-result', skill: 'skill-body-accepted', resource: 'resource-access', 'resource-template': 'resource-access' }
|
|
11
|
+
|
|
12
|
+
/** Locked metadata only; never persists arguments, results, resource contents or Skill bodies. */
|
|
13
|
+
export class CapabilityStatisticsStore {
|
|
14
|
+
constructor({ filename, ctx, dshHome } = {}) {
|
|
15
|
+
this.filename = resolve(filename ?? join(resolveDshHome(dshHome), 'ptc-plus', 'capability-statistics.json'))
|
|
16
|
+
this.userId = hash(dirname(this.filename))
|
|
17
|
+
this.ctx = ctx
|
|
18
|
+
this.tail = Promise.resolve()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async read(windowSize) {
|
|
22
|
+
let value
|
|
23
|
+
try { value = JSON.parse(await readFile(this.filename, 'utf8')) } catch (error) {
|
|
24
|
+
if (error.code !== 'ENOENT') throw error
|
|
25
|
+
return { schema: 'ptc-capability-statistics-store/v1', nextOrder: 1, members: [], families: [],
|
|
26
|
+
state: createCapabilityStatistics({ windowSize, maxFamilies: 10000, maxScopes: 20000, maxCapabilitiesPerFamily: 2000 }) }
|
|
27
|
+
}
|
|
28
|
+
if (value?.schema !== 'ptc-capability-statistics-store/v1' || !Array.isArray(value.members)
|
|
29
|
+
|| !Array.isArray(value.families) || value.families.length > 10000
|
|
30
|
+
|| value.members.length > 10000 || !Number.isSafeInteger(value.nextOrder) || value.nextOrder < 1) throw new TypeError('damaged capability statistics store')
|
|
31
|
+
validateCapabilityStatistics(value.state)
|
|
32
|
+
const identities = new Set()
|
|
33
|
+
const orders = new Set()
|
|
34
|
+
for (const family of value.families) {
|
|
35
|
+
if (typeof family?.id !== 'string' || !family.id.trim() || identities.has(family.id)
|
|
36
|
+
|| !Number.isSafeInteger(family.observationRevision) || family.observationRevision < 0
|
|
37
|
+
|| (family.eligibleOrder !== null && (!Number.isSafeInteger(family.eligibleOrder)
|
|
38
|
+
|| family.eligibleOrder < 1 || family.eligibleOrder >= value.nextOrder || orders.has(family.eligibleOrder)))) {
|
|
39
|
+
throw new TypeError('damaged capability statistics family provenance')
|
|
40
|
+
}
|
|
41
|
+
identities.add(family.id)
|
|
42
|
+
if (family.eligibleOrder !== null) orders.add(family.eligibleOrder)
|
|
43
|
+
}
|
|
44
|
+
for (const retained of value.state.families) {
|
|
45
|
+
const family = value.families.find(item => item.id === retained.familyId)
|
|
46
|
+
if (retained.userId !== this.userId || !family || (value.state.status === 'usable'
|
|
47
|
+
&& (family.eligibleOrder !== retained.eligibleOrder || family.observationRevision !== retained.observationRevision))) {
|
|
48
|
+
throw new TypeError('inconsistent capability statistics family provenance')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (value.state.windows.some(window => window.userId !== this.userId || window.floor >= value.nextOrder)) {
|
|
52
|
+
throw new TypeError('inconsistent capability statistics qualification frontier')
|
|
53
|
+
}
|
|
54
|
+
for (const member of value.members) {
|
|
55
|
+
if (typeof member.id !== 'string' || typeof member.familyId !== 'string' || !identities.has(member.familyId)
|
|
56
|
+
|| !['active', 'settled', 'unknown'].includes(member.status)) throw new TypeError('damaged capability statistics member')
|
|
57
|
+
}
|
|
58
|
+
if (new Set(value.members.map(item => item.id)).size !== value.members.length) throw new TypeError('duplicate capability statistics member')
|
|
59
|
+
if (value.state.limits.windowSize !== windowSize) throw new TypeError('statistics window changed; retained samples are unavailable')
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
family(agent) {
|
|
64
|
+
let session = agent.session
|
|
65
|
+
const visited = new Set()
|
|
66
|
+
while (session.header?.origin === 'subagent' && session.header?.parentSession) {
|
|
67
|
+
if (visited.has(session.id)) return null
|
|
68
|
+
visited.add(session.id)
|
|
69
|
+
session = this.ctx?.get('sessions')?.get?.(session.header.parentSession)
|
|
70
|
+
?? this.ctx?.get('agents')?.get?.(session.header.parentSession)?.session
|
|
71
|
+
if (!session) return null
|
|
72
|
+
}
|
|
73
|
+
return session.id
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
memberSettlement(session, live) {
|
|
77
|
+
if (live && live.status !== 'idle') return 'active'
|
|
78
|
+
const events = session.snapshotEvents().filter(event => session.isOwnSeq?.(event.seq) !== false)
|
|
79
|
+
const start = events.findLast(event => event.type === 'turn/start')
|
|
80
|
+
if (!start) return 'unknown'
|
|
81
|
+
const end = events.findLast(event => event.type === 'turn/end' && event.data.turn === start.data.turn)
|
|
82
|
+
const entered = events.some(event => event.type === 'step/start' && event.data.turn === start.data.turn)
|
|
83
|
+
if (!end) return live ? 'active' : 'unknown'
|
|
84
|
+
if (end.data.reason?.kind !== 'completed' || !entered) return 'unknown'
|
|
85
|
+
if (live?.inbox?.nextStep?.length || live?.inbox?.nextTurn?.length) return 'active'
|
|
86
|
+
return 'settled'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
mutate(agent, snapshot, additions = [], status = 'active') {
|
|
90
|
+
const settings = effectiveCapabilitySettings(snapshot.settings).statistics
|
|
91
|
+
if (!snapshot.settings.policy && !snapshot.plan.enabled || settings?.enabled !== true || !Number.isSafeInteger(settings.windowSize)
|
|
92
|
+
|| settings.windowSize < 1 || settings.windowSize > 10000) return Promise.resolve()
|
|
93
|
+
const familyId = this.family(agent)
|
|
94
|
+
if (!familyId) return Promise.resolve()
|
|
95
|
+
const operation = async () => {
|
|
96
|
+
await mkdir(dirname(this.filename), { recursive: true, mode: 0o700 })
|
|
97
|
+
return withFileLock(this.filename, async () => {
|
|
98
|
+
const document = await this.read(settings.windowSize)
|
|
99
|
+
if (document.state.status !== 'usable') return
|
|
100
|
+
const retained = structuredClone(document)
|
|
101
|
+
const capacityExceeded = async () => {
|
|
102
|
+
retained.state.status = 'capacity-exceeded'
|
|
103
|
+
await writeFileAtomic(this.filename, JSON.stringify(retained) + '\n', { mode: 0o600, dirMode: 0o700 })
|
|
104
|
+
}
|
|
105
|
+
if (additions.length > document.state.limits.maxCapabilitiesPerFamily) return capacityExceeded()
|
|
106
|
+
const liveAgents = this.ctx?.get('agents')?.list?.()
|
|
107
|
+
const sessions = this.ctx?.get('sessions')?.list?.()
|
|
108
|
+
const completeCensus = Array.isArray(liveAgents) && Array.isArray(sessions)
|
|
109
|
+
const known = new Map((sessions ?? []).map(session => [session.id, session]))
|
|
110
|
+
for (const live of liveAgents ?? []) known.set(live.session.id, live.session)
|
|
111
|
+
known.set(agent.session.id, agent.session)
|
|
112
|
+
for (const session of known.values()) {
|
|
113
|
+
if (this.family({ session }) !== familyId) continue
|
|
114
|
+
let member = document.members.find(item => item.id === session.id)
|
|
115
|
+
if (!member) { member = { id: session.id, familyId, status: 'unknown' }; document.members.push(member) }
|
|
116
|
+
if (member.familyId !== familyId) throw new TypeError('capability statistics lineage changed')
|
|
117
|
+
const live = (liveAgents ?? []).find(item => item.session.id === session.id)
|
|
118
|
+
member.status = completeCensus ? this.memberSettlement(session, live) : 'unknown'
|
|
119
|
+
if (session.id === agent.session.id && status !== 'idle') member.status = 'active'
|
|
120
|
+
}
|
|
121
|
+
// A missing descendant or an incomplete public census cannot establish
|
|
122
|
+
// a family denominator, even if every observed agent currently is idle.
|
|
123
|
+
for (const member of document.members.filter(item => item.familyId === familyId)) {
|
|
124
|
+
if (!known.has(member.id)) member.status = 'unknown'
|
|
125
|
+
}
|
|
126
|
+
const familyMembers = document.members.filter(item => item.familyId === familyId)
|
|
127
|
+
const settlement = familyMembers.every(item => item.status === 'settled') ? 'settled'
|
|
128
|
+
: familyMembers.some(item => item.status === 'unknown') ? 'unknown' : 'active'
|
|
129
|
+
// Window eviction removes sample payloads, never the immutable family
|
|
130
|
+
// qualification or revision used by restored and concurrent observers.
|
|
131
|
+
let provenance = document.families.find(item => item.id === familyId)
|
|
132
|
+
if (!provenance) {
|
|
133
|
+
provenance = { id: familyId, observationRevision: -1, eligibleOrder: null }
|
|
134
|
+
document.families.push(provenance)
|
|
135
|
+
}
|
|
136
|
+
if (document.families.length > 10000 || provenance.observationRevision >= Number.MAX_SAFE_INTEGER
|
|
137
|
+
|| document.nextOrder >= Number.MAX_SAFE_INTEGER) return capacityExceeded()
|
|
138
|
+
provenance.eligibleOrder ??= settlement === 'settled' ? document.nextOrder++ : null
|
|
139
|
+
provenance.observationRevision++
|
|
140
|
+
document.state = reduceCapabilityStatistics(document.state, { type: 'family-observation', userId: this.userId, familyId,
|
|
141
|
+
observationRevision: provenance.observationRevision, settlement, eligibleOrder: provenance.eligibleOrder, observations: additions })
|
|
142
|
+
if (document.members.length > 10000) return capacityExceeded()
|
|
143
|
+
await writeFileAtomic(this.filename, JSON.stringify(document) + '\n', { mode: 0o600, dirMode: 0o700 })
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
const task = this.tail.then(operation, operation)
|
|
147
|
+
this.tail = task.catch(() => {})
|
|
148
|
+
return task
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
observations(agent, snapshot, id, useEvidence = null, outcome = null) {
|
|
152
|
+
const workspaceId = hash(resolve(agent.session.header.cwd))
|
|
153
|
+
return snapshot.plan.entries.filter(entry => ['full', 'on-demand', 'host'].includes(entry.selection)
|
|
154
|
+
&& (id === undefined || entry.id === id)).map(entry => ({ workspaceId,
|
|
155
|
+
id: entry.id, revision: entry.revision, category: entry.category, disclosure: entry.selection,
|
|
156
|
+
planRevision: snapshot.plan.configurationRevision, useEvidence, outcome }))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
available(agent, snapshot) { return this.mutate(agent, snapshot, this.observations(agent, snapshot)) }
|
|
160
|
+
used(agent, snapshot, id, useEvidence, outcome) {
|
|
161
|
+
const entry = snapshot.plan.entries.find(item => item.id === id)
|
|
162
|
+
if (!entry || evidence[entry.category] !== useEvidence) return Promise.resolve()
|
|
163
|
+
return this.mutate(agent, snapshot, this.observations(agent, snapshot, id, useEvidence, outcome))
|
|
164
|
+
}
|
|
165
|
+
status(agent, snapshot, status) { return this.mutate(agent, snapshot, [], status) }
|
|
166
|
+
|
|
167
|
+
async coverage(agent, capabilities, settings) {
|
|
168
|
+
try {
|
|
169
|
+
await this.tail
|
|
170
|
+
const { state } = await this.read(settings.windowSize)
|
|
171
|
+
const workspaceId = hash(resolve(agent.session.header.cwd))
|
|
172
|
+
return Object.fromEntries(capabilities.map(item => [item.id, { revision: item.revision,
|
|
173
|
+
workspace: capabilityCoverage(state, { userId: this.userId, workspaceId, id: item.id, revision: item.revision }),
|
|
174
|
+
global: capabilityCoverage(state, { userId: this.userId, workspaceId: null, id: item.id, revision: item.revision }) }]))
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.ctx?.logger?.warn?.('ptc-plus: automatic allocation unavailable; retaining manual capability settings', error)
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|