@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.
Files changed (106) hide show
  1. package/README.md +6 -4
  2. package/README.zh.md +6 -4
  3. package/cordis.patch.yml +3 -1
  4. package/package.json +51 -46
  5. package/packages/agent-team/README.md +2 -2
  6. package/packages/agent-team/README.zh.md +2 -2
  7. package/packages/agent-team/core-skills/member-memory-manager/SKILL.md +30 -0
  8. package/packages/agent-team/lib/context-continuity-host.js +90 -0
  9. package/packages/agent-team/lib/context-projection.js +266 -489
  10. package/packages/agent-team/lib/context-source.js +4 -72
  11. package/packages/agent-team/lib/human-avatar.js +61 -0
  12. package/packages/agent-team/lib/human-profile.js +157 -0
  13. package/packages/agent-team/lib/human-update-check.js +119 -0
  14. package/packages/agent-team/lib/index.js +464 -254
  15. package/packages/agent-team/lib/ledger.js +120 -15
  16. package/packages/agent-team/lib/member-context.js +6 -3
  17. package/packages/agent-team/lib/member-runtime.js +1 -1
  18. package/packages/agent-team/lib/pressure-policy.js +6 -1
  19. package/packages/agent-team/lib/typert.host.js +489 -33
  20. package/packages/agent-team/lib/typert.remote-client.d.ts +15 -3
  21. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  22. package/packages/agent-team/lib/typert.remote-client.js +250 -28
  23. package/packages/agent-team/lib/types/context-continuity-host.d.ts +84 -0
  24. package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -0
  25. package/packages/agent-team/lib/types/context-projection.d.ts +130 -185
  26. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/context-source.d.ts +4 -35
  28. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/human-avatar.d.ts +29 -0
  30. package/packages/agent-team/lib/types/human-avatar.d.ts.map +1 -0
  31. package/packages/agent-team/lib/types/human-profile.d.ts +116 -0
  32. package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -0
  33. package/packages/agent-team/lib/types/human-update-check.d.ts +73 -0
  34. package/packages/agent-team/lib/types/human-update-check.d.ts.map +1 -0
  35. package/packages/agent-team/lib/types/index.d.ts +153 -41
  36. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  37. package/packages/agent-team/lib/types/ledger.d.ts +31 -2
  38. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  39. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/pressure-policy.d.ts +6 -0
  41. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  42. package/packages/agent-team/lib/types/types/entities.d.ts +17 -0
  43. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  44. package/packages/agent-team/lib/types/types/requests-results.d.ts +81 -0
  45. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  46. package/packages/client-agent-team/README.md +1 -1
  47. package/packages/client-agent-team/README.zh.md +1 -1
  48. package/packages/client-agent-team/lib/client.js +1545 -303
  49. package/packages/client-agent-team/lib/client.js.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +15 -0
  51. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +1 -0
  52. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +115 -0
  53. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +6 -2
  55. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +22 -1
  56. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -1
  57. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +25 -2
  58. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +6 -1
  59. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +67 -29
  61. package/packages/client-agent-team/lib/types/client/TeamComposer.js +2 -2
  62. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  63. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  64. package/packages/client-agent-team/lib/types/client/TeamConversation.js +12 -5
  65. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +4 -1
  66. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -1
  67. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +22 -7
  68. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts +11 -1
  69. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +66 -5
  71. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +18 -3
  72. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  73. package/packages/client-agent-team/lib/types/client/TeamMessage.js +213 -67
  74. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +5 -0
  75. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  76. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +52 -26
  77. package/packages/client-agent-team/lib/types/client/avatar-image.d.ts +21 -0
  78. package/packages/client-agent-team/lib/types/client/avatar-image.d.ts.map +1 -0
  79. package/packages/client-agent-team/lib/types/client/avatar-image.js +9 -0
  80. package/packages/client-agent-team/lib/types/client/human-identity.d.ts +90 -0
  81. package/packages/client-agent-team/lib/types/client/human-identity.d.ts.map +1 -0
  82. package/packages/client-agent-team/lib/types/client/human-identity.js +96 -0
  83. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  84. package/packages/client-agent-team/lib/types/client/index.js +76 -4
  85. package/packages/client-agent-team/lib/types/client/locales.d.ts +72 -2
  86. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/locales.js +78 -2
  88. package/packages/client-agent-team/lib/types/client/refs.d.ts +71 -0
  89. package/packages/client-agent-team/lib/types/client/refs.d.ts.map +1 -0
  90. package/packages/client-agent-team/lib/types/client/refs.js +181 -0
  91. package/packages/client-agent-team/lib/types/client/slots.d.ts +9 -3
  92. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +57 -9
  94. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/team-formatters.js +85 -17
  96. package/packages/tool-agent-team/README.md +2 -2
  97. package/packages/tool-agent-team/README.zh.md +2 -2
  98. package/packages/tool-agent-team/lib/context-tools.js +20 -6
  99. package/packages/tool-agent-team/lib/index.js +2 -2
  100. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  101. package/packages/agent-team/lib/context-management.js +0 -371
  102. package/packages/agent-team/lib/types/context-management.d.ts +0 -154
  103. package/packages/agent-team/lib/types/context-management.d.ts.map +0 -1
  104. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +0 -31
  105. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +0 -1
  106. 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
+ }