@wowyuarm/dsh-agent-team 0.1.14-alpha.2 → 0.1.15
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 +6 -4
- package/README.zh.md +6 -4
- package/cordis.patch.yml +3 -1
- package/package.json +51 -46
- package/packages/agent-team/README.md +2 -2
- package/packages/agent-team/README.zh.md +2 -2
- package/packages/agent-team/core-skills/member-memory-manager/SKILL.md +30 -0
- package/packages/agent-team/lib/context-continuity-host.js +90 -0
- package/packages/agent-team/lib/context-projection.js +266 -489
- package/packages/agent-team/lib/context-source.js +4 -72
- package/packages/agent-team/lib/human-avatar.js +61 -0
- package/packages/agent-team/lib/human-profile.js +157 -0
- package/packages/agent-team/lib/human-update-check.js +119 -0
- package/packages/agent-team/lib/index.js +464 -254
- package/packages/agent-team/lib/ledger.js +120 -15
- package/packages/agent-team/lib/member-context.js +6 -3
- package/packages/agent-team/lib/member-runtime.js +1 -1
- package/packages/agent-team/lib/pressure-policy.js +6 -1
- package/packages/agent-team/lib/typert.host.js +489 -33
- package/packages/agent-team/lib/typert.remote-client.d.ts +15 -3
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +250 -28
- package/packages/agent-team/lib/types/context-continuity-host.d.ts +84 -0
- package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-projection.d.ts +130 -185
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-source.d.ts +4 -35
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/human-avatar.d.ts +29 -0
- package/packages/agent-team/lib/types/human-avatar.d.ts.map +1 -0
- package/packages/agent-team/lib/types/human-profile.d.ts +116 -0
- package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -0
- package/packages/agent-team/lib/types/human-update-check.d.ts +73 -0
- package/packages/agent-team/lib/types/human-update-check.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +153 -41
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +31 -2
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/pressure-policy.d.ts +6 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/entities.d.ts +17 -0
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +81 -0
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +1545 -303
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +15 -0
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +115 -0
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +6 -2
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +22 -1
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +25 -2
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +6 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +67 -29
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +2 -2
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +12 -5
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +4 -1
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +22 -7
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +11 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +66 -5
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +18 -3
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +213 -67
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +5 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +52 -26
- package/packages/client-agent-team/lib/types/client/avatar-image.d.ts +21 -0
- package/packages/client-agent-team/lib/types/client/avatar-image.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/avatar-image.js +9 -0
- package/packages/client-agent-team/lib/types/client/human-identity.d.ts +90 -0
- package/packages/client-agent-team/lib/types/client/human-identity.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/human-identity.js +96 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +76 -4
- package/packages/client-agent-team/lib/types/client/locales.d.ts +72 -2
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +78 -2
- package/packages/client-agent-team/lib/types/client/refs.d.ts +71 -0
- package/packages/client-agent-team/lib/types/client/refs.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/refs.js +181 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +9 -3
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +57 -9
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +85 -17
- package/packages/tool-agent-team/README.md +2 -2
- package/packages/tool-agent-team/README.zh.md +2 -2
- package/packages/tool-agent-team/lib/context-tools.js +20 -6
- package/packages/tool-agent-team/lib/index.js +2 -2
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/agent-team/lib/context-management.js +0 -371
- package/packages/agent-team/lib/types/context-management.d.ts +0 -154
- package/packages/agent-team/lib/types/context-management.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts +0 -31
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/task-refs.js +0 -99
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* snapshot-aware surface.
|
|
16
16
|
*
|
|
17
17
|
* The validators below are the single place that recognizes these messages, so
|
|
18
|
-
* callers never match on localized body text.
|
|
18
|
+
* callers never match on localized body text. Writing them belongs to the
|
|
19
|
+
* context-continuity engine's codec, which Team constructs with its own plugin
|
|
20
|
+
* identity and prose (`context-continuity-host.ts`): one writer, so the
|
|
21
|
+
* envelope Team reads back can never drift from the one it wrote.
|
|
19
22
|
* @module @wowyuarm/dsh-agent-team/context-source
|
|
20
23
|
*/
|
|
21
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
22
24
|
/** The producer kind attributing every Agent Team message source. */
|
|
23
25
|
export const AGENT_TEAM_PLUGIN_ID = '@wowyuarm/dsh-agent-team';
|
|
24
26
|
/**
|
|
@@ -50,47 +52,6 @@ export function isAgentTeamSourceKind(kind) {
|
|
|
50
52
|
export const HANDOFF_SECTION_NAME = 'HANDOFF';
|
|
51
53
|
/** Stable section name marking a checkpoint continuation and carrying its ref. */
|
|
52
54
|
export const CHECKPOINT_SECTION_NAME = 'Checkpoint';
|
|
53
|
-
/** Fixed text of the quiet checkpoint continuation delivered on the next turn. */
|
|
54
|
-
export const CHECKPOINT_CONTINUATION_TEXT = 'A context checkpoint was recorded at the end of the previous turn. Continue the work you were doing.';
|
|
55
|
-
/**
|
|
56
|
-
* Build the first model-facing context of one rollover generation. The Host
|
|
57
|
-
* owns only the verifiable envelope; the prose is the Member's own handoff.
|
|
58
|
-
*
|
|
59
|
-
* The envelope sections are additive structure, not the only carrier of these
|
|
60
|
-
* facts: {@link handoffBody} already states them in the model-facing text, so a
|
|
61
|
-
* surface that renders only the body loses nothing.
|
|
62
|
-
*/
|
|
63
|
-
export function createHandoffMessage(input) {
|
|
64
|
-
return createUserMessage({
|
|
65
|
-
content: [{ type: 'text', text: handoffBody(input) }],
|
|
66
|
-
source: {
|
|
67
|
-
kind: AGENT_TEAM_PLUGIN_ID,
|
|
68
|
-
form: 'snapshot',
|
|
69
|
-
sections: handoffSections(input),
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Build the quiet follow-up that continues work after an explicit checkpoint
|
|
75
|
-
* concluded its turn. Delivery is scheduled only after the checkpoint's
|
|
76
|
-
* successful tool result is durable, so a result-render failure can never
|
|
77
|
-
* leave a ghost continuation behind.
|
|
78
|
-
*
|
|
79
|
-
* This is a `snapshot` rather than a `notice` for one reason: the projection
|
|
80
|
-
* must read the checkpoint ref back out of the durable log to record delivery,
|
|
81
|
-
* and `sections` is the only admitted payload slot that carries structure. A
|
|
82
|
-
* notice would have forced the ref into its human-readable one-line summary.
|
|
83
|
-
*/
|
|
84
|
-
export function createCheckpointContinuationMessage(checkpointRef) {
|
|
85
|
-
return createUserMessage({
|
|
86
|
-
content: [{ type: 'text', text: CHECKPOINT_CONTINUATION_TEXT }],
|
|
87
|
-
source: {
|
|
88
|
-
kind: AGENT_TEAM_PLUGIN_ID,
|
|
89
|
-
form: 'snapshot',
|
|
90
|
-
sections: [{ name: CHECKPOINT_SECTION_NAME, text: checkpointRef }],
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
55
|
/**
|
|
95
56
|
* Read one message's snapshot sections when it is this plugin's own snapshot.
|
|
96
57
|
* @param message - candidate user message.
|
|
@@ -200,32 +161,3 @@ export const HANDOFF_TRIGGER = 'Trigger';
|
|
|
200
161
|
export const HANDOFF_EVENT_SEQ = 'Handoff event seq';
|
|
201
162
|
export const HANDOFF_CHECKPOINT = 'Continued from checkpoint';
|
|
202
163
|
export const HANDOFF_RELATED_FILES = 'Related files';
|
|
203
|
-
/** The envelope contributions of one handoff, prose first. */
|
|
204
|
-
function handoffSections(input) {
|
|
205
|
-
return [
|
|
206
|
-
{ name: HANDOFF_SECTION_NAME, text: input.handoff },
|
|
207
|
-
{ name: HANDOFF_PREVIOUS_SESSION, text: input.previousSessionId },
|
|
208
|
-
{ name: HANDOFF_NEW_SESSION, text: input.newSessionId },
|
|
209
|
-
{ name: HANDOFF_TRIGGER, text: input.trigger },
|
|
210
|
-
{ name: HANDOFF_EVENT_SEQ, text: String(input.handoffEventSeq) },
|
|
211
|
-
...(input.checkpointRef === undefined ? [] : [{ name: HANDOFF_CHECKPOINT, text: input.checkpointRef }]),
|
|
212
|
-
...(input.relatedFiles === undefined || input.relatedFiles.length === 0
|
|
213
|
-
? []
|
|
214
|
-
: [{ name: HANDOFF_RELATED_FILES, text: JSON.stringify(input.relatedFiles.map(file => file.path)) }]),
|
|
215
|
-
];
|
|
216
|
-
}
|
|
217
|
-
function handoffBody(input) {
|
|
218
|
-
const lines = [
|
|
219
|
-
'Context handoff: you are continuing as the same Team Member in a fresh private context.',
|
|
220
|
-
`Previous session: ${input.previousSessionId}`,
|
|
221
|
-
`New session: ${input.newSessionId}`,
|
|
222
|
-
`Trigger: ${input.trigger}`,
|
|
223
|
-
...(input.checkpointRef === undefined ? [] : [`Continued from checkpoint: ${input.checkpointRef}`]),
|
|
224
|
-
...(input.relatedFiles === undefined || input.relatedFiles.length === 0 ? [] : [`Related files: ${input.relatedFiles.map(file => file.path).join(', ')}`]),
|
|
225
|
-
'',
|
|
226
|
-
'Your handoff from the previous context follows. Verify external state before relying on it; a context change never rolls back files, processes, Team facts, or remote side effects.',
|
|
227
|
-
'',
|
|
228
|
-
input.handoff,
|
|
229
|
-
];
|
|
230
|
-
return lines.join('\n');
|
|
231
|
-
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
5
|
+
import { ATTACHMENT_MAX_BYTES, sanitizeFileName, sanitizeMediaType } from "./attachments.js";
|
|
6
|
+
/**
|
|
7
|
+
* Persistent human avatar store. Layout mirrors the composer attachment
|
|
8
|
+
* cache (`<avatarRef>/` payload plus `meta.json`) but lives under its own
|
|
9
|
+
* root with no TTL sweep: avatar bytes must survive longer than the 72h
|
|
10
|
+
* referenced / 24h orphan windows that bound message attachments.
|
|
11
|
+
*/
|
|
12
|
+
export function humanAvatarsRoot() {
|
|
13
|
+
return dshHomePath('agent-team', 'human', 'v1');
|
|
14
|
+
}
|
|
15
|
+
function avatarDir(root, avatarRef) {
|
|
16
|
+
return join(root, avatarRef);
|
|
17
|
+
}
|
|
18
|
+
/** Accept only image payloads; the settings page sends `image/*` exclusively. */
|
|
19
|
+
export function assertAvatarMediaType(mediaType) {
|
|
20
|
+
const sanitized = sanitizeMediaType(mediaType);
|
|
21
|
+
if (!sanitized.startsWith('image/'))
|
|
22
|
+
throw new Error(`human avatar must be an image (got '${mediaType}')`);
|
|
23
|
+
return sanitized;
|
|
24
|
+
}
|
|
25
|
+
/** Write one avatar as an immutable entry; the caller stores the ref in settings. */
|
|
26
|
+
export async function writeHumanAvatar(root, rawName, mediaType, bytes) {
|
|
27
|
+
if (bytes.byteLength === 0)
|
|
28
|
+
throw new Error('human avatar must not be empty');
|
|
29
|
+
if (bytes.byteLength > ATTACHMENT_MAX_BYTES)
|
|
30
|
+
throw new Error(`human avatar exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
|
|
31
|
+
const storedMediaType = assertAvatarMediaType(mediaType);
|
|
32
|
+
const name = sanitizeFileName(rawName);
|
|
33
|
+
const avatarRef = randomUUID();
|
|
34
|
+
const dir = avatarDir(root, avatarRef);
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
await writeFile(join(dir, name), bytes);
|
|
37
|
+
await writeFile(join(dir, 'meta.json'), JSON.stringify({ name, mediaType: storedMediaType, uploadedAt: new Date().toISOString() }), 'utf8');
|
|
38
|
+
return { avatarRef, path: join(dir, name), name, byteSize: bytes.byteLength, mediaType: storedMediaType };
|
|
39
|
+
}
|
|
40
|
+
/** Read one avatar back; `undefined` when removed or never written. */
|
|
41
|
+
export async function readHumanAvatar(root, avatarRef) {
|
|
42
|
+
const dir = avatarDir(root, avatarRef);
|
|
43
|
+
let metaRaw;
|
|
44
|
+
try {
|
|
45
|
+
metaRaw = await readFile(join(dir, 'meta.json'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const meta = JSON.parse(metaRaw.toString('utf8'));
|
|
51
|
+
const entries = await readdir(dir);
|
|
52
|
+
const payload = entries.filter(entry => entry !== 'meta.json')[0];
|
|
53
|
+
if (payload === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
const bytes = await readFile(join(dir, payload));
|
|
56
|
+
return { name: meta.name, mediaType: meta.mediaType, byteSize: bytes.byteLength, uploadedAt: meta.uploadedAt, bytes };
|
|
57
|
+
}
|
|
58
|
+
/** Remove one avatar entry; missing entries already satisfy the removal. */
|
|
59
|
+
export async function removeHumanAvatar(root, avatarRef) {
|
|
60
|
+
await rm(avatarDir(root, avatarRef), { recursive: true, force: true });
|
|
61
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import z from '@deepseek-ai/schemastery';
|
|
5
|
+
import { parse } from 'yaml';
|
|
6
|
+
/**
|
|
7
|
+
* Human identity profile: the one configurable display name plus the avatar
|
|
8
|
+
* reference. `member:human` stays the durable identity everywhere; only the
|
|
9
|
+
* handle shown in team_view, @ matching, and UI follows this profile.
|
|
10
|
+
*
|
|
11
|
+
* Storage split (see spec.md v1):
|
|
12
|
+
* - name + avatarRef are the Team Host row's own Config, so they live in the
|
|
13
|
+
* active profile's patch document as that row's `config` and the settings
|
|
14
|
+
* service derives their form from this schema. rc.1 derives every form from
|
|
15
|
+
* a plugin's Config, so the retired `installSection` namespace — a section of
|
|
16
|
+
* its own — has no counterpart; the row id below is the settings namespace.
|
|
17
|
+
* - avatar bytes live under a persistent directory below; the profile holds
|
|
18
|
+
* only the reference, never a data URL. The composer attachment cache is
|
|
19
|
+
* TTL-bound and must not hold avatar bytes.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Settings namespace of the Human profile: the Team Host row's id in
|
|
23
|
+
* `cordis.patch.yml`, which is what the settings service addresses a form and a
|
|
24
|
+
* write by AND the id the profile-document write patches. It is addressed by
|
|
25
|
+
* this constant, never by the running Host's `ctx.fiber.entry`: a Remote call
|
|
26
|
+
* runs under its caller's context, so that lookup names the RPC gateway's row.
|
|
27
|
+
* `shipping.spec.ts` pins the constant to the row the composition declares.
|
|
28
|
+
*/
|
|
29
|
+
export const HUMAN_PROFILE_SETTINGS_NAMESPACE = 'wowyuarm-agent-team-host';
|
|
30
|
+
/** Fallback display name before any user override is stored. */
|
|
31
|
+
export const HUMAN_PROFILE_DEFAULT_NAME = 'human';
|
|
32
|
+
/** Repository home for the version footnote link. */
|
|
33
|
+
export const HUMAN_PROFILE_REPO_URL = 'https://github.com/wowyuarm/dsh-agent-team';
|
|
34
|
+
/**
|
|
35
|
+
* Bundle version shown in the settings footnote, and the current side of the
|
|
36
|
+
* update check: the version of the package THIS Host runs from — read from the
|
|
37
|
+
* installed manifest, so a `link:` checkout under the development profile and a
|
|
38
|
+
* registry tarball under stable each state their own truth. It is not a
|
|
39
|
+
* hand-maintained string: the 0.1.14 bundle shipped with `0.1.13` written in
|
|
40
|
+
* it, which made the footnote name the previous release and the update check
|
|
41
|
+
* offer the release the user already had.
|
|
42
|
+
*
|
|
43
|
+
* Resolved once at load, three levels above this module — `packages/agent-team/{src,lib}`
|
|
44
|
+
* sits that deep in both layouts, the same relative positioning
|
|
45
|
+
* `member-runtime.ts` uses to find `core-skills`. An unreadable or malformed
|
|
46
|
+
* manifest degrades to `'unknown'`: the Remote's `version: string` contract
|
|
47
|
+
* holds and the update comparison simply compares nothing. The footnote is
|
|
48
|
+
* informational only and never gates behavior.
|
|
49
|
+
*/
|
|
50
|
+
export const HUMAN_PROFILE_VERSION = readInstalledBundleVersion();
|
|
51
|
+
/** Version of the manifest this package installed from, or `'unknown'`. */
|
|
52
|
+
function readInstalledBundleVersion() {
|
|
53
|
+
try {
|
|
54
|
+
const manifestPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../../package.json');
|
|
55
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
56
|
+
if (typeof manifest.version === 'string' && manifest.version !== '')
|
|
57
|
+
return manifest.version;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Reading our own manifest must never fail the Host boot: a host that
|
|
61
|
+
// cannot find its own package.json is a broken install, and the footnote
|
|
62
|
+
// reporting 'unknown' says so more honestly than a stale number.
|
|
63
|
+
}
|
|
64
|
+
return 'unknown';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Schemastery schema of the Human profile: the Team Host row's Config. Both
|
|
68
|
+
* fields are volatile, which is what lets an edit reach the running Host
|
|
69
|
+
* without remounting it — and what makes the settings service derive a form
|
|
70
|
+
* from this schema at all.
|
|
71
|
+
*/
|
|
72
|
+
export const HUMAN_PROFILE_SETTINGS_SCHEMA = z.object({
|
|
73
|
+
name: z.string().default(HUMAN_PROFILE_DEFAULT_NAME).volatile(),
|
|
74
|
+
// Schemastery fields are optional unless `.required()`: a missing avatarRef
|
|
75
|
+
// simply resolves absent, which the profile reads as "no custom avatar".
|
|
76
|
+
avatarRef: z.string().volatile(),
|
|
77
|
+
});
|
|
78
|
+
/** Normalize one candidate display name the way Member handles normalize. */
|
|
79
|
+
export function normalizeHumanName(raw) {
|
|
80
|
+
return raw.normalize('NFKC').trim();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate one candidate display name with the same floor as Member handles:
|
|
84
|
+
* non-empty after trim. Uniqueness against live Members is checked by the
|
|
85
|
+
* Host (which owns the ledger), not here, so this stays a pure function.
|
|
86
|
+
*/
|
|
87
|
+
export function assertValidHumanName(raw) {
|
|
88
|
+
const name = normalizeHumanName(raw);
|
|
89
|
+
if (name === '')
|
|
90
|
+
throw new Error('human name must not be empty');
|
|
91
|
+
return name;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The retired settings section this profile's facts lived in before rc.1.
|
|
95
|
+
* rc.1 derives every settings form from a plugin's Config, and its legacy
|
|
96
|
+
* `settings.yaml` importer maps a section to a composition entry id through a
|
|
97
|
+
* closed built-in list — this third-party section matches nothing there, so
|
|
98
|
+
* the import rejects it and the values survive only in the renamed document.
|
|
99
|
+
* This Host owns the section, so adopting what is left belongs here.
|
|
100
|
+
*/
|
|
101
|
+
export const LEGACY_HUMAN_PROFILE_SECTION = 'agent-team-human';
|
|
102
|
+
/**
|
|
103
|
+
* Read the legacy section out of one legacy settings document. An
|
|
104
|
+
* unparsable document or a missing section means "nothing to adopt" rather
|
|
105
|
+
* than an error — the file is retired input, not an authority — and each
|
|
106
|
+
* field is validated independently so one bad field never costs the other.
|
|
107
|
+
*/
|
|
108
|
+
export function parseLegacyHumanProfile(yamlText) {
|
|
109
|
+
let document;
|
|
110
|
+
try {
|
|
111
|
+
document = parse(yamlText);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (typeof document !== 'object' || document === null)
|
|
117
|
+
return undefined;
|
|
118
|
+
const section = document[LEGACY_HUMAN_PROFILE_SECTION];
|
|
119
|
+
if (typeof section !== 'object' || section === null)
|
|
120
|
+
return undefined;
|
|
121
|
+
const fields = section;
|
|
122
|
+
let name;
|
|
123
|
+
if (typeof fields.name === 'string') {
|
|
124
|
+
try {
|
|
125
|
+
name = assertValidHumanName(fields.name);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
name = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const avatarRef = typeof fields.avatarRef === 'string' && fields.avatarRef.trim() !== ''
|
|
132
|
+
? fields.avatarRef.trim()
|
|
133
|
+
: undefined;
|
|
134
|
+
if (name === undefined && avatarRef === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
return { ...(name === undefined ? {} : { name }), ...(avatarRef === undefined ? {} : { avatarRef }) };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Decide what adoption may write: nothing unless the stored profile is still
|
|
140
|
+
* the pristine default, so a value the Human re-entered after the upgrade
|
|
141
|
+
* always wins, and never the legacy default name itself. The current profile
|
|
142
|
+
* carries explicit `undefined` on `avatarRef` — the Host getter spreads a
|
|
143
|
+
* settings read — and so is not `HumanProfile` under
|
|
144
|
+
* exactOptionalPropertyTypes; taking that shape directly keeps the Host call
|
|
145
|
+
* cast-free. The returned fields are exactly the ops the profile page would
|
|
146
|
+
* have written; byte existence for `avatarRef` is the caller's I/O and must
|
|
147
|
+
* already hold.
|
|
148
|
+
*/
|
|
149
|
+
export function planLegacyAdoption(current, legacy) {
|
|
150
|
+
if (current.name !== HUMAN_PROFILE_DEFAULT_NAME || current.avatarRef !== undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
const name = legacy.name !== undefined && legacy.name !== HUMAN_PROFILE_DEFAULT_NAME ? legacy.name : undefined;
|
|
153
|
+
const avatarRef = legacy.avatarRef;
|
|
154
|
+
if (name === undefined && avatarRef === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
return { ...(name === undefined ? {} : { name }), ...(avatarRef === undefined ? {} : { avatarRef }) };
|
|
157
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort Host-side check for a newer published bundle release.
|
|
3
|
+
*
|
|
4
|
+
* The settings footnote needs `updateAvailable`/`latestVersion`, but the
|
|
5
|
+
* profile read path must never wait on the network: the checker serves a
|
|
6
|
+
* cached snapshot synchronously and refreshes it in the background. Every
|
|
7
|
+
* failure mode — disabled by the operator, no fetch implementation, timeout,
|
|
8
|
+
* non-OK status, malformed payload, unparsable version — settles as "no
|
|
9
|
+
* update known", so the footnote degrades to version + link exactly as before.
|
|
10
|
+
*
|
|
11
|
+
* No durable state: the cache lives only in memory. A Host restart simply
|
|
12
|
+
* starts unknown again, which is the safe default for an informational tip.
|
|
13
|
+
*/
|
|
14
|
+
/** Public npm metadata document for the bundle's `latest` dist-tag. */
|
|
15
|
+
export const HUMAN_UPDATE_CHECK_REGISTRY_URL = 'https://registry.npmjs.org/@wowyuarm%2fdsh-agent-team/latest';
|
|
16
|
+
/** How long one settled check stays authoritative before a re-check. */
|
|
17
|
+
export const HUMAN_UPDATE_CHECK_TTL_MS = 12 * 60 * 60 * 1000;
|
|
18
|
+
/** Upper bound for one registry round trip; the read path never waits on it. */
|
|
19
|
+
export const HUMAN_UPDATE_CHECK_TIMEOUT_MS = 5000;
|
|
20
|
+
/** Setting this env var to `0`/`false`/`off` disables the outbound check. */
|
|
21
|
+
export const HUMAN_UPDATE_CHECK_ENV = 'DSH_AGENT_TEAM_UPDATE_CHECK';
|
|
22
|
+
/** Operator kill-switch; anything but an explicit off value keeps the default on. */
|
|
23
|
+
export function isUpdateCheckEnabled(env = process.env) {
|
|
24
|
+
const raw = env[HUMAN_UPDATE_CHECK_ENV]?.trim().toLowerCase();
|
|
25
|
+
return raw !== '0' && raw !== 'false' && raw !== 'off';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Numeric core of a `major.minor.patch…` version. Pre-release/build suffixes
|
|
29
|
+
* are ignored for ordering; anything else unparsable resolves absent so the
|
|
30
|
+
* caller treats it as "no update known" instead of guessing.
|
|
31
|
+
*/
|
|
32
|
+
export function parseVersionCore(value) {
|
|
33
|
+
const core = value.split('+', 1)[0]?.split('-', 1)[0];
|
|
34
|
+
if (core === undefined || core === '')
|
|
35
|
+
return undefined;
|
|
36
|
+
const parts = core.split('.');
|
|
37
|
+
const numbers = [];
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
if (part === '' || !/^[0-9]+$/.test(part))
|
|
40
|
+
return undefined;
|
|
41
|
+
numbers.push(Number(part));
|
|
42
|
+
}
|
|
43
|
+
return numbers.length === 0 ? undefined : numbers;
|
|
44
|
+
}
|
|
45
|
+
/** True when `latest` orders strictly after `current` on the numeric core. */
|
|
46
|
+
export function isNewerVersion(current, latest) {
|
|
47
|
+
const from = parseVersionCore(current);
|
|
48
|
+
const to = parseVersionCore(latest);
|
|
49
|
+
if (from === undefined || to === undefined)
|
|
50
|
+
return false;
|
|
51
|
+
const width = Math.max(from.length, to.length);
|
|
52
|
+
for (let index = 0; index < width; index += 1) {
|
|
53
|
+
const left = from[index] ?? 0;
|
|
54
|
+
const right = to[index] ?? 0;
|
|
55
|
+
if (right !== left)
|
|
56
|
+
return right > left;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One registry round trip resolving the published `latest` version string.
|
|
62
|
+
* Never throws: anything unexpected resolves absent.
|
|
63
|
+
*/
|
|
64
|
+
export async function fetchLatestVersion(fetchImpl, url = HUMAN_UPDATE_CHECK_REGISTRY_URL, timeoutMs = HUMAN_UPDATE_CHECK_TIMEOUT_MS) {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
67
|
+
if (!response.ok)
|
|
68
|
+
return undefined;
|
|
69
|
+
const payload = (await response.json());
|
|
70
|
+
return typeof payload?.version === 'string' ? payload.version : undefined;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Synchronous snapshot over a background-refreshed latest-version cache.
|
|
78
|
+
* `snapshot()` never blocks: a stale or empty cache serves the last known
|
|
79
|
+
* value (initially "no update") and kicks off at most one shared refresh.
|
|
80
|
+
* Concurrent and repeated calls while a refresh is in flight share it, and a
|
|
81
|
+
* refresh that settles only stamps the cache — failures simply leave the
|
|
82
|
+
* previous value standing.
|
|
83
|
+
*/
|
|
84
|
+
export function createHumanUpdateChecker(options) {
|
|
85
|
+
const { currentVersion, ttlMs = HUMAN_UPDATE_CHECK_TTL_MS, timeoutMs = HUMAN_UPDATE_CHECK_TIMEOUT_MS, now = Date.now, env = process.env, } = options;
|
|
86
|
+
const fetchImpl = options.fetchImpl ?? ((url, init) => globalThis.fetch(url, init));
|
|
87
|
+
let latestVersion;
|
|
88
|
+
let checkedAt = Number.NEGATIVE_INFINITY;
|
|
89
|
+
let inFlight;
|
|
90
|
+
const refresh = () => {
|
|
91
|
+
if (inFlight !== undefined)
|
|
92
|
+
return;
|
|
93
|
+
if (!isUpdateCheckEnabled(env))
|
|
94
|
+
return;
|
|
95
|
+
const settled = fetchLatestVersion(fetchImpl, HUMAN_UPDATE_CHECK_REGISTRY_URL, timeoutMs).then(version => {
|
|
96
|
+
checkedAt = now();
|
|
97
|
+
if (version !== undefined && isNewerVersion(currentVersion, version))
|
|
98
|
+
latestVersion = version;
|
|
99
|
+
});
|
|
100
|
+
const tracked = settled.finally(() => {
|
|
101
|
+
if (inFlight === tracked)
|
|
102
|
+
inFlight = undefined;
|
|
103
|
+
});
|
|
104
|
+
inFlight = tracked;
|
|
105
|
+
// Rejections are already absorbed inside fetchLatestVersion, but the
|
|
106
|
+
// finally chain still needs a settlement handler so a Host without an
|
|
107
|
+
// unhandled-rejection policy never sees one from this floating refresh.
|
|
108
|
+
void tracked.catch(() => { });
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
snapshot: () => {
|
|
112
|
+
if (checkedAt + ttlMs <= now())
|
|
113
|
+
refresh();
|
|
114
|
+
if (latestVersion === undefined)
|
|
115
|
+
return { updateAvailable: false };
|
|
116
|
+
return { updateAvailable: true, latestVersion };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|