@dmsdc-ai/aigentry-telepty 0.3.3 → 0.4.0
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/CHANGELOG.md +182 -0
- package/README.md +67 -1
- package/cli.js +161 -54
- package/cross-machine.js +132 -0
- package/daemon.js +355 -5
- package/host-spec.js +60 -0
- package/mcp-server/index.mjs +24 -3
- package/package.json +30 -5
- package/session-state.js +23 -0
- package/skill-installer.js +42 -6
- package/skills/telepty/SKILL.md +1 -1
- package/skills/telepty-allow/SKILL.md +1 -1
- package/skills/telepty-attach/SKILL.md +1 -1
- package/skills/telepty-broadcast/SKILL.md +1 -1
- package/skills/telepty-daemon/SKILL.md +1 -1
- package/skills/telepty-inject/SKILL.md +76 -4
- package/skills/telepty-list/SKILL.md +1 -1
- package/skills/telepty-listen/SKILL.md +1 -1
- package/skills/telepty-rename/SKILL.md +1 -1
- package/skills/telepty-session/SKILL.md +1 -1
- package/src/init/print-snippet.js +114 -0
- package/src/init/snippets/agents.md +15 -0
- package/src/init/snippets/claude.md +15 -0
- package/src/init/snippets/gemini.md +15 -0
- package/src/prompt-symbol-registry.js +43 -1
- package/.claude/commands/telepty-allow.md +0 -58
- package/.claude/commands/telepty-attach.md +0 -22
- package/.claude/commands/telepty-inject.md +0 -72
- package/.claude/commands/telepty-list.md +0 -22
- package/.claude/commands/telepty-manual-test.md +0 -73
- package/.claude/commands/telepty-start.md +0 -25
- package/.claude/commands/telepty-test.md +0 -25
- package/.claude/commands/telepty.md +0 -82
- package/AGENTS.md +0 -74
- package/BOUNDARY.md +0 -31
- package/BUS_EVENT_SCHEMA.md +0 -206
- package/CLAUDE.md +0 -100
- package/GEMINI.md +0 -10
- package/URGENT_ISSUES.resolved.md +0 -1
- package/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +0 -447
- package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +0 -571
- package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +0 -608
- package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +0 -139
- package/protocol/mailbox.md +0 -244
- package/specs/codex-inject-spec.md +0 -201
- package/specs/enforce-report-spec.md +0 -237
- package/templates/AGENTS.md +0 -71
package/daemon.js
CHANGED
|
@@ -15,6 +15,7 @@ const { UnixSocketNotifier } = require('./src/mailbox/notifier');
|
|
|
15
15
|
const { SessionStateManager, STATE_DISPLAY, stripAnsi: stripAnsiState } = require('./session-state');
|
|
16
16
|
const { classifyReportPrompt, buildAutoSummary } = require('./src/report-enforcement');
|
|
17
17
|
const submitGate = require('./src/submit-gate');
|
|
18
|
+
const readyRegistry = require('./src/prompt-symbol-registry');
|
|
18
19
|
|
|
19
20
|
const config = getConfig();
|
|
20
21
|
const EXPECTED_TOKEN = config.authToken;
|
|
@@ -26,6 +27,8 @@ const SESSION_STALE_SECONDS = Math.max(1, Number(process.env.TELEPTY_SESSION_STA
|
|
|
26
27
|
const SESSION_CLEANUP_SECONDS = Math.max(SESSION_STALE_SECONDS, Number(process.env.TELEPTY_SESSION_CLEANUP_SECONDS || 300));
|
|
27
28
|
const DELIVERY_TIMEOUT_MS = Math.max(100, Number(process.env.TELEPTY_DELIVERY_TIMEOUT_MS || 5000));
|
|
28
29
|
const HEALTH_POLL_MS = Math.max(100, Number(process.env.TELEPTY_HEALTH_POLL_MS || 10000));
|
|
30
|
+
const BOOTSTRAP_READY_TIMEOUT_MS = Math.max(500, Number(process.env.TELEPTY_BOOTSTRAP_READY_TIMEOUT_MS || 30000));
|
|
31
|
+
const WRAPPED_SUBMIT_DELAY_MS = 500;
|
|
29
32
|
|
|
30
33
|
// Session state machine manager — auto-detects session state from PTY output
|
|
31
34
|
const sessionStateManager = new SessionStateManager({
|
|
@@ -354,6 +357,292 @@ function getSessionHealthReason(session, healthStatus) {
|
|
|
354
357
|
return session.ptyProcess && !session.ptyProcess.killed ? 'PTY_RUNNING' : 'PTY_EXITED';
|
|
355
358
|
}
|
|
356
359
|
|
|
360
|
+
function sleep(ms) {
|
|
361
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isBootstrapGatedSession(session) {
|
|
365
|
+
return !!(session && session.type === 'wrapped' && readyRegistry.isKnownAiCli(session.command));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function initializeBootstrapState(session) {
|
|
369
|
+
if (!session) return session;
|
|
370
|
+
if (!Array.isArray(session.bootstrapQueue)) {
|
|
371
|
+
session.bootstrapQueue = [];
|
|
372
|
+
}
|
|
373
|
+
session.bootstrapDraining = session.bootstrapDraining === true;
|
|
374
|
+
session.bootstrapDrainPromise = session.bootstrapDrainPromise || null;
|
|
375
|
+
session.bootstrapPromptPoll = session.bootstrapPromptPoll || null;
|
|
376
|
+
|
|
377
|
+
if (isBootstrapGatedSession(session)) {
|
|
378
|
+
session.bootstrapReady = session.bootstrapReady === true;
|
|
379
|
+
session.bootstrapReadyAt = session.bootstrapReadyAt || null;
|
|
380
|
+
session.bootstrapReadyReason = session.bootstrapReadyReason || null;
|
|
381
|
+
session.ready = session.bootstrapReady === true;
|
|
382
|
+
} else {
|
|
383
|
+
session.bootstrapReady = true;
|
|
384
|
+
session.bootstrapReadyAt = session.bootstrapReadyAt || new Date().toISOString();
|
|
385
|
+
session.bootstrapReadyReason = session.bootstrapReadyReason || 'generic_command_compat';
|
|
386
|
+
session.ready = true;
|
|
387
|
+
}
|
|
388
|
+
return session;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isBootstrapReady(session) {
|
|
392
|
+
return !isBootstrapGatedSession(session) || session.bootstrapReady === true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function buildBootstrapBlock(session) {
|
|
396
|
+
return {
|
|
397
|
+
gated: isBootstrapGatedSession(session),
|
|
398
|
+
ready: isBootstrapReady(session),
|
|
399
|
+
ready_at: session.bootstrapReadyAt || null,
|
|
400
|
+
reason: session.bootstrapReadyReason || null,
|
|
401
|
+
queued: Array.isArray(session.bootstrapQueue) ? session.bootstrapQueue.length : 0,
|
|
402
|
+
draining: session.bootstrapDraining === true
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function shouldQueueBootstrapOperation(session) {
|
|
407
|
+
return isBootstrapGatedSession(session) && !isBootstrapReady(session);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function hasBootstrapBacklog(session) {
|
|
411
|
+
return !!(session && Array.isArray(session.bootstrapQueue) && session.bootstrapQueue.length > 0);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function emitBootstrapEvent(eventType, sessionId, session, extra = {}) {
|
|
415
|
+
broadcastSessionEvent(eventType, sessionId, session, {
|
|
416
|
+
extra: {
|
|
417
|
+
bootstrap: buildBootstrapBlock(session),
|
|
418
|
+
...extra
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function enqueueBootstrapOperation(sessionId, session, operation) {
|
|
424
|
+
initializeBootstrapState(session);
|
|
425
|
+
const op = {
|
|
426
|
+
op_id: crypto.randomUUID(),
|
|
427
|
+
queued_at: new Date().toISOString(),
|
|
428
|
+
...operation
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
if (op.type === 'submit') {
|
|
432
|
+
op.promise = new Promise((resolve) => {
|
|
433
|
+
op.resolve = resolve;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
session.bootstrapQueue.push(op);
|
|
438
|
+
emitBootstrapEvent('bootstrap_queue_queued', sessionId, session, {
|
|
439
|
+
op_id: op.op_id,
|
|
440
|
+
operation: op.type,
|
|
441
|
+
depth: session.bootstrapQueue.length
|
|
442
|
+
});
|
|
443
|
+
scheduleBootstrapPromptPoll(sessionId, session);
|
|
444
|
+
return op;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function resolveBootstrapSubmit(op, result) {
|
|
448
|
+
if (op && typeof op.resolve === 'function') {
|
|
449
|
+
op.resolve(result);
|
|
450
|
+
op.resolve = null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function bootstrapQueuedResponse(op, extra = {}) {
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
strategy: 'bootstrap_queue',
|
|
458
|
+
queued: true,
|
|
459
|
+
bootstrap_queued: true,
|
|
460
|
+
bootstrap_op_id: op.op_id,
|
|
461
|
+
...extra
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function executeBootstrapInject(sessionId, session, op) {
|
|
466
|
+
const prompt = typeof op.prompt === 'string' ? op.prompt : '';
|
|
467
|
+
const textResult = await writeDataToSession(sessionId, session, prompt);
|
|
468
|
+
if (!textResult.success) return textResult;
|
|
469
|
+
|
|
470
|
+
if (!op.noEnter) {
|
|
471
|
+
await sleep(WRAPPED_SUBMIT_DELAY_MS);
|
|
472
|
+
const submitResult = await writeDataToSession(sessionId, session, '\r');
|
|
473
|
+
if (!submitResult.success) return submitResult;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
session.lastActivityAt = new Date().toISOString();
|
|
477
|
+
return {
|
|
478
|
+
success: true,
|
|
479
|
+
strategy: 'bootstrap_direct',
|
|
480
|
+
submit: op.noEnter ? 'skipped' : 'sent'
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function executeBootstrapSubmit(sessionId, session, op) {
|
|
485
|
+
const strategy = terminalLevelSubmit(sessionId, session);
|
|
486
|
+
if (!strategy) {
|
|
487
|
+
return {
|
|
488
|
+
status: 503,
|
|
489
|
+
body: {
|
|
490
|
+
error: 'Submit failed via all strategies (kitty/cmux/pty)',
|
|
491
|
+
strategy: 'none',
|
|
492
|
+
attempts: 0,
|
|
493
|
+
gated: false,
|
|
494
|
+
bootstrap_queued: true
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
status: 200,
|
|
500
|
+
body: {
|
|
501
|
+
success: true,
|
|
502
|
+
strategy,
|
|
503
|
+
attempts: 1,
|
|
504
|
+
gated: false,
|
|
505
|
+
verify: null,
|
|
506
|
+
bootstrap_queued: true
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function drainBootstrapQueue(sessionId, session) {
|
|
512
|
+
if (!session || session.bootstrapDraining) {
|
|
513
|
+
return session ? session.bootstrapDrainPromise : null;
|
|
514
|
+
}
|
|
515
|
+
if (!isBootstrapReady(session)) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
session.bootstrapDraining = true;
|
|
520
|
+
session.bootstrapDrainPromise = (async () => {
|
|
521
|
+
while (hasBootstrapBacklog(session)) {
|
|
522
|
+
const op = session.bootstrapQueue.shift();
|
|
523
|
+
try {
|
|
524
|
+
if (op.cancelled) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (op.type === 'inject') {
|
|
528
|
+
const result = await executeBootstrapInject(sessionId, session, op);
|
|
529
|
+
if (!result.success) {
|
|
530
|
+
emitBootstrapEvent('bootstrap_queue_failed', sessionId, session, {
|
|
531
|
+
op_id: op.op_id,
|
|
532
|
+
operation: op.type,
|
|
533
|
+
code: result.code || 'DELIVERY_FAILED',
|
|
534
|
+
error: result.error || 'bootstrap delivery failed'
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
} else if (op.type === 'submit') {
|
|
538
|
+
const result = await executeBootstrapSubmit(sessionId, session, op);
|
|
539
|
+
resolveBootstrapSubmit(op, result);
|
|
540
|
+
if (result.status >= 400) {
|
|
541
|
+
emitBootstrapEvent('bootstrap_queue_failed', sessionId, session, {
|
|
542
|
+
op_id: op.op_id,
|
|
543
|
+
operation: op.type,
|
|
544
|
+
code: result.body.code || 'SUBMIT_FAILED',
|
|
545
|
+
error: result.body.error || 'bootstrap submit failed'
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (op.type === 'submit') {
|
|
551
|
+
resolveBootstrapSubmit(op, {
|
|
552
|
+
status: 500,
|
|
553
|
+
body: {
|
|
554
|
+
error: error.message || 'bootstrap submit failed',
|
|
555
|
+
strategy: 'none',
|
|
556
|
+
attempts: 0,
|
|
557
|
+
gated: false,
|
|
558
|
+
bootstrap_queued: true
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
emitBootstrapEvent('bootstrap_queue_failed', sessionId, session, {
|
|
563
|
+
op_id: op.op_id,
|
|
564
|
+
operation: op.type,
|
|
565
|
+
code: 'BOOTSTRAP_DRAIN_FAILED',
|
|
566
|
+
error: error.message || 'bootstrap drain failed'
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
emitBootstrapEvent('bootstrap_queue_drained', sessionId, session);
|
|
572
|
+
})().finally(() => {
|
|
573
|
+
session.bootstrapDraining = false;
|
|
574
|
+
session.bootstrapDrainPromise = null;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
return session.bootstrapDrainPromise;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function markBootstrapReady(sessionId, session, reason) {
|
|
581
|
+
if (!session) return false;
|
|
582
|
+
initializeBootstrapState(session);
|
|
583
|
+
if (!isBootstrapGatedSession(session)) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (session.bootstrapReady === true) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
session.bootstrapReady = true;
|
|
591
|
+
session.bootstrapReadyAt = new Date().toISOString();
|
|
592
|
+
session.bootstrapReadyReason = reason || 'ready';
|
|
593
|
+
session.ready = true;
|
|
594
|
+
emitBootstrapEvent('bootstrap_ready', sessionId, session, { reason: session.bootstrapReadyReason });
|
|
595
|
+
drainBootstrapQueue(sessionId, session);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function scheduleBootstrapPromptPoll(sessionId, session) {
|
|
600
|
+
if (!session || !isBootstrapGatedSession(session) || isBootstrapReady(session)) return;
|
|
601
|
+
if (session.bootstrapPromptPoll || session.backend !== 'cmux' || !session.cmuxWorkspaceId) return;
|
|
602
|
+
if (!isOpenWebSocket(session.ownerWs)) return;
|
|
603
|
+
|
|
604
|
+
session.bootstrapPromptPoll = submitGate.awaitPromptSymbol(session, {
|
|
605
|
+
timeoutMs: BOOTSTRAP_READY_TIMEOUT_MS
|
|
606
|
+
}).then((result) => {
|
|
607
|
+
session.bootstrapPromptPoll = null;
|
|
608
|
+
if (result && result.ready && isOpenWebSocket(session.ownerWs)) {
|
|
609
|
+
markBootstrapReady(sessionId, session, 'cmux_prompt_symbol');
|
|
610
|
+
} else if (result && result.reason) {
|
|
611
|
+
emitBootstrapEvent('bootstrap_ready_timeout', sessionId, session, {
|
|
612
|
+
reason: result.reason,
|
|
613
|
+
waited_ms: result.waited_ms || 0
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}).catch((error) => {
|
|
617
|
+
session.bootstrapPromptPoll = null;
|
|
618
|
+
emitBootstrapEvent('bootstrap_ready_timeout', sessionId, session, {
|
|
619
|
+
reason: 'prompt_symbol_error',
|
|
620
|
+
error: error.message || String(error)
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function waitForBootstrapSubmit(op, session, timeoutMs) {
|
|
626
|
+
const timeout = sleep(timeoutMs).then(() => {
|
|
627
|
+
op.cancelled = true;
|
|
628
|
+
return {
|
|
629
|
+
status: 504,
|
|
630
|
+
body: {
|
|
631
|
+
error: 'Submit bootstrap-timeout — target CLI did not become ready',
|
|
632
|
+
reason: 'bootstrap_not_ready',
|
|
633
|
+
last_state: sessionStateManager.getState(session.id)?.state || null,
|
|
634
|
+
strategy: 'none',
|
|
635
|
+
attempts: 0,
|
|
636
|
+
gated: true,
|
|
637
|
+
bootstrap_queued: true,
|
|
638
|
+
bootstrap_op_id: op.op_id,
|
|
639
|
+
bootstrap: buildBootstrapBlock(session)
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
return Promise.race([op.promise, timeout]);
|
|
644
|
+
}
|
|
645
|
+
|
|
357
646
|
function buildSessionTransportBlock(session, options = {}) {
|
|
358
647
|
if (!session) {
|
|
359
648
|
return null;
|
|
@@ -380,7 +669,8 @@ function buildSessionTransportBlock(session, options = {}) {
|
|
|
380
669
|
last_disconnected_at: session.lastDisconnectedAt || null,
|
|
381
670
|
last_inject_from: session.lastInjectFrom || null,
|
|
382
671
|
last_reply_to: session.lastInjectReplyTo || null,
|
|
383
|
-
last_thread_id: session.lastThreadId || null
|
|
672
|
+
last_thread_id: session.lastThreadId || null,
|
|
673
|
+
bootstrap: buildBootstrapBlock(session)
|
|
384
674
|
};
|
|
385
675
|
}
|
|
386
676
|
|
|
@@ -645,6 +935,28 @@ function terminalLevelSubmit(id, session) {
|
|
|
645
935
|
|
|
646
936
|
async function deliverInjectionToSession(id, session, prompt, options = {}) {
|
|
647
937
|
const now = Date.now();
|
|
938
|
+
if (!options.bypassBootstrapQueue && shouldQueueBootstrapOperation(session)) {
|
|
939
|
+
const healthStatus = getSessionHealthStatus(session, { nowMs: now });
|
|
940
|
+
if (healthStatus === 'STALE') {
|
|
941
|
+
return { success: false, httpStatus: 410, code: 'STALE', error: 'Session is stale and awaiting cleanup.' };
|
|
942
|
+
}
|
|
943
|
+
const op = enqueueBootstrapOperation(id, session, {
|
|
944
|
+
type: 'inject',
|
|
945
|
+
prompt,
|
|
946
|
+
noEnter: !!options.noEnter,
|
|
947
|
+
options: {
|
|
948
|
+
source: options.source || 'inject',
|
|
949
|
+
from: options.from || 'daemon'
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
session.lastActivityAt = new Date(now).toISOString();
|
|
953
|
+
return bootstrapQueuedResponse(op, {
|
|
954
|
+
msg_id: op.op_id,
|
|
955
|
+
pending: session.bootstrapQueue.length,
|
|
956
|
+
submit: options.noEnter ? 'skipped' : 'queued'
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
648
960
|
const injectFailure = getInjectFailure(session, { nowMs: now });
|
|
649
961
|
if (injectFailure) {
|
|
650
962
|
return { success: false, ...injectFailure };
|
|
@@ -834,6 +1146,7 @@ for (const [id, meta] of Object.entries(_persisted)) {
|
|
|
834
1146
|
lastStateReportAt: meta.lastStateReportAt || null,
|
|
835
1147
|
stateReport: meta.stateReport || null,
|
|
836
1148
|
clients: new Set(), isClosing: false, outputRing: [], ready: true, };
|
|
1149
|
+
initializeBootstrapState(sessions[id]);
|
|
837
1150
|
console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
|
|
838
1151
|
}
|
|
839
1152
|
}
|
|
@@ -1020,6 +1333,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
1020
1333
|
existing.ready = true;
|
|
1021
1334
|
markSessionConnected(existing);
|
|
1022
1335
|
}
|
|
1336
|
+
initializeBootstrapState(existing);
|
|
1023
1337
|
console.log(`[REGISTER] Re-registered session ${session_id} (type: ${existing.type}, updated metadata)`);
|
|
1024
1338
|
return res.status(200).json({ session_id, type: existing.type, command: existing.command, cwd: existing.cwd, reregistered: true });
|
|
1025
1339
|
}
|
|
@@ -1049,8 +1363,9 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
1049
1363
|
clients: new Set(),
|
|
1050
1364
|
isClosing: false,
|
|
1051
1365
|
outputRing: [],
|
|
1052
|
-
ready: true, //
|
|
1053
|
-
|
|
1366
|
+
ready: true, // unknown commands remain injectable once registered (#150)
|
|
1367
|
+
};
|
|
1368
|
+
initializeBootstrapState(sessionRecord);
|
|
1054
1369
|
// Check for existing session with same base alias and emit replaced event
|
|
1055
1370
|
const baseAlias = session_id.replace(/-\d+$/, '');
|
|
1056
1371
|
const replaced = Object.keys(sessions).find(id => {
|
|
@@ -1521,6 +1836,18 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
1521
1836
|
|
|
1522
1837
|
console.log(`[SUBMIT] Session ${id} (${session.command})${retries > 0 ? `, retries: ${retries}, pre_delay: ${preDelayMs}ms` : ''}${gateOff ? ' [gate=off]' : ''}`);
|
|
1523
1838
|
|
|
1839
|
+
if (isBootstrapGatedSession(session) && (!isBootstrapReady(session) || hasBootstrapBacklog(session) || session.bootstrapDraining)) {
|
|
1840
|
+
const op = enqueueBootstrapOperation(id, session, {
|
|
1841
|
+
type: 'submit',
|
|
1842
|
+
body: { ...(req.body || {}) }
|
|
1843
|
+
});
|
|
1844
|
+
if (isBootstrapReady(session)) {
|
|
1845
|
+
drainBootstrapQueue(id, session);
|
|
1846
|
+
}
|
|
1847
|
+
const queuedSubmit = await waitForBootstrapSubmit(op, session, gateTimeoutMs);
|
|
1848
|
+
return res.status(queuedSubmit.status).json(queuedSubmit.body);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1524
1851
|
function emitSubmitBus(payload) {
|
|
1525
1852
|
const busMsg = JSON.stringify({
|
|
1526
1853
|
type: 'submit',
|
|
@@ -1805,6 +2132,12 @@ app.post('/api/sessions/:id/inject', async (req, res) => {
|
|
|
1805
2132
|
delete pendingReports[senderAlias];
|
|
1806
2133
|
const elapsedSecs = Number(((Date.now() - new Date(senderPending.injectedAt).getTime()) / 1000).toFixed(1));
|
|
1807
2134
|
const senderSession = sessions[senderAlias];
|
|
2135
|
+
sessionStateManager.markIdle(senderAlias, 1.0, {
|
|
2136
|
+
trigger: 'report_inject',
|
|
2137
|
+
report_inject_id: inject_id,
|
|
2138
|
+
report_status: classification,
|
|
2139
|
+
source: senderPending.source
|
|
2140
|
+
});
|
|
1808
2141
|
const eventType =
|
|
1809
2142
|
classification === 'report_blocked' ? 'TASK_BLOCKED_WITH_REASON' :
|
|
1810
2143
|
classification === 'report_dismissed' ? 'TASK_DISMISSED' :
|
|
@@ -1881,7 +2214,17 @@ app.post('/api/sessions/:id/inject', async (req, res) => {
|
|
|
1881
2214
|
});
|
|
1882
2215
|
}
|
|
1883
2216
|
|
|
1884
|
-
res.json({
|
|
2217
|
+
res.json({
|
|
2218
|
+
success: true,
|
|
2219
|
+
inject_id,
|
|
2220
|
+
strategy: delivery.strategy,
|
|
2221
|
+
submit: delivery.submit,
|
|
2222
|
+
...(delivery.bootstrap_queued ? {
|
|
2223
|
+
bootstrap_queued: true,
|
|
2224
|
+
bootstrap_op_id: delivery.bootstrap_op_id || delivery.msg_id,
|
|
2225
|
+
pending: delivery.pending
|
|
2226
|
+
} : {})
|
|
2227
|
+
});
|
|
1885
2228
|
} catch (err) {
|
|
1886
2229
|
emitInjectFailureEvent(id, 'DELIVERY_FAILED', err.message, { inject_id }, session);
|
|
1887
2230
|
res.status(500).json(buildErrorBody('DELIVERY_FAILED', err.message));
|
|
@@ -2607,6 +2950,7 @@ wss.on('connection', (ws, req) => {
|
|
|
2607
2950
|
outputRing: [],
|
|
2608
2951
|
ready: true,
|
|
2609
2952
|
};
|
|
2953
|
+
initializeBootstrapState(autoSession);
|
|
2610
2954
|
sessions[sessionId] = autoSession;
|
|
2611
2955
|
console.log(`[WS] Auto-registered wrapped session ${sessionId} on reconnect`);
|
|
2612
2956
|
// Set tab title via kitty (no \x0c redraw — it causes flickering on multi-session reconnect)
|
|
@@ -2639,7 +2983,9 @@ wss.on('connection', (ws, req) => {
|
|
|
2639
2983
|
}
|
|
2640
2984
|
activeSession.ownerWs = ws;
|
|
2641
2985
|
markSessionConnected(activeSession);
|
|
2986
|
+
initializeBootstrapState(activeSession);
|
|
2642
2987
|
console.log(`[WS] Wrap owner ${isOwnerConnect && activeSession.clients.size > 1 ? 're-' : ''}connected for session ${sessionId} (Total: ${activeSession.clients.size})`);
|
|
2988
|
+
scheduleBootstrapPromptPoll(sessionId, activeSession);
|
|
2643
2989
|
if (hadDisconnectedOwner) {
|
|
2644
2990
|
emitSessionLifecycleEvent('session_reconnect', sessionId, activeSession);
|
|
2645
2991
|
}
|
|
@@ -2665,7 +3011,11 @@ wss.on('connection', (ws, req) => {
|
|
|
2665
3011
|
}
|
|
2666
3012
|
});
|
|
2667
3013
|
} else if (type === 'ready') {
|
|
2668
|
-
activeSession
|
|
3014
|
+
if (isBootstrapGatedSession(activeSession)) {
|
|
3015
|
+
markBootstrapReady(sessionId, activeSession, 'bridge_ready');
|
|
3016
|
+
} else {
|
|
3017
|
+
activeSession.ready = true;
|
|
3018
|
+
}
|
|
2669
3019
|
activeSession.lastActivityAt = new Date().toISOString();
|
|
2670
3020
|
console.log(`[READY] Session ${sessionId} CLI is ready for inject`);
|
|
2671
3021
|
// Broadcast readiness to bus (cmux/kitty paths now enabled for this session)
|
package/host-spec.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PORT = 3848;
|
|
4
|
+
|
|
5
|
+
function parseHostSpec(value, defaultPort = DEFAULT_PORT) {
|
|
6
|
+
if (value === undefined || value === null || value === '') {
|
|
7
|
+
return { host: '127.0.0.1', port: defaultPort };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let raw = String(value).trim();
|
|
11
|
+
if (!raw) {
|
|
12
|
+
return { host: '127.0.0.1', port: defaultPort };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
raw = raw.replace(/^https?:\/\//i, '');
|
|
16
|
+
raw = raw.replace(/\/.*$/, '');
|
|
17
|
+
|
|
18
|
+
const ipv6Bracketed = raw.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
19
|
+
if (ipv6Bracketed) {
|
|
20
|
+
const port = ipv6Bracketed[2] ? Number(ipv6Bracketed[2]) : defaultPort;
|
|
21
|
+
return { host: ipv6Bracketed[1], port };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const colonCount = (raw.match(/:/g) || []).length;
|
|
25
|
+
if (colonCount > 1) {
|
|
26
|
+
return { host: raw, port: defaultPort };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const hostPort = raw.match(/^(.+):(\d+)$/);
|
|
30
|
+
if (hostPort) {
|
|
31
|
+
return { host: hostPort[1], port: Number(hostPort[2]) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { host: raw, port: defaultPort };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatHostForUrl(host) {
|
|
38
|
+
if (host && host.includes(':') && !host.startsWith('[')) {
|
|
39
|
+
return `[${host}]`;
|
|
40
|
+
}
|
|
41
|
+
return host;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildDaemonUrl(value, defaultPort = DEFAULT_PORT) {
|
|
45
|
+
const { host, port } = parseHostSpec(value, defaultPort);
|
|
46
|
+
return `http://${formatHostForUrl(host)}:${port}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildDaemonWsUrl(value, defaultPort = DEFAULT_PORT) {
|
|
50
|
+
const { host, port } = parseHostSpec(value, defaultPort);
|
|
51
|
+
return `ws://${formatHostForUrl(host)}:${port}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
DEFAULT_PORT,
|
|
56
|
+
parseHostSpec,
|
|
57
|
+
formatHostForUrl,
|
|
58
|
+
buildDaemonUrl,
|
|
59
|
+
buildDaemonWsUrl
|
|
60
|
+
};
|
package/mcp-server/index.mjs
CHANGED
|
@@ -34,9 +34,30 @@ function getAuthToken() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function getDaemonUrl() {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// TELEPTY_HOST accepts: `host`, `host:port`, or `http://host:port`. Embedded
|
|
38
|
+
// port from TELEPTY_HOST is used unless TELEPTY_PORT is set explicitly.
|
|
39
|
+
const explicitPort = process.env.TELEPTY_PORT ? Number(process.env.TELEPTY_PORT) : null;
|
|
40
|
+
const raw = process.env.TELEPTY_HOST || "127.0.0.1";
|
|
41
|
+
let stripped = String(raw).trim().replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
|
|
42
|
+
let host = stripped;
|
|
43
|
+
let embeddedPort = null;
|
|
44
|
+
const ipv6Bracketed = stripped.match(/^\[([^\]]+)\](?::(\d+))?$/);
|
|
45
|
+
if (ipv6Bracketed) {
|
|
46
|
+
host = ipv6Bracketed[1];
|
|
47
|
+
if (ipv6Bracketed[2]) embeddedPort = Number(ipv6Bracketed[2]);
|
|
48
|
+
} else {
|
|
49
|
+
const colonCount = (stripped.match(/:/g) || []).length;
|
|
50
|
+
if (colonCount === 1) {
|
|
51
|
+
const m = stripped.match(/^(.+):(\d+)$/);
|
|
52
|
+
if (m) {
|
|
53
|
+
host = m[1];
|
|
54
|
+
embeddedPort = Number(m[2]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const port = explicitPort != null ? explicitPort : (embeddedPort != null ? embeddedPort : 3848);
|
|
59
|
+
const hostForUrl = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
60
|
+
return `http://${hostForUrl}:${port}`;
|
|
40
61
|
}
|
|
41
62
|
|
|
42
63
|
async function daemonFetch(endpoint, options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aigentry-telepty": "install.js",
|
|
@@ -8,10 +8,35 @@
|
|
|
8
8
|
"telepty-install": "install.js",
|
|
9
9
|
"telepty-mcp": "mcp-server/index.mjs"
|
|
10
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"cli.js",
|
|
13
|
+
"daemon.js",
|
|
14
|
+
"install.js",
|
|
15
|
+
"auth.js",
|
|
16
|
+
"cross-machine.js",
|
|
17
|
+
"daemon-control.js",
|
|
18
|
+
"entitlement.js",
|
|
19
|
+
"host-spec.js",
|
|
20
|
+
"interactive-terminal.js",
|
|
21
|
+
"runtime-info.js",
|
|
22
|
+
"session-routing.js",
|
|
23
|
+
"session-state.js",
|
|
24
|
+
"shared-context.js",
|
|
25
|
+
"skill-installer.js",
|
|
26
|
+
"terminal-backend.js",
|
|
27
|
+
"tui.js",
|
|
28
|
+
"install.sh",
|
|
29
|
+
"install.ps1",
|
|
30
|
+
"mcp-server/",
|
|
31
|
+
"src/",
|
|
32
|
+
"skills/",
|
|
33
|
+
"CHANGELOG.md"
|
|
34
|
+
],
|
|
11
35
|
"scripts": {
|
|
12
|
-
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js",
|
|
13
|
-
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js",
|
|
14
|
-
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js"
|
|
36
|
+
"test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
37
|
+
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js",
|
|
38
|
+
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
39
|
+
"regen-fixtures": "node scripts/regen-snippet-fixtures.js"
|
|
15
40
|
},
|
|
16
41
|
"keywords": [
|
|
17
42
|
"pty",
|
|
@@ -48,7 +73,7 @@
|
|
|
48
73
|
"node-pty": "^1.2.0-beta.11",
|
|
49
74
|
"prompts": "^2.4.2",
|
|
50
75
|
"update-notifier": "^5.1.0",
|
|
51
|
-
"uuid": "^
|
|
76
|
+
"uuid": "^9.0.0",
|
|
52
77
|
"ws": "^8.19.0",
|
|
53
78
|
"zod": "^3.24.0"
|
|
54
79
|
}
|
package/session-state.js
CHANGED
|
@@ -203,6 +203,9 @@ class SessionStateMachine {
|
|
|
203
203
|
|
|
204
204
|
// Strip ANSI and split into lines for pattern analysis
|
|
205
205
|
const cleaned = stripAnsi(data);
|
|
206
|
+
if (cleaned.trim().length === 0) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
206
209
|
const lines = cleaned.split(/\r?\n/).filter(l => l.trim().length > 0);
|
|
207
210
|
|
|
208
211
|
for (const line of lines) {
|
|
@@ -269,6 +272,16 @@ class SessionStateMachine {
|
|
|
269
272
|
this._transition(STATES.RESTARTING, 1.0, { trigger: 'lifecycle' });
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
markIdle(confidence = 1.0, detail = {}) {
|
|
276
|
+
if (this._state === STATES.DEAD || this._state === STATES.RESTARTING) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this._transition(STATES.IDLE, confidence, {
|
|
280
|
+
trigger: 'manual_idle',
|
|
281
|
+
...detail,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
272
285
|
// --- Internal ---
|
|
273
286
|
|
|
274
287
|
_transition(newState, confidence, detail) {
|
|
@@ -538,6 +551,16 @@ class SessionStateManager {
|
|
|
538
551
|
if (sm) sm.markRestarting();
|
|
539
552
|
}
|
|
540
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Mark a session as idle from a semantic daemon event.
|
|
556
|
+
*/
|
|
557
|
+
markIdle(sessionId, confidence = 1.0, detail = {}) {
|
|
558
|
+
const sm = this._machines.get(sessionId);
|
|
559
|
+
if (!sm) return false;
|
|
560
|
+
sm.markIdle(confidence, detail);
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
|
|
541
564
|
/**
|
|
542
565
|
* Unregister and cleanup a session's state machine.
|
|
543
566
|
*/
|