pi-harness-runtime 1.1.390 → 1.1.392

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 (172) hide show
  1. package/AGENTS.md +38 -11
  2. package/harness/startup-report.ts +36 -6
  3. package/harness/telegram-auth.ts +32 -0
  4. package/harness/telegram-update.ts +125 -0
  5. package/index.ts +122 -29
  6. package/package.json +1 -1
  7. package/packages/a2a-adapter/package.json +1 -1
  8. package/packages/agent-policy/package.json +1 -1
  9. package/packages/architecture-generator/package.json +1 -1
  10. package/packages/auth/package.json +1 -1
  11. package/packages/autonomous-refactor/package.json +1 -1
  12. package/packages/autonomous-runtime/package.json +1 -1
  13. package/packages/cache-strategy/package.json +1 -1
  14. package/packages/capability-registry/package.json +1 -1
  15. package/packages/checkpoint/package.json +1 -1
  16. package/packages/cli-plugin-sdk/package.json +1 -1
  17. package/packages/clipboard/package.json +1 -1
  18. package/packages/clipboard-plugin/package.json +1 -1
  19. package/packages/code-generation/package.json +1 -1
  20. package/packages/code-review/package.json +1 -1
  21. package/packages/codex-adapter/package.json +1 -1
  22. package/packages/codex-watcher/package.json +1 -1
  23. package/packages/config-capture/package.json +1 -1
  24. package/packages/connection-runtime/package.json +1 -1
  25. package/packages/context-compiler/package.json +1 -1
  26. package/packages/context-discovery/package.json +1 -1
  27. package/packages/context-manager/package.json +1 -1
  28. package/packages/contracts/dist/workspace-dotenv.d.ts +44 -0
  29. package/packages/contracts/dist/workspace-dotenv.js +111 -0
  30. package/packages/contracts/dist/workspace-env-format.d.ts +185 -9
  31. package/packages/contracts/dist/workspace-env-format.js +402 -33
  32. package/packages/contracts/dist/workspace-env.d.ts +10 -6
  33. package/packages/contracts/dist/workspace-env.js +459 -112
  34. package/packages/contracts/package.json +1 -1
  35. package/packages/contracts/src/workspace-dotenv.ts +114 -0
  36. package/packages/contracts/src/workspace-env-format.ts +464 -29
  37. package/packages/contracts/src/workspace-env.ts +503 -114
  38. package/packages/contracts/test/workspace-env.test.ts +492 -11
  39. package/packages/cookie-sanitizer/package.json +1 -1
  40. package/packages/cost-optimizer/package.json +1 -1
  41. package/packages/dependency-analyzer/package.json +1 -1
  42. package/packages/django-plugin/package.json +1 -1
  43. package/packages/doc-generator/package.json +1 -1
  44. package/packages/durable-review/package.json +1 -1
  45. package/packages/e2e-tester/package.json +1 -1
  46. package/packages/evaluation-engine/package.json +1 -1
  47. package/packages/evaluation-runner/package.json +1 -1
  48. package/packages/event-bus/package.json +1 -1
  49. package/packages/event-sink/package.json +1 -1
  50. package/packages/event-store/package.json +1 -1
  51. package/packages/experience-replay/package.json +1 -1
  52. package/packages/feedback-collector/package.json +1 -1
  53. package/packages/file-copy-helper/package.json +1 -1
  54. package/packages/framework-detector/package.json +1 -1
  55. package/packages/framework-plugin-sdk/package.json +1 -1
  56. package/packages/frappe-plugin/package.json +1 -1
  57. package/packages/generic-web-plugin/package.json +1 -1
  58. package/packages/health-monitor/package.json +1 -1
  59. package/packages/intent-analyzer/package.json +1 -1
  60. package/packages/jev-judge/package.json +1 -1
  61. package/packages/knowledge-graph/package.json +1 -1
  62. package/packages/knowledge-retrieval/package.json +1 -1
  63. package/packages/laravel-plugin/package.json +1 -1
  64. package/packages/learning-engine/package.json +1 -1
  65. package/packages/mcp-adapter/package.json +1 -1
  66. package/packages/memory-engine/package.json +1 -1
  67. package/packages/milestone-manager/package.json +1 -1
  68. package/packages/model-registry/package.json +1 -1
  69. package/packages/nextjs-plugin/package.json +1 -1
  70. package/packages/notification/dist/adapters/line-adapter.d.ts +39 -0
  71. package/packages/notification/dist/adapters/line-adapter.d.ts.map +1 -1
  72. package/packages/notification/dist/adapters/line-adapter.js +119 -8
  73. package/packages/notification/dist/adapters/line-adapter.js.map +1 -1
  74. package/packages/notification/dist/adapters/telegram-adapter.d.ts +95 -0
  75. package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
  76. package/packages/notification/dist/adapters/telegram-adapter.js +284 -18
  77. package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
  78. package/packages/notification/dist/base-adapter.d.ts +20 -0
  79. package/packages/notification/dist/base-adapter.d.ts.map +1 -1
  80. package/packages/notification/dist/base-adapter.js +17 -0
  81. package/packages/notification/dist/base-adapter.js.map +1 -1
  82. package/packages/notification/dist/notification-center.d.ts +8 -0
  83. package/packages/notification/dist/notification-center.d.ts.map +1 -1
  84. package/packages/notification/dist/notification-center.js +11 -0
  85. package/packages/notification/dist/notification-center.js.map +1 -1
  86. package/packages/notification/dist/telegram-auth.d.ts +23 -0
  87. package/packages/notification/dist/telegram-auth.d.ts.map +1 -0
  88. package/packages/notification/dist/telegram-auth.js +32 -0
  89. package/packages/notification/dist/telegram-auth.js.map +1 -0
  90. package/packages/notification/dist/types.d.ts +5 -0
  91. package/packages/notification/dist/types.d.ts.map +1 -1
  92. package/packages/notification/package.json +1 -1
  93. package/packages/notification/src/adapters/line-adapter.ts +145 -13
  94. package/packages/notification/src/adapters/telegram-adapter.ts +283 -18
  95. package/packages/notification/src/base-adapter.ts +28 -0
  96. package/packages/notification/src/notification-center.ts +12 -0
  97. package/packages/notification/src/telegram-auth.ts +34 -0
  98. package/packages/notification/src/types.ts +5 -0
  99. package/packages/observability/package.json +1 -1
  100. package/packages/okf-indexer/package.json +1 -1
  101. package/packages/paperclip-integration/package.json +1 -1
  102. package/packages/performance-optimizer/package.json +1 -1
  103. package/packages/permission-mode/package.json +1 -1
  104. package/packages/privilege-broker/package.json +1 -1
  105. package/packages/project-analyzer/package.json +1 -1
  106. package/packages/project-bootstrap/package.json +1 -1
  107. package/packages/projection-engine/package.json +1 -1
  108. package/packages/prompt-compiler/package.json +1 -1
  109. package/packages/prompt-versioning/package.json +1 -1
  110. package/packages/provider-adapter-sdk/package.json +1 -1
  111. package/packages/provider-router/package.json +1 -1
  112. package/packages/provider-selector/package.json +1 -1
  113. package/packages/providers/package.json +1 -1
  114. package/packages/qdrant-skills/package.json +1 -1
  115. package/packages/quota-manager/package.json +1 -1
  116. package/packages/rate-limiter/package.json +1 -1
  117. package/packages/react-vite-plugin/package.json +1 -1
  118. package/packages/relay/dist/server.js +7 -2
  119. package/packages/relay/dist/types.d.ts +3 -1
  120. package/packages/relay/dist/ws-server.d.ts +2 -9
  121. package/packages/relay/dist/ws-server.js +5 -3
  122. package/packages/relay/package.json +1 -1
  123. package/packages/relay/src/server.ts +7 -2
  124. package/packages/relay/src/types.ts +6 -1
  125. package/packages/relay/src/ws-server.ts +9 -13
  126. package/packages/relay/test/relay-e2e.test.ts +36 -0
  127. package/packages/release-manager/package.json +1 -1
  128. package/packages/requirement-compiler/package.json +1 -1
  129. package/packages/runtime/package.json +1 -1
  130. package/packages/scheduler/package.json +1 -1
  131. package/packages/scheduler-adapter/package.json +1 -1
  132. package/packages/session/package.json +1 -1
  133. package/packages/session-api/package.json +1 -1
  134. package/packages/session-export/package.json +1 -1
  135. package/packages/shared-context/package.json +1 -1
  136. package/packages/skill-mcp-client/package.json +1 -1
  137. package/packages/skill-registry/package.json +1 -1
  138. package/packages/skills/package.json +1 -1
  139. package/packages/sprint-planner/package.json +1 -1
  140. package/packages/subscription-engine/package.json +1 -1
  141. package/packages/task-compiler/package.json +1 -1
  142. package/packages/telegram/package.json +1 -1
  143. package/packages/tencentdb-memory/package.json +1 -1
  144. package/packages/tencentdb-sync/package.json +1 -1
  145. package/packages/test-data-generator/package.json +1 -1
  146. package/packages/test-generator/package.json +1 -1
  147. package/packages/todo-bd-sync/package.json +1 -1
  148. package/packages/token-estimation/package.json +1 -1
  149. package/packages/token-optimizer/package.json +1 -1
  150. package/packages/trajectory/package.json +1 -1
  151. package/packages/tui/dist/harness-footer.d.ts +23 -8
  152. package/packages/tui/dist/harness-footer.d.ts.map +1 -1
  153. package/packages/tui/dist/harness-footer.js +144 -2
  154. package/packages/tui/dist/harness-footer.js.map +1 -1
  155. package/packages/tui/dist/index.d.ts +1 -1
  156. package/packages/tui/dist/index.d.ts.map +1 -1
  157. package/packages/tui/dist/index.js +1 -1
  158. package/packages/tui/dist/index.js.map +1 -1
  159. package/packages/tui/package.json +1 -1
  160. package/packages/tui/src/harness-footer.ts +149 -2
  161. package/packages/tui/src/index.ts +1 -0
  162. package/packages/tui/test/harness-footer.test.ts +134 -15
  163. package/packages/types/package.json +1 -1
  164. package/packages/voice-api/package.json +1 -1
  165. package/packages/webmcp-tester/package.json +1 -1
  166. package/packages/wiki/package.json +1 -1
  167. package/packages/workflow-events/package.json +1 -1
  168. package/packages/workspace-scanner/package.json +1 -1
  169. package/packages/worktree/package.json +1 -1
  170. package/packages/write-review/package.json +1 -1
  171. package/scripts/unified-webhook-server.ts +141 -14
  172. package/scripts/ws-server.ts +7 -1
package/AGENTS.md CHANGED
@@ -494,36 +494,63 @@ bun run build && bun run start # Server deployment
494
494
  ### 14. Workspace Environment Auto-Detection
495
495
 
496
496
  **At session start, the harness auto-detects:**
497
- - `.env` file presence → Dev environment evidence
497
+ - `.env` / `.env.*` file variables → Dev environment evidence
498
498
  - `package.json` `dev` script → Hot reload capability
499
- - `NODE_ENV` value → development/production/test
499
+ - The environment variable, in this order: the `NODE_ENV` of the process env
500
+ (it decides, also when it names no known mode), then the dotenv files of the
501
+ workspace: `NODE_ENV`, an environment shaped key whose value names a known
502
+ mode (`*_ENV`, `*_ENVIRONMENT`, `*_MODE`, `*_STAGE`, e.g. `AMOS_ENV`), then
503
+ any other environment shaped key - and only then an environment shaped key
504
+ of the process env → development/production/test
500
505
  - System RAM → Low RAM warning (<16GB total or <4GB available)
501
506
  - Running processes → dev/build server status
502
507
 
508
+ The dotenv files are read from the WORKSPACE (not from the current working
509
+ directory), the template files (`.env.example`, `.env.sample`,
510
+ `.env.template`, ...) are ignored and a file bigger than 256 kB is not read.
511
+ A value that is not provably harmless is masked (`<set>`): only the
512
+ `NODE_ENV` of the process env and a value that IS the name of the mode are
513
+ shown. A key that only looks like a branch (`BRANCH=production`) never
514
+ decides the mode.
515
+
503
516
  **This info is shown in UI and delivered to agent context:**
504
517
  ```
505
- Workspace: DEVELOPMENT | dev server: running | build: not running (script available) | RAM: 10/16
518
+ Workspace: ~/pi-harness-runtime | OS: Linux | mode: DEVELOPMENT (from AMOS_ENV in .env) | dev server: running | dev script: available | build script: available | RAM: 10/16
506
519
  ```
507
520
 
508
521
  The mode words are exactly what the detector emits: `DEVELOPMENT`, `PRODUCTION`,
509
- `TEST` or `UNKNOWN` (with its reason, e.g. `UNKNOWN (no explicit NODE_ENV)`).
510
- A dev/build script is a CAPABILITY, not a state: `not running (script available)`
511
- means the script exists, not that a server runs.
522
+ `TEST` or `UNKNOWN` (with its reason, e.g.
523
+ `UNKNOWN (no NODE_ENV or environment variable found)` or
524
+ `UNKNOWN (AMOS_ENV in .env names no known mode)`). A known mode also
525
+ names its source: `DEVELOPMENT (from AMOS_ENV in .env)`.
526
+ A dev/build script is a CAPABILITY, not a state: `dev script: available` means
527
+ the script exists, not that a server runs. The server state is separate and
528
+ never claims more than it knows: `running`, `not detected` (a check ran and
529
+ found nothing) or `not checked` (no check was possible, e.g. no `pgrep`).
530
+
531
+ The OS is part of the line because it decides which command prints the
532
+ working directory: `pwd` on Linux/macOS, `(pwd).Path` or `$pwd.Path` in
533
+ PowerShell (the plain `pwd` there prints a "Path: " header), `cd` or
534
+ `echo %cd%` in the Windows command prompt. The agent context names the
535
+ command to use.
512
536
 
513
537
  **IMPORTANT Rules:**
514
538
 
515
539
  | State | Action |
516
540
  |-------|--------|
517
- | `Workspace: UNKNOWN (…)` | Explicitly ask user before build |
518
- | `Workspace: DEVELOPMENT` | Use `dev` mode, don't auto-build |
519
- | `Workspace: PRODUCTION` | Build allowed, but confirm with user |
541
+ | `mode: UNKNOWN (…)` | Explicitly ask user before build |
542
+ | `mode: DEVELOPMENT` | Use `dev` mode, don't auto-build |
543
+ | `mode: PRODUCTION` | Build allowed, but confirm with user |
520
544
  | `Low RAM` | Never run parallel build + dev |
521
545
  | `Dev server running` | Reuse it, don't restart |
522
546
 
523
547
  **Decision flow:**
524
548
  ```
525
- Check process.env.NODE_ENV first
526
- Check .env file NODE_ENV second
549
+ Check process.env.NODE_ENV first (it decides, also when it names no known mode)
550
+ Check the .env and .env.* files of the workspace second
551
+ (NODE_ENV, then an environment shaped key whose value names a mode,
552
+ then any other environment shaped key)
553
+ Only then an environment shaped key of the process env (e.g. AMOS_ENV)
527
554
  Presence of dev script = capability, not proof of mode
528
555
  UNKNOWN requires investigation, not auto-build
529
556
  Production detection ≠ deployment authorization
@@ -59,7 +59,10 @@ export function oneLineError(error: unknown, maxLength = 160): string {
59
59
  "$1<redacted>",
60
60
  )
61
61
  // A url path: "/bot123456:ABC-DEF/", "/token/abc12345".
62
- .replace(/\bbot\d{4,}:[A-Za-z0-9_\-]{4,}/g, "<redacted>")
62
+ // The boundary is "not a digit" and not a word boundary, so a
63
+ // token that is glued to a word ("xbot123:ABC-DEF") is redacted
64
+ // as well.
65
+ .replace(/(^|[^0-9])\d{6,}:[A-Za-z0-9_\-]{4,}/g, "$1<redacted>")
63
66
  .replace(
64
67
  /(\/((?:access_token|api[-_]?key|token|secret)[=/]))[A-Za-z0-9_\-:]{4,}/gi,
65
68
  "$1<redacted>",
@@ -177,6 +180,21 @@ export function lineConfigurationReport(options: {
177
180
  telegramConfigured?: boolean;
178
181
  /** The fix commands for the missing LINE credentials. */
179
182
  fixes: string[];
183
+ /**
184
+ * The fix commands for the channel access token only.
185
+ *
186
+ * The startup check validates the ACCESS TOKEN. The user id is only the
187
+ * recipient of the messages, so a failed check must not point at
188
+ * line-user-id.txt.
189
+ */
190
+ accessTokenFixes?: string[];
191
+ /**
192
+ * Why the channel did not start (from the adapter).
193
+ *
194
+ * It says WHICH problem it is - an invalid or revoked token, a LINE API
195
+ * failure or a network failure - and it never contains a credential.
196
+ */
197
+ channelDiagnostic?: string | null;
180
198
  }): StartupReport | null {
181
199
  if (!options.clientKeyFilePresent) return null;
182
200
  const channelState = options.channelState ?? "not-started";
@@ -204,15 +222,27 @@ export function lineConfigurationReport(options: {
204
222
  }
205
223
  if (channelState === "failed") {
206
224
  // The credentials are there but the channel did not start - the
207
- // credentials themselves are the likely cause.
225
+ // channel access token is what the startup check validates, so
226
+ // the user id must not be named as the cause.
227
+ const details = [
228
+ ...(options.accessTokenFixes && options.accessTokenFixes.length > 0
229
+ ? options.accessTokenFixes
230
+ : options.fixes),
231
+ ];
232
+ if (options.channelDiagnostic) {
233
+ details.push(options.channelDiagnostic);
234
+ }
235
+ details.push(
236
+ "The startup check validates the channel access token - the user id is only the recipient of the messages.",
237
+ );
238
+ details.push(
239
+ "Fix the file, then reload Pi so the channel is checked again.",
240
+ );
208
241
  return {
209
242
  label: "LINE",
210
243
  text: "configured but the LINE channel did not start",
211
244
  status: "warn",
212
- details: [
213
- ...options.fixes,
214
- "Check the LINE credentials in ~/.pi-harness-runtime/keys/, then reload Pi.",
215
- ],
245
+ details,
216
246
  };
217
247
  }
218
248
  // The credentials are there but the channel was never started in this
@@ -90,6 +90,38 @@ export function getAllowedUserIds(): Set<number> {
90
90
  return new Set();
91
91
  }
92
92
 
93
+ /**
94
+ * Describe who may write into Pi through Telegram.
95
+ *
96
+ * The allow-list falls back to the chat id file, and a GROUP chat id is
97
+ * negative - it identifies a chat, not a person. A sender rule that only
98
+ * knows a group id lets nobody in, silently. The description makes that
99
+ * visible.
100
+ */
101
+ export function describeInboundAccess(
102
+ allowedUserIds: Iterable<number>,
103
+ chatId?: string,
104
+ ): string {
105
+ const ids = [...allowedUserIds].filter((n) => Number.isFinite(n));
106
+ const users = ids.filter((n) => n > 0);
107
+ const groups = ids.filter((n) => n < 0);
108
+ if (users.length === 0) {
109
+ const known = groups.length > 0 ? ` (the allow-list holds the group chat id ${groups.join(", ")})` : "";
110
+ return `nobody may write in${known}`;
111
+ }
112
+ const chat = chatId ? ` in chat ${chatId}` : "";
113
+ return `user id(s) ${users.join(", ")} may write in${chat}`;
114
+ }
115
+
116
+ /**
117
+ * Decide whether a Telegram sender may write into Pi.
118
+ *
119
+ * The rule itself lives in the notification package
120
+ * (packages/notification/src/telegram-auth.ts) so that the adapter, this
121
+ * module and the remote webhook server all use ONE implementation.
122
+ */
123
+ export { isAuthorizedTelegramSender } from "../packages/notification/dist/telegram-auth.js";
124
+
93
125
  /**
94
126
  * Check if a Telegram user ID is authorized
95
127
  */
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Map a relay "telegram_message" payload onto a Telegram update.
3
+ *
4
+ * The mapping lives in a pure module so that it can be tested without the
5
+ * harness: the relay carries the text, the adapter decides whether the
6
+ * sender is allowed to write in.
7
+ *
8
+ * Rules:
9
+ * - the text is never put into an id field (ids are logged by the client)
10
+ * - the real bot flag has to survive the transport, otherwise the adapter
11
+ * could not refuse bot messages
12
+ * - a payload without text produces no update
13
+ */
14
+
15
+ export interface TelegramMessageUpdate {
16
+ message: {
17
+ message_id: number;
18
+ from: {
19
+ id: number;
20
+ is_bot: boolean;
21
+ first_name: string;
22
+ username?: string;
23
+ };
24
+ chat: { id: number };
25
+ text: string;
26
+ };
27
+ }
28
+
29
+ export interface TelegramMessagePayload {
30
+ userId: number;
31
+ username?: string;
32
+ messageId?: number;
33
+ chatId?: number;
34
+ text?: string;
35
+ isBot?: boolean;
36
+ }
37
+
38
+ export interface TelegramCallbackUpdate {
39
+ callback_query: {
40
+ id: string;
41
+ from: {
42
+ id: number;
43
+ is_bot: boolean;
44
+ first_name: string;
45
+ username?: string;
46
+ };
47
+ data: string;
48
+ message: {
49
+ chat: { id: number };
50
+ message_id: number;
51
+ };
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Build the Telegram update for a relayed button callback.
57
+ *
58
+ * The real bot flag has to survive the transport: the adapter refuses
59
+ * messages and callbacks of a bot.
60
+ */
61
+ export function buildTelegramCallbackUpdate(
62
+ payload: TelegramMessagePayload & {
63
+ action?: string;
64
+ targetId?: string;
65
+ callbackQueryId?: string;
66
+ id?: string;
67
+ },
68
+ ): TelegramCallbackUpdate | null {
69
+ const userId = Number(payload.userId);
70
+ if (!Number.isFinite(userId)) return null;
71
+ const chatId = Number(payload.chatId);
72
+ const effectiveChatId = Number.isFinite(chatId) ? chatId : userId;
73
+ return {
74
+ callback_query: {
75
+ id: payload.callbackQueryId || String(payload.id || ""),
76
+ from: {
77
+ id: userId,
78
+ is_bot: payload.isBot === true,
79
+ first_name: payload.username || "User",
80
+ username: payload.username,
81
+ },
82
+ data: `${payload.action || ""}:${payload.targetId || ""}`,
83
+ message: {
84
+ chat: { id: effectiveChatId },
85
+ message_id: Number(payload.messageId) || 0,
86
+ },
87
+ },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Build the Telegram update for a relayed text message.
93
+ *
94
+ * Returns null when the payload carries no text - there is nothing to
95
+ * forward and the adapter must not see an empty message.
96
+ */
97
+ export function buildTelegramMessageUpdate(
98
+ payload: TelegramMessagePayload,
99
+ ): TelegramMessageUpdate | null {
100
+ const text = payload.text ?? "";
101
+ if (!text) return null;
102
+
103
+ const userId = Number(payload.userId);
104
+ if (!Number.isFinite(userId)) return null;
105
+
106
+ // A direct message has the chat id of the user; the remote always sends
107
+ // the real chat id, so the fallback is only for a DM.
108
+ const chatId = Number(payload.chatId);
109
+ const effectiveChatId = Number.isFinite(chatId) ? chatId : userId;
110
+
111
+ return {
112
+ message: {
113
+ message_id: Number(payload.messageId) || 0,
114
+ from: {
115
+ id: userId,
116
+ // The real flag: the adapter refuses bot messages.
117
+ is_bot: payload.isBot === true,
118
+ first_name: payload.username || "User",
119
+ username: payload.username,
120
+ },
121
+ chat: { id: effectiveChatId },
122
+ text,
123
+ },
124
+ };
125
+ }
package/index.ts CHANGED
@@ -28,6 +28,8 @@ import {
28
28
  type KeyFileAuditResult,
29
29
  } from "./harness/key-file-audit.ts";
30
30
  import { renderDetailLines } from "./harness/startup-lines.ts";
31
+ import { describeInboundAccess, getAllowedUserIds } from "./harness/telegram-auth.ts";
32
+ import { buildTelegramCallbackUpdate, buildTelegramMessageUpdate } from "./harness/telegram-update.ts";
31
33
  import {
32
34
  jevMissingKeyReport,
33
35
  lineConfigurationReport,
@@ -163,6 +165,15 @@ let keyFileAuditDone = false;
163
165
  * startup line must not claim a green "configured".
164
166
  */
165
167
  let lineChannelState: "started" | "failed" | "not-started" = "not-started";
168
+ /**
169
+ * Why the LINE channel did not start (from the adapter).
170
+ *
171
+ * The adapter knows the difference between an invalid or revoked token
172
+ * (401/403), a LINE API failure and a network failure - the startup
173
+ * summary shows it so the user knows what to fix. It never contains a
174
+ * credential.
175
+ */
176
+ let lineChannelDiagnostic: string | null = null;
166
177
  let telegramChannelState: "started" | "failed" | "not-started" = "not-started";
167
178
  /** Did the relay client connect at least once in this extension load? */
168
179
  let relayStreamConnected = false;
@@ -1355,6 +1366,7 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1355
1366
  return;
1356
1367
  }
1357
1368
 
1369
+ const allowedUserIds = [...getAllowedUserIds()];
1358
1370
  const channels: Array<{
1359
1371
  id: string;
1360
1372
  type: string;
@@ -1365,10 +1377,27 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1365
1377
  id: "telegram",
1366
1378
  type: "telegram",
1367
1379
  enabled: true,
1368
- config: { botToken, chatId },
1380
+ config: { botToken, chatId, allowedUserIds },
1369
1381
  },
1370
1382
  ];
1371
1383
 
1384
+ // Who may write into Pi? A group chat id is not a user id: when the
1385
+ // allow-list holds only a group id, the inbound path is closed - and
1386
+ // that must be visible, not silent.
1387
+ const inboundAccess = describeInboundAccess(allowedUserIds, chatId);
1388
+ logStartup(`[pi-harness] Telegram inbound: ${inboundAccess}`);
1389
+ if (inboundAccess.startsWith("nobody")) {
1390
+ addHarnessMessage(
1391
+ "Telegram",
1392
+ "nobody may write in: the allow-list holds no user id",
1393
+ "warn",
1394
+ [
1395
+ "Write your Telegram user id into ~/.pi-harness-runtime/keys/telegram-allowed-users.txt (a chat id of a group is not a user id).",
1396
+ "Then reload Pi.",
1397
+ ],
1398
+ );
1399
+ }
1400
+
1372
1401
  // Add LINE channel if credentials exist
1373
1402
  const lineChannelConfigured = Boolean(hasLineCredentials && lineAccessToken && lineUserId);
1374
1403
  if (lineChannelConfigured) {
@@ -1398,9 +1427,13 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1398
1427
  if (lineChannelConfigured) {
1399
1428
  if (center.isChannelHealthy("line")) {
1400
1429
  lineChannelState = "started";
1430
+ lineChannelDiagnostic = null;
1401
1431
  addHarnessMessage("LINE", "configured and enabled (tap Toggle LINE to disable)", "ok");
1402
1432
  } else {
1403
1433
  lineChannelState = "failed";
1434
+ // Keep the reason: "the token is wrong" and "LINE is down"
1435
+ // need different actions.
1436
+ lineChannelDiagnostic = center.getChannelDiagnostic("line");
1404
1437
  }
1405
1438
  }
1406
1439
  if (telegramChannelState !== "started") {
@@ -1574,7 +1607,9 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1574
1607
  return;
1575
1608
  }
1576
1609
 
1577
- logToFile("[TelegramInbound]", `Forwarding message to Pi: "${text.slice(0, 50)}..."`);
1610
+ // The user's own words stay out of the log files: only the length
1611
+ // is recorded.
1612
+ logToFile("[TelegramInbound]", `Forwarding message to Pi (${text.length} chars)`);
1578
1613
  // Forward the message to Pi as a user message
1579
1614
  await pi.sendUserMessage(text, { deliverAs: "steer" });
1580
1615
  });
@@ -1631,7 +1666,32 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1631
1666
  ]);
1632
1667
  return;
1633
1668
  }
1634
- telegram?.startPolling();
1669
+ // Polling and the remote webhook are mutually exclusive. Telegram
1670
+ // rejects getUpdates while a webhook is active, and the rejected poll
1671
+ // also terminates the getUpdates stream of every other poller of this
1672
+ // bot, which delayed the inbound text messages by seconds.
1673
+ if (telegram) {
1674
+ // A 409 (a webhook or another poller owns this bot) stops the
1675
+ // inbound path. The user must see it - a silent 409 looks like a
1676
+ // working inbound path.
1677
+ telegram.onPollStopped = (reason) => {
1678
+ addHarnessMessage("Telegram", `inbound polling stopped: ${reason}`, "warn", [
1679
+ "Telegram rejects getUpdates (409) while a webhook or another poller owns this bot.",
1680
+ "Stop the other poller of this bot (or remove the remote webhook), then reload Pi.",
1681
+ ]);
1682
+ };
1683
+ if (!telegram.isWebhookConfigured()) {
1684
+ telegram.startPolling();
1685
+ // The poll may already have been rejected (a 409 fires the
1686
+ // callback above); make the reason visible either way.
1687
+ const stopReason = telegram.getPollStopReason();
1688
+ if (stopReason) {
1689
+ addHarnessMessage("Telegram", `inbound polling stopped: ${stopReason}`, "warn", [
1690
+ "Stop the other poller of this bot (or remove the remote webhook), then reload Pi.",
1691
+ ]);
1692
+ }
1693
+ }
1694
+ }
1635
1695
  if (!telegram?.isGreetingVerified()) {
1636
1696
  addHarnessMessage("Telegram", "Hi sent; tap Say Hi to verify inbound (tap Toggle Telegram to disable)", "wait");
1637
1697
  }
@@ -1795,8 +1855,15 @@ function reportLineConfiguration(): void {
1795
1855
  channelState: lineChannelState,
1796
1856
  telegramConfigured: telegramIsConfigured(),
1797
1857
  // The fallback makes sure that a warning always carries a command,
1798
- // even when the audit could not answer.
1799
- fixes: keyFileFixesOrFallback("LINE", ["channel access token", "user id"]),
1858
+ // even when the audit could not answer. It names the ACCESS TOKEN
1859
+ // only - the user id is the recipient of the messages, not the cause
1860
+ // of a failed startup check.
1861
+ fixes: keyFileFixesOrFallback("LINE", ["channel access token"]),
1862
+ // The startup check validates the access token - the user id is only
1863
+ // the recipient, so it must not be named as the cause.
1864
+ accessTokenFixes: keyFileFixesOrFallback("LINE", ["channel access token"]),
1865
+ // The reason the channel did not start (invalid token, LINE down, ...).
1866
+ channelDiagnostic: lineChannelDiagnostic,
1800
1867
  // Without Telegram the LINE channel cannot start - the user needs
1801
1868
  // the Telegram commands, not only an explanation.
1802
1869
  telegramFixes: keyFileFixesOrFallback("Telegram", ["bot token", "chat id"]),
@@ -1911,33 +1978,57 @@ async function initRelayStream(): Promise<void> {
1911
1978
  logToFile("[RelayStream]", "Synchronized - receiving live events");
1912
1979
  },
1913
1980
  onCallback: async (callback: CallbackPayload) => {
1914
- logToFile("[RelayStream]", `Callback: ${callback.action} ${callback.targetId}`);
1981
+ logToFile(
1982
+ "[RelayStream]",
1983
+ callback.type === "telegram_message"
1984
+ ? `Message from ${callback.username || callback.userId} (${(callback.text || "").length} chars)`
1985
+ : `Callback: ${callback.action} ${callback.targetId}`,
1986
+ );
1915
1987
 
1916
1988
  // Forward Telegram callbacks to the adapter
1917
1989
  if (callback.type === "telegram_callback" && _relayTelegramAdapter) {
1918
- // Convert relay callback to Telegram update format
1919
- const update = {
1920
- callback_query: {
1921
- id: callback.callbackQueryId || String(callback.id),
1922
- from: {
1923
- id: callback.userId,
1924
- is_bot: false,
1925
- first_name: callback.username || "User",
1926
- username: callback.username,
1927
- },
1928
- data: `${callback.action}:${callback.targetId}`,
1929
- message: {
1930
- chat: { id: callback.chatId || callback.userId },
1931
- message_id: callback.messageId || 0,
1932
- },
1933
- },
1934
- };
1990
+ // The mapping is a pure function (harness/telegram-update.ts) so
1991
+ // that it can be tested without the harness.
1992
+ const update = buildTelegramCallbackUpdate({
1993
+ id: callback.id,
1994
+ action: callback.action,
1995
+ targetId: callback.targetId,
1996
+ callbackQueryId: callback.callbackQueryId,
1997
+ userId: callback.userId,
1998
+ username: callback.username,
1999
+ messageId: callback.messageId,
2000
+ chatId: callback.chatId,
2001
+ isBot: callback.isBot,
2002
+ });
2003
+ if (update) {
2004
+ try {
2005
+ await _relayTelegramAdapter.handleWebhookUpdate(update);
2006
+ logToFile("[RelayStream]", "Callback processed by Telegram adapter");
2007
+ } catch (err) {
2008
+ logToFile("[RelayStream]", `Adapter error: ${oneLineError(err)}`);
2009
+ }
2010
+ }
2011
+ }
1935
2012
 
1936
- try {
1937
- await _relayTelegramAdapter.handleWebhookUpdate(update);
1938
- logToFile("[RelayStream]", "Callback processed by Telegram adapter");
1939
- } catch (err) {
1940
- logToFile("[RelayStream]", `Adapter error: ${err}`);
2013
+ // Forward a Telegram TEXT message (the user typed it) to the adapter
2014
+ if (callback.type === "telegram_message" && _relayTelegramAdapter) {
2015
+ // The mapping is a pure function (harness/telegram-update.ts) so
2016
+ // that it can be tested without the harness.
2017
+ const update = buildTelegramMessageUpdate({
2018
+ userId: callback.userId,
2019
+ username: callback.username,
2020
+ messageId: callback.messageId,
2021
+ chatId: callback.chatId,
2022
+ text: callback.text,
2023
+ isBot: callback.isBot,
2024
+ });
2025
+ if (update) {
2026
+ try {
2027
+ await _relayTelegramAdapter.handleWebhookUpdate(update);
2028
+ logToFile("[RelayStream]", "Message processed by Telegram adapter");
2029
+ } catch (err) {
2030
+ logToFile("[RelayStream]", `Adapter error: ${oneLineError(err)}`);
2031
+ }
1941
2032
  }
1942
2033
  }
1943
2034
  },
@@ -2688,8 +2779,10 @@ const HARNESS_LOG_FILE = "/tmp/pi-harness.log";
2688
2779
  /** Log a harness message to file (not stderr - bypasses Pi's cursor tracking) */
2689
2780
  function logToFile(...parts: string[]): void {
2690
2781
  const msg = parts.join(" ");
2782
+ // A timestamp makes the log usable for latency measurements.
2783
+ const stamp = new Date().toISOString();
2691
2784
  try {
2692
- appendFileSync(HARNESS_LOG_FILE, msg + "\n");
2785
+ appendFileSync(HARNESS_LOG_FILE, `[${stamp}] ${msg}\n`);
2693
2786
  } catch {
2694
2787
  // Ignore file write errors
2695
2788
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-refactor",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "Autonomous refactoring engine",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cache-strategy",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "Smart caching with TTL and invalidation strategies (RFC-0022)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/capability-registry",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/checkpoint",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "Enhanced checkpoint system with incremental snapshots, diff-based storage, and recovery strategies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cli-plugin-sdk",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "CLI Plugin SDK for pi-harness — install, list, remove, update, invoke plugins",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard-plugin",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/code-generation",
3
- "version": "1.1.390",
3
+ "version": "1.1.392",
4
4
  "description": "Code generation pipeline for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",