instar 1.3.595 → 1.3.597
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/LiveTestRunner.d.ts +66 -0
- package/dist/core/LiveTestRunner.d.ts.map +1 -0
- package/dist/core/LiveTestRunner.js +85 -0
- package/dist/core/LiveTestRunner.js.map +1 -0
- package/dist/core/TelegramLiveSender.d.ts +53 -0
- package/dist/core/TelegramLiveSender.d.ts.map +1 -0
- package/dist/core/TelegramLiveSender.js +69 -0
- package/dist/core/TelegramLiveSender.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.596.md +21 -0
- package/upgrades/1.3.597.md +21 -0
- package/upgrades/side-effects/live-test-runner.md +28 -0
- package/upgrades/side-effects/live-test-telegram-sender.md +28 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiveTestRunner — the orchestrator that APPLIES the live-user-channel-proof standard
|
|
3
|
+
* to the cross-machine transfer (docs/specs/live-user-channel-proof-standard.md §6 —
|
|
4
|
+
* the first feature held to the bar). It encodes the capstone flow that a user-role
|
|
5
|
+
* session runs:
|
|
6
|
+
*
|
|
7
|
+
* 1. MOVE THE SEAT FIRST — transfer the (throwaway) topic to the target machine.
|
|
8
|
+
* If the transfer reports the seat did NOT move, the capstone premise is false and
|
|
9
|
+
* we stop loudly (that "ok:true but moved nothing" lie is the original bug — we
|
|
10
|
+
* refuse to paper over it).
|
|
11
|
+
* 2. Then run the LiveTestHarness over a scenario matrix that sends a real message
|
|
12
|
+
* through each channel and asserts the reply was served FROM the target machine
|
|
13
|
+
* (the `responderMachine` expectation — the deterministic cross-machine proof).
|
|
14
|
+
*
|
|
15
|
+
* This module is pure orchestration over the injected harness + an injected `transfer`
|
|
16
|
+
* action, so it is unit-testable with fakes. The server.ts route wires the real
|
|
17
|
+
* RealChannelDriver (real senders + placement reader) into the harness and the real
|
|
18
|
+
* `POST /pool/transfer` into `transfer`.
|
|
19
|
+
*
|
|
20
|
+
* Honesty contract: a non-moved seat THROWS (never records a misleading PASS); a moved
|
|
21
|
+
* seat that then fails to reply-from-target is a normal harness FAIL (recorded with the
|
|
22
|
+
* responder mismatch). The artifact only ever shows PASS when the seat moved AND the
|
|
23
|
+
* reply genuinely came from the target machine.
|
|
24
|
+
*/
|
|
25
|
+
import type { LiveTestHarness } from './LiveTestHarness.js';
|
|
26
|
+
export interface TransferResult {
|
|
27
|
+
/** Did the seat genuinely move (the honest signal, NOT a bare ok:true)? */
|
|
28
|
+
seatMoved: boolean;
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface MultiMachineCapstoneOpts {
|
|
32
|
+
featureId?: string;
|
|
33
|
+
/** The machine the seat must move to (e.g. the Mini's machine id) — also the expected responder. */
|
|
34
|
+
targetMachine: string;
|
|
35
|
+
/** The throwaway Telegram topic id (also the placement key the responder reader uses). */
|
|
36
|
+
telegramTopicId: string;
|
|
37
|
+
/** Optional Slack channel id for the Slack half of the Telegram-AND-Slack bar. */
|
|
38
|
+
slackChannelId?: string;
|
|
39
|
+
/** The real transfer action (server.ts injects POST /pool/transfer). MUST report seatMoved honestly. */
|
|
40
|
+
transfer: (topicId: string, toMachine: string) => Promise<TransferResult>;
|
|
41
|
+
/** The user message to send (default a benign probe). */
|
|
42
|
+
message?: string;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
runId?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare class LiveTestRunnerError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
export declare class LiveTestRunner {
|
|
50
|
+
private readonly deps;
|
|
51
|
+
constructor(deps: {
|
|
52
|
+
harness: LiveTestHarness;
|
|
53
|
+
logger?: (m: string) => void;
|
|
54
|
+
});
|
|
55
|
+
private log;
|
|
56
|
+
/**
|
|
57
|
+
* Run the multi-machine transfer capstone. Throws LiveTestRunnerError if the seat did
|
|
58
|
+
* not move (the capstone cannot meaningfully run). Otherwise returns the harness
|
|
59
|
+
* artifact (PASS only when the reply came FROM `targetMachine`).
|
|
60
|
+
*/
|
|
61
|
+
runMultiMachineTransferCapstone(opts: MultiMachineCapstoneOpts): Promise<{
|
|
62
|
+
artifact: import("./LiveTestArtifactStore.js").LiveTestArtifact;
|
|
63
|
+
entry: ReturnType<import("./LiveTestArtifactStore.js").LiveTestArtifactStore["write"]>;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=LiveTestRunner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveTestRunner.d.ts","sourceRoot":"","sources":["../../src/core/LiveTestRunner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkC,MAAM,sBAAsB,CAAC;AAG5F,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,aAAa,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,eAAe,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wGAAwG;IACxG,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1E,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAC5B;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE;QAAE,OAAO,EAAE,eAAe,CAAC;QAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE;IAE7F,OAAO,CAAC,GAAG;IAEX;;;;OAIG;IACG,+BAA+B,CAAC,IAAI,EAAE,wBAAwB;;;;CAiDrE"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiveTestRunner — the orchestrator that APPLIES the live-user-channel-proof standard
|
|
3
|
+
* to the cross-machine transfer (docs/specs/live-user-channel-proof-standard.md §6 —
|
|
4
|
+
* the first feature held to the bar). It encodes the capstone flow that a user-role
|
|
5
|
+
* session runs:
|
|
6
|
+
*
|
|
7
|
+
* 1. MOVE THE SEAT FIRST — transfer the (throwaway) topic to the target machine.
|
|
8
|
+
* If the transfer reports the seat did NOT move, the capstone premise is false and
|
|
9
|
+
* we stop loudly (that "ok:true but moved nothing" lie is the original bug — we
|
|
10
|
+
* refuse to paper over it).
|
|
11
|
+
* 2. Then run the LiveTestHarness over a scenario matrix that sends a real message
|
|
12
|
+
* through each channel and asserts the reply was served FROM the target machine
|
|
13
|
+
* (the `responderMachine` expectation — the deterministic cross-machine proof).
|
|
14
|
+
*
|
|
15
|
+
* This module is pure orchestration over the injected harness + an injected `transfer`
|
|
16
|
+
* action, so it is unit-testable with fakes. The server.ts route wires the real
|
|
17
|
+
* RealChannelDriver (real senders + placement reader) into the harness and the real
|
|
18
|
+
* `POST /pool/transfer` into `transfer`.
|
|
19
|
+
*
|
|
20
|
+
* Honesty contract: a non-moved seat THROWS (never records a misleading PASS); a moved
|
|
21
|
+
* seat that then fails to reply-from-target is a normal harness FAIL (recorded with the
|
|
22
|
+
* responder mismatch). The artifact only ever shows PASS when the seat moved AND the
|
|
23
|
+
* reply genuinely came from the target machine.
|
|
24
|
+
*/
|
|
25
|
+
export class LiveTestRunnerError extends Error {
|
|
26
|
+
constructor(message) { super(message); this.name = 'LiveTestRunnerError'; }
|
|
27
|
+
}
|
|
28
|
+
export class LiveTestRunner {
|
|
29
|
+
deps;
|
|
30
|
+
constructor(deps) {
|
|
31
|
+
this.deps = deps;
|
|
32
|
+
}
|
|
33
|
+
log(m) { this.deps.logger?.(`[live-test-runner] ${m}`); }
|
|
34
|
+
/**
|
|
35
|
+
* Run the multi-machine transfer capstone. Throws LiveTestRunnerError if the seat did
|
|
36
|
+
* not move (the capstone cannot meaningfully run). Otherwise returns the harness
|
|
37
|
+
* artifact (PASS only when the reply came FROM `targetMachine`).
|
|
38
|
+
*/
|
|
39
|
+
async runMultiMachineTransferCapstone(opts) {
|
|
40
|
+
// 1. Move the seat FIRST — and demand the honest seatMoved signal.
|
|
41
|
+
const t = await opts.transfer(opts.telegramTopicId, opts.targetMachine);
|
|
42
|
+
if (!t.seatMoved) {
|
|
43
|
+
throw new LiveTestRunnerError(`transfer to ${opts.targetMachine} did not move the seat (${t.detail ?? 'seatMoved=false'}) — ` +
|
|
44
|
+
`capstone cannot run (refusing to record a misleading PASS over a non-move)`);
|
|
45
|
+
}
|
|
46
|
+
this.log(`seat moved to ${opts.targetMachine}; running channel scenarios`);
|
|
47
|
+
// 2. Build the matrix — each channel asserts the reply was served FROM targetMachine.
|
|
48
|
+
const message = (opts.message ?? `live-test probe ${opts.runId ?? ''}`).trim();
|
|
49
|
+
const surfaces = opts.slackChannelId ? ['telegram', 'slack'] : ['telegram'];
|
|
50
|
+
const scenarios = [
|
|
51
|
+
{
|
|
52
|
+
id: 'mm-transfer-telegram-reply-from-target',
|
|
53
|
+
description: `after transfer to ${opts.targetMachine}, the Telegram reply is served FROM it`,
|
|
54
|
+
surface: 'telegram',
|
|
55
|
+
riskCategory: 'happy-path',
|
|
56
|
+
volatility: 'safe',
|
|
57
|
+
channelId: opts.telegramTopicId,
|
|
58
|
+
input: message,
|
|
59
|
+
expect: { replyNotEmpty: true, responderMachine: opts.targetMachine },
|
|
60
|
+
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
if (opts.slackChannelId) {
|
|
64
|
+
scenarios.push({
|
|
65
|
+
id: 'mm-transfer-slack-reply-from-target',
|
|
66
|
+
description: `after transfer to ${opts.targetMachine}, the Slack reply is served FROM it (channel parity)`,
|
|
67
|
+
surface: 'slack',
|
|
68
|
+
riskCategory: 'channel-parity',
|
|
69
|
+
volatility: 'safe',
|
|
70
|
+
channelId: opts.slackChannelId,
|
|
71
|
+
input: message,
|
|
72
|
+
expect: { replyNotEmpty: true, responderMachine: opts.targetMachine },
|
|
73
|
+
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const matrix = {
|
|
77
|
+
featureId: opts.featureId ?? 'multi-machine-transfer',
|
|
78
|
+
surfaces,
|
|
79
|
+
riskCategories: opts.slackChannelId ? ['happy-path', 'channel-parity'] : ['happy-path'],
|
|
80
|
+
scenarios,
|
|
81
|
+
};
|
|
82
|
+
return this.deps.harness.run(matrix, opts.runId ? { runId: opts.runId } : {});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=LiveTestRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveTestRunner.js","sourceRoot":"","sources":["../../src/core/LiveTestRunner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TelegramLiveSender — the real Telegram `SurfaceSender` for the live-test harness
|
|
3
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.4). It posts a message into a
|
|
4
|
+
* Telegram forum topic AS A NON-AGENT IDENTITY (a DEMO bot / second account in a demo
|
|
5
|
+
* group — NOT the agent's own bot, which the agent would not treat as inbound), then
|
|
6
|
+
* waits for the AGENT's reply by polling the topic history for the next agent-authored
|
|
7
|
+
* message.
|
|
8
|
+
*
|
|
9
|
+
* Same shape as SlackLiveSender: the send transport (the demo-bot post) and the
|
|
10
|
+
* history read are INJECTED, so the CODE is complete + unit-testable here and the only
|
|
11
|
+
* wiring-time dependency is the demo-bot CREDENTIAL + a demo group/topic (provisioning).
|
|
12
|
+
*
|
|
13
|
+
* The agent's reply is identified DETERMINISTICALLY: the earliest history entry with
|
|
14
|
+
* `fromUser === false` (an agent outbound) whose messageId is strictly after the one we
|
|
15
|
+
* sent. Null on timeout. The responder-MACHINE attribution (the cross-machine proof) is
|
|
16
|
+
* the RealChannelDriver's injected placement reader, not this sender.
|
|
17
|
+
*/
|
|
18
|
+
import type { SurfaceSender } from './RealChannelDriver.js';
|
|
19
|
+
import type { SendResult, ReplyResult } from './LiveTestHarness.js';
|
|
20
|
+
/** The subset of TelegramAdapter.getTopicHistory's LogEntry this sender reads. */
|
|
21
|
+
export interface TelegramHistoryEntry {
|
|
22
|
+
messageId: number;
|
|
23
|
+
text: string;
|
|
24
|
+
/** true = inbound user message; false = an agent (bot) outbound — the reply we wait for. */
|
|
25
|
+
fromUser: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface TelegramLiveSenderDeps {
|
|
28
|
+
/** Post a message into the topic AS THE DEMO identity (demo-bot token). Returns the new message id. */
|
|
29
|
+
postAsDemoUser: (topicId: number, text: string) => Promise<{
|
|
30
|
+
messageId: number;
|
|
31
|
+
}>;
|
|
32
|
+
/** Read recent topic history (Echo's getTopicHistory), newest-or-oldest order tolerated. */
|
|
33
|
+
getHistory: (topicId: number, limit?: number) => TelegramHistoryEntry[] | Promise<TelegramHistoryEntry[]>;
|
|
34
|
+
pollIntervalMs?: number;
|
|
35
|
+
sleep?: (ms: number) => Promise<void>;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
logger?: (m: string) => void;
|
|
38
|
+
}
|
|
39
|
+
export declare class TelegramLiveSender implements SurfaceSender {
|
|
40
|
+
private readonly d;
|
|
41
|
+
constructor(deps: TelegramLiveSenderDeps);
|
|
42
|
+
private now;
|
|
43
|
+
private sleep;
|
|
44
|
+
private log;
|
|
45
|
+
private topicNum;
|
|
46
|
+
send(channelId: string, text: string): Promise<SendResult>;
|
|
47
|
+
awaitReply(channelId: string, opts: {
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
afterMessageId?: string;
|
|
50
|
+
}): Promise<Omit<ReplyResult, 'responderMachineId'> | null>;
|
|
51
|
+
private findAgentReply;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=TelegramLiveSender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelegramLiveSender.d.ts","sourceRoot":"","sources":["../../src/core/TelegramLiveSender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEpE,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,uGAAuG;IACvG,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClF,4FAA4F;IAC5F,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,oBAAoB,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAC1G,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qBAAa,kBAAmB,YAAW,aAAa;IACtD,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAyB;gBAC/B,IAAI,EAAE,sBAAsB;IAExC,OAAO,CAAC,GAAG;YACG,KAAK;IAGnB,OAAO,CAAC,GAAG;IAEX,OAAO,CAAC,QAAQ;IAMV,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAQ1D,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,oBAAoB,CAAC,GAAG,IAAI,CAAC;YAepI,cAAc;CAW7B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TelegramLiveSender — the real Telegram `SurfaceSender` for the live-test harness
|
|
3
|
+
* (docs/specs/live-user-channel-proof-standard.md §5.4). It posts a message into a
|
|
4
|
+
* Telegram forum topic AS A NON-AGENT IDENTITY (a DEMO bot / second account in a demo
|
|
5
|
+
* group — NOT the agent's own bot, which the agent would not treat as inbound), then
|
|
6
|
+
* waits for the AGENT's reply by polling the topic history for the next agent-authored
|
|
7
|
+
* message.
|
|
8
|
+
*
|
|
9
|
+
* Same shape as SlackLiveSender: the send transport (the demo-bot post) and the
|
|
10
|
+
* history read are INJECTED, so the CODE is complete + unit-testable here and the only
|
|
11
|
+
* wiring-time dependency is the demo-bot CREDENTIAL + a demo group/topic (provisioning).
|
|
12
|
+
*
|
|
13
|
+
* The agent's reply is identified DETERMINISTICALLY: the earliest history entry with
|
|
14
|
+
* `fromUser === false` (an agent outbound) whose messageId is strictly after the one we
|
|
15
|
+
* sent. Null on timeout. The responder-MACHINE attribution (the cross-machine proof) is
|
|
16
|
+
* the RealChannelDriver's injected placement reader, not this sender.
|
|
17
|
+
*/
|
|
18
|
+
export class TelegramLiveSender {
|
|
19
|
+
d;
|
|
20
|
+
constructor(deps) { this.d = deps; }
|
|
21
|
+
now() { return (this.d.now ?? Date.now)(); }
|
|
22
|
+
async sleep(ms) {
|
|
23
|
+
return (this.d.sleep ?? ((m) => new Promise((r) => setTimeout(r, m))))(ms);
|
|
24
|
+
}
|
|
25
|
+
log(m) { this.d.logger?.(`[telegram-live-sender] ${m}`); }
|
|
26
|
+
topicNum(channelId) {
|
|
27
|
+
const n = Number(channelId);
|
|
28
|
+
if (!Number.isFinite(n))
|
|
29
|
+
throw new Error(`telegram channelId "${channelId}" is not a numeric topic id`);
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
async send(channelId, text) {
|
|
33
|
+
const res = await this.d.postAsDemoUser(this.topicNum(channelId), text);
|
|
34
|
+
if (!Number.isFinite(res?.messageId)) {
|
|
35
|
+
throw new Error(`postAsDemoUser returned no messageId — message not posted`);
|
|
36
|
+
}
|
|
37
|
+
return { messageId: String(res.messageId) };
|
|
38
|
+
}
|
|
39
|
+
async awaitReply(channelId, opts) {
|
|
40
|
+
const topic = this.topicNum(channelId);
|
|
41
|
+
const afterId = opts.afterMessageId != null ? Number(opts.afterMessageId) : -Infinity;
|
|
42
|
+
const deadline = this.now() + opts.timeoutMs;
|
|
43
|
+
const pollMs = this.d.pollIntervalMs ?? 2000;
|
|
44
|
+
for (let first = true; first || this.now() < deadline; first = false) {
|
|
45
|
+
const reply = await this.findAgentReply(topic, afterId);
|
|
46
|
+
if (reply)
|
|
47
|
+
return reply;
|
|
48
|
+
if (this.now() >= deadline)
|
|
49
|
+
break;
|
|
50
|
+
await this.sleep(pollMs);
|
|
51
|
+
}
|
|
52
|
+
this.log(`no agent reply in topic ${topic} within ${opts.timeoutMs}ms`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
async findAgentReply(topicId, afterId) {
|
|
56
|
+
const entries = await this.d.getHistory(topicId, 100);
|
|
57
|
+
// Oldest-first by messageId so we return the EARLIEST agent reply after the prompt.
|
|
58
|
+
const oldestFirst = [...entries].sort((a, b) => a.messageId - b.messageId);
|
|
59
|
+
for (const e of oldestFirst) {
|
|
60
|
+
if (e.messageId <= afterId)
|
|
61
|
+
continue; // strictly after the prompt
|
|
62
|
+
if (e.fromUser)
|
|
63
|
+
continue; // skip inbound user messages (incl. our own demo post)
|
|
64
|
+
return { text: e.text ?? '', messageId: String(e.messageId) };
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=TelegramLiveSender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelegramLiveSender.js","sourceRoot":"","sources":["../../src/core/TelegramLiveSender.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-16T10:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-16T10:44:35.492Z",
|
|
5
|
+
"instarVersion": "1.3.597",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`TelegramLiveSender` — a real Telegram `SurfaceSender` for the live-test harness (spec §5.4), ships DARK (not wired). Posts into a forum topic as a non-agent demo identity (injected post fn), awaits the agent's reply by polling topic history for the earliest `fromUser===false` entry strictly after the sent messageId. Parameterized on the demo-bot post fn + history reader, so only the demo-bot credential + demo group need provisioning.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
Nothing yet — internal harness infrastructure, ships dark (no runtime surface, no behavior change).
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
None user-facing. Internally: the real Telegram drive for the live-test harness (Telegram half of the Telegram-AND-Slack bar). No new routes, config, or flags.
|
|
17
|
+
|
|
18
|
+
## Evidence
|
|
19
|
+
|
|
20
|
+
- `tests/unit/TelegramLiveSender.test.ts` — 8 tests: post→id, non-numeric-topic throw, no-id throw, earliest-agent-reply-after-prompt, ignores inbound, poll-until-lands, timeout→null, async getHistory.
|
|
21
|
+
- `tsc --noEmit` clean. instar-dev gate green.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`LiveTestRunner` (spec §6) — the orchestrator that applies the live-test standard to the cross-machine transfer, ships DARK (not wired). It transfers the throwaway topic to the target machine, demands the honest `seatMoved` signal (throws if the seat didn't move — no misleading PASS over the original "ok:true but moved nothing" bug), then runs the LiveTestHarness over a matrix asserting the reply was served FROM the target machine (the cross-machine proof). Pure orchestration over an injected harness + transfer action.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
Nothing yet — internal harness infrastructure, ships dark (no runtime surface). The payoff is the capstone: proving a real Laptop↔Mini seat move with a reply served from the destination, through the real channels.
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
None user-facing. Internally: the orchestration that runs the multi-machine transfer capstone and records the signed PASS/FAIL artifact. No new routes, config, or flags (the route wrapper is the next increment).
|
|
17
|
+
|
|
18
|
+
## Evidence
|
|
19
|
+
|
|
20
|
+
- `tests/unit/LiveTestRunner.test.ts` — 5 tests: non-move throws (no harness run), moved+reply-from-target→PASS, moved+wrong-machine→FAIL, slack channel-parity scenario added, empty-reply→FAIL.
|
|
21
|
+
- `tsc --noEmit` clean. instar-dev gate green.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Side-Effects Review — LiveTestRunner (multi-machine transfer capstone orchestrator)
|
|
2
|
+
|
|
3
|
+
**Slug:** live-test-runner
|
|
4
|
+
**Spec:** docs/specs/live-user-channel-proof-standard.md §6 (apply the standard to multi-machine — the first feature)
|
|
5
|
+
**Files:** src/core/LiveTestRunner.ts, tests/unit/LiveTestRunner.test.ts
|
|
6
|
+
**Posture:** ships DARK — pure orchestration over an injected harness + transfer action. NOT wired into server.ts.
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
LiveTestRunner encodes the capstone flow: (1) transfer the throwaway topic to the target machine and DEMAND the honest `seatMoved` signal (throw if it didn't move — refuse a misleading PASS over the original "ok:true but moved nothing" bug); (2) run the LiveTestHarness over a matrix that sends a real message per channel and asserts the reply was served FROM the target machine (the `responderMachine` expectation = the cross-machine proof).
|
|
10
|
+
|
|
11
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
12
|
+
Not a decision gate over agent behavior — it's a test orchestrator. The one control-flow decision (throw on `seatMoved===false`) is a FAIL-LOUD on a test premise, not a runtime authority over messages/sessions. Compliant: it produces a PASS/FAIL artifact (a signal the completion gate already consumes), holds no blocking authority.
|
|
13
|
+
|
|
14
|
+
## Phase 4 — Side-effects answers
|
|
15
|
+
1. **Over-block** — n/a (no runtime block). Throwing on a non-moved seat is intended: it prevents recording a PASS when the premise failed. The alternative (running anyway) would manufacture a misleading verdict.
|
|
16
|
+
2. **Under-block** — the runner trusts the injected `transfer`'s `seatMoved`. If a transfer LIES (reports seatMoved=true but didn't move), the harness still catches it downstream: the reply's `responderMachine` won't equal target → FAIL. So a lying transfer can't produce a false PASS — the responder assertion is the real backstop.
|
|
17
|
+
3. **Level-of-abstraction fit** — correct: thin orchestration over the merged harness + an injected transfer. It does NOT reimplement transfer or the harness; it sequences them. The server.ts route injects the real `/pool/transfer` + RealChannelDriver.
|
|
18
|
+
4. **Signal vs authority** — compliant (produces an artifact; no authority).
|
|
19
|
+
5. **Interactions** — none yet (dark, unwired). When wired, it CALLS `/pool/transfer` (a real seat move on a THROWAWAY topic) then sends real messages — those surfaces are the server.ts route's review. The runner itself only sequences injected actions.
|
|
20
|
+
6. **External surfaces** — none in this increment (unwired). When wired, the real transfer + sends are external — but scoped to a throwaway topic/demo channel by the caller (the §5.3 isolation in DemoChannelRegistry + the throwaway-topic choice).
|
|
21
|
+
7. **Multi-machine posture** — this IS the multi-machine capstone: it proves a seat moves between machines and the reply is served from the destination. The `responderMachine` assertion (via the driver's placement reader, which reads the authoritative `/pool/placement`) is the cross-machine proof. No single-machine assumption — the whole point is cross-machine.
|
|
22
|
+
8. **Rollback cost** — trivial: dark, unwired. Revert the commit.
|
|
23
|
+
|
|
24
|
+
## No-deferrals
|
|
25
|
+
The server.ts route (`POST /live-test/run`) that wires the real driver + transfer, and the demo-channel sender-credential provisioning, are the NEXT tracked steps (CMT-1568, `.instar/plans/live-test-harness-drivers-BUILD.md` GATE 1/2), not deferrals of this orchestrator. This module is complete + unit-tested (5 tests: non-move throws, moved+reply-from-target PASS, wrong-machine FAIL, slack-parity scenario, empty-reply FAIL).
|
|
26
|
+
|
|
27
|
+
## Phase 5 — Second-pass review
|
|
28
|
+
Borderline (it has a control-flow throw), but it adds no runtime authority over messages/sessions/lifecycle — it orchestrates a test and records an artifact. The genuine cross-machine safety (no false PASS) rests on the harness's responder assertion (reviewed in #1195/#1196), which this can't bypass. Not required; noted.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Side-Effects Review — Live-test Telegram sender
|
|
2
|
+
|
|
3
|
+
**Slug:** live-test-telegram-sender
|
|
4
|
+
**Spec:** docs/specs/live-user-channel-proof-standard.md §5.4 (Platform-Sanctioned Automation)
|
|
5
|
+
**Files:** src/core/TelegramLiveSender.ts, tests/unit/TelegramLiveSender.test.ts
|
|
6
|
+
**Posture:** ships DARK — one pure/injectable SurfaceSender, NOT wired into server.ts.
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
TelegramLiveSender — the real Telegram `SurfaceSender`: posts into a forum topic AS A NON-AGENT identity (an injected demo-bot post fn), then `awaitReply` polls the topic history for the AGENT's reply (the earliest entry with `fromUser === false` strictly after the sent messageId). Same proven shape as SlackLiveSender; parameterized on the demo-bot post fn + the history reader, so only the demo-bot CREDENTIAL + a demo group/topic need provisioning.
|
|
10
|
+
|
|
11
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
12
|
+
Not a decision point — transport (post + poll). No block/filter/gate. Compliant.
|
|
13
|
+
|
|
14
|
+
## Phase 4 — Side-effects answers
|
|
15
|
+
1. **Over-block** — n/a. Worst case: the reply match misses a genuine agent reply → scenario records no-reply FAIL. Mitigated by the deterministic `fromUser===false` + strictly-after-messageId match over the full poll window.
|
|
16
|
+
2. **Under-block** — n/a. Risk of matching a STALE agent message mitigated by the `messageId > afterId` strictly-after guard + oldest-first scan (earliest reply after the prompt).
|
|
17
|
+
3. **Level-of-abstraction fit** — correct: the concrete Telegram adapter for the harness's `SurfaceSender` seam. Wraps the demo-bot post + getTopicHistory rather than reinventing them.
|
|
18
|
+
4. **Signal vs authority** — compliant (transport, no authority).
|
|
19
|
+
5. **Interactions** — none yet (dark, unwired). The demo-bot identity is SEPARATE from Echo's own bot, so a demo post can't be confused with Echo's outbound; history read is read-only.
|
|
20
|
+
6. **External surfaces** — when wired, it WILL post a real message into a demo Telegram group via the demo-bot token. THIS increment: no wiring, no external surface. The demo-bot token + demo group is the provisioning dependency; the code is parameterized on it.
|
|
21
|
+
7. **Multi-machine posture** — machine-agnostic (talks to the Telegram Bot API + reads topic history). The cross-machine attribution is the RealChannelDriver's PlacementResponderReader, not this sender. No single-machine assumption.
|
|
22
|
+
8. **Rollback cost** — trivial: dark, unwired. Revert the commit.
|
|
23
|
+
|
|
24
|
+
## No-deferrals
|
|
25
|
+
The runner route (wires both senders + RealChannelDriver + harness + the multi-machine matrix) is the NEXT tracked increment (CMT-1568, `.instar/plans/live-test-harness-drivers-BUILD.md`), not a deferral. This sender is complete + fully unit-tested (8 tests). The demo-bot/group provisioning is an external-credential dependency tracked in the plan, not a code deferral — the code is parameterized so only the credential is missing.
|
|
26
|
+
|
|
27
|
+
## Phase 5 — Second-pass review
|
|
28
|
+
Not required: transport adapter, no block/allow/lifecycle/sentinel/gate surface (the Phase-5 triggers).
|