@phnx-labs/agents-cli 1.22.114 → 1.22.116

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 (243) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +210 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +214 -148
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/feed.js +65 -14
  19. package/dist/commands/focus.js +1 -1
  20. package/dist/commands/go.d.ts +0 -17
  21. package/dist/commands/go.js +2 -19
  22. package/dist/commands/logs.js +1 -1
  23. package/dist/commands/mcp.js +8 -83
  24. package/dist/commands/memory.js +4 -47
  25. package/dist/commands/message.js +4 -4
  26. package/dist/commands/plugins.js +1 -93
  27. package/dist/commands/repo.js +0 -44
  28. package/dist/commands/resume.js +4 -1
  29. package/dist/commands/secrets-passthrough.js +2 -2
  30. package/dist/commands/send.d.ts +4 -4
  31. package/dist/commands/send.js +6 -51
  32. package/dist/commands/sessions-backup-setup.js +1 -1
  33. package/dist/commands/sessions-picker.d.ts +11 -0
  34. package/dist/commands/sessions-picker.js +88 -7
  35. package/dist/commands/sessions-resume.js +0 -1
  36. package/dist/commands/sessions-share.d.ts +5 -7
  37. package/dist/commands/sessions-share.js +98 -49
  38. package/dist/commands/sessions.d.ts +21 -2
  39. package/dist/commands/sessions.js +158 -11
  40. package/dist/commands/setup-browser.js +18 -2
  41. package/dist/commands/setup-computer.js +20 -6
  42. package/dist/commands/setup-secrets.d.ts +2 -2
  43. package/dist/commands/setup-secrets.js +24 -4
  44. package/dist/commands/setup-term.d.ts +24 -0
  45. package/dist/commands/setup-term.js +70 -0
  46. package/dist/commands/setup-terminal.d.ts +3 -0
  47. package/dist/commands/setup-terminal.js +22 -0
  48. package/dist/commands/setup.d.ts +1 -1
  49. package/dist/commands/setup.js +29 -11
  50. package/dist/commands/skills.js +0 -8
  51. package/dist/commands/ssh.d.ts +6 -0
  52. package/dist/commands/ssh.js +93 -302
  53. package/dist/commands/sync.js +14 -5
  54. package/dist/commands/teams.js +1 -1
  55. package/dist/commands/traces.js +1 -1
  56. package/dist/lib/accounting/rotate.d.ts +63 -1
  57. package/dist/lib/accounting/rotate.js +56 -0
  58. package/dist/lib/accounts/add.d.ts +0 -5
  59. package/dist/lib/accounts/add.js +3 -9
  60. package/dist/lib/accounts/slots.js +32 -2
  61. package/dist/lib/answer-router.d.ts +11 -2
  62. package/dist/lib/answer-router.js +26 -2
  63. package/dist/lib/artifacts-client.d.ts +20 -0
  64. package/dist/lib/artifacts-client.js +46 -0
  65. package/dist/lib/auth-mint.d.ts +5 -4
  66. package/dist/lib/auth-mint.js +6 -5
  67. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  68. package/dist/lib/browser/runtime-state.d.ts +55 -0
  69. package/dist/lib/browser/runtime-state.js +99 -18
  70. package/dist/lib/browser/service.d.ts +10 -0
  71. package/dist/lib/browser/service.js +229 -37
  72. package/dist/lib/browser/types.d.ts +18 -0
  73. package/dist/lib/cli-resources.js +3 -1
  74. package/dist/lib/cloud/dispatch.js +1 -1
  75. package/dist/lib/cloudflare/creds.d.ts +10 -0
  76. package/dist/lib/cloudflare/creds.js +46 -0
  77. package/dist/lib/cloudflare/provision.d.ts +35 -0
  78. package/dist/lib/cloudflare/provision.js +144 -0
  79. package/dist/lib/computer/sessions-list.d.ts +55 -0
  80. package/dist/lib/computer/sessions-list.js +168 -1
  81. package/dist/lib/config-keys.d.ts +1 -1
  82. package/dist/lib/config-keys.js +5 -0
  83. package/dist/lib/daemon/daemon.js +8 -1
  84. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  85. package/dist/lib/daemon/feed-stream-service.js +40 -0
  86. package/dist/lib/daemon-services.d.ts +1 -1
  87. package/dist/lib/daemon-services.js +5 -0
  88. package/dist/lib/device-config.js +61 -0
  89. package/dist/lib/devices/connect.d.ts +49 -5
  90. package/dist/lib/devices/connect.js +169 -21
  91. package/dist/lib/devices/doctor-findings.js +2 -6
  92. package/dist/lib/feed/answer.d.ts +153 -4
  93. package/dist/lib/feed/answer.js +716 -105
  94. package/dist/lib/feed/envelope.d.ts +79 -0
  95. package/dist/lib/feed/envelope.js +23 -0
  96. package/dist/lib/feed/events.d.ts +6 -0
  97. package/dist/lib/feed/events.js +8 -0
  98. package/dist/lib/feed/feed.d.ts +61 -1
  99. package/dist/lib/feed/feed.js +226 -14
  100. package/dist/lib/feed/hub-server.d.ts +141 -0
  101. package/dist/lib/feed/hub-server.js +586 -0
  102. package/dist/lib/feed/hub.d.ts +95 -0
  103. package/dist/lib/feed/hub.js +255 -0
  104. package/dist/lib/feed/pr-status.d.ts +8 -0
  105. package/dist/lib/feed/pr-status.js +9 -1
  106. package/dist/lib/feed/tool-activity.d.ts +108 -0
  107. package/dist/lib/feed/tool-activity.js +313 -0
  108. package/dist/lib/feed/tools.d.ts +198 -0
  109. package/dist/lib/feed/tools.js +265 -0
  110. package/dist/lib/feed/watch.d.ts +50 -50
  111. package/dist/lib/feed/watch.js +147 -16
  112. package/dist/lib/feed-outcome.d.ts +1 -1
  113. package/dist/lib/feed-outcome.js +9 -2
  114. package/dist/lib/feed-policy.js +9 -3
  115. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  116. package/dist/lib/fleet/auth-sync.js +2 -89
  117. package/dist/lib/format.d.ts +1 -1
  118. package/dist/lib/format.js +1 -1
  119. package/dist/lib/git.d.ts +0 -16
  120. package/dist/lib/git.js +0 -58
  121. package/dist/lib/harness-auth-capabilities.js +7 -2
  122. package/dist/lib/helper-versions.js +1 -1
  123. package/dist/lib/hosts/dispatch.d.ts +20 -1
  124. package/dist/lib/hosts/dispatch.js +52 -30
  125. package/dist/lib/hosts/remote-cmd.d.ts +72 -1
  126. package/dist/lib/hosts/remote-cmd.js +151 -10
  127. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  128. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  129. package/dist/lib/mailbox.d.ts +12 -0
  130. package/dist/lib/mailbox.js +16 -2
  131. package/dist/lib/mcp.js +17 -11
  132. package/dist/lib/menubar/snapshot.d.ts +51 -0
  133. package/dist/lib/menubar/snapshot.js +42 -3
  134. package/dist/lib/open-url.js +2 -2
  135. package/dist/lib/probe.d.ts +4 -1
  136. package/dist/lib/probe.js +5 -2
  137. package/dist/lib/projects.d.ts +23 -0
  138. package/dist/lib/projects.js +78 -0
  139. package/dist/lib/pwsh.d.ts +33 -0
  140. package/dist/lib/pwsh.js +56 -0
  141. package/dist/lib/redact.d.ts +8 -0
  142. package/dist/lib/redact.js +11 -0
  143. package/dist/lib/refresh.d.ts +6 -2
  144. package/dist/lib/refresh.js +92 -72
  145. package/dist/lib/secrets-cli.d.ts +11 -0
  146. package/dist/lib/secrets-cli.js +30 -0
  147. package/dist/lib/secrets-client.js +3 -2
  148. package/dist/lib/session/active.d.ts +1 -0
  149. package/dist/lib/session/active.js +8 -0
  150. package/dist/lib/session/db.d.ts +67 -3
  151. package/dist/lib/session/db.js +381 -126
  152. package/dist/lib/session/detached.d.ts +7 -0
  153. package/dist/lib/session/detached.js +29 -0
  154. package/dist/lib/session/prompt.d.ts +23 -7
  155. package/dist/lib/session/prompt.js +46 -8
  156. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  157. package/dist/lib/session/remote/peer-stream.js +33 -6
  158. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  159. package/dist/lib/session/remote/remote-list.js +22 -6
  160. package/dist/lib/session/remote/watch.d.ts +12 -0
  161. package/dist/lib/session/remote/watch.js +9 -0
  162. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  163. package/dist/lib/session/remote-preview-cache.js +373 -0
  164. package/dist/lib/session/sync/backend.d.ts +3 -3
  165. package/dist/lib/session/sync/backend.js +3 -3
  166. package/dist/lib/session/sync/provision.d.ts +1 -1
  167. package/dist/lib/session/sync/provision.js +2 -2
  168. package/dist/lib/session/tail.d.ts +50 -0
  169. package/dist/lib/session/tail.js +219 -0
  170. package/dist/lib/sessions-client.js +0 -3
  171. package/dist/lib/setup-tool-install.d.ts +3 -0
  172. package/dist/lib/setup-tool-install.js +27 -0
  173. package/dist/lib/setup-tool-status.d.ts +22 -0
  174. package/dist/lib/setup-tool-status.js +220 -0
  175. package/dist/lib/share-runtime.d.ts +11 -0
  176. package/dist/lib/share-runtime.js +63 -0
  177. package/dist/lib/signin-badge.d.ts +19 -4
  178. package/dist/lib/signin-badge.js +29 -11
  179. package/dist/lib/smart-launch.d.ts +1 -5
  180. package/dist/lib/smart-launch.js +3 -11
  181. package/dist/lib/ssh-exec.d.ts +44 -0
  182. package/dist/lib/ssh-exec.js +119 -0
  183. package/dist/lib/startup/command-registry.d.ts +6 -4
  184. package/dist/lib/startup/command-registry.js +10 -7
  185. package/dist/lib/state.js +2 -2
  186. package/dist/lib/storage/selection.d.ts +2 -2
  187. package/dist/lib/storage/selection.js +2 -2
  188. package/dist/lib/sync-umbrella.d.ts +5 -0
  189. package/dist/lib/sync-umbrella.js +18 -10
  190. package/dist/lib/term-driver.d.ts +24 -0
  191. package/dist/lib/term-driver.js +36 -0
  192. package/dist/lib/terminal/index.d.ts +1 -1
  193. package/dist/lib/terminal/index.js +1 -1
  194. package/dist/lib/terminal/inject.d.ts +38 -0
  195. package/dist/lib/terminal/inject.js +55 -9
  196. package/dist/lib/terminal/transport.d.ts +15 -5
  197. package/dist/lib/terminal/transport.js +61 -11
  198. package/dist/lib/traces/backend.d.ts +1 -2
  199. package/dist/lib/traces/backend.js +1 -2
  200. package/dist/lib/traces/provision.d.ts +1 -1
  201. package/dist/lib/traces/provision.js +2 -2
  202. package/dist/lib/types.d.ts +8 -6
  203. package/package.json +2 -3
  204. package/dist/commands/artifacts-setup.d.ts +0 -53
  205. package/dist/commands/artifacts-setup.js +0 -161
  206. package/dist/commands/artifacts.d.ts +0 -18
  207. package/dist/commands/artifacts.js +0 -58
  208. package/dist/commands/attach.d.ts +0 -12
  209. package/dist/commands/attach.js +0 -86
  210. package/dist/commands/auth-mint.d.ts +0 -12
  211. package/dist/commands/auth-mint.js +0 -108
  212. package/dist/commands/reconnect.d.ts +0 -46
  213. package/dist/commands/reconnect.js +0 -115
  214. package/dist/commands/share.d.ts +0 -293
  215. package/dist/commands/share.js +0 -1424
  216. package/dist/lib/fleet/remote-login.d.ts +0 -170
  217. package/dist/lib/fleet/remote-login.js +0 -568
  218. package/dist/lib/share/analytics.d.ts +0 -13
  219. package/dist/lib/share/analytics.js +0 -45
  220. package/dist/lib/share/backend.d.ts +0 -120
  221. package/dist/lib/share/backend.js +0 -176
  222. package/dist/lib/share/capture.d.ts +0 -31
  223. package/dist/lib/share/capture.js +0 -174
  224. package/dist/lib/share/config.d.ts +0 -72
  225. package/dist/lib/share/config.js +0 -211
  226. package/dist/lib/share/delete.d.ts +0 -123
  227. package/dist/lib/share/delete.js +0 -173
  228. package/dist/lib/share/html.d.ts +0 -20
  229. package/dist/lib/share/html.js +0 -88
  230. package/dist/lib/share/http-error.d.ts +0 -53
  231. package/dist/lib/share/http-error.js +0 -65
  232. package/dist/lib/share/og.d.ts +0 -26
  233. package/dist/lib/share/og.js +0 -84
  234. package/dist/lib/share/provision.d.ts +0 -127
  235. package/dist/lib/share/provision.js +0 -285
  236. package/dist/lib/share/publish.d.ts +0 -379
  237. package/dist/lib/share/publish.js +0 -818
  238. package/dist/lib/share/worker-template.d.ts +0 -27
  239. package/dist/lib/share/worker-template.js +0 -2424
  240. package/dist/lib/storage/index.d.ts +0 -14
  241. package/dist/lib/storage/index.js +0 -14
  242. package/dist/lib/storage/visibility.d.ts +0 -82
  243. package/dist/lib/storage/visibility.js +0 -99
@@ -4,7 +4,7 @@ import { buildServeEnv, invocation, resolveSecretsBin, SecretsClientError } from
4
4
  import { flagValue } from '../lib/hosts/routing-flag.js';
5
5
  import { stripRoutingFlags } from '../lib/hosts/remote-cmd.js';
6
6
  import { resolveRemoteDevice } from '../lib/ssh-tunnel.js';
7
- const INSTALL_HINT = 'npm i -g @phnx-labs/secrets-cli';
7
+ import { SECRETS_CLI_INSTALL_HINT } from '../lib/secrets-cli.js';
8
8
  /**
9
9
  * Rewrite a `--device <name>` (or `-D`) on the forwarded argv to
10
10
  * `--host ssh://user@host`, the address grammar `secrets` speaks (PHNX-4090).
@@ -39,7 +39,7 @@ export function registerSecretsCommands(program) {
39
39
  if (err instanceof SecretsClientError && err.code === 'SECRETS_BIN_MISSING') {
40
40
  console.error(chalk.red('The standalone `secrets` CLI is not installed.'));
41
41
  console.error(chalk.gray(`Install it, then re-run this command:`));
42
- console.error(chalk.cyan(` ${INSTALL_HINT}`));
42
+ console.error(chalk.cyan(` ${SECRETS_CLI_INSTALL_HINT}`));
43
43
  process.exit(1);
44
44
  }
45
45
  throw err;
@@ -10,10 +10,10 @@
10
10
  *
11
11
  * Compat: positional text still works (`agents send "hi" --channel … --to …`).
12
12
  *
13
- * `agents notify` is the same delivery path with owner defaults
14
- * (`send --to owner`) and is deprecated — new callers should use
15
- * `agents feed post`. Not a second stack. Not agent control — use
16
- * `agents message` / `agents sessions inject` for running agents.
13
+ * `send --to owner` is the owner-delivery path; `agents feed post --level
14
+ * important` records a milestone and broadcasts it through the same sink.
15
+ * Not a second stack. Not agent control — use `agents message` /
16
+ * `agents sessions inject` for running agents.
17
17
  *
18
18
  * Feed / activity are a different plane (record + read); feed.broadcast may
19
19
  * call this command as a forward sink.
@@ -9,7 +9,7 @@ function mergeAttachments(opts) {
9
9
  const list = [...(opts.attach ?? []), ...(opts.attachment ?? [])];
10
10
  return list.length ? list : undefined;
11
11
  }
12
- function toInput(positionalText, opts, ownerMode) {
12
+ function toInput(positionalText, opts) {
13
13
  return {
14
14
  text: opts.text,
15
15
  positionalText,
@@ -20,18 +20,17 @@ function toInput(positionalText, opts, ownerMode) {
20
20
  urls: opts.url,
21
21
  from: opts.from,
22
22
  dryRun: opts.dryRun,
23
- ownerMode,
24
23
  };
25
24
  }
26
- async function runSend(positionalText, opts, ownerMode) {
25
+ async function runSend(positionalText, opts) {
27
26
  const meta = readMeta();
28
- let input = toInput(positionalText, opts, ownerMode);
29
- // An owner-bound ping (`agents notify`, `agents send --to owner`) goes through
27
+ let input = toInput(positionalText, opts);
28
+ // An owner-bound ping (`agents send --to owner`) goes through
30
29
  // the SAME composer as an important `feed post` (PHNX-3698): short-shaped body,
31
30
  // TEAM-N keys linkified, session crumb as a tappable console URL — instead of a
32
31
  // raw dump. A non-owner send (explicit --channel/--to) is delivered verbatim.
33
32
  let ownerCompose;
34
- if (ownerMode || isOwnerAlias(opts.to)) {
33
+ if (isOwnerAlias(opts.to)) {
35
34
  const flagged = opts.text?.trim() ?? '';
36
35
  const positional = (positionalText ?? '').trim();
37
36
  const raw = flagged || positional;
@@ -112,9 +111,6 @@ export function registerSendCommand(program) {
112
111
  # Attach a local file
113
112
  agents send --to owner --text "screenshot" --attach ./out/cover.png
114
113
 
115
- # Owner alias without the notify verb
116
- agents send --to owner --text "wiring test"
117
-
118
114
  # Legacy positional text still works
119
115
  agents send "hi" --channel desktop --to local
120
116
 
@@ -124,47 +120,6 @@ export function registerSendCommand(program) {
124
120
  notes: SHARED_NOTES,
125
121
  });
126
122
  sendCmd.action(async (text, opts) => {
127
- await runSend(text, opts, false);
128
- });
129
- const notifyCmd = program
130
- .command('notify [text]')
131
- .description('[DEPRECATED] Deliver to the owner (alias of send --to owner). Use "agents feed post" for new code.')
132
- .option('--text <text>', 'message body (preferred over positional text)')
133
- .option('--channel <name>', 'override owner channel')
134
- .option('--to <target>', 'override owner target (or pass a non-owner dest with --channel)')
135
- .option('--thread <id>', 'channel thread id / timestamp')
136
- .option('--attach <path...>', 'local file attachment path (repeatable)')
137
- .option('--attachment <path...>', 'alias of --attach')
138
- .option('--url <url...>', 'link or remote media URL to include in the body (repeatable)')
139
- .option('--from <who>', 'sender label (mailbox)')
140
- .option('--json', 'output JSON')
141
- .option('--dry-run', 'resolve + build but do not send');
142
- setHelpSections(notifyCmd, {
143
- examples: `
144
- # Deprecated: prefer "agents feed post --title \"…\" \"…\" --level important"
145
- agents notify --text "Build finished — PR #1346 is green"
146
- agents notify "legacy positional still works"
147
-
148
- # Override channel for this one ping
149
- agents notify --text "fallback" --channel desktop --to local
150
-
151
- agents notify --text "probe" --dry-run --json
152
- `,
153
- notes: `
154
- DEPRECATED. notify ≡ send --to owner and still works, but new callers
155
- should use "agents feed post" (record + optional broadcast) instead.
156
- Set owner.channels + owner.policy.normal in humans.yaml once per fleet.
157
- Every channel listed in the normal policy receives an owner-addressed send.
158
-
159
- Owner sends go through the same composer as "feed post": the body is
160
- short-shaped, any TEAM-N key becomes a Linear URL, and the session crumb
161
- becomes a tappable https://prix.dev/console/sessions/<id> link.
162
-
163
- ${SHARED_NOTES}
164
- `,
165
- });
166
- notifyCmd.action(async (text, opts) => {
167
- console.error(chalk.yellow('Warning: "agents notify" is deprecated. Use "agents feed post" for progress posts that can also reach the owner.'));
168
- await runSend(text, opts, true);
123
+ await runSend(text, opts);
169
124
  });
170
125
  }
@@ -10,7 +10,7 @@
10
10
  // the same idempotent `deployWorker`/`createBucket` primitives. Provisioning is
11
11
  // idempotent — re-running redeploys the current Worker template in place.
12
12
  import chalk from 'chalk';
13
- import { DEFAULT_CF_BUNDLE, readCloudflareCreds } from '../lib/share/config.js';
13
+ import { DEFAULT_CF_BUNDLE, readCloudflareCreds } from '../lib/cloudflare/creds.js';
14
14
  import { PHOENIX_ID_BASE } from '../lib/identity/client.js';
15
15
  import { provisionSessions } from '../lib/session/sync/provision.js';
16
16
  import { DEFAULT_SESSIONS_BUCKET_NAME, DEFAULT_SESSIONS_DOMAIN, DEFAULT_SESSIONS_WORKER_NAME, } from '../lib/session/sync/managed-config.js';
@@ -153,6 +153,17 @@ export interface SessionPreviewDigest {
153
153
  firstError?: string;
154
154
  toolHistogram: ReturnType<typeof toolHistogram>;
155
155
  test: ReturnType<typeof detectTestResult>;
156
+ /**
157
+ * True when this digest was built WITHOUT parsing the transcript (PHNX-3999):
158
+ * the file exceeded {@link PREVIEW_DIGEST_MAX_PARSE_BYTES} and no cached
159
+ * digest existed yet. `firstUser`/`lastAssistant` fall back to the already-
160
+ * indexed `SessionMeta.firstUserMessage`/`lastUserMessage` (cheap, no parse)
161
+ * rather than being silently empty; every event-derived field (toolCalls,
162
+ * changedFiles, artifacts, etc.) stays at its zero-value default because it
163
+ * genuinely was not computed, not because nothing happened.
164
+ */
165
+ partial?: boolean;
166
+ partialReason?: string;
156
167
  }
157
168
  /** Fold a harness-normalized event stream into the stable preview data model. */
158
169
  export declare function buildSessionPreviewDigest(events: SessionEvent[], session: SessionMeta): SessionPreviewDigest;
@@ -8,12 +8,13 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import chalk from 'chalk';
11
- import { truncate, humanDuration } from '../lib/format.js';
11
+ import { truncate, humanDuration, formatBytes } from '../lib/format.js';
12
12
  import { sessionDisplayAgent } from '../lib/session/types.js';
13
13
  import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
14
14
  import { parseSession, sanitizeForTerminal, SNAPSHOT_TODO_TOOLS } from '../lib/session/parse.js';
15
+ import { readSessionTail, readSessionHead } from '../lib/session/tail.js';
15
16
  import { safeTeamText } from '../lib/session/team-filter.js';
16
- import { cleanSessionPrompt, extractSessionTopic, isSyntheticUserMessage } from '../lib/session/prompt.js';
17
+ import { cleanSessionPrompt, extractSessionTopic, isSyntheticUserMessage, firstUserMessageFromEvents } from '../lib/session/prompt.js';
17
18
  import { linkPath, linkUrl, relativeToCwd, shortenModel } from '../lib/session/render.js';
18
19
  import { linearIssueUrl } from '../lib/session/linear.js';
19
20
  import { extractTodoProgress, WORKTREE_RE } from '../lib/session/state.js';
@@ -324,7 +325,14 @@ export function loadSessionPreviewDigest(session) {
324
325
  archived.plugins = getSessionPlugins(session.id);
325
326
  return { digest: archived, events: [] };
326
327
  }
327
- return { events: [] };
328
+ // No file on disk AND no archived digest: a metadata-only row (a Rush
329
+ // dispatch/audit row, a synthesized attach-only entry, a live session not
330
+ // yet indexed) with nothing to read (PHNX-3999). Callers that only
331
+ // destructure `digest` (the picker's "not indexed here" note) are
332
+ // unaffected; a JSON caller reading `error` gets a truthful reason instead
333
+ // of a `preview: null` that looks identical to "this session genuinely has
334
+ // no content yet".
335
+ return { events: [], error: 'no local transcript for this session (metadata-only entry, no archived digest)' };
328
336
  }
329
337
  const safe = sanitizeMeta(session);
330
338
  let events = [];
@@ -340,9 +348,53 @@ export function loadSessionPreviewDigest(session) {
340
348
  fileSize: sourceStamp.size,
341
349
  });
342
350
  if (!digest) {
343
- try {
344
- events = parseSession(session.filePath, session.agent);
351
+ if (sourceStamp.size > PREVIEW_DIGEST_MAX_PARSE_BYTES) {
352
+ // A full `parseSession` on a cache miss is a synchronous, unbounded
353
+ // whole-file parse with no time/byte cap of its own (PHNX-3999) — real
354
+ // 6.3 MiB and 35.7 MiB screenshot-heavy transcripts on this fleet both
355
+ // lacked a computed digest/timeline, consistent with this path not
356
+ // finishing in a reasonable request budget for either. `PREVIEW_DIGEST_MAX_PARSE_BYTES`
357
+ // is deliberately smaller than the daemon's own background-work ceiling
358
+ // (see that constant's own doc) so this catches both real cases.
359
+ //
360
+ // This IS genuinely partial, not empty: rather than parsing nothing,
361
+ // `readSessionTail` (`tail.ts`) reads only the LAST 128 KiB of the file
362
+ // (already the live-view's own bounded reader, reused verbatim — no new
363
+ // parse logic) for a real recent-events window on the two harnesses it
364
+ // supports (Claude/Codex); event-derived fields below (toolCalls,
365
+ // toolTags, etc.) reflect that tail window, not the whole session, which
366
+ // `partialReason` states explicitly.
367
+ //
368
+ // `firstUser` is NEVER set from the tail fold: a tail window's "first
369
+ // user message IN THAT WINDOW" is a mid-session follow-up on any
370
+ // multi-turn session, not the session's actual original request, and
371
+ // there is no honest way to tell the two apart from the tail alone.
372
+ // Falling back to it would silently mislabel a follow-up as the
373
+ // original ask. Preference order for the CANONICAL original request:
374
+ // (1) the already-indexed `SessionMeta.firstUserMessage` (zero
375
+ // extra I/O); (2) failing that, a bounded HEAD read (`readSessionHead`,
376
+ // `tail.ts` — the mirror of the tail reader, first ~32 KiB from byte 0,
377
+ // where a session's opening turn always lives) so a row with no indexed
378
+ // value yet still gets the REAL original request rather than nothing.
379
+ // Only when neither is available does `firstUser` stay empty —
380
+ // `partial`/`partialReason` already say why detail is missing, which is
381
+ // the truthful signal, never a guessed value. The digest is cached
382
+ // against this stamp so the bound is paid once per transcript version,
383
+ // not once per call.
384
+ events = readSessionTail(session.filePath, session.agent);
345
385
  digest = buildSessionPreviewDigest(events, safe);
386
+ if (session.firstUserMessage) {
387
+ digest.firstUser = session.firstUserMessage;
388
+ }
389
+ else {
390
+ // Reuse the canonical extractor (rejects synthetic/system-injected
391
+ // turns, unwraps a Grok/Cursor <user_query> wrapper) rather than a
392
+ // bespoke inline find — the same rules SessionMeta.firstUserMessage
393
+ // itself was built with.
394
+ digest.firstUser = firstUserMessageFromEvents(readSessionHead(session.filePath, session.agent)) ?? '';
395
+ }
396
+ digest.partial = true;
397
+ digest.partialReason = `transcript is ${formatBytes(sourceStamp.size)}, over the ${formatBytes(PREVIEW_DIGEST_MAX_PARSE_BYTES)} bounded-parse limit for an uncached preview; digest reflects only the last ~128 KiB (tail) of the transcript, not the whole session`;
346
398
  writeSessionPreviewCache({
347
399
  id: session.id,
348
400
  fileMtimeMs: sourceStamp.mtimeMs,
@@ -350,8 +402,20 @@ export function loadSessionPreviewDigest(session) {
350
402
  preview: digest,
351
403
  });
352
404
  }
353
- catch (err) {
354
- return { events, error: sanitizeForTerminal(err?.message ?? String(err)) };
405
+ else {
406
+ try {
407
+ events = parseSession(session.filePath, session.agent);
408
+ digest = buildSessionPreviewDigest(events, safe);
409
+ writeSessionPreviewCache({
410
+ id: session.id,
411
+ fileMtimeMs: sourceStamp.mtimeMs,
412
+ fileSize: sourceStamp.size,
413
+ preview: digest,
414
+ });
415
+ }
416
+ catch (err) {
417
+ return { events, error: sanitizeForTerminal(err?.message ?? String(err)) };
418
+ }
355
419
  }
356
420
  }
357
421
  digest.plugins = getSessionPlugins(session.id);
@@ -711,6 +775,23 @@ const DIRS_TOUCHED_MAX = 5;
711
775
  // enough to cover any real session's edits, low enough that a runaway rewrite
712
776
  // can't bloat the cached JSON. The `changes` counts stay the true totals.
713
777
  const CHANGED_FILES_MAX = 200;
778
+ /**
779
+ * Bound for an uncached `loadSessionPreviewDigest` parse (PHNX-3999).
780
+ *
781
+ * Deliberately SMALLER than the daemon's own
782
+ * `TIMELINE_PASS_MAX_WHOLE_FILE_BYTES` (16 MiB, `timeline-pass.ts`) — that
783
+ * number bounds BACKGROUND work the daemon tick can afford to spend; this one
784
+ * bounds a SYNCHRONOUS request an interactive caller (the Menu, with its own
785
+ * end-to-end latency budget) is actively waiting on. Two real screenshot-heavy
786
+ * transcripts on this fleet (6.3 MiB and 35.7 MiB) both lacked any computed
787
+ * digest/timeline — a 16 MiB threshold here would still miss the smaller one.
788
+ * The exact per-byte cost of `parseSession` is not measured in this change;
789
+ * 4 MiB is a conservative invariant (well under the 6.3 MiB failure case,
790
+ * comfortably above ordinary non-screenshot transcript sizes), not a timing
791
+ * guarantee — verify against real transcripts before relying on a specific
792
+ * elapsed-time bound.
793
+ */
794
+ const PREVIEW_DIGEST_MAX_PARSE_BYTES = 4 * 1024 * 1024;
714
795
  /** Fold a harness-normalized event stream into the stable preview data model. */
715
796
  export function buildSessionPreviewDigest(events, session) {
716
797
  let firstUser = '';
@@ -95,7 +95,6 @@ export function registerSessionsResumeCommand(sessionsCmd) {
95
95
  - Attach a live pane without forking: agents sessions focus <id>.
96
96
  - This is the ONE verb for getting back in. It detects the state: a live tmux pane is attached, a headless session comes to the foreground, an ended one recovers on its owning device.
97
97
  - A UUID/prefix or ag-<agent>-<suffix> alias bypasses the picker. A live alias attaches by name even when the session index cannot attribute it.
98
- - Retired spellings still work for one release and print the replacement: sessions attach, sessions go, reconnect.
99
98
  - Going the other way (foreground -> background) is 'agents sessions detach <id>'.
100
99
  - With no identity selector, space toggles a session, enter confirms, and tab toggles the preview pane.
101
100
  - Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
@@ -1,6 +1,5 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { SessionMeta } from '../lib/session/types.js';
3
- import { type PublishOptions } from '../lib/share/publish.js';
4
3
  interface ShareOptions {
5
4
  public?: boolean;
6
5
  slug?: string;
@@ -13,13 +12,12 @@ interface ShareOptions {
13
12
  /** `session-<shortId>` — stable per session, so re-sharing updates the same URL. */
14
13
  export declare function defaultSessionSlug(session: SessionMeta): string;
15
14
  /**
16
- * Map the command's flags onto {@link PublishOptions}.
15
+ * Build the `artifacts share` argv for a session publish.
17
16
  *
18
- * Extracted so the defaults are testable directly. `unlisted` in particular is the
19
- * security-relevant one — it inverts the parent command's public-by-default
20
- * behavior, and a test that re-implements this mapping would still pass with it
21
- * flipped.
17
+ * Extracted so the mapping is testable directly. The security-relevant default
18
+ * is `--visibility unlisted` — it inverts `artifacts share`'s public default, and
19
+ * a test that re-implements this mapping would still pass with it flipped.
22
20
  */
23
- export declare function buildSharePublishOptions(session: SessionMeta, options: Pick<ShareOptions, 'public' | 'slug' | 'label' | 'expire' | 'force' | 'cover'>): PublishOptions;
21
+ export declare function buildArtifactsShareArgs(session: SessionMeta, options: Pick<ShareOptions, 'public' | 'slug' | 'label' | 'expire' | 'force' | 'cover'>, file: string): string[];
24
22
  export declare function registerSessionsShareCommand(sessionsCmd: Command): void;
25
23
  export {};
@@ -1,27 +1,29 @@
1
1
  /**
2
2
  * `agents sessions share <id>` — publish one session transcript as a link.
3
3
  *
4
- * Composes three pieces that already existed but were never wired together:
5
- * `renderSessionMarkdownDocument()` (redacted transcript),
6
- * `renderSessionHtmlDocument()` (self-contained branded page), and
7
- * `publishFile()` (the R2-backed share Worker). Before this, sharing a session
8
- * meant three commands and a detour through the external artifacts-cli.
4
+ * Renders the session locally — `renderSessionMarkdownDocument()` (redacted
5
+ * transcript) and `renderSessionHtmlDocument()` (self-contained branded page) —
6
+ * then publishes it through the standalone `artifacts` CLI (`artifacts share`),
7
+ * the single home for artifact sharing since PHNX-3992. agents-cli holds no
8
+ * share engine of its own; this command contributes the session-specific
9
+ * rendering and redaction, and forwards the finished page to `artifacts share`.
9
10
  *
10
- * Unlisted by default, unlike `agents artifacts share`. A transcript carries file
11
- * paths, command output, error text, and whatever a tool printed — strictly more
12
- * than a plan does — so it does not belong in the public `/<user>` gallery unless
13
- * the operator asks for it with `--public`. The URL itself stays world-readable:
14
- * unlisted is a capability URL, not a secret.
11
+ * Unlisted by default, unlike `artifacts share` (public by default). A transcript
12
+ * carries file paths, command output, error text, and whatever a tool printed —
13
+ * strictly more than a plan does — so it does not belong in the public `/<user>`
14
+ * gallery unless the operator asks for it with `--public`. The URL itself stays
15
+ * world-readable: unlisted is a capability URL, not a secret.
15
16
  */
16
17
  import * as fs from 'fs';
17
18
  import * as os from 'os';
18
19
  import * as path from 'path';
20
+ import { spawnSync } from 'node:child_process';
19
21
  import chalk from 'chalk';
20
22
  import { setHelpSections } from '../lib/help.js';
21
- import { knownSecretValuesFromEnv } from '../lib/redact.js';
23
+ import { knownSecretValuesFromEnv, redactEmails } from '../lib/redact.js';
22
24
  import { discoverSessions } from '../lib/session/discover.js';
23
25
  import { renderSessionHtmlDocument } from '../lib/session/share-html.js';
24
- import { publishFile, redactEmails } from '../lib/share/publish.js';
26
+ import { resolveArtifactsBin, invocation, ArtifactsClientError } from '../lib/artifacts-client.js';
25
27
  import { renderSessionMarkdownDocument } from './sessions-render.js';
26
28
  import { selectSessions } from './sessions-export.js';
27
29
  import { parseAgentFilter } from './sessions.js';
@@ -35,23 +37,33 @@ export function defaultSessionSlug(session) {
35
37
  return `session-${session.shortId || session.id}`;
36
38
  }
37
39
  /**
38
- * Map the command's flags onto {@link PublishOptions}.
40
+ * Build the `artifacts share` argv for a session publish.
39
41
  *
40
- * Extracted so the defaults are testable directly. `unlisted` in particular is the
41
- * security-relevant one — it inverts the parent command's public-by-default
42
- * behavior, and a test that re-implements this mapping would still pass with it
43
- * flipped.
42
+ * Extracted so the mapping is testable directly. The security-relevant default
43
+ * is `--visibility unlisted` — it inverts `artifacts share`'s public default, and
44
+ * a test that re-implements this mapping would still pass with it flipped.
44
45
  */
45
- export function buildSharePublishOptions(session, options) {
46
- return {
47
- slug: options.slug ?? defaultSessionSlug(session),
48
- unlisted: options.public !== true,
49
- expire: options.expire,
50
- force: options.force === true,
51
- cover: options.cover !== false,
52
- label: options.label,
53
- meta: { kind: 'session' },
54
- };
46
+ export function buildArtifactsShareArgs(session, options, file) {
47
+ const args = [
48
+ 'share',
49
+ file,
50
+ '--slug',
51
+ options.slug ?? defaultSessionSlug(session),
52
+ '--visibility',
53
+ options.public === true ? 'public' : 'unlisted',
54
+ '--meta',
55
+ 'kind=session',
56
+ '--json',
57
+ ];
58
+ if (options.expire)
59
+ args.push('--expire', options.expire);
60
+ if (options.force === true)
61
+ args.push('--force');
62
+ if (options.cover === false)
63
+ args.push('--no-cover');
64
+ if (options.label)
65
+ args.push('--label', options.label);
66
+ return args;
55
67
  }
56
68
  export function registerSessionsShareCommand(sessionsCmd) {
57
69
  const cmd = sessionsCmd
@@ -59,7 +71,7 @@ export function registerSessionsShareCommand(sessionsCmd) {
59
71
  .description('Publish one session as a redacted, self-contained web page and print the link.')
60
72
  .option('--public', 'List the page in your public share gallery (default: unlisted capability URL)')
61
73
  .option('--slug <slug>', 'URL slug under your namespace (default: session-<shortId>)')
62
- .option('--label <text>', 'Display title in the gallery and `agents artifacts share list`')
74
+ .option('--label <text>', 'Display title in the gallery and `artifacts share list`')
63
75
  .option('--expire <spec>', 'Auto-expire window: 30d, 12h, a date, or never (default: 30d)')
64
76
  .option('--reasoning <mode>', 'Reasoning visibility: omit, fold, or include', 'omit')
65
77
  .option('--force', 'Publish despite the sensitive-content scan flagging the transcript')
@@ -76,22 +88,23 @@ agents sessions share a1b2c3d4 --reasoning fold
76
88
 
77
89
  # A link that does not decay
78
90
  agents sessions share a1b2c3d4 --expire never`,
79
- notes: `Requires a share endpoint: 'agents artifacts share status' shows it, 'agents artifacts setup'
80
- provisions one, 'agents artifacts share join <baseUrl>' uses an existing one.
91
+ notes: `Publishes through the standalone artifacts CLI — install it with
92
+ 'npm i -g @phnx-labs/artifacts-cli'. Sign in with
93
+ 'artifacts auth login' for the managed endpoint (zero Cloudflare setup), or configure
94
+ your own bucket with 'artifacts share setup' / 'artifacts share join <baseUrl>'.
81
95
 
82
96
  Unlisted by default — the URL is world-readable but the page stays out of your public
83
- gallery and out of 'agents artifacts share list'. Pass --public to list it.
97
+ gallery and out of 'artifacts share list'. Pass --public to list it.
84
98
 
85
- Secrets are redacted and the same pre-publish scan as 'agents artifacts share' runs on the
86
- page, so a transcript carrying emails or credential-shaped strings is refused
87
- unless you pass --force. --no-redact (the sessions-level flag) disables redaction
88
- and is a bad idea for anything you publish.
99
+ Secrets are redacted and 'artifacts share' runs its own pre-publish scan on the page,
100
+ so a transcript carrying emails or credential-shaped strings is refused unless you pass
101
+ --force. --no-redact (the sessions-level flag) disables redaction and is a bad idea for
102
+ anything you publish.
89
103
 
90
104
  Re-running with the same session updates the same URL and keeps the prior version
91
- as a revision ('agents artifacts share revisions <slug>').
105
+ as a revision ('artifacts share revisions <slug>').
92
106
 
93
- Manage published sessions with 'agents artifacts share list' and
94
- 'agents artifacts share delete <slug>'.`,
107
+ Manage published sessions with 'artifacts share list' and 'artifacts share delete <slug>'.`,
95
108
  });
96
109
  cmd.action(async (selector, options, command) => {
97
110
  const globals = command.optsWithGlobals();
@@ -125,25 +138,60 @@ Manage published sessions with 'agents artifacts share list' and
125
138
  });
126
139
  // Emails on top of what the renderer masks. Almost every real transcript
127
140
  // carries a few — git author addresses, `gh api user`, a pasted log — and
128
- // publishToEndpoint refuses a body containing any (RUSH-2428). Masking them
129
- // means the published page genuinely does not carry them; the alternative,
130
- // telling people to pass --force, would train everyone to bypass the gate
131
- // that also catches real credentials.
141
+ // `artifacts share` refuses a body containing any (its own scan). Masking
142
+ // them means the published page genuinely does not carry them; the
143
+ // alternative, telling people to pass --force, trains everyone to bypass the
144
+ // gate that also catches real credentials.
132
145
  //
133
146
  // Applied to the RENDERED PAGE, not the Markdown, so the text that is masked
134
- // is exactly the text `scanShareContent` will scan. Markdown escaping stands
147
+ // is exactly the text the publish scan will see. Markdown escaping stands
135
148
  // between the two: `foo\@example.com` hides from the pattern in the Markdown
136
149
  // and reappears as a live address once marked drops the backslash.
137
150
  const page = renderSessionHtmlDocument(session, markdown, { redacted: redact });
138
151
  const html = redact ? redactEmails(page) : page;
139
- // A real file on disk is what publishFile() takes, and the OG capturer opens
140
- // it in a browser. 0600 + a per-run directory keeps the intermediate off a
141
- // world-readable /tmp path while it exists.
152
+ // A real file on disk is what `artifacts share` takes, and its OG capturer
153
+ // opens it in a browser. 0600 + a per-run directory keeps the intermediate
154
+ // off a world-readable /tmp path while it exists.
142
155
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-session-share-'));
143
156
  const file = path.join(dir, `${defaultSessionSlug(session)}.html`);
144
157
  try {
145
158
  fs.writeFileSync(file, html, { mode: 0o600 });
146
- const result = await publishFile(file, buildSharePublishOptions(session, options));
159
+ let bin;
160
+ try {
161
+ bin = resolveArtifactsBin();
162
+ }
163
+ catch (err) {
164
+ if (err instanceof ArtifactsClientError) {
165
+ process.stderr.write(chalk.red(err.message + '\n'));
166
+ process.exitCode = 1;
167
+ return;
168
+ }
169
+ throw err;
170
+ }
171
+ const { command: execCommand, prefix } = invocation(bin);
172
+ const args = [...prefix, ...buildArtifactsShareArgs(session, options, file)];
173
+ const proc = spawnSync(execCommand, args, { encoding: 'utf-8' });
174
+ if (proc.error)
175
+ throw proc.error;
176
+ if (proc.status !== 0) {
177
+ process.stderr.write(proc.stderr || chalk.red(`artifacts share exited ${proc.status ?? 'with no status'}\n`));
178
+ process.exitCode = proc.status ?? 1;
179
+ return;
180
+ }
181
+ let result;
182
+ try {
183
+ result = JSON.parse(proc.stdout);
184
+ }
185
+ catch {
186
+ // A 0 exit with non-JSON stdout (a version-skewed `artifacts` that
187
+ // printed a banner before the JSON, or one predating `--json` here) must
188
+ // fail loud with the bytes it actually returned, not an uncaught parse
189
+ // stack trace — mirroring `secrets-client.ts`'s `parseResponse`.
190
+ const preview = proc.stdout.trim().slice(0, 200);
191
+ process.stderr.write(chalk.red(`artifacts share returned a non-JSON response${preview ? `: ${preview}` : ' (empty output)'}\n`));
192
+ process.exitCode = 1;
193
+ return;
194
+ }
147
195
  if (globals.json) {
148
196
  process.stdout.write(JSON.stringify({
149
197
  session: session.id,
@@ -154,9 +202,10 @@ Manage published sessions with 'agents artifacts share list' and
154
202
  return;
155
203
  }
156
204
  process.stdout.write(`${result.url}\n`);
157
- const bits = [result.unlisted ? 'unlisted' : 'public', redact ? 'redacted' : chalk.red('NOT redacted')];
205
+ const visibility = result.visibility ?? (result.unlisted ? 'unlisted' : options.public ? 'public' : 'unlisted');
206
+ const bits = [visibility, redact ? 'redacted' : chalk.red('NOT redacted')];
158
207
  if (result.expiresAt)
159
- bits.push(`expires ${result.expiresAt.slice(0, 10)}`);
208
+ bits.push(`expires ${String(result.expiresAt).slice(0, 10)}`);
160
209
  process.stderr.write(chalk.dim(`${bits.join(' · ')}\n`));
161
210
  }
162
211
  finally {
@@ -1,13 +1,13 @@
1
1
  import { type Command } from 'commander';
2
2
  import { type ProjectDef } from '../lib/projects.js';
3
- import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types.js';
3
+ import type { SessionAgentId, SessionEvent, SessionMeta, ViewMode } from '../lib/session/types.js';
4
4
  import { type ActiveSession } from '../lib/session/active.js';
5
5
  export { activeSessionProjectKey, backfillActiveRowsFromIndex, backfillActiveRowsFromMeta, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson, type BackfillMeta } from '../lib/session/active.js';
6
6
  import { loadLocalActiveSessions } from '../lib/session/session-cache.js';
7
7
  import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
8
8
  import { type RemoteAgentsJsonParseResult } from '../lib/remote-agents-json.js';
9
9
  import { type RunMeta } from '../lib/scheduling/routines.js';
10
- import { type PickedSession } from './sessions-picker.js';
10
+ import { type PickedSession, type SessionPreviewDigest } from './sessions-picker.js';
11
11
  import { type ComputerRunRow } from '../lib/computer/sessions-list.js';
12
12
  import { type ToolSearchEnvelope, type ToolProgramCountEnvelope } from '../lib/session/tool-index.js';
13
13
  interface SessionFilterOptions {
@@ -466,6 +466,23 @@ export declare function printRoutineDrilldown(drill: RoutineDrilldown, liveIndex
466
466
  hiddenCount?: number;
467
467
  hiddenUnmanaged?: number;
468
468
  }): void;
469
+ export interface SessionDetailMessage {
470
+ role: 'user' | 'assistant';
471
+ text: string;
472
+ at: string | null;
473
+ }
474
+ export declare function buildSessionDetailBlock(session: SessionMeta, digest: SessionPreviewDigest | undefined, events: SessionEvent[], sourceStamp?: {
475
+ fileMtimeMs: number;
476
+ fileSize: number;
477
+ }): {
478
+ request: unknown;
479
+ timeline: unknown;
480
+ files: unknown;
481
+ messages: SessionDetailMessage[];
482
+ sourceRevision: string | null;
483
+ partial: boolean;
484
+ reason: string | null;
485
+ };
469
486
  /** Resolve a session by id/query globally and print its compact preview (no pager).
470
487
  * Backs `--preview` — the fast path for the "peek before resume" hot loop. */
471
488
  export declare function renderSessionPreview(query: string, scope: {
@@ -474,6 +491,8 @@ export declare function renderSessionPreview(query: string, scope: {
474
491
  local?: boolean;
475
492
  hosts?: string[];
476
493
  json?: boolean;
494
+ refresh?: boolean;
495
+ revision?: string;
477
496
  }): Promise<void>;
478
497
  /**
479
498
  * The one-line live status banner shown above a session preview: the glyph, the